REUSEPORT

REUSEPORT 场景

TCP REUSEPORT 选项通过每个监听线程使用不同的 listen fd 来改进accpet的负载分配。相比于在一个线程中进行 accept 或者多个线程在同一个 listen fd 上进行 accept,有助于系统的负载均衡。

REUSEPORT 解决的问题是:两个线程同时 bind 在相同的 IP:Port 上,当两者同时进入 TCP_LISTEN 状态,内核如何将一个客户端连接分发给正确的线程。

bind

内核 bind 过程:

  1. 根据 IP 和 Port 等计算 hash key ;
  2. 在 bind_hash_bucket 中找到对应的 bind_bucket 链表,链表由 sock 结构体组成;
  3. bind 将新的 sock 结构体添加到对应链表;
  4. 添加时,由 inet_csk_get_port 检查当前使用的 port 的合法性;
  5. 检查 fastreuse 标志位是否为真,或者 fastreuseport 标志位是否为真,且新连接的 UID 是否和当前进程相同。标志位快速判断成功时,直接返回可用;
  6. 若快速判断不成功,将遍历 sock 链表,判断是否由冲突;

REUSEPORT 选项开启后,两个线程的连接冲突的条件为,sock 当前不是 TCP_TIME_WAIT 状态且连接 UID 不同与线程 UID。其他情况下,不会存在连接冲突的情况。

测试

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// server 1
#include <arpa/inet.h>
#include <errno.h>
#include <iostream>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <unistd.h>

using namespace std;

int main(int argc, char *argv[]) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
std::cout << "socket error" << std::endl;
return -1;
}

int port = 30001;
std::string addr_str = "127.0.0.1";
std::cout << "[tcp server_test1] addr:" << addr_str << ":" << port
<< " fd=" << listenfd << std::endl;

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(addr_str.c_str());
// addr.sin_addr.s_addr = htonl(INADDR_ANY);

#ifdef RU_PORT
int val = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val)) < 0) {
std::cout << "set SO_REUSEPORT error" << std::endl;
}
std::cout << "set SO_REUSEPORT succ" << std::endl;
#endif

#ifdef RU_ADDR
int val = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
std::cout << "set SO_REUSEADDR error" << std::endl;
}
std::cout << "set SO_REUSEADDR succ" << std::endl;
#endif

//===========================================================================
// bind
int ret = bind(listenfd, (sockaddr *)&addr, sizeof(addr));

if (ret < 0) {
std::cout << "bind error! errno=" << errno << ", err = " << strerror(errno)
<< std::endl;
return -1;
}
std::cout << "bind success" << std::endl;

//===========================================================================
// listen
#ifndef NO_LISTEN
ret = listen(listenfd, 5);
if (ret < 0) {
std::cout << "listen error! errno=" << errno
<< ", err = " << strerror(errno) << std::endl;
return -1;
}
std::cout << "listen success" << std::endl;
#endif

//===========================================================================
// accept
while (1) {
#ifndef NO_LISTEN
sockaddr cli_addr;
socklen_t len;

// accept
std::cout << "accepting " << std::endl;
int connfd = accept(listenfd, &cli_addr, &len);
if (connfd < 0) {
std::cout << "accept error, error=" << strerror(errno) << std::endl;
}
std::cout << "accept success, connfd=" << connfd << std::endl;

// shutdown
std::cout << "INFO: input 1 to close connection, go to timewait" << std::endl;
int close_flag;
std::cin >> close_flag;
if (close_flag == 1) {
shutdown(connfd, SHUT_WR);
}
std::cout << "success call shutdown, now exit" << std::endl;

break;
#endif
}

close(listenfd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// server 2
#include <arpa/inet.h>
#include <errno.h>
#include <iostream>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
std::cout << "socket error" << std::endl;
return -1;
}

int port = 30001;
std::string addr_str = "127.0.0.1";
std::cout << "[tcp server_test2] addr:" << addr_str << ":" << port
<< " fd=" << listenfd << std::endl;

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(addr_str.c_str());
addr.sin_port = htons(port);

#ifdef RU_PORT
int val = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val)) < 0) {
std::cout << "set SO_REUSEPORT error" << std::endl;
}
std::cout << "set SO_REUSEPORT success" << std::endl;
#endif

#ifdef RU_ADDR
int val = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
std::cout << "set SO_REUSEADDR error" << std::endl;
}
std::cout << "set SO_REUSEADDR success" << std::endl;
#endif

int ret = bind(listenfd, (sockaddr *)&addr, sizeof(addr));
if (ret < 0) {
std::cout << "bind error! errno=" << errno << ", err = " << strerror(errno)
<< std::endl;
return -1;
}
std::cout << "bind success" << std::endl;

#ifndef NO_LISTEN
ret = listen(listenfd, 5);
if (ret < 0) {
std::cout << "listen error! errno=" << errno << ", err = " <<
strerror(errno) << std::endl; return -1;
}
std::cout << "listen success" << std::endl;
#endif

while (1) {}

close(listenfd);
return 0;
}

Case 1: 不开启 REUSEADDR 和 REUSEPORT,使用相同 IP 和 PORT

Case 2: 开启 REUSEADDR ,但是不调用 listen,使用相同 IP 和 PORT

Case 3: 开启 REUSEADDR ,调用 listen,使用相同 IP 和 PORT

Case 4: 开启 REUSEPORT ,调用 listen,使用相同 IP 和 PORT


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!