pipeline
下面是使用管道与不使用管道对redis set 6000条数据的效果,可以看到,使用管道技术用了不到1秒的时间,而不使用则耗费了4秒左右的时间。
源码地址:https://github.com/qiaomengnan16/redis-demo/tree/main/redis-pipeline
客户端与服务端消息交互过程
当客户端与Redis服务端进行交互时,客户端先发送指令,服务端处理完毕后返回结果,客户端读取到结果,此次操作结束,花费了一个网络数据包来回的时间。
如果连续执行多条指令,就花费了多个网络数据包来回的时间,
从客户端角度来看,两次请求是经过 写 -> 读,写 -> 读,四次操作执行完成的,写就是写入指令,读就是读取返回结果,读是要等待服务器响应的,这中间会经历一点耗时,例如第一次读耗时0.3秒,第二次读又耗时0.3秒,加起来就是0.6秒了。
改变交互过程
如果我们 set x1 1 , set x2 2,执行这两个指令存两个值,显然这两次操作并不相关,可以将这两个set指令一起发送过去,再读取返回值,省去中间一次读返回值的阻塞耗时,即调整读写顺序,将 写 -> 读,写 -> 读 的顺序改为 写写读读,
两次连续的写操作和连续的读操作只会花费一次网络来回的时间,连续的写操作被合并成一个,连续的读操作也被合并成一个,即读等待时间只花费0.3秒,redis的管道其实就是改变了写读的顺序使的速度变得更快。
服务器
原理
通过改变写读的顺序就可以让速度大幅度提升,根本原因是减少多个网络来回的开销
上图是发起一次完整的操作过程,步骤如下:
- 客户端调用 wirte 将消息写到操作系统内核为套接字分配的发送缓冲区 send buffer 中。
- 客户端操作系统内核将缓冲内容发送到网卡,网卡将数据发送到服务器端的网卡中。
- 服务器操作系统内核将网卡中的数据放到内核为套接字分配的接收缓冲区 recv buffer 中。
- 服务器通过调用 read 从接收缓冲区中取出消息进行处理。
- 服务器调用 wirte 将响应消息写到内核为套接字分配的发送缓冲区 send buffer 中。
- 服务器操作系统内核将缓冲区内容发送到网卡,网卡再将数据发送到客户端的网卡中。
- 客户端操作系统内核将网卡中的数据放到内核为套接字准备的接收缓冲区 recv buffer中。
- 客户端进程调用 read 从接收缓冲区中取出消息返回给上层业务逻辑进行处理。
- 结束。
步骤5、8与1、4是一样的,只是方向不同,一个是请求,一个是响应。
客户端的write操作将数据写入到操作系统内核的缓冲区中后就立刻返回,而不是等待发送到服务端才返回,剩下的事情由操作系统内核异步将数据发送到服务端,
如果发送缓冲区满了,那么write操作就需要等待缓冲区空闲出来,这是wirte的耗时时间,如果缓冲区没有满的话,返回速度是非常快的,随后就可以进行下一个写操作,
客户端的read操作负责从本地操作系统内核的接收缓冲区中读取数据,而不是从服务端拉取数据,如果服务端响应的很快,read操作就可以很快的读到数据,
如果接收缓冲区中没有数据,那么read就需要进行等待数据的来临,读取结束后返回,
对于管道的连续写和连续读来说,连续write操作基本等于是没有耗时的,只要缓冲区不满,就可以一直写,而read是等待服务端的数据响应,
响应过来的数据就是之前所有的操作结果响应,由此可以看出管道的快速本质在于客户端通过改变读写的顺序获得了巨大的性能提升。
适合场景
当Redis的多次操作毫不相关时,即后一个指令不依赖于上一个指令的执行结果,则可以使用管道进行处理,例如开篇的for循环set值,如果只是使用普通的set指令的话,接口限制时间如果是3秒,那就会因为redis大量的写读等待导致接口超时,
由于每次set操作都是不相干的,所以放心的用管道去做加速处理,相反如果后续的指令需要依赖前面的指令结果,则不适合用管道去做 (管道也是支持脚本的,可以在管道中发送一些脚本做依赖逻辑处理) ,从这种角度看,pipeline和批处理也比较相似。
编码时请注意,pipeline期间,管道将独占连接connection,不能进行其他非pipeline的操作,否则将会出错,如果需要做其他的处理, 需要使用新的 connection,和管道使用中的连接进行隔离,
同时需要注意,redis在处理完所有命令前,会将响应结果缓存在server端,缓存的越多,对内存的占用也会越多。
管道压力测试
redis自带一个压力测试工具 redis-benchmark,借此可以对管道进行测试,试试管道的威力。
下面是对一个普通的set指令进行压测,QPS大约为 145985.41/s。
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -q
SET: 145985.41 requests per second, p50=0.167 msec
下面加入-P参数,表示单个管道内并行的请求数量,可以看到下面的结果,数量每加1个,QPS会多出十万个左右,从第14个往后,QPS无法提升,甚至开始下降,这是因为CPU的消耗已经达到100%的极限,无法继续提升了。
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 2 -q
SET: 273224.03 requests per second, p50=0.175 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 3 -q
SET: 378795.47 requests per second, p50=0.183 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 4 -q
SET: 483091.78 requests per second, p50=0.183 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 5 -q
SET: 549450.56 requests per second, p50=0.191 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 6 -q
SET: 653607.88 requests per second, p50=0.199 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 7 -q
SET: 724652.19 requests per second, p50=0.191 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 8 -q
SET: 806451.62 requests per second, p50=0.215 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 9 -q
SET: 877263.19 requests per second, p50=0.255 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 10 -q
SET: 1408450.62 requests per second, p50=0.255 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 11 -q
SET: 917440.38 requests per second, p50=0.375 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 11 -q
SET: 952390.50 requests per second, p50=0.375 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 12 -q
SET: 1020489.81 requests per second, p50=0.399 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 13 -q
SET: 1333453.25 requests per second, p50=0.367 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 14 -q
SET: 1075290.25 requests per second, p50=0.455 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 15 -q
SET: 1020459.19 requests per second, p50=0.479 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 16 -q
SET: 1000000.00 requests per second, p50=0.519 msec
\
root@f5cd3ecb4cd8:/data# redis-benchmark -t set -P 17 -q
SET: 1020520.44 requests per second, p50=0.551 msec