首页 / 文章 / 构建可靠 Node.js API 的 12 条实用准则

构建可靠 Node.js API 的 12 条实用准则

实用的 Node.js API 可靠性准则:超时处理、幂等性、事务管理、缓存失效、日志记录以及依赖项延迟——在凌晨3点的文章之前。

906 词

有些关于可靠性的经验只有在实际故障发生后才会牢记。以下这十二条在众多 Node.js 服务中频繁出现,明确列出它们比在紧急情况下重新摸索要省时得多。在设计处理程序时应将其视为默认原则,而非上线后的可选优化措施。

1. 可重试的请求将会被重新发送。

不是“可能”。客户端会超时,连接在响应过程中中断,用户也会不断点击提交。负载均衡器和移动网络还会导致请求被重复发送。任何创建或修改状态的处理程序都需要明确应对“如果完全相同的请求再次出现”这种情况的方案,因为从足够长的时间维度来看,这并非边缘情况——而是不可避免的。应同时设计正常流程和重复请求的处理流程。

2. 为每个外部调用设置超时时间。

数据库查询、第三方HTTP请求、支付网关——如果未设定截止时间,出现故障的节点可能会让套接字一直处于开放状态。单纯的fetch(url)并不会“耐心等待一段时间”,而是会无限期地等待,而这种无限制的等待会导致某个缓慢的依赖项冻结整个上游流程。应选择那些会强制设置时间限制的库默认配置,出现故障时应及时触发重试或向用户显示错误信息,而非默默挂起。

3. 浏览器校验属于用户体验层面,而非安全屏障。

攻击者可以绕过你的单页应用,使用curl、Postman或被篡改的脚本发送任意格式的JSON数据到API。那些重要的规则——格式正确的电子邮件、非负价格、必填字段——必须在服务器端以与处理恶意请求相同的严格程度来执行。客户端验证仅能帮助那些本来就不会违反规则的友好用户。

4. 没有失效机制的缓存其实是一种延迟出现的错误。

缓存会创建第二个数据副本,而这两个副本可能会产生差异。在引入缓存时,必须明确指定哪种写入操作、Webhook事件或TTL时间触发机制来清除缓存,并将该决策记录在缓存键旁边。依赖“缓存最终会过期”这一说法往往意味着“过期时间比客户所能容忍的错误余额持续时间还要长”,尤其是当出于“性能考虑”而将TTL设置为几小时时。

5. 相关的写入操作需要通过事务来处理,而非仅凭乐观假设。

await update1(); await update2(); 这种写法看似是原子操作,但实际上并非如此。在两次await之间,如果系统发生崩溃、故障或请求被拒绝,可能只会有一个更新操作被执行,这就是库存数据与账单数据出现偏差的原因。只有真正的数据库事务——或是带有补偿机制的明确流程——才能确保操作在受控范围内完成。

6. 针对尚未出现的故障的工具。

一旦系统出现故障,再补充能够解释故障原因的日志记录为时已晚。在系统出问题之前就建立结构化的日志、跨服务传递的关联标识,以及足以还原请求上下文的信息,这些比最巧妙的事后调试更为有效。在系统正常运行时,应妥善保存日志输入、运行结果以及各组件间的依赖延迟数据。

7. 进程内存只是缓存,绝非持久化存储。

仅存在于 Node 进程中的数据在进程重启后会消失,其他实例也无法看到这些数据,而且它们本身就不具备持久性。存储在 RAM 中的功能标志、计数器或“临时”映射表也都面临同样的命运。如果这些数据需要在部署、崩溃或无会话粘性的负载均衡场景下依然存在,就应该将其存入数据库或其他持久化存储中,而非依赖偶然的 RAM 内存位置。

8. 要为每次网络调用最终失败的情况做好规划。

关键问题不在于“它通常能否正常工作?”,而在于“在极少数失败的情况下会发生什么?”超时、部分响应以及5xx错误频发都属于需要考虑的情况。如果诚实的答案是“不确定”,那就通过安全的重试机制、断路器或降级模式来填补这一漏洞,以免在正式上线后才发现问题。

9. 队列只是重新安排任务,并不会删除它们。

将任务放入队列后似乎就意味着任务已完成。实际上,工作进程仍会在稍后执行相同的操作,面临同样的失败风险,只是时间上有所延迟。如果工作进程处理速度变慢,尽管API仍能响应,积压的任务量也会增加——这虽然比阻塞请求要好,但工作成本并未降低。在规划HTTP层时,同样需要谨慎考虑任务处理器的规模以及死信处理路径的设置。

10. 起名时要考虑未来六个月的读者,而非眼前的便利。

data、temp和handleStuff只有在另外三个概念也被视为“数据”时才能保持准确性。与业务理念相关的名称即便在缺乏上下文的情况下也能被理解——包括未来在使用不熟悉服务时的自己。应优先使用领域名词,而非描述初始实现细节的机械性动词。

11. 创建操作会被调用两次。

不稳定的网络、频繁重试的客户端、急躁的双击操作——任何一种情况都可能出现。创建相关的POST请求都需要妥善处理重复情况,通常是通过将幂等性键与生成的资源一起存储来实现,否则就会出现重复收费和订单重复的问题,而这往往取决于时间因素。当检测到“已创建”时,应返回原有结果而非产生新的副作用。

12. 端点延迟取决于最慢的依赖项。

即便使用高效的处理器去处理未建立索引的查询或远程 API,且没有设置超时时间,速度依然会很慢。只分析自己的代码而忽略被调用的函数,其实是在回答错误的问题。应针对整个调用流程来测量 p95 和 p99 值,然后针对真正耗时的地方优化索引、批量处理机制以及超时设置。

这些建议其实都不算什么高明之策,而这正是关键所在:深夜里才处理的故障往往也是如此。它们通常属于以下十二种情况之一,直到问题变得不再紧急才会被处理。应在服务规模尚小、易于修改时,就提前建立这些必要的防护措施。