C10K 问题

随着互联网的蓬勃发展,一个非常重要的问题摆在计算机工业界面前。这个问题就是如何使用最低的成本满足高性能和高并发的需求。这个问题在过去可能不是一个严重的问题,但是在 2000 年前后,互联网用户的人数井喷,如果说之前单机服务的用户数量还保持在一个比较低的水平,比如说只有上百个用户,那么在互联网逐渐普及的情况下,服务于成千上万个用户就将是非常普遍的情形,在这种情形下,如果还按照之前单机的玩法,成本就将超过人们想象,只有超级有钱的大玩家才可以继续下去。

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

操作系统层面

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

文件句柄

首先我们知道每个客户连接都代表一个文件描述符,一旦文件描述符不够用了,新的连接就会被放弃,产生如下的错误:

Socket/File:Can't open so many files

在 Linux 下,单个进程打开的文件句柄数是有限制的,没有经过修改的值一般都是 1024。

$ulimit -n
1024

这意味着最多可以服务的连接数上限只能是 1024。不过,我们可以对这个值进行修改,比如用 root 权限修改 /etc/sysctl.conf 文件,使得系统可用支持 10000 个描述符上限。

fs.file-max = 10000
net.ipv4.ip_conntrack_max = 10000
net.ipv4.netfilter.ip_conntrack_max = 10000

系统内存

每个 TCP 连接占用的资源可不止一个连接套接字这么简单,在前面的章节中,我们多少接触到了类似发送缓冲区、接收缓冲区这些概念。每个 TCP 连接都需要占用一定的发送缓冲区和接收缓冲区。

下面一段 shell 代码,分别显示了在 Linux 4.4.0 下发送缓冲区和接收缓冲区的值。

$cat   /proc/sys/net/ipv4/tcp_wmem
4096    16384   4194304
$ cat   /proc/sys/net/ipv4/tcp_rmem
4096    87380   6291456

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

发送缓冲区: 16384*10000/8 = 20M bytes
接收缓冲区: 87380*10000/8 = 110M bytes

当然,我们的应用程序本身也需要一定的缓冲区来进行数据的收发,为了方便,我们假设每个连接需要 128K 的缓冲区,那么 1 万个链接就需要大约 1.2G 的应用层缓冲。

这样,我们可以得出大致的结论,支持 1 万个并发连接,内存并不是一个巨大的瓶颈。

网络带宽

假设 1 万个连接,每个连接每秒传输大约 1KB 的数据,那么带宽需要 10000 x 1KB/s x8 = 80Mbps。这在今天的动辄万兆网卡的时代简直是小菜一碟。

C10K 问题解决之道

通过前面我们对操作系统层面的资源分析,可以得出一个结论,在系统资源层面,C10K 问题是可以解决的。

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

举一个例子,如果没有考虑好 C10K 问题,一个基于 select 的经典程序可能在一台服务器上可以很好处理 1000 的并发用户,但是在性能 2 倍的服务器上,却往往并不能很好地处理 2000 的并发用户。

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

第一个层面,应用程序如何和操作系统配合,感知 I/O 事件发生,并调度处理在上万个套接字上的 I/O 操作? 阻塞 I/O、非阻塞 I/O 讨论的就是这方面的问题。

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

这两个层面的组合就形成了解决 C10K 问题的几种解法方案,下面我们一起来看。

阻塞 I/O + 进程

这种方式最为简单直接,每个连接通过 fork 派生一个子进程进行处理,因为一个独立的子进程负责处理了该连接所有的 I/O,所以即便是阻塞 I/O,多个连接之间也不会互相影响。

这个方法虽然简单,但是效率不高,扩展性差,资源占用率高。

下面的伪代码描述了使用阻塞 I/O,为每个连接 fork 一个进程的做法:

do{accept connectionsfork for conneced connection fdprocess_run(fd)
}

虽然这个方式比较传统, 但是可以很好地帮我们理解父子进程、僵尸进程等,我们将在下一讲中详细讲一下如何使用这个技术设计一个服务器端程序。

阻塞 I/O + 线程

进程模型占用的资源太大,幸运的是,还有一种轻量级的资源模型,这就是线程。

通过为每个连接调用 pthread_create 创建一个单独的线程,也可以达到上面使用进程的效果。

do{accept connectionspthread_create for conneced connection fdthread_run(fd)
}while(true)

