Redis 管道
如何通过批量处理Redis命令来优化往返时间
Redis 流水线是一种通过一次性发出多个命令而不等待每个单独命令的响应来提高性能的技术。大多数 Redis 客户端都支持流水线。本文档描述了流水线旨在解决的问题以及流水线在 Redis 中的工作原理。
请求/响应协议和往返时间(RTT)
Redis 是一个使用客户端-服务器模型的 TCP 服务器,采用所谓的请求/响应协议。
这意味着通常一个请求是通过以下步骤完成的:
- 客户端向服务器发送查询,并从套接字读取,通常以阻塞方式读取,以获取服务器响应。
- 服务器处理命令并将响应发送回客户端。
例如,一个四个命令的序列是这样的:
- 客户端: INCR X
- 服务器: 1
- 客户端: INCR X
- 服务器: 2
- 客户端: INCR X
- 服务器: 3
- 客户端: INCR X
- 服务器: 4
客户端和服务器通过网络链接连接。 这样的链接可以非常快(回环接口)也可以非常慢(通过互联网建立的两个主机之间有许多跳的连接)。 无论网络延迟如何,数据包从客户端到服务器,再从服务器返回客户端以携带回复都需要时间。
这次被称为RTT(往返时间)。 很容易看出,当客户端需要连续执行许多请求时(例如向同一列表添加许多元素,或用许多键填充数据库),这会如何影响性能。 例如,如果RTT时间为250毫秒(在互联网上非常慢的链接情况下),即使服务器能够每秒处理100k个请求,我们最多也只能每秒处理四个请求。
如果使用的接口是回环接口,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倍,如图所示。
一个真实世界的代码示例
在以下基准测试中,我们将使用支持流水线的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 |rp|
10_000.times do
rp.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
而不会有失败的风险)。
附录:为什么即使在回环接口上,忙循环也很慢?
即使本页涵盖了所有背景信息,您可能仍然会想知道为什么像下面这样的Redis基准测试(伪代码),即使在同一台物理机器上运行服务器和客户端时,在回环接口上执行也会很慢:
FOR-ONE-SECOND:
Redis.SET("foo","bar")
END
毕竟,如果Redis进程和基准测试都在同一台机器上运行,这不就是在内存中从一个地方复制消息到另一个地方,而没有实际的延迟或网络参与吗?
原因是系统中的进程并不总是在运行,实际上是内核调度器让进程运行。 因此,例如,当基准测试被允许运行时,它会从Redis服务器读取回复(与最后执行的命令相关),并写入一个新命令。 该命令现在位于回环接口缓冲区中,但为了被服务器读取,内核应该调度服务器进程(当前在系统调用中被阻塞)运行,依此类推。 因此,实际上回环接口仍然涉及类似网络的延迟,这是由于内核调度器的工作方式。
基本上,繁忙循环基准测试是在网络服务器上测量性能时可以做的最愚蠢的事情。明智的做法是避免以这种方式进行基准测试。