一、为什么单个worker会变成“饭桶”

uWSGI是一个应用服务器,很多Python项目都需要靠它跑起来。我们常把uWSGI启动的worker进程比作餐厅里的服务员。一个worker同时只能服务一个客人(一个请求),当客人多了,就在门口排队。

正常情况下,上菜还算顺畅。可是有些请求特别“调皮”:比如一个接口把整个数据库表读到内存里做排序,另一个接口陷入死循环疯狂计算,还有一个用户上传了很大的文件。这个时候,服务于这些请求的worker就被“黏住”了,内存开始疯涨,CPU像踩了油门冲出去。如果这台服务器上还跑着别的应用,别的应用就会变得卡顿,甚至整个机器都无响应。

这时候我们不可能每次都靠人肉登录服务器去kill出问题的进程,所以要用uWSGI自带的资源限制功能,给它设定“吃饭上限”。超过上限,要么主动暂停,要么让worker“重生”,从而保证其他进程能正常抢到资源。

二、uWSGI为我们准备了哪些限制手段

2.1 内存阈值:超了就让worker“换血”

内存问题是常见的,uWSGI有两个关键参数:reload-on-rss 和 reload-on-as。rss是常驻内存,类似一个人坐在座位上不动的记忆;as是虚拟内存,包括程序申请的虚拟空间。两个都能设置阈值。

当worker对应的内存值超过阈值时,uWSGI会把这个worker优雅地杀掉,再重新启动一个新的worker。因为新进程会继承旧进程的配置,但内存从零开始,所以能够有效清理累积的内存垃圾。

你还可以设置 max-requests,让每个worker处理一定数量的请求后主动“退休”。这样做的好处是,即使你的代码没有明显泄漏,也能定时重启,避免不可预见的缓慢膨胀。

举个例子:下面配置文件里,一个worker处理满2000个请求后重启,常驻内存超过300MB时重启,虚拟内存超过512MB时重启。这样任何一个worker都不会成为“内存黑洞”。

技术栈:uWSGI(Python Flask项目配置文件)

; Python Flask项目使用的uWSGI配置示例
[uwsgi]
; 指定WSGI入口,myapp.py中有一个app实例
module = myapp:app
; 同时开启4个worker进程
processes = 4
; 每个worker最多处理2000个请求
max-requests = 2000
; 每个worker常驻内存超过300MB就重启
reload-on-rss = 300
; 每个worker虚拟内存超过512MB就重启
reload-on-as = 512
; 宽限时间:给被重启的worker留出20秒处理收尾工作
worker-reload-mercy = 20

运行它很简单,在服务器上执行下面的命令:

# 使用指定的ini配置文件启动uWSGI
uwsgi --ini myapp_uwsgi.ini

这样配置之后,一个调皮请求最多只能吃掉300MB常驻内存,到点就会被清理掉。

2.2 CPU时间:限制不死循环的“疯牛”

内存之外,CPU更让人头疼。有的接口逻辑写得不好,数据量稍大就进入死循环,CPU核心一直打满。uWSGI提供了 rlimit 配置,直接对操作系统底层资源提出限制。

rlimit = cpu 60 表示每个worker累计占用CPU时间上限是60秒。所谓“累计”,是从worker启动开始计算的所有CPU时间,不区分是哪个请求。如果达到60秒,操作系统会发送信号处理这个进程。

下面继续在上面的配置文件里添加:

; 限制每个worker的累计CPU时间为60秒
rlimit = cpu 60
; 限制文件描述符数量,防止进程泄漏大量文件句柄
rlimit = nofile 4096
; 限制栈空间为1MB,避免栈溢出
rlimit = stack 1024

需要提醒大家:rlimit 不是“CPU使用率不能超过80%”,它是“累计秒数”。如果你希望控制的是使用率百分比,那属于完全不同的技术方向,通常要借助cgroup的 cpu.max 或系统监控工具。uWSGI本身没有直接限制CPU百分比的选项,这一点提前说明能帮你少踩坑。

2.3 并发及队列长度:不要让大家挤在家门口

我们可以用 threads 让一个worker能同时处理多个请求,但是并发数并不是越大越好。每个线程都要占用栈空间和调度资源。如果你把 processes 设为8,threads 设为64,并发一下子堆到512,可能还没等请求来,内存就先用掉一大截。

参数 listen 控制操作系统的socket监听队列长度。假设 listen = 1024,意思是在连接还没被worker处理之前,内核最多帮我们排队1024个连接,超过这个数量就快速失败,不再接受新连接。这样系统不至于被高并发冲垮。

还有 harakiri 和 timeout,它们是请求如果处理时间太长,就主动放弃。harakiri 是uWSGI内置的杀手锏,一个请求超过时长立刻杀掉当前worker,然后重建。timeout 一般用于Socket读写超时。两者结合,能极大缩短“坏请求”占住worker的时间。

; 每个worker内部开4个线程,增加并发处理能力
threads = 4
; 内核监听队列最多1024个
listen = 1024
; 单个请求最多处理30秒,超过就被强制杀掉
harakiri = 30
; 网络读写超时设置为30秒
timeout = 30

这样配置后,任何一个请求最多只会“折腾”30秒,不管它数据量多大,都不会无限耗下去。

2.4 请求体积:不要把整头牛端上来

大请求也是资源杀手。比如上传一个几GB的文件,如果uWSGI直接开一块大内存去接收,很快内存就见底。uWSGI有 post-buffering 功能,当POST请求体大于设定阈值时,先把请求体写入临时文件,再交给应用。这样,即使上传内容很大,也不会把内存一次性占满。

buffer-size 是每个请求的缓冲区大小。它和协议头的大小有关,不要调得太小导致正常请求头都不能完整接收。如果你不确定,就用默认的4KB或者调成8KB。

