参考:

https://time.geekbang.org/column/article/143388

C10K问题

什么是C10K 问题

C10K 问题是这样的:如何在一台物理机上同时服务 10000 个用户?这里 C 表示并发,10K 等于 10000。得益于操作系统、编程语言的发展,在现在的条件下,普通用户使用框架或库就可以轻轻松松写出支持并发超过 10000 的服务器端程序,甚至于经过优化之后可以达到十万,乃至百万的并发,但在二十年前,突破 C10K 问题可费了不少的心思,是一个了不起的突破。

C10K的本质与考虑方面

C10K 问题本质上是一个操作系统问题,一台主机上同时支持 1 万个连接,需要考虑哪些方面?

文件句柄

每个客户连接都代表一个文件描述符,一旦文件描述符不够用了,新的连接就会被放弃并产生错误。在 Linux 下,单个进程打开的文件句柄数是有限制的,没有经过修改的值一般都是 1024。可以使用 root 权限修改 /etc/sysctl.conf 文件,使得系统可以支持 10000 个描述符上限。

系统内存

每个 TCP 连接占用的资源不简单的就是一个连接套接字,还需要占用一定的发送缓冲区和接收缓冲区。Linux 5.4.0 下发送缓冲区和接收缓冲区的值如下:

leacock@leacock-virtual-machine:~$ cat /proc/sys/net/ipv4/tcp_wmem
4096    16384   4194304
leacock@leacock-virtual-machine:~$ cat /proc/sys/net/ipv4/tcp_rmem
4096    131072  6291456
leacock@leacock-virtual-machine:~$

这三个值分别表示了最小分配值、默认分配值和最大分配值 。按照默认分配值计算,一万个连接需要的内存消耗为:

发送缓冲区: 16384*10000 = 160M bytes
接收缓冲区: 87380*10000 = 880M bytes

可见支持 1 万个并发连接,当下内存并不是一个巨大的瓶颈。

网络带宽

假设 1 万个连接,每个连接每秒传输大约 1KB 的数据,那么带宽需要 10000 x 1KB/s x8 = 80Mbps。在当下千兆万兆网卡之下也是小菜一碟。

在系统资源层面,C10K 问题是可以解决的。但是,能解决并不意味着可以很好地解决。在网络编程中,涉及到频繁的用户态 - 内核态数据拷贝,设计不够好的程序可能在低并发的情况下工作良好,一旦到了高并发情形,其性能可能呈现出指数级别的损失。

两个层面考虑

要想解决 C10K 问题,就需要从两个层面上来统筹考虑。

  • 第一个层面,应用程序如何和操作系统配合,感知 I/O 事件发生,并调度处理在上万个套接字上的 I/O 操作? 可参见IO模式与IO多路复用

  • 第二个层面,应用程序如何分配进程、线程资源来服务上万个连接?

解决方案:

两条思路方向

主要思路有两个:

  • 一个是对于每个连接处理分配一个独立的进程/线程;

  • 另一个思路是用同一进程/线程来同时处理若干连接。

几种解决方案

  • 阻塞 I/O + 进程

  • 阻塞 I/O + 线程

  • 非阻塞 I/O + readiness notification + 单线程

  • 非阻塞 I/O + readiness notification + 多线程

  • 异步 I/O+ 多线程

阻塞 I/O + 进程

最为简单直接最传统的方式,每个连接通过 fork 派生一个子进程进行处理,由于一个独立的子进程负责处理了该连接所有的 I/O,所以即便是阻塞 I/O,多个连接之间也不会互相影响。方法虽然简单,但是效率不高,扩展性差,资源占用率高。要处理好父子进程、僵尸进程等。

父进程和子进程

创建一个新的进程,使用函数 fork 就可以

pid_t fork(void)
返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1

在调用该函数的进程(即为父进程)中返回的是新派生的进程 ID 号,在子进程中返回的值为 0。通过返回值可以区分父子进程然后进行相应的处理。

当一个子进程退出时,系统内核还保留了该进程的若干信息,比如退出状态。这样的进程如果不回收,就会变成僵尸进程。由父进程派生出来的子进程,也必须由父进程负责回收,否则子进程就会变成僵尸进程。

有两种方式可以在子进程退出后回收资源,分别是调用 wait 和 waitpid 函数。

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

wait 和 waitpid 函数参见 https://blog.csdn.net/csdn_kou/article/details/81091191

