大家都知道Redis的速度非常快,而且每秒能支撑上万的请求。
得益于三点:
- 最关键的一点Redis是纯内存操作
- 它的数据结构非常高效
- 它采用了多路复用机制 使用的是epoll模型
那么我们今天就来讲讲这个 多路复用机制和 epoll 模型。
那么 它俩到底是什么?
IO多路复用 是网络模型中的一种,而 epoll 是它的一种实现。
接下来我来展开说说。
网络模型
讲到网络模型 就不得不提到一本神作:《Unix网络编程》
作者将LinuxIO模型分为五类:
阻塞IO,非阻塞IO,IO多路复用,信号驱动,异步IO
为了让后续的讲解能正常进行或者说更好理解,我在这里提一嘴:
应用是没法直接去硬件拿数据的,必须通过内核与硬件进行交互。
而为了避免用户应用导致冲突甚至内存崩溃,用户应用与内核是分离的,进程的寻址空间划分为了两部分:内核空间和用户空间
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后才能写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后再拷贝到用户缓冲区。
不管是读取还是写入,无非都是从用户应用程序发送一个请求到内核。
在内核中,可以分为两个阶段:
- 第一阶段是内核等待数据;
- 第二阶段是数据就绪后,将数据拷贝到用户空间。
而我们讲的IO模型,就是为了优化这个读取和写入而进行的。
阻塞IO
顾名思义,就是这两阶段都必须阻塞等待。
比如我使用recvfrom操作,去找你拿信息,你没有,那我就等着。等到你有。
非阻塞IO
非阻塞IO的 recvfrom操作 会立即返回结果。也就是 第一阶段,你要是没有 我就直接返回没有 不会像阻塞IO模型一样阻塞在那边。
但是! 它会反复去询问,现在有了吗?现在有了吗? 这样。
这其实很鸡肋对吧,因为看着你确实没阻塞住,但是你还得一直问,不仅是瞎忙活,还浪费了CPU的性能去重复发请求。所以可能还不如阻塞IO。
所以
无论是阻塞IO还是非阻塞IO,用户应用在一阶段,都需要去调用recvfrom来获取数据,差别其实就在于无数据时的处理方案:
-
如果调用recvfrom时,切好没有数据,阻塞IO会使进程阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU作用。
-
如果调用recvfrom时,切好有数据,则用户进程可以直接进入第二阶段,读取并处理数据
这就会有个问题,服务器端处理客户端Socker请求时,在单线程情况下,只能依次处理每一个Socket,如果正在处理的Socket切好未就绪(数据不可读或不可写),线程就阻塞住了,其他所有客户端Socket就只能等着,即使他们的数据已经就绪了。而第二阶段实际上是不可避免的, 只能优化第一阶段。
这可以结合场景来理解:
顾客排队点餐,分两步:
-
顾客想想吃什么(等待数据)
-
顾客想好了,开始点餐(读取数据)
想要提高效率有两种办法:
-
加更多服务员(多线程嘛)
-
不排队,谁想好了吃什么(线程就绪了),他就叫服务员过来(用户应用就去读数据)
IO多路复用
既然一个一个来很慢,那我就监听所有人,也就是方法2。
就像现在我们去餐厅吃饭,都是坐在座位上先看菜单,选好了就叫服务员来。这也就是IO多路复用的基本思想。
利用单个线程来同时监听多个文件描述符 FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。(也能回答 IO多路复用是什么)
IO多路复用实际上是种思想嘛,他有
三种实现:
-
select :它只能支持1024个连接
-
poll :它可以自定义
-
epoll :它是最高效的实现方式,分为两种事件通知机制:TL 水平触发(默认)和 ET 边缘触发
那么问题来了:用户进程要怎么知道内核中数据是否就绪呢?
这个要说说FD:
文件描述符 FD
在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字。
它们全都是文件。而文件有一个文件描述符(File Descriptor),简称FD。
FD是一个从0 开始递增的无符号整数,用来关联Linux中的一个文件。
那现在用户进程,需要1 2 5 这三个文件。
当用户进程将这个需求传递给内核时,它会创建一个类似于表或数组的数据结构,表示需要的文件,例如: 2、5、7对应的位置为1,其余位置为0,形如:
0 0 1 0 0 1 0 1 0 0 0 …
在内核中,当这个表中的某个文件 比如 5准备好后,内核会将表更新为:
0 0 1 0 0 0 0 1 0 0 0 …
然后将这个表或数组传回给用户进程。
select和poll这两种实现方式就是:
用户进程逐个比对所接收到的表,若发现某个文件的状态发生变化,就意味着它已准备好,用户进程就可以进入第二阶段进行读取等操作。
还是结合点餐场景,有顾客想好吃什么了,它就按座位上的灯,服务员看到灯亮了,他就去找这个顾客,但是他不知道具体是谁按的,所以就得逐个问,是你嘛?是你嘛?直到找到对应的用户。这个是最早期的IO多路复用实现方案,也就是 select 和 poll 的实现方案。
这个通知机制不太好。所以后来linux就升级了一下,epoll 这种就是 每桌有个桌号,我灯亮了可以看到桌号 就不用一个个问了。
其实也就是 他返回的时候 我直接告诉你是谁就得了,你还比对个啥,对吧。效率一下就上来了。
那么为什么select只能支持1024个连接呢?
就是因为这个数组,他把数组大小写死了,是个常量 1024,那只有1024那么大,不就只能支持1024个连接了,在实际中甚至还更低。
再比如poll实现,他是可以自定义大小,连接数上来了。但是我们前面说到了,他是挨个比对的,那太大了能行吗。大小为10000000的数组 你挨个过去 得比对到什么时候。
而epoll的方式, 就哪个好了通知哪个,把CPU的运用率拉满了。
这也能回答一个问题,Redis为什么选择单线程。
就是因为他在内存中操作,内存操作速度太快了。甚至可以到微妙级别,配合上 epoll,单线程就够了。它的性能瓶颈永远是网络延迟而不是执行速度。还不用考虑上下文切换的性能开销,还有多线程的线程安全问题。
那到这就结束了。当然,这里面还有非常非常多细致的点。
后续我有空了会再整理发文。如果觉得对你有帮助的话,帮忙点赞评论支持一下。
谢谢观看。