文章目录
  1. 1. 消息交互
  2. 2. 管道
  3. 3. 压力测试
  4. 4. 管道VS事务
  5. 5. 管道VS脚本
  6. 6. 总结

redis管道技术对执行结果没有互相依赖,对结果响应也无需立即获得的命令集批量提交到redis服务器的方式,能在一定程度上提升redis性能,性能提升的原因主要是TCP连接中减少了交互往返的时间。

Redis管道(Pipeline)本身并不是Redis服务器直接提供的技术,这个技术本质上是由客户端提供的,跟服务器没有什么直接的关系。

消息交互

redis是使用客户端-服务器模型的TCP服务器,称为请求/响应协议。
这意味着通常一个请求是通过以下步骤完成的:

客户端向服务器发送查询,并通常以阻塞的方式从套接字读取服务器响应。
服务器处理命令并将响应发送回客户端。

每一个redis命令request/response都需要经历一个RTT(Round-Trip Time 往返时间), 如果需要执行很多短小的命令,这些往返时间的开销是很大的,在此情形下,redis提出了管道来提高执行效率。

管道

如果client执行一些相互之间无关的命令或者不需要获取命令的返回值,那么redis允许你连续发送多条命令,而不需要等待前面命令执行完毕;

比如我们执行3条INCR命令,如果使用管道,理论上只需要一个RTT+3条命令的执行时间即可,如果不适用管道,那么可能需要额外的两个RTT时间;

因此,管道相当于批处理脚本,相当于是命令集;

执行管道命令中, redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。

注意

执行管道命令期间将独占链接,此期间将不能进行非管道类型的其他操作,直到管道关闭;

如果管道的指令集很庞大,为了不干扰链接中的其他操作,建议为管道操作新建Client链接,让管道和其他正常操作分离在2个client中;

不过管道事实上所能容忍的操作个数,和socket-output缓冲区大小/返回结果的数据尺寸都有很大的关系;同时也意味着每个redis-server同时所能支撑的管道链接的个数,也是有限的,这将受限于server的物理内存或网络接口的缓冲能力。

压力测试

Redis 自带了一个压力测试工具redis-benchmark,使用这个工具就可以进行管道测试。

对一个普通的set指令进行压测,QPS 大约6w/s

1
2
➜  02 docker exec -it redisbloom redis-benchmark -t set -q
SET: 64350.06 requests per second

-P参数,表示单个管道内并行的请求数量,看下面P=2,QPS 达到了8w/s

1
2
docker exec -it redisbloom redis-benchmark -t set -q -P 2
SET: 89365.51 requests per second

再看看P=5QPS达到了10w/s

1
2
➜  02 docker exec -it redisbloom redis-benchmark -t set -q -P 5
SET: 106723.59 requests per second

但如果再继续提升P参数,发现QPS已经上不去了。这是为什么呢?

因为这里CPU处理能力已经达到了瓶颈,Redis的单线程CPU已经飙到了100%,所以无法再继续提升了。

深入理解管道本质
接下来我们深入分析一个请求交互的流程,真实的情况是它很复杂,因为要经过网络协议栈,这个就得深入内核了。

1
2
3
4

client ---> request ---> send buffer ---> NIC ---> Gateway Router ---> NIC ---> recv buffer ---> request ---> server
|
client <--- response <--- recv buffer <--- NIC <--- Gateway Router <--- NIC <--- send buffer <--- response <--- V

上图就是一个完整的请求交互流程图,

  1. 客户端进程调用write将消息写到操作系统内核为套接字分配的发送缓冲send buffer;
  2. 客户端操作系统内核将发送缓冲的内容发送到网卡网卡硬件将数据通过「路由」送到服务器的网卡;
  3. 服务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer;
  4. 服务器进程调用read从接收缓冲中取出消息进行处理;
  5. 服务器进程调用write将响应消息写到内核为套接字分配的发送缓冲send buffer;
  6. 服务器操作系统内核将发送缓冲的内容发送到网卡网卡硬件将数据通过「路由」送到客户端的网卡
  7. 客户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer;
  8. 客户端进程调用read从接收缓冲中取出消息返回给上层业务逻辑进行处理;

write操作不是等到对方收到消息才会返回。write操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间来,这个就是写操作IO操作的真正耗时。

read操作不是从目标机器拉取数据。read操作只负责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就需要等待数据到来,这个就是读操作IO操作的真正耗时。

所以对于value = redis.get(key)这样一个简单的请求来说,write操作几乎没有耗时,直接写到发送缓冲就返回,而read就会比较耗时了,因为它要等待消息经过网络路由到目标机器处理后的响应消息,再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开销。

而对于管道来说,连续的write操作根本就没有耗时,之后第一个read操作会等待一个网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的read操作直接就可以从缓冲拿到结果,瞬间就返回了。

管道VS事务

管道和事务是不同的,pipeline只是表达”交互”中操作的传递的方向性,pipeline也可以在事务中运行,也可以不在。

无论如何,pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的相应结果集中得到信息;也就是pipeline并不是表达”所有command都一起成功”的语义,管道中前面命令失败,后面命令不会有影响,继续执行。

简单来说就是管道中的命令是没有关系的,它们只是像管道一样流水发给server,而不是串行执行,仅此而已; 但是如果pipeline的操作被封装在事务中,那么将有事务来确保操作的成功与失败。

pipeline 只是把多个redis指令一起发出去,redis并没有保证这些指定的执行是原子的;multi相当于一个redis的transaction的,保证整个操作的原子性,避免由于中途出错而导致最后产生的数据不一致

管道VS脚本

使用管道可能在效率上比使用script要好,但是有的情况下只能使用script。因为在执行后面的命令时, 无法得到前面命令的结果,就像事务一样,所以如果需要在后面命令中使用前面命令的value等结果,则只能使用script或者事务+watch;

使用Redis脚本(在Redis版本2.6+), 可以使用执行服务器端所需的大量工作的脚本更高效地处理一些pipelining用例;

脚本的一大优势是它能够以最小的延迟读取和写入数据,使得读取,计算,写入等操作非常快速(在这种情况下, 流水线操作无法提供帮助, 因为客户端先需要读命令的回应, 它才可以调用写命令);

有时,应用程序可能还想在管道中发送EVALEVALSHA命令。这是完全可能的,Redis通过SCRIPT LOAD命令明确地支持它(它保证可以调用EVALSHA而没有失败的风险);

总结
  • 从分析redis采用tcp消息协议入手, 为进一步提升redis性能的角度,探讨了管道技术在redis中的应用;
  • 进一步分析了管道的原理, 因为redis需要在处理完管道命令集前把之前的结果先缓存下来,所以管道并不是打包的命令越多越好,因为打包的命令越多占用的缓存也会相应的增大, 同时在执行管道命令完成前, 同一个redis连接无法继续执行非管道命令;
  • 通过消息交互示例, 进一步深入分析了管道的本质,管道打包多条命令为客服端–服务端节省了往返(RTT)等待耗时, 因而进一步提升了redis性能;
  • 最后对比分析了管道redis事务, redis脚本之间的区别, 进一步阐述了管道事务, 脚本各自适用的场景;

作者署名:朴实的一线攻城狮
本文标题:redis专题14 性能提升之管道技术
本文出处:http://researchlab.github.io/2018/10/11/redis-14-pipeline/
版权声明:本文由Lee Hong创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处, 否则保留追究法律责任的权利。

@全栈炼狱之路

关注微信公众号 @全栈炼狱之路

总访问:
总访客: