使用Pipelining加速Redis查询

305 阅读6分钟

Request/Response protocols and 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上的链接非常慢的情况下),即使服务器每秒能够处理10万个请求,我们每秒最多也可以处理四个请求。

如果使用的接口是环回接口,则RTT要短得多(例如,我的主机报告ping到127.0.0.1时为0.044毫秒),但是如果您需要连续执行多次写入操作,则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 /的角度来看这非常昂贵。这涉及到调用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
    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 OS X系统中通过环回接口提供以下数据,由于RTT已经非常低,因此流水线方法将提供最小的改进:

without pipelining 1.185238 seconds
with pipelining 0.250783 seconds

如您所见,使用流水线,我们将传输速度提高了五倍。

Pipelining VS Scripting

使用Redis脚本(在Redis 2.6或更高版本中可用),可以使用执行服务器端所需的大量工作的脚本来更有效地解决许多流水线用例。脚本编写的一大优势在于,它能够以最小的延迟读取和写入数据,从而使读取,计算,写入等操作非常快(在这种情况下,流水处理无济于事,因为客户端需要在读取之前回复读取命令) 它可以调用write命令)。

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

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

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

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

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

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

基本上,繁忙的基准测试是衡量网络服务器中的性能时可以完成的最简单的事情。明智的做法是避免以这种方式进行基准测试。