本文转载自微信公众号“小姐姐的味道”,作者小姐姐养的狗。转载本文请联系味觉小姐公众号。今天介绍一个可以用来吹牛的功能:实现进程间socket句柄的迁移!为了这篇文章,xjjdog辛苦了,半夜还在翻资料。因为需要验证才能证明技术确实是正确的。单词。在我们的服务器上,运行着大量的服务器实例(instance)。这些实例中的每一个都承载着数十万个连接和非常繁忙的网络请求。能把玩这么多的连接,这么大的流量,是每一个互联网程序员的梦想。但是软件总是需要升级的。升级时需要先停止原有实例,再启动新实例。这一停一停之间,几十秒过去了,更不用说JAVA在一个启动时间内能生出孩子的速度了。传统的方法是先从负载均衡器中移除实例,然后再启动并添加;对于微服务,需要先隔离,启动后再取消隔离。这些操作对于海量应用来说是一场噩梦。1、有没有办法零宕机更新,将一个进程挂载的连接(socket)转移到另一个进程?这样,我升级的时候,可以先启动一个升级版本的进程,然后再把老进程的socket,一个一个的转移。实现零停机更新。这个有可能。Facebook也实践过类似的技术,他们把这个技术叫做SocketTakeover。不要用百度搜索这个关键字,你可能会得到一堆垃圾。这么牛逼的技术还那么好用,怎么没人科普一下?别问我,我不知道,可能大家都在纠结怎么研究茴香豆的写法,没时间做正事。那么今天就让xjjdog来介绍一下,顺便增加你以后吹牛的资本。这个很棒的函数是由Linux中的一对低级系统调用函数实现的:sendmsg()和recvmsg()。我们一般在发送网络包的时候使用send函数,但是send函数只能在socket连接的时候使用;不同的是sendmsg可以随时使用。2、技术要点在c语言的网络编程中,首先通过listen函数注册监听地址,然后使用accept函数接收新的连接。例如:intlisten_fd=socket(addr->ss_family,SOCK_STREAM,0);...bind(listen_fd,(structsockaddr*)addr,addrlen);...intaccept_fd=accept(fd,(structsockaddr*)&addr,&addrlen);intaccept_fd=accept(fd,(structsockaddr*)&addr,&addrlen);我们需要做的第一件事是将listen_fd从一个进程传递到另一个进程。如何发送?它必须通过一个通道。在Linux上,也就是UDS,UnixDomainSockets的全称。2.1UnixDomainSockets监控Linux上的UDS(UnixDomainSockets)性能,是一个文件。相对于普通socket监听端口,一个进程还可以监听一个UDS文件,比如/tmp/xjjdog.sock。由于通过该文件传输数据不需要网卡等物理设备,因此通过UDS传输数据的速度非常快。但是今天我们不关心它有多块,而是它有多有用。通过bind函数,我们也可以通过这个文件接收连接,就像端口接收连接一样。structsockaddr_unaddr;char*path="/tmp/xjjdog.sock";interr,fd;fd=socket(AF_UNIX,SOCK_STREAM,0);memset(&addr,0,sizeof(structsockaddr_un));addr.sun_family=AF_UNIX;strncpy(addr.sun_path,path,strlen(path));addrlen=sizeof(addr.sun_family)+strlen(path);err=bind(fd,(structsockaddr*)&addr,addrlen);...accept_fd=accept(fd,(structsockaddr*)&addr,&addrlen);其他进程可以通过两种不同的方式连接到我们的服务。通过端口:进行正常的业务,输出正常的业务数据。通过UDS进行正常业务:开始接收listen_fd和accept_fd。进行socket业务的不间断迁移2.2如何迁移fd迁移的技术要点?我们主要看第二步。实际上,当新升级的服务通过UDS连接上后,我们就开始使用sendmsg函数将listen_fd传递给它。我们来看看sendmsg函数的参数。ssize_tsendmsg(intsocket,conststructmsghdr*消息,intflags);socket可以理解为我们的UDS连接。关键在于msghdr结构。structmsghdr{void*msg_name;/*optionaladdress*/socklen_tmsg_namelen;/*sizeofaddress*/structiovec*msg_iov;/*scatter/gatherarray*/intmsg_iovlen;/*#elementsinmsg_iov*/void*msg_control;/*辅助数据,见下文*/socklen_tmsg_con/*ancillarydatabufferlen*/intmsg_flags;/*flagsonreceivedmessage*/};其中msg_iov表示正常发送的数据,如HelloWord;此外,还有两个辅助(附加)变量提供附加功能,即变量msg_control和msg_controllen。其中,msg_control指向另一个结构体cmsghdr。structcmsghdr{socklen_tcmsg_len;/*databytecount,includingheader*/intcmsg_level;/*originatingprotocol*/intcmsg_type;/*protocol-specifictype*//*followedby*/unsignedcharcmsg_data[];};在这个结构体中,有一个成员叫cmsg_type变量,是我们socket迁移的关键。它有三种类型。SCM_RIGHTSSCM_CREDENTIALSSCM_SECURITY其中,SCM_RIGHTS是我们所需要的,它允许我们将一个文件句柄从一个进程发送到另一个进程。structmsghdrmsg;...structcmsghdr*cmsg=CMSG_FIRSTHDR(&msg);cmsg->cmsg_level=SOL_SOCKET;cmsg->cmsg_type=SCM_RIGHTS;//socketfd列表,在cmsg_data上设置int*fds=(int*)CMSG_DATA(cmsg);根据sendmsg函数,套接字句柄被发送到另一个进程。3.接收和恢复同样,recvmsg函数会接收到这部分数据,然后将其恢复到cmsghdr结构中。然后我们可以从cmsg_data中获取句柄列表。为什么可以这样做?因为套接字句柄,在某个进程中,其实只是一个引用。真正的fd句柄其实是放在内核中的。所谓迁移,无非就是从一个进程中去掉一个指针,添加到另一个进程中。fd句柄的属性有两种情况。监听fd,直接调用accept函数作用于fd,可以使用普通的fd,需要恢复成普通的socket镜像来自论文:(零宕机发布:百亿用户的无中断负载均衡Website)对于普通的fd,一定要调用和原来newconnection来的时候一样的代码逻辑。因此,一般的迁移过程包括:首先将监听fd迁移到新进程,开启监听,使新进程能够快速接收新请求。如果我们开启SO_REUSEADDR选项,甚至可以让新老服务一起服务,等待新进程预热,然后停止对原进程的监听,迁移大量原老进程的socket。这些插座可能有数万个。最好看代码直到迁移进度,新的进程接收到这些socket,并陆续恢复正常连接。相当于跳过accept阶段,直接获取socket列表。迁移完成后,旧进程将处于空闲状态。这时候就可以安全的停下来了。4.结束这是一个黑科技,其实已经在一些主流应用中使用了。.你会看到一些非常熟悉的软件,这个功能对他们来说是一个很大的卖点。例如运行在四层网络上的负载均衡器HAProxy;例如,Istio默认的数据平面软件Envoy就使用了类似的技术来完成热重启。其实在servicemesh的推广过程中,代理替换也会用到类似的技术,比如SOFA。对于golang和C语言,由于API更好的暴露,这个功能很容易实现;但是在Java中,有很多困难,因为Java的跨平台特性不会为LinuxAPI做这种定制。可以看到,sendmsg和recvmsg这两个函数可以实现很酷的功能。更适合无状态的代理服务。如果服务中有状态保留,这种迁移不一定是安全的。当然你也可以尝试把这个技术应用到一些中间件上。但不管怎么说,这个黑科技有着别样的暴力美感,绝对会让windowsserver的用户哭笑不得。作者简介:品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。
