代码改变世界

服务器无损升级技术解析

2023-07-01 19:29  itwolf  阅读(330)  评论(0编辑  收藏  举报

声明:本人原创文章,详细内容已发布在我的微信个人技术公众号---网络技术修炼,公众号总结普及网络基础知识,包括基础原理、网络方案、开发经验和问题定位案例等,欢迎关注。

概述

软件工程中持续迭代和更新是必不可少的,在服务端软件更新时,保持服务的连续性是一项关键任务。本文将从技术角度解析服务端软件更新过程如何实现不停止服务的重要功能。

在进行热升级时,进程的代码和数据都是非常重要的。为了实现代码的更新,同时又不丢失有用的数据,需要采取一些措施。有用的数据包括内存中的数据和文件描述符。对于内存中的数据,例如配置信息,可以通过将其落盘到配置文件中来实现保留。这样,在升级过程中,新的进程可以读取配置文件并继续使用之前的配置。而对于文件描述符,可以采用一种叫做UNIX域套接字的机制,在进程之间进行迁移。通过这种方式,新进程可以接管原来进程的文件描述符,从而保持之前打开的文件和网络连接的状态。在某些情况下,项目可能会选择不迁移文件描述符,而是通过让新旧进程共同处理一段时间的请求来逐步过渡。这样,新进程可以逐渐接收和处理新的请求,而老进程则继续处理旧的请求,直到所有请求都由新进程处理完毕。

另外,为了减轻对客户端的影响,还可以采用一些HTTP协议的特性。例如,在HTTP1中可以使用"Connection: Close"头部字段,告知客户端断开连接并重新连接。而在HTTP2中,可以使用Goaway帧来类似地通知客户端断开连接。这样一来,客户端就能够及时与新进程建立新的连接,以继续进行请求和响应的处理。

通过这些措施和优化方法,可以实现热升级过程中代码更新和数据保留的目标,并尽可能减少对系统和客户端的影响。

详解

通过fork + execve实现无损升级

典型项目

nginx

nginx为例解析

交互流程

  • 先不停掉老进程,启动新进程。
  • 老进程继续处理仍然没有处理完的请求,但不再接受新请求。
  • 新进程接受新请求。
  • 老进程处理完所有存量请求,关闭所有连接,退出。

信号支持

官方文档:http://nginx.org/en/docs/control.html

nginx中master进程为管理进程,woker进程为master进程fork出的子进程,是处理网络的进程。

master进程支持的信号

TERM,INT

快速退出

QUIT

优雅退出master+worker进程(worker进程处理完存量请求再退出)

KILL

强子终止进程

HUP

使用新的的配置启动worker进程,并优雅退出老的worker进程

USR1

重新打开日志文件

USR2

升级可执行文件(即启动新的master进程)

WINCH

优雅退出woker进程

worker进程支持的信号:

TERM,INT

快速退出

QUIT

优雅退出(处理完存量请求再退出)

USR1

重新打开日志文件

实验

  • 更新前进程状态查看。
#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     82562  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82563  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82564  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82565  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82566  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82567  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82569  82556  2 11:58 ?        00:00:03 nginx: worker process
nginx     82570  82556 14 11:58 ?        00:00:24 nginx: worker process

#cat /app/nginx/logs/nginx.pid
82556
可以看出nginx.pid记录的是当前master的进程号。
  • 将旧Nginx二进制换成新Nginx二进制(注意备份旧二进制)。
  • 向master进程发送USR2信号。
kill -USR2 `cat /app/nginx/logs/nginx.pid`
    • nginx收到信号会创建新master并fork出新worker,此时新老共存,都会处理请求。

执行后结果

#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     82562  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82563  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82564  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82565  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82566  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82567  82556  0 11:58 ?        00:00:02 nginx: worker process
nginx     82569  82556  2 11:58 ?        00:00:06 nginx: worker process
nginx     82570  82556 13 11:58 ?        00:00:43 nginx: worker process
root      85710  82556  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process

#cat /app/nginx/logs/nginx.pid
85710
可以看出nginx.pid已经变成新master进程号

#cat /app/nginx/logs/nginx.pid.oldbin
82556
nginx.pid.oldbin存放老master进程号。
  • 向老master进程发送WINCH信号。
kill -WINCH `cat /app/nginx/logs/nginx.pid.oldbin`
    • nginx的老master进程收到信号会给所有老worker进程发送信号,老worker执行优雅退出。
    • 老worker收到优雅退出信号后不再接收新请求,只处理存量请求,处理完后进程退出。
#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     82569  82556  1 11:58 ?        00:00:06 nginx: worker process is shutting down
nginx     82570  82556 11 11:58 ?        00:00:43 nginx: worker process is shutting down
root      85710  82556  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process

此过程要不停有请求访问到nginx才能看到worker优雅退出过程,一段时间后存量请求全部处理完毕。

#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
root      85710  82556  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process
  • 检查是否回滚
    • 一段时间后,老woker请求全部处理完,就变成了新老master、新worker共存,此时老master并没有关闭listen sockets,如果新二进制有问题还有办法回滚。
    • 回滚方法:
    • 方法1
    • 向老master发送HUP信号。
    • 老master收到HUP信号会创建worker进程。
    • 向新master发送QUIT信号。
    • 新master收到QUIT会退出所有新worker和新master进程。
    • 方法2
    • 向新master发送TERM信号。
    • nginx新进程收到这个信号,对应master和worker会退出,同时老master会创建出老worker继续工作。
  • 如果不需要回滚,向老master发送QUIT信号。
kill -QUIT `cat /app/nginx/logs/nginx.pid.oldbin`
    • 老master收到这个信号会退出。
#ps -ef | grep nginx
root      85710      1  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process

源码

nginx信号处理函数:ngx_signal_handler

unix domain sockets

典型项目

envoy

mosn

原理概括

linux环境可以使用下面函数在进程间传递fd。

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

mosn为例解析

交互流程

ps:下面代码均以v1.5.0版本为例。

listen fd迁移

涉及Domain Socket:

reconfig.sock记录老进程的监听

listen.sock记录新进程的监听

流程:

  • 老进程启动时候会执行ReconfigureListener函数,这里面监听reconfig.sock并通过写一个字节(uc.Write([]byte{0}))阻塞,直到有新进程启动并执行read才会继续往下执行。
  • 新进程init-->inheritConfig-->IsReconfigure通过uc.Read(buf)触发老进程执行reconfigure流程。
  • 老进程通过reconfig.sock向新进程发送fd。

ReconfigureHandler

sendInheritListeners:老进程将已经存在的 fd 通过 listen.sock 发送给新进程。

shutdownServers:老进程不再接收新连接,并优雅关闭。

WaitConnectionsDone:处理完存量请求后退出。

  • 新进程接收老进程的fd并处理:GetInheritListeners。

长连接迁移

涉及Domain Socket:conn.sock

流程:

  • 新进程启动一个协程运行TransferServer,将监听conn.sock。
  • 老进程通过transferRead和transferWrite进入长链接迁移过程。

参考文档

nginx官方文档:http://nginx.org/en/docs/control.html

MOSN 平滑升级原理解析:https://mosn.io/docs/products/structure/smooth-upgrade/

MOSN 源码解析 - reconfig 机制:https://mosn.io/blog/code/mosn-reconfig-mechanism/

浅谈长连接的平滑重启:https://www.infoq.cn/article/Qfkq8Wk4FtVot46LaVkR?source=app_share

Nginx vs Envoy vs Mosn 平滑升级原理解析:https://ms2008.github.io/2019/12/28/hot-upgrade/