无状态服务

2023年 1月 4日 46.5k 0

1.问题描述

背景:一个 Django 开发的 SaaS 应用,对外提供文档服务功能。其中,搜索功能通过 Django Haystack 实现。问题:搜索功能有时可用,有时不可用。多次测试,发现可用和不可用会交替出现,出现概率各占约 50%。补充一下搜索功能实现的细节:Django Haystack 在提供搜索功能之前,需要执行如下命令:

1
python manage.py update_index

生成索引文件:

1
2
3
_MAIN_1.toc
MAIN_l4dclez7n5lsj047.seg
MAIN_WRITELOCK

只有存在有效索引文件的前提下,才能提供搜索服务。因此,将更新索引的命令,通过 subprocess 在 Python 中直接执行,代码如下:

1
2
3
cmd = "echo 'y'| python manage.py update_index"
subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, err = p.communicate()

2. 问题定位

发现服务的可用状态和不可用状态交替出现时,首先想到 Nginx 的默认负载策略就是轮询,交替将请求分配给不同负载。上面是 SaaS 部署的示意图。开发者将 SaaS,也就是图中 app server, 通过 Docker 进行部署。为了实现 app server 高可用,在部署时,PaaS 平台会自动实例化 SaaS 两次,保证两个 Docker 实例同时提供服务。在 app server 的前端,通过 Nginx 做负载均衡,分发用户请求,采用的是正是轮询策略。应该来说,定位十分准确了,接下来就是如何保证两个实例都进行了索引重建。但就是这个问题花费了不少时间。

2.1 Django Once 代码

在 StackOverFlow 上,找到两种方法实现 Django 在启动时,仅执行一次的功能:

  • 利用顶层的 urls.py

urls.py 模块仅会被导入并执行一次。

1
2
3
4
from django.confs.urls.defaults import *
from my_app import one_time_startup
urlpatterns = ...
one_time_startup()
  • 利用 Django App 的 apps.py

apps.py 文件可以配置一些 Django App 自定义的初始操作。

1
2
3
4
5
6
7
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
    name = 'myapp'
    verbose_name = 'My Application'
    def ready(self):
        pass # one_time_startup code here
1
2
#  myapp/__init__.py
default_app_config = 'myapp.apps.MyAppConfig'

one_time_startup() 函数中,实现索引重建的功能。按照部署逻辑分析,PaaS 平台实例化 app server 时,每个实例都会重建索引,搜索功能可用性应该为 100% ,同时在本地验证搜索功能正常。但是,实际上线之后,搜索服务并不可用。在部署日志里面,也没有重建索引的日志出现。似乎是 PaaS 平台禁止了在启动实例时,执行内置的一些命令。

2.2 Django URL 访问重建索引

在实例 app server 时,不能重建索引文件,那么直接调用接口呢?

1
2
3
4
5
# urls.py
from django.conf.urls import include, patterns
urlpatterns = patterns('document.views',
                       (r'^build/$', 'build'),
                       )
1
2
3
4
5
6
# views.py
import subprocess
def build(request):
    cmd = "echo 'y'| python manage.py update_index"
    subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    out, err = p.communicate()

于是,写了一个 url 接口,访问时,执行索引重建命令。在 SaaS 发布上线之后,连续访问两次,分别在两个实例更新索引。但这种方式,操作难度较大,不能保证两次访问之间,没有其他人访问。由于使用容器部署,每次部署时,之前部署的本地数据都会被销毁掉。因此,每次部署后都是全新的,只需要将重建索引的代码逻辑放在首页访问的 views 中,在执行重建索引之前添加一个判断。

1
2
3
4
5
def article(request):
    if not os.path.exists('索引文件'):
        pass # 重建索引
    # 返回文档内容       
    

按照分析,到这步,如果实例中没有索引文件,就重建;如果实例中有索引文件,则跳过重建。两实例中应该都有索引文件,搜索服务的可用性应该是 100% 。然而,并没有!为了节约 CPU 和内存资源,两台服务器上有数百个 app server 实例。每次重建索引都需要接近 20 秒的时间。在这 20 秒的时间内,如果有其他请求,因为索引正在创建,检测不到索引文件,又会触发一次重建索引。测试多次,发现搜索功能的可用性依然不是 100%,出现了一个比较奇怪的现象,有时可用,有时不可用,有时还会 500。初步怀疑是,由于连续触发重建索引,消耗大量服务资源出现服务不可以 500,同时,重建索引之前清除了索引,导致正在重建索引的实例搜索服务不可用。

2.3 使用第三方服务

对于一个服务,有时可用,有时不可用,发布起来还特别繁琐易错,显然是不可接受的。于是,使用了 NFS 服务,可以看做是一个第三方的挂载目录服务。在实例化 app server 之后,将本地独享的 RES 目录挂载到 app server 实例容器中的 RES 目录上。最棒的是全部实例共享 RES 目录。配置非常简单,将 Haystack 的索引目录配置在 RES 目录中:

1
2
3
4
5
6
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'document.whoosh_cn_backend.WhooshEngine',
        'PATH': os.path.join(PROJECT_DIR, 'RES','whoosh_index'),
    },
}

然后,在通过 URL 访问,执行索引重建命令:

1
2
3
4
5
# urls.py
from django.conf.urls import include, patterns
urlpatterns = patterns('document.views',
                       (r'^build/$', 'build'),
                       )
1
2
3
4
5
6
# views.py
import subprocess
def rebuild(request):
    cmd = "echo 'y'| python manage.py update_index"
    subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    out, err = p.communicate()

2.4 小节

对于本地正常,但线上不能正常提供服务的异常定位,通常问题在部署上。了解部署流程和逻辑,对于 SaaS 开发十分重要,特别是依赖 PaaS 进行应用开发、测试、部署的人员。上面的例子,实际上是一个高可用与高一致性的矛盾。高可用意味着需要多个服务实例,而高一致性需要全部的服务实例数据一致。对于这类矛盾,解决办法就是将数据服务独立出来,上面的例子是通过挂载 NFS 服务来实现。尽量使用第三方服务,在 SaaS 中不要保持状态。

3. 无状态

无状态是高并发设计的原则之一。如果服务实例不在本地存储持久化的数据,并且多个实例对于同一请求响应的结果是完全一致的,那么称这个服务是无状态的。一个无状态的服务,很容易的对其进行水平扩展。通过新增更多的实例,可以显著的提高服务的并发性能。如上图,对于有状态的服务,每个服务内部维护一个状态。而无状态服务只是把状态从服务中独立出来,共享状态。如上图,节点 A 与节点 B 提供相同的应用服务,同时共享状态。从而实现应用服务的生命周期与状态的生命周期解耦。如果状态服务,也就是数据服务高可用,那么所有的应用服务也都是高可用的。

4. 参考

  • https://www.geekhub.cn/a/894.html

相关文章

KubeSphere 部署向量数据库 Milvus 实战指南
探索 Kubernetes 持久化存储之 Longhorn 初窥门径
征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
那些年在 Terraform 上吃到的糖和踩过的坑
无需 Kubernetes 测试 Kubernetes 网络实现
Kubernetes v1.31 中的移除和主要变更

发布评论