- 本文基于Linux>=3.9,测试代码:github.com/csioza/reus…
- 本文转自:端口:通往操作系统的大门
一、背景
最近一个服务升级,原来仅能处理TCP请求,现在又想处理UDP,为了方便维护,想使用同一个端口处理业务请求,那么TCP和UDP能同时绑定同一个端口吗?
然后引申出两个问题:
- 多个UDP可以同时绑定同一端口吗?
- 多个TCP可以同时绑定同一端口吗?
所以想详细研究和总结下端口相关的知识,一劳永逸。
二、结论:地址 / 端口复用
- 多个Socket(均未开启 SO_REUSEADDR 和 SO_REUSEPORT)同时绑定相同IP:PORT,除第一个外所有绑定的Socket会报错(Address already in use);
图1
- 多个Socket(均开启SO_REUSEADDR或者均开启SO_REUSEPORT)同时绑定相同IP:PORT,所有绑定的Socket均能绑定成功;
图2
- 多个Socket(均开启 SO_REUSEADDR 且均未开启 SO_REUSEPORT )同时绑定相同IP:PORT,但是只有最后绑定的Socket能收到数据;
图3
图4
三、应用场景:地址复用
场景一
TCP服务器重启有时会遇到“地址已经被使用”错误,这个错误就是多个Socket绑定到同一个IP:PORT所致。那为啥第一次启动服务不会报错呢?因为这里的“多个Socket”指的是服务启动前的Socket(处于TCP的TIME_WAIT状态,还未关闭)和服务启动后新创建的Socket。
复习一下TIME_WAIT,下面是TCP的四次挥手:
图5
主动发起关闭链接的TCP Socket在TIME_WAIT停留持续时间是固定的,是最长分节生命期MSL(maximum segment lifetime)的两倍,一般称之为2MSL。Linux系统停留在TIME_WAIT的时间为固定的60秒。
如果重启前服务的TCP Socket处于TIME_WAIT状态,重启服务一般都是几秒的事,那么服务重启后肯定会报错(Address already in use)。
那么如何解决呢,答案就Socket设置SO_REUSEADDR。
UDP没有像TCP那么多状态,UDP服务重启立刻就关闭了所依赖的连接,所以单纯为了重启场景不需要设置SO_REUSEADDR。
场景二
如图6,机器上有多张网卡时,Socket 1 绑定0.0.0.0:12345,Socket 2 绑定172.17.0.3:12345,在绑定之前均开启了SO_REUSEADDR并且均未开启SO_REUSEPORT,那么客户端发给172.17.0.1:12345和172.17.0.2:12345的数据均会被Socket 1接收到,而发给172.17.0.3:12345的数据会被Socket 2接收到。
图6
说白了就是 Socket 1 少绑定了一步操作,除非IP特别多,否则不建议这么操作,做了就是给自己挖坑。
四、应用场景:端口复用
多个Socket在Bind之前开启了SO_REUSEPORT,那么它们可以同时绑定和监听同一个IP:PORT,每个线程/进程处理一个Socket的数据收发,这样可以充分发挥多核的性能。
TCP服务架构的演进
- 如图7,单线程accept socket,然后把接收到来自客户端发起(connect)的连接返回的socket分发到其他工作线程。性能瓶颈在单accept线程。
图7
- 如图8,多个线程accept 同一个listen socket,所有新链接只保存在一个listen socket 的全链接队列中,那么多个线程去这个队列里获取(accept)新的链接,势必会出现多个线程对一个公共资源的争抢,争抢过程中,大量资源的损耗,Linux<2.6还会出现惊群现象。
图8
- 如图9,多个工作线程的socket同时bind同一个IP:PORT,并且各自独立运行listen、accept和process。每个socket要设置SO_REUSEPORT。当客户端发起连接请求(connect),内核根据客户端的IP:PORT hash映射到对应工作线程的socket上。
图9
nginx 开启 SO_REUSEPORT 功能后,性能有立竿见影的提升。
UDP使用场景
和TCP类似,只不过UDP相对简单。
- 如图10,单线程bind socket并处理数据收发,通常只做这一件事,一般上限是处理10万QPS。
图10
- 如图11,多个工作线程共用一个绑定过后的socket,与TCP一样存在资源竞争。实际工作中很少这么用。
图11
- 如同12,多个工作线程的 socket 同时 bind 同一个IP:PORT,并且各自独立运行 process。每个 socket 要设置 SO_REUSEPORT。当客户端发送(sendto)数据,内核根据客户端的IP:PORT hash映射到对应工作线程的socket上。
图12
我最近写的一个服务就是采用这种方式,性能明显提升,由原来的10万QPS(图10的方式),提升到50万QPS(5个工作线程)。
五、客户端:端口复用
对于TCP的客户端来说不需要Bind,但是客户端可能出现大量TIME_WAIT状态的TCP连接,导致端口被耗尽。
net.ipv4.tcp_tw_reuse这个内核参数开启后,客户端调用connect函数时,如果选择到的端口,已经被相同五元组的连接占用的时候,就会判断该连接是否处于TIME_WAIT状态。
如果该连接处于TIME_WAIT状态并且TIME_WAIT状态持续的时间超过了1秒,那么就会重用这个连接,然后就可以正常使用该端口了。只适用于连接的发起方。
六、TCP和UDP可以同时绑定同一端口吗?
回到文章开头的问题,为啥放在最后解答,是因为默认情况下,TCP和UDP就可以同时绑定相同的IP:PORT。
TCP Socket创建函数和参数:
int fd = socket(AF_INET, SOCK_STREAM, 0);
UDP Socket创建函数和参数:
int fd = socket(AF_INET, SOCK_DGRAM, 0);
TCP和UDP的Bind操作一样:
int ret = bind(fd, (struct sockaddr *)&svrAddr, sizeof(svrAddr));
实际上Socket绑定端口前都设置了具体的协议,所以不是绑定IP:PORT,而是绑定了PROTOCOL:IP:PORT。
如图13,TCP Socket绑定TCP:172.17.0.2:12345,UDP Socket绑定UDP:172.17.0.2:12345,它们各自处理自己的数据,互不干扰。
图13
七、参考
- TIME_WAIT:隐藏在细节下的魔鬼
- 怎么老是出现“地址已经被使用”?
- TCP 和 UDP 可以使用同一个端口吗?
- Linux内核中reuseport的演进
- 探索惊群 ⑥ – nginx – reuseport
创作不易,你的点赞、转发、收藏是我创作的最大动力!
更多分享尽在微信公众号(科英)!