函数 wait 和 waitpid 都可以返回两个值,一个是函数返回值,表示已终止子进程的进程 ID 号,另一个则是通过 statloc 指针返回子进程终止的实际状态。这个状态可能的值为正常终止、被信号杀死、作业控制停止等。

处理子进程退出的方式一般是注册一个信号处理函数,捕捉信号 **SIGCHILD **信号,然后再在信号处理函数里调用 waitpid 函数来完成子进程资源的回收。SIGCHLD 是子进程退出或者中断时由内核向父进程发出的信号,默认这个信号是忽略的。

阻塞 I/O + 进程 代码示例

GitHub:BIOAndProgressDemo

fork

服务端:


#define MAX_LINE 4096
#define SERV_PORT 5555char convert_char(char c) {if ( 'A' <= c && c <= 'Z')return c + 32; // 转换小写else if ( 'a' <= c && c <= 'z')return c - 32; // 转换大写elsereturn c; // 其他不变
}void child_run(int fd) {printf("child_run int fd = %d\n",fd);char outbuf[MAX_LINE + 1];size_t outbuf_used = 0;ssize_t result;char ch[128];while (1) {bzero(outbuf,MAX_LINE + 1);bzero(ch,128);result = recv(fd, &ch, 128, 0);if (result == 0) {// 这里表示对端的socket已正常关闭.break;} else if (result == -1) {perror("read");break;}u_long len = strlen(ch);outbuf_used = 0;for (int i = 0; i < len; ++i) {outbuf[outbuf_used++] = convert_char(ch[i]);}send(fd, outbuf, outbuf_used, 0);}printf("child_run out\n");
}/*** 信号处理函数* @param sig*/
void sigchld_handler(int sig) {///  pid =  -1 等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样/// WNOHANG 若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0while (waitpid(-1, 0, WNOHANG) > 0);printf("sigchld_handler out\n");
}/*** 创建服务端套 并 返回 监听套接字* @param port  监听端口* @return 监听套接字*/
int tcp_server_listen(int port) {int listenfd;/// 监听套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);/// 填写 sockaddr_instruct sockaddr_in server_addr;bzero(&server_addr, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(port);int on = 1;/// 设置属性setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));/// 绑定ipint rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));if (rt1 < 0) {error(1, errno, "bind failed ");}/// 监听 套接字int rt2 = listen(listenfd, 1024);if (rt2 < 0) {error(1, errno, "listen failed ");}/// 捕获SIGPIPE信号  参见 https://blog.csdn.net/xinguan1267/article/details/17357093signal(SIGPIPE, SIG_IGN);return listenfd;
}
int main(int c, char **v) {/// 创建服务端int listener_fd = tcp_server_listen(SERV_PORT);/// 捕获 SIGCHLD 信号, 设置信号处理函数  sigchld_handlersignal(SIGCHLD, sigchld_handler);/// 循环 监听 有连接到来 fork 进程处理while (1) {struct sockaddr_storage ss;socklen_t slen = sizeof(ss);/// acceptint fd = accept(listener_fd, (struct sockaddr *) &ss, &slen);if (fd < 0) { /// accept 失败error(1, errno, "accept failed");exit(1);}if (fork() == 0) { /// fork 子进程 并通过返回值 区分 子父进程/// 子进程close(listener_fd); /// 关闭从父进程复制来的 listener_fdchild_run(fd); /// 运行子程序exit(0);} else {/// 父进程close(fd);}}return 0;
}

客户端:


#define MAXLINE     4096
#define SERV_PORT 5555int main() {int sockfd;struct sockaddr_in servaddr;// 创建了一个本地套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {perror( "create socket failed");}// 初始化目标服务器端的地址, TCP 编程中,使用的是服务器的 IP 地址和端口作为目标bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// 发起对目标套接字的 connect 调用if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {perror("connect failed");}char send_line[MAXLINE];bzero(send_line, MAXLINE);char recv_line[MAXLINE];bzero(recv_line, MAXLINE);// 从标准输入中读取字符串,向服务器端发送while (1) {bzero(recv_line,MAXLINE); // 注意每次清空if (fgets(send_line, MAXLINE, stdin) == NULL)break;int nbytes = sizeof(send_line);if (send(sockfd, send_line, nbytes,0) != nbytes)perror("write error");bzero(recv_line, MAXLINE); // 注意每次清空if (recv(sockfd, recv_line, MAXLINE,0) == 0)perror("server terminated prematurely");fputs(recv_line, stdout);}exit(0);
}

测试:

阻塞 I/O + 线程

使用进程模型来处理用户连接请求,进程切换上下文的代价是比较高的,有一种轻量级的模型可以处理多用户连接请求,就是线程模型。

线程由操作系统内核管理。每个线程都有自己的上下文(context),包括一个可以唯一标识线程的 ID(thread ID,或者叫 tid)、栈、程序计数器、寄存器等。在同一个进程中,所有的线程共享该进程的整个虚拟地址空间,包括代码、数据、堆、共享库等。

主要线程函数

创建线程


int pthread_create(pthread_t *tid, const pthread_attr_t *attr,void *(*func)(void *), void *arg);返回:若成功则为0,若出错则为正的Exxx值
  • 第一个参数为指向线程标识符的指针。创建线程成功,tid 就返回正确的线程 ID

  • 第二个参数用来设置线程属性。如优先级、是否为守护进程等,如无特殊设置,可以直接指定这个参数为 NUL。

  • 第三个参数是线程运行函数的起始地址

  • 最后一个参数是运行函数的参数。如果我们想给线程入口函数传多个值,那么需要把这些值包装成一个结构体

在新线程的入口函数内,可以执行 pthread_self 函数返回线程 tid。

pthread_t pthread_self(void)

终止线程

终止一个线程最直接的方法是在父线程内调用函数:

void pthread_exit(void *status)

调用这个函数之后,父线程会等待其他所有的子线程终止,之后父线程自己终止。

但是绝大多数的子线程执行体都是一个无限循环。也可以通过调用 pthread_cancel 来主动终止一个子线程,和 pthread_exit 不同的是,它可以指定某个子线程终止。

int pthread_cancel(pthread_t tid)

回收已终止线程的资源

pthread_join 回收已终止线程的资源


int pthread_join(pthread_t tid, void ** thread_return)

当调用 pthread_join 时,主线程会阻塞,直到对应 tid 的子线程自然终止。和 pthread_cancel 不同的是,它不会强迫子线程终止。

分离线程

一个线程的重要属性是可结合的,或者是分离的。一个可结合的线程是能够被其他线程杀死和回收资源的;而一个分离的线程不能被其他线程杀死或回收资源。一般来说,默认的属性是可结合的。

pthread_detach 函数可以分离一个线程:

int pthread_detach(pthread_t tid)

阻塞 I/O + 线程 代码示例

GitHub:BIOAndThreadDemo

对上面服务端稍作修改,客户端不变

pthread

服务端:

#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <error.h>
#include <errno.h>
#include <signal.h>
#include  <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>#define MAX_LINE 4096
#define SERV_PORT 5555char convert_char(char c) {if ( 'A' <= c && c <= 'Z')return c + 32; // 转换小写else if ( 'a' <= c && c <= 'z')return c - 32; // 转换大写elsereturn c; // 其他不变
}void thread_run(void *arg) {pthread_detach(pthread_self());int fd = (int)arg;printf("thread_run int fd = %d\n",fd);char outbuf[MAX_LINE + 1];size_t outbuf_used = 0;ssize_t result;char ch[128];while (1) {bzero(outbuf,MAX_LINE + 1);bzero(ch,128);result = recv(fd, &ch, 128, 0);if (result == 0) {// 这里表示对端的socket已正常关闭.break;} else if (result == -1) {perror("read");break;}u_long len = strlen(ch);outbuf_used = 0;for (int i = 0; i < len; ++i) {outbuf[outbuf_used++] = convert_char(ch[i]);}send(fd, outbuf, outbuf_used, 0);}printf("thread_run out\n");
}/*** 创建服务端套 并 返回 监听套接字* @param port  监听端口* @return 监听套接字*/
int tcp_server_listen(int port) {int listenfd;/// 监听套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);/// 填写 sockaddr_instruct sockaddr_in server_addr;bzero(&server_addr, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(port);int on = 1;/// 设置属性setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));/// 绑定ipint rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));if (rt1 < 0) {error(1, errno, "bind failed ");}/// 监听 套接字int rt2 = listen(listenfd, 1024);if (rt2 < 0) {error(1, errno, "listen failed ");}/// 捕获SIGPIPE信号  参见 https://blog.csdn.net/xinguan1267/article/details/17357093signal(SIGPIPE, SIG_IGN);return listenfd;
}
int main(int c, char **v) {/// 创建服务端int listener_fd = tcp_server_listen(SERV_PORT);pthread_t tid;/// 循环 监听 有连接到来 fork 进程处理while (1) {struct sockaddr_storage ss;socklen_t slen = sizeof(ss);/// acceptint fd = accept(listener_fd, (struct sockaddr *) &ss, &slen);if (fd < 0) { /// accept 失败error(1, errno, "accept failed");} else {pthread_create(&tid, NULL, &thread_run, (void *) fd);}}return 0;
}

测试:

非阻塞 I/O + readiness notification + 单线程(reactor)

事件驱动模型

事件驱动模型,也被叫做反应堆模型(reactor),或者是 Event loop 模型。这个模型的核心有两点。

  • 第一,它存在一个无限循环的事件分发线程,或者叫做 reactor 线程、Event loop 线程。这个事件分发线程的背后,就是 poll、epoll 等 I/O 分发技术的使用。
  • 第二,所有的 I/O 操作都可以抽象成事件,每个事件必须有回调函数来处理。通过事件分发,这些事件都可以一一被检测,并调用对应的回调函数加以处理。

single reactor thread

一个 reactor 线程上同时负责分发 acceptor 的事件、已连接套接字的 I/O 事件。

single reactor thread + worker threads

进一步优化将耗时的操作分离出来,反应堆线程只负责处理 I/O 相关的工作,业务逻辑相关的工作都被裁剪成一个一个的小任务,放到线程池里由空闲的线程来执行。当结果完成后,再交给反应堆线程,由反应堆线程通过套接字将结果发送出去。

非阻塞 I/O + readiness notification + 单线程(reactor)代码示例

参见:

Reactor模式与单线程Reactor的C和C++实现

非阻塞 I/O + readiness notification + 多线程(主 - 从 reactor)

主 - 从 reactor 模式

单 reactor 线程既分发连接建立,又分发已建立连接的 I/O;将 acceptor 上的连接建立事件和已建立连接的 I/O 事件分离,形成所谓的主 - 从 reactor 模式。

主 - 从这个模式的核心思想是,主反应堆线程只负责分发 Acceptor 连接建立,已连接套接字上的 I/O 事件交给 sub-reactor 负责分发。其中 sub-reactor 的数量,可以根据 CPU 的核数来灵活设置。

主反应堆线程一直在感知连接建立的事件,如果有连接成功建立,主反应堆线程通过 accept 方法获取已连接套接字,接下来会按照一定的算法选取一个从反应堆线程,并把已连接套接字加入到选择好的从反应堆线程中。主反应堆线程唯一的工作,就是调用 accept 获取已连接套接字,以及将已连接套接字加入到从反应堆线程中。

主 - 从 reactor+worker threads 模式

主 - 从 reactor 模式解决了 I/O 分发的高效率问题,那么 work threads 就解决了业务逻辑和 I/O 分发之间的耦合问题。

主 - 从反应堆下加上 worker 线程池。

主 - 从反应堆跟上面介绍的做法是一样的。和上面不一样的是,这里将 decode、compute、encode 等 CPU 密集型的工作从 I/O 线程中拿走,这些工作交给 worker 线程池来处理,而且这些工作拆分成了一个个子任务进行。encode 之后完成的结果再由 sub-reactor 的 I/O 线程发送出去。

非阻塞 I/O + readiness notification + 多线程(主 - 从 reactor) 代码示例

未实现

异步 I/O+ 多线程

待整理

可以看下 AIO 的新归宿:io_uring

浅谈C10K问题 与 解决方案相关推荐

  1. 浅谈薄膜行业MES解决方案

    随着国家节能减排的号召,新能源电动汽车蓬勃发展,带动整个锂电行业的崛起,锂电池的结构中,隔膜是关键的内层组件之一.隔膜的性能决定了电池的界面结构.内阻等,直接影响电池的容量.循环以及安全性能等特性,性 ...

  2. 浅谈齿轮行业MES解决方案的模块和功能

    齿轮生产业是我国装备制造业的基础性产业,产业关联度高,吸纳就业强,技术资金密集,是装备制造业实现产业升级.技术进步的重要保障.在机械工业当中,齿轮是最重要不可缺的基础部件,应用在国民经济各个领域,我国 ...

  3. (转)认识动作捕捉系统 浅谈三种主流解决方案

    转自http://news.17173.com/content/2016-03-14/20160314102718031.shtml 目前动作捕捉系统有惯性式和光学式两大主流技术路线,而光学式又分为标 ...

  4. 浅谈图片宽度自适应解决方案

    在网页设计中,随着响应式设计的到来,各种响应式设计方案层出不穷.对于图片响应式的问题也有很多前端开发人员在进行研究.比较好的图片响应式设想便是在不同的屏幕分辨率下使用不同实际尺寸的图片,而达到在高速网 ...

  5. 中小企业私有云解决方案浅谈

    http://tech.watchstor.com/cloud-storage-125882.htm 最近几年,云计算的大潮日渐澎湃,大有席卷全球,颠覆传统IT的趋势.公有云与私有云都受到了大家的关注 ...

  6. 浅谈对腾讯云微信小程序解决方案服务端的理解(主要针对信道服务)

    浅谈对腾讯云微信小程序解决方案服务端的理解(主要针对信道服务) 参考文章: (1)浅谈对腾讯云微信小程序解决方案服务端的理解(主要针对信道服务) (2)https://www.cnblogs.com/ ...

  7. 浅谈web应用的负载均衡、集群、高可用(HA)解决方案

    浅谈web应用的负载均衡.集群.高可用(HA)解决方案 转载于:https://www.cnblogs.com/hfultrastrong/p/7887420.html

  8. 浅谈mysql主从复制的高可用解决方案

    1.熟悉几个组件(部分摘自网络) 1.1.drbd      -- DRBD(Distributed Replicated Block Device),DRBD号称是 "网络 RAID&qu ...

  9. 跨平台开发解决方案浅谈

    跨平台开发解决方案浅谈 前言 本文所讨论技术都是基于大前端生态下的 本文所指跨平台概念无特殊说明时泛指跨安卓和ios 用前端工程化技术开发出app也是本文所讲重点 作为用户来说,当然希望使用App的时 ...

  10. 浅谈政务行业售前架构(解决方案)岗位应知应会

    如果这是一个命题<浅谈政务行业售前架构岗位应知应会>,那么我下面会从工作实际情况进行阐述: 一.但凡谈到政务行业,一定少不了公检法司.教育.文旅.发改工信科技委员会等等政务机构,今天只谈谈 ...

最新文章

  1. DAC输出波形的最大频率是多少?
  2. python是人工智能最大骗局-Python与人工智能的关系原来是这样的...
  3. Keras梯度累积优化器:用时间换取效果
  4. html5的消息通知
  5. vue解构赋值_前端开发es6知识 模块化、解构赋值、字符串模板
  6. Android 开发总结大全
  7. 快速搭建移动应用直传服务
  8. 车位编号lisp_CAD自动编号操作
  9. atitit. 深入理解Cohesion)原理ad  attilax大总结
  10. iOS HealthKit
  11. 5G端到端网络切片进展与挑战分析
  12. Flink简介以及与sparkStreaming和Storm比较
  13. 光盘自动播放 html,插入dvd光盘直接播放 禁用dvd菜单制作刻录dvd光盘 光盘直接放在DVD中就可以自动播放...
  14. 数据量太大,节省内存的几种方式
  15. KB和kb的区别以及我的网速
  16. c语言程序设计作业心得,C语言程序设计实习心得体会
  17. postgrepSQL
  18. Black-Lives-Matter-Resources
  19. 能在html中打开ppt吗,怎样在PPT中直接打开网页
  20. ReadProcessMemory Types of actual and formal var parameters must be identical

热门文章

  1. 又在折腾那个手机骷髅病毒,真无聊
  2. Awesome Mac :好用的Mac软件和教程
  3. 不要效法世界只要爱天父
  4. TIMESTEN安装配置指南-中文版
  5. 使用wordPress搭建个人博客
  6. win7 计算机刷新dns,刷新dns,教您怎么刷新DNS
  7. catia怎么将特征参数化_catia 怎么做参数化设计
  8. Java二维码生成代码
  9. 360浏览器兼容css,css样式怎样调360浏览器的兼容性
  10. Javascript分割/截取/连接字符串