如何在不惊动用户的情况下更新服务?
想象一下:周五晚上,你决定快速将 bug 修复推送到生产环境。你点击部署——然后你的即时通讯软件、在线影院或 CRM 系统宕机了 30 秒。成千上万的用户在这段时间里看到 502 Bad Gateway。如果这是一个支付网关呢?算算损失吧。
零停机部署(Zero-downtime 部署)不是奢侈品,而是现代 TypeScript 服务的标准。在本文中,我们将剖析生产环境中使用的真实技术:从优雅关闭到蓝绿部署和滚动更新。你将获得可直接应用于 TypeScript 的现成模式。
为什么常规的 TypeScript 应用部署是危险的?
当你运行 npm run build && node dist/index.js 时,会发生几个问题:
- 连接中断 — 旧进程被杀死,而新请求仍在发送。
- 内存数据丢失 — 未保存的会话、缓存、队列。
- 冷启动 — 新进程尚未预热,却已经开始接收流量。
在 TypeScript (Node.js) 中,异步性使情况更糟:你不知道哪些 Promise 仍在执行。但解决方案是存在的。
技术 #1:优雅关闭 (Graceful Shutdown)
零停机的基础 — 正确处理操作系统信号。当编排器(Kubernetes、PM2)决定停止你的服务时,它会发送 SIGTERM。你的代码应该停止接受新请求,等待当前请求完成,然后才退出。
使用 Express 在 TypeScript 中实现
import express, { Request, Response } from 'express';import http from 'http';
const app = express();let server: http.Server;
app.get('/health', (req: Request, res: Response) => { res.status(200).json({ status: 'ok' });});
// 来自 Docker/K8s 的信号process.on('SIGTERM', async () => { console.log('收到 SIGTERM,开始优雅关闭...'); // 1. 停止监听新连接 server.close(() => { console.log('HTTP 服务器已关闭'); // 2. 关闭数据库、Redis 等连接 // await db.disconnect(); process.exit(0); });
// 3. 给当前请求一些时间(超时 30 秒) setTimeout(() => { console.error('超时强制终止'); process.exit(1); }, 30000);});
const PORT = process.env.PORT || 3000;server = app.listen(PORT, () => { console.log(`服务已启动,端口 ${PORT}`);});重要提示: 在 TypeScript 中,使用 http 模块的 http.Server,而不是 Express 类型。方法 server.close() 能正确处理 keep-alive 连接。
技术 #2:滚动更新 (Rolling Update)
如果你的服务有多个副本(运行 3-5 个实例),滚动更新会逐一更新它们。当一个实例重启时,其他实例继续处理流量。
Kubernetes Deployment 示例
# deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: my-typescript-servicespec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 # 最多允许多少个 Pod 不可用 maxSurge: 1 # 最多可以额外创建多少个新 Pod template: spec: containers: - name: app image: myregistry/service:v2 ports: - containerPort: 3000 livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 3000 initialDelaySeconds: 3 periodSeconds: 5在你的 TypeScript 应用中添加一个 /ready 端点,仅在服务就绪时返回 200。