热重启概述

热重启

热重启背景是在高访问量搞并发的网站中,对于服务的升级重启,会造成对正在通信的客户端的影响,从而影响了正在通信的客户端的访问,为了解决这个问题就提出了一种优雅、零宕机的解决方案即热重启。热重启的主要步骤如下;

  1. 监听优雅升级或热重启的信号。
  2. 收到信号后fork子进程,子进程加载新的执行进程,父进程将当前监听的socket文件描述符传递个子进程(此时就需要将close_on_exec标志位清除掉)。
  3. 子进程重新通过文件描述符生成sock并监听该socket,此时相当于有两个进程同时监听了一个文件描述符,此时父子进程都可以进行请求处理。
  4. 子进程启动监听成功之后,父进程就停止接受新的连接,等待旧的连接处理完成或者超时之后父进程退出,热重启完成。

通过上述的流程就可以达到在热更新的过程中,能够将旧的请求处理完成,从而平滑升级。在golang中有提供现成的第三方库如endless等,当然本文并没有讨论通过守护进程工具来实现的平滑升级方案,因为涉及到父子进程的生成,所有在有守护进程的过程中如上的步骤不太适用,此时可以通过使用master/worker的方案,来单独管理worker热更新worker来达到平滑升级的目的,从步骤描述也可知道,热升级的方案需要代码的改动来适应不同服务端事件响应的模型。不过这不妨碍我们去学习与了解其中原理,因为golang提供了现成的库,本文就采用python来实现一个简单的热重启的一个模型。

代码编写

环境是python3.7,操作系统是centos7.1。

#!/bin/python
import os
import socket
import sys
import fcntl
import signal
import time

is_grace = False
is_stop_master = False
is_exit = False
is_request_done = False

sock = None
msg = None


if os.environ.get("GRACEFUL"):
    fd = os.environ.get("GRACEFUL")
    print("GRACEFUL   ", os.environ.get("GRACEFUL"))
    is_grace = True
    sock = socket.socket(fileno=int(fd))
    msg = b'child'
else:
    sock = socket.socket()
    sock.bind(('localhost', 1234))
    sock.listen(100)

    print(sock.fileno(), sock)
    msg = b'parent'


def hup(signum, frame):
    global is_grace
    is_grace = True
    env = os.environ.copy()
    argvs = sys.argv.copy()
    # argvs.append("w")
    argvs.insert(0, sys.executable)
    print(argvs, sys.executable)

    pid = os.fork()
    if pid == 0:
        print("worker  ", pid)
        env["GRACEFUL"] = str(sock.fileno())
        os.execve(sys.executable, argvs, env)
        print("unreachable")
    print("master")
    return


def term(signum, frame):
    # 父进程开始退出
    print("*" * 100)
    # 等待数据处理完成之后就退出
    global is_stop_master, is_exit
    is_stop_master = True
    is_exit = True


def handle_request(conn):
    # 处理请求
    # 模拟处理时间
    print("should send msg ", msg)
    global is_request_done
    is_request_done = False
    time.sleep(20)
    conn.send(msg)
    conn.close()
    is_request_done = True


def main():
    signal.signal(signal.SIGHUP, hup)
    signal.signal(signal.SIGTERM, term)
    print(os.environ.get("GRACEFUL"))
    print(sys.argv, sys.executable)
    while True:
        print("os pid   ", os.getpid(), os.getppid(), is_grace, sock)
        if is_grace is False and is_exit is False:
            conn, addr = sock.accept()
            print(" parent recv  ", conn, addr)
            handle_request(conn)
        elif is_grace is True and is_exit is False:
            # 通知父进程可以退出了 此时通过信号来通知
            print("-"*100)
            global is_stop_master
            if is_stop_master is False:
                is_stop_master = True
                os.kill(os.getppid(), signal.SIGTERM)
            conn, addr = sock.accept()
            print("child   recv  ", conn, addr)
            handle_request(conn)
        else:
            if is_request_done:
                print("master should  exit")
                sys.exit(0)
            else:
                print(" parent  should   wait and exit  ")
                time.sleep(3)


if __name__ == '__main__':
    main()

代码量不多,其中就是通过环境变量来传递文件描述符,然后再子进程中来生成一个sock,此时开开心心的来运行一把,使用的client的脚本来测试一下。

import socket
import time

sock = socket.socket()
sock.connect(('localhost', 1234))

sock.send(b"testword")
print(sock.recv(10000))

此时我们先启动server,启动完成之后再启动client,此时可以从client上顺利的打印出来parent的数据,此时我们通过给server进程发送HUP信号来开始热重启。

kill -hup 526982

此时查看父进程缺报错了。

Traceback (most recent call last):
  File "grace_restart_v3.py", line 22, in <module>
    sock = socket.socket(fileno=int(fd))
  File "/root/.pyenv/versions/3.7.3/lib/python3.7/socket.py", line 151, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
OSError: [Errno 9] Bad file descriptor: 'family'

