可能遇到的性能问题
- 缓冲区溢出:大量网络流量可能导致接收或发送缓冲区不足以容纳数据,造成数据丢失。
- 频繁系统调用开销:高并发场景下频繁的epoll_wait调用、I/O操作等系统调用会带来较大开销。
- 惊群效应:多个进程或线程等待同一个事件,当事件发生时,所有等待的进程或线程都被唤醒,但只有一个能真正处理该事件,其他被唤醒的进程或线程又重新进入等待,浪费CPU资源。
- 网络拥塞:大量连接请求和流量波动可能导致网络拥塞,进一步降低性能。
代码优化
- 合理设置缓冲区:
- 接收缓冲区:在创建套接字后,可以通过
setsockopt
函数增大接收缓冲区大小,例如:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int main() {
int sockfd, epollfd;
struct sockaddr_in servaddr;
struct epoll_event ev, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
int recvbuf = 32 * 1024; // 设置接收缓冲区为32KB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf));
memset(&servaddr, 0, sizeof(servaddr));
memset(buffer, 0, sizeof(buffer));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
if (listen(sockfd, 10) < 0) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1");
close(sockfd);
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = sockfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
perror("epoll_ctl: listen_sock");
close(sockfd);
close(epollfd);
exit(EXIT_FAILURE);
}
while (1) {
int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == sockfd) {
int connfd = accept(sockfd, NULL, NULL);
if (connfd == -1) {
perror("accept");
continue;
}
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1) {
perror("epoll_ctl: conn_sock");
close(connfd);
}
} else {
int connfd = events[n].data.fd;
ssize_t read_bytes = recv(connfd, buffer, sizeof(buffer), 0);
if (read_bytes <= 0) {
if (read_bytes == 0) {
printf("Connection closed by peer\n");
} else {
perror("recv");
}
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
close(connfd);
} else {
buffer[read_bytes] = '\0';
printf("Received: %s\n", buffer);
ssize_t write_bytes = send(connfd, buffer, read_bytes, 0);
if (write_bytes != read_bytes) {
perror("send");
}
}
}
}
}
close(sockfd);
close(epollfd);
return 0;
}
- **发送缓冲区**:同样使用`setsockopt`函数增大发送缓冲区大小,示例如下:
int sendbuf = 32 * 1024; // 设置发送缓冲区为32KB
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
- 调整I/O策略:
- 使用边缘触发模式:在epoll中设置
EPOLLET
标志,减少不必要的事件通知。如上述代码中,在添加连接套接字到epoll监控时设置ev.events = EPOLLIN | EPOLLET;
。边缘触发模式下,只有在数据到达或状态变化时才触发事件,应用程序需要一次性读取或处理完所有数据。
- 非阻塞I/O操作:将套接字设置为非阻塞模式,避免I/O操作阻塞线程或进程。可以通过
fcntl
函数实现,例如:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
系统参数调整
- 内核参数:
- net.core.somaxconn:该参数定义了
listen
函数中backlog
参数的最大值。增大此值可以允许更多的连接请求排队等待处理,例如:
echo "net.core.somaxconn = 65535" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p /etc/sysctl.conf
- **net.ipv4.tcp_max_syn_backlog**:此参数控制TCP SYN请求队列的最大长度。增加该值可以应对大量的连接请求,示例如下:
echo "net.ipv4.tcp_max_syn_backlog = 65535" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p /etc/sysctl.conf
- **net.ipv4.tcp_tw_reuse**:设置为1时,允许将处于TIME - WAIT状态的套接字重新用于新的连接,有助于快速重用端口,减少端口耗尽问题:
echo "net.ipv4.tcp_tw_reuse = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p /etc/sysctl.conf
惊群效应处理
- 使用epoll的EPOLLONESHOT:
- 原理:设置
EPOLLONESHOT
标志后,当一个文件描述符被epoll_wait返回事件后,这个文件描述符就会被epoll从监控列表中移除。之后如果这个文件描述符上再有事件发生,需要再次通过epoll_ctl
将其添加回监控列表。这样可以保证只有一个进程或线程能处理该事件,避免惊群效应。
- 代码示例:
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
ev.data.fd = connfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1) {
perror("epoll_ctl: conn_sock");
close(connfd);
}
- **处理逻辑**:在处理完事件后,重新添加文件描述符到epoll监控列表,例如:
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
ev.data.fd = connfd;
if (epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &ev) == -1) {
perror("epoll_ctl: mod conn_sock");
close(connfd);
}
- 使用互斥锁(Mutex):
- 原理:在多进程或多线程环境下,在处理事件前加锁,只有获取到锁的进程或线程能处理事件,其他进程或线程等待,从而避免多个进程或线程同时处理同一事件导致的惊群效应。
- 代码示例(以POSIX线程为例):
#include <pthread.h>
pthread_mutex_t mutex;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 在处理事件前加锁
pthread_mutex_lock(&mutex);
// 处理事件逻辑
// 处理完事件后解锁
pthread_mutex_unlock(&mutex);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);