看板服务卡死排查:HTTPServer单线程的坑

问题:看板突然打不开了

昨晚发现内容聚合看板(43.155.247.87:8897)打不开了。这个看板跑着一个简单的 Python HTTP 服务,用来收各个智能体提交的内容选题,然后人工在上面审核标记。页面刷新就是 loading 转圈,最后超时。

第一反应是安全组。毕竟前几天调过防火墙规则。ssh 上去一查——规则全在,8897 全放行。而且 netstat 能看到有外部 IP 的连接记录,只是状态不太对。

再查端口:

$ ss -tlnp | grep 8897
LISTEN 0 6 0.0.0.0:8897 ...

# Recv-Q 列显示 6——有 6 个连接积压在接收队列里,排队等着被处理。

Recv-Q 积压,说明服务活着但处理不过来。安全组没问题,问题在服务本身。

根因:单线程被一个坏请求堵死了

看板服务是用 Python 标准库 http.server.HTTPServer 写的。这个东西有个致命弱点——它是单线程的。同一时间只能处理一个请求,后面的全得排队。

某个智能体提交内容的时候,发了一个不完整的 POST 请求——声明了 Content-Length 但 body 没发完,或者中途连接断了。服务器线程就一直傻等着收完 body,永远不会超时。这一个请求堵死了唯一的处理线程,所有后续连接——包括浏览器的正常页面访问——全部排在这个死请求后面等死。

这就是为什么 本机 curl 能通但浏览器打不开:curl 请求来了,线程还在死等上一个请求的 body,Recv-Q 越积越多。

修复:一行代码的事

改起来很简单,把 HTTPServer 换成 ThreadingHTTPServer

# 改之前:
from http.server import HTTPServer
server = HTTPServer(("0.0.0.0", PORT), Handler)

# 改之后:
from http.server import HTTPServer, ThreadingHTTPServer
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)

ThreadingHTTPServer 是 Python 标准库自带的,不需要装任何东西。它给每个请求分配一个独立线程,一个慢请求只卡它自己的线程,不影响其他人。同样的 API,同样的用法,把类名换掉就行。

重启服务后验证:

=== 验证1:本机访问 ===
HTTP:200 耗时:0.001s

=== 验证2:公网 ===
8897公网: ✅ 通

=== Recv-Q积压 ===
LISTEN 0 0 ... (之前是6,已清零)

兜底:加了个看门狗

光改代码不够。万一以后又有类似的问题(虽然多线程不太会被单请求堵死了,但谁也说不准),总不能每次出事了才手动上去看。加了个每 5 分钟跑一次的服务看板看门狗:

现在 crontab 里的 15 个定时任务各司其职——从每 2 分钟的交付重试到每 5 分钟的看门狗,已经是一套小规模的自动化运维体系了。今天修的这个看板,是整个内容生产线的前端入口——选题从各个渠道汇总到这里,人工筛过再走自动发布流水线。它要是挂了,后面的链就全断了。

教训

这次排查走了弯路。一开始盯着安全组查了半天,因为最近刚好调过防火墙,惯性思维。端口监听正常、外部连接也能看到,就以为是规则没生效——其实 ss 命令那行里 Recv-Q=6 的信号已经明明白白告诉我"服务处理不过来",只是当时没注意这个细节。

搞技术久了就知道,最坑的 bug 往往不是复杂的东西,而是最简单的假设不对——"HTTPServer 多线程?不用吧,就一个小看板而已"——然后还真就是这个问题。

另一个教训:Python 标准库的 HTTPServer 不适合任何生产环境——哪怕是内部用的看板。它没有超时机制、没有并发处理、没有错误恢复。用 ThreadingHTTPServer 是最低限度的改进,长远看如果这个看板继续扩展,可能得换成 Flask 或者 FastAPI 加 gunicorn。