redis使用管道(Pipelining)提高查询速度

*请求/响应协议和RTT redis 管道(Pipelining)

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务器。

这意味着请求通常按如下步骤处理:

  • 客户端发送一个请求到服务器,并以阻塞的方式从socket读取数据,获取服务端响应 。
  • 服务端处理请求命令并发送响应回给客户端。

所以对于四个命令的处理流程如下:

  • Client: INCR X
  • Server: 1
  • Client: INCR X
  • Server: 2
  • Client: INCR X
  • Server: 3
  • Client: INCR X
  • Server: 4

Clients 和 Servers 通过网络连接. 可以是本地非常快的网络,或者是通过互联网连接很远的网络。不管网络延迟如何,数据包从客户端发给服务端,再从服务端返回给客户端都要花费一个时间。

这个时间叫做 RTT (Round Trip Time往返时间). 所以如果客户端需要连续发送多个请求的情况下,RTT对性能的影响是很严重的。例如在延迟很大的网络中RRT是250ms,即使服务端每秒能处理10万个请求,我们也只能每秒最多处理四个请求。

如果使用本机网络,这个RTT回非常小,一般小于1ms,但是如果大量请求累加起来仍然会影响性能。

一个好的办法就是使用管道

*Redis Pipelining

对于基于 Request/Response 的服务器可以被实现成处理新的请求,即使客户端还没收到旧的响应。这样就可以同时发送多个命令给服务器而不用等待响应,最后统一读回多个返回。

这种方式被叫做 pipelining, 是一个广泛使用多年的技术,批量发送,批量返回。例如POP3协议也是这样,来加速从服务器上下载邮件。

Redis 很早就开始支持 pipelining , 所以不管什么版本的Redis都能使用 pipelining 命令。下面是使用netcat命令的例子:

$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

不再每个命令都花费一次RTT,只用了一个RTT就执行了所有命令。第一个例子修改如下:

  • Client: INCR X
  • Client: INCR X
  • Client: INCR X
  • Client: INCR X
  • Server: 1
  • Server: 2
  • Server: 3
  • Server: 4

特别注意: 当客户端使用管道 pipelining发送命令时,服务器端需要消耗内存来存放响应,所以如果你需要发送大量的命令,最好分批发送,例如一次发送1万个,读取回报,再循环发剩余的命令。速度上几乎无差异,但是内存最大消耗1万个命令回复结果的内存。

*基准测试

下面使用的是Redis Ruby客户端,来测试 pipelining 对速度的提升:

require 'rubygems'
require 'redis'

def bench(descr)
    start = Time.now
    yield
    puts "#{descr} #{Time.now-start} seconds"
end

def without_pipelining
    r = Redis.new
    10000.times {
        r.ping
    }
end

def with_pipelining
    r = Redis.new
    r.pipelined {
        10000.times {
            r.ping
        }
    }
end

bench("without pipelining") {
    without_pipelining
}
bench("with pipelining") {
    with_pipelining
}

在mac上执行上面的脚本得到如下输出,因为是本机访问,提升并不明显,本机环境下RTT已经很小:

不用pipelining 1.185238 seconds
使用 pipelining 0.250783 seconds

使用pipelining, 我们能大概提高5倍速度。

*Pipelining VS Scripting

通过使用 Redis scripting (2.6以后) ,许多使用管道的场景可以更有效,脚本执行许多服务端需要的工作。 scripting 的一个大的优势是它可以以最小的延迟同时读和写数据,使read, compute, write 这些操作都非常快。 (pipelining在这种场景没有效果,因为客户端需要得到返回之后才能继续写)

有时应用程序想在pipeline中发送 EVAL 或 EVALSHA 命令。Redis使用 SCRIPT LOAD 命令支持 (它确保 EVALSHA 能被调用而没有失败的风险)。