1、前言
昨天總結(jié)了一下Linux下網(wǎng)絡(luò)編程“驚群”現(xiàn)象,給出Nginx處理驚群的方法,使用互斥鎖。為例發(fā)揮多核的優(yōu)勢,目前常見的網(wǎng)絡(luò)編程模型就是多進程或多線程,根據(jù)accpet的位置,分為如下場景:
?。?)單進程或線程創(chuàng)建socket,并進行l(wèi)isten和accept,接收到連接后創(chuàng)建進程和線程處理連接
(2)單進程或線程創(chuàng)建socket,并進行l(wèi)isten,預(yù)先創(chuàng)建好多個工作進程或線程accept()在同一個服務(wù)器套接字、
這兩種模型解充分發(fā)揮了多核CPU的優(yōu)勢,雖然可以做到線程和CPU核綁定,但都會存在:
單一listener工作進程胡線程在高速的連接接入處理時會成為瓶頸
多個線程之間競爭獲取服務(wù)套接字
緩存行跳躍
很難做到CPU之間的負(fù)載均衡
隨著核數(shù)的擴展,性能并沒有隨著提升
參考:http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html
Linux kernel 3.9帶來了SO_REUSEPORT特性,可以解決以上大部分問題。
2、SO_REUSEPORT解決了什么問題
SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提高服務(wù)器程序的性能,解決的問題:
允許多個套接字 bind()/listen() 同一個TCP/UDP端口
每一個線程擁有自己的服務(wù)器套接字
在服務(wù)器套接字上沒有了鎖的競爭
內(nèi)核層面實現(xiàn)負(fù)載均衡
安全層面,監(jiān)聽同一個端口的套接字只能位于同一個用戶下面
其核心的實現(xiàn)主要有三點:
擴展 socket option,增加 SO_REUSEPORT 選項,用來設(shè)置 reuseport。
修改 bind 系統(tǒng)調(diào)用實現(xiàn),以便支持可以綁定到相同的 IP 和端口
修改處理新建連接的實現(xiàn),查找 listener 的時候,能夠支持在監(jiān)聽相同 IP 和端口的多個 sock 之間均衡選擇。
有了SO_RESUEPORT后,每個進程可以自己創(chuàng)建socket、bind、listen、accept相同的地址和端口,各自是獨立平等的。讓多進程監(jiān)聽同一個端口,各個進程中accept socket fd
不一樣,有新連接建立時,內(nèi)核只會喚醒一個進程來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 888816 #define WORKER 417 #define MAXLINE 409618 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 }
我的測試機器內(nèi)核版本為:
測試結(jié)果如下所示:
從結(jié)果可以看出,四個進程監(jiān)聽相同的IP和port。
4、參考資料
http://lists.dragonflybsd.org/pipermail/users/2013-July/053632.html
http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html
http://m.blog.chinaunix.net/uid-10167808-id-3807060.html
http://www.cnblogs.com/Anker/p/7076537.html