⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7158097493031567373 「格格步入」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

一、前言

「本文围绕 C10K 问题展开,讲述 I/O 多路复用:」

IO多路复用

「本文提要:」 发现总结C10K 问题 -> 操作系统内核支撑不足 -> 提出 I/O 多路复用。

C10K 问题

「背景:」 互联网刚开始普及、用户群体几何增长,但单机服务器性能无法被完全充分利用,要投入更多服务器

  • 当时服务器采用 PPCProcess Per Connection)模型」 :指每次有新的连接就创建一个进程去专门处理这个连接。
  • C10K:」 有 1万个连接,服务器就要创建 1万个进程,当时单机操作系统无法承受(出现效率低下、甚至瘫痪)。
  • 在这情况下,「即使提高服务器配置,并发能力也不能线性提升:」 假设原单机能处理 1000 并发,现提高一倍服务器性能,单机处理并发达不到 2000。

IO多路复用

「归结问题:如何突破单机性能局限」

C10K 问题由来:」 这些局限和问题最早被 Dan Kegel 进行了归纳和总结,并成系统地分析和提出解决方案,后来这种普遍的网络现象和技术局限都被大家称为 C10K 问题。

  • C10K 问题的最大特点:」 设计不够良好的程序,其性能和连接数及机器性能的关系往往是非线性的。

  • C10K 问题本质上:」「操作系统」 的问题,传统的同步阻塞 I/O 模型。

      1. 创建进程/线程过多:进程是最昂贵资源。
      1. 数据频繁拷贝。
      1. 进程/线程上下文切换消耗大。

C10K 问题的解决方案:I/O 多路复用」,每个进程/线程能同时处理多个连接。

I/O 多路复用,实现方式有多种:」

  1. select 方式:」 有连接请求抵达再检查处理,查询所有文件句柄。
  2. poll 方式:」 设计新的数据结构,解决 select 的两个问题。
  3. epoll 方式:」 只返回状态变化的文件句柄。

I/O 模型:同步、异步、阻塞、非阻塞

「再来回顾下 I/O 模型:」

  • 「阻塞和非阻塞的区别:」 线程是否挂起。
  • 「异步和同步的区别:」 主动与被动的通知方式。

IO多路复用

UNIX网络编程 中定义 五种 I/O 模型:」

  • 等待数据: 等待数据就绪,数据从网卡写入到内存。
  • 「将数据从内核复制到用户空间:」 将数据从内核空间写到用户空间,这才能给应用使用。

IO多路复用

根据上述定义,前4种模型都是同步 I/O 模型:因为真正的 I/O 操作(recvfrom)将阻塞进程。

「问题来了:epoll 是异步非阻塞嘛?」

「结论是:epoll 是同步非阻塞。」

网上很多说 epoll 是异步非阻塞,但我们从 UNIX 这定义上来看:它就不是。那 Linux 上加上 MMAP 呢,这样在『数据从内核拷贝到用户空间』这一步不就省了嘛,那在这步就没有阻塞了,那就能算得上是 异步非阻塞。这个说法有点牵强,定义了模式了,具体实现是在这框定下进行。

Tipsepoll 里没有涉及 MMAP。」

Reactor 是同步非阻塞网络模型」

这个很好理解:因为还有一个 Proactor 模式。

  • Reactor:关注写入事件和读取事件,需要应用程序自己完成读取或者写入数据。
  • Proactor:关注写入完成事件和读取完成事件。

I/O 多路复用:select/poll/epoll

IO多路复用

这一趴,只介绍这三种方式:遇到什么问题、解决了什么问题、有什么缺陷。

「首先是暴力法:直接循环每个连接(对应 socket)」

  • 当所有的 socket 都有数据时,这种方式是可行的。
  • 当应用读取某个 socket 时,没有数据,那么整个应用会阻塞在这。

1) select

select 的提出是为了解决暴力法的阻塞问题。

select 方案:」 在读取文件句柄之前,「先检查其状态」ready 就处理。

  • fd_set_ 结构体来存储文件句柄,表示内核同时关注这些句柄。
  • FD_ISSET 方法来查看哪个文件句柄的状态发生了变化。

#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)

// 结构体 (bitmap)
typedef struct {
unsigned long fds_bits[__FDSET_LONGS];
} fd_set;

// 返回值就绪描述符的数目
int select(
int max_fd,
fd_set *readset,
fd_set *writeset,
fd_set *exceptset,
struct timeval *timeout // 阻塞时长
)

FD_ZERO(int fd, fd_set* fds) // 清空集合, 置为 0
FD_SET(int fd, fd_set* fds) // 将给定的描述符加入集合, 某位置为 1
FD_ISSET(int fd, fd_set* fds) // 判断指定描述符是否在集合中, 某位是否为 1
FD_CLR(int fd, fd_set* fds) // 将给定的描述符从文件中删除,某位置为 0

「缺点有:」

  1. 「性能开销大:」 每次需要轮询 fd_set 每一位。
  2. 「性能开销大:」 调用 select 时,需要将参数中 fd_set 从用户空间拷贝到内核空间。
  3. 「同时监听的文件句柄数量少:」 受限于 FD_SETSIZE 大小,一般 1024,编译内核时就确定了无法更改。

