c10k 问题
背景
上面这张图是所有 web server 的一般工作方式。
服务端 bind 到一个端口,然后开始监听。客户端 connect 上来以后,将请求发送给服务端。服务端处理完请求之后返回给客户端。
这样,一个请求就处理结束了。不过,当服务端要处理成千上万连接的请求时,我们就需要在上面的基础上考虑更多的问题。这就是大家熟悉的 The C10k Problem.
为了解决 C10k 问题,有各种各样的 I/O 策略,它们的不同之处大概如下:
- 是否一个线程当中处理多个 I/O 调用
- 单个线程阻塞同步地处理多个 IO 的方式,通过多个进程或多个线程达到并发。
- 使用非阻塞的 I/O,利用 IO 就绪通知来达到并发。这种方式只适合网络 IO,对于磁盘 IO 并不合适。
- 使用异步 IO 调用,当 IO 完成时会有通知。这种方式对于网络 IO 和磁盘 IO 都很适用。
- 如何控制服务于每个客户端的代码
- 一个进程一个客户端。这是 Unix 采用的经典方法,从 80 年代采用直到现在一直在使用。
- 一个 OS Level 的线程同时处理多个客户端
- 每个客户端分配一个 OS Level 的线程
最流行的 IO 策略
1. 一个线程服务多个客户端,使用非阻塞 I/O 和水平触发(LT)的就绪通知。
这种方式将所有的网络文件句柄的工作模式设置为 NONBLOCK,通过 select 或者 poll 方法告诉应用层哪些文件描述符准备就绪。这里的就绪方式是水平触发,只要条件满足就会触发。水平触发也就是内核通知应用层某个文件描述符已经就绪,如果应用层没有完整的处理该文件描述符,内核就会不断地通知。
那么为什么要将文件描述符的设置为 NOBLOCK 模式呢?因为内核提示的通知消息并非100%准确。很有可能内核通知的是未准备就绪的文件描述符,这时候如何是 BLOCK 模式,线程很有可能阻塞在那个位置。
同样的原因使得这种方式不适合磁盘文件的 IO 操作。将磁盘文件的操作句柄设置为 NOBLOCK 是无效的,对于磁盘文件的读写依然可能导致阻塞。一个解决办法是用 worker 线程来处理磁盘 IO。
2. 一个线程服务多个客户端,使用非阻塞 I/O 和边沿触发(ET)的就绪通知。
所谓边沿触发是相对于水平触发而言的,内核只在文件描述符状态发生变化时通知。内核通知应用层文件描述符后,除非文件描述符的数据已经完全被读取从而使得就绪状态发生变化。
3. 一个服务线程服务多个客户端,使用异步 I/O。
异步 IO 是由内核进程完成 IO 操作,并且给一个完成通知。
4. 一个服务线程服务一个客户端,使用阻塞 I/O。
这种方式最大的问题是每个线程占据一个完整的栈帧,对于内存要求大。
5. 把服务代码编译进内核。
这种方式效率最好,成本最大,通用性低。