Redis pipelining

内容纲要

如何通过批处理Redis命令优化往返时间

Redis流水线是一种通过一次发出多个命令而无需等待对每个命令的响应来提高性能的技术。大多数Redis客户端都支持Pipelining。本文档描述了管道设计用来解决的问题,以及管道在Redis中如何工作。

请求/响应协议和往返时间(RTT)

Redis是一个TCP服务器,使用客户机-服务器模型和所谓的请求/响应协议。

这意味着通常通过以下步骤来完成请求:

客户端向服务器发送一个查询,并从套接字中读取服务器响应,通常以阻塞的方式。
服务器处理命令并将响应发送回客户端。
例如,四个命令序列是这样的:

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

客户端和服务器通过网络链接连接。这样的链接可以非常快(环回接口),也可以非常慢(在两台主机之间通过Internet建立的多跳连接)。无论网络延迟是多少,数据包从客户端到服务器以及从服务器返回到客户端以携带应答都需要时间。

这个时间称为RTT(往返时间)。当客户端需要在一行中执行许多请求时(例如向同一个列表中添加许多元素,或者用许多键填充数据库),很容易看出这会如何影响性能。例如,如果RTT时间是250毫秒(在Internet上非常慢的链接的情况下),即使服务器每秒能够处理100k个请求,我们每秒最多只能处理4个请求。

如果使用的接口是环回接口,则RTT要短得多,通常为亚毫秒级,但如果需要在一行中执行许多写入操作,则即使是这样,RTT也会增加很多。

幸运的是,有一种方法可以改进这个用例。

Redis Pipelining

可以实现一个请求/响应服务器,这样即使客户端还没有读取旧的响应,它也能够处理新的请求。这样就可以向服务器发送多个命令,而根本不需要等待响应,最后在一个步骤中读取响应。

这被称为流水线,是一种广泛使用了几十年的技术。例如,许多POP3协议实现已经支持这一功能,极大地加快了从服务器下载新邮件的过程。

Redis从早期就支持流水线,所以无论你运行的是什么版本,你都可以在Redis中使用流水线。这是一个使用原始netcat实用程序的例子:

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

这一次,我们不再为每个呼叫支付RTT费用,而只是为三个命令支付一次费用。

为了明确起见,我们第一个例子的流水线操作顺序如下:

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

重要提示:当客户端使用流水线发送命令时,服务器将被迫使用内存将应答排队。因此,如果您需要使用流水线发送大量命令,最好分批发送,每个批量包含一个合理的数量,例如10k条命令,读取响应,然后再次发送另外10k条命令,以此类推。速度几乎相同,但所使用的额外内存最多是这10k命令的应答排队所需的内存。

这不仅仅是RTT的问题

流水线不仅仅是一种减少与往返时间相关的延迟成本的方法,它实际上极大地提高了在给定的Redis服务器上每秒可以执行的操作数量。这是因为如果不使用流水线,从访问数据结构和生成应答的角度来看,为每个命令提供服务非常便宜,但从执行套接字I/O的角度来看,它的成本非常高。这涉及到调用read()和write()系统调用,这意味着从用户域到内核域。上下文切换是一个巨大的速度损失。

当使用流水线时,通常用一个read()系统调用读取许多命令,用一个write()系统调用传递多个响应。因此,使用更长的管道,每秒执行的查询总数最初几乎呈线性增长,最终达到不使用管道获得的基线的10倍,如图所示。

一个真实的代码示例

在下面的基准测试中,我们将使用支持管道的Redis Ruby客户端来测试由于管道而带来的速度提升:

require 'rubygems'
require 'redis'

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

def without_pipelining
  r = Redis.new
  10_000.times do
    r.ping
  end
end

def with_pipelining
  r = Redis.new
  r.pipelined do
    10_000.times do
      r.ping
    end
  end
end

bench('without pipelining') do
  without_pipelining
end
bench('with pipelining') do
  with_pipelining
end

在我的Mac OS X系统上运行上面的简单脚本会得到下面的图,在环回接口上运行,管道将提供最小的改进,因为RTT已经很低了:

without pipelining 1.185238 seconds
with pipelining 0.250783 seconds

正如您所看到的,使用流水线,我们将传输提高了五倍。

流水线与脚本

使用Redis脚本,从Redis 2.6开始就可以使用脚本,可以更有效地解决管道的许多用例,这些脚本执行了服务器端所需的大量工作。脚本的一个很大的优势是,它能够以最小的延迟读取和写入数据,使得读、计算、写等操作非常快(在这种情况下,流水线无法提供帮助,因为客户端需要在调用写命令之前返回读命令)。

有时应用程序可能还希望在管道中发送EVAL或EVALSHA命令。这是完全可能的,并且Redis通过SCRIPT LOAD命令明确地支持它(它保证可以在没有失败风险的情况下调用EVALSHA)。

附录:为什么在loopback 接口上 busy loops 也慢?

即使在这一页中涵盖了所有的背景,你可能仍然想知道为什么像下面这样的Redis基准测试(在伪代码中),即使在环回接口中执行也很慢,当服务器和客户端运行在同一台物理机器上时:

FOR-ONE-SECOND:
    Redis.SET("foo","bar")
END

毕竟,如果Redis进程和基准测试都运行在同一个机器中,它不就是将内存中的消息从一个地方复制到另一个地方,而不涉及任何实际的延迟或网络吗?

原因是系统中的进程并不总是在运行,实际上是内核调度器让进程运行。因此,例如,当基准测试被允许运行时,它从Redis服务器读取回复(与最后执行的命令相关),并写入一个新命令。该命令现在位于环回接口缓冲区中,但为了被服务器读取,内核应该调度服务器进程(当前在系统调用中阻塞)运行,等等。因此,实际上,由于内核调度器的工作方式,环回接口仍然涉及类似网络的延迟。

基本上,当在网络服务器上测量性能时,繁忙循环基准测试是最愚蠢的事情。明智的做法是避免以这种方式进行基准测试。

Leave a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注

close
arrow_upward