分享一套服务集群灰度发布的逻辑

前言

因为工作原因已经好一段时间没有发布新文章了,今天正巧有空分享一套服务集群灰度发布的逻辑。
之前有朋友问过我分组上线怎么操作,因为这个问题涉及到的理论并不多而实际操作细节比较重要,所以一时也没有办法口头上解答这个问题,那么今天我就以我最近做的一套简单的灰度发布逻辑来用具体操作分享一下。


架构图

如上架构图所示,这套逻辑要需要在以上架构中生效


逻辑流程

1,准备分组

从架构图上可以看到我们后端服务器有六台,为了在上线的时候对线上服务不造成影响那么当我们灰度发布的时候就需要对服务器进行分组。因为对部分服务器上线新版本的时候这一部分服务器会从线上给撤掉,那么当前还处于服务器状态的服务器无疑压力就变大了,所以分组的数量越多每组服务器的数量越少在上线操作的时候对线上业务的影响就越小,但是上线操作的复杂度会略微增加所用时间会延长很多。

那么在本文的例子中我们将6台服务器分为三组,每组2台服务器。
第一组:SRV1 SRV2
第二组:SRV3 SRV4
第三组:SRV5 SRV6

2,从Nginx的upstream上撤掉第一组服务器

在上一步中我们将服务器已经分好组,在我们对第一组服务器进行新版本的发布之前我们需要在Nginx上撤掉改组服务器,因为如果我们不在Nginx上撤掉当前组服务器的话在对服务器进行新版本发布的时候服务器是无法正常提供服务的,而这个时候Nginx因为该组服务器还在upstream中所以Nginx依然会将访问请求调度到正在发布新版本的服务器上,这样的话就会造成客户端无法正常请求。所以在服务器上线新版本之前先撤掉Nginx的调度列表里第一组服务器所在配置段。

3, 对第一组服务器进行新版本发布操作

现在我们已经从线上将第一组服务器撤掉了,那么现在可以对服务器进行新版本发布操作了。这里可以使用ansible的playbook配合上线脚本来进行该步操作。

4,等到第一组服务器的服务成功启动后将第一组加回Nginx的调度列表

等到上一步操作完成以后那么服务器上的服务处于正在启动状态,等到服务成功启动后便将第一组服务器加回Nginx调度列表,这样的话当前线上第一组服务器提供新版的新特性的服务,而其他两组服务器提供旧的服务特性,这便是典型的灰度。

5,依照第一组的发布逻辑依次发布后面两组

依照上面的逻辑从Nginx调度列表中撤掉第二组然后发布新版本到第二组等到第二组服务完全启动后再将第二组加回Nginx调度列表,第三组操作也是如此。


实现

以下内容都是通过脚本和ansible剧本实现,其间会有详细讲解,脚本水平不高大牛轻喷。

脚本

#!/bin/bash
export PATH=$PATH

#这里是各组的IP地址变量定义
GRP1=' 
SRV1
SRV2
'
GRP2='
SRV3
SRV4
'
GRP3='
SRV5
SRV6
'

#该函数是服务启动时的等待,其实这里更好的办法是测试访问某个接口,后面再做讲解
sleeptime() {  
    for i in {20..0};do
        echo "等待${i}秒后执行下一步"
        sleep 1
    done
    unset i
}

