TCP SO_REUSEPORT 套接字分片与零停机热重启深度实战
一、为什么需要 SO_REUSEPORT
在 Linux 高并发网络编程中,经典的 C10K/C100K 问题催生了大量解决方案:单线程 epoll、线程池 accept、SO_REUSEPORT 等。其中 SO_REUSEPORT 从 Linux 3.9(2013 年)引入之后,逐渐成为生产环境中的首选方案。
1.1 Accept 惊群:被误解多年的"问题"
早期 Linux 版本中,多个进程/线程阻塞在同一个 listen socket 上调用 accept(),当新连接到来时,内核会唤醒所有等待者——但只有一个能成功获取连接。这就是所谓的"accept 惊群"(thundering herd)。
然而很多人不知道的是,Linux 2.6 之后 accept 惊群问题基本已经被内核修复了。内核使用等待队列的 exclusive 模式,保证每次只唤醒一个等待者。真正的性能瓶颈不在于惊群,而在于:
- 单点 accept 的串行化:所有线程共享同一个 accept queue,必须互斥访问
- 负载不均衡:内核唤醒策略可能导致某些线程过载,某些线程空闲
- 缓存局部性差:连接在不同 CPU 核间频繁跳转
1.2 SO_REUSEPORT 的核心思路
SO_REUSEPORT 的本质是:允许多个 socket 绑定到同一个 IP:PORT 组合,内核在 TCP 连接建立阶段直接将连接分发给对应的 listen socket。
这意味着:
- 每个工作进程/线程拥有独立的 listen socket
- accept 操作不再需要跨进程/线程同步
- TCP 三次握手的 CPU 开销天然分布在多个核上
- 配合 CPU affinity,可以实现最佳缓存局部性
二、内核实现深度剖析
2.1 数据结构:reuseport_array
在内核中,绑定在同一端口上的所有 SO_REUSEPORT socket 通过一个共享的 struct sock_reuseport 关联。关键数据结构定义在 net/core/sock_reuseport.c:
struct sock_reuseport {
struct rcu_head rcu;
u16 max_socks; /* 组内最大 socket 数 */
u16 num_socks; /* 当前 socket 数 */
/* 用于 BPF 的匹配程序 */
struct bpf_prog __rcu *prog;
/* socket 成员数组 */
struct sock *socks[0]; /* 变长数组 */
};
当应用程序对 N 个 socket 设置 SO_REUSEPORT 时,它们共享同一个 sock_reuseport 实例,内核通过 socks 数组管理这些 socket 的引用。
2.2 连接分发算法
新 TCP 连接进入时,内核在 inet_lookup_listener() 中调用 reuseport_select_sock() 来确定接收该连接的 listen socket:
TCP SYN 到达
↓
inet_csk_route_req() → 确定目标路由
↓
inet_lookup_listener() → 端口查找
↓
reuseport_select_sock() → 选择目标 socket
↓
inet_csk_reqsk_queue_add() → 添加到该 socket 的半连接队列
↓
完成三次握手后进入该 socket 的 accept queue
关键代码路径(简化版):
static struct sock *reuseport_select_sock(struct sock *sk, u32 hash, struct sk_buff *skb)
{
struct sock_reuseport *reuse;
struct bpf_prog *prog;
struct sock *sk2 = NULL;
u16 socks;
rcu_read_lock();
reuse = rcu_dereference(sk-

发表评论 取消回复