在刚刚过去的 15 年天猫双十一中,Node.js(后文简称 node) 大放异彩,不仅帮助前端团队快速、高效的解决双十一各个业务上的页面渲染问题,同时在性能和稳定性上也表现非常出色,大大降低了双十一硬件成本的同时,在整个双十一期间未出现任何一起由 node 引发的线上故障。

覆盖业务

经过一年时间的改造和推进,到 15 年双十一的时候,已经有大量的业务都有了 node 的身影,基本上天猫大部分的 web 页面都是通过 node 渲染出来:

  • 天猫首页、大部分天猫频道页、双十一会场以及所有天猫的活动页面都全部基于 node 应用提供服务。
  • 商品详情、店铺和搜索页等主流程链路上,以及天猫超市和天猫会员等业务线上的页面渲染。
  • 提供给内部运营小二的天猫页面搭建平台 web 层基于 node 进行开发,双十一期间在此平台上搭建了超过 1000+ 个双十一相关活动页面。

工作职责

在上述覆盖了 node 的业务中,node 在其中扮演了多种角色:

完整的 web 应用

天猫页面搭建平台即是一个由 node 负责整个 web 端包括业务逻辑和模板渲染等工作的应用。基于支付宝的 node web 框架 chair,通过 hsf 调用和淘宝共建的页面数据存储的接口,用 node 完成业务逻辑处理、页面渲染和前端接口。

轻量级的模板渲染容器

通过 node 整合前端的天猫组件规范 MUI,开发了一套专注于模板渲染的 node 容器(wormhole),通过这个 node 容器,前端可以专注于展现层的开发,统一前端的本地和线上的代码运行环境,也让后端摆脱了繁琐的套模板工作,专注于提供数据接口。同时这套容器基于天猫的模块化规范,横向打通了各个业务和应用之间的模块共享。

基于这个模板容器,我们完成了商品详情、店铺、搜索页以及超市等业务线上的前后端分离工作,大大提升了前端的开发效率,并有效降低了前后端沟通成本。

页面渲染服务

同样基于天猫前端的组件规范 MUI 和模板渲染的 node 容器,我们完成了一套模块化搭建页面的系统,同时开发并运维了一个用来渲染基于模块搭建的页面的服务,同时这个服务和阿里的 cache CDN 打通,在保证满足业务需求的前提下,降低消耗的计算资源。

基于这个服务,在双十一中提供了 900+ 活动页面的渲染,以及天猫首页和各个频道页的渲染工作,天猫的所有营销引流页面基本都由这个服务提供页面。

进入正题

上面讲了许多我们用 node 做了什么,以及覆盖了那些业务,现在我们来看看,到底我们是怎样用 node 解决实际的业务需求的。

拿这次双十一的会场页举例:

  1. 用户在不同的终端环境下访问 fhttps://1111.tmall.com 这个网址,请求会直接来到 CDN 上。
  2. CDN 对用户的终端环境进行判断,并在内存中找到对应终端的缓存文件返回,若未命中缓存,则继续往下执行。
  3. CDN 将请求转发到 node 渲染服务,根据终端类型选择不同的页面响应(pc 页面,h5 页面, react-native 页面)。CDN 响应用户请求,并缓存页面。

在上述流程中,我们看到同一个 url 对应到后端其实是完全不同的页面输出内容,为了达到这个目的,我们和 CDN 团队一起做了许多工作:

  1. 开发了一个 tengine-detector 组件,通过请求的 user-agent 以及约定的一些 cookie 信息,判断用户的终端类型。并部署到 CDN 上,让 CDN 拥有了终端判断的能力。
  2. 用户请求到 CDN 上之后,CDN 会根据用户的终端类型分类,设置一个请求头,例如: detector: pc 表明这个请求的终端设备是 PC 上的浏览器。
  3. 渲染服务获取到这个头之后,根据 url 和设备类型选择不同的页面返回。返回时设置 fœhl3KIRnpsq201512011036146103936934521448937374610œdetector,保证 CDN 根据不同的设备类型缓存不同页面。

上面提到会根据终端类型对于同一个 url 返回不同的页面,而这些页面其实都是通过一个基于 node 开发的天猫页面搭建平台用模块搭建的。在这个平台上,超过 95% 的模块都拥有 pc 和无线两个版本,本次双十一所有用到的模块都有 react native 的版本。运营只需搭建 PC 上的页面,就会自动生成无线以及 react native 的页面。基于这套方案,我们通过 70+ 高质量的模块,让运营同学完成了超过 900+ 活动页面的搭建。

再深入一点,我们如何来完成这些页面或者是模块的呢?首先,我们希望让前端开发做什么?

  • 编写模板
  • 拿到数据(并处理),和模板进行结合
  • 拿到请求上下文,时间、环境等系统变量来确定不同的展现
  • 管理前端资源和依赖

我们在 xtemplate 模板引擎的基础上进行扩展,让前端通过编写 xtemplate 模板,在 context 中注入一些必需的页面上下文,扩展 xtemplate 的语法,支持引入前端资源。基于这套模板,我们可以在拿到数据后渲染得到完整的页面,基本满足了开发页面在功能上的所有需求。

