多个进程可以绑定同一个端口号.

SO_REUSEPORT 是什么

默认情况下,一个 IP、端口组合只能被一个套接字绑定,Linux 内核从 3.9 版本开始引入一个新的 socket 选项 SO_REUSEPORT,又称为 port sharding,允许多个套接字监听同一个IP 和端口组合

为了充分发挥多核 CPU 的性能,多进程的处理网络请求主要有下面两种方式:

  • 主进程 + 多个 worker 子进程监听相同的端口
  • 多进程 + REUSEPORT

第一种方最常用的一种模式,Nginx 默认就采用这种方式。主进程执行 bind()、listen() 初始化套接字,然后 fork 新的子进程。在这些子进程中,通过 accept/epoll_wait 同一个套接字来进行请求处理,示意图如下所示。

image.png

这种方式看起来很完美,但是会带来著名的“惊群”问题(thundering herd)。

惊群问题(thundering herd)

计算机中的惊群问题指的是:多进程/多线程同时监听同一个套接字,当有网络事件发生时,所有等待的进程/线程同时被唤醒,但是只有其中一个进程/线程可以处理该网络事件,其它的进程/线程获取失败重新进入休眠。

惊群问题带来的是 CPU 资源的浪费和锁竞争的开销。根据使用方式的不同,Linux 上的网络惊群问题分为 accept 惊群和 epoll 惊群两种。

  • accept 惊群
    • Linux 在早期的版本中,多个进程 accept 同一个套接字会出现惊群问题
  • epoll 惊群

image.png

为了表示打开文件,linux 内核维护了三种数据结构,分别是:

  • 内核为每个进程维护了一个其打开文件的「描述符表」(file descriptor table),我们熟知的 fd 为 0 的 stdin 就是属于文件描述符表。
  • 内核为所有打开文件维护了一个系统级的「打开文件表」(open file table),这个打开文件表存储了当前文件的偏移量,状态信息和对 inode 的指针等信息,父子进程的 fd 可以指向同一个打开文件表项。
  • 最后一个是文件系统的 inode 表(i-node table)

image.png

SO_REUSEPORT 选项基本使用

当一个新请求到来,内核是如何确定应该由哪个 LISTEN socket 来处理?接下来我们来看 SO_REUSEPORT 底层实现原理,

SO_REUSEPORT 源码分析

内核为处于 LISTEN 状态的 socket 分配了大小为 32 哈希桶。监听的端口号经过哈希算法运算打散到这些哈希桶中,相同哈希的端口采用拉链法解决冲突。当收到客户端的 SYN 握手报文以后,会根据目标端口号的哈希值计算出哈希冲突链表,然后遍历这条哈希链表得到最匹配的得分最高的 Socket。对于使用 SO_REUSEPORT 选项的 socket,可能会有多个 socket 得分最高,这个时候经过随机算法选择一个进行处理。

image.png

SO_REUSEPORT 与安全性

  • 只有第一个启动的进程启用了 SO_REUSEPORT 选项,后面启动的进程才可以绑定同一个端口。
  • 后启动的进程必须与第一个进程的有效用户ID(effective user ID)匹配才可以绑定成功。

SO_REUSEPORT 的应用

  • 实现了内核级的负载均衡
  • 支持滚动升级(Rolling updates)

image.png

  1. 新启动一个新版本 v2 ,监听同一个端口,与 v1 旧版本一起处理请求。
  2. 发送信号给 v1 版本的进程,让它不再接受新的请求
  3. 等待一段时间,等 v1 版本的用户请求都已经处理完毕时,v1 版本的进程退出,留下 v2 版本继续服务