C10K问题
如何在一台物理机上同时服务 10000 个用户?
C10K 问题是由一个叫 Dan Kegel 的工程师提出并总结归纳的,可参考如下链接
http://www.kegel.com/c10k.html
C10K 问题解决之道
网络编程中,涉及到频繁的用户态-内核态数据拷贝,设计不够好的程序可能在低并发的情况下工作良好,一旦到高并发情形,其性能可能呈现出指数级别的损失。
从两个方面统筹考虑
- 应用程序如何和操作系统配合,感知 I/O 事件发生,并调度处理在上万个套接字上的 I/O 操作(阻塞IO和非阻塞IO)
- 应用程序如何分配进程、线程资源来服务上万个连接
解决方案并分析:
-
阻塞IO + 进程
伪代码描述了使用阻塞 I/O,为每个连接 fork 一个进程的做法
do
{
accept connecitons
fork for connected connection fd
process_fun(fd)
}
该方法最为简单直接,每个连接通过 fork 派生一个子进程进行处理,因为一个独立的子进程负责处理了该连接所有的 I/O,所以即便是阻塞 I/O,多个连接之间也不会互相影响,但是但是效率不高,扩展性差,资源占用率高。
-
阻塞IO + 线程
伪代码描述
do
{
accept connections
pthread_create for conneced connection fd
thread_run(fd)
}while(true)
相比于进程模型占用的资源太大的问题,线程模型属于轻量级的,占用资源少。
进一步改进:
线程的创建是比较消耗资源的,况且不是每个连接在每个时刻都需要服务,因此,我们可以预先通过创建一个线程池,并在多个连接中复用线程池来获得某种效率上的提升
伪代码描述
create thread pool
do{
accept connections
get connection fd
push_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时间,而且也会出现在一个循环内啥事也没有。所以改进的方法是让操作系统告诉我们哪个套接字可读或者可写(该方法就是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 设计
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)
-
非阻塞 I/O + readiness notification + 多线程
利用CPU多核的能力,让每个核都可以作为一个 I/O 分发器进行 I/O 事件的分发,这就是所谓的主从reactor 模式,基于 epoll/poll/select 的 I/O 事件分发器可以叫做 reactor,也可以叫做事件驱动,或者事件轮询(eventloop)
-
异步 I/O+ 多线程
异步非阻塞 I/O 模型是一种更为高效的方式,当调用结束之后,请求立即返回,由操作系统后台完成对应的操作,当最终操作完成,就会产生一个信号,或者执行一个回调函数来完成 I/O 处理。
小结
- 阻塞IO+多进程——实现简单,性能一般
- 阻塞IO+多线程——相比于阻塞IO+多进程,减少了上下文切换所带来的开销,性能有所提高。
- 阻塞IO+线程池——相比于阻塞IO+多线程,减少了线程频繁创建和销毁的开销,性能有了进一步的提高。
- Reactor+线程池——相比于阻塞IO+线程池,采用了更加先进的事件驱动设计思想,资源占用少、效率高、扩展性强,是支持高性能高并发场景的利器。
- 主从Reactor+线程池——相比于Reactor+线程池,将连接建立事件和已建立连接的各种IO事件分离,主Reactor只负责处理连接事件,从Reactor只负责处理各种IO事件,这样能增加客户端连接的成功率,并且可以充分利用现在多CPU的资源特性进一步的提高IO事件的处理效率。
- 主 - 从Reactor模式的核心思想是,主Reactor线程只负责分发 Acceptor 连接建立,已连接套接字上的 I/O 事件交给 从Reactor 负责分发。其中 sub-reactor 的数量,可以根据 CPU 的核数来灵活设置。
- 在 Linux 下通用的解决高性能问题的利器是 非阻塞I/O加上epoll 机制,在利用多线程