多种方式(含docker容器)实现零停机时间(Zero-Downtime)部署

零停机时间 (Zero-Downtime)

对于互联网上需要直接面向用户的应用在更新时一般要求尽可能地减少停机时间,所谓零停机时间意思就是应用更新或回滚时不会导致服务不可用,一般实现有结合软负载均衡器、SO_REUSEPORT等。

结合软负载均衡器

该方案架构上要求由web server对外提供服务,接收到请求后均衡转发给多个app server,部署时利用reload特性每次更新一部分机器,可以实现零停机时间,缺点是整个部署过程较长,比较繁琐。假设存在这样一个工作集:一台nginx服务器和两台tomcat服务器tomcat1和tomcat2,这样的工作集零停机部署过程大致是这个样子的:

  1. 将app-v1@tomcat1从nginx负载中移除
  2. 部署app-v2到tomcat1,即为app-v2@tomcat1
  3. 将app-v2@tomcat1重新加入到负载中(此时负载中app-v1和app-v2共存)
  4. 将app-v1@tomcat2从nginx负载中移除
  5. 部署app-v2到tomcat2,即为app-v2@tomcat2
  6. 将app-v2@tomcat2重新加入到负载中

说明:app-v1表示v1版本的应用,app-v1@tomcat1表示在tomcat1上的v1版本的应用

SO_REUSEPORT

SO_REUSEPORT是linux kernel 3.9之后的一个新特性,它支持多个进程同时listen同一个地址和端口,在此之前我相信大家都遇到过类似Address already in use的错误,这样一来就给我们一种错觉认为多个进程不能同时listen同一个端口,其实并非如此,下面看一个python应用的例子。

$ vi server.py
import socket
import os

SO_REUSEPORT = 15

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, SO_REUSEPORT, 1)
s.bind(('', 10000))
s.listen(1)
while True:
    conn, addr = s.accept()
    print('Connected to {}'.format(os.getpid()))
    data = conn.recv(1024)
    conn.send(data)
    conn.close()
$ python server.py &
[1] 5748
$ python server.py &
[2] 5749
$ echo | nc localhost 10000
Connected to 5748

$ echo | nc localhost 10000
Connected to 5749

$ kill 5748
$ echo | nc localhost 10000
Connected to 5749

从上面的例子可以看到首先两个进程先后都监听了同一个端口并没有报错,其次两次请求分别请求到了不同的进程。
利用SO_REUSEPORT特性实现零停机部署过程如下:

  1. 启动app-v2
  2. 停止app-v1

可以看到整个部署过程简单了很多,但其也有自身的缺陷和限制,比如需要linux kernel 3.9及以上版本,java6在语言层面上不能支持等。

docker容器实现

docker的出现给运维的工作带来了很大的变化,比如大幅降低了环境差异出错的可能性,以及提供了更优雅的部署更新和回滚等。

$ docker run -d --name containerA -p 3456:3012 app:v1

上面的命令启动了一个容器并将主机的3456端口和容器的3012端口进行了关联,我们看一下docker是如何做到的。

$ docker inspect -f '{{ .NetworkSettings.IPAddress }}' containerA
172.17.0.8
$ iptables -t nat -L DOCKER -n --line-numbers
Chain DOCKER (2 references)
num  target     prot opt source               destination
1    RETURN     all  --  0.0.0.0/0            0.0.0.0/0
2    RETURN     all  --  0.0.0.0/0            0.0.0.0/0
3    DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:3456 to:172.17.0.8:3012

从上面的结果可以看到docker增加了一条iptables规则,将对主机的3456端口的请求改写到172.17.0.8:3012,其中172.17.0.8就是上面创建的containerA的ip地址,当我们把这条规则删掉之后会发现端口映射任然有效,这是由于docker-proxy的存在。

$ iptables -t nat -D DOCKER 3
$ echo | nc localhost 3456
$ netstat -ntpl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      682/sshd
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      908/master
tcp6       0      0 :::6379                 :::*                    LISTEN      4503/docker-proxy

这里说明一下,docker daemon启动时缺省情况会启用iptables规则,当iptables规则启用时优先选择iptables,通过增加参数–iptables=false可以禁用iptables规则改为直接由docker-proxy转发请求。
了解了docker的端口映射机制再回到零停机时间部署上面来,我们不难发现docker容器实现零停机部署大致过程如下:

从零部署

  1. 启动app-v1@containerA,但不配置端口映射
  2. 增加iptables规则将主机端口映射到containerA上
$ docker run --name containerA app:1.0
$ iptables -t nat -A DOCKER -p tcp --dport 3012 -j DNAT --to containerA:3012

部署更新

  1. 启动app-v2@containerB,但不配置端口映射
  2. 调整iptables规则将主机端口映射到containerB上
  3. 停止app-v1@containerA
$ docker run --name containerB app:2.0
$ iptables -t nat -R DOCKER 3 -p tcp --dport 3012 -j DNAT --to-destination containerB:3012

部署回滚

  1. 启动app-v1@containerA
  2. 调整iptables规则将主机端口映射到containerA上
  3. 停止app-v2@containerB
$ docker start containerA
$ iptables -t nat -R DOCKER 3 -p tcp --dport 3012 -j DNAT --to-destination containerA:3012

参考资料:

猜你喜欢

转载自blog.csdn.net/kmter/article/details/51968554