redis管道(pipeline)是由客户端(不是服务器)提供的能够加速redis存储效率的一项技术
redis的消息交互
当我们使用客户端对redis进行一次操作时,如图所示,客户端将请求传送给服务器,服务器处理完毕后,再将响应回复给客户端。这要花费一个网络数据包来回的时间。
如果连续执行多条指令,那么就会花费多个网络数据包来回的时间
回到客户端代码层面,客户端是经历了【写-读-写-读】四个操作才完整的执行了两条指令
现在如果我们调整读写顺序,改成【写-写-读-读】,这两个指令同样可以正常完成
两个连续的写操作和两个连续的读操作总共只会花费一次网络来回,就好像连续的写操作合并了,连续的读操作也合并了一样
这就是管道操作的本质,服务器根本没有任何区别对待,还是【收到一条消息—执行一条消息—回复一条消息】的正常流程。客户端通过对管道中的指令列表改变读写顺序就可以大幅节省IO时间。管道中指令越多,效果越好
管道压力测试
redis自带了一个压力测试工具redis-benchmark,使用这个工具就可以进行管道测试
首先我们对一个普通的set指令进行压测,QPS大约5w/s
我们加入管道选项P参数,它表示单个管道内并行的请求数量。如下,当P=2时,QPS达到了9w/s
当P=3时,QPS达到了10w/s
但如果我们继续提升P参数,发现QPS已经上不去了。这是为什么呢?
因为这里CPU处理能力已经达到了瓶颈。Redis的单线程CPU消耗已经到了100%,所以无法继续提升了
深入理解管道本质
- 客户端进程调用write将消息写到操作系统内核为套接字分配的发送缓冲区send buffer中
- 客户端操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过“网际路由”送到服务器的网卡
- 服务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲区recv buffer中
- 服务器进程调用read从接收缓冲区中取出数据进行处理
- 服务器进程调用write将响应消息写到内核为套接字分配的发送缓冲send buffer中
- 服务器操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过“网际路由”送到客户端的网卡
- 客户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓存recv buffer中
- 客户端进程调用read从接收缓存中取出消息返回给上层业务逻辑进行处理
其中(5)-(8)与(1)-(4)是一样的,只不过方向是反过来的,一个是请求,一个是响应
我们开始以为write操作是要等到对方收到消息才会返回,但实际上不是这样的。write操作只负责将数据写到本地操作系统内核的发送缓存中然后就返回了,剩下的事交给操作系统内核异步将数据送到目标机器。但是如果发送缓存满了,那么就需要等待缓冲区空出空闲空间来,这个就是写操作IO操作的真正耗时
我们开始以为read操作是从目标机器拉取数据,但实际上不是这样的。read操作只负责将数据从本地操作系统内核的接收缓冲区中取出来就好了,但是如果缓存时空的,那么就需要等待数据的到来,这个就是read操作IO擦走的真正耗时
所以对于value=redis.get(key)这样一个简单的请求来说
- write操作几乎没有耗时,直接写到发送缓冲区中就返回
- 而read操作就比较耗时了,因为它要等待消息经过网络路由到目标机器处理后的响应消息,在回送到当前的内核读缓存才可以返回。
这才是一个网络来回的真正开销
而对于管道来说,连续的write操作根本就没有耗时,之后第一个read操作会等待一个网络的来回开销,然后所有的响应消息就已经送回到内核的读缓存了,后继的read操作直接就可以从缓存中拿到结果,瞬间就返回了
这就是管道的本质,它并不是服务器的什么特性,而是客户端通过改变了读写的顺序带来的性能的巨大提升
原生批量命令 VS 管道
- 原生批量命令是原子的,pipeline是非原子的
- 原生批量命令是一个命令对应多个key,pipeline是非原子的
- 原生批量命令是redis服务端支持实现的,而pipeline需要服务端和客户端的共同实现
使用建议
- pipeline虽然好用,但是每次pipeline组装的命令个数不能没有节制,否则一次组装pipeline数据量太大,一方面会增加客户端等待时间,另一方面会造成一定的网络阻塞
- pipeline只能操作一个redis实例,但是即使在分布式redis场景中,也可以作为批量超时的重要优化手段