本地跑得好好的 Nest 应用,一上生产就变了个物种。本地是一个进程、保存即重启、数据库只有自己在连、请求直接从浏览器过来。生产环境是负载均衡后面挂着好几个副本、每次部署都会被停掉、数据库要和别的进程抢、依赖的 API 时不时不吭声。很多在第一次搭建时完全没问题的默认配置,放到第二种场景里全是错的。 **启动阶段就该拦住错误配置** 一个缺失或格式不对的环境变量,应该在进程接受任何请求之前就把它拦下来。否则应用照常启动、健康检查照常通过、流量照常进来,然后在第一个读取该变量的请求上崩掉。 @nestjs/config 支持把任意 Standard Schema(Zod、Valibot、ArkType)作为 validationSchema 传入。用 Zod 定义 NODE_ENV、DATABASE_URL、STRIPE_SECRET_KEY、PORT 这些字段,类型和必填项一次写清楚。读取必填值时用 config.getOrThrow('STRIPE_SECRET_KEY'),而不是 get()。滚动部署期间,一个启动就失败的版本永远不会变成就绪状态,上一个版本继续服务——这正是你想要的失败方式。 顺带检查值的来源。密钥应该放在平台的密钥存储里(Kubernetes Secrets、AWS Secrets Manager、Vault 或你的 PaaS 配置),运行时注入。不要放进被复制进镜像的 .env 文件,也不要用 Docker 的 ARG 或 ENV——这两者最终都会留在镜像层里,任何能拉取镜像的人都能读到。一个包含 .env* 的 .dockerignore 是成本最低的防线。 还有一件事:不要打印配置对象。启动时 console.log(config) 是很常见的调试动作,但它会留在代码里,把每一份凭据送进日志管道。要看加载了什么,打印键名,不要打印值。 **SIGTERM 和容器配置** 部署、缩容和节点排空都会发送 SIGTERM。Nest 默认不监听它,所以 onModuleDestroy 和 onApplicationShutdown 永远不会执行:连接池保持打开、队列 worker 在任务执行到一半时被杀、正在处理的请求被直接丢弃。 调用 app.enableShutdownHooks() 之后,信号会依次触发:HTTP 适配器标记为关闭中、onModuleDestroy、beforeApplicationShutdown、HTTP 服务器与网关和微服务关闭、最后 onApplicationShutdown。大多数集成在最后一步做清理,比如 @nestjs/bullmq 在这里关闭它的 worker,而 worker.close() 会等待正在执行的任务结束。 容器里信号丢失有两种常见方式: - 运行时和 Node 之间隔了东西。CMD npm run start:prod 或者 shell 形式的 CMD node dist/main.js,会在 Node 前面放上 npm 或一个 shell,两者都不能可靠地转发信号。用 exec 形式:CMD ["node", "dist/main.js"]。 - Node 以 PID 1 运行。内核不会对 PID 1 应用默认信号行为。enableShutdownHooks 会安装自己的处理器,钩子能跑,但 Nest 随后通过移除该处理器、把信号重新发给自己进程来退出。作为 PID 1,第二个信号被忽略,进程只有在事件循环恰好为空时才退出。用 init 进程运行(docker run --init,或用 tini 作为入口),或者用 app.enableShutdownHooks(undefined, { useProcessExit: true }) 让 Nest 调用 process.exit()。 镜像本身也值得看一眼。分阶段构建,编译工具、开发依赖和源码树就不会被打包进去;设置 NODE_ENV=production;不要以 root 运行,官方 Node 镜像已经内置了 node 用户。构建阶段用 npm ci 装依赖、npm run build 编译、npm prune --omit=dev 清掉开发依赖,运行阶段只复制 package.json、node_modules 和 dist。package.json 是有意复制的:Nest 12 的起步模板是 ESM,Node 需要从它读取 "type": "module"。 然后检查内存。如果容器有内存限制,而 V8 的堆上限高于容器实际能给的内存,内核会在 V8 有机会报错之前杀掉进程:退出码 137,没有 JavaScript 错误,没有日志行,只有一次重启。根据 Node 版本和限制的设置方式,默认堆大小不一定跟随容器的限制,所以要显式设置,给堆之外的东西(缓冲区、原生内存、栈)留出空间,比如把 NODE_OPTIONS 设为 --max-old-space-size=768,大约是 1 GiB 限制的 75%。这样泄漏或过大的请求体会以带堆栈的 JavaScript heap out of memory 错误结束,而不是静默被杀。无论哪种情况都要对重启告警,尤其是 OOMKilled。 **关闭前先排空** Kubernetes 终止一个 Pod 时,两件事同时开始:Pod 从 Service 的 endpoints 中移除,kubelet 停止容器(preStop 钩子,然后 SIGTERM)。Endpoint 移除需要几秒钟才能到达每个负载均衡器和 kube-proxy。一个在收到 SIGTERM 时立即关闭的服务器,会拒绝这段时间内被路由过来的请求。 通常的修复是在关闭开始前加一个短暂延迟,比如在 lifecycle 里配置 preStop 执行 sleep 5。 Nest 12 在 NestFactory.create() 上还有 return503OnClosing: true。它会在进行中的请求完成时,用 503 和 Connection: close 回应新请求。这在流量已经排空后有用,但在传播窗口期间这些 503 会到达客户端,所以要和延迟一起用,而不是替代它。 然后检查时间预算。Kubernetes 在 terminationGracePeriodSeconds(默认 30)之后发送 SIGKILL,而 preStop 的时间也算在里面。
![]()
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.