黑科技解密!实现 socket 进程间迁移
今天介绍一个可以拿出去吹牛的功能:实现 socket 句柄在进程之间迁移!为了这篇文章,xjjdog 可算下了苦功夫,半夜还在翻资料。因为需要验证后,才能证明这项技术确实是正确的。
正文。
我们的服务器上,运行着大量的 server 实例(instance)。这些 instance,每个都要承载着数十万的连接和非常繁忙的网络请求。能够把这样的连接数,这样的流量,玩弄于股掌之间,是每个互联网程序员的梦想。
但软件总是要升级的,每当升级的时候,就需要先停掉原来的 instance,然后再启动一个新的。在这一停一起之间,数十秒就过去了,更不要说 JAVA 这种启动时间就能生个孩子的速度了。
传统的做法,是先把这个 instance 从负载均衡上面摘除,然后启动起来再加上;对于微服务来说,就要先隔离,然后启动后再取消隔离。这些操作,对于海量应用来说,就是个噩梦。
1. 零停机更新
有没有一种方法,能够把一个进程所挂载的连接(socket),转移到另外一个进程之上呢?这样,我在升级的时候,就可可以先启动一个升级版本的进程,然后把老进程的 socket,one by one 的给转移过去。
实现零停机更新。
这个是可以的。Facebook 就实践过类似的技术,它们把这项技术,叫做 Socket Takeover。千万别用百度搜这个关键字,你得到的可能是一堆垃圾。
这么牛 x 的技术,还这么有用,为什么就没人科普呢?别问我,我也不知道,可能大家现在都在纠结怎么研究茴香豆的茴字写法,没时间干正事吧。
那今天就由 xjjdog 来介绍一下吧,顺便增加一下大家以后的吹牛资本。
这个牛 x 的功能,是由 Linux 一对底层的系统调用函数所实现的:sendmsg()和 recvmsg()。我们一般在发送网络数据包的时候,一般会使用 send 函数,但 send 函数只有在 socket 处于连接状态时才可以使用;与之不同的是,sendmsg 在任何时候都可以使用。
2. 技术要点
在 c 语言网络编程中,首先要通过 listen 函数,来注册监听地址,然后再用 accept 函数接收新连接。比如:
我们首先要做的,就是把 listen_fd,从一个进程,传递到另外一个进程中去。怎么发送呢?肯定是要通过一个通道的。在 Linux 上,那就是 UDS,全称 Unix Domain Sockets。
2.1 Unix Domain Sockets 监听
UDS(Unix Domain Sockets)在 Linux 上的表现,是一个文件。相比较于普通 socket 监听在端口上,一个进程也可以监听在一个 UDS 文件上,比如/tmp/xjjdog.sock。由于通过这个文件进行数据传输,并不需要走网卡等物理设备,所以通过 UDS 传输数据,速度是非常快的。
但今天我们不关心它有多快,而是关心它多有用。通过 bind 函数,我们同样可以通过这个文件接收连接,就像端口接收连接一样。
这样。其他的进程,就可以通过两种不同的方式,来连接我们的服务。
通过端口:进行正常的服务,输出正常的业务数据。执行正常业务
通过 UDS:开始接收 listen_fd 和 accept_fd 们。执行不停机迁移 socket 业务
2.2 fd 迁移技术要点
怎么迁移呢?我们关键看第二步。
实际上,当新升级的服务通过 UDS 连接上来,我们就开始使用 sendmsg 函数,将 listen_fd 给转移过去。
我们来看一下 sendmsg 这个函数的参数。
socket 可以理解为我们的 UDS 连接。关键在于 msghdr 这个结构体。
其中, msg_iov 表示要正常发送的数据,比如 HelloWord;除此之外,还有两个 ancillary (附属的) 的变量,提供了附加的功能,那就是变量 msg_control 和 msg_controllen。其中,msg_control 又指向了另外一个结构体 cmsghdr。
在这个结构体中,有一个叫做 cmsg_type 的成员变量,是我们实现 socket 迁移的关键。
它共有三个类型。
SCM_RIGHTS
SCM_CREDENTIALS
SCM_SECURITY
其中,SCM_RIGHTS 就是我们所需要的,它允许我们从一个进程,发送一个文件句柄到另外一个进程。
依靠 sendmsg 函数,socket 句柄就发送到另外一个进程了。
3. 接收和还原
同样的,recvmsg 函数,将会接收这部分数据,然后将其还原成 cmsghdr 结构体。然后我们就可以从 cmsg_data 中获取句柄列表。
为什么能这么做呢?因为 socket 句柄,在某个进程里,其实只是一个引用。真正的 fd 句柄,其实是放在内核中的。所谓的迁移,只不过是把一个指针,从一个进程中去掉,再加到另外一个进程中罢了。
fd 句柄的属性,有两种情况。
监听 fd,直接调用 accept 函数作用在 fd 上即可
普通 fd,需要将其还原成正常的 socket
图片来自论文:(Zero Downtime Release: Disruption-free Load Balancing of a Multi-Billion User Website)
对于普通 fd,肯定要调用与原新连接到来时相同的代码逻辑。所以,一个大体的迁移过程,包括:
首先迁移 listener fd 到新进程,并开启监听,以便新进程能快速接收新的请求。如果我们开启了 SO_REUSEADDR 选项,新老服务甚至能够一起进行服务
等待新进程预热之后,停掉原进程的监听
迁移原老进程中的大量 socket,这些 socket 可能有数万条,最好编码能看到迁移进度
新进程接收到这些 socket,陆续将其还原为正常的连接。相当于略过了 accept 阶段,直接就获取了 socket 列表
迁移完毕,老进程就空转了,此时可以安全地停掉
4. End
这是一项黑科技,其实已经在一些主流的应用中使用了。你会看到一些非常眼熟的软件,这项功能是它们的一大卖点。比如 HAProxy,运行在 4 层网络的负载均衡;比如 Envoy,Istio 默认的数据平面软件,使用类似的技术完成热重启。
其实,在 servicemesh 的推进过程中,proxy 的替换,也会使用类似的技术,比如 SOFA。对于 golang 和 C 语言来说,由于 API 暴露得比较好,这种功能可以很容易地实现;但在 Java 中,却有不少的困难,因为 Java 的跨平台特性不会做这种为 Linux 定制的 API。
可以看到,sendmsg 和 recvmsg 这两个函数,可以实现的功能非常的酷。它比较适合无状态的 proxy 服务,如果服务内有状态存留,这种迁移并不见得安全,当然也可以尝试把此项技术运用在一些中间件上。但无论如何,这种黑科技,有一种别样的暴力美,肯定会把 windows server 用户给馋哭了。
如果您觉得文章对您有帮助,可以点赞评论转发支持一下~蟹蟹!
评论