2) poll

poll 主要解决 select 文件句柄数少的问题。」

  • 解决方法:定义新的结构体。
  • 在用户空间通过**「数组」方式传递文件描述符,在内核空间会转为「链表」**方式进行存储,所以没有最大数量的限制。

// 结构体
struct pollfd {
int fd; // 需要监视的文件描述符
short events; // 需要内核监视的事件
short revents; // 实际发生的事件
};

// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

「缺点:同 select 。」

3) epoll

epoll 解决了 select/poll 的缺点,成为了 C10K Killer:」

  1. 「解决每次查询文件句柄:」

    • 使用**「红黑树」存储文件句柄、使用「队列」**存储就绪的文件句柄。
  • 通过事件更改文件描述符状态。
  1. 有红黑树存储,只需要传入一个需要检测的文件句柄,减少了内核和用户空间大量的数据拷贝和内存分配。

    select/poll 是传入整个文件句柄集合。

// 结构体
struct eventpoll {
...
// 红黑树:监听列表,所有要监听的文件描述符,使用红黑树
struct rb_root rbr;
// 双链表:就绪列表,所有就绪的文件描述符,使用链表
struct list_head rdlist;
...
};

// API
// 1. 创建 eventpoll 对象
int epoll_create(int size);
// 2. epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 3. epoll_wait 检测双链表中是否有就绪的文件描述符,如果有,则返回
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

IO多路复用

  1. epoll_create():」 创建 eventpoll 对象,同时返回引用该实例的文件句柄。

  2. epoll_ctl():」 会监听文件句柄 fd 上发生的 event 事件,op 表示对 fd 执行的操作。

    • 因为 epoll 内核维护一颗红黑树,新增和删除操作,复杂度为 O(logn)
  3. epoll_wait():」 当有事件发生时,内核通过回调函数将这个文件句柄加入就绪队列中。

    • 当被调用时,只会返回有事件发生的文件句柄的个数。

Tips:」 通过 epoll 内核源码可知,epoll_wait() 没有使用 MMAP__put_user 函数就是将数据从内核拷贝到用户空间。

epoll 支持两种事件触发模式:「边缘触发(Edge TriggerET)」「水平触发(Level TriggerLT)」

  • 「边缘触发:」 当文件句柄就绪时,会触发通知,如果用户程序没有一次性把数据读/写完,下次还会发出可读/可写信号进行通知。(「重复通知」
  • 「水平触发:」 仅当文件句柄从未就绪变为就绪时,「通知一次」,之后不会再通知。

区别:边缘触发效率更高,减少了事件被重复触发的次数,函数不会返回大量用户程序可能不需要的文件描述符。

水平触发、边缘触发的名称来源:数字电路当中的电位水平,高低电平切换瞬间的触发动作叫边缘触发,而处于高电平的触发动作叫做水平触发。

「各操作系统提供了功能为了解决 C10K 问题:」

  • FreeBSD 推出了 kqueueLinux 推出了 epollWindows 推出了 IOCPSolaris推出了/dev/poll
  • 但每个接口都有自己的特点,程序移植困难,于是需要对这些接口进行封装,以便使用和移植。
  • libevent 库提供统一的 API,底层在不同平台上自动选择合适的调用。在以下操作系统中编译通过 LinuxBSDMac OSXSolarisWindows

C10M问题

既然 C10K 问题已经解决了,那就要考虑下 C10M 问题了。

「实现 10M(即1千万)的并发连接挑战意味着什么」http://www.52im.net/thread-568-1-1.html

  • 1千万的并发连接数;
  • 100万个连接/秒:每个连接以这个速率持续约10秒;
  • 10GB/秒的连接:快速连接到互联网;
  • 1千万个数据包/秒:据估计目前的服务器每秒处理50K数据包,以后会更多;
  • 10微秒的延迟:可扩展服务器也许可以处理这个规模(但延迟可能会飙升);
  • 10微秒的抖动:限制最大延迟;
  • 并发10核技术:软件应支持更多核的服务器(通常情况下,软件能轻松扩展到四核,服务器可以扩展到更多核,因此需要重写软件,以支持更多核的服务器)。

「解决思路:关键在于如何将功能逻辑做好恰当的划分」

  1. 「数据包直接传递到业务逻辑:」 目的减少 Linux 内核传输的链路。

    • 解决方案:网卡问题,使用自己的驱动程序并管理它们,使适配器远离操作系统。
  2. 「多线程的核间绑定:」 每个线程绑定到特定处理核心,这样最大化使用核心 Cache、实现无锁设计、避免进程切换消耗等。

    • 解决方案:核绑定。
  3. 「内存:」 采用更适合的页大小,比如从 4K2M,一定程度上减少地址转换等的性能消耗。

    • 解决方案:系统启动时,分配大内存、管理大内存页。
文章目录
  1. 1. 一、前言
  2. 2. C10K 问题
  3. 3. I/O 模型:同步、异步、阻塞、非阻塞
  4. 4. I/O 多路复用:select/poll/epoll
    1. 4.1. 1) select
    2. 4.2. 2) poll
    3. 4.3. 3) epoll
  5. 5. C10M问题