使用confd和etcd来实现零停机程序版本切换

目录

作者:杨冬 欢迎转载,也请保留这段声明。谢谢!
出处:https://andyyoung01.github.io/http://andyyoung01.16mb.com/

因为容器比较轻量化,一台主机上可以同时运行多个容器,所以程序的版本切换可以通过移除旧版本的容器同时启动新版本的容器来简单的实现。对于基于web应用来说,可以通过nginx和confd来实现版本切换而不需要停机。

现在有很多容器的编排和调度工具(如Rancher,K8S,Mesos等)都可以实现零停机的程序版本切换,并且提供了很好的操作接口。我们这里没有直接使用这些工具,而是通过比较基础的工具来探索一下程序的配置文件的动态更新过程,以及程序版本切换的实现。

作为示例,首先,启动一个应用程序,此程序就是一个在Ubuntu上运行的Python的内置的web服务器,来列出容器根目录下的文件,后面将更新应用程序实现新的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[yangdong@centos7 ~]$ PY1_WEB_IP=192.168.71.131
[yangdong@centos7 ~]$ docker run -d --name py1 -p 80 ubuntu:14.04 \
> sh -c 'cd / && python3 -m http.server 80'
5c733a9a81b7b126cbbe82e3be9a8408cb61bc5f2b6c6a3418e8ece547f0e94d
[yangdong@centos7 ~]$ docker inspect -f '{{.NetworkSettings.Ports}}' py1
map[80/tcp:[{0.0.0.0 32768}]]
[yangdong@centos7 ~]$ curl -s $PY1_WEB_IP:32768 | tail
<li><a href="sbin/">sbin/</a></li>
<li><a href="srv/">srv/</a></li>
<li><a href="sys/">sys/</a></li>
<li><a href="tmp/">tmp/</a></li>
<li><a href="usr/">usr/</a></li>
<li><a href="var/">var/</a></li>
</ul>
<hr>
</body>
</html>

上面的命令启动了web服务器,然后通过inspect命令过滤出主机映射到容器内部端口的信息,最后确认了服务器正常运行。
下面我们使用了上篇文章“{% post_link 使用etcd来存储配置信息 %}”中配置的etcd集群来存储配置信息,确保你的运行环境内的etcd集群及etcd proxy正常运行。这次使用etcdctl工具(是“etcd controller”的简写)代替curl来和etcd交互:

1
2
3
4
5
6
7
8
9
10
11
[yangdong@centos7 ~]$ IMG=dockerinpractice/etcdctl
[yangdong@centos7 ~]$ ETCD_HOST_IP=192.168.71.131
[yangdong@centos7 ~]$ docker pull $IMG
[yangdong@centos7 ~]$ alias etcdctl="docker run --rm $IMG -C \"$ETCD_HOST_IP:8080\""
[yangdong@centos7 ~]$ etcdctl set /test value
value
[yangdong@centos7 ~]$ etcdctl ls
/mykey
/mykey2
/mykey3
/test

上面的命令使用了已经构建好的etcdctl的镜像,并且设置了一个alias来简化命令行的输入,它总是连接到上篇设置好的etcd-proxy上。然后使用该命令设置了一个键值对,确认了etcd集群工作正常。
下面的命令下载了另一个已经构建好的镜像,我们可以通过镜像源代码的地址来窥探镜像的构建过程。此镜像的功能就是一个可以动态更新nginx配置文件的反向代理服务器。它使用confd每隔10秒来从etcd取得配置信息,如果etcd中的信息有更新,confd便通过模板更新nginx的配置文件,然后使nginx自动重新载入配置文件。下面来看看其使用:

1
2
3
4
[yangdong@centos7 ~]$ IMG=dockerinpractice/confd-nginx
[yangdong@centos7 ~]$ docker pull $IMG
[yangdong@centos7 ~]$ NGINX_REV_PROXY_IP=192.168.71.131
[yangdong@centos7 ~]$ docker run -d --name nginx -p 8000:80 $IMG $ETCD_HOST_IP:8080

上面的命令下载了该镜像,然后启动了容器,将主机的8000端口映射到容器的80端口上,最后面的参数告诉容器可以从哪里连接到etcd集群。不过在启动了上面的容器后,查看其日志输出,会发现它一直报错:

1
2
3
4
5
6
7
8
[yangdong@centos7 ~]$ docker logs nginx
Using 192.168.71.131:8080 as backend
2016-09-06T13:23:15Z 4864f3220b05 confd[14]: ERROR 100: Key not found (/app) [18]
2016-09-06T13:23:25Z 4864f3220b05 confd[14]: ERROR 100: Key not found (/app) [18]
2016-09-06T13:23:35Z 4864f3220b05 confd[14]: ERROR 100: Key not found (/app) [18]
2016-09-06T13:23:45Z 4864f3220b05 confd[14]: ERROR 100: Key not found (/app) [18]
2016-09-06T13:23:55Z 4864f3220b05 confd[14]: ERROR 100: Key not found (/app) [18]
2016-09-06T13:24:05Z 4864f3220b05 confd[14]: ERROR 100: Key not found (/app) [18]

