因此,为了让我们自己和我们的系统做好准备来处理更多的客户,我们知道现在是时候承担一个扩展我们系统的项目了。
在这篇文章中,我们将向您展示如何优化流程和服务器以处理10 倍规模,我们在解决问题时做错了什么,以及如何找到适合我们的方法。
我们先来看一下早期的框架。
Python 如何在 Web 上运行
Python 应用程序使用一种叫做 Web 服务器的东西来为 Web 提供服务。
Web 服务器无法自动与您的应用集成。为此,我们为所有 Python Web 应用构建了一个标准流程,您需要从项目中公开 WSGI 应用程序。
此 WSGI 应用程序可以提供给独立于我们的 Python 项目构建的任何服务器。因此,我们可以使用任何 WSGI 服务器来运行我们的 WSGI 应用程序。
使用 Gunicorn 进行扩展 – 为什么它不起作用
这是我们使用 gunicorn 运行 django 应用程序所使用的配置:
此配置会启动一个 gunicorn 同步工作器,它会同步处理每个请求,即一次处理一个请求。因此,如果您同时发出两个请求,则一个请求必须等待另一个请求完成。
对于用户来说这不是一个很好的体验。
然后,我们启动了 30 个 Kubernetes pod(通过自动扩展增加到 60 个),每个 pod 都具有针对 Django 应用的此配置。这样,通过并行运行 30 个 gunicorn 同步工作器来处理传入的请求,我们可以并行处理 30 个请求。
但是,如果每个 pod 可以同步处理所有请求,那么如果一个请求需要花费大量时间来执行(可能是因为查询或另一个 API 调用花费的时间太长,或者与 rabbitmq 的连接被阻止),那么到达该 pod 的所有其他请求都必须等到该请求执行完毕。
上述配置启动了 2 个异步工作进程和 500 个 greenlet。
现在,每个 pod 可以使用所谓的“猴子补丁”同时处理 500*2=1000 个连接。然而,Django 对“猴子补丁”的回应是负面的,因为它会泄漏数据库连接。
使用此配置启并发请求听起来很容易,但实际上并非如此。
由于需要同时处理多个请求,因此我们还需要很多数据库连接。
然而,由于上限限制,我们的数据库连接耗尽,现在每个请求都失败了。
我们还看到如下错误:
这些错误意味着数据库没有更多的连接可以提供给您。
我们怎样解决这个问题?
使用池限制连接
接下来,我们引入了 pgbouncer(Postgres 连接 俄罗斯电子邮件清单 池)进行全局池化,因为 Django 本身并不支持池化。
(仅供参考 – 池化可减少 PostgreSQL 资源消耗,并支持在线重启/升级,而无需断开客户端连接。)
然后我们再次对服务器进行了压力测试。以下是我们注意到的情况:
连接问题已解决,没有错误
RPS 永远不会超过 125 RPS
当并发连接数仅有 100 个时,延迟就会非常高,第 95 个百分位数会超过 50000 毫秒。
这种延迟并不理想。连接池应该可以消除为每个请求建立新连接的麻烦,从而减少延迟;但这里的情况并非如此。
我们在使用 gunicorn 异步工作器时还遇到了其他问题。主要问题是连接泄漏。
数据库连接永远不会在随机 从一首歌曲的名字来看 时间关闭,这是因为 gunicorn async 工作使用 greenlet,如果你在代码中手动生成另一个 greenlet 或者调用 celery 任务,就会导致问题。
它导致的一个问题是它永远不会调用 Django 信号 request_finished,因此数据库连接永远不会关闭。
查看此处了解其工作原理。
经过这次实验,我们尝试了所有可能的配置组合,我们意识到根据我们的用例扩展 gunicorn 根本行不通。
是时候继续前进了。
寻找新服务器
经过这些实验后,我们知道我们正在寻找的新服务器应该:
处理大约 200 个并发连接
轻松处理超过 1000 RPS。(这是当前负载的 10 倍)
不要使用 monkey patching,以免遇到任何与 celery 相关的问题
我们尝试了两个服务器——Bjoern 和 CherryPy。
Bjoern 是一个非常轻量 b2c 传真版 级的单线程 WSGI 服务器,可与 libev 事件循环协同工作。
因此,您可以像扩展 nodejs 服务器一样扩展它,启动多个进程来利用所有 CPU 资源。
从纸面上看,Bjoern 很棒,根据我们读到的一些评论,Bjoern 也很棒。但是,当我们通过 Bjoern 启动我们的应用进行压力测试时,结果并不理想,因为延迟影响很大。
看到这些数字后我们并不相信,并认为这对我们来说太慢了。
CherryPy 简介
CherryPy 是一个纯用 Python 编写的多线程线程池服务器。创建者声称它是目前性能最高的 Python 服务器之一。
尽管是用纯 Python 编写的,但它可以处理大量并发连接,并且具有高 RPS 和低延迟。
我们编写了一小段代码来使用 cherrypy 启动我们的 Django WSGI 应用程序并开始压力测试:
启动服务器脚本
我们在线程池中运行了 100 个线程的服务器,并根据需要将其增加到 150 个线程,使用 1000 个 rqs(请求队列大小)并启动了 10 个 pod。我们还为 Django 服务器删除了 pgbouncer。
压力测试结果如下:
它很容易维持 200 个并发连接
能够轻松实现 1200+ RPS
第 95 百分位延迟降低至 600 毫秒以内
平均延迟小于 200ms
终于!一切都如我们所愿了。
芹菜和它有什么关系
电池包括为 Django 构建的用于后台处理的分布式任务队列。
随着我们开始扩大规模,这些电池开始困扰我们。我们首先从 Redis 转移到 RabbitMq 作为代理。
一切运行顺利,直到我们开始收到比平时更多的流量。在出现此错误后,所有 celery 工作单元都会死机! worker 上重复,直到每个 worker 都死掉,任务开始在队列中堆积,这导致 rabbitmq连接被阻塞。这会导致Django 服务器经常卡住。
因此,目标变成了在芹菜荚之间均匀分配任务,以便只有一个工作者不会预取所有任务。
为了解决这个错误,我们放弃了 gevent,转而使用 celery 并转向本机线程。
我们的目标是优化我们的系统和流程,以管理10 倍规模。通过尝试这些实验并不断迭代,我们实现了目标!
那 100x 呢?(未来计划)
虽然我们已经优化了系统,但很快我们就必须在它的基础上进行构建并再次扩展它。