#以下函数都为每个分组的详细操作,这里其实可以复用优化,我暂时没这么做。。又不是不能用对吧。。
group1() {  
    #清空ansible的hosts中专门用于上线服务器的IP地址分组
    /bin/sed -ri '/\[server\]/,${/server/!d}' /etc/ansible/hosts
    #清空upstream中所有服务器配置段前的井号,这一步其实不执行也可以,这是为了以防万一的操作。这里的匹配逻辑是匹配server段后面的内容全部删除
    /bin/sed -ri 's/^#(server.*)/\1/' /path/upstream.conf
    #该循环的作用是循环添加第一组中的IP地址到ansible的hosts的[server]段用于上线操作,并在upstream中在该地址配置段前面加上井号
    for i in $GRP1;do 
        #添加hosts操作
        /bin/sed -ri "/\[server\]/a$i" /etc/ansible/hosts 
        #upstream中加井号操作
        /bin/sed -ri "s/.*$i.*/#&/" /path/upstream.conf 
    done
    unset i
    #这个ansible剧本的作用是是同步nginx的配置文件并reload
    /usr/bin/ansible-playbook /path/sync_ng.yml
    #service.yml的作用是停止服务部署新版本启动服务等作用
    /usr/bin/ansible-playbook /path/service.yml
    #重新启动服务后需要一些时间服务才能启动起来,这里的等待时间就是为了等待服务启动,这里更好的办法是循环访问服务的接口判断是否启动成功,如果启动成功再执行下一步。这里因为不同的业务检测的接口也不同,需要根据实际情况来作判断所以在这里我就写了一个延时作为代替
    sleeptime
    #下面这个sed的作用是将之前加上的井号去掉
    /bin/sed -ri 's/^#(server.*)/\1/' /path/upstream.conf
    #到目前为止延时已经过去了,我们假定服务已经成功启动,所以执行同步Nginx配置的剧本将刚才发布的服务器重新上线
    /usr/bin/ansible-playbook /path/sync_ng.yml
}

#第二组第三组的函数基本上都是一样的,唯一不一样的是在nginx配置文件中加上井号的服务器不一样,就不做解释了
group2() {
    /bin/sed -ri '/\[server\]/,${/server/!d}' /etc/ansible/hosts
    /bin/sed -ri 's/^#(server.*)/\1/' /path/upstream.conf
    for i in $GRP2;do
        /bin/sed -ri "/\[server\]/a$i" /etc/ansible/hosts
        /bin/sed -ri "s/.*$i.*/#&/" /path/upstream.conf
    done
    unset i
    /usr/bin/ansible-playbook /path/sync_ng.yml
    /usr/bin/ansible-playbook /path/service.yml
    sleeptime
    /bin/sed -ri 's/^#(server.*)/\1/' /path/upstream.conf
    /usr/bin/ansible-playbook /path/sync_ng.yml
}

group3() {
    /bin/sed -ri '/\[server\]/,${/server/!d}' /etc/ansible/hosts
    /bin/sed -ri 's/^#(server.*)/\1/' /path/upstream.conf
    for i in $GRP3;do
        /bin/sed -ri "/\[server\]/a$i" /etc/ansible/hosts
        /bin/sed -ri "s/.*$i.*/#&/" /path/upstream.conf
    done
    unset i
    /usr/bin/ansible-playbook /path/sync_ng.yml
    /usr/bin/ansible-playbook /path/service.yml
    sleeptime
    /bin/sed -ri 's/^#(server.*)/\1/' /path/upstream.conf
    /usr/bin/ansible-playbook /path/sync_ng.yml
}

#case语句一目了然,不做解释
main() {
    case $1 in
    group1)
        echo '第一组上线'
        group1
        ;;
    group2)
        echo '第二组上线'
        group2
        ;;
    group3)
        echo '第三组上线'
        group3
        ;;
    *)
        echo "usage:$0 <option>"
        echo 
        echo 'where options include:'
        echo '    group1'
        echo '    group2'
        echo '    group3'
        exit
        ;;
    esac
}

main $1

主逻辑脚本如上面所示,其他脚本就不演示了。
Nginx的同步剧本就是几行copy和一个service就行了
服务的新项目部署和启动脚本这个和业务类型有关,不同的业务类型脚本的逻辑都不同,不过大体逻辑只是停止服务部署业务然后启动服务而已


结语

我理解的灰度发布是要以请求不会受到影响为前提下进行的分组发布,以我上面这套逻辑来发布新版本的话在效率上并不高,但是以这套逻辑来发布新版本的话稳定性还是不错的,当然这是个简单逻辑,线上使用的话还需要根据实际情况完善容易出现bug的地方。