这是因为我们还未将正确的配置信息写入到etcd集群中,该信息告诉上面的名为nginx的反向代理容器其upstream的web服务器的地址和端口。在最开始,我们配置了一个基于Ubuntu的内置的Python网页服务器,通过inspect命令得到其映射到主机的端口信息(在第一段代码的第5行得到,这里是32768),也知道其容器所在主机的IP地址(在PY1_WEB_IP变量中)。下面我们将配置信息写入到etcd中:

1
2
[yangdong@centos7 ~]$ etcdctl set /app/upstream/py1 $PY1_WEB_IP:32768
192.168.71.131:32768

在等待最少10秒后,查询nginx容器的日志发现配置文件已更新,然后确认反向代理正常运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[yangdong@centos7 ~]$ docker logs nginx
...
2016-09-06T13:40:39Z 4864f3220b05 confd[14]: INFO /etc/nginx/conf.d/default.conf has md5sum 8acce95d7a823ee747d694d138af36f2 should be b12d9a3cb239a66b2c3b3287356855a6
2016-09-06T13:40:39Z 4864f3220b05 confd[14]: INFO Target config /etc/nginx/conf.d/default.conf out of sync
2016-09-06T13:40:40Z 4864f3220b05 confd[14]: INFO Target config /etc/nginx/conf.d/default.conf has been updated
[yangdong@centos7 ~]$ curl -s $NGINX_REV_PROXY_IP:8000 | tail
<li><a href="sbin/">sbin/</a></li>
<li><a href="srv/">srv/</a></li>
<li><a href="sys/">sys/</a></li>
<li><a href="tmp/">tmp/</a></li>
<li><a href="usr/">usr/</a></li>
<li><a href="var/">var/</a></li>
</ul>
<hr>
</body>
</html>

好了,我们已经使用了一个可以动态更新配置文件的反向代理容器nginx,它不断将请求转发到py1 web容器。这个py1容器只是简单得将根目录“/”下面的文件列出。
假如我们现在有了新的需求,想要列出“/etc”目录下的文件,这就需要通过更新web服务器来实现。然而这里我们通过启动另外一个web服务器容器py2来实现这个功能,而不是在py1上更新:

1
2
3
4
5
6
7
8
9
10
11
[yangdong@centos7 ~]$ PY2_WEB_IP=192.168.71.131
[yangdong@centos7 ~]$ docker run -d --name py2 -p 80 ubuntu:14.04 \
> sh -c 'cd /etc && python3 -m http.server 80'
[yangdong@centos7 ~]$ docker inspect -f '{{.NetworkSettings.Ports}}' py2
map[80/tcp:[{0.0.0.0 32771}]]
[yangdong@centos7 ~]$ curl $PY2_WEB_IP:32771 | tail | head -n 5
<li><a href="udev/">udev/</a></li>
<li><a href="update-motd.d/">update-motd.d/</a></li>
<li><a href="upstart-xsessions">upstart-xsessions</a></li>
<li><a href="vim/">vim/</a></li>
<li><a href="vtrgb">vtrgb@</a></li>

上面的命令启动了py2容器,并且验证了容器的正常运行。下面要将py2的相关信息写入etcd,使得nginx反向代理可以得知py2的存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[yangdong@centos7 ~]$ etcdctl set /app/upstream/py2 $PY2_WEB_IP:32771
192.168.71.131:32771
[yangdong@centos7 ~]$ etcdctl ls /app/upstream
/app/upstream/py1
/app/upstream/py2
[yangdong@centos7 ~]$ curl -s $NGINX_REV_PROXY_IP:8000 | tail | head -n 5
<li><a href="sbin/">sbin/</a></li>
<li><a href="srv/">srv/</a></li>
<li><a href="sys/">sys/</a></li>
<li><a href="tmp/">tmp/</a></li>
<li><a href="usr/">usr/</a></li>
[yangdong@centos7 ~]$ curl -s $NGINX_REV_PROXY_IP:8000 | tail | head -n 5
<li><a href="udev/">udev/</a></li>
<li><a href="update-motd.d/">update-motd.d/</a></li>
<li><a href="upstart-xsessions">upstart-xsessions</a></li>
<li><a href="vim/">vim/</a></li>
<li><a href="vtrgb">vtrgb@</a></li>

通过前面的命令,可以发现现在nginx反向代理容器可以感知到两个backends同时存在。整个过程可以通过下图描述出来:
“将py2容器加入etcd”
要将py1容器切换至py2容器,现在只剩下最后一步,在etcd集群中移除py1的配置:

1
2
3
4
5
6
[yangdong@centos7 ~]$ etcdctl rm /app/upstream/py1
[yangdong@centos7 ~]$ etcdctl ls /app/upstream/
/app/upstream/py2
[yangdong@centos7 ~]$ docker rm -vf py1
py1

好了,现在我们实现了程序版本的切换。在切换过程中,应用程序一直没有停机,并且管理员也不再需要手动连接到web服务器上去更新其配置文件,然后重新reload nginx。

本篇我们探索了confd和etcd配合使用,实现配置文件的更新。这里是手动更新的etcd集群的信息,其实还有更加自动化的方式来更新etcd,来实现服务发现机制。可以通过容器集群管理工具(如Rancher,K8S,Mesos等)来进行容器生产环境的管理,避免手动更新etcd集群信息。