; 每个worker建立连接时使用的缓冲区大小,设置成8KB
buffer-size = 8192
; 当POST请求体超过64KB时,uWSGI先写入临时文件,避免内存暴涨
post-buffering = 65536

三、完整示例:把限制全部装进一个配置文件

上面四个小节分别讲了不同维度的限制。下面把它们拼在一起,形成一个可用的完整配置。技术栈统一为uWSGI(Python Flask项目)。

[uwsgi]
; ========== 应用入口 ==========
; 这里假设你的Flask项目里有一个myapp.py文件,内含app对象
module = myapp:app

; ========== 进程和线程 ==========
; 4个worker进程
processes = 4
; 每个worker 4个线程,总并发能力为16
threads = 4

; ========== 内存防护 ==========
; 处理完2000个请求后自动重启
max-requests = 2000
; 常驻内存超过300MB重启
reload-on-rss = 300
; 虚拟内存超过512MB重启
reload-on-as = 512

; ========== CPU及资源描述符 ==========
; 每个worker累计CPU时间最多60秒
rlimit = cpu 60
; 最多打开4096个文件描述符
rlimit = nofile 4096
; 栈空间限制为1MB
rlimit = stack 1024

; ========== 并发及超时 ==========
; 内核监听连接队列为1024
listen = 1024
; 单个请求最多处理30秒
harakiri = 30
; socket读写超时30秒
timeout = 30

; ========== 请求体积 ==========
; 缓冲区8KB
buffer-size = 8192
; POST体超过64KB进入临时文件
post-buffering = 65536

启动命令:

# 在项目目录下运行以下命令
uwsgi --ini app_uwsgi.ini

如果希望后台运行,可以加 -d 参数。如果你用的是systemd管理,也可以把这个文件放到 /etc/uwsgi/sites/ 下。

四、应用场景分析

什么样的项目最需要这套配置?第一,跑在共享服务器上的中小型项目。你租一台2C4G云主机,上面同时放着Nginx、MySQL、Redis、uWSGI,任何一个进程突然吃掉2G内存,都会让数据库可能直接被杀掉。第二,对外提供API的公共平台,你无法预料调用方会发什么怪异请求。第三,已经发现有内存缓慢增长的情况,但业务比较急,先用自动重启来续命。第四,内置了文件上传、下载功能的Web服务,如校园网盘、内部资料库。第五,多人同时使用的在线课堂或直播聊天室,并发波动大,容易被打爆。

在这些场景下,给uWSGI加上资源限制,就像给车位画了线,谁也不能越线停车。

五、资源限制配置的优缺点

5.1 优点

uWSGI资源限制的很大优点是不需要修改业务代码。只要改几行配置,重启一下进程,立刻就能产生效果。其次是可选范围广,内存、CPU、文件句柄、并发数、超时时间都能管。再者,自动重建可以让服务在遇到异常后快速恢复,避免了需要人工介入的尴尬。

5.2 缺点

它也有副作用。比如自动重启会中断正在处理的请求。当worker通过 reload-on-rss 被强制重载时,如果这个worker还在处理一个长任务,那么这个请求会被打断,用户可能会看到“500错误”或“连接断开”。另外,这些限制都是进程级的“粗糙”保护,不能精确控制整个uWSGI进程组的总内存之和。还有 rlimit 在部分容器化环境中可能不生效,因为容器本身往往会强制不同的资源限制策略。

六、注意事项

第一,阈值不能拍脑袋定。建议先让服务正常运行一段时间,观察监控,看看正常波动是多少。比如正常worker大概占用150MB,那300MB就有一定的余量;如果正常就250MB,那300MB就太容易误杀。

第二,reload-on-rss 和 reload-on-as 不要同时设得太低。虚拟内存往往比驻留内存大很多,你把as设置成512,可能代码刚启动就触发重启。最好先用 ps 或 top 查看某个worker的RSS和VSZ,再定数值。

第三,要留意worker的重启风暴。当多个worker同时达到上限,uWSGI会同时重建多个进程,瞬时压力可能升高。可以考虑加上 worker-reload-mercy 或者调整阈值错开重启时间。事实上uWSGI提供了 reload-mercy 和 worker-reload-mercy 等参数,但不同版本名称有差异,务必以官方文档为准。

第四,rlimit 并不适用于所有平台。请确认你的服务器是Linux系统,并且在普通用户下能正常使用。如果遇到 Permission denied,说明系统不允许该用户修改此限制,需要用root或者放到Docker的启动参数里处理。

第五,要分清uWSGI和系统级cgroups的关系。uWSGI的 rlimit 只能限制自身启动的worker,如果worker又去fork子进程,子进程可能不受同样限制。这时你需要借助cgroup或Systemd的 MemoryMax、CPUQuota 对进程树整体限制。

第六,不要把所有希望都寄在uWSGI上。最理想的做法是在Nginx层先限制请求头大小和上传大小,再通过uWSGI限制进程资源,最后用监控报警发现重启次数异常。

七、文章总结

uWSGI资源限制配置是一项实用且上手门槛低的操作。我们通过内存阈值、重启策略、CPU时间限制、并发队列和请求缓冲,把单个worker控制在一个安全范围之内,进而保证整个服务器的稳定。但它不是银弹,它会打断业务,无法做到精确到毫秒的资源分配。真正的稳定运行需要结合代码质量、系统监控和更强力的隔离机制。先把这一层做起来,至少可以在意外发生时,让服务器不至于因为某个“暴走”进程而全线崩溃。这就像给家里的电闸装上了保险丝,电流大了先跳闸,而不是把整个电线烧掉。用对uWSGI的资源限制,就是给你的服务加上一道可靠的安全网。