网络编程中的惊群效应——1
参考博客:https://www.cnblogs.com/Anker/p/7071849.html
惊群效应:简单的说就是多个进程同时等待网络的连接事件,当真正来了一个连接的时候会把所有监听的进程都唤醒,而最终只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠或其他。这样的现象带来最主要的问题是造成性能浪费。
打个比方,比如说fork4个进程,这4个进程都在各自的进程工作函数里使用父进程的socket文件描述符调用accept函数,当来了一个客户端的连接请求后,4个进程都会accpet到请求,但是只有一个能成功,其他进程都会返回-1,显然造成了性能的浪费,因为本来只要唤醒一个进程处理就行了,而此时却唤醒了4个。
不过呢,据参考博客里的描述,Linux2.6版本以后,内核内核已经解决了accept()函数的“惊群”问题,大概的处理方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程。所以,如果服务器采用accept阻塞调用方式,在最新的Linux系统上,已经没有“惊群”的问题了。
因此,我在实际运行代码的时候也并没有看到惊群效应,应该是系统进行了优化吧。
PS:以下代码来自参考博客,非原创。推荐手敲以下代码,个人觉得照着手敲一遍印象和理解都更深刻些。
accept惊群效应
#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;
}
运行代码,客户端直接使用以下命令即可。
telnet 127.0.0.1 8888
epoll惊群效应
epoll的思路差不多。
第一阶段,在main函数里将服务器端的文件描述符sfd初始化绑定端口等操作,并设置为非阻塞模式,然后添加到epoll的红黑树上,准备工作到此结束。
第二阶段,创建4个进程,并分别在子进程的工作函数woker中调用epoll_wait监听,当来消息后,epoll_wait会返回,进行accept调用,同样只有一个能成功,其他三个均失败,实验测试也符合实际情况。
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.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;
}
int 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);
sleep(1);
printf("worker %d return from epoll_wait!\n", k);
for (int i = 0; i < n; i++) {
/* 文件描述符发生错误,被挂断,不可读 */
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) {
fprintf(stderr, "epoll error\n");
close(events[i].data.fd);
continue;
}
/* 到来一个连接请求 */
else if (sfd == events[i].data.fd) {
struct sockaddr in_addr;
socklen_t in_len;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len = sizeof(in_addr);
int infd = accept(sfd, &in_addr, &in_len);
if (infd == -1) {
printf("woker %d accept failed!\n", k);
break;
}
printf("woker %d accept successed!\n", k);
/* 将connfd设置为非阻塞并加入到epoll的监听树上 */
close(infd);
}
}
}
return 0;
}
int main() {
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();
}
events = calloc(MAXEVENTS, sizeof(event));
for (int i = 0; i < PROCESS_NUM; i++) {
printf("Create worker %d\n", i + 1);
int pid = fork();
if (pid == 0) { /* 子进程 */
printf("I am %dth sub process,pid = %d!", i, pid);
worker(sfd, efd, events, i); /* 新进程开始epoll监听 */
}
}
int status;
wait(&status);
free(events);
close(sfd);
return EXIT_SUCCESS;
}
不过在linux 4.5之后已经部分解决了epoll的惊群问题,出现EPOLLEXCLUSIVE选项,详细实验我在《网络编程中的惊群效应——2》这篇博文里写了。下面直接给出实验结果:
- 不添加 EPOLLEXCLUSIVE
项目 | LT模式 | ET模式 |
---|---|---|
共享epoll_fd | 存在 | 不存在 |
独享epoll_fd | 存在 | 存在 |
- 添加EPOLLEXCLUSIVE
项目 | LT模式 | ET模式 |
---|---|---|
共享epoll_fd | 存在 | 不存在 |
独享epoll_fd | 不存在 | 不存在 |
其他
之前的一些面经上看到不少问惊群效应和雪崩的,比较典型的是accept、epoll、select、poll等,这都和上面的实验比较类似。
不过nginx的惊群效应和处理方式也是很重要的,此外还有个雪崩效应一般这两个词配套出现。
推荐的相关博文
http://blog.csdn.net/russell_tao/article/details/7204260