请求/响应 协议与往返时延
Redis 基于TCP协议实现服务端,使用客户端服务器模型(请求/应答 协议)进行通讯。
这决定了Redis中,请求需要经过以下步骤才能完成:
- 客户端发送请求到服务端,之后等待读取来自服务端的响应内容(通常采用阻塞模型)。
- 服务端处理客户端命令,并将响应返回给客户端。
举个例子,连续的四条命令执行过程类似这样:
- Client: INCR X
- Server: 1
- Client: INCR X
- Server: 2
- Client: INCR X
- Server: 3
- Client: INCR X
- Server: 4
客户端与服务器之间通过网络连接进行通讯,这种连接可以很快(例如:本地回环)或者也可能非常慢(例如:一个跨越多个节点的互联网连接)。但无论如何,一个数据包从客户端发送到服务器并成功返回总要耗费一段时间。
这段时间称为往返时延(RTT)。当客户端需要连续执行大量请求(例如:向一个列表中加入大量元素或者从数据库中映射大量键)时,我们很容易发现这种请求响应模型对性能的影响。举个例子:在一个连接状况不好的互联网环境下,RTT为250毫秒,即便服务端每秒可以轻松处理十万请求,我们每秒钟也仅仅能处理4个请求。
如果采用本地回环连接,RTT可以很短(例如我的机器在本机ping 127.0.0.1地址只需要0.044毫秒),但是当请求量巨大时,这个短暂的延时仍然影响巨大。
幸运的是,我们有方法应对这种情况。
Redis 管道
我们可以实现这样一种请求响应模型,客户端在收到之前发送请求响应之前允许发送新的请求。这样,我们就可以实现快速发送多条命令到服务器而无需等待,之后统一处理多条响应的高效通讯场景。
这种模型被称为管道, 事实上类似的技术已被大范围应用了几十年。例如:我们熟知的POP3协议就已经支持这个特性,可以实现动态加速新邮件下载速度。
Redis 从一开始就支持了这个特性,因此不管你在用哪个Redis版本,都可以使用管道技术。
以下是一个采用了netcat utility的示例:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
扎这种模型下我们无需对每次请求耗费RTT时间,而是三条命令仅需一次RTT。
之前的示例如果使用了管道之后,其执行命令顺序如下:
- Client: INCR X
- Client: INCR X
- Client: INCR X
- Client: INCR X
- Server: 1
- Server: 2
- Server: 3
- Server: 4
重要提示: 由于在管道通讯模型下,服务端会在请求发送时,强制缓存全部响应内容到内存中。因此,当需要发送大量请求时,最好分批次进行发送。例如:每10k个命令发送后,读取一次响应,之后再发送另外10k请求,以此类推。这种分批处理方式不会影响总体处理效率,但可以保证服务端缓存的响应内容不至于太多。
除了RTT还有其他好处
管道技术其实不仅减少了RTT,事实上,在Redis server中,采用管道技术也大大减少了需要处理的命令数量。
在不采用管道技术的情况下,但从操作数据并返回响应内容角度看,影响并不大,但是对于 socket 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
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
可以很明显的看出,采用管道技术,性能提升了5倍。
管道还是脚本
使用Redis scripting (Redis2.6之后支持) 功能可以更高效的完成一些管道适用的用户场景,但在服务端需要更复杂的编程工作。
脚本的好处是可以支持延时更低的读写数据操作,使得 read, compute, write 操作更快, (在写入前需要读取操作的情况下,管道无法提供更多效率提升)。