Clubhouse如何使用Python每天处理十亿个请求?


2021 年初,Clubhouse 开始经历爆发式增长时期。在两个月的时间里,我们从每分钟不到 1 万个后端请求增加到超过 100 万个后端请求,我们必须迅速适应以在现有堆栈上每天处理数十亿个请求。而且我们只有两名全职后端工程师。这是一个关于我们扩展服务并将 Python 工作负载运行效率提高 3 倍的当下热潮的故事。
 
现有技术堆栈资源利用率不高
我们的核心 Clubhouse 网络堆栈相当初级:使用 Gunicorn 和 NGINX 的 Python/Django 。当我们开始看到访问量增长时,我们没有太多时间来调整效率,我们一直在添加更多的网络节点,我们一直都承认我们的 Django 单体只能以每个实例大约 30-35% 的 CPU 自动扩展,这肯定是浪费的
添加了更多的网络节点——并且根据需要越来越多。有了这么多实例,我们的负载均衡器开始间歇性地超时,并在蓝/绿部署期间翻转流量时使部署“卡住”。我们尝试与我们的云提供商一起追查超时,但他们无法找出发生这种情况的根本原因。
 
一种简单的解决方案是单个节点运行更大的实例!
在切换到非常大的 96 个 vCPU 实例类型(在每个节点上运行 144 个 Gunicorn 工作程序)后,我们震惊地发现延迟开始膨胀,CPU 仅占 25%。在那个令人尴尬的低阈值下,我们的 p50 延迟猛增,节点变得不稳定。
我们被难住了。我们花了几个小时寻找一些系统级的限制。(当然,这是我们默默地达到的一些随机内核限制或资源......)相反,我们发现更令人震惊:我们在这些巨大(且昂贵)机器上的 144 个 Gunicorn 进程中只有 29 个收到任何请求!其他 115 个进程处于闲置状态。
事实证明,这是雷鸣般的羊群问题的另一个实例:当大量进程试图在同一个套接字上等待以处理进入的下一个请求时,就会发生这种情况。你的所有进程都在努力处理下一个请求,在这个过程中浪费了大量资源。事实证明,这是Gunicorn 的一个有据可查的限制。
 
尝试#1:uWSGI
我们旅程中的第一个实验是将我们的 Python 应用服务器从 Gunicorn 切换到 uWSGI,它内置了针对我们确切问题的精心解决方案。(关于它文档值得一读!)解决方案是一个名为“--thunder-lock”的标志,它对内核做了一件非常奇特的事情,将负载均匀地分布在我们的所有 144 个进程中。
我们迅速部署了 uWSGI 来取代 Gunicorn,令我们高兴的是,平均延迟降低了 2 倍!负载现在均匀分布在所有 144 个进程中。一切看起来都很好。
还有一个主要问题。
当我们从 Gunicorn 时代开始将流量增加到超过神秘的 25% CPU 阈值时,我们开始遇到一个更大的问题:uWSGI 套接字会在某些机器上以不可预测的时间间隔锁定。当 uWSGI 被锁定时,网络服务器会在几秒钟内拒绝所有请求——在此期间,我们会看到延迟和 500 秒的大幅峰值。
这个问题很神秘。我们在 uWSGI 文档和 StackOverflow 帖子旁边匹配了神秘的日志行——甚至翻译了德语和俄语的帖子——但找不到确凿的证据。
这加剧了另一个问题:uWSGI太混乱了。毫无疑问,这是一款了不起的软件,但它附带了数十个您可以调整的选项。如此多的选项意味着有很多杠杆可以扭转,但缺乏明确的文档意味着我们经常只能猜测给定标志的真实意图。
最终,我们无法可靠地重现或缓解问题。
所以uWSGI不适合我们。我们又回到了第一个问题:我们如何才能 100% 地利用我们的应用服务器上的 CPU?
 
尝试#2:NGINX
使用NGINX作为负载平衡功能会有严重限制。没有选项可以限制每个套接字的并发性或阻止挂起的套接字接收新请求(没有请求排队机制)。这让我们产生了一个问题:我们为什么要使用 NGINX作为负载平衡器?许多真正有用的负载平衡功能都由“NGINX Plus”控制,但我们不确定这些是否会对我们有所帮助。
就在那时,我们有了一个疯狂的想法。
我们知道 Gunicorn 本身表现得足够好,但它在其工作人员之间的负载平衡请求方面非常糟糕。(这就是我们首先看到 115 个空闲工作进程的原因。)
如果我们不是在每台服务器上运行 10 个 Gunicorn 服务器,而是全力以赴运行完整的 144 个独立的 Gunicorn 主进程,每个进程只有一个Web 工作者会怎样?如果我们能找到一种方法在这些工作人员之间实际进行负载平衡,那么肯定会导致完美平衡、完美行为的超大网络节点。
 
尝试 #3:HAProxy 来救援!
幸运的是,HAProxy 为我们的用例做了 NGINX 所能做的一切,甚至更多。这将使我们能够:

  1. 跨 144 个后端(Gunicorn 套接字)均匀分布请求
  2. 在每个后端的基础上限制并发——这样我们只向每个 Gunicorn 套接字发送一个请求,而不会给它带来压力
  3. 将请求放在一个地方——HAProxy 前端——而不是每个 Gunicorn 进程的单独积压
  4. 在应用程序服务器和 Gunicorn 套接字的基础上监控并发性、错误率和延迟。

我们使用 supervisord 来启动每个 Gunicorn 套接字,并简单地列出 HAProxy 后端中的 144 个 Gunicorn 套接字中的每一个。
我们验证了假设并挤压测试了单个 96 核实例直到 CPU 饱和。
实践中,我们的工作负载意味着我们开始在 80% 左右的 CPU 上体验更高的延迟,因为由于负载不均匀导致的暂时峰值使机器饱和。
 
总结
我们从中可以得到什么主要收获?
  1. uwsgi 刚开始对你有用——也许试一试!这是一个了不起的软件。
  2. 如果您在应用程序前使用 NGINX 作为 sidecar边车 代理/负载平衡器,请考虑调整您的配置以使用 HAProxy。因此,您将获得惊人的监控和排队功能。
  3. Python 为您的应用程序运行 N 个单独进程的模型并不像人们认为的那样不合理!您可以通过这种方式获得合理的结果,只需稍加挖掘。