# Redis 流水线

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

Redis 流水线是一种通过一次发出多个命令而不等待每个单独命令的响应来提高性能的技术。大多数 Redis 客户端都支持流水线。本文档描述了流水线旨在解决的问题以及流水线在 Redis 中的工作原理。

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

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

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

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

例如,四个命令序列是这样的:

  • 客户: INCR X
  • 服务器: 1
  • 客户: INCR X
  • 服务器: 2
  • 客户: INCR X
  • 服务器: 3
  • 客户: INCR X
  • 服务器: 4

客户端和服务器通过网络链接连接。这样的链接可以非常快(环回接口)或非常慢(通过 Internet 建立的连接,在两台主机之间有许多跃点)。无论网络延迟是多少,数据包从客户端传输到服务器,然后从服务器返回到客户端以携带回复都需要时间。

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

如果使用的接口是环回接口,则 RTT 会短得多,通常为亚毫秒,但如果您需要连续执行多次写入,即使这样也会增加很多。

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

# Redis 流水线

可以实现请求/响应服务器,以便即使客户端尚未读取旧响应,它也能够处理新请求。这样就可以向服务器发送多个命令,而无需等待回复,最后一步即可读取回复。

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

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

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

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

明确地说,我们第一个示例的流水线操作顺序如下:

  • 客户: INCR X
  • 客户: INCR X
  • 客户: INCR X
  • 客户: INCR X
  • 服务器: 1
  • 服务器: 2
  • 服务器: 3
  • 服务器: 4

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

# 这不仅仅是 RTT 的问题

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

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

管道尺寸和 IOP

# 真实世界的代码示例

在以下基准测试中,我们将使用支持流水线的 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 2.6 起可用的 Redis 脚本,可以使用在服务器端执行大量所需工作的脚本更有效地解决流水线的许多用例。脚本的一大优势是它能够以最小的延迟读取和写入数据,使得读取、计算、写入等操作非常快(在这种情况下,流水线无法提供帮助,因为客户端需要之前读取命令的回复它可以调用写命令)。

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

# 附录:为什么即使在环回接口上繁忙的循环也很慢?

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

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

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

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

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

# 反馈

如果您在此页面上发现问题,或有改进建议,请提交请求以合并或打开存储库中的问题。

Last Updated: 4/18/2023, 8:45:33 AM