因为线程的创建是比较消耗资源的,况且不是每个连接在每个时刻都需要服务,因此,我们可以预先通过创建一个线程池,并在多个连接中复用线程池来获得某种效率上的提升。

create thread pool
do{accept connectionsget connection fdpush_queue(fd)
}while(true)

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

应用程序其实可以采取轮询的方式来对保存的套接字集合进行挨个询问,从而找出需要进行 I/O 处理的套接字,像下面给出的伪码一样,其中 is_readble 和 is_writeable 可以通过对套接字调用 read 或 write 操作来判断。

for fd in fdset{if(is_readable(fd) == true){handle_read(fd)}else if(is_writeable(fd)==true){handle_write(fd)}
}

但这个方法有一个问题,如果这个 fdset 有一万个之多,每次循环判断都会消耗大量的 CPU 时间,而且极有可能在一个循环之内,没有任何一个套接字准备好可读,或者可写。

既然这样,CPU 的消耗太大,那么干脆让操作系统来告诉我们哪个套接字可以读,哪个套接字可以写。在这个结果发生之前,我们把 CPU 的控制权交出去,让操作系统来把宝贵的 CPU 时间调度给那些需要的进程——这就是 select、poll 这样的 I/O 分发技术

于是,程序就变成了这样:

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)

但是,这样的方法需要每次 dispatch 之后,对所有注册的套接字进行逐个排查,效率并不是最高的。

如果 dispatch 调用返回之后只提供有 I/O 事件或者 I/O 变化的套接字,这样排查的效率不就高很多了么?——这就是epoll的设计思想

于是,基于 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)

Linux 是互联网的基石,epoll 也就成为了解决 C10K 问题的钥匙。FreeBSD 上的 kqueue,Windows 上的 IOCP,Solaris 上的 /dev/poll,这些不同的操作系统提供的功能都是为了解决 C10K 问题。

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

前面的做法是所有的 I/O 事件都在一个线程里分发,如果我们把线程引入进来,可以利用现代 CPU 多核的能力,让每个核都可以作为一个 I/O 分发器进行 I/O 事件的分发。——这就是所谓的主从 reactor 模式

基于 epoll/poll/select 的 I/O 事件分发器可以叫做 reactor,也可以叫做事件驱动,或者事件轮询(eventloop)。

我没有把基于 select/poll 的所谓“level triggered”通知机制和基于 epoll 的“edge triggered”通知机制分开(C10K 问题总结里是分开的),我觉得这只是 reactor 机制的实现高效性问题,而不是编程模式的巨大区别。

异步 I/O+ 多线程

异步非阻塞 I/O 模型是一种更为高效的方式,当调用结束之后,请求立即返回,由操作系统后台完成对应的操作,当最终操作完成,就会产生一个信号,或者执行一个回调函数来完成 I/O 处理。——这就是Linux 下的 aio 机制。

总结

支持单机 1 万并发的问题被称为 C10K 问题,为了解决 C10K 问题,需要重点考虑两个方面的问题:

  • 如何和操作系统配合,感知 I/O 事件的发生?
  • 如何分配和使用进程、线程资源来服务上万个连接?

基于这些组合,产生了一些通用的解决方法,在 Linux 下,解决高性能问题的利器是非阻塞 I/O 加上 epoll 机制,再利用多线程能力

