进程间传递网络连接(文件描述符)
潘忠显 / 2022-05-29
本文介绍通过实验(代码仓库)模拟两个 Server 之间传输网络连接。做个实验的主要背景:
- 进程间传递网络连接可以被用于热重启。之前的文章 《Envoy网关热重启原理分析》中介绍过,但 Envoy 代码较重,比较难以理解相关概念。
- 与客户端有长连接建立的服务可能不适合滚动更新
- 了解 UDS(Unix Domain Socket) 的使用方法。 APUE 第 17 章有介绍 UDS 及通过 UDS 传递文件描述符的具体做法。
项目使用 Bazel 构建,依赖 libevent 事件引擎库,也用到 APUE 示例代码(以库的方式使用)。
0. 场景说明
启动两个 Server,分别监听 1001 和 1002 端口,用于建立 Client 来的连接。每个 Server 处理完一条消息便将 网络连接/文件描述符 传递给另外一个服务进程,继续后续处理:
为了模拟实际中最常用的"带Buffer"的连接情况,也就是我们常说的 Stream RPC,这里还额外模拟一个客户端,将请求批量发送出去,然后服务只要能正确的切包,也能够支持每条消息交替由不同服务进程处理:
观察连接以及日志,可以观察到连接被转移的过程。下图中 Server1 (pid:32396) 建立的 fd 被传送到了 Server2 (pid:32397):
1. Server
调用 sh start_server.sh
会构建 Server 程序,并拉起两个进程,分别监听 1000 和 1001 端口,同时会创建并监听对应端口的一个命名 UDS。两个 Server进程的逻辑是完全一样的,只是监听的端口不同:
-
创建 TCP socket 并调用
listen()
监听 TCP 端口accept()
建立客户端连接
-
创建 UDS socket 并调用
listen()
监听recv_fd()
接收另外一个 Server 进程发过来的连接
-
在连接 fd 上调用
recv()
读一个请求长度 -
在连接 fd 上调用
recv()
读一个请求内容 -
在该连接上调用
send()
发送返回,返回内容中有标识是哪个 Server -
通过
send_fd()
将连接发送给另外一个 Server 进程
额外解释一下上边流程:TCP listen 和 UDS listen 的结果都能得到一个新的连接,而之后的处理则是完全相同的,即收-收-发-转移fd。
【接收协议类型】 为了能够恰好去切包。针对不能确切的切包的协议,后边会有讨论。
// 仅为协议描述
struct proto {
char len;
char data[len];
};
【返回内容】 为带端口的纯文本:
rsp from port: ${服务监听端口}
【日志文件】 每个server都会写对应端口的log文件,1000.log 和 1001.log 会接收到相同条数的内容。
【监听端口】 这里为了简便,所以让他们监听了不同的端口,其实 Linux 允许多个进程监听同一个端口——真是场景中才有意义。
2. 一元 RPC Client
调用 sh start_unary_client.sh
会构建 Unary RPC 请求的 Client,其基本逻辑:
- 并发启动 100 个 client,即建立100连接,每个 client 重复执行以下发送请求和接收返回 1000 次
- 发送一条请求
- 等待一条返回
- 继续发送下一条请求
- 继续等待下一条请求
【校验内容】 校验每个 Client 的每次获得的返回与前一次返回来自于不同的处理进程:
assert((port ^ client->last_server_port) == 1);
client->last_server_port = port;
【屏幕输出】 让一个 Client 打印每次处理服务的进程端口信息:
if (client->index == 0) {
printf("port: %d, last_port: %d\n", port, client->last_server_port);
}
可以直观的看到每个消息处理完都有发生连接的传送:
3. 流 RPC Client
使用流 Client 测试是为了验证当操作系统已经接收到完整消息之后,即内容在内核缓存时,是否能够正常的进行连接传递。
调用 sh start_stream_client.sh
会构建 Stream RPC 请求的 Client。其处理逻辑是:
- 一次性发送
1000
条请求 - 等待所有返回,直到
EAGAIN
之后 5 秒超时,关闭连接 - 校验接收数据
【校验内容】 校验总返回条数为 1000
,且每次返回来源于两个不同的 Server 且严格交替:
while (pos != std::string::npos) {
port = get_current_port(); // 省略处理过程
assert((port ^ last_port) == 1);
last_port = port;
}
assert(counter == kRepeateTime);
4. 其他思考
- 两个 Server 其实可以监听相同的 TCP 端口(
SO_REUSEPORT
选项,Linux 3.9 之后支持) - 无法恰好切好的协议,可以在传递 fd 的同时,将已经读出来的内容同时通过 UDS 发送给另外的 Server