一颗洋葱再好,也有需要拆开或换宿主的一天。这一站讲两种形态迁移:拆成微服务舰队时,服务间调用的传递纪律怎么定;塞进 Serverless 函数时,Koa 的 callback 怎么复用、冷启动怎么压。最后补一块实时通信的拼图——WebSocket 的接入位置。
拆服务的决策在 9.1 节说过(组织信号优先),这里讲拆完之后的两条纪律,它们决定了舰队会不会散架。
纪律一:reqId 必须贯通全链。 第 6.2 节的 requestId 层已经预留了伏笔——入口优先读取上游的 x-request-id。跨服务调用时,HTTP 客户端要把这个头继续传下去:
import axios from 'axios'; import { als } from '../context'; async function callInventory(orderId: string): Promise<InventoryResult> { const res = await axios.get(`http://inventory/api/v1/stocks/${orderId}`, { timeout: 2000, // 显式超时:不带超时的调用都是隐患 headers: { 'x-request-id': als.getStore()?.reqId }, // 链路 ID 跨服务延续 }); return res.data; }
于是一次跨三服务的请求,在日志平台按同一个 reqId 能拉出完整轨迹——分布式排障的全部基础就这一条纪律。更工程化的做法是把上述逻辑封装成统一的内部客户端(超时、重试、reqId 传递内置),业务代码不直接裸调 axios。
纪律二:超时、重试、降级三件套成对出现。 重试必须配幂等保障(只重试幂等操作,或用幂等键);每个外部依赖要有降级预案(库存服务挂了,下单接口是拒绝还是降级为"异步确认")。这三件事写进统一的调用封装,比散在各处可靠得多。
第 2.1 节说过 app.listen 是 http.createServer(app.callback()).listen() 的糖——Serverless 适配就是把这个糖剥了,直接把 callback 交给函数运行时:
import Koa from 'koa'; import Router from 'koa-router'; // 以 AWS Lambda 风格为例:各云厂商运行时接口略异,思路一致 import serverless from 'serverless-http'; const app = new Koa<AppState, AppContext>(); const router = new Router(); router.get('/api/v1/hello', (ctx) => { ctx.body = { ok: true }; }); app.use(errorHandler()).use(requestId()).use(router.routes()); // 模块顶层创建实例:复用跨请求的连接池与编译产物 const handler = serverless(app); export const main = async (event: unknown, context: unknown) => { // 运行时重用进程时 context 中断复用,避免等待回调悬挂 return handler(event as object, context as object); };
三个适配要点。其一,实例顶层创建:模块加载只发生一次,后续请求复用同一实例——数据库连接池、Redis 连接、路由表都天然复用,这是冷启动优化的第一原则。其二,不调 listen:函数运行时管事件循环生命周期,自己起端口等于抢运行时的活。其三,冷启动预算:首次请求要付模块加载与实例初始化的成本,压缩手段包括——按需 require(重依赖延迟到首次使用)、精简依赖树(Serverless 打包体积直接影响加载时间)、配合平台的预热机制。
配置与环境的第三件事:Serverless 里没有常驻进程,环境变量来自函数配置而非机器;日志走平台采集(6.2 节的结构化一行 JSON 直接受益);优雅退出(6.3 节)由运行时的 freeze 机制接管,自编排逻辑保留在容器形态的部署里。
实时通信(推送、协作、聊天)与请求响应是两种模型,接入位置有讲究。社区方案 koa-websocket 把 WebSocket 升级挂进 Koa 实例:
import websocket from 'koa-websocket'; import Koa from 'koa'; const app = websocket(new Koa()); app.ws.use(async (ctx) => { // ctx 与 HTTP 中间件同源:鉴权层的身份信息在这里可读 ctx.websocket.send(JSON.stringify({ type: 'welcome', user: ctx.state.user?.id })); ctx.websocket.on('message', (raw) => { // 消息处理:注意这里不是 Promise 链,异常要单独接 }); });
两个工程判断。其一,鉴权在升级握手时做:WebSocket 建连请求也是 HTTP 请求,把 auth 中间件挂到 ws 路由前,凭证无效直接拒绝升级——长连接建立后不再重复认证,连接生命期就是会话生命期。其二,连接状态放进程内、路由放网关层:多实例部署时,用户 A 的连接在实例一,给 A 推消息的请求可能落在实例二——解法是 Redis 订阅广播(实例一注册连接、各实例订阅消息频道),3.3 节"内存存储只管单实例"的教训在这里原样重演。
💡 关键直觉:形态会变,层的语言不变。微服务里每个服务仍是洋葱,reqId 是贯穿舰队的层间语义;Serverless 里实例仍是洋葱,只是宿主从常驻进程换成函数沙箱;WebSocket 是洋葱旁边新开的一条长连接隧道——入口同样要过鉴权层。认出"层"在新形态里的化身,迁移就不慌。
两种形态都过完了,最后一站把全册知识装进两个真实容器:API 网关与中后台——看看这条剥层主线走到系统级长什么样。