内核连接多路复用器

内核连接多路复用器 (KCM) 是一种机制,它为通用应用程序协议通过 TCP 提供基于消息的接口。借助 KCM,应用程序可以使用数据报套接字高效地通过 TCP 发送和接收应用程序协议消息。

KCM 在内核中实现了一个 NxM 多路复用器,如下所示

+------------+   +------------+   +------------+   +------------+
| KCM socket |   | KCM socket |   | KCM socket |   | KCM socket |
+------------+   +------------+   +------------+   +------------+
    |                 |               |                |
    +-----------+     |               |     +----------+
                |     |               |     |
            +----------------------------------+
            |           Multiplexor            |
            +----------------------------------+
                |   |           |           |  |
    +---------+   |           |           |  ------------+
    |             |           |           |              |
+----------+  +----------+  +----------+  +----------+ +----------+
|  Psock   |  |  Psock   |  |  Psock   |  |  Psock   | |  Psock   |
+----------+  +----------+  +----------+  +----------+ +----------+
    |              |           |            |             |
+----------+  +----------+  +----------+  +----------+ +----------+
| TCP sock |  | TCP sock |  | TCP sock |  | TCP sock | | TCP sock |
+----------+  +----------+  +----------+  +----------+ +----------+

KCM 套接字

KCM 套接字提供了多路复用器的用户接口。绑定到多路复用器的所有 KCM 套接字都被认为具有相同的功能,并且可以在不同的套接字中并行执行 I/O 操作,而无需用户空间中线程之间的同步。

多路复用器

多路复用器提供消息转向。在发送路径中,在 KCM 套接字上写入的消息会原子地在适当的 TCP 套接字上发送。类似地,在接收路径中,消息会在每个 TCP 套接字 (Psock) 上构建,并将完整的消息引导到 KCM 套接字。

TCP 套接字和 Psock

TCP 套接字可以绑定到 KCM 多路复用器。为每个绑定的 TCP 套接字分配一个 Psock 结构,该结构保存接收时构造消息的状态以及 KCM 的其他连接特定信息。

连接模式语义

每个多路复用器都假设所有连接的 TCP 连接都连接到同一目标,并且可以在传输时使用不同的连接进行负载均衡。可以使用正常的 send 和 recv 调用(包括 sendmmsg 和 recvmmsg)从 KCM 套接字发送和接收消息。

套接字类型

KCM 支持 SOCK_DGRAM 和 SOCK_SEQPACKET 套接字类型。

消息定界

消息通过 TCP 流发送,其中包含一些应用程序协议消息格式,该格式通常包括一个标头,用于构成消息的框架。接收到的消息的长度可以从应用程序协议标头中推断出来(通常只是一个简单的长度字段)。

必须解析 TCP 流以确定消息边界。伯克利数据包过滤器 (BPF) 用于此。将 TCP 套接字附加到多路复用器时,必须指定 BPF 程序。该程序在开始接收新消息时调用,并获得一个包含到目前为止接收到的字节的 skbuff。它解析消息标头并返回消息的长度。有了这些信息,KCM 将构造指定长度的消息并将其传递到 KCM 套接字。

TCP 套接字管理

当 TCP 套接字附加到 KCM 多路复用器时,数据就绪 (POLLIN) 和写空间可用 (POLLOUT) 事件由多路复用器处理。如果 TCP 套接字上的状态发生更改(断开连接)或其他错误,则会在 TCP 套接字上发布错误,以便发生 POLLERR 事件,并且 KCM 将停止使用该套接字。当应用程序获取 TCP 套接字的错误通知时,它应该从 KCM 中取消附加该套接字,然后处理错误情况(典型的响应是关闭套接字并在必要时创建新连接)。

KCM 将最大接收消息大小限制为附加的 TCP 套接字上的接收套接字缓冲区的大小(套接字缓冲区大小可以通过 SO_RCVBUF 设置)。如果 BPF 程序报告的新消息的长度大于此限制,则会在 TCP 套接字上发布相应的错误 (EMSGSIZE)。BPF 程序还可以强制执行最大消息大小,并在超出最大消息大小时报告错误。

可以为在接收套接字上组装消息设置超时。超时值取自附加的 TCP 套接字的接收超时(这由 SO_RCVTIMEO 设置)。如果计时器在组装完成之前过期,则会在套接字上发布错误 (ETIMEDOUT)。

用户界面

创建多路复用器

新的多路复用器和初始 KCM 套接字通过套接字调用创建

socket(AF_KCM, type, protocol)
  • 类型是 SOCK_DGRAM 或 SOCK_SEQPACKET

  • 协议是 KCMPROTO_CONNECTED

克隆 KCM 套接字

在使用上述套接字调用创建第一个 KCM 套接字后,可以通过克隆 KCM 套接字来为多路复用器创建其他套接字。这是通过 KCM 套接字上的 ioctl 完成的

/* From linux/kcm.h */
struct kcm_clone {
      int fd;
};

struct kcm_clone info;

memset(&info, 0, sizeof(info));