但是页面中其实有非常多重复性的内容,我们完全可以把他们抽象成一个个的模块,让页面通过模块化的方式来基于模块搭建,在这个过程中我们需要解决几个问题。

  1. 模块版本和静态资源版本的管理:页面可能引用几十个模块,而这些模块依赖的静态资源有重复、有冲突,因此我们会通过一份统一的 seed 来进行依赖版本的管理,每一个模块在发布的时候都会打包好自身的依赖关系,而在将所有的模块组合成页面的时候,将所有模块的依赖表重新进行合并和去重,最终保证页面引用的模块和静态资源唯一。同时我们在模板中通过扩展引入了 FELoader(天猫的静态资源加载器),收集页面的所有静态资源,combo 后插入到页头(css)或者页尾(js)。
  2. 模块如何拿到相应的数据:对于模块而言,他并不需要知道被哪个页面引用了,所有的页面在引用模块的时候需要将模块所需的数据传递进去。而所有的模块开发者需要编写一份模块需要数据的 JSON Schema 描述,通过这份描述文件,搭建平台、投放系统以及其他使用这个模块的人都能够知道要为这个模块产生什么格式的数据。
  3. 配套的搭建平台和数据投放平台来让运营自由组合所有的模块生成页面,并为页面上的每一个模块进行数据投放。

解决完上述问题之后,我们将每一个页面都变成了以下几个部分:

  1. 一份页面的描述文件,声明了这个页面依赖的所有模块,以及渲染这些模块所需的数据的地址。
  2. 一系列相互独立的模块。
  3. 一份包含页面上所有模块需要的数据的数据文件。

最终,我们的渲染服务会根据 URL 和请求的终端环境,找到对应的页面描述文件,请求相应的数据,合并所有的模板渲染成为 HTML 页面。

当我们完成了 web 页面的模块化搭建之后回头再看,是不是 react native(RN) 的页面也能够搭建呢?我们只需要所有的模块都有对应的 react native 版本,就可以像搭建 web 的 html 一样搭建渲染出 RN 需要的 js 了!所以本次双十一使用的所有模块都有 RN 版本,并有多个会场采用了 RN 进行搭建,取得了非常不错的效果,在接下来的双十二中,我们所有的会场都会支持 RN,而这一切对于搭建会场的运营来说都是完全透明的。

稳定性保障

在阿里,所有的双十一相关应用都需要面临的一个大问题就是稳定性,为了保证能够在几亿用户买买买的时候不掉链子,任何一个应用都需要花很大的精力来保障它的稳定性,node 的应用也一样。

对于 node 应用自身而言,我们首先要保证它有充足的测试,通过 mocha + istanbul ,尽可能让测试覆盖每一个功能点和边缘情况。

需要有完善的监控和报警。在阿里内部,我们已经有了内部的监控系统,对于 node 应用而言,只需要按照要求的格式打印的日志,或者通过自己编写日志采集脚本,就可以轻松的搞定监控和报警。

  • 错误日志监控:通过采集脚本采集上来并分类,并设置单机报警和阈值和集群报警的阈值,在异常出现时能够及时发现。
  • 系统状态监控:内存、CPU、load 等的监控,并设置报警阈值,当系统状态异常时能够及时发现。
  • 应用状态监控:QPS、响应时间以及所有的远程调用记录,时刻了解系统的负载和各个依赖节点的服务状态。

同时,对于 node 应用,我们可以使用阿里云团队提供的 falinode ,他们可以提供更多 node 的日志和监控,并提供了在线的 profiler 和快照功能,方便排查线上异常和性能优化。

尽管我们可以对自身的代码做各种测试、各种监控,但是在一个复杂的系统中,各种上下游依赖非常复杂,网络情况也很复杂,这个时候为了保证稳定性,我们还有许多的工作要做。

没有单点

假设一个机房的光缆被挖断了,或者机房所在的城市大规模断电了,然后整个天猫的大部分页面都不能访问了,这明显不能接受,所以我们需要在多个城市的多个机房部署我们的服务。如果存放模板文件或者数据文件的服务挂了怎么办?多个节点,主备读取,同时对所有的文件都加上磁盘文件容灾。对外提供服务的整条链路上的每一个依赖都不能够出现单点问题。

弱化依赖

在排除完单点问题之后,我们再来审视我们的服务,是不是所有的依赖在挂掉后就无法正常服务了?是否我们对于每个依赖异常都有容灾的方案,弱化掉整条链路上的依赖。

预案自动化

对于每一个可能出现问题的环节,我们都需要有针对性的预案,如果这个预案需要人工去执行,就需要思考能否做到自动化。在 node 渲染服务中,可能有各个缓解出问题,链路上的所有预案都要能够自动切换:

  • CDN 回源到多个机房,当某个机房异常时能够通过健康检查自动剔除。
  • 当源站 load 过高时,服务自动切换到静态版本不做渲染。
  • 当模板或者数据的存储节点挂了,通过健康检查自动剔除。
  • ...

总结

再回过头来看看在天猫我们使用 node 做的事情,不一定很牛逼,但是确实是在天猫现在的业务场景下,一个相对较优的使用方案,不论是在解决前端开发效率、还是提升服务质量方面,都发挥了很重要的作用。而经过了这次双十一的考验,我们也认为它已经是一个很成熟的工具,可以帮助我们更好的完成我们的工作。

node 只是工具,在每一个具体的业务场景下都有最合适的使用方法,而随着业务的发展,node 能做的事情也在变化,我们期望它能在之后能在更多的场景下落地。:)