TT Lab
博客 开始
学习 学习路径 课程

Node.js 后端 — 框架藏起来的东西

打开套接字的服务器默认值不是生产配置

在 TT Lab 中继续学习

一句话总结

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,而每个服务的原因各不相同。