1孽鸡、前言
我從事Linux系統(tǒng)下網(wǎng)絡(luò)開發(fā)將近4年了,經(jīng)常還是遇到一些問題栏豺,只是知其然而不知其所以然彬碱,有時(shí)候和其他人交流,搞得非常尷尬奥洼。如今計(jì)算機(jī)都是多核了巷疼,網(wǎng)絡(luò)編程框架也逐步豐富多了,我所知道的有多進(jìn)程灵奖、多線程嚼沿、異步事件驅(qū)動(dòng)常用的三種模型。最經(jīng)典的模型就是Nginx中所用的Master-Worker多進(jìn)程異步驅(qū)動(dòng)模型桑寨。今天和大家一起討論一下網(wǎng)絡(luò)開發(fā)中遇到的“驚群”現(xiàn)象伏尼。之前只是聽說過這個(gè)現(xiàn)象忿檩,網(wǎng)上查資料也了解了基本概念尉尾,在實(shí)際的工作中還真沒有遇到過。今天周末燥透,結(jié)合自己的理解和網(wǎng)上的資料沙咏,徹底將“驚群”弄明白。需要弄清楚如下幾個(gè)問題:
(1)什么是“驚群”班套,會(huì)產(chǎn)生什么問題肢藐?
(2)“驚群”的現(xiàn)象怎么用代碼模擬出來?
(3)如何處理“驚群”問題吱韭,處理“驚群”后的現(xiàn)象又是怎么樣呢吆豹?
2鱼的、何為驚群
如今網(wǎng)絡(luò)編程中經(jīng)常用到多進(jìn)程或多線程模型,大概的思路是父進(jìn)程創(chuàng)建socket痘煤,bind凑阶、listen后,通過fork創(chuàng)建多個(gè)子進(jìn)程衷快,每個(gè)子進(jìn)程繼承了父進(jìn)程的socket宙橱,調(diào)用accpet開始監(jiān)聽等待網(wǎng)絡(luò)連接。這個(gè)時(shí)候有多個(gè)進(jìn)程同時(shí)等待網(wǎng)絡(luò)的連接事件蘸拔,當(dāng)這個(gè)事件發(fā)生時(shí)师郑,這些進(jìn)程被同時(shí)喚醒,就是“驚群”调窍。這樣會(huì)導(dǎo)致什么問題呢宝冕?我們知道進(jìn)程被喚醒,需要進(jìn)行內(nèi)核重新調(diào)度陨晶,這樣每個(gè)進(jìn)程同時(shí)去響應(yīng)這一個(gè)事件猬仁,而最終只有一個(gè)進(jìn)程能處理事件成功,其他的進(jìn)程在處理該事件失敗后重新休眠或其他先誉。網(wǎng)絡(luò)模型如下圖所示:
簡而言之湿刽,驚群現(xiàn)象(thundering herd)就是當(dāng)多個(gè)進(jìn)程和線程在同時(shí)阻塞等待同一個(gè)事件時(shí),如果這個(gè)事件發(fā)生褐耳,會(huì)喚醒所有的進(jìn)程诈闺,但最終只可能有一個(gè)進(jìn)程/線程對該事件進(jìn)行處理,其他進(jìn)程/線程會(huì)在失敗后重新休眠铃芦,這種性能浪費(fèi)就是驚群雅镊。
3、編碼模擬“驚群”現(xiàn)象
我們已經(jīng)知道了“驚群”是怎么回事刃滓,那么就按照上面的圖編碼實(shí)現(xiàn)看一下效果仁烹。我嘗試使用多進(jìn)程模型,創(chuàng)建一個(gè)父進(jìn)程綁定一個(gè)端口監(jiān)聽socket咧虎,然后fork出多個(gè)子進(jìn)程卓缰,子進(jìn)程們開始循環(huán)處理(比如accept)這個(gè)socket。測試代碼如下所示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#define IP "127.0.0.1"
#define PORT 8888
#define WORKER 4
int worker(int listenfd, int i)
{
while (1) {
printf("I am worker %d, begin to accept connection.\n", i);
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof( client_addr );
int connfd = accept( listenfd, ( struct sockaddr* )&client_addr, &client_addrlen );
if (connfd != -1) {
printf("worker %d accept a connection success.\t", i);
printf("ip :%s\t",inet_ntoa(client_addr.sin_addr));
printf("port: %d \n",client_addr.sin_port);
} else {
printf("worker %d accept a connection failed,error:%s", i, strerror(errno)); close(connfd);
}
}
return 0;
}
int main()
{
int i = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton( AF_INET, IP, &address.sin_addr);
address.sin_port = htons(PORT);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
for (i = 0; i < WORKER; i++) {
printf("Create worker %d\n", i+1);
pid_t pid = fork();
/*child process */
if (pid == 0) {
worker(listenfd, i);
}
if (pid < 0) {
printf("fork error");
}
}
/*wait child process*/
int status;
wait(&status);
return 0;
}
編譯執(zhí)行砰诵,在本機(jī)上使用telnet 127.0.0.1 8888測試征唬,結(jié)果如下所示:
按照“驚群"現(xiàn)象,期望結(jié)果應(yīng)該是4個(gè)子進(jìn)程都會(huì)accpet到請求茁彭,其中只有一個(gè)成功总寒,另外三個(gè)失敗的情況。而實(shí)際的結(jié)果顯示理肺,父進(jìn)程開始創(chuàng)建4個(gè)子進(jìn)程摄闸,每個(gè)子進(jìn)程開始等待accept連接善镰。當(dāng)telnet連接來的時(shí)候,只有worker2 子進(jìn)程accpet到請求年枕,而其他的三個(gè)進(jìn)程并沒有接收到請求媳禁。
這是什么原因呢?難道驚群現(xiàn)象是假的嗎画切?于是趕緊google查一下竣稽,驚群到底是怎么出現(xiàn)的。
其實(shí)在Linux2.6版本以后霍弹,內(nèi)核內(nèi)核已經(jīng)解決了accept()函數(shù)的“驚群”問題毫别,大概的處理方式就是,當(dāng)內(nèi)核接收到一個(gè)客戶連接后典格,只會(huì)喚醒等待隊(duì)列上的第一個(gè)進(jìn)程或線程岛宦。所以,如果服務(wù)器采用accept阻塞調(diào)用方式耍缴,在最新的Linux系統(tǒng)上砾肺,已經(jīng)沒有“驚群”的問題了。
但是防嗡,對于實(shí)際工程中常見的服務(wù)器程序变汪,大都使用select、poll或epoll機(jī)制蚁趁,此時(shí)裙盾,服務(wù)器不是阻塞在accept,而是阻塞在select他嫡、poll或epoll_wait番官,這種情況下的“驚群”仍然需要考慮。接下來以epoll為例分析:
使用epoll非阻塞實(shí)現(xiàn)代碼如下所示:
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#include <unistd.h>
#define IP "127.0.0.1"
#define PORT 8888
#define PROCESS_NUM 4
#define MAXEVENTS 64
static int create_and_bind ()
{
int fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton( AF_INET, IP, &serveraddr.sin_addr);
serveraddr.sin_port = htons(PORT);
bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
return fd;
}
static int make_socket_non_blocking (int sfd)
{
int flags, s;
flags = fcntl (sfd, F_GETFL, 0);
if (flags == -1) {
perror ("fcntl");
return -1;
}
flags |= O_NONBLOCK;
s = fcntl (sfd, F_SETFL, flags);
if (s == -1) {
perror ("fcntl");
return -1;
}
return 0;
}
void worker(int sfd, int efd, struct epoll_event *events, int k) {
/* The event loop */
while (1) {
int n, i;
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("worker %d return from epoll_wait!\n", k);
for (i = 0; i < n; i++) {
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events &EPOLLIN))) {
/* An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events[i].data.fd);
continue;
} else if (sfd == events[i].data.fd) {
/* We have a notification on the listening socket, which means one or more incoming connections. */
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len = sizeof in_addr;
infd = accept(sfd, &in_addr, &in_len);
if (infd == -1) {
printf("worker %d accept failed!\n", k);
break;
}
printf("worker %d accept successed!\n", k);
/* Make the incoming socket non-blocking and add it to the list of fds to monitor. */
close(infd);
}
}
}
}
int main (int argc, char *argv[])
{
int sfd, s;
int efd;
struct epoll_event event;
struct epoll_event *events;
sfd = create_and_bind();
if (sfd == -1) {
abort ();
}
s = make_socket_non_blocking (sfd);
if (s == -1) {
abort ();
}
s = listen(sfd, SOMAXCONN);
if (s == -1) {
perror ("listen");
abort ();
}
efd = epoll_create(MAXEVENTS);
if (efd == -1) {
perror("epoll_create");
abort();
}
event.data.fd = sfd;
event.events = EPOLLIN;
s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);
if (s == -1) {
perror("epoll_ctl");
abort();
}
/* Buffer where events are returned */
events = calloc(MAXEVENTS, sizeof event);
int k;
for(k = 0; k < PROCESS_NUM; k++) {
printf("Create worker %d\n", k+1);
int pid = fork();
if(pid == 0) {
worker(sfd, efd, events, k);
}
}
int status;
wait(&status);
free (events);
close (sfd);
return EXIT_SUCCESS;
}
父進(jìn)程中創(chuàng)建套接字钢属,并設(shè)置為非阻塞徘熔,開始listen。然后fork出4個(gè)子進(jìn)程淆党,在worker中調(diào)用epoll_wait開始accpet連接酷师。使用telnet測試結(jié)果如下:
從結(jié)果看出,與上面是一樣的宁否,只有一個(gè)進(jìn)程接收到連接窒升,其他三個(gè)沒有收到缀遍,說明沒有發(fā)生驚群現(xiàn)象慕匠。這又是為什么呢?
在早期的Linux版本中域醇,內(nèi)核對于阻塞在epoll_wait的進(jìn)程台谊,也是采用全部喚醒的機(jī)制蓉媳,所以存在和accept相似的“驚群”問題。新版本的的解決方案也是只會(huì)喚醒等待隊(duì)列上的第一個(gè)進(jìn)程或線程锅铅,所以酪呻,新版本Linux 部分的解決了epoll的“驚群”問題。所謂部分的解決盐须,意思就是:對于部分特殊場景玩荠,使用epoll機(jī)制,已經(jīng)不存在“驚群”的問題了贼邓,但是對于大多數(shù)場景阶冈,epoll機(jī)制仍然存在“驚群”。
epoll存在驚群的場景如下:在worker保持工作的狀態(tài)下塑径,都會(huì)被喚醒女坑,例如在epoll_wait后調(diào)用sleep一次。改寫woker函數(shù)如下:
void worker(int sfd, int efd, struct epoll_event *events, int k) {
/* The event loop */
while (1) {
int n, i;
n = epoll_wait(efd, events, MAXEVENTS, -1);
/*keep running*/
sleep(2);
printf("worker %d return from epoll_wait!\n", k);
for (i = 0; i < n; i++) {
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events &EPOLLIN))) {
/* An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events[i].data.fd);
continue;
} else if (sfd == events[i].data.fd) {
/* We have a notification on the listening socket, which means one or more incoming connections. */
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len = sizeof in_addr;
infd = accept(sfd, &in_addr, &in_len);
if (infd == -1) {
printf("worker %d accept failed,error:%s\n", k, strerror(errno));
break;
}
printf("worker %d accept successed!\n", k);
/* Make the incoming socket non-blocking and add it to the list of fds to monitor. */
close(infd);
}
}
}
}
測試結(jié)果如下所示:
終于看到驚群現(xiàn)象的出現(xiàn)了统舀。
4匆骗、解決驚群問題
Nginx中使用mutex互斥鎖解決這個(gè)問題,具體措施有使用全局互斥鎖誉简,每個(gè)子進(jìn)程在epoll_wait()之前先去申請鎖碉就,申請到則繼續(xù)處理,獲取不到則等待闷串,并設(shè)置了一個(gè)負(fù)載均衡的算法(當(dāng)某一個(gè)子進(jìn)程的任務(wù)量達(dá)到總設(shè)置量的7/8時(shí)铝噩,則不會(huì)再嘗試去申請鎖)來均衡各個(gè)進(jìn)程的任務(wù)量。后面深入學(xué)習(xí)一下Nginx的驚群處理過程窿克。
5骏庸、參考網(wǎng)址
http://blog.csdn.net/russell_tao/article/details/7204260