C10K问题及常见的解决方案

C10K问题及常见的解决方案本文探讨了解决 C10K 问题的多种网络编程模型 包括阻塞 I O 配合多进程或多线程 线程池 非阻塞 I O 结合事件通知以及 Reactor 模式

大家好,欢迎来到IT知识分享网。

C10K问题及常见的解决方案

瓶颈分析

文件句柄

linux系统默认支持的文件描述符大小是1024,可通过ulimit -n查看.我的wsl中默认是.

系统内存

通过如下shell代码,可查询一个socket连接对应的缓冲区大小,单位byte

xyf@Xiong-New:~$ cat /proc/sys/net/ipv4/tcp_wmem 4096 16384  xyf@Xiong-New:~$ cat /proc/sys/net/ipv4/tcp_rmem 4096   

相当于

xyf@Xiong-New:~$ cat /proc/sys/net/ipv4/tcp_wmem 4KB 16KB 4MB xyf@Xiong-New:~$ cat /proc/sys/net/ipv4/tcp_rmem 4KB 128KB 6MB 

三个值分别代表最小分配值、默认分配值和最大分配值, 按默认分配值计算,10K个连接需要1.4GB内存.

网络带宽

  • 第一个层面,应用程序如何和操作系统配合,感知 I/O 事件发生,并调度处理在上万个套接字上的 I/O 操作?前面讲过的阻塞 I/O、非阻塞 I/O 讨论的就是这方面的问题。
  • 第二个层面,应用程序如何分配进程、线程资源来服务上万个连接?这在接下来会详细讨论。

阻塞io+多进程

在这里插入图片描述

来一个连接, 服务器主进程就创建一个子进程进行客户端连接的处理, 此方法开销非常大, 但实现起来比较简单.

do{ 
    accept connections fork for conneced connection fd process_run(fd) }while(true) 

有没有知名的开源框架使用此方案?

阻塞io+多线程/线程池

在这里插入图片描述

do{ 
    accept connections pthread_create for conneced connection fd thread_run(fd) }while(true) 

来一个连接, 服务器主线程就创建一个子线程进行客户端连接的处理, 此方法开销略小, 子线程共享主线程资源,实现起来也比较简单. 但是线程的创建与销毁对资源的占用仍然不可忽视.

可以通过创建线程池进一步减小切换线程导致的开销, 以下是线程池的示意图及伪代码:

在这里插入图片描述

 thread_func() { 
    while(1){ 
    1. 上锁 2. 从连接队列中取出一个连接 if(连接队列空) 解锁, 阻塞等待队列非空条件变量为真, 返回时加锁 else 取出连接队列中的连接 3. 解锁 4. 处理io事件 } } PushToList(fd) { 
    1.上锁 2. 修改队列 if(队列非满) 将新的连接加入到队列中 else 扩展队列容量, 并将新连接加入队列中 3.解锁 4.队列非空条件变量为真 } thread_create(...,thread_func,...);// 首先创建线程池, 每个线程执行thread_func函数 while(1) { 
    int fd = accept(); connectio_list.PushToList(fd); //将连接存到连接队列中  } 

队列已满时, 需要扩容, 可采用链表的形式,动态增加连接大小,不存在扩容问题,理论上可以存无限多连接. 示意图如下:

在这里插入图片描述

线程池能扩展吗, 需要扩展吗?

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

在这里插入图片描述

本质上是通过selector事件分发器维护一个事件列表, 可通过系统内核来判断哪些事件就绪, 然后对就绪事件进行io处理. 相比于阻塞io+多进程,多线程模型, 此模型实现了单个线程进行多路io处理, 又称io多路复用. 结合非阻塞io, 可以高效的处理并发问题.

do { 
    poller.dispatch() for fd in registered_fdset{ 
    if(is_readable(fd) == true){ 
    handle_read(fd) }else if(is_writeable(fd)==true){ 
    handle_write(fd) } }while(ture) 

上述方案需要遍历注册的套接字,如果直接返回有事件发生的套接字,则是epoll方案

do { 
    poller.dispatch() for fd_event in active_event_set{ 
    if(is_readable_event(fd_event) == true){ 
    handle_read(fd_event) }else if(is_writeable_event(fd_event)==true){ 
    handle_write(fd_event) } }while(ture) 

为什么IO多路复用需要结合非阻塞IO使用?

  • 如果采用阻塞IO, 线程有可能阻塞在recv/read/write/send处, 不能重复利用单线程处理多个IO事件的能力.

Reactor模式本质上就是IO多路复用+非阻塞IO, 只不过换了一种写法, 将fd,缓冲区,回调函数包装在一起定义为一个连接对象, 有可读事件发生时调用recv_cb回调函数, 有可写事件发生时调用send_cb回调函数.

这样写的好处,我理解有两点:

  • 首先代码结构更加清晰
  • 更加面向业务, 每个连接有自己的缓冲区, 业务程序员不关心如何通过系统IO函数收发数据, 只关心数据从哪里取,处理好后放在哪里. 关于从内核拷贝到用户,从用户拷贝到内核,业务程序员并不关心.

以下是Reactor模式示意图:

在这里插入图片描述

并发量增加时,会产生一些常见文件, 见下文:
并发量增加时常见的三个问题

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

主从reactor模型

异步 I/O+ 多线程

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/109874.html

(0)
上一篇 2024-11-17 11:45
下一篇 2024-11-17 12:00

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信