48

基于Nodejs的前端灰度发布方案

1. 灰度发布和A/B测试简介

灰度发布

将某个功能灰度发布(逐渐放量)给特定线上人群,避免新功能全量上线带来的风险。

上面的图可以通过两个方面来理解

  1. 蓝色实线和蓝色虚线访问Nginx服务器,nginx通过负载均衡将流量分摊到后端服务器。
  2. 黄色的线是应用了灰度的流量(配置Nginx规则)可以将特定流量分发到特定的机房,以达到对特定用户应用产品新功能;
举个简单的例子:将http请求cookie中含有test=1字段的请求都转发到灰度代码的机房;

上面通过通过配置特定Nginx规则的方法来达到产品灰度的方法虽然可以满足一定业务量的需求,但是他也有很多的缺点

  1. 不灵活,每次上线新业务代码需要做灰度都要重新更新nginx规则,造成开发和运维负担;
  2. 上线的代码要做机房区分,不能够将代码全量。本地的Git代码也要区分开发分支和测试分支,线上分支等若干分支,管理起开麻烦;
  3. 不能满足业务量大或者业务需要频繁迭代,需要频繁做测试的业务;

那么有没有更好的方法来做灰度发布呢?当然是有的,A/B测试就能够弥补上面通过Nginx规则来做灰度的缺点。

A/B测试

将线上一部分真实人群流量随机拆分成多个组,对每个分组的人群应用不同策略或功能,通过计算每组人群的业务指标(转化率、成交率等)来衡量策略或功能的实际效果。

我们通过下面的这张图简单的了解下A/B测试的原理:

由上图我们可以知道A/B和传统的灰度方法的区别:

传统的灰度是通过Nginx分发流量到服务器,A/B测试是通过业务代码区分流量访问不同的代码块。

那么A/B测试的优缺点是什么呢?

优点:

  1. 随着业务的变化不用频繁的变化Nginx规则,不用分机房上线业务代码,本地git分支不用为了做灰度而建专门的分支;
  2. 流量区分是业务代码做的。所以上线代码的时候可以全量上线到所有机房;

缺点:

  1. 因为流量区分是业务代码做的。所以在代码中会存在很多的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来讲旧链接都转成新的链接。

  1. 升级后的业务怎么访问新的连接
  2. 已经请求相关的配置

避免不必要的请求

前端路由可以分为两种方式,hash和path切换。因为对于前端渲染页面来说,当第一次请求完成后,其实所有的页面都已经下载到了本地(页面异步加载除外)。在我们通过path切换页面的时候,每次都会向服务端发送请求,其实这些请求是不需要到达Nodejs服务的。我们可以通过Nginx配置将这些无用的流量抵挡在Nginx这一层,减少服务器的压力。

如果是使用hash的方式则不存在这样的问题,但是会有另外的问题就是对搜索引擎不友好。当然前端路由切换还是应该根据自己的业务做取舍。

4. 前端业务拓展

当我们应用了Nodejs服务之后,可以拓展的技术点有哪些,一下简单列举一些:

  1. 服务端渲染:提高首屏渲染时间,提升用户体验。
  2. 前端接口校验:增加前端访问后端接口,后端接口返回数据的安全性。
  3. 前后端分离,前端工程师的灵活性更加的高。

5. 技术升级带来的收益

  1. 前端上线可以实现小流量、灰度、发布,可以对线上流量做A/B测试,减少线上问题;
  2. 可以定制化对部分用户推动新功能;
  3. 加快首屏的渲染时间,提升用户体验;
  4. 多机房部署前端代码,降低前端服务不可用的风险;
  5. 团队成员技术能力的提升;

6. 最后

当然这种方案也不仅仅是可以使用Nodejs来做,也可以使用其它语言。因为我们公司已经有基于A/B测试的Nodejs-SDK。我这我就不具体介绍原理了。原理可以参考百度百科。如果有问题需要一起讨论可以留言或者是邮箱联系我:hpuhouzhiqiang@gmail.com


zhiqiang21
1.4k 声望1.1k 粉丝

I'm a Vimer!