基于 Nodejs 的前端灰度发布方案
1. 灰度发布和 A / B 测试简介
灰度发布
将某个功能灰度发布(逐渐放量)给特定线上人群,避免新功能全量上线带来的风险。
上面的图可以通过两个方面来理解:
蓝色实线和蓝色虚线访问 Nginx 服务器,nginx 通过负载均衡将流量分摊到后端服务器。
黄色的线是应用了灰度的流量(配置 Nginx 规则)可以将特定流量分发到特定的机房,以达到对特定用户应用产品新功能;
举个简单的例子:将 http 请求 cookie 中含有 test= 1 字段的请求都转发到灰度代码的机房;
上面通过通过配置特定 Nginx 规则的方法来达到产品灰度的方法虽然可以满足一定业务量的需求,但是他也有很多的缺点:
不灵活,每次上线新业务代码需要做灰度都要重新更新 nginx 规则,造成开发和运维负担;
上线的代码要做机房区分,不能够将代码全量。本地的 Git 代码也要区分开发分支和测试分支,线上分支等若干分支,管理起开麻烦;
不能满足业务量大或者业务需要频繁迭代,需要频繁做测试的业务;
那么有没有更好的方法来做灰度发布呢?当然是有的,A/ B 测试就能够弥补上面通过 Nginx 规则来做灰度的缺点。
A/ B 测试
将线上一部分真实人群流量随机拆分成多个组,对每个分组的人群应用不同策略或功能,通过计算每组人群的业务指标(转化率、成交率等)来衡量策略或功能的实际效果。
我们通过下面的这张图简单的了解下 A / B 测试的原理:
由上图我们可以知道 A / B 和传统的灰度方法的区别:
传统的灰度是通过 Nginx 分发流量到服务器,A/ B 测试是通过业务代码区分流量访问不同的代码块。
那么 A / B 测试的优缺点是什么呢?
优点:
随着业务的变化不用频繁的变化 Nginx 规则,不用分机房上线业务代码,本地 git 分支不用为了做灰度而建专门的分支;
流量区分是业务代码做的。所以上线代码的时候可以全量上线到所有机房;
缺点:
因为流量区分是业务代码做的。所以在代码中会存在很多的 if…else 分支语句。但是这样还好,因为根据 SDK 的规范来书写代码,还是很好管理的。
2. 基于 A / B 测试的前端灰度怎么做
前端跟后端很大的区别就是直接面对用户,就算很简单的修改一次按钮的颜色就需要一次上线。这种操作对用户是可感知的。
现代前端有个特点就是脱离了后端模板引擎的渲染,大多数是使用 React、Vue 这种 MVVM 框架的前端(浏览器)渲染。这种情况下后端其实仅仅是给用户提供一个空的 html 文件(工作中经常称作为壳)。大多数业务代码开发完以后都是作为静态文件上线到服务器,经过用户访问后缓存到 CDN 节点上的。而且这个过程大多数是增量上线的。
其实我们每次上线完之后服务器上缓存的 html 文件就包含不同的版本信息。如果我们把这些版本信息管理起来,并且通过特定的手段(对用户请求应用 A / B 测试)就可以完成前端不同版本的灰度发布。
使用 Nodejs 灵活控制前端发布
我们可以观察下 Webpack 或者是其它打包工具打包后的 html 文件。每次外联的静态文件都包含不同的 hash 戳。这些外链的文件又都是增量缓存到服务其上的。
index.html(我们页面的“壳”)一些 xxx.js 文件(渲染页面 + 页面的业务逻辑)xxx.css 文件(控制页面显示样式)
大概就是下面的这个样子
基于以上的特点,我们能不能尽量减少对业务代码侵入,而可以覆盖业务改动较大的需求进行灰度或者是 A / B 测试呢?
看下下面的这个这个请求的图:
每次我们打包编译完之后,就将相关的 css 文件和 js 文件信息保存到本地的一个 json 文件中。这些信息的 key 可以是我们的 git 的 tag 信息(主要来描述本次发版信息包含的功能等)。
基本上 json 文件包含的信息如下:
const version = {
// 可以描述本次的上线内容 / 或者是 git tag
‘tag1’: {
‘css’: ‘xxxxxxx.css’,
‘app’: ‘app_xxx.js’,
‘ventor’:‘ver_xxx.js’
}
}
这里仅仅是一个简单的 demo 示例,可以使用 Nodejs 写文件的特性直接将文件版本号写入到 index.html 返回给前端浏览器
Nodejs 服务的特点是每次更新完代码需要重启之后才能生效。每次上完线重启服务就会先检查本地代码根目录下的这个 json 文件。看下这个其中包含的 tag 是否在 DB 中存储,如果有存储就不做操作,如果没有就将它存储 DB 做持久化。
上面图上面的 Apollo 就是用来配置那些用户访问新功能的平台。在 Nodejs 端,每次接收到用户请求的时候都会判断用户的信息是否满足相关条件,然后从 DB 中读取相关静态文件信息渲染到 index.html 中去。
简单总结下:将每次打包的静态文件信息先存储下来,之后请求到达 Nodejs 的时候判断用户是否满足相关条件,如果满足就读取 DB 将相关的静态文件信息返回给 Nodejs,Nodejs 将静态页渲染好之后返回给用户,达到灰度的目的。
3. 其它细节问题
使用 Nodejs 之前我们的页面就是直接部署在服务其上,这次使用了 Nodejs 后,会有很多其它的问题需要做,比如说 Nodejs 服务的监控,多机房部署等。这些在大部分的公司应该都有相关的运维工程师来做。我这里简单介绍一些其它的内容
规范的确定
这里的规范包括本地开发时工程目录的规范和线上用户访问 url 的规范。
开发目录规范
在笔者写这篇文章的时候最新的 Nodejs 版本已经是 11.10 版本了,最新的 LTS 版本是 10.15.1 版本。建议使用 Nodejs 的同学都升级自己的 Node 到 8.0 版本以上,因为 8.0 版本是一个官方原生支持 async…await 语句的版本。
.├── client // 放置客户端的代码├── index.html├── index.js├── node_modules├── output├── package-lock.json├── package.json├── server // 放置服务端 Nodejs 代码├── test.sh
需要注意的就是在编写 webpack 打包工具的时候将 server 目录下的给排除掉。放置不必要的编译和产出,增加打包速度。
线上 url 的约定
当使用了新的服务的时候为了防止跟旧业务的冲突肯定需要使用新的 url。这个时候就需要做一些约定。目前我们是这么约定的
// 域名 / 产品线 / 模块 /
http://wwww.aaa.com/driver/bus/index.html
// 域名 / 产品线 / 模块 / 静态文件目录
http://wwww.aaa.com/driver/bus/static/js/index.js
http://wwww.aaa.com/driver/bus/static/css/index.html
兼容老业务需要做的工作
前面提到这次业务升级我们使用了新的 url,但是为了保证业务的稳定性我们不是一次性将所有的流量都切到新服务上去的。我们也是通过批量的切的,所以会存在线上用户有的地区访问新服务有地区访问旧服务。那么有一天会有全部切换的一天,但是还是会有一些用户访问到旧链接,这个时候可以通过配置 Nginx 的 `rewrite 来讲旧链接都转成新的链接。
升级后的业务怎么访问新的连接
已经请求相关的配置
避免不必要的请求
前端路由可以分为两种方式,hash 和 path 切换。因为对于前端渲染页面来说,当第一次请求完成后,其实所有的页面都已经下载到了本地(页面异步加载除外)。在我们通过 path 切换页面的时候,每次都会向服务端发送请求,其实这些请求是不需要到达 Nodejs 服务的。我们可以通过 Nginx 配置将这些无用的流量抵挡在 Nginx 这一层,减少服务器的压力。
如果是使用 hash 的方式则不存在这样的问题,但是会有另外的问题就是对搜索引擎不友好。当然前端路由切换还是应该根据自己的业务做取舍。
4. 前端业务拓展
当我们应用了 Nodejs 服务之后,可以拓展的技术点有哪些,一下简单列举一些:
服务端渲染:提高首屏渲染时间,提升用户体验。
前端接口校验:增加前端访问后端接口,后端接口返回数据的安全性。
前后端分离,前端工程师的灵活性更加的高。
5. 技术升级带来的收益
前端上线可以实现小流量、灰度、发布,可以对线上流量做 A / B 测试,减少线上问题;
可以定制化对部分用户推动新功能;
加快首屏的渲染时间,提升用户体验;
多机房部署前端代码,降低前端服务不可用的风险;
团队成员技术能力的提升;
6. 最后
当然这种方案也不仅仅是可以使用 Nodejs 来做,也可以使用其它语言。因为我们公司已经有基于 A / B 测试的 Nodejs-SDK。我这我就不具体介绍原理了。原理可以参考百度百科。如果有问题需要一起讨论可以留言或者是邮箱联系我:hpuhouzhiqiang@gmail.com