C10K问题—高并发网络IO模型相关推荐

  1. 网络编程实战之高级篇, 彻底解决面试C10k问题, 高并发服务器, IO多路复用, 同时监视多个IO事件

    目录 一.前言 二.IO多路复用的理解 三.IO多路复用的发展 select poll epoll ​四.C10K服务端代码 五. 总结 一.前言 网络入门篇,从操作系统的层次推开网络大门 网络入门基 ...

  2. 网络编程(三):Linux 网络IO模型、select、pool、epoll 内核设计

    Linux网络IO模型 同步和异步,阻塞和非阻塞 同步和异步 关注的是调用方是否主动获取结果 同步: 同步的意思就是调用方需要主动等待结果的返回 异步: 异步的意思就是不需要主动等待结果的返回,而是通 ...

  3. 大白话详解5种网络IO模型

    1 前言 我们都知道,为了实现高性能的通信服务器,BIO在高并发的情况下会出现性能急剧下降的问题,甚至会由于创建过多线程而导致系统OOM.因此在Java业界,BIO的性能问题一直被开发者所诟病,所幸的 ...

  4. 网络IO模型的深入浅出

    标题索引 追溯IO原因 网络数据流 网络IO模型 IO模型举例 追溯IO原因     从事项目多年来,有个问题一直困扰着我,但因种种原因一直没有翻阅资料去释怀,随着项目经历的增加.年龄的增长和责任的使 ...

  5. Linux 网络 IO 模型

    写在前面 本文主要介绍 Unix/Linux 下五种网络 IO 模型,但是.为了更好的理解下面提到的五种网络 IO 的概念,我们有必要先理清下面这几个概念. 用户空间与内核空间 一个计算机通常有一定大 ...

  6. python 网络编程 异步io_python网络编程——网络IO模型

    服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型. (2)同步非阻塞IO(Non-blocking IO):默认创建的s ...

  7. 网络模型——四种常见网络IO模型

    文章目录 1.IO读写原理 1.1 内核缓冲区和进程缓存区 1.1.1 用户进程和操作系统 1.1.2 缓冲区的目的 1.2 Java读写IO底层流程 2.四种主要的IO模型 2.1 基本概念 2.1 ...

  8. C++后台开发—网络IO模型与Reactor模式

    一.三种网络IO模型: 分类: BIO 同步的.阻塞式 IO NIO 同步的.非阻塞式 IO AIO 异步非阻塞式 IO 推荐视频: C++架构师学习地址:C/C++Linux服务器开发高级架构师/L ...

  9. 五种网络IO模型详解

    一 IO操作本质 数据复制的过程中不会消耗CPU # 1 内存分为内核缓冲区和用户缓冲区 # 2 用户的应用程序不能直接操作内核缓冲区,需要将数据从内核拷贝到用户才能使用 # 3 而IO操作.网络请求 ...

  10. 5种网络IO模型介绍

    5种网络IO模型介绍 IO 模型分为以下几种: 阻塞IO 非阻塞IO 信号驱动IO IO多路复用 异步IO 前四个为同步IO 1 阻塞IO 一个IO操作需要两步: 等待数据和拷贝数据. blockin ...

最新文章

  1. pandas 中有关isin()函数的介绍,python中del解释
  2. linux 修改权限 anyone,linux 进程管理工具
  3. 计算机设备的热量,帮我计算机一下这块冰能吸收多少热量?
  4. 托管 非托管_如何在托管Kubernetes上还原Neo4J备份
  5. 实时数仓入门训练营:基于 Apache Flink + Hologres 的实时推荐系统架构解析
  6. ifix怎么装服务器系统上,ifix服务器和客户端配置
  7. angularjs php 实例下载,AngularJS Eclipse 1.2.0 插件下载
  8. VS中监视窗口,即时窗口和输出窗口的使用
  9. 【我的物联网成长记17】一条物联网设备控制命令的一生
  10. C++面向对象编程之类的使用(基础案例学习)
  11. 二相步进电机和三相步进电机有什么区别?
  12. Mysql优化系列(1)--Innodb引擎下mysql自身配置优化
  13. Puppeteer -headless Chrome 的 Node.js API
  14. python怎么写判断语句_Python中的if判断语句入门
  15. TZOJ 1072: 编辑距离(动态规划)
  16. 如何通过蒲公英批量获取iPhone设备的udid
  17. oracle同义词只有谁可以使用,oracle 同义词(synonym)
  18. SVN报错Skipped ‘xxxController.class.php‘ -- Node remains in conflict
  19. python 百度地图api热力图,Python+百度API 画出美美哒热力地图(代码+数据)
  20. android手机管理器在哪里打开,安卓手机re管理器在哪里打开图文教程

热门文章

  1. java塞班播放器_coreplayer移动播放器下载塞班版s60v5 v1.42_手机通用播放器
  2. WPF TextBox 设置多行
  3. SSM高校实验室安全培训系统设计与实现.docx
  4. erp软件涉及哪些计算机技术?,ERP软件应该学习哪些内容?
  5. 管家婆财贸双全 凭证记账 Date exceeds maximum of 19-12-31 报错解决办法
  6. 永久免费的pdf编辑器
  7. EditPlus中文版64bit百度网盘下载
  8. uniapp分销商城源码开发
  9. 虚幻4引擎实现自动开门蓝图
  10. 小米手机adb命令解锁BL_小米10解锁BL刷机卸载自带APP等