【笔记】 c10K

2013 年 02 月 19 日

写在前面

《The C10K problem》 是十年前的经典文章了, 虽然现在动不动都爱讨论 C100K 乃至 C1000K, 但是经典的东西还是值得回顾.

比较知名的 IO 框架

  • 重量级的 C++ IO 框架, 其中的 Reactor 框架用 OO 的方式处理非阻塞 IO, Proactor 框架用 OO 方式处理异步IO. ACE 的库代码量不小, 真正核心的东东其实不多, 值得学习.
  • C++ 的 IO 框架, 慢慢成为了 Boost 库的一部分.
  • 轻量级的 C IO 框架, 支持 epoll, poll, select, kqueue. 它的分级触发机制 (level-triggered) 各有利弊.

IO 策略

  • 在单线程中是否处理多个 IO, 如何处理?

    • 不处理, 只用阻塞/同步 IO, 通过多线程或者多进程的方式实现并发.
    • 使用非阻塞调用(例如设置 Socket 为 O_NONBLOCK), 当 IO 可用时发出通知.
    • 使用异步调用(例如 aio_write), IO 完成时发出通知.
  • 如何控制每个客户端?

    • 每个客户端一个进程(经典的 Unix 实现方式).
    • 单 OS 线程中处理多个客户端.
    • 每个客户端一个 OS 线程.
    • 每个 Active 客户端一个 OS 线程(例如: Tomcat + Apache, 线程池).
  • 是否使用标准的 OS services, 还是把代码加到 Kernel 中, 例如定制驱动, Kernel模块等.

常用的 IO 策略

  • 每个线程多个客户端, 使用非阻塞 IO 和水平触发通知.

    设置网络句柄非阻塞模式(nonblock), 然后使用 Select 或者 Poll 来等待返回可处理的通知. 当磁盘文件不在内存中时, 通过 read() 或者 sendfile() 从磁盘读取文件会成为瓶颈. 所以如果处理磁盘 IO, 最好还是使用异步 IO. 如果在没有 AIO 的系统中, 靠谱的解决方法是当收到通知时, 开启一个 worker 线程来单独处理, 主线程继续处理其他通知.

    • 传统的 select(), 受限于 FD_SETSIZE 句柄数, 这个限制可能被编译进了 stdlib, 改起来麻烦.
    • 传统的 poll(), 由于 poll 的轮询机制, 在句柄数达到K级别时, 速度会变得很慢.
    • /dev/poll, Solaris 上的推荐方式, 核心思想是 poll() 调用时的大部分时间内, 句柄是不变的, 通过写句柄到 /dev/poll 中并读取, 可以提高性能.
    • kqueue, FreeBSD 上的推荐方式, 可以水平触发, 也可以边缘触发.
    • epoll, Linux 上的高性能实现方式, 可以水平触发, 也可以边缘触发.
  • 每个线程多个客户端, 使用非阻塞 IO 和边缘触发通知.

    相比于水平触发通知方式, 性能更高, 推荐方式, 但是在代码编写时也需要更加小心.

    补充说明一下: 如果就是单线程实现的话, 无法充分利用多核 CPU, 如果多线程实现的话, 代码复杂度会加大, 甚至某些 OS 不支持多线程的 IO 触发通知.

  • 每个线程多个客户端, 使用异步 IO.

    Linux 2.5.32 开始支持 Linux AIO, 但是没有采用内核线程, 而是使用了一个高效的 underlying API, 目前( Linux 2.6.* ? ) 不支持 AIO Socket. 一般 Windows 平台上 AIO 应用比较广泛.

  • 每个线程一个客户端, 阻塞 IO.

    把复杂度转移给 OS 去处理, 比较耗性能, 小规模的应用倒也不失为一个选择.

  • 把 IO 服务代码编译进内核

    大多人不是很赞成这么干.

其他的一些观点

  • epoll, kqueue, /dev/poll.

    epoll 是 Linux 的方案, kqueue 是 FreeBSD 的方案, /dev/poll 是最古老的 Solaris 的方案, 使用难度依次递增.

    简单的说, 这些 API 做了两件事:

    1. 避免了每次调用 select/poll 时 Kernel 分析参数建立事件等待结构的开销, Kernel 维护一个长期的事件关注列表, 应用程序通过句柄修改这个列表和捕获 I/O 事件.
    2. 避免了 select/poll 返回后, 应用程序扫描整个句柄表的开销xi, Kernel 直接返回具体的事件列表给应用程序.
  • Select 服务器的一份测试数据.

  • zero-copy, sendfile(), 努力避免在 IO 过程中的无谓的数据拷贝带来的性能流失.

  • TCP_CORK 避免频繁的发送碎片数据, 不过在某些场合不适合, 例如实时性高的游戏服务器. 其他可以参考 《TCP 参数调优》.