领域修炼之路

在DC/OS集群中实现AB测试和灰度发布

在软件自动化过程中,特别是敏捷开发及DevOps领域,自动部署是一个非常重要的环节。单一系统的部署与系统所采用的技术有很大的关系,通常会有对应的解决方案。但随着应用系统的复杂度不断增加,用户对系统服务持续在线的要求越来越高,同时系统也在不断向规模化,集群化方向扩展,人工部署的模式再也无法满足业务的需求。

当前常见的自动部署模式包括:蓝-绿部署(Blue-Green Deployment)和金丝雀部署(Canary Deployment)。这些部署模式仅作为系统自动部署的指导性原则,具体实践还需要与实际的技术,环境及工作流程相结合,进而支撑A/B测试和灰度发布等各种需求。

持续部署模式

蓝-绿部署(Blue-Green Deployment)

蓝-绿部署是通过创建两个版本的应用程序(蓝色和绿色),通过在两个版本之间安全切换,确保应用提供的服务实时在线的一种方式。


如上图所示,应用程序的新版本(绿色)部署后,如果通过功能及性能测试,可以在当前版本(蓝色)应用处理完所有的流量、请求及待处理操作后通过路由切换的方式将新的流量、请求调度给新的版本(绿色)。如果新版应用系统(绿色)出现问题,可以及时回滚到原有版本(蓝色);如果一切正常可以停止原有版本(蓝色),回收资源。在发布新版本时,继续重复上述操作。

关于蓝-绿部署的详细过程,请参考Martin Fowler的文章

蓝-绿部署的优点包括:1)上线过程对用户透明,不影响用户的体验;2)可以在生产环境对新版本进行功能和性能进行测试,便于试点;3)在新系统出现故障时,可以及时降级回滚。

金丝雀部署(Canary Deployment)

十七世纪的矿工,工作时会带一只金丝雀进入矿场,当矿井中二氧化碳浓度升高时,人类不易察觉,金丝雀会先死亡,矿工们以此作为监测二氧化碳浓度的指标。Google的Chrome浏览器提供金丝雀版本(Canary Version),并特别注明“胆小者请勿轻易尝试”。

金丝雀部署作为一种测试策略,一小部分服务器被升级到一个新版本或者新配置,随后保持一定的观察期。

如果没有任何异常出现,发布过程会继续,剩余的服务器也会升级到新版本。

如果出现异常,这部分单独修改过的应用服务可以很快被回退到原来的状态。

基于DC/OS的部署实践

在DC/OS中实现蓝-绿部署或金丝雀部署,一种方案是需要通过Marathon和Marathon-LB配合实现。

部署方法

前提条件

  • 应用通过Marathon管理部署,并且实现健康检查

  • 应用程序必须提供一个指标度量接口,以确定应用是否有任何待处理操作。例如,应用程序可以用一个全局Gauge暴露当前排队的DB事务的数量。

  • 安装JSON命令行处理工具-jq(针对CLI模式)。

操作步骤

下述步骤中蓝色表示应用当前版本,绿色表示应用新版本。

  1. 通过Marathon部署应用的新版本。为了区分,给应用添加一个新的ID(可以使用Git的Commit ID),在本示例步骤中,为新应用的ID添加green前缀:
1
2
# launch green
dcos marathon app add green-myapp.json

如果使用API调用而非CLI,需要使用如下命令:

1
curl -H "Content-Type: application/json" -X POST -d @green-myapp.json <hosturl>/marathon/v2/apps

  1. 根据需要扩展绿色新版应用的服务实例数据到1个或多个(起始值为0)。注意,当前启动的服务实例仍未提供服务,因为还未配置负载均衡。
1
2
# scale green
dcos marathon app update /green-myapp instances=1
  1. 等待确保所有的绿色服务实例都正常启动并通过了健康检查。可以通过下述命令进行确认:
1
2
# wait until healthy
dcos marathon app show /green-myapp | jq '.tasks[].healthCheckResults[] | select (.alive == false)'
  1. 使用上述命令片段检查所有的绿色应用服务实例是否健康,如果存在任何异常,终止部署并解决问题。

  2. 将新增的绿色应用服务实例添加到负载均衡池(Marathon-LB)。

  3. 从当前蓝色应用服务中选择一个或多个实例。

1
2
# pick tasks from blue
dcos marathon task list /blue-myapp
  1. 更新负载均衡池配置,将上述选择的服务实例从池中移除。

  2. 等待蓝色应用服务实例不再有待处理操作。使用应用提供的指标度量接口确定是否有待处理操作。

  3. 一旦蓝色应用服务待处理的操作都已完成,通过该API杀死并缩减蓝色应用服务,参考下述命令:

1
2
# kill and scale blue tasks
echo "{\"ids\":[\"<task_id>\"]}" | curl -H "Content-Type: application/json" -X POST -d @- <hosturl>/marathon/v2/tasks/delete?scale=true

此命令将删除特定实例(具有0个待处理操作的实例),并防止它们重新启动。

  1. 重复上述2-9步骤直至蓝色应用服务不再有实例运行。

  2. 监控绿色应用服务状态(持续一段时间),确认一切正常,从DC/OS集群中移除蓝色应用服务。

1
2
# remove blue
dcos marathon app remove /blue-myapp

在生产环境中,通常使用脚本将上述过程集成到部署系统中实现过程的自动化控制。

