Taobao FED

Node.js 源站应用稳定性保障

Node.js 源站应用稳定性保障

源站与 CDN

源站是 CDN 技术中的一部分,是发布内容的原始站点。CDN 负责承载流量的部分称做缓存服务器,而缓存服务器自身不生产内容,需要从源站获取原始内容。Dragonfly 作为淘宝内容管理系统(CMS)的源站,渲染并为缓存服务器提供了所有的页面内容。

Dragonfly 使用 Node.js 开发,稳定性保障是一边实践探索、一边总结经验。现在来回顾,稳定性保障涉及了 Dragonfly 完整的开发运维的生命周期。因此本文依次从设计、实现、验收、运维四个环节展开。

系统设计

从设计入手,我们分别梳理了 Dragonfly 在淘宝 CMS 生态圈的拓扑图和源站内部流程的草图。

源站的外部环境比较简单。前面对接 CDN,实现流量承载和核心页面的容灾,稳定性很高。后面主要对接 Redis 缓存,用于获取页面的各种资源。(图中的 配置中心 和 FileSync,分别用于获取配置和共享模板片段,为弱依赖)

Dragonfly 外部环境

TMS 支持多终端页面的投放,因此 CDN 需要有识别终端的能力,Dragonfly 为此做了相关的处理。梳理前,源站的内部流程如下(Dragonfly 使用了 Koa.js,其中间件执行流程为回形针型——正序进,逆序出):

Dragonfly 内部流程 - 旧

这里我们注意到:

  1. 没有输入过滤模块,为保证环境一致,用户 Query 需要统一丢掉。
  2. 获取页面入口模块依赖 Redis 这样欠稳定的系统,但是并没有纳入容灾备份的流程。
  3. 没有统一的异常处理模块,容灾模块只作异常检测,并为妥善处理异常。

为此我们先在设计上进行了如下调整:

Dragonfly 内部流程

依据新的设计,我们确认了依赖不稳定系统的模块都能容灾模块覆盖。同时根据这张草图,我么确认了需要在实现、运维环节检查的内容:

  1. 确认外部依赖系统都有容错策略。
  2. 确认内部错误抛出都做了正确的日志记录,依据自身场景作恰当的容灾或其它异常处理。
  3. 确认监控脚本被正确的配置。

系统实现

依据系统设计环节的评审成果,我们开始评审系统实现部分。

外部依赖容灾保障

基本原则:

  • 关键链路依赖的外部系统越少越好,外部依赖一定要有详细的容灾策略和预案。
  • 依赖也包括第三方模块,应该使用最新稳定版本。过期版本易出现 BUG 无人解决、集成困难、性能差等问题。

Dragonfly 外部依赖的具体保障:

CDN/源站

  • CDN,根据自身特点有大量节点,依赖专业运维团队的维护,部分节点异常不影响可用性。
  • 源站异常,CDN 使用过期的副本。

终端探测

  • 终端探测模块使用 UA 进行判断。遇到未知设备,可能出现判断错误,因此系统提供了强制切换参数。

配置中心

  • 配置中心即 Dragonfly 使用的配置推送系统。设计上,配置中心从服务端至客户端有多级容灾。
  • 另外 Dragonfly 还在源码中做了一份本地容灾。

FileSync

  • 文件同步系统 FileSync 维护了 CMS/应用 共用的前端代码片段。
  • 推送后存在本地,遇异常可本地容灾、手工更新。

Redis

  • Redis 性能不错,但受网络影响,Dragonfly 实际使用时超时较多。我们做了大量测试,发现主要原因为 Redis 传输小数据较多,因此确保 TCP 连接做了 Keep-Alive、关闭 Nagle 算法、关闭 Delay ACK 优化后,性能得到了很大改善。
  • Redis 是数据缓存,Dragonfly 读取数据以 Redis 为主,另外使用了 Aliyun OSS 作为备份数据源。在 Redis 异常、超时时使用 Aliyun OSS。
  • Redis、Aliyun OSS 皆异常时页面将无法更新,源站启用本地容灾。

内部容错保障

基本原则有:

  • 规范异常格式、抛出方式,进行统一处理是容灾保障的基础。
  • 关键系统资源遇到瓶颈,要有降级策略。

Dragonfly 的具体处理方式:

上下文异常处理

  • 发生后需要做日志记录,使用静态副本容灾。

未捕获的异常

  • 写日志触发报警,重启 Worker 进程。

实时备份

  • 每个页面请求,每 10s 生成一次静态容灾副本写入硬盘。

内存监控

  • 渲染过程中会产生大量缓存、临时字符串,给垃圾回收带来了很大压力。
  • 内存占用过高,无法及时回收时,需要强制重启 Worker 进行回收。
  • 更多优化方案持续进行。

过载降级

  • 压力过高时,Dragonfly 会收到 nginx 提供的 Over-Load 头,此时直接返回静态副本。

静态开关

  • 与开发团队交流学到的手动降级方案。用于应付未知 BUG 导致的大批页面异常。

测试(验收)

系统设计时也要一并思考如何测试。优秀的设计应该是易于测试的。

单元测试

充分的单元测试是持续重构的保障。Dragonfly 的单元测试细节本文就不展开了,这里给出笔者的一些总结:

  • 单元测试不拖累开发效率,反而是持续高效开发的保障。
  • 测试覆盖可以验证代码和测试质量,帮助我们找到潜在的缺陷。
  • 单元测试设计要充分,从程序的基本单元入手,需求变更时必须及时更新。
  • 单元测试应保持独立性,每项测试不依赖其它测试,产生可覆盖、一致的结果。这里 Mock 是项很有用的技术。

功能测试

单元测试保障了每个模块的质量。对于整个系统而言,功能测试是确定是否实现用户需求的有效方法。

功能测试的实现与单元测试大同小异。要特别说明的是,对于容灾模块,除了功能测试,我们还做了线上演练。

性能测试

主要利用压测平台模拟真实用户访问,页面使用线上机器抓取的真实地址。出现明显性能下滑的变更不能发布。

持续集成

既然测试是质量的保障,我们应该把测试自动化。选择一个成熟的持续集成方案吧。

日志与监控(维护)

日志

日志是用于监控和排查问题的。应以监控和问题排查者的角度记录。做到统一格式,按模块分类记录,集中管理。笔者理解的日志分类为:

诊断日志

  • 例如源站会按 config/redis/xtemplate 具体分类记录。

统计日志

  • 例如源站的 QPS/RT 等访问日志统计。

审计日志

  • 例如用户操作日志。

日志是需要定时维护的,这里依据 Dragonfly 的日志维护经验给出总结:

  • 无用日志必须清理。
  • 设计日志时思考如何方便故障排查者。
  • 排查故障后,应反思日志是否合理,并及时完善日志。
  • 给一台线上机器实现详细 Debug 日志的开关,用于复杂问题的排查。

监控

有日志无监控等于没做,部分监控经验:

  • 监控信息要便于快速解决问题。
  • 监控要依据运维经验调整合理,误报过多容易导致麻痹。

总结

千里之堤,溃于蚁穴。系统稳定性需要持续的点滴积累,疏忽任何一个细微环节都可能给系统带来巨大的风险。但是依赖合理的规划、严格的验收标准、持续完善的监控,稳定性保障并不困难。