err = ioctl(kcmfd, SIOCKCMCLONE, &info);

if (!err)
  newkcmfd = info.fd;

附加传输套接字

通过对多路复用器的 KCM 套接字调用 ioctl 来执行将传输套接字附加到多路复用器。例如

/* From linux/kcm.h */
struct kcm_attach {
      int fd;
      int bpf_fd;
};

struct kcm_attach info;

memset(&info, 0, sizeof(info));

info.fd = tcpfd;
info.bpf_fd = bpf_prog_fd;

ioctl(kcmfd, SIOCKCMATTACH, &info);

kcm_attach 结构包含

  • fd:正在附加的 TCP 套接字的文件描述符

  • bpf_prog_fd:已下载的已编译 BPF 程序的文件描述符

取消附加传输套接字

从多路复用器取消附加传输套接字很简单。使用 kcm_unattach 结构作为参数完成“取消附加”ioctl

/* From linux/kcm.h */
struct kcm_unattach {
      int fd;
};

struct kcm_unattach info;

memset(&info, 0, sizeof(info));

info.fd = cfd;

ioctl(fd, SIOCKCMUNATTACH, &info);

禁用 KCM 套接字上的接收

setsockopt 用于禁用或启用 KCM 套接字上的接收。禁用接收时,套接字接收缓冲区中任何挂起的消息都会移动到其他套接字。如果应用程序线程知道它将在请求上执行大量工作并且在一段时间内无法处理新消息,则此功能非常有用。示例用法

int val = 1;

setsockopt(kcmfd, SOL_KCM, KCM_RECV_DISABLE, &val, sizeof(val))

用于消息定界的 BPF 程序

可以使用 BPF LLVM 后端编译 BPF 程序。例如,用于解析 Thrift 的 BPF 程序是

#include "bpf.h" /* for __sk_buff */
#include "bpf_helpers.h" /* for load_word intrinsic */

SEC("socket_kcm")
int bpf_prog1(struct __sk_buff *skb)
{
     return load_word(skb, 0) + 4;
}

char _license[] SEC("license") = "GPL";

在应用程序中使用

KCM 加速应用程序层协议。具体来说,它允许应用程序使用基于消息的接口来发送和接收消息。内核提供了必要的保证,即消息是原子地发送和接收的。这减轻了应用程序将基于消息的协议映射到 TCP 流中的许多负担。KCM 还使应用程序层消息成为内核中的工作单元,用于引导和调度,这反过来允许在多线程应用程序中使用更简单的网络模型。

配置

在 Nx1 配置中,KCM 在逻辑上为同一 TCP 连接提供多个套接字句柄。这允许 TCP 套接字上的 I/O 操作之间并行(例如,数据的 copyin 和 copyout 是并行化的)。在应用程序中,可以为每个处理线程打开一个 KCM 套接字,并将其插入 epoll 中(类似于 SO_REUSEPORT 用于允许在同一端口上使用多个侦听器套接字的方式)。

在 MxN 配置中,将建立到同一目标的多个连接。这些用于简单的负载均衡。

消息批处理

KCM 的主要目的是在 KCM 套接字和因此在正常用例中的线程之间进行负载均衡。完美的负载均衡(即将每个接收到的消息引导到不同的 KCM 套接字或将每个发送的消息引导到不同的 TCP 套接字)可能会对性能产生负面影响,因为它不允许建立亲和性。基于组或批量消息进行平衡可能有利于性能。

在传输方面,应用程序可以通过三种方式在 KCM 套接字上批量处理(流水线化)消息。

  1. 在单个 sendmmsg 中发送多个消息。

  2. 发送一组消息,每个消息都有一个 sendmsg 调用,其中除最后一个消息之外的所有消息在 sendmsg 调用的标志中都具有 MSG_BATCH。

  3. 创建由多个消息组成的“超级消息”,并使用单个 sendmsg 发送此消息。

在接收方面,KCM 模块尝试在每次 TCP 就绪回调期间将接收到的消息排队到同一 KCM 套接字。目标 KCM 套接字在 KCM 套接字上的每次接收就绪回调时都会更改。应用程序不需要配置此项。

错误处理

应用程序应包含一个线程来监视 TCP 连接上引发的错误。通常,这将通过将附加到 KCM 多路复用器的每个 TCP 套接字放置在 epoll 中以用于 POLLERR 事件来完成。如果附加的 TCP 套接字上发生错误,KCM 会在套接字上设置 EPIPE,从而唤醒应用程序线程。当应用程序看到错误(可能只是断开连接)时,它应该从 KCM 中取消附加该套接字,然后将其关闭。假定一旦在 TCP 套接字上发布错误,数据流就无法恢复(即,可能在接收消息的过程中发生了错误)。

TCP 连接监视

在 KCM 中,没有办法将消息与其发送或接收时使用的 TCP 套接字相关联(除非只有一个附加的 TCP 套接字)。但是,应用程序会保留一个打开的套接字文件描述符,因此它可以从套接字获取统计信息,这些统计信息可用于检测问题(例如套接字上的高重传率)。