1、前言
昨天總結了一下Linux下網絡編程“驚群”現象,給出Nginx處理驚群的方法,使用互斥鎖。為例發揮多核的優勢,目前常見的網絡編程模型就是多進程或多線程,根據accpet的位置,分為如下場景:
(1)單進程或線程創建socket,並進行listen和accept,接收到連接后創建進程和線程處理連接
(2)單進程或線程創建socket,並進行listen,預先創建好多個工作進程或線程accept()在同一個服務器套接字、
這兩種模型解充分發揮了多核CPU的優勢,雖然可以做到線程和CPU核綁定,但都會存在:
- 單一listener工作進程胡線程在高速的連接接入處理時會成為瓶頸
- 多個線程之間競爭獲取服務套接字
- 緩存行跳躍
- 很難做到CPU之間的負載均衡
- 隨着核數的擴展,性能並沒有隨着提升
參考:http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html
Linux kernel 3.9帶來了SO_REUSEPORT特性,可以解決以上大部分問題。
2、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
,並且保證喚醒的均衡性。
3、測試代碼
1 include <stdio.h>
2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <sys/socket.h> 5 #include <netinet/in.h> 6 #include <arpa/inet.h> 7 #include <assert.h> 8 #include <sys/wait.h> 9 #include <string.h> 10 #include <errno.h> 11 #include <stdlib.h> 12 #include <fcntl.h> 13 14 #define IP "127.0.0.1" 15 #define PORT 8888 16 #define WORKER 4 17 #define MAXLINE 4096 18 19 int worker(int i) 20 { 21 struct sockaddr_in address; 22 bzero(&address, sizeof(address)); 23 address.sin_family = AF_INET; 24 inet_pton( AF_INET, IP, &address.sin_addr); 25 address.sin_port = htons(PORT); 26 27 int listenfd = socket(PF_INET, SOCK_STREAM, 0); 28 assert(listenfd >= 0); 29 30 int val =1; 31 /*set SO_REUSEPORT*/ 32 if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val))<0) { 33 perror("setsockopt()"); 34 } 35 int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address)); 36 assert(ret != -1); 37 38 ret = listen(listenfd, 5); 39 assert(ret != -1); 40 while (1) { 41 printf("I am worker %d, begin to accept connection.\n", i); 42 struct sockaddr_in client_addr; 43 socklen_t client_addrlen = sizeof( client_addr ); 44 int connfd = accept( listenfd, ( struct sockaddr* )&client_addr, &client_addrlen ); 45 if (connfd != -1) { 46 printf("worker %d accept a connection success. ip:%s, prot:%d\n", i, inet_ntoa(client_addr.sin_addr), client_addr.sin_port); 47 } else { 48 printf("worker %d accept a connection failed,error:%s", i, strerror(errno)); 49 } 50 char buffer[MAXLINE]; 51 int nbytes = read(connfd, buffer, MAXLINE); 52 printf("read from client is:%s\n", buffer); 53 write(connfd, buffer, nbytes); 54 close(connfd); 55 } 56 return 0; 57 } 58 59 int main() 60 { 61 int i = 0; 62 for (i = 0; i < WORKER; i++) { 63 printf("Create worker %d\n", i); 64 pid_t pid = fork(); 65 /*child process */ 66 if (pid == 0) { 67 worker(i); 68 } 69 if (pid < 0) { 70 printf("fork error"); 71 } 72 } 73 /*wait child process*/ 74 while (wait(NULL) != 0) 75 ; 76 if (errno == ECHILD) { 77 fprintf(stderr, "wait error:%s\n", strerror(errno)); 78 } 79 return 0; 80 }
我的測試機器內核版本為:
測試結果如下所示:
從結果可以看出,四個進程監聽相同的IP和port。
4、參考資料
http://lists.dragonflybsd.org/pipermail/users/2013-July/053632.html
http://m.blog.chinaunix.net/uid-10167808-id-3807060.html
SO_REUSEPORT學習筆記
前言
本篇用於記錄學習SO_REUSEPORT的筆記和心得,末尾還會提供一個bindp小工具也能為已有的程序享受這個新的特性。
當前Linux網絡應用程序問題
運行在Linux系統上網絡應用程序,為了利用多核的優勢,一般使用以下比較典型的多進程/多線程服務器模型:
- 單線程listen/accept,多個工作線程接收任務分發,雖CPU的工作負載不再是問題,但會存在:
- 單線程listener,在處理高速率海量連接時,一樣會成為瓶頸
- CPU緩存行丟失套接字結構(socket structure)現象嚴重
- 所有工作線程都accept()在同一個服務器套接字上呢,一樣存在問題:
- 多線程訪問server socket鎖競爭嚴重
- 高負載下,線程之間處理不均衡,有時高達3:1不均衡比例
- 導致CPU緩存行跳躍(cache line bouncing)
- 在繁忙CPU上存在較大延遲
上面模型雖然可以做到線程和CPU核綁定,但都會存在:
- 單一listener工作線程在高速的連接接入處理時會成為瓶頸
- 緩存行跳躍
- 很難做到CPU之間的負載均衡
- 隨着核數的擴展,性能並沒有隨着提升
比如HTTP CPS(Connection Per Second)吞吐量並沒有隨着CPU核數增加呈現線性增長:
Linux kernel 3.9帶來了SO_REUSEPORT特性,可以解決以上大部分問題。
SO_REUSEPORT解決了什么問題
linux man文檔中一段文字描述其作用:
The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.
SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提高服務器程序的性能,解決的問題:
- 允許多個套接字 bind()/listen() 同一個TCP/UDP端口
- 每一個線程擁有自己的服務器套接字
- 在服務器套接字上沒有了鎖的競爭
- 內核層面實現負載均衡
- 安全層面,監聽同一個端口的套接字只能位於同一個用戶下面
其核心的實現主要有三點:
- 擴展 socket option,增加 SO_REUSEPORT 選項,用來設置 reuseport。
- 修改 bind 系統調用實現,以便支持可以綁定到相同的 IP 和端口
- 修改處理新建連接的實現,查找 listener 的時候,能夠支持在監聽相同 IP 和端口的多個 sock 之間均衡選擇。
代碼分析,可以參考引用資料 [多個進程綁定相同端口的實現分析[Google Patch]]。
CPU之間平衡處理,水平擴展
以前通過fork
形式創建多個子進程,現在有了SO_REUSEPORT,可以不用通過fork
的形式,讓多進程監聽同一個端口,各個進程中accept socket fd
不一樣,有新連接建立時,內核只會喚醒一個進程來accept
,並且保證喚醒的均衡性。
模型簡單,維護方便了,進程的管理和應用邏輯解耦,進程的管理水平擴展權限下放給程序員/管理員,可以根據實際進行控制進程啟動/關閉,增加了靈活性。
這帶來了一個較為微觀的水平擴展思路,線程多少是否合適,狀態是否存在共享,降低單個進程的資源依賴,針對無狀態的服務器架構最為適合了。
新特性測試或多個版本共存
可以很方便的測試新特性,同一個程序,不同版本同時運行中,根據運行結果決定新老版本更迭與否。
針對對客戶端而言,表面上感受不到其變動,因為這些工作完全在服務器端進行。
服務器無縫重啟/切換
想法是,我們迭代了一版本,需要部署到線上,為之啟動一個新的進程后,稍后關閉舊版本進程程序,服務一直在運行中不間斷,需要平衡過度。這就像Erlang語言層面所提供的熱更新一樣。
想法不錯,但是實際操作起來,就不是那么平滑了,還好有一個hubtime開源工具,原理為SIGHUP信號處理器+SO_REUSEPORT+LD_RELOAD
,可以幫助我們輕松做到,有需要的同學可以檢出試用一下。
SO_REUSEPORT已知問題
SO_REUSEPORT根據數據包的四元組{src ip, src port, dst ip, dst port}和當前綁定同一個端口的服務器套接字數量進行數據包分發。若服務器套接字數量產生變化,內核會把本該上一個服務器套接字所處理的客戶端連接所發送的數據包(比如三次握手期間的半連接,以及已經完成握手但在隊列中排隊的連接)分發到其它的服務器套接字上面,可能會導致客戶端請求失敗,一般可以使用:
- 使用固定的服務器套接字數量,不要在負載繁忙期間輕易變化
- 允許多個服務器套接字共享TCP請求表(Tcp request table)
- 不使用四元組作為Hash值進行選擇本地套接字處理,挑選隸屬於同一個CPU的套接字
與RFS/RPS/XPS-mq協作,可以獲得進一步的性能:
- 服務器線程綁定到CPUs
- RPS分發TCP SYN包到對應CPU核上
- TCP連接被已綁定到CPU上的線程accept()
- XPS-mq(Transmit Packet Steering for multiqueue),傳輸隊列和CPU綁定,發送數據
- RFS/RPS保證同一個連接后續數據包都會被分發到同一個CPU上
- 網卡接收隊列已經綁定到CPU,則RFS/RPS則無須設置
- 需要注意硬件支持與否
目的嘛,數據包的軟硬中斷、接收、處理等在一個CPU核上,並行化處理,盡可能做到資源利用最大化。
SO_REUSEPORT不是一貼萬能膏葯
雖然SO_REUSEPORT解決了多個進程共同綁定/監聽同一端口的問題,但根據新浪林曉峰同學測試結果來看,在多核擴展層面也未能夠做到理想的線性擴展:
可以參考Fastsocket在其基礎之上的改進,鏈接地址。
支持SO_REUSEPORT的Tengine
淘寶的Tengine已經支持了SO_REUSEPORT特性,在其測試報告中,有一個簡單測試,可以看出來相對比SO_REUSEPORT所帶來的性能提升:
使用SO_REUSEPORT以后,最明顯的效果是在壓力下不容易出現丟請求的情況,CPU均衡性平穩。
Java支持否?
JDK 1.6語言層面不支持,至於以后的版本,由於暫時沒有使用到,不多說。
Netty 3/4版本默認都不支持SO_REUSEPORT特性,但Netty 4.0.19以及之后版本才真正提供了JNI方式單獨包裝的epoll native transport版本(在Linux系統下運行),可以配置類似於SO_REUSEPORT等(JAVA NIIO沒有提供)選項,這部分是在io.netty.channel.epoll.EpollChannelOption
中定義(在線代碼部分)。
在linux環境下使用epoll native transport,可以獲得內核層面網絡堆棧增強的紅利,如何使用可參考Native transports文檔。
使用epoll native transport倒也簡單,類名稍作替換:
NioEventLoopGroup → EpollEventLoopGroup NioEventLoop → EpollEventLoop NioServerSocketChannel → EpollServerSocketChannel NioSocketChannel → EpollSocketChannel
比如寫一個PING-PONG應用服務器程序,類似代碼:
public void run() throws Exception { EventLoopGroup bossGroup = new EpollEventLoopGroup(); EventLoopGroup workerGroup = new EpollEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); ChannelFuture f = b .group(bossGroup, workerGroup) .channel(EpollServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new StringDecoder(CharsetUtil.UTF_8), new StringEncoder(CharsetUtil.UTF_8), new PingPongServerHandler()); } }).option(ChannelOption.SO_REUSEADDR, true) .option(EpollChannelOption.SO_REUSEPORT, true) .childOption(ChannelOption.SO_KEEPALIVE, true).bind(port) .sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }
若不要這么折騰,還想讓以往Java/Netty應用程序在不做任何改動的前提下順利在Linux kernel >= 3.9下同樣享受到SO_REUSEPORT帶來的好處,不妨嘗試一下bindp,更為經濟,這一部分下面會講到。
bindp,為已有應用添加SO_REUSEPORT特性
以前所寫bindp小程序,可以為已有程序綁定指定的IP地址和端口,一方面可以省去硬編碼,另一方面也為測試提供了一些方便。
另外,為了讓以前沒有硬編碼SO_REUSEPORT
的應用程序可以在Linux內核3.9以及之后Linux系統上也能夠得到內核增強支持,稍做修改,添加支持。
但要求如下:
- Linux內核(>= 3.9)支持SO_REUSEPORT特性
- 需要配置
REUSE_PORT=1
不滿足以上條件,此特性將無法生效。
使用示范:
REUSE_PORT=1 BIND_PORT=9999 LD_PRELOAD=./libbindp.so java -server -jar pingpongserver.jar &
當然,你可以根據需要運行命令多次,多個進程監聽同一個端口,單機進程水平擴展。
使用示范
使用python腳本快速構建一個小的示范原型,兩個進程,都監聽同一個端口10000,客戶端請求返回不同內容,僅供娛樂。
server_v1.py,簡單PING-PONG:
# -*- coding:UTF-8 -*- import socket import os PORT = 10000 BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', PORT)) s.listen(1) while True: conn, addr = s.accept() data = conn.recv(PORT) conn.send('Connected to server[%s] from client[%s]\n' % (os.getpid(), addr)) conn.close() s.close()
server_v2.py,輸出當前時間:
# -*- coding:UTF-8 -*- import socket import time import os PORT = 10000 BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', PORT)) s.listen(1) while True: conn, addr = s.accept() data = conn.recv(PORT) conn.send('server[%s] time %s\n' % (os.getpid(), time.ctime())) conn.close() s.close()
借助於bindp運行兩個版本的程序:
REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v1.py & REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v2.py &
模擬客戶端請求10次:
for i in {1..10};do echo "hello" | nc 127.0.0.1 10000;done
看看結果吧:
Connected to server[3139] from client[('127.0.0.1', 48858)] server[3140] time Thu Feb 12 16:39:12 2015 server[3140] time Thu Feb 12 16:39:12 2015 server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48862)] server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48864)] server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48866)] Connected to server[3139] from client[('127.0.0.1', 48867)]
可以看出來,CPU分配很均衡,各自分配50%的請求量。
嗯,雖是小玩具,有些意思 :))
bindp的使用方法
更多使用說明,請參考README。
參考資料
- 《SO_REUSEPORT: Scaling Techniques for Servers with High Connection Rates》PPT
- huptime
- SO_REUSEPORT and accept(2) performance
- 多個進程綁定相同端口的實現分析[Google Patch]
多個進程綁定相同端口的實現分析[Google Patch]
Google REUSEPORT 新特性,支持多個進程或者線程綁定到相同的 IP 和端口,以提高 server 的性能。
1. 設計思路
該特性實現了 IPv4/IPv6 下 TCP/UDP 協議的支持, 已經集成到 kernel 3.9 中。
核心的實現主要有三點:
- 擴展 socket option,增加 SO_REUSEPORT 選項,用來設置 reuseport。
- 修改 bind 系統調用實現,以便支持可以綁定到相同的 IP 和端口
- 修改處理新建連接的實現,查找 listener 的時候,能夠支持在監聽相同 IP 和端口的多個 sock 之間均衡選擇。
共包含 7 個 patch,其中有兩個為 buf fix
- 數據結構調整: 055dc21a1d1d219608cd4baac7d0683fb2cbbe8a
- TCP/IPv4: da5e36308d9f7151845018369148201a5d28b46d
- UDP/IPv4: ba418fa357a7b3c9d477f4706c6c7c96ddbd1360
- TCP/IPv6: 5ba24953e9707387cce87b07f0d5fbdd03c5c11b
- UDP/IPv6: 72289b96c943757220ccc681fe2e22b46e21aced
- bug fix: 7c0cadc69ca2ac8893aa162ee80d92a805840909 fix: UDP/IPv4
- bug fix: 5588d3742da9900323dc3d766845a53bacdfb5ab fix: 數據結構定義
下面根據該特性的實現,簡單介紹 IPv4 下多個進程綁定相同 IP 和端口的邏輯分析。 kernel 代碼版本:3.11-rc1。
2. 數據結構擴展
通用 sock 結構擴展,增加 skc_reuseport 成員,用於 socket option 配置是記錄對應 結果:
--- a/include/net/sock.h +++ b/include/net/sock.h @@ -140,6 +140,7 @@ typedef __u64 __bitwise __addrpair; * @skc_family: network address family * @skc_state: Connection state * @skc_reuse: %SO_REUSEADDR setting + * @skc_reuseport: %SO_REUSEPORT setting * @skc_bound_dev_if: bound device index if != 0 * @skc_bind_node: bind hash linkage for various protocol lookup tables * @skc_portaddr_node: second hash linkage for UDP/UDP-Lite protocol @@ -179,7 +180,8 @@ struct sock_common { unsigned short skc_family; volatile unsigned char skc_state; - unsigned char skc_reuse; + unsigned char skc_reuse:4; + unsigned char skc_reuseport:4; int skc_bound_dev_if; union { struct hlist_node skc_bind_node; @@ -297,6 +299,7 @@ struct sock { #define sk_family __sk_common.skc_family #define sk_state __sk_common.skc_state #define sk_reuse __sk_common.skc_reuse +#define sk_reuseport __sk_common.skc_reuseport #define sk_bound_dev_if __sk_common.skc_bound_dev_if #define sk_bind_node __sk_common.skc_bind_node #define sk_prot __sk_common.skc_prot --- a/net/core/sock.c +++ b/net/core/sock.c @@ -665,6 +665,9 @@ int sock_setsockopt(struct socket *sock, int level, int optname, case SO_REUSEADDR: sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE); break; + case SO_REUSEPORT: + sk->sk_reuseport = valbool; + break; case SO_TYPE: case SO_PROTOCOL: case SO_DOMAIN:
bind socket 結構擴展,記錄 fastreuseport 和 fastuid。這個會在執行 bind 時做相關 的初始化。其中,fastuid 應該是創建 fd 的 uid。
--- a/include/net/inet_hashtables.h +++ b/include/net/inet_hashtables.h @@ -81,7 +81,9 @@ struct inet_bind_bucket { struct net *ib_net; #endif unsigned short port; - signed short fastreuse; + signed char fastreuse; + signed char fastreuseport; + kuid_t fastuid; int num_owners; struct hlist_node node; struct hlist_head owners;
對於 TCP 來講,owners 記錄了使用相同端口號的 sock 列表。這個列表中的 sock 也包含 了監聽 IP 不同的情況。而我們要分析的相同 IP 和端口 sock 也在該列表中。
3. bind 系統調用
分析該函數的 callpath,就是為了明確 google patch 中如果是綁定相同 IP 和 端口號的 多個 socket 如何成功的通過 bind 系統調用。如果沒有該 patch 的話,應該返回 Address in use 之類的錯誤。
sys_bind() -> inet_bind() (TCP) -> sk->sk_prot->get_port(TCP: inet_csk_get_port)
inet_csk_get_port() 根據 bind 參數中指定的端口,查表 hashinfo->bhash
3.1. 初次綁定某端口
初次綁定某個端口的話,應該查表找不到對應的 struct inet_bind_bucket tb,因此要調用 inet_bind_bucket_create 創建一個表項,並作 resue 方面的初始化:
216 tb_not_found: 217 ret = 1; 218 if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep, 219 net, head, snum)) == NULL) 220 goto fail_unlock; 221 if (hlist_empty(&tb->owners)) { 222 if (sk->sk_reuse && sk->sk_state != TCP_LISTEN) 223 tb->fastreuse = 1; 224 else 225 tb->fastreuse = 0; 226 if (sk->sk_reuseport) { 227 tb->fastreuseport = 1; 228 tb->fastuid = uid; 229 } else 230 tb->fastreuseport = 0; 231 } else {
226-228 行: 如果 socket 設置了 reuseport 的話,則新建表項的 fastreuseport 置 1, fastuid 也記錄下來,應該就是創建當前 socket fd 的 uid
接着調用 inet_bind_hash() 將當前的 sock 插入到 tb->owners 中,並增加計數
62 void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb, 63 const unsigned short snum) 64 { 65 struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; 66 67 atomic_inc(&hashinfo->bsockets); 68 69 inet_sk(sk)->inet_num = snum; 70 sk_add_bind_node(sk, &tb->owners); 71 tb->num_owners++; 72 inet_csk(sk)->icsk_bind_hash = tb; 73 }
並將 sock 對應 inet_connection_sock 的icsk_bind_hash 執行新分配的 tb。
3.2. 再次綁定相同端口
這次應該就可以找到對應的 tb,因此應該進行如下流程:
190 tb_found: 191 if (!hlist_empty(&tb->owners)) { 192 if (sk->sk_reuse == SK_FORCE_REUSE) 193 goto success; 194 195 if (((tb->fastreuse > 0 && 196 sk->sk_reuse && sk->sk_state != TCP_LISTEN) || 197 (tb->fastreuseport > 0 && 198 sk->sk_reuseport && uid_eq(tb->fastuid, uid))) && 199 smallest_size == -1) { 200 goto success; 201 } else { 202 ret = 1; 203 if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) { 204 if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) || 205 (tb->fastreuseport > 0 && 206 sk->sk_reuseport && uid_eq(tb->fastuid, uid))) && 207 smallest_size != -1 && --attempts >= 0) { 208 spin_unlock(&head->lock); 209 goto again; 210 } 211 212 goto fail_unlock; 213 } 214 } 215 }
195-196 為 socket reuse 的判斷,並且非 LISTEN 的認為可以 bind,如果已經處理 LISTEN 狀態的話,這里的條件不成立
197-198 為 Google patch 的檢測,tb 配置啟用了 reuseport,並且當前 socket 也設置 了reuseport,且 tb 和當前 socket 的 UID 一樣,可以認為當前 socket 也可以放到 bind hash 中,隨后會調用 inet_bind_hash 將當前 sock 也加入到 tb->owners 鏈表中。
4. listen 系統調用
sys_listen -> inet_listen -> inet_csk_listen_start
關鍵的實現就在 inet_csk_listen_start 中。重要的檢測主要是再次檢查端口是否可用。 因為 bind 和 listen 的執行有時間差,完全有可能被別的進程占去:
769 sk->sk_state = TCP_LISTEN; 770 if (!sk->sk_prot->get_port(sk, inet->inet_num)) { 771 inet->inet_sport = htons(inet->inet_num); 772 773 sk_dst_reset(sk); 774 sk->sk_prot->hash(sk); 775 776 return 0; 777 }
774 行調用 sk->sk_prot->hash(sk) 將對應的 sock 加入到 listening hash 中。 對於 TCP 而言, hash 指針指向 inet_hash()。這里記錄下 listen socket 的 hash 的計算邏輯:
inet_hash ->__inet_hash(sk) ->inet_sk_listen_hashfn ->inet_lhashfn
238 /* These can have wildcards, don't try too hard. */ 239 static inline int inet_lhashfn(struct net *net, const unsigned short num) 240 { 241 return (num + net_hash_mix(net)) & (INET_LHTABLE_SIZE - 1); 242 }
對於 listening socket,可以看出,應該是按照端口做 key 的,最終將 socket 放到了 listening_hash[] 中。
因此,綁定同一個端口的多個 listener sock 最后是放在了同一個 bucket 中。
5. 接受新連接
這里主要就是重點觀察 TCP 協議棧將新建連接的請求分發給綁定了相同 IP 和端口的不同 listening socket。
tcp_v4_rcv -> __inet_lookup_skb -> __inet_lookup -> __inet_lookup_listener (新建連接,只能通過 listener hash 查到其所屬 listener)
__inet_lookup_listener 函數增加兩個參數,saddr 和 sport。沒有 Google patch 之前, 查找 listener 的話是不需要這兩個參數的:
177 struct sock *__inet_lookup_listener(struct net *net, 178 struct inet_hashinfo *hashinfo, 179 const __be32 saddr, __be16 sport, 180 const __be32 daddr, const unsigned short hnum, 181 const int dif) 182 { ... ... 191 begin: 192 result = NULL; 193 hiscore = 0; 194 sk_nulls_for_each_rcu(sk, node, &ilb->head) { 195 score = compute_score(sk, net, hnum, daddr, dif); 196 if (score > hiscore) { 197 result = sk; 198 hiscore = score; 199 reuseport = sk->sk_reuseport; 200 if (reuseport) { 201 phash = inet_ehashfn(net, daddr, hnum, 202 saddr, sport); 203 matches = 1; 204 } 205 } else if (score == hiscore && reuseport) { 206 matches++; 207 if (((u64)phash * matches) >> 32 == 0) 208 result = sk; 209 phash = next_pseudo_random32(phash); 210 } 211 }
該函數就是根據 sip+sport+dip+dport+dif 來查找合適的 listener。在沒加入 google REUSEPORT patch 之前,是沒有 sip 和 sport 的。這兩個元素就是用來幫助在多個監 聽相同 port 的 listener 之間做選擇,並可能盡量保證公平。
這里有個函數調用 compute_score(),用來計算匹配的分數,得分最高的 listener 將作為 result 返回。計算的匹配分數主要是看 listener 的 portnum,rcv_saddr, 目的接口與 listener 的匹配程度。
196-204 行: 查到一個合適的 listener,而且得分比歷史記錄還高,記下該 sock。同時, 考慮到 reuseport 的問題,根據四元組計算一個 phash,match 置 1.
205 行: 走到這個分支,說明就是出現了 reuseport 的情況,而且是遍歷到了第 N 個 (N>1)個監聽相同端口的 listener。因此,其得分與歷史得分肯定相等。
206-209 行:這幾行代碼就是實現了是否使用當前 listener 的邏輯。如果不使用的話, 那就繼續遍歷下一個。最終的結果就會在多個綁定相同端口的 listener 中使用其中一個。 因為 phash 的初次計算中加入了 saddr 和 sport,這個算法在 IP 地址及 port 足夠多 的情況下保證了多個 listener 都會被平均分配到請求。
至此,google REUSEPORT 的 patch 簡單的分析完畢。