Redis网络模型
- Redis网络模型
- 阻塞IO
- 应用程序想要去读取磁盘中的某个数据。 用户去读取数据时,会去先发起recvform一个命令,去尝试从内核上加载数据,如果内核没有数据,那么用户就会等待,此时内核会去从硬件上读取数据,内核读取数据之后,会把数据拷贝到用户态,并且返回ok,整个过程,都是阻塞等待的,这就是阻塞IO
 
 - 非阻塞IO
- 非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。 可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。 虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,也就是忙等。
 
 - IO多路复用
- IO多路复用是利用单个线程来同时监听多个FD(文件),并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听FD的方式、通知的方式又有多种实现,常见的有: ● select ● poll ● epoll
- 其中select和pool相当于是当被监听的数据(FD)准备好之后,但是你不确定这个是哪个FD准备好了,它只告诉你有FD好了,你需要到整个FD中去找,但是你现在并不知道这个FD是谁的,所以你需要通过遍历的方式去FD列表中寻找这个FD是谁的,所以性能也并不是那么好,select和pool的区别就是poll可以监听任意个FD的数据,但是select只能监听固定数的FD(1024) 而epoll,则相当于内核准备好了之后,他会把准备好的数据,直接发给你,并且携带FD的个人信息,咱们就省去了遍历的动作。
- 具体描述一下epoll epoll模式是对select和poll的改进,它提供了三个函数: 1. eventpoll函数,初始化eventpoll,并且创建内部的属性,他内部包含两个东西: (1)红黑树-> 记录的事要监听的FD (2) 链表->记录的是就绪的FD 2. epoll_ctl函数 将要监听的数据添加到红黑树上去,并且给每个fd设置一个监听函数,这个函数会在fd数据就绪时触发:把fd添加到list_head(FD就绪链表)中去 3. epoll_wait函数 当调用epoll_wait函数的时候,会去检查FD就绪链表, 如果检查到了FD就绪链表中有数据,会把数据拷贝到events数组(用户态有一个空的events数组用来存储就绪的FD)中,并且返回对应的就绪FD数量,用户态此时收到响应后,从events中拿到对应准备好的数据的节点,再去调用方法去拿数据。
 - 当FD有数据可读时,会调用epoll_wait。但是事件通知的模式有两种: ● LevelTriggered:简称LT,也叫做水平触发。只要某个FD中有数据可读,每次调用epoll_wait都会得到通知。 ● EdgeTriggered:简称ET,也叫做边沿触发。只有在某个FD有状态变化时,调用epoll_wait才会被通知。 举个栗子: 1. 假设一个客户端socket对应的FD已经注册到了epoll实例中 2. 客户端socket发送了2kb的数据 3. 服务端调用epoll_wait,得到通知说FD就绪 4. 服务端从FD读取了1kb数据 5. 回到步骤3(再次调用epoll_wait,形成循环) 这里之后就可以看出来区别了 ● 如果我们采用LT模式,因为FD中仍有1kb数据,则第⑤步依然会得到可读通知 ● 如果我们采用ET模式,因为第③步已经消费了FD可读事件,所以到第⑤步的时候,epoll_wait不会返回通知,剩下的数据就无法读取了,客户端响应超时。  不过ET也可以实现类似LT的效果,比如你读取了1kb,但是发现没有读完,可以调用之前我们说过的epoll_ctl函数修改FD的状态,就会进行重新通知了,如果读完了就不用再修改状态了,这样就可以达到和同样的效果LT 你可能会说ET实现这么复杂,为啥还要用ET呢,原因是:LT存在惊群现象,并且LT的频繁通知也会很消耗性能。ET解决了这两个问题。所以存在即合理
