为什么要有IO多路复用
大家印象中的redis都是单线程的,没有加锁的操作,因此才会是redis这么快的原因其中之一。先暂且不说redis究竟是不是单线程,即便是单线程的,作为服务提供方,面对成百上千的客户端连接请求,读写操作,单线程是怎么做到高效的处理这些请求?单线程处理socket连接,面对客户端发送的指令如何处理?一个个轮询吗?明显是比较低效的操作,作为高效著称的redis肯定是不会用这么简单粗暴的方式去处理的。这里就引出一个解决方式IO多路复用
。
在redis6/7中,非常受关注的第一个新特性就是多线程。
redis一直被大家熟知的就是它的单线程架构,虽然有的命令操作可以用后台线程或者子进程执行(比如数据删除,快照生成,AOF重写)。但是,从网络IO处理到实际的读写命令处理,都是由单个线程完成的。
为了应对这个问题,采用多个IO线程来处理网络请求,提高网络请求处理的并行度
,Redis6/7就是采用的这种方法。
但是redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为redis处理请求时,网络处理经常是瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本,事务的原子性,额外开发多线程互斥加锁机制了,这样一来redis线程模型实现就简单了。
什么是IO多路复用
网络IO在系统层面上指的是数据从用户态到内核态的读写操作,多路是指多个socket连接。在UNIX网络编程中写道为了执行网络IO,一个进程必须要做的第一件事就是调用socket函数。复用是指一个或多个连接处理。
一个服务端进程可以同时处理多个套接字描述符。
redis使用IO多路复用简单来说就是,单线程处理多个客户端连接的网络读写请求,并且能够保证不会阻塞主流程的一种机制。
实现IO多路复用的模型有3种:可以分select->poll->epoll三个阶段来描述。
基本IO模型与阻塞点
以Get请求为例,服务端为了处理一个Get请求,需要监听客户端请求(bind/listen),和客户端建立连接(accept),从scoket中读取请求(recv),解析客户端发送请求(parse),根据请求类型读取键值数据(get),最后客户端返回结果,即向socket中写回数据(send)。
下图显示了这一过程,其中,bind/listen、accept、recv、parse和send属于网络IO处理,而get属于键值数据操作。既然Redis是单线程,那么,最基本的一种实现是在一个线程中依次执行上面说的这些操作。
但是,在这里的网络IO操作中,有潜在的阻塞点,分别是accept()和recv()。当Redis监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在accept()函数这里,导致其他客户端无法和Redis建立连接。类似的,当Redis通过recv()从一个客户端读取数据时,如果数据一直没有到达,Redis也会一直阻塞在recv()。这就导致Redis整个线程阻塞,无法处理其他客户端请求,效率很低。
以上描述的就是在阻塞式socket可能会出现的阻塞点。假设一个socket被阻塞在其中某一个步骤,那么其他的socket也将会被阻塞住。这种情况肯定是不能容忍的。幸运的是socket网络模型本身支持非阻塞式。
非阻塞模式
Socket网络型的非阻塞式模式设置,主要体现在三个关键的函数调用上,如果想使用socket非阻塞模式,就必须要了解这三个函数的调用返回类型和设置模式。
在socket模型中,不同操作调用后返回不同的套接字类型。socket()方法会返回主动套接字,然后调用listen()方法,将主动套接字转换为监听套接字,此时,可以监听来自客户端的请求连接。最后调用accpet()方法接受到的客户端连接,并返回已连接套接字。
针对监听套接字,可以设置非阻塞模式:当redis调用accpet()单一直未有连接请求到达时,redis线程可以返回处理其他操作,而不用一直等待。
虽然redis线程可以不用继续等待,但是总得有机制继续在监听套接字上等待后续连接请求并且在有请求时通知redis。
同样也可以针对已连接套接字设置非阻塞模式:在redis调用recv()后,如果已连接套接字上一直没有数据到达,redis线程同样可以返回处理其他操作。也需要有机制继续监听已连接套接字,并在有数据到达时通知redis。
这样才能保证redis线程,既不会像基本IO模型中一直在阻塞点等待,也不会导致redis无法处理实际到达的连接请求或数据。
那么这个机制究竟是什么来实现的?
简述多路复用模型
将用户socket对应的文件描述符注册进epoll,然后epoll监听哪些scoket上有消息到达,这样就避免了大量的无用操作。设置sokcet模型为非阻塞模式,这样整个过程只在调用epoll这些的时候才会阻塞。收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor响应模式。至此我们明白了用于监听事件拉起通知的机制是通过epoll这些来实现管理的。
Linux中的IO多路复用机制是指一个线程处理多个IO流,就是我们经常听到的select/epoll机制。简单来说在redis只运行单线程的情况下,该机制允许内核中同事存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给redis线程处理,这就是实现了一个redis线程处理多个IO流的效果。
下图就是基于多路复用的redis IO模型。图中的多个FD就是刚才所说的多个套接字。Redis网络框架调用epoll机制,让内核监听这些套接字。此时Redis线程不会阻塞在某一个特定的监听或者已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,redis可以同时和多个客户端连接并处理请求,从而提升并发性。
为了在请求到达时能够通知到redis线程,select/epoll提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
那么,回调机制是怎么工作的?其实select/epoll一旦监听到FD上有请求到达时,就会触发相应的事件。
这些事件会被放进一个事件队列,redis单线程对该事件队列不断进行处理。这样一来,redis无需一直轮询是否有请求实际发生,这就可以避免造成CPU资源浪费。同时redis在对事件队列中的事件进行处理时,会调用相应处理函数。这就实现了基于事件的回调。因为redis一直在对时间队列进行处理,所以能够及时响应客户端请求,提升redis的响应性能。