部署脚本

Marathon-LB针对上述部署方法提供了一个部署脚本zdd.py,通过该脚本可以实现零宕机部署(Zero-downtime Deployments)。

使用该脚本必须满足以下条件:

  • 在应用的Marathon定义中设定HAPROXY_DEPLOYMENT_GROUPHAPROXY_DEPLOYMENT_ALT_PORT两个标签(label)。

  • HAPROXY_DEPLOYMENT_GROUP:此标签唯一标识一对属于蓝色/绿色部署的应用程序,并将用作HAProxy配置中的应用程序名称。

  • HAPROXY_DEPLOYMENT_ALT_PORT:需要一个额外的服务端口,因为Marathon要求服务端口在所有应用程序中是唯一的。

  • 当前只支持一个服务端口。

  • ZDD脚本会调用Marathon的API并使用HAProxy的状态接口来正常终止实例。

  • Marathon-LB容器必须以privileged模式运行(需要执行iptables命令)。

  • 如果负责应用负载的HAProxy实例同时管理了TCP长连接,可能导致部署花费超出必要的时间。该脚本默认情况下设置5分钟等待HAProxy负载的连接逐步断开,但任何TCP长连接将导致HAProxy实例无法终止。

零宕机部署使用Lua模块来完成,该模块通过/_haproxy_getpids接口获取的HAProxy进程状态来报告当前正在运行的HAProxy进程数。

一个警告是,如果在同一个LB上有任何长连接,HAProxy将继续运行为这些连接提供服务,直到它们停止,这会导致部署过程无法完成。

脚本应用

使用该脚本可以实现蓝色/绿色应用程序两个版本同时运行并分割两者之间的流量。要实现此功能,需要设置HAPROXY_DEPLOYMENT_NEW_INSTANCES标签。

执行ZDD脚本时,通过参数--new-instances可以指定创建新版本应用程序的实例数并同时删除相同数量的旧版本应用程序实例(设定值等于旧应用程序实例数时,就是创建所有新应用程序实例并删除所有旧应用程序实例),以确保新应用程序和旧应用程序中的实例数之和等于HAPROXY_DEPLOYMENT_TARGET_INSTANCES的设定值。

示例:考虑相同的Nginx应用程序示例,其中有10个Nginx运行镜像版本v1的实例,现在我们可以使用ZDD创建版本v2的2个实例,并保留v1的8个实例,以便流量以比例80:20(旧:新)分割。

创建2个新应用程序实例并自动删除2个旧应用程序实例,可以使用如下命令:

1
$ ./zdd.py -j 1-nginx.json -m http://master.mesos:8080 -f -l http://marathon-lb.marathon.mesos:9090 --syslog-socket /dev/null --new-instances 2

同时存在同一个应用程序的旧版本和新版本的实例的状态称之为混合状态

当部署处于混合状态时,在部署任何其他版本之前,需要将其全部转换为新版本或全部为旧版本。这可以通过ZDD提供的--complete-cur(-c)和--complete-prev(-p)两个参数实现。

运行以下命令时,它将所有实例转换为新版本,以便流量拆分比例变为0:100(旧:新),并删除旧应用程序。这个过程是优雅的,因为ZDD会等待任务/实例停止服务之后再删除它们。

1
$ ./zdd.py -j 1-nginx.json -m http://master.mesos:8080 -f -l http://marathon-lb.marathon.mesos:9090 --syslog-socket /dev/null --complete-cur

类似地,可以使用--complete-prev参数将所有实例转换为旧版本(这本质上是一个回滚),以便流量分流比变为100:0(旧:新),并删除新的应用程序。

当前只支持一次流量分割,因此只有当应用程序的所有实例具有相同版本(完全蓝色或完全绿色)时,才可以指定新实例的数量(与流量分流比成正比)。这意味着不能在混合模式中指定-new-instances参数来更改流量拆分比例(实例比),因为当前更新Marathon标签(HAPROXY_DEPLOYMENT_NEW_INSTANCES)会触发新的部署。目前对于上述示例,服务实例拆分比是100:0 -> 80:20 -> 0:100,其中两个版本同时存在服务实例时的中间过渡状态只有一次。

上述示例的Marathon应用JSON定义示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"id": "nginx",
"container": {
"type": "DOCKER",
"docker": {
"image": "brndnmtthws/nginx-echo-sleep",
"network": "BRIDGE",
"portMappings": [
{ "hostPort": 0, "containerPort": 8080, "servicePort": 10000 }
],
"forcePullImage":true
}
},
"instances": 5,
"cpus": 0.1,
"mem": 65,
"healthChecks": [{
"protocol": "HTTP",
"path": "/",
"portIndex": 0,
"timeoutSeconds": 15,
"gracePeriodSeconds": 15,
"intervalSeconds": 3,
"maxConsecutiveFailures": 10
}],
"labels":{
"HAPROXY_DEPLOYMENT_GROUP":"nginx",
"HAPROXY_DEPLOYMENT_ALT_PORT":"10001",
"HAPROXY_GROUP":"external"
},
"acceptedResourceRoles":["*", "slave_public"]
}

除了上述方案,也可以通过其他工具实现此过程,如VampSwanLinkerd等,详细信息请参考后续章节。

参考

chrisrc wechat
更多信息请订阅我的微信订阅号