1. SO_REUSEADDR
假如服务端出现故障,主动断开连接以后,需要等 2 个 MSL 以后才最终释放这个连接,而服务重启以后要绑定同一个端口,默认情况下,操作系统的实现都会阻止新的监听套接字绑定到这个端口上。启用 SO_REUSEADDR 套接字选项可以解除这个限制。
2. SO_REUSEPORT
默认情况下,一个 IP、端口组合只能被一个套接字绑定,Linux 内核从 3.9 版本开始引入一个新的 socket 选项 SO_REUSEPORT,又称为 port sharding,允许多个套接字监听同一个IP 和端口组合。
2.1 多进程网络模型
- 主进程 + 多个 worker 子进程监听相同的端口(导致惊群问题)
- 多进程 + REUSEPORT
2.2 惊群问题
多进程/多线程同时监听同一个套接字,当有网络事件发生时,所有等待的进程/线程同时被唤醒,但是只有其中一个进程/线程可以处理该网络事件,其它的进程/线程获取失败重新进入休眠。
2.3 accept系统调用的惊群问题
在Linux 2.6之前,会出现惊群问题。而在后面的版本中,引入了WQ_FLAG_EXCLUSIVE 选项解决了 accept 调用的惊群问题。
2.4 epoll系统调用的惊群问题
当有新的网络事件发生时,阻塞在 epoll_wait 的多个进程同时被唤醒。在这种情况下,epoll 的惊群还是存在的。
2.5 SO_REUSEPORT原理
内核为处于 LISTEN 状态的 socket 分配了大小为 32 哈希桶。监听的端口号经过哈希算法运算打散到这些哈希桶中,相同哈希的端口采用拉链法解决冲突。
-
当收到客户端的 SYN 握手报文以后,会根据目标端口号的哈希值计算出哈希冲突链表
-
对于设置了 SO_REUSEPORT 选项的 socket 经过二次哈希找到对应的 SO_REUSEPORT group,从中随机选择一个进行处理。
2.6 SO_REUSEPORT作用
- 内核有数据到来的时候就会把请求均衡的分给不同的socket,也就是不同的线程了,这样内核可以自动的做到负载均衡
- 支持滚动升级
- 新启动一个新版本 v2 ,监听同一个端口,与 v1 旧版本一起处理请求。
- 发送信号给 v1 版本的进程,让它不再接受新的请求
- 等待一段时间,等 v1 版本的用户请求都已经处理完毕时,v1 版本的进程退出,留下 v2 版本继续服务
2.7 SO_REUSEPORT安全性
出于安全性考虑,防止端口劫持
- 只有effective-user-id相同的服务器进程才能监听同一ip:port
- 只有第一个启动的进程启用了 SO_REUSEPORT 选项,后面启动的进程才可以绑定同一个端口。
3. SO_LINGER
设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。
SO_LINGER选项使用如下结构:
struct linger {int l_onoff;int l_linger;
};
- l_onoff为0,则该选项关闭,l_linger的值被忽略,close()用上述缺省方式关闭连接。
- l_onoff非0,l_linger为0,close()用下面的a方式关闭连接。
- l_onoff非0,l_linger非0,close()用下面的b方式关闭连接。
-
a.立即关闭该连接,通过发送RST分组(而不是用正常的FIN|ACK|FIN|ACK四个分组)来关闭该连接。至于发送缓冲区中如果有未发送完的数据,则丢弃。主动关闭一方的TCP状态则跳过TIMEWAIT,直接进入CLOSED。
-
b.将连接的关闭设置一个超时。如果socket发送缓冲区中仍残留数据,进程进入睡眠,内核进入定时状态去尽量去发送这些数据。
在超时之前,如果所有数据都发送完且被对方确认,内核用正常的FIN|ACK|FIN|ACK四个分组来关闭该连接,close()成功返回。
如果超时之时,数据仍然未能成功发送及被确认,用上述a方式来关闭此连接。close()返回EWOULDBLOCK。
参考文章
深入理解 TCP 协议:从原理到实战