请求/响应协议和RTT(往返时间(Round Trip Time))
Redis是一个TCP服务器使用客户端-服务器模型,被称为请求/响应协议。
这意味着请求通常通过以下步骤完成:
- 客户端向服务器发送一个查询,并从套接字读取服务器响应(通常以阻塞的方式)。
- 服务器处理命令并将响应发送回客户机。
客户端和服务器通过网络连接。这样的链路可以非常快(环回接口),也可以非常慢(在Internet上建立的连接,在两台主机之间有许多跳点)。无论网络延迟是什么,数据包从客户机传输到服务器,然后从服务器返回到客户机以携带应答都需要一段时间。
这个时间称为RTT(往返时间)。很容易看出,当客户机需要在一行中执行许多请求时(例如向同一个列表添加许多元素,或使用许多键填充数据库),这会如何影响性能。例如,如果RTT时间是250毫秒(在Internet上链接非常慢的情况下),即使服务器每秒能够处理100k个请求,我们每秒最多也只能处理4个请求。
如果使用的接口是环回接口,RTT就会短得多(例如,我的主机报告的ping值为127.0.0.1,为0 044毫秒),但是如果您需要在一行中执行许多写操作,那么RTT仍然很短。
幸运的是,有一种方法可以改进这个用例。
redis 流水线
可以实现请求/响应服务器,这样即使客户端没有读取旧响应,它也能够处理新请求。通过这种方式,可以向服务器发送多个命令,而根本不需要等待响应,并最终在一个步骤中读取响应。
这被称为管道铺设,是一项几十年来广泛使用的技术。例如,许多POP3协议实现已经支持这个特性,大大加快了从服务器下载新邮件的过程。
Redis从早期就支持pipelining,所以无论你运行的是什么版本,你都可以在Redis中使用pipelining。这是一个使用原始netcat实用程序的例子:
重要提示:当客户端使用管道发送命令时,服务器将被迫使用内存对响应进行排队。因此,如果您需要使用管道发送大量命令,最好以数量合理的批次发送,例如10k命令,读取应答,然后再次发送另一个10k命令,以此类推。速度几乎相同,但所使用的额外内存最大将达到对这些10k命令的响应进行排队所需的容量。
这不仅仅是RTT的问题
Pipelining不仅仅是为了减少由于往返时间而造成的延迟成本,它实际上大大提高了你在给定Redis服务器上每秒可以执行的总操作量。这是因为,如果不使用管道,从访问数据结构和生成应答的角度来看,为每个命令提供服务是非常便宜的,但从执行套接字I/O的角度来看,则是非常昂贵的。这涉及到调用read()和write()系统调用,这意味着从用户地到内核地。上下文切换是一个巨大的速度损失。
当使用管道时,通常通过一个read()系统调用来读取许多命令,通过一个write()系统调用来传递多个应答。正因为如此,最初每秒执行的查询总数几乎随着管道长度的增加而线性增长,最终达到不使用管道获得基线的10倍,如下图所示:
一些真实的代码示例
在下面的基准测试中,我们将使用支持pipelining的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 OS X系统,运行在环回接口,管道将提供最小的改善,因为RTT已经很低:
without pipelining 1.185238 seconds
with pipelining 0.250783 seconds
如您所见,使用流水线,我们将传输性能提高了五倍。
流水线和脚本
使用Redis脚本(可以在2.6版本或更高版本中获得),可以更有效地解决流水线的许多用例,这些用例可以执行很多服务器端需要的工作。脚本的一大优点是,它能够以最小的延迟读写数据,使读、计算、写等操作非常快(在这种情况下,管道没有帮助,因为客户端在调用写命令之前需要对读命令进行应答)。
有时,应用程序可能还希望在管道中发送EVAL或EVALSHA命令。这是完全可能的,Redis明确支持它与脚本加载命令(它保证EVALSHA可以被调用没有失败的风险)。
附录:为什么即使在环回接口上,忙碌的循环也会很慢?
即使有了这页的所有背景,你可能仍然想知道为什么一个Redis基准像下面(在伪代码),是很慢,即使在环回接口执行,当服务器和客户端运行在同一物理机器:
FOR-ONE-SECOND:
Redis.SET("foo","bar")
END
毕竟,如果Redis进程和基准测试都在同一个盒子里运行,这难道不只是消息从一个地方复制到另一个地方,没有任何实际的延迟和实际的网络涉及?
原因是系统并不总是运行过程,实际上它是内核调度器,让进程运行,那么会发生什么是,例如,允许基准运行,从复述,服务器读取应答(最后一个命令执行相关),和写一个新的命令。该命令现在在环回接口缓冲区中,但是为了让服务器读取该命令,内核应该安排服务器进程(目前在系统调用中被阻塞)运行,等等。因此在实际中,环回接口仍然涉及类似网络的延迟,这是因为内核调度器的工作方式。
基本上,在度量网络服务器中的性能时,繁忙循环基准测试是最愚蠢的做法。明智的做法是避免以这种方式进行基准测试。