此时,百思不得其解,一顿查找,我们通过如下命令来查看调用了哪些系统调用(多谢同事的分析调试才注意到这个问题)

strace python grace_restart_v3.py 1>> trace.txt 2>>trace.txt

在trace.txt文件中可以发现有如下这行内容;

mmap(NULL, 50624, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f25be573000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340$\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2151672, ...}) = 0

可以清晰的看出在我们创建sock监听1234的时候默认传入的是O_CLOEXEC,即在创建子进程的时候,会关闭继承自父进程的fd,所以我们需要将该标志位去掉。修改后的代码如下;

#!/bin/python
import os
import socket
import sys
import fcntl
import signal
import time

is_grace = False
is_stop_master = False
is_exit = False
is_request_done = False

sock = None
msg = None


if os.environ.get("GRACEFUL"):
    fd = os.environ.get("GRACEFUL")
    print("GRACEFUL   ", os.environ.get("GRACEFUL"))
    is_grace = True
    sock = socket.socket(fileno=int(fd))
    msg = b'child'
else:
    sock = socket.socket()
    sock.bind(('localhost', 1234))
    sock.listen(100)
	
    flags = fcntl.fcntl(sock.fileno(), fcntl.F_GETFD)
    flags &= (~fcntl.FD_CLOEXEC)
    fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, flags)
    
    print(sock.fileno(), sock)
    msg = b'parent'


def hup(signum, frame):
    global is_grace
    is_grace = True
    env = os.environ.copy()
    argvs = sys.argv.copy()
    # argvs.append("w")
    argvs.insert(0, sys.executable)
    print(argvs, sys.executable)

    pid = os.fork()
    if pid == 0:
        print("worker  ", pid)
        env["GRACEFUL"] = str(sock.fileno())
        os.execve(sys.executable, argvs, env)
        print("unreachable")
    print("master")
    return


def term(signum, frame):
    # 父进程开始退出
    print("*" * 100)
    # 等待数据处理完成之后就退出
    global is_stop_master, is_exit
    is_stop_master = True
    is_exit = True


def handle_request(conn):
    # 处理请求
    # 模拟处理时间
    print("should send msg ", msg)
    global is_request_done
    is_request_done = False
    time.sleep(20)
    conn.send(msg)
    conn.close()
    is_request_done = True


def main():
    signal.signal(signal.SIGHUP, hup)
    signal.signal(signal.SIGTERM, term)
    print(os.environ.get("GRACEFUL"))
    print(sys.argv, sys.executable)
    while True:
        print("os pid   ", os.getpid(), os.getppid(), is_grace, sock)
        if is_grace is False and is_exit is False:
            conn, addr = sock.accept()
            print(" parent recv  ", conn, addr)
            handle_request(conn)
        elif is_grace is True and is_exit is False:
            # 通知父进程可以退出了 此时通过信号来通知
            print("-"*100)
            global is_stop_master
            if is_stop_master is False:
                is_stop_master = True
                os.kill(os.getppid(), signal.SIGTERM)
            conn, addr = sock.accept()
            print("child   recv  ", conn, addr)
            handle_request(conn)
        else:
            if is_request_done:
                print("master should  exit")
                sys.exit(0)
            else:
                print(" parent  should   wait and exit  ")
                time.sleep(3)


if __name__ == '__main__':
    main()

此时我们重新运行server,然后打开两个终端分别运行client脚本。

(dbenv) [root@node202 wuzh]# python grace_restart_v4.py
3 <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1234)>
None
['grace_restart_v4.py'] /root/.pyenv/versions/dbenv/bin/python
os pid    527283 523154 False <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1234)>

(dbenv) [root@node202 wuzh]# python grace_restart_client.py
b'parent'
(dbenv) [root@node202 wuzh]# python grace_restart_client.py
b'parent'

此时从终端输出可看出都是输出parent,接着我们来测试一下热更新,因为在代码中显示的让处理的请求sleep了20秒,

此时操作步骤如下;

  1. 首先先执行client的脚本执行,python grace_restart_client.py。
  2. 在20秒内立马执行hup信号发送,对server进程进行信号发送,kill -hup 527283。
  3. 接着再执行python grace_restart_client.py。

此时执行的结果就是第一步中返回的数据未parent,证明在处理的过程中还是按照旧的逻辑执行,在第三部中的返回数据为child,即是按照新的加载的业务逻辑返回。测试的过程中按照如上步骤就可以检验一下热更新的逻辑。但是在发送hup信号之后,在父进程退出之后,子进程的父进程就变成了1号进程,这也印证了再使用守护进程工具的时候,会出现守护工具会以为对应的监控的程序退出而重新启动的问题,不过这可以再实现热更新的代码逻辑中避开。

总结

本文只是从简单实践的角度来进行了热更新原理的实践,从实践中可知,实现热更新的逻辑对代码有一定的改动并需要适配不同的事件响应模型,并且需要在守护进程等方面做额外的工作,实现起来代码相对复杂,考虑的因素较多。由于本人才疏学浅,如有错误请批评指正。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页