打开套接字的服务器默认值不是生产配置
一句话总结
http.createServer 的默认值是为“在本地能跑起来”而设定的。在前端(负载均衡器)后面、需要承受慢客户端、大请求体和
部署信号的生产服务器,必须亲自决定其中五处默认值——请求体大小、请求头与请求超时、
keep-alive 时间、错误响应、关闭顺序。
为什么需要它
上一个模块中的处理函数是返回 {status, body} 的函数,契约在那里确定。但当这个函数接到 socket 上的那一刻,
就会出现契约之外的事情:客户端每秒只发一行请求头;声称请求体有 10MB 就闯进来;前端用闲置了 1 分钟的
连接发送请求;部署工具发来 SIGTERM。这四种情况在处理函数的测试中一次都不会出现,在生产环境里却天天发生。
测量一下 Node 22 的默认值,结果如下(2026-11-05,Node 22.11 实测):keepAliveTimeout 5 秒,headersTimeout 60 秒,
requestTimeout 300 秒,请求体大小没有上限。而且超时检查每隔 connectionsCheckingInterval(默认 30 秒)
才运行一次——即使把 headersTimeout 设为 3 秒,没有缩短检查周期的服务器过了 10 秒仍没有断开慢连接。
只改了值却不测量,就会以为已经修好了。
它如何工作
| 机制 | 防止什么 | 在哪里设置 |
|---|---|---|
| 请求体上限 | 大请求体耗尽内存和时间。先用请求头的 Content-Length 拒绝,不声明大小的 chunked 则边接收边计数 | 处理函数(自己实现) |
| headersTimeout | 拖延发送请求头的连接(slowloris) | createServer 选项 |
| requestTimeout | 拖延发送请求体的连接。不包括生成响应的时间 | createServer 选项 |
| keepAliveTimeout | 比前端先关闭连接而引起的“向正在关闭的连接发送”竞争 | server 属性 |
| 错误响应 | 整个进程崩溃(抛出的错误无人接收),或响应中带出堆栈和机密 | 请求处理器的 catch |
keep-alive 的关键在于方向。前端会把通往服务器的连接汇集起来重复使用。如果服务器 5 秒就关闭连接,而前端以为 可以用 60 秒,那么就在服务器关闭的那一刻,前端发出的请求会被切断,用户看到 502。所以负责关闭的一方 必须是前端,服务器的 keepAliveTimeout 必须比前端的空闲时间更长。
关闭是一个顺序。收到 SIGTERM 后,(1)不再接受新连接(server.close()),(2)关闭空闲连接,(3)对进行中的
请求完整响应,但响应后关闭该连接(Connection: close),(4)全部结束后自行以 0 退出。(5)超过截止时间则
切断剩余连接——在编排器的 SIGKILL 到来之前,由自己完成。这里有一个陷阱:server.close() 只关闭
那一刻空闲的连接。当时正在工作的 keep-alive 连接在响应结束后仍然开着,所以 close 回调永远不会到来,
前端也会继续通过这个连接发送请求。实测中,这样的进程一直存活到被 SIGKILL。
在现场会遇到的情形
“只有部署时才出现几个 502”这一常见报告,通常是以下三种之一:收到 SIGTERM 就立即 process.exit 的代码(进行中的请求
被切断);截止时间设得太短的优雅关闭(比最长的请求还短);以及上面所说的未关闭的 keep-alive 连接(一直不结束,最后被 SIGKILL)。
三者在日志中看起来都是同样的一行“502”,区分它们的线索是旧进程在 SIGTERM 之后何时、以何种方式结束。
在 Kubernetes 中,端点移除和 SIGTERM 是同时开始的,所以还会加上用 preStop 撑几秒的机制,但那是本
实验之外的话题。
请求体和超时限制,往往先在事故中遇到,而不是在攻击中。移动应用的重试 bug 不停地重发同一个 10MB 上传,使服务器内存
剧烈波动;或者网络条件差的地区的客户端留下数百个半开的连接,耗尽 socket。这时“框架会自动帮我挡住”
的想法通常是错的——Express 的 body-parser 默认有 100KB 上限,但标准 http 什么都没有,而超时设置两者都原样继承 Node 的
默认值。
错误响应也一样。一次 throw 就让整个进程崩溃的服务器;为了防止这种情况,把 unhandledRejection 改成只记日志,
结果请求永远得不到回答的服务器(上一个模块的事件);还有在 500 响应中原样附上 err.stack、暴露了数据库密码的
服务器——这些都真实存在过。
下一个实验要做的事
用六个步骤逐步完善一个 /root/work/srv/server.mjs。评分器会用空闲端口直接启动这个文件,用真正的 socket 进行探测——
每 0.5 秒发一行请求头;声称 10MB 却只发 1KB;让连接闲置 7 秒后再使用;在处理过程中发送 SIGTERM。
最后一步是事件:三个服务每次部署都会出现 502,而每个服务的原因各不相同。