- 具体梳理一下epoll的流程 服务器启动以后,服务端会去调用epoll_create,这个函数会创建一个epoll实例,底层调用的是eventpoll函数,epoll实例中包含两个数据 1. 红黑树(为空):rb_root 用来去记录需要被监听的FD 2. 链表(为空):list_head,用来存放已经就绪的FD 创建好了之后,会去调用epoll_ctl函数,此函数会将需要监听的数据添加到rb_root中去,并且对当前这些存在于红黑树的节点设置回调函数,当这些被监听的数据一旦就绪,就会将红黑树的fd添加到list_head中去(但是此时并没有进行处理) 3、当第二步完成后,就会调用epoll_wait函数,这个函数会去校验是否有数据准备完毕(因为数据一旦准备就绪,就会被回调函数添加到list_head中),在等待了一段时间后,去判断是否超过我们定义的等待超时时间,如果超时则返回没有数据。如果在超时时间内发现了已就绪的FD,则进一步判断当前是什么事件,如果是建立连接事件,则调用accept() 接受客户端socket,拿到建立连接的socket,然后建立起来连接。如果是其他事件,则进行其他事件的处理。
 
 - 总结 select模式存在的三个问题: ● 能监听的FD最大不超过1024 ● 每次select都需要把所有要监听的FD都拷贝到内核空间,等就绪了还需要再拷贝回去,拷贝太多了 ● 每次都要遍历所有FD来判断就绪状态 poll模式的问题: ● poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降 epoll模式中如何解决这些问题的? ● 基于红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高,性能不会随着监听的FD数量增多而下降 ● 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epoll_wait无需传递任何参数,无需重复拷贝所有的FD到内核空间,只需要把就绪的FD拷贝回用户空间就行了 ● 内核会将就绪的FD直接拷贝到用户空间的指定位置,用户进程无需遍历所有的FD就知道就绪的FD是谁
 
 
 - 其中select和pool相当于是当被监听的数据(FD)准备好之后,但是你不确定这个是哪个FD准备好了,它只告诉你有FD好了,你需要到整个FD中去找,但是你现在并不知道这个FD是谁的,所以你需要通过遍历的方式去FD列表中寻找这个FD是谁的,所以性能也并不是那么好,select和pool的区别就是poll可以监听任意个FD的数据,但是select只能监听固定数的FD(1024) 而epoll,则相当于内核准备好了之后,他会把准备好的数据,直接发给你,并且携带FD的个人信息,咱们就省去了遍历的动作。
 
 - IO多路复用是利用单个线程来同时监听多个FD(文件),并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听FD的方式、通知的方式又有多种实现,常见的有: ● select ● poll ● epoll
 - 信号驱动IO
- 当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。
- 你可能会问,这种方式不是挺好的嘛,Redis为啥不用这个呢?因为它也是有缺点的:当有大量IO操作时,信号较多,内核空间与用户空间的频繁信号交互性能也较低(因为是一条一条通知的,不如IO多路复用会一次直接返回多个FD)。
 
 
 - 当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。
 - 异步IO
- 这种方式,不仅仅是用户态在试图读取数据后不阻塞,并且当内核的数据准备完成后也不会阻塞,在异步IO模型中,用户进程在两个阶段都是非阻塞状态。 内核将数据加载完成后,不用用户请求就会把数据拷贝到用户态中。少了一次请求,所以性能极高
- 这种情况的缺点是,在高并发场景下,请求太多,然后内核态不断往用户态复制数据,导致内存崩溃。
 
 
 - 这种方式,不仅仅是用户态在试图读取数据后不阻塞,并且当内核的数据准备完成后也不会阻塞,在异步IO模型中,用户进程在两个阶段都是非阻塞状态。 内核将数据加载完成后,不用用户请求就会把数据拷贝到用户态中。少了一次请求,所以性能极高
 - Redis网络模型面试题
- Redis到底是单线程还是多线程?
- 得看是什么从什么角度来看Redis是单线程还是多线程了: ● 如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程 ● 如果是聊整个Redis,那么答案就是多线程 在Redis版本迭代过程中,在两个时间节点上引入了多线程: ● Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如把客户端发过来的请求都转换成redis认识的命令 ● Redis v6.0:在核心网络模型中引入多线程,其实主要还是解决的带宽的问题,通过多线程来向用户发送数据。
 
 - 为什么Redis要选择单线程?
- ● Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。 ● 多线程会导致过多的上下文切换,带来不必要的开销 ● 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣。 所以即使是在现在,redis仍然在命令处理模块使用的还是单线程,在网络模块使用的是多线程。
 
 
 - Redis到底是单线程还是多线程?
 
 - 阻塞IO
 
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 是小白菜哦!

