高性能服务器
高性能服务器分析
声明:一点点自己的总结与感悟吧,如果有不对的地方,烦请指正
服务器最重要的是能够解决复杂的网络问题,大量并发请求,如经典的c10K ,当然后面这个并发问题延伸到c10M
当然这个 c10k 已经被从硬件和软件两个方面解决了,硬件方面应该是在 摩尔定律上下功夫? 软件方面就有很多方向了,然后接下来着重讲软件方面的解决方案
OS & question
有人说,操作系统的底层设计让通信开销增大
原话是,os 的内核不是解决c10M问题的办法,相反os 的内核正是导致C10M问题的关键
简单解释一下,比如网络包进入网卡,然后通过网卡进入内核处理,内核接着拷贝把数据转给用户程序处理,再把处理的数据通过网卡发送客户端
大致的思路是这样,然后就有人在这里分析,分析的结论有以下几个方面
- 中断处理,频繁的硬件中断请求会增加os资源的开销,尤其是硬件中断会打断低级的软中断或者系统调用的执行过程,这种打断会增加一定的性能开销
- 内存拷贝,数据包从网卡到内核开辟的缓冲区,再由内核缓冲区到用户态空间,在linux 内核协议栈汇中的耗时占到了整个数据包处理过程的57.1%
- 上下文切换,频繁的中断处理,会产生大量的上下文切换开销,而且可能涉及资源竞争,这就会涉及锁的处理,用锁也是一个很大开销
- 局部性失效,在多核处理器中,一个数据包跨多个cpu核心,如数据包中断在cpu0,内核处理在cpu1,用户态处理在cpu2,这样跨多核,容易增加cpu缓存失效,造成局部性失效
- 内存管理,服务器内存页4k ?然后为了提高内存的访问速度,避免cache miss ,增加cache 的映射表条目,但是这增加了cpu 检索效率
当然也有解决方案
- 控制层和数据层分层
- 多核技术
- NUMA 亲和性
- 大页存技术
- 无锁技术
解决案例
是的,有一些包含上述的解决方案的网络框架(🤦)好像有很多很多网络框架,都说自己贼牛逼(高可用、分布式、高性能
具体到的有,6wind、Windriver、Netmap、DPDK
这个DPDK 其实好像很火,后面可以有空研究一下
心得
好像,自己写过的项目,高性能是这样走的,
首先是对数据的处理,使用各种池
线程池(Thread Pool)
线程池是预先创建一组线程,任务提交后将分配给空闲的线程处理。线程池用于避免线程频繁创建和销毁的开销,提高并发性能。
数据库连接池(Connection Pool)
数据库连接池管理数据库连接。它预先创建一组数据库连接,并在需要时分配。这样可以减少每次连接数据库的开销,提高数据库操作的性能。
对象池(Object Pool)
对象池用于管理可重复使用的对象,避免对象的频繁创建和销毁。对象池可以用在各种场景,如游戏开发中的游戏对象、数据库连接、网络资源等。
缓存池(Cache Pool)
缓存池用于管理缓存,提供快速访问数据的能力。缓存池通常用于提高系统性能和响应速度,减少数据库或其他后端系统的负载。
线程本地池(Thread Local Pool)
线程本地池为每个线程创建一组独立的资源,确保每个线程都有自己独立的资源池。这在需要避免线程间资源共享的情况下非常有用。
内存池(Memory Pool)
内存池用于管理内存块。通过预先分配一块大内存,然后从中划分小块,减少内存分配和释放的开销。这种技术在需要频繁分配和释放内存的应用中非常有用。
连接池(Connection Pool)
连接池可以用于网络连接管理。它类似于数据库连接池,但用于管理网络连接,如与服务器、外部服务等的连接。
资源池(Resource Pool)
资源池可以管理各种类型的资源,如线程、对象、连接等。资源池的目的是提高资源利用率,减少资源创建和销毁的开销。
消息池(Message Pool)
消息池用于管理消息对象,通常用于消息队列、事件系统等。消息池可以减少消息对象的创建和销毁,提高消息处理的性能。
然后是io多路复用
比如常见的“三剑客” select、poll、epoll
(环境linux c/c++)
- select
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_port = htons(8080);
address.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
fd_set readfds;
while (true) {
FD_ZERO(&readfds);
FD_SET(server_fd, &readfds);
int max_fd = server_fd;
int activity = select(max_fd + 1, &readfds, nullptr, nullptr, nullptr);
if (activity < 0 && errno != EINTR) {
std::cerr << "Select error" << std::endl;
break;
}
if (FD_ISSET(server_fd, &readfds)) {
int client_fd = accept(server_fd, nullptr, nullptr);
const char *message = "Hello from select server\n";
send(client_fd, message, strlen(message), 0);
close(client_fd);
}
}
close(server_fd);
return 0;
}
- poll
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_port = htons(8080);
address.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
pollfd fds[1];
fds[0].fd = server_fd;
fds[0].events = POLLIN;
while (true) {
int activity = poll(fds, 1, -1);
if (activity < 0) {
std::cerr << "Poll error" << std::endl;
break;
}
if (fds[0].revents & POLLIN) {
int client_fd = accept(server_fd, nullptr, nullptr);
const char *message = "Hello from poll server\n";
send(client_fd, message, strlen(message), 0);
close(client_fd);
}
}
close(server_fd);
return 0;
}
- epoll
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_port = htons(8080);
address.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int epoll_fd = epoll_create(1);
epoll_event event = {0};
event.events = EPOLLIN;
event.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
while (true) {
epoll_event events[10];
int activity = epoll_wait(epoll_fd, events, 10, -1);
if (activity < 0) {
std::cerr << "Epoll error" << std::endl;
break;
}
for (int i = 0; i < activity; i++) {
if (events[i].data.fd == server_fd) {
int client_fd = accept(server_fd, nullptr, nullptr);
const char *message = "Hello from epoll server\n";
send(client_fd, message, strlen(message), 0);
close(client_fd);
}
}
}
close(server_fd);
return 0;
}
pr & create ?
(这块我暂时想不了,系统的写一篇博客真的要点东西啊)
边缘触发 && 水平触发
边缘触发 edge trigger (ET) 水平触发 level trigger
- server
//server
#include <stdlib.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <fcntl.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>
static int do_listen(const char* host, int port) {
struct addrinfo ai_hints;
struct addrinfo* ai_list = NULL;
char portstr[16];
sprintf(portstr, "%d", port);
memset(&ai_list, 0, sizeof(ai_hints));
ai_hints.ai_socktype = SOCK_STREAM;
ai_hints.ai_protocol = IPPROTO_TCP;
int status = getaddrinfo(host, portstr, &ai_hints, &ai_list);
if (status != 0) {
return -1;
}
int fd = socket(ai_list->ai_family, ai_list->ai_socktype, 0);
if (fd < 0) {
freeaddrinfo(ai_list);
return -1;
}
status = bind(fd, (struct sockaddr*)ai_list->ai_addr, ai_list->ai_addrlen);
if (status != 0) {
close(fd);
freeaddrinfo(ai_list);
return -1;
}
listen(fd, 30);
printf("do_listen success fd:%d\n", fd);
return fd;
}
int main(int argc, char** argv) {
if (argc < 2) {
printf("please input mode, lt or et\n");
return -1;
}
int ep_event = 0;
if (strcmp(argv[1], "et") == 0) {
ep_event = EPOLLET;
}
else if (strcmp(argv[1], "lt") == 0) {
ep_event = 0;
}
else {
printf("unknow mode %s please input lt or et\n", argv[1]);
return -1;
}
int epfd = epoll_create(1024);
if (epfd == -1) {
printf("fail to create epoll %d\n", errno);
return -1;
}
int listen_fd = do_listen("127.0.0.1", 8001);
if (listen_fd < 0) {
printf("do listen fail");
return -1;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ee);
for(;;) {
printf("before epoll epoll_wait\n");
struct epoll_event ev[16];
int n = epoll_wait(epfd, ev, 16, -1);
if (n == -1) {
printf("epoll_wait error %d", errno);
break;
}
printf("after epoll_wait event n:%d\n", n);
for (int i = 0; i < n; i ++) {
struct epoll_event* e = &ev[i];
if (e->data.fd == listen_fd) {
struct sockaddr s;
socklen_t len = sizeof(s);
int client_fd = accept(listen_fd, &s, &len);
if (client_fd < 0) {
break;
}
fcntl(client_fd, F_SETFL, O_NONBLOCK);
ee.events = EPOLLIN | ep_event;
ee.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ee);
printf("accpet new connection fd:%d\n", client_fd);
}
else {
int client_fd = e->data.fd;
int flag = e->events;
int r = (flag & EPOLLIN) != 0;
if (r) {
printf("read fd:%d\n", client_fd);
char buffer[2] = {0};
int n = read(client_fd, buffer, 2);
printf("read number %d\n", n);
if (n < 0) {
switch(errno) {
case EINTR: break;
case EWOULDBLOCK: break;
default: break;
}
}
else if (n == 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
close(client_fd);
break;
}
else {
printf("----%c%c\n", buffer[0], buffer[1]);
}
}
}
}
}
return 0;
}
- client
//client
#include <stdlib.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <fcntl.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>
static int do_listen(const char* host, int port) {
struct addrinfo ai_hints;
struct addrinfo* ai_list = NULL;
char portstr[16];
sprintf(portstr, "%d", port);
memset(&ai_list, 0, sizeof(ai_hints));
ai_hints.ai_socktype = SOCK_STREAM;
ai_hints.ai_protocol = IPPROTO_TCP;
int status = getaddrinfo(host, portstr, &ai_hints, &ai_list);
if (status != 0) {
return -1;
}
int fd = socket(ai_list->ai_family, ai_list->ai_socktype, 0);
if (fd < 0) {
freeaddrinfo(ai_list);
return -1;
}
status = bind(fd, (struct sockaddr*)ai_list->ai_addr, ai_list->ai_addrlen);
if (status != 0) {
close(fd);
freeaddrinfo(ai_list);
return -1;
}
listen(fd, 30);
printf("do_listen success fd:%d\n", fd);
return fd;
}
int main(int argc, char** argv) {
if (argc < 2) {
printf("please input mode, lt or et\n");
return -1;
}
int ep_event = 0;
if (strcmp(argv[1], "et") == 0) {
ep_event = EPOLLET;
}
else if (strcmp(argv[1], "lt") == 0) {
ep_event = 0;
}
else {
printf("unknow mode %s please input lt or et\n", argv[1]);
return -1;
}
int epfd = epoll_create(1024);
if (epfd == -1) {
printf("fail to create epoll %d\n", errno);
return -1;
}
int listen_fd = do_listen("127.0.0.1", 8001);
if (listen_fd < 0) {
printf("do listen fail");
return -1;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ee);
for(;;) {
printf("before epoll epoll_wait\n");
struct epoll_event ev[16];
int n = epoll_wait(epfd, ev, 16, -1);
if (n == -1) {
printf("epoll_wait error %d", errno);
break;
}
printf("after epoll_wait event n:%d\n", n);
for (int i = 0; i < n; i ++) {
struct epoll_event* e = &ev[i];
if (e->data.fd == listen_fd) {
struct sockaddr s;
socklen_t len = sizeof(s);
int client_fd = accept(listen_fd, &s, &len);
if (client_fd < 0) {
break;
}
fcntl(client_fd, F_SETFL, O_NONBLOCK);
ee.events = EPOLLIN | ep_event;
ee.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ee);
printf("accpet new connection fd:%d\n", client_fd);
}
else {
int client_fd = e->data.fd;
int flag = e->events;
int r = (flag & EPOLLIN) != 0;
if (r) {
printf("read fd:%d\n", client_fd);
char buffer[2] = {0};
int n = read(client_fd, buffer, 2);
printf("read number %d\n", n);
if (n < 0) {
switch(errno) {
case EINTR: break;
case EWOULDBLOCK: break;
default: break;
}
}
else if (n == 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
close(client_fd);
break;
}
else {
printf("----%c%c\n", buffer[0], buffer[1]);
}
}
}
}
}
return 0;
}
阻塞与非阻塞
阻塞 I/O 在阻塞模式下,当一个 I/O 操作(例如读取数据、写入数据)被调用时,如果数据不可用或操作无法立即完成,程序会等待,直到操作完成。这意味着程序的执行将暂停,直到 I/O 操作完成。
特点 简单易用:阻塞 I/O 通常更简单,编程更容易理解。程序只需等待操作完成,无需处理异步操作。 可能导致性能瓶颈:阻塞 I/O 会导致整个线程暂停,这在处理大量并发请求时可能导致性能瓶颈。 适用于简单应用:对于不需要高并发的应用,阻塞 I/O 是一种简单有效的解决方案。 适用场景 单线程应用:在单线程环境中,阻塞 I/O 可能更容易实现,因为没有并发问题。 低并发应用:在不需要处理大量并发请求的情况下,阻塞 I/O 可以简化编程。 批处理任务:在不需要即时响应的批处理任务中,阻塞 I/O 可能是合适的选择。 非阻塞 I/O 在非阻塞模式下,I/O 操作立即返回,而不等待操作完成。如果操作无法立即完成,程序会得到一个标志(如 EAGAIN 或 EWOULDBLOCK),指示稍后再尝试。非阻塞 I/O 通常与 I/O 多路复用技术(如 select、poll、epoll)结合使用。
特点 高并发:非阻塞 I/O 适合处理大量并发请求,因为线程不会因为 I/O 操作而阻塞。 需要异步处理:非阻塞 I/O 通常需要处理异步操作,可能增加代码的复杂性。 更复杂的设计:非阻塞 I/O 需要考虑并发和同步问题,设计复杂度更高。 适用场景 高并发服务器:在处理大量并发请求时,非阻塞 I/O 可以提高服务器的吞吐量和性能。 事件驱动架构:非阻塞 I/O 通常与事件驱动架构结合使用,实现高性能服务器。 实时应用:在需要实时响应的应用中,非阻塞 I/O 可以提供更好的性能。 总结 阻塞和非阻塞 I/O 在性能和应用场景上有显著区别。阻塞 I/O 适用于简单、低并发的应用,而非阻塞 I/O 适用于高并发、事件驱动的环境。选择哪种模式取决于应用的需求、并发量和性能要求。
对于高性能服务器,非阻塞 I/O 通常是更好的选择,因为它可以处理大量并发请求,提高服务器的性能和可扩展性。阻塞 I/O 更适合简单、低并发的应用。选择哪种模式要根据具体场景和需求而定。
(问的gpt)
总结
高性能服务器,首先要完成最基本的通信问题,其次在考虑根据各种复杂环境做相应的处理。 不如,多线程就不适合做阻塞 但是,事件处理机制就不能用阻塞而要用非阻塞
有很多时候,会有这样一个idea,一个问题,站在不同角度去分析又是另个味道,下次具体化说来听听🙊