Golang 优雅重启长连接服务


优雅重启长连接服务

目前一些设备长连接服务,每次发布新版本都需要踢掉连接,然后再发布,本文研究go实现长连接服务器的优雅重启

我们要解决的事情:

  • 如何做到不中断接收连接
  • 如何做到已有连接不中断

第一个问题,如何做到不中断接受连接

看linux源码

https://elixir.bootlin.com/linux/1.0/source/net/socket.c#L537

https://elixir.bootlin.com/linux/1.0/source/net/inet/sock.c#L1061

// linux-1.0/net/socket.c 537
/*
 * Bind a name to a socket. Nothing much to do here since its
 * the protocol's responsibility to handle the local address.
 */
static int sock_bind(int fd, struct sockaddr *umyaddr, int addrlen)
{
  struct socket *sock;
  int i;

  DPRINTF((net_debug, "NET: sock_bind: fd = %d\n", fd));
  if (fd < 0 || fd >= NR_OPEN || current->filp[fd] == NULL)
								return(-EBADF);
  if (!(sock = sockfd_lookup(fd, NULL))) return(-ENOTSOCK);
  if ((i = sock->ops->bind(sock, umyaddr, addrlen)) < 0) {
	DPRINTF((net_debug, "NET: sock_bind: bind failed\n"));
	return(i);
  }
  return(0);
}
// linux-1.0/net/inet/sock.c 1061

static intinet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len){
    ...
    outside_loop:
      for(sk2 = sk->prot->sock_array[snum & (SOCK_ARRAY_SIZE -1)];
                        sk2 != NULL; sk2 = sk2->next) {
    #if 	1	/* should be below! */
        if (sk2->num != snum) continue;
    /*	if (sk2->saddr != sk->saddr) continue; */
    #endif
        if (sk2->dead) {
            destroy_sock(sk2);
            goto outside_loop;
        }
        if (!sk->reuse) {
            sti();
            return(-EADDRINUSE);
        }
        if (sk2->num != snum) continue;		/* more than one */
        if (sk2->saddr != sk->saddr) continue;	/* socket per slot ! -FB */
        if (!sk2->reuse) {
            sti();
            return(-EADDRINUSE);
        }
    }
    ...
}
  • sock_array是一个链式哈希表,保存着各端口号的sock结构
  • bind的时候会检测要绑定的地址和端口是否合法以及已被绑定, 如果发版时另一个进程和旧进程没有关系,则bind会返回错误Address already in use
  • 若旧进程fork出新进程,新进程和旧进程为父子关系,新进程继承旧进程的文件表,本身”本进程”就已经监听这个端口了,则不会出现上面的问题

第二个问题,如何做到已有连接不中断

  • 新进程继承旧进程的用于连接的fd,并且继续维持与客户端的心跳
  • linux提供了unix域套接字可用于socket的传输, 新进程起来后通过unix socket通信继承旧进程所维护的连接
#include 
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

发送端调用sendmsg发送文件描述符,接收端调用revmsg接收文件描述符

两进程共享同一打开文件表,这与fork之后的父子进程共享打开文件表的情况完全相同

Demo 实现

  • 进程每次启动时必须check有无继承socket(尝试连接本地的unix server,如果连接失败,说明是第一次启动,否则可能有继承的socket),如果有,就将socket加入到自己的连接池中, 并初始化连接状态
  • 旧进程监听USR2信号(通知进程需要重启,使用信号、http接口等都可),监听后动作
  1. 监听Unix socket, 等待新进程初始化完成,发来开始继承连接的请求
  2. 使用旧进程启动的命令fork一个子进程(发布到线上的新二进制)。
  3. accept到新进程的请求,关闭旧进程listener(保证旧进程不会再接收新请求,同时所有connector不在进行I/O操作。
  4. 旧进程将现有连接的socket,以及连接状态(读写buffer,connect session)通过 unix socket发送到新进程。
  5. 最后旧进程给新进程发送发送完毕信号,随后退出

以下是简单实现的demo, demo中实现较为简单,只实现了文件描述符的传递,没有实现各连接状态的传递

server

client

References

https://blog.csdn.net/weixin_39519619/article/details/112672312
https://www.luozhiyun.com/archives/584


文章作者: 江湖义气
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 江湖义气 !
  目录