1. 问题描述
一个进程监听端口,经验告诉我们,如果多次启动一个进程会报错:“Address already in use!"。这是由于bind函数导致的,由于该端口号已经被第一个进程监听了。有哪些方法可以实现多个进程监听同一个端口呢?
2. 方案一:fork
只要在绑定端口号(bind函数)之后,监听端口号之前(listen函数),用fork()函数生成子进程,这样子进程就可以克隆父进程,达到监听同一个端口的目的,而且还相互竞争,提高程序效率。
这里要注意的是,TCP三次握手创建连接是不需要服务进程参数的,而服务进程仅仅要做的事调用accept将已建立的连接构建对应的连接套接字connfd
bind()
fork()
listen()
2.1 惊群现象
多个服务进程同时阻塞在accept等待监听套接字已建立连接的信息,那么当内核在该监听套接字上建立一个连接,那么将同时唤起这些处于accept阻塞的服务进程,从而导致“惊群现象”的产生,唤起多余的进程间影响服务器的性能(仅有一个服务进程accept成功,其他进程被唤起后没抢到“连接”而再次进入休眠)。
2.2 惊群问题
惊群会导致资源竞争,对于操作系统来说,多个进程/线程在等待同一资源时,也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,会造成以下后果:
- 系统对用户进程/线程频繁的做无效的调度、上下文切换,系统性能大打折扣。
- 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
2.3 惊群解决
2.3.1 方法一
在Linux 2.6版本之前,监听同一个socket的进程会挂在一个等待队列上,当请求到来时,会唤醒所有等待的子进程。
当时可以使用锁解决这种惊群问题。
for(;;) {
lock();
int client = accept(...);
unlock();
if (client < 0) continue;
...
}
2.3.2 方法二
在Linux 2.6版本之后,通过引入一个标记位,解决掉了惊群问题。内核开发者增加了一个“互斥等待”选项。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
1)当一个进程加入等待队列时,如果该进程有 WQ_FLAG_EXCLUSEVE 标志置位,它被添加到等待队列的尾部。没有这个标志,则添加到队列开始。
2)当 wake_up 在一个等待队列上被调用时,它会唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
也就是说,对于互斥等待的行为,比如对一个Socket,多线程阻塞accept时,系统内核只会唤醒所有正在等待此事件的队列的第一个,队列中的其他进程则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket时的惊群问题。
3. 方案二:SO_REUSEPORT
SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能
- 允许多个套接字 bind()/listen() 同一个TCP/UDP端口
- 每一个线程拥有自己的服务器套接字
- 在服务器套接字上没有了锁的竞争
- 内核层面实现负载均衡
- 安全层面,监听同一个端口的套接字只能位于同一个用户下面
其核心的实现主要有三点:
- 扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport。
- 修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口
- 修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 和端口的多个 sock 之间均衡选择。
有了SO_RESUEPORT后,每个进程可以自己创建socket、bind、listen、accept相同的地址和端口,各自是独立平等的。让多进程监听同一个端口,各个进程中accept socket fd
不一样,有新连接建立时,内核只会唤醒一个进程来accept
,并且保证唤醒的均衡性。