
午夜的生产告警里最让人心头一紧的往往不是一大片 5xx 错误码而是一行[CRITICAL] WORKER TIMEOUT (pid: 23245)。第一次见到这行日志的人多半会怀疑是自己的代码触发了段错误、内存泄漏把 worker 搞挂了。实际上真正生成这行日志的不是 worker 自己而是 gunicorn 的 master 进程——它拿一个名为WorkerTmp的心跳文件当“探针”发现某个 worker 已经连续 N 秒没有向这个文件更新“我还活着”的证据于是直接对它发送SIGKILL再重新拉起一个新的 worker。这套机制听起来很简单但真把它从源码层面拆开你会发现里面藏了不少工程上非常讲究的设计。这篇文章要拆的就是 Gunicorn 超时检测机制的完整链路WorkerTmp 是怎么创建出来的、worker 侧每次心跳都做了什么、master 侧又是如何判定超时并动手杀进程的以及这套机制在生产环境里最常见的几个调优误区和排查手段。适合已经跑过一段时间 Gunicorn、想深入理解框架内部工作的 Python 后端开发者阅读也适合正在排查“不明不白 worker 被杀”的运维同学参考。1. 先厘清一个概念master 凭什么判定 worker“超时”在进源码之前我们需要先把“超时检测”到底在检测什么这件事说透。很多人把 Gunicorn 的超时机制理解成“请求处理超时”这其实是个相当普遍的误解。1.1 假死与心跳进程活着并不代表 worker 真的活着Gunicorn 采用经典的 pre-fork 模型一个 master 进程管家多个 worker 进程干苦力。master 不直接处理请求它的核心职责是监控 worker 状态、接收信号、维护 worker 数量。这里的关键问题是master 怎么知道一个 worker“还活着”最原始的想法是看进程是否存在。但进程存在不等于 worker 正常工作。一个 worker 可能因为以下原因处于“假死”状态发起了阻塞式数据库调用比如 PostgreSQL 的连接池被占满查询一直卡着调用了一个没有设置超时的第三方 HTTP SDK上游服务长时间不响应代码里出现死循环或 GIL 被某段 C 扩展长期霸占磁盘 IO 异常文件读取永久阻塞。这些场景下ps看到的进程状态是鲜活的CPU 占用可能还是 0但 worker 实际上已经无法接受任何新请求。master 如果只检查进程存活永远发现不了问题用户请求就会长时间堆积在 accept 队列里。所以 master 需要的不是“进程是否活着”的信息而是“worker 主循环是否还在正常运转”的信息。这就必须靠心跳worker 定期主动向外发出一个“我还在处理事件”的信号master 检查这个信号是否及时更新。如果超过阈值没有更新就判定为超时也就是我们常说的假死。1.2 为什么不能靠 CPU 占用或单请求耗时判断有人会想那我直接看 worker 的 CPU 占用率不就行了空闲的 worker CPU 占用很低挂掉的 worker CPU 占用也可能是 0但一个卡在死循环里的 worker CPU 占用会非常高这又怎么区分更关键的问题是Gunicorn 本身并不在代码层追踪“每个请求处理了多久”。它把请求处理的整个生命周期都交给了 WSGI 应用Gunicorn 的 worker 循环只知道自己在 accept socket、调用应用、写回响应。如果应用内部卡住Gunicorn 的 worker 循环是感知不到的它自己也想知道自己是不是该被“拯救”。而单纯用单请求耗时来判断也不可靠因为有些合法场景就是会长时间占住 worker比如Server-Sent EventsSSE长连接推送大文件上传/下载WebSocket 长连接一次需要跑几分钟的数据导出任务。这些场景下请求处理时间远超 30 秒但 worker 本身没有“死”。所以超时检测必须和“请求处理时长”解耦只有拥有独立旁路的心跳机制才能做到底层兜底。1.3 WorkerTmp 这套方案的核心思路Gunicorn 的答案非常朴素给每个 worker 分配一个临时文件/套接字worker 循环正常运行时就隔一段时间往里面写点东西master 只负责检查这个文件的最后修改时间。谁都不需要给对方发消息也不需要在进程间共享内存一切交给文件系统的时间戳去记录。WorkerTmp这个名字里的Tmp就是临时文件的意思它本质上就是 Gunicorn 内置的一根“心跳探针”。在源码里master 持有的worker对象中每个 worker 都带有一个tmp属性类型就是WorkerTmp。master 在检查 worker 心跳时实际上就是去查这个WorkerTmp关联的文件最后是什么时候被写入的。2. WorkerTmp 到底是个什么东西看名字容易以为WorkerTmp是个复杂抽象类实际上它就是一个围绕临时文件做了薄封装的小工具。但它有几个设计细节很值得琢磨。2.1 每个 worker 独占一个心跳文件/tmp/gunicorn- .sock在 Gunicorn 的源码中WorkerTmp最关键的行为之一是在初始化时根据当前进程 PID 生成一个唯一的路径import os import time from gunicorn.util import TempFile class WorkerTmp(TempFile): def __init__(self, cfg): super().__init__() # 每个 worker 的心跳文件名和路径 self.tmp os.path.join( cfg.worker_tmp_dir, gunicorn-%s.sock % os.getpid() ) self.last_update time.time() # 触发一次写入让文件先出现在操作系统里 self.notify()这里cfg.worker_tmp_dir默认是/tmp所以生产环境里你会在/tmp下看到一堆类似gunicorn-12345.sock的文件文件名里的数字就是 worker 的 PID。注意后缀是.sock但它的本质不一定是真正的 Unix domain socket不同版本的 Gunicorn 采用过不同实现有的版本直接就是一个普通文件有的版本用的是 AF_UNIX 套接字。但从 master 检测的角度看核心都是同一个文件路径master 只需检查这个路径对应文件的元数据即可。2.2 为什么文件路径要比对 PID 而不是随机值用 PID 作为文件名的一部分最大的好处是便于人工排查。当你在生产环境看到某行 worker 超时日志时可以直接用日志里的 PID 去/tmp下找到对应文件然后通过stat查看它的 mtime就能判断这个 worker 当时最后活动的时间点。当然PID 复用会带来潜在冲突如果上一个 worker 异常退出但文件没有被清理干净新的 worker 使用相同 PID 启动时可能碰到遗留文件。Gunicorn 在创建 WorkerTmp 时会先检查路径是否已存在如果存在且可以写入会直接复用如果存在但无法访问则会尝试删除后重建。这部分逻辑在不同版本上有些差异但思路是一致的宁可覆盖掉旧文件也不能让一个不可用的文件阻塞新 worker 的启动。2.3 WorkerTmp 继承自 TempFile一个耐人寻味的实现细节在 Gunicorn 源码里WorkerTmp并不是从object直接继承的它继承自TempFile。TempFile是对 Python 标准库tempfile.TemporaryFile的一层封装它保证了文件描述符有正确的读写权限并且底层文件会被设置在合适的临时目录中。这里有一个很关键的工程点worker 进程是由 master 通过fork()产生的而fork()的子进程会默认继承父进程所有已打开的文件描述符。所以当 master 初始化 worker 对象时如果先在 master 侧创建了 WorkerTmp 的文件对象fork 之后 worker 和 master 会同时持有同一个物理文件的描述符。描述符被继承了但两个进程各自拥有独立的文件偏移量指针。对于心跳这种只写一两个字节的操作来说偏移量冲突的影响几乎可以忽略。worker 每次写入都把内容追加或者覆盖到文件上内核会自动更新时间戳master 完全不需要知道 worker 具体写了什么。2.4 权限和目录可用性两个容易忽略的约束WorkerTmp初始化时会检查目录是否可写# gunicorn/util.py 中相关逻辑 def ensure_writable_dir(path): if not os.path.isdir(path): os.makedirs(path, mode0o750) if not os.access(path, os.W_OK): raise RuntimeError(Worker tmp dir %s is not writable % path)这意味着如果/tmp被挂载成只读或者 Docker 容器里/tmp的权限被调整过Gunicorn 启动时会直接报错而不是静默降级。这其实是刻意设计的——心跳机制是整个超时兜底系统的地基地基如果不可用就应该尽早失败而不是等 worker 卡死之后 master 无法判断。文件本身的权限通常会被设置为 0600也就是只有启动 Gunicorn 的同一个系统用户才能读写避免其他用户能伪造心跳或者窥探信息。生产环境里如果你发现自己无法stat某个 gunicorn 心跳文件先确认当前 shell 用户和 Gunicorn 运行用户是否一致。3. worker 侧的心跳链路notify() 是如何被周期性触发的WorkerTmp 只是一个被动的存储载体真正让它产生价值的是 worker 进程里不断循环调用的notify()方法。链路不复杂但每一环都有讲究。3.1 从 Worker.run() 主循环开始追Gunicorn 的 worker 基类中有一个主循环run()同步 workerSyncWorker的实现非常直白class SyncWorker(Worker): def run(self): # 初始化监听 socket、装配自旋锁等 while self.alive: self.notify() # 等待新连接 try: client, addr self.socket.accept() except OSError as e: if e.errno errno.EINTR: continue raise # 处理单次请求 self.handle(client)可以看到self.notify()被放在了循环的最前面。这样设计保证了只要 worker 还在循环里正常运转即使暂时没有新连接进来它也会不断刷新心跳反过来如果 worker 卡在某次请求处理逻辑中出不来notify()就不会被调用master 就能检测到异常。对于 gevent、eventlet 这类异步 worker主循环里也有类似的周期性调用只是它们的循环粒度更细通常是在每个 event loop 回合一圈后触发一次 notify。核心逻辑仍然一致worker 一旦进入无法响应的状态心跳就会停止更新。3.2 notify() 调用链细节从 Worker 到 WorkerTmp在gunicorn/workers/base.py里Worker类对外的通知方法长这样class Worker(object): def notify(self): 通知 master我仍活着 self.tmp.notify()这个包装看起来多余但它的意义在于如果未来 WorkerTmp 的实现发生变化worker 主循环调用的地方不需要跟着改。调用链清晰简洁while self.alive → self.notify() → self.tmp.notify() → 写入文件/套接字 → 内核更新 mtime由 Gunicorn 的默认同步 worker 每隔很短的时间就会执行一次循环所以实际上心跳是非常频繁的。默认的timeout是 30 秒30 秒内只要 worker 有任意一次成功调用了 notifymaster 就不会判定它超时。3.3 内核自动更新的 mtime 就是心跳的灵魂WorkerTmp.notify()的关键动作是写入并 flushdef notify(self): try: self._tmp.write(b1) self._tmp.flush() self.last_update time.time() except OSError: # master 可能已经关闭或删除该文件 pass这里有几件事需要拆开说。第一写入的内容是什么并不重要。可以是b1可以是bx甚至可以是当前时间戳的字符串master 读取文件内容基本没有意义它真正关心的是写入动作带来的副产物文件 mtime 被内核自动更新。第二flush()非常重要。很多人在写类似心跳逻辑时会漏掉这一步。在 Python 里写文件是有缓冲的如果只执行write()而不执行flush()内容可能暂时停留在用户态缓冲区还没有真正落到底层的文件描述符上。虽然 mtime 的更新时机是由文件描述符上的写入系统调用触发的缓冲区未刷新时可能不会触发系统调用master 读取到的可能是旧 mtime进而产生误判。第三self.last_update time.time()是 worker 进程自身内存里维护的最后更新时间。有些版本的 Gunicorn 会通过一个共享机制让 master 能读到这个值有些版本则主要依赖文件 mtime。无论如何这行代码保证了 WorkerTmp 对象本身也有一个可查询的内存态时间戳便于在代码里做某些快速判断。3.4 不同 worker 类型在心跳频率上的差异Gunicorn 提供了多种 worker 类型各自的地址循环模型不同但 notify 的触发频率大体一致。Worker 类型主循环机制notify 触发方式SyncWorker阻塞式 accept每次 accept 循环前触发GeventWorkergevent event loop每个 loop 周期触发EventletWorkereventlet hub loop每个 hub 周期触发TornadoWorkertornado IOLoopIOLoop 回调中触发ThreadWorker线程池 accept 线程accept 线程的循环中触发值得注意的是虽然线程型 worker 内部会创建多个线程来处理并发请求但 WorkerTmp 的心跳仍然只是由主 accept 线程来维持。如果所有工作线程都被某些阻塞任务占满但 accept 线程还活着那心跳依然是新鲜的。也就是说线程型 worker 的超时检测只能反映 accept 线程的健康状态不能精确反映每个业务线程的卡顿情况。这算是一个边界特性在调 timeout 时需要心里有数。4. master 侧的超时审判murder_workers 的判定细节worker 侧把心跳写进文件只是半条链路master 侧的读取和判决才是完整闭环的另一半。master 的逻辑集中在gunicorn/arbiter.py的murder_workers()方法中。4.1 Arbiter 主循环的检查时机master 进程的主循环由Arbiter类管理它每隔一个很短的时间间隔就会做一轮管理操作。Gunicorn 默认的管理循环间隔是 1 秒期间会处理来自 worker 的管道信号、检查 worker 数量、处理待重启列表等。murder_workers()正是在这个周期性循环中被调用的# gunicorn/arbiter.py 中管理循环的大致逻辑 def manage_workers(self): if len(self.WORKERS.keys()) self.num_workers: self.spawn_workers() self.murder_workers()也就是说master 每秒都会对全部 worker 做一次存活检查。基于这个频率心跳文件的 mtime 检测精度可以控制在 1 秒级对于默认 30 秒的超时阈值来说已经足够。4.2 判定条件mtime 超过 timeout 阈值就动手murder_workers()的核心代码逻辑可以简化成下面这段def murder_workers(self): 杀掉不活跃的 worker if not self.timeout: return for pid, worker in self.WORKERS.items(): try: # 通过文件 mtime 获取 worker 最后心跳时间 last_update os.stat(worker.tmp.tmp).st_mtime if time.time() - last_update self.timeout: self.log.critical(WORKER TIMEOUT (pid:%s), pid) self.kill_worker(pid, signal.SIGKILL) except FileNotFoundError: # 心跳文件已经不存在说明 worker 可能已经退出 continue判定的逻辑一句话就能说清当前时间减去心跳文件的最后修改时间若差值超过self.timeoutmaster 就向该 worker 发送SIGKILL。这里有几个细节值得记住。第一个是worker.tmp.tmp这个双层属性。外层worker.tmp是 WorkerTmp 实例内层tmp是文件路径读起来有点绕但意思是明确的——master 拿到的是路径再用os.stat读取路径对应文件的元数据。第二个细节是SIGKILL信号的选择。为什么不是SIGTERM或者其他温和的信号因为SIGKILL不可被捕获、不可被阻塞能确保进程被强制终止。超时检测本来就是因为 worker 已经假死才触发的此时很可能连信号处理器都执行不了如果用温和信号可能永远等不到回应反而让情况恶化。第三个细节是if not self.timeout这个前置判断。如果你在配置里把timeout设置为 0就等于显式关闭了超时检测机制master 不会杀任何 worker。这个选项通常只建议在调试或者确定服务端可以无限期等待的场景中使用。4.3 kill worker 之后master 的下一步动作发完SIGKILL后master 并不会立刻补位。它会等系统回收子进程也就是收到SIGCHLD信号后调用 wait 系列函数收尸然后从自己的WORKERS字典里移除这个 worker 记录。下一轮循环开始时manage_workers()发现 worker 数量低于配置值会自动调用spawn_worker()生成一个全新的 worker 进程。整个闭环是发起请求 → worker 卡死 → 心跳停止更新 → master 在管理循环中发现 mtime 超时 → 发送 SIGKILL → 回收子进程 → 启动新 worker在线扩容的能力本质上依赖于Arbiter对 worker 数量的持续维护。所以你会发现即使某个 worker 被杀你的服务也不会出现永久性容量下降最多只是瞬间少了一个 worker 的处理能力。4.4 timeout 检查的边界kill 之前是否存在“抢救”机会有人可能担心master 每秒检查一次如果 worker 只是有一次心跳写慢了会不会被误杀从源码逻辑看判定条件是比较严格的只有当mtime超过当前时间整整timeout秒才会触发杀进程。也就是说如果timeout是 30 秒worker 至少连续 30 秒没有任何心跳更新才会被杀。因为 master 每秒检查一次实际触发时机大约在第 30 秒到第 31 秒之间。但这里有个容易被忽略的坑如果 worker 在这 30 秒内恰好恢复了一次心跳mtime 就被刷新了master 的计时从那一刻重新开始。也就是说一个 worker 即使频繁假死只要每次假死时间小于timeout并且中间偶尔能恢复一次它就能一直存活下去。生产环境中那种“每隔 20 秒卡顿一次”的 worker很可能永远不会触发超时杀进程但它对请求的响应已经受到了严重影响。这种情况光靠调timeout是发现不了的需要结合上游超时、数据库连接池等外围手段去解决。5. 从 WorkerTmp 实战踩坑中总结的参数调优理解了 WorkerTmp 的机制之后再来看 Gunicorn 的超时相关配置很多坑其实是可以提前避开的。下面这几个点都是我在实际部署和排查经验中踩过或者见过的。5.1 timeout 配多大合适没有标准答案但有判断标准timeout的默认值是 30 秒。对大多数 Web 接口来说30 秒已经是很宽松的阈值但对另一些场景来说这个值可能太小。判断标准不能只看“接口平均耗时”而是要看 P99 甚至 P99.9 的耗时分布。如果一个接口的正常耗时曲线中有 1% 的请求会超过 25 秒那么把 timeout 设为 30 秒就是冒着极大的误杀风险。这类请求一旦发生worker 就会因为心跳停止而被 master 杀掉正在处理的请求也会被直接中断。我的建议是对于普通 API 服务timeout 可以设在 30 到 60 秒之间如果存在长轮询、SSE、文件上传下载等长耗时场景timeout 至少要大于这些场景的 P99 耗时如果服务依赖外部系统而外部系统的超时时间设置得比 Gunicorn 的 timeout 更大那么 Gunicorn 的 worker 就可能替外部系统的慢响应“背锅”。我还遇到过一个典型的案例上游数据库查询本身设置了 120 秒超时但 Gunicorn 的 timeout 还是默认 30 秒。结果数据库一抖动worker 先被 master 杀掉请求直接中断问题表象就变成“Gunicorn 一直在重启 worker”。后来把 Gunicorn timeout 调到 150 秒让数据库超时先生效问题才算解决。5.2 worker_tmp_dir 别放在慢磁盘或网络挂载上Gunicorn 允许通过worker_tmp_dir配置心跳文件的存放目录。很多人从来没有注意过这个配置觉得放哪都一样。但在某些环境里这个目录的选择直接决定了误杀率。想想看master 每秒都会对每个 worker 执行os.stat来读取 mtime。如果心跳文件放在 NFS 或 CIFS 这类网络文件系统上每一次 stat 都是一次网络 IO延迟高且不稳定。更可怕的是worker 写心跳时如果因为网络抖动迟迟无法落盘mtime 更新就会延迟master 看到的是过期时间然后可能误杀健康的 worker。在 Docker 容器里也要注意/tmp的挂载方式。默认情况下容器内的/tmp是容器本地磁盘通常没问题。但如果有人把/tmp挂载成了网络盘或者被系统的临时文件清理策略干扰就要考虑用worker_tmp_dir显式指定到一个可靠目录。比较好的做法是让心跳目录落在内存文件系统上比如 Linux 的/dev/shm。由于心跳文件内容极小写入极其频繁但总量有限放在内存文件系统里能获得最低的延迟也完全不用担心磁盘磨损。5.3 我自己实现过一版“伪 WorkerTmp”踩了两个坑如果你想在项目里做 WorkerTmp 的单元测试或者想自定义一个心跳行为通常会自己写一个替代类。我在一次做 Gunicorn 集成测试时干过这事踩了两个印象深刻的坑。第一个坑是忘记 flush。当时我写了一个简化版 WorkerTmpnotify()里只写了self._tmp.write(b1)没有调用 flush结果 master 侧看到的 mtime 根本不更新worker 启动没几秒就被杀了。后来查了很久才发现是缓冲区问题。这也解释了为什么 Gunicorn 源码里特意保留了 flush 调用——它不是多余的。第二个坑是忘记处理文件不存在的情况。master 在每次检查时都会做os.stat但如果某个环境下心跳文件被人为清理master 会触发FileNotFoundError。如果这个异常没有被捕获master 进程本身可能崩溃。Gunicorn 源码中对此做了兜底所以在自定义实现时也要保留对应的异常处理。5.4 心跳文件堆积、PID 复用与清理策略高频率重启 worker 时/tmp/gunicorn-*.sock文件可能大量堆积。正常情况下 worker 退出后Gunicorn 会负责删除对应的心跳文件但如果进程是被kill -9强制杀掉或者宿主机崩溃文件就可能会残留。残留文件本身占用空间不大但有两个隐患下一个 worker 复用了相同 PID 时可能碰到旧文件。Gunicorn 通常会尝试覆盖或删除但权限不一致时可能启动失败如果系统配置了临时目录定时清理策略比如systemd-tmpfiles定期清理/tmp下的旧文件正在使用的心跳文件也可能被误删文件被删后 worker 继续写入会失败master 的os.stat又找不到文件最后可能导致误杀。所以在容器环境或使用 systemd 管理服务时最好确认一下权限和清理策略。如果服务会被频繁重启可以写一个定时任务把/tmp/gunicorn-*.sock中那些 mtime 已经超过一小时且对应 PID 不存在的文件清理掉。5.5 手动排查 worker 是否假死的快速方法最后分享一个排查技巧。当线上出现超时日志但你又想手动确认某个 worker 当时的状态时直接看心跳文件 mtime 是最快的路径stat /tmp/gunicorn-pid.sock看Modify字段的时间。如果当前时间与它相差不到几秒说明这个 worker 的心跳是新鲜的健康状态如果相差时间已经超过 timeout 配置并且日志里已经出现 WORKER TIMEOUT说明这确实是一个假死 worker。连续多盯几次还能观察到心跳更新的频率。正常 worker 的 mtime 应该每 1 到 2 秒变化一次如果发现某个 worker 的 mtime 总是停在同一秒后面跟着几次跳跃式更新那说明 worker 的主循环存在间歇性阻塞。这种间歇性阻塞在早期不一定会触发超时杀进程但它是系统健康度下降的信号值得顺着代码排查。6. 从 WorkerTmp 看到的 Gunicorn 设计哲学超时检测的机制本身并不复杂但WorkerTmp这个模块背后体现的设计取舍值得在后端工程中反复推敲。6.1 用内核时间戳当“分布式同步”机制Gunicorn 没有选择在 master 和 worker 之间维护一套复杂的心跳消息协议也没有引入额外的共享存储而是直接利用了文件系统的 mtime 特性。写入文件 → 内核自动更新 mtime → 另一个进程通过os.stat读取到最新时间这几步操作天然解决了两件分布式系统里最头痛的事同步与一致性。不需要显式的锁因为 mtime 的更新是内核原子完成的不需要额外的网络通信因为文件系统就是通信介质不需要关心消息丢失因为只要有一次成功的写入mtime 就会往前推进不需要补偿机制。这套方案的优雅之处在于它把最底层、最可靠的系统能力变成了一种“活体信号”。任何时候当你在设计心跳机制、任务队列的租约超时或者分布式锁时都可以借鉴这个思路能落在内核层面的机制尽量别在用户态重复造轮子。文件系统的时间戳、epoll 的超时时间、信号的传递这些由操作系统保证的基础能力通常比自研的通信协议更稳定。6.2 故障模式与可观测性WorkerTmp 的设计还有一个隐性的巨大价值可观测性。你可以直接通过stat命令查看 worker 的最后心跳时间可以在启动脚本里检查心跳文件是否存在来判断 worker 是否成功启动可以在监控系统里采集心跳文件 mtime 与当前时间的差值做成一条“worker 活度”指标。这些操作完全不需要 Gunicorn 暴露额外的 API也不需要改动业务代码纯粹基于一个文件就能做到了。我在生产环境里就曾经用一行 shell 命令批量检查所有 worker 的心跳新鲜度for f in /tmp/gunicorn-*.sock; do age$(($(date %s) - $(stat -c %Y $f))) echo $f age${age}s done这个方法在定位“某个节点上的请求突然变慢”的问题时非常有效可以在不重启、不打断请求的情况下快速找出哪个 worker 可能已经进入异常状态。6.3 什么时候需要考虑替换这套机制WorkerTmp 虽然设计巧妙但它能检测的范围有限它只能反映 worker 主循环是否还在转不能反映业务层的健康状态。比如你的数据库连接池耗尽worker 还在等待连接Redis 响应极慢worker 还挂在 I/O 上——这些情况下心跳可能依然是新鲜的因为 master 看到的是“循环活着”而不是“请求处理正常”。如果对服务健康有更细粒度的要求比如需要做到按接口维度检测、需要做优雅摘流、需要和外部注册中心联动那就应该在 Gunicorn 之上再叠加一层应用健康检查机制。常见做法是用一个独立进程定时请求服务的/healthz端点根据返回状态决定是否把节点从负载均衡池中摘除。WorkerTmp 解决的是“进程级别兜底”健康检查解决的是“业务级别兜底”两者并不冲突而是互补。按照我个人长时间维护生产环境的体会一个 Gunicorn 服务如果要做到稳定省心最简单的组合就是保持合理 timeout把 worker_tmp_dir 放到内存盘再配一个独立健康检查探活。这三点做完90% 的“worker 神秘死亡”问题都已经被前置拦截了。剩下那 10%再拿着心跳文件 mtime 和日志一条条慢慢对基本也都能水落石出。