网络性能上限瓶颈

C10K

什么是 C10K 问题?C 指的是 client,所谓的 C10K 就是系统如何支持 1 万并发的客户端请求

I/O 事件通知的方式

水平触发和边缘触发,用在套接字的文件描述符中

水平触发

只要文件描述符可以非阻塞地执行 I/O,就会触发通知。

应用程序可以随时检查文件描述符的状态,根据状态进行 I/O 操作

边缘触发

只有文件描述的状态发生改变时,也就是 I/O 请求到达的时候才触发通知。

此时程序需要尽可能多地执行 I/O,知道无法继续读写。

如果 I/O 没有执行完或者没有来得及处理,那么这次通知也就丢失了。

I/O 多路复用

非阻塞I/O + 水平触发

select 和 poll 需要从文件描述符列表中找到可以执行的 I/O,然后进行真正的网络 I/O 读写。

因为是非阻塞I/O,一个线程可以监控一组fd

问题:需要轮询所有的 fd,当请求多的时候就会慢

select

  • 使用固定长度的位相量来表示 fd 的集合,所以有最大文件描述符的限制。
  • 内部就是轮询实现的,加上通知之后程序的循环,就循环了两次

poll

  • 没有了固定长度的数组,所以没有 fd 的限制
  • 但还是轮询的,所以一样

两者每次调用需要将 fd 的集合在用户和内核空间传递,也有成本

非阻塞I/O + 边缘触发

epoll

  • 使用红黑树管理 fd
  • 在内核中管理 fd,不用切换
  • 使用事件驱动机制,只关注有 I/O 事件的 fd

由于边缘触发只在 fd 可读写的时候才有通知,所以程序需要尽可能多的执行,并且需要处理更多的异常事件

异步I/O

异步 I/O 允许应用程序发起很多 I/O 操作,而不用等待完成。在 I/O 完成之后系统用事件通知告诉应用程序,此时应用程序才去查询 I/O 操作的结果

工作模型优化

主进程 + 多个 worker 子进程

主进程 bind() + listen() 子进程都通过 accept() 或 epoll_wait() 来处理相同的 socket。

惊群问题

当网络发生 I/O 时多个进程同时被唤醒,但是最后只有一个去处理

解决:nginx 通过全局锁解决,只有抢到锁的才会加入 epoll 中

监听相同端口的多进程

所有进程都监听相同端口,开启 SO_REUESPORT 由内核将请求负载均衡到进程中,内核确保了只有一个进程被唤醒,不会惊群

但 SO_REUSEPORT 需要 linux3.9 以上

继续优化?C100K? C10M?

借助硬件来完成,或者考虑 DPDK 和 XDP 来做,直接跳过内核协议栈,直接由用户态进程通过轮询来处理网络接收

IO 多路复用好文

https://segmentfault.com/a/1190000003063859

https://ninokop.github.io/2018/02/18/go-net/