redis的网络模型

一个进程的内存空间可以分为用户空间和内核空间

当进行IO从网络中接受数据时,需要先通过内核态将数据接受到内核缓冲区中,然后将数据再传到用户缓冲区中,才能对这些数据进行处理

所以从外部读数据的时间消耗主要可以分成两个部分:等待数据就绪+读取数据

等待数据就绪:包括内核空间从硬件将数据读取到内核缓冲区的时间

读取数据:将内核缓冲区的数据拷贝到用户缓冲区的时间

阻塞IO:

在用户向内核空间请求数据的时候,如果没有数据,也在等待,直到有数据返回

非阻塞IO:

如果内核空间没有数据的话,不等立即返回,但是会一直问,直到问到数据

但是实际上提升也不多,因为阻塞IO一直等,但是非阻塞的也在一直问也没干啥,而且忙等可能会导致CPU空转,CPU使用率暴增

IO多路复用:

不管是阻塞IO还是非阻塞IO,调用recvfrom的时候,如果数据没有就绪要么等要么一直问询

如果数据就绪,就直接接受数据

思考这样的一个场景:如果一个线程监听了多个socket请求,由于是单个线程,所以只能依次处理这个socket请求,如果正在处理的这个socket数据还没有就绪,那就被阻塞,后面的socket就算数据就绪了也不能处理数据,所以效果很差

怎么解决:多线程(上下文开销就会增大)、先处理就绪的,没就绪的就不要等待处理

但是怎么知道有没有就绪?

Linux通过FD来标识就绪情况

FD(文件描述符):是一个从0开始递增的无符号整数,用来关联一个Linux中的一个文件

(在linux中,一切皆文件,例如常规文件、视频、硬件设备、网络套接字等,也就是每个socket都会对应一个FD)

现在流程就变成这样的,用户线程先调用selcet去查询这个线程监听的fd,内核去检查这些fd对应的socket有没有数据就绪,如果所有socket都没有就绪,那就等着,这个时候用户线程也会阻塞,如果有就绪的,那就把就绪的fd返回,然后用户线程再通过recvfrom就调用这个fd,就可以保证这个fd对应的数据一定是就绪的

recvfrom只能监听一个fd,当前fd没有就绪不代表其他fd没有就绪

selcet是一次性传多个fd进去,只要有任意一个就绪就会传回来

虽然没有数据就绪的时候用户应用也是阻塞的,但是实际上避免了很多的无效阻塞等待

select:只知道有fd就绪了,但是不知道是哪个fd就绪了,挨个去问看谁就绪了

poll:同selcet,

epoll:会直接通知有fd就绪的同时就把就绪的fd写入用户空间,避免了对fd的遍历过程

selcet:

一次执行:

用户创建fd_set readfds(这里只有读事件,所以只监听readfds)。fd_set 是一个long类型的数组,长度为1024/32,但是一个long有32位,所以一个fd_set还是有1024位

用户将要监听的fd对应的位置1(比如要监听fd=1,2,5,就把这个fd_set的第1,2,5位置1)

执行selcet(5+1, readfds, null, null,3)

将readfds拷贝到内核态

内核遍历readfds,如果没有就绪的,就休眠

等待数据就绪被唤醒或者超时

内核将readfds中就绪的置1,其他的置0

内核将修改后的readfds拷贝回用户空间,用户空间遍历找到就绪的fd,再yongrecvfrom读取数据

缺点:

每次都将readfds拷贝到内核空间,结束之后还得再拷贝回去

无法得知哪个fd就绪,只能遍历readfds

readfds的数量不超过1024

poll:

没有具体的数据划分,这里只有一个数组,但是数组中每个元素都有个类型

一次执行:

创建pollfd数组,添加关注的fd信息,数组大小自定义

调用poll函数,将pollfd数组拷贝到内核空间,将数组转成链表

内核遍历fd,判断是否就绪

有数据就绪或者超时之后把pollfd数组返回到用户空间,返回就绪fd数量n

用户判断就绪数量是否大于0

大于0就遍历数组,找到就绪的fd

改进和缺点:

相对selcet,实际上只改进了数量限制

但是数量没有限制的话,每次便利消耗时间更久,性能反而下降

epoll:

用户态首先会创建epoll实例,在内核态就会创建一个rb_root和一个list_head

通过epoll_ctl添加要监听的fd,关联callback,内核态就会把这个fd添加到rb_root的红黑树上

就绪的时候就会把这个fd添加到list_head上

当epoll_wait就会判断list_head上有没有节点,也就是有没有就绪的fd

所以内核态会通过一个链表记录就绪的fd,避免了遍历fd结合来得到就绪的fd

信号驱动IO

用户建立一个信号处理函数,调用之后立即返回,等内核有fd就绪好了之后递交信号,用户态收到信号再recvfrom进行系统调用接受数据

缺点:如果有大量IO操作,信号较多,信号处理函数不能及时处理可能导致信号队列溢出

内核态与用户态频繁交互信号性能低

异步IO:

异步IO没有执行recvfrom这个操作

用户调用aio_read执行系统调用后马上返回,等内核数据就绪之后直接将数据拷贝到用户态

异步在两个阶段都是不阻塞的

总结:

redis网络模型:

redis在核心业务处理部分是单线程,但是整个redis是多线程

(这里注意像关闭文件、AOF 刷盘、释放内存是后台线程,但是像AOF重写、bgsave保存RDB数据快照是子进程

为什么redis要选择单线程:

1.最最重要的原因就是,redis是基于内存的,执行速度非常非常快,多线程不会带来巨大的性能提升,反而会导致上下文切换带来不必要的开销

2.多线程会面临线程安全问题,就需要引入锁等安全手段,实现复杂度高,性能也会有影响

流程:

首先有一个serverSocket(用来监听别人对这个服务端的连接),然后这个sercerSocket会对应一个fd,把这个fd注册到监听的红黑树(eventloop)上,然后等待就绪(在等待就绪之前会遍历客户socket,监听fd写事件,绑定【写处理器】)

当serverSocket有可读事件的时候(实际上就是有客户端连接上了这个服务端),就会触发【连接应答处理器】,会接受客户端的请求,得到客户端socket的fd,然后注册到eventloop上,然后继续等待事件

现在来的就可能是serverSocket的可读事件(建立新的连接),也可能是客户socket的可读事件,如果是客户socket的可读事件的话,就交给【命令请求处理器】去处理(相当于客户端的请求来了,要去处理客户端的请求)

【命令请求处理器】会把每个客户请求的请求数据写到缓冲区中,然后解析缓冲区中的命令转为redis命令,再处理解析好的命令,再根据命令去找对应的处理函数(每个命令都有一个对应的函数,比如set就对应一个setCommand函数),再将结果返回回去(先把返回的数据写到缓冲区中,然后放到一个队列中)

绑定了写处理器之后,再来的客户socket写事件就会交给【写处理器】,将队列中的数据取出来写到客户端的socket中

简化一下:

就是redis通过一个IO多路复用+事件派发机制,监听socket来的命令,如果是serversocket可读事件的话,就给【连接应答处理器】处理,如果是客户socket可读事件的话,就交给【命令请求处理器】,如果是客户端可写事件的话,就交给【命令回复处理器】去处理解决

性能瓶颈:从IO读数据和向IO写数据

所以redis6.0引入的多线程就是在【命令请求处理器】读取解析命令的时候和【命令回复处理器】将数据写回的时候使用多线程

学习资源:原理篇-27.Redis网络模型-Redis单线程及多线程网络模型变更_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值