Redis学习:Redis为什么快?高性能设计之epoll和IO多路复用

Redis学习

1. Redis为什么快?高性能设计之epoll和IO多路复用

  1. IO多路复用(进程) 解决的问题
    1. 在多路复用之前,采用同步阻塞网络IO模型,此时每来一个网络请求就要创建一个进程去处理(也要new一个新连接),即一对一,会造成巨大的开销,且进程切换也会耗时
    2. 多路复用就是解决一对一下创建多进程问题,让一个进程可以同时处理多个TCP连接
    3. 可以采用循环遍历来监控多个IO,但是效率太低,故使用IO多路复用当监控的IO有请求时才会进行处理
    4. IO多路复用是指对进程的复用
  2. IO多路复用
    1. I/O:网络I/O
    2. 多路:多个客户端连接(多个TCP连接)
    3. 复用:用一个进程处理多条连接,可以实现单进程处理多个连接
    4. 实现一个进程可以处理多个不同的IO网络请求,且不使用轮询,而是使用中断方式(epoll函数,当某个请求准备好时才进行处理) ![[Pasted image 20241105080637.png]]
    5. IO多路复用就是用一个进程来处理多个IO连接的请求,实现:select(轮询效率低)、poll、epoll(中断效率高)
  3. redis为什么快?IO多路复用
    1. redis的性能瓶颈不在CPU和内存,而是网络
    2. redis采用epoll()函数实现IO多路复用实现一个进程处理多个IO连接,整体由epoll()函数接受多个IO请求,并依次放到队列中,然后由事件派发器将请求发送到主进程进行处理
    3. redis用一个主线程执行所有redis命令,用多个IO线程来处理IO请求,采用Reactor方式实现文件事件处理器,每一个网络连接都对应一个文件描述符FD,并注册到epoll中![[Pasted image 20241105082112.png]]
    4. redis的网络事件处理器,有多个IO连接,并使用IO多路复用程序监控多个连接并将请求放到队列中,然后由时间分派器从队列中依次将请求分派给处理器,因为队列的消费是单线程的,故Redis才叫单线程模型,且只有一个主线程执行Redis命令
    5. 此时使用多个IO线程同时处理多个IO请求,解决网络IO问题,然后使用单个工作主线程执行Redis命令,保证线程安全
  4. Unix网络编程中的五种IO模型
    1. 同步和异步:服务提供者提供结果的方式
      1. 同步:顺序执行,发送请求后一直等待结果后才可以执行后续操作
      2. 异步:异步执行不会一直等待,而是先执行别的,结果出来后再继续执行,一般通过回调通知
      3. 同步和异步区别在于服务提供者提供调用结果的消息通知方式上同步是一直等待结果返回异步是回调通知,不会一直等待
    2. 阻塞与非阻塞:服务调用者调用请求后的行为
      1. 阻塞:调用者一直等待而别的事情什么也不做,当前线程/进程会被挂起
      2. 非阻塞:调用者发出请求后,先去忙别的事情不会阻塞当前线程/进程
      3. 阻塞和非阻塞谈论服务调用者(请求者),重点在于等待消息时的行为
      4. 同步与非同步谈论服务提供者,重点在于返回调用结果的方式(当面,回调)
    3. 组合方式![[Pasted image 20241105085115.png]]
    4. 阻塞IO、非阻塞IO、IO多路复用 ![[Pasted image 20241105082757.png]]
  5. BIO、NIO、IO多路复用
    1. BIO:阻塞式IO(会一直阻塞等待数据)
      1. 单线程时一个服务端进程只可以同时处理一个Socket连接,无法同时处理多个socket请求,当有多个socket连接时,只会处理一个,其余全部阻塞,并将请求加入队列,当前的连接关闭后才会处理别的连接,且会直接全部执行某个连接的请求(可能造成瞬间压力过大) ![[Pasted image 20241105092758.png]]
      2. 多线程模型:服务端为每个socket连接均建立一个线程进行处理,此时read()只会阻塞对应的线程,此时可以同时处理多个socket连接,但要创建多个线程,如果某个线程等待数据则会阻塞,利用率不高,且创建线程要进入内核态,来回切换线程也会造成极大开销 ![[Pasted image 20241105095008.png]]![[Pasted image 20241105095737.png]]
      3. 阻塞式BIO,每次发起都会阻塞,会一直等待数据传输完成![[Pasted image 20241105100232.png]]
      4. 用户进程调用recvfrom()函数从一个socket上获得数据,数据先从socket到OS内核,然后从内核到用户内存用户进程在这两个阶段均阻塞,效率最低
      5. recvfrom():接收一个数据并保存地址阻塞式IO模型![[Pasted image 20241105090429.png]]
      6. BIO每发起一个请求就会阻塞,一直等待,而NIO是不阻塞,直接返回,不存在时返回error,用轮询替代阻塞
      7. Tomcat7之前就是使用BIO多线程来解决多连接,会为每个连接创建一个线程,造成极大开销
    2. NIO:非阻塞式IO轮询代替阻塞
      1. 阻塞式BIO,当数据未准备好时会一直阻塞等待;而非阻塞式NIO,当数据未准备好时会返回error,此时需要轮询去询问数据是否准备好,用轮询代替阻塞,且轮询期间占用CPUNIO一切都是非阻塞数据没有准备好时则直接返回error,有数据时阻塞读取) ![[Pasted image 20241105102534.png]]
      2. NIO下没有阻塞,用轮询代替阻塞,可以将多个连接加入数组中,遍历数组,如果read()没有返回error说明有数据则进行读取,否则继续遍历,此时就可以实现一个线程处理多个socket连接,但效率很低,要一直占用CPU进行轮询![[Pasted image 20241105103826.png]]
      3. 对于BIO阻塞式IO,此时每个线程只处理一个socket连接,会一直阻塞等待请求数据准备完成,故可以使用非阻塞NIO,当数据未准备好时,返回error,需要轮询去询问数据是否准备好,即用轮询代替阻塞,轮询期间占用CPU ![[Pasted image 20241105101540.png]]
      4. 使用异步非阻塞,当服务端发出接收数据的请求后,异步非阻塞,当数据准备好后再继续执行,此时可以转去处理别的连接的请求,可以同时处理多个连接
      5. NIO不需要为每个连接创建一个线程,一个单线程就可以处理多个socket连接,将多个socket连接存放到数组中,循环遍历每个连接是否有数据,用轮询代替阻塞,但会一直占用CPU,且每次都会遍历所有的连接,使用read()方法也会进行用户态到核心态的切换![[Pasted image 20241105104753.png]]![[Pasted image 20241105105003.png]]
      6. 多个socket连接的遍历一次性传给内核去完成内核是非阻塞的
    3. IO多路复用
      1. 解决的问题
        1. NIO非阻塞式时,此时可以一个线程同时处理多个socket连接,但需要轮询遍历所有的数组来判断是否有数据,且调用read()要进行用户态与核心态的转换,故引入了IO多路复用,其不需要遍历所有的socket,而是哪个请求处理哪个,且在内核态进行
        2. IO多路复用就是一个线程同时处理多个socket连接,且是触发式,谁发送请求就处理谁不是轮询遍历
      2. 是什么
        1. 文件描述符FD来表示每个socket连接
          1. FD是一个索引,每当进程打开一个文件时,内核就会返回该文件的文件描述符FD,通过该FD去访问打开文件表中的文件
        2. IO多路复用:一个线程同时处理多个socket连接,且刚开始线程阻塞等待某个socket发出请求,而不是去轮询是否有数据,当某个有数据时,再使用工作线程对该请求进行处理,不是轮询是否有数据,而是等待连接发出处理请求,将对每个FD的遍历操作封装到内核态函数中,此时一次系统调用就可以完成之前多次的系统调用 ![[Pasted image 20241105125636.png]]
        3. IO多路复用技术(事件驱动IO)通过一种机制,使得一个进程可以同时监控多个描述符(socket连接)一旦某个描述符IO就绪,就通知程序进行相应的操作,是由描述符通知程序进行操作,而不是程序去轮询描述符
        4. IO多路复用就是select\poll\epoll,等待多个描述符进入就绪状态
        5. 将客户端socket的FD注册进入epoll()由epoll来监控哪个socket准备就绪(不需要轮询是否完成),并将事件返回到用户态,此时再由工作线程执行对应的操作,当无准备就绪时,epoll会阻塞,不会占用CPU,且epoll函数在内核中,不需要状态切换
      3. 能干嘛
        1. redis服务采用Reactor的方式实现文件事件处理器(每一个socket连接就是一个文件描述符),由IO多路复用程序(select、poll、epoll)来监控多个socket连接,当有准备就绪时就将事件发送到事件分派器由事件分派器将事件分派到事件处理器中进行处理,因为Redis事件分派器的消费是单线程的,即只有一个事件处理器,故Redis才叫单线程模型 ![[Pasted image 20241105132454.png]]
        2. IO多路复用就是使用select\poll\epoll来实现,由epoll来监控多个socket连接的FD,当准备就绪时通知epoll将其放入队列中,然后由事件分派器将队列中的时间分派给处理器进行执行,从而实现一个线程可以同时处理多个socket连接,且epoll函数不是轮询且在在内核中运行![[Pasted image 20241105131406.png]]
        3. 只需要一个阻塞对象来监控多个socket连接的描述符,当没有准备就绪时就阻塞,当存在准备就绪时就从阻塞状态返回进行业务处理
        4. Reactor设计模式(Dispatcher)就是设置一个阻塞对象来监控所有的描述符,当有准备就续时就加入事件分派器中由事件分派器将事件分派给事件处理器进行处理![[Pasted image 20241105132229.png]]
      4. IO多路复用程序:select、poll、epoll
        1. select
          1. 优点:select函数做到了一个线程同时处理多个socket连接FD,且相对于NIO,select减少了系统调用的开销,只需要一次select函数调用就可以在内核态中遍历所有的socket连接是否就绪,而不用每次都进入内核态遍历,但仍要调用系统调用进入内核态对就绪的FD进行读取![[Pasted image 20241105135305.png]]
          2. 缺点:使用1024位bitmap存放FD(故最多只可以遍历1024个socket连接的FD)、返回的FD置位数组rset不可以重复使用仍要将FD数组拷贝到内核态进行遍历不可以直接返回FD,而是返回置位后的FD数组rset,仍要O(N)遍历才可以知道哪个就绪![[Pasted image 20241105134942.png]]
          3. 就是将NIO中遍历socket数组是否有数据的操作封装到select函数中去内核态执行,此时只需要一次内核态切换就可以遍历所有的数组,返回对就绪的FD进行置位,返回FD数组,此时用户态就可以遍历FD数组得到就绪的socket连接,而不用每次判断都进入内核态
          4. 调用后会阻塞直到有描述符就绪,会将就绪的数组拷贝到内核空间,也就是当调用select时会在内核中判断哪个socketFD有数据,并对其FD在数组对应位置进行置位,然后返回,在用户态下直接遍历FD数组就可以得到就绪的FD,然后进行读取,而不用每次去内核态遍历是否就绪 ![[Pasted image 20241105133127.png]]![[Pasted image 20241105134454.png]]
        2. poll:对select优化解决了bitmap大小限制和rset不可重用
          1. select是将NIO中轮询查找socket数组是否有数据的操作封装到内核中进行实现,减少了系统调用的次数
          2. poll函数对select函数进行了优化解决了bitmap大小限制和rset不可重用(遍历后重置为0,使得可以重用) ![[Pasted image 20241105140322.png]]
          3. 仍要将socket连接的FD数组拷贝到内核态才可以遍历,且无法直接返回就绪的FD,仍要在用户态进行O(N)遍历rset数组看哪个就绪
        3. epoll:最优方法
          1. epoll_create(size):创建指定建议大小的数组来存放socket连接的FD
          2. epoll_ctl():对指定数组下的某个FD进行相应的添加、删除、修改监控操作每个socket连接的FD只在第一次拷贝进入内核态,其余时间遍历时不需要重复拷贝
          3. epoll_wait():从指定的数组中监听设置的每个FD,当有请求时就将对应的事件放到event中,不需要全部进行遍历,然后直接返回总的事件个数K,此时就可以直接遍历event数组前k个就绪事件进行处理即可,不需要遍历整个FD数组,不需要遍历FD数组看是否置位,而是直接遍历event事件进行处理 ![[Pasted image 20241105192058.png]]
          4. epoll函数只将准备就绪的FD描述符的事件放到数组中,此时服务端就不需要遍历整个FD数组,而是直接遍历前k个事件执行处理即可,且epoll通过回调来监控每个FD
          5. FD拷贝操作epoll_ctl()和监控操作epoll_wait()分开进行只有第一次执行epoll_ctl()时才会将FD拷贝到核心态,然后调用epoll_Wait()对FD进行监控,故只需要拷贝一次,且epoll_wait()使用回调的方法监控FD,不是遍历
        4. 对比![[Pasted image 20241105192655.png]]
  6. 小总结
    1. Redis对三个IO多路复用函数均进行了保存,以实现对不同OS的兼容,且Redis在Linux系统上(epoll())效率最高
    2. IO多路复用就是一个线程同时处理多个socket连接,且之所以快,是因为其将原来的多次轮询系统调用(NIO)变为一次系统调用+内核态中遍历,然后返回到用户态,可以减少系统调用的时间,其epoll还将拷贝和遍历分开,当新添加一个连接时使用epoll_ctl()将新连接的FD加入FD数组中,只在第一次加入时拷贝,后面就不需要拷贝了,然后调用epoll_wait()对所有FD数组以回调的方式进行遍历,且直接返回event事件,此时用户态直接遍历event事件数组即可不需要O(N)遍历整个FD数组看是否置位 ![[Pasted image 20241105193138.png]]
  7. redis只有装在linux系统上(因为linux系统使用的是epoll()来实现IO多路复用)才可以发挥最大性能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值