从C10K问题来看常见的中小型服务器I/O模型

问题描述:

关于C10问题的经典描述可以查看这个网页

具体来说就是服务器如何处理10k个客户端的并发连接,即 concurrent 10,000 connection。如果在很早以前互联网还不普及的时候,一个服务器很少会同时出现有10k的连接,但是现在互联网高速发展,这种规模的连接可能随处可见,所以如何来解决C10k的问题对于服务提供者来说是一个最先需要解决的问题。有人可以说现在硬件成本很低,连接增多可能会消耗很大的内存那么我扩充内存就可以了,cpu的负载很高,那么我提高cpu的性能就可以了。

OK,这种解决方案类似于兵来将挡,水来土掩,感觉很easy,但不幸的是,连接的客户端数超过一定的规模之后对服务器的资源的要求往往是不是线性的,大多数是O(n^2)的需求,所以你再任性,再有钱,传统的解决方案也是解决不了的。

同时,服务商一般都会从经济的角度去考虑问题,而对于互联网公司来说,很大的一笔成本就是硬件的投入和消耗,当然电费也在里面。所以如果相同的硬件成本和电费可以处理更多的请求完成更多的事情,我想他们何乐不为?

解决方案:

解决C10k问题的总体的思路就是从两个方面来考虑:

1.应用程序以何种方式和操作系统合作来处理I/O的问题,阻塞,非阻塞,还是异步I/O?2. 应用程序如何处理任务和线程/进程的关系, one task one thread/process, 还是one thread/process more tasks, or thread poll ?

展开来说主要有:

应用程序以何种方式和操作系统合作来处理I/O

0. 阻塞式的I/O,例如read()函数,这种模型如果想实现并发就必须要使用多线程机制来完成。

1. 非阻塞I/O,例如I/O多路复用中的select,poll等通过设置句柄的方式在I/o就绪的时候由内核通知应用程序进行读写,epoll则直接通知进行那个文件描述符的读写,当然还有将传统的阻塞式I/O函数,如write()设置为O_NONBLOCK的模式来进行非阻塞的写。当然,这种方法只是适合网络IO,对于磁盘IO这种方法并不合适。

2. 使用异步IO调用(比如aio_write())来启动IO,这种I/O由内核线程来完成I/O操作,并在完成之后通知应用程序,由于内核线程的效率较高,所以可以取得很好的效率,但是遗憾的是在*nix中并没有对此种模式直接的支持,而在windows中是可以的。这种方法对于网络IO和磁盘IO都很适合。

应用程序如何处理任务和线程/进程的关系

以客户端连接服务器的情况来说,,主要有下面的几个模型

1.为每个客户端分配一个OS Level的线程,因为往往进程消耗的资源较多,并且在创建和切换上比较麻烦,所以后来OS开始支持轻量级的进程,也即线程来为每个连接做服务。但是,后来随着硬件资源越来越便宜,进程模型又变的比线程模型要好,因为进程之间不牵扯到资源的同步问题,关于这些这里不做讨论

2. 一个线程处理多个客户端等。

具体的I/O模型

(注:这里的内容来自于网上其他作者的资源,主有:)

这种方式很简单,它将所有的网络文件句柄的工作模式都设置成NON-BLOCKING,通过调用select()方法或者poll()方法来告诉应用层哪些个网络句柄有正在等待着并需要被处理的数据。这是一种非常传统的方法。通过这种机制,内核能够告诉应用层一个文件描述符是否准备好了(这里的准备好有着明确的含义,对于读描述符,准备好了意味着此时该描述符的缓冲区内数据已经准备好,读取该描述符的数据不会发生阻塞,而对于写描述符而言,准备好了意味着另外一层含义,它意味着写缓冲区已经准备好了,此时对该操作符的写操作也将不会导致任何阻塞发生),以及你是否已经利用该文件描述符作了相应的事情。因为这里的就绪通知方式是水平触发,也就说如果内核通知应用层某一个文件描述符已经就绪,而如果应用层此后一直没有完整的处理该描述符(没有读取完相应的数据或者没有写入任何数据),那么内核会不断地通知应用层该文件描述符已经就绪。这就是所谓的水平触发L-T:只要条件满足,那内核就会触发一个事件(只要文件描述符对应的数据没有被读取或者写入,那内核就不断地通知你)。

需要注意的是:内核的就绪通知只是一个提示,提示也就意味着这个通知消息未必是100%准确的,当你读取一个就绪的读文件描述符时,实际上你有可能会发现这个描述符对应的数据并没有准备好。这就是为什么如果使用就绪通知的话一定要将文件描述符的模式设置成NOBLOCK的,因为NOBLOCK模式的读取或者写入在文件描述符没有就绪的时候会直接返回,而不是引起阻塞。如果这里发生了阻塞,那将是非常致命的,因为我们只有一个线程,唯一的线程被阻塞了的话,那我们就玩完了。

这种方式的一个缺陷就是不适用磁盘文件的IO操作。将磁盘文件的操作句柄的工作模式设置成NOBLOCK是无效的,此时对该磁盘文件进行读写依然有可能导致阻塞。对于缺乏AIO(异步IO)支持的系统,将磁盘IO操作委托给worker线程或者进程是一个好方法来绕过这个问题。一个可行的方法是使用memory mapped file,然后调用mincore(),mincore会返回一个向量来表示相应的page是否在ram缓存中,如果page不在ram缓存中,则意味着读取该页面会导致page falut,从而引起阻塞,那么就需要通过委托的worker线程来进行IO操作。这种方式的实现方法在Linux上就是select,poll这样的系统调用。

“人”的结构就是相互支撑,“众”人的事业需要每个人的参与。

从C10K问题来看常见的中小型服务器I/O模型

相关文章:

你感兴趣的文章:

标签云: