uWSGI Python 高负载配置

13

我们有一个具有32个核的大型EC2实例,目前正在运行Nginx、Tornado和Redis,平均每秒提供5000个请求。一切似乎都很正常,但CPU负载已经达到了70%,而且我们需要支持更多的请求。其中一个想法是用uWSGI替换Tornado,因为我们实际上没有使用Tornado的异步特性。

我们的应用程序由一个函数组成,它接收一个JSON(约为4KB),执行一些阻塞但非常快速的操作(Redis),然后返回JSON。

  • 代理HTTP请求到Tornado实例之一(Nginx)
  • 解析HTTP请求(Tornado)
  • 读取POST正文字符串(字符串化的JSON)并将其转换为Python字典(Tornado)
  • 从位于同一机器上的Redis(py-redis with hiredis)中获取数据(阻塞)
  • 处理数据(Python3.4)
  • 在同一台机器上更新Redis(py-redis with hiredis)
  • 为响应准备字符串化的JSON(Python3.4)
  • 将响应发送给代理(Tornado)
  • 将响应发送给客户端(Nginx)

我们认为速度的提升将来自uwsgi协议,我们可以在单独的服务器上安装Nginx,并将所有请求代理到使用uwsgi协议的uWSGI。但是,在尝试所有可能的配置并更改操作系统参数后,我们仍然无法在当前负载下使其正常工作。大多数情况下,nginx日志包含499和502错误。在某些配置中,它停止接收新请求,就像达到了某些操作系统限制。

所以,正如我所说,我们有32个核,60GB的可用内存和非常快的网络。我们没有做重负载的操作,只有非常快速的阻塞操作。在这种情况下,最好的策略是什么?进程、线程、异步?应设置哪些操作系统参数?

当前的配置为:

[uwsgi]
master = 2
processes = 100
socket = /tmp/uwsgi.sock
wsgi-file = app.py
daemonize = /dev/null
pidfile = /tmp/uwsgi.pid
listen = 64000
stats = /tmp/stats.socket
cpu-affinity = 1
max-fd = 20000
memory-report = 1
gevent = 1000
thunder-lock = 1
threads = 100
post-buffering = 1

Nginx配置:

user www-data;
worker_processes 10;
pid /run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
    use epoll;
}

操作系统配置:

sysctl net.core.somaxconn
net.core.somaxconn = 64000

我知道限制太高了,开始尝试每个可能的值。

更新:

我最终采用了以下配置:

[uwsgi]
chdir = %d
master = 1
processes = %k
socket = /tmp/%c.sock
wsgi-file = app.py
lazy-apps = 1
touch-chain-reload = %dreload
virtualenv = %d.env
daemonize = /dev/null
pidfile = /tmp/%c.pid
listen = 40000
stats = /tmp/stats-%c.socket
cpu-affinity = 1
max-fd = 200000
memory-report = 1
post-buffering = 1
threads = 2
1个回答

12

我认为你的请求处理大致可以分为以下几个步骤:

  • HTTP解析、请求路由、JSON解析
  • 执行一些Python代码,生成一个Redis请求
  • (阻塞式)Redis请求
  • 执行一些Python代码,处理Redis响应
  • JSON序列化、HTTP响应序列化

你可以在一个接近空闲状态的系统上对处理时间进行基准测试。我猜这个往返时间将缩短到2或3毫秒。在70%的CPU负载下,这个时间会增加到4或5毫秒(不计算在nginx请求队列中花费的时间,只计算在uWSGI worker中的处理时间)。

在5k req/s的情况下,你的平均进程请求可能会达到20到25个。这与你的虚拟机相当匹配。

下一步是平衡CPU核心。如果你有32个核心,分配1000个工作进程就没有意义了。你可能会因为上下文切换开销而使系统崩溃。一个好的平衡将使得总工作量(包括nginx+uWSGI+redis)与可用CPU核心数量相同,或者略多于可用CPU核心数量,以覆盖阻塞I/O(即文件系统,但主要是针对与其他主机(如DBMS)交互的网络请求)。如果阻塞I/O成为方程中的一个很大部分,考虑重写为异步代码并集成一个异步堆栈。

首先观察:你为nginx分配了10个worker。然而,nginx在请求上花费的CPU时间远远低于uWSGI花费的时间。我会从系统中分配大约10%的时间给nginx(3或4个worker进程)开始。

剩下的工作将需要在uWSGI和redis之间分配。我不知道你在redis中的索引大小或者你的Python代码的复杂性,但我的第一次尝试将是将75%/25%的比例分配给uWSGI和redis。这将使得redis占用约6个worker,uWSGI占用约20个worker+一个master。

关于uwsgi配置中的线程选项:线程切换比进程切换轻量级,但如果你的Python代码中有很大一部分是CPU密集型的,由于GIL的存在,它将无法工作。如果处理时间的一个重要部分被I/O阻塞所占用,则线程选项可能更加有趣。您可以禁用线程,或尝试使用workers=10,threads=2作为初始尝试。

非常感谢,我已经成功将其运行到每秒9K个请求,使用16个进程和20个线程。CPU现在大约为45%。我可以尝试减少线程数量,但是这台服务器正在生产中,我不能太过于玩弄它。此外,我已根据此文章调整了操作系统参数。 Redis的问题在于它仅使用一个核心,但我可以减少nginx上的工作程序。如果我理解正确,每个工作程序都接近使用100%的核心时,工作程序的正确数量就是正确的。 - offline15
很高兴听到你做出了不错的改进(5000 req/s @ 70% CPU --> 9000 req/s % 45%)!我认为uWSGI的_threads_设置仍然太高了。现在你有16个工作进程*每个20个线程。每个工作进程的20个同时线程仅在Python代码95%受I/O限制时才能最大化CPU。我建议启用更多的工作进程(如24个)并将_threads_设置减少到2或4个。运行htop以查看如何平衡负载在CPU上。 - Freek Wiekmeijer
有没有办法在运行中的进程中知道进程花费了多少时间在I/O操作上,以及在计算上CPU又花费了多少时间? - offline15
1
top 命令给出以下指示:%Cpu(s): 7,8 us, 3,3 sy, 0,0 ni, 89,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st(用户/系统/空闲/等待/硬件中断/软件中断/被窃)。请参阅 http://linuxaria.com/howto/understanding-the-top-command-on-linux 了解详细说明。在 top 中按 1 键可查看每个单独 CPU 核心的统计信息。 - Freek Wiekmeijer
@offline15,你能否更新一下帖子并分享你的成功配置吗?我也需要 :) - woozly
1
已更新到我服务器上的最新配置。加上代码更改,我能够运行30k/s。 - offline15

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接