一、背景
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:
- 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
- 服务端处理命令,并将结果返回给客户端。
因此,例如下面是4个命令序列执行情况:
1 2 3 4 5 6 7 8 |
Client: INCR X Server: 1 Client: INCR X Server: 2 Client: INCR X Server: 3 Client: INCR X Server: 4 |
客户端和服务器通过网络进行连接。这个连接可以很快(loopback接口)或很慢(建立了一个多次跳转的网络连接)。无论网络延如何延时,数据包总是能从客户端到达服务器,并从服务器返回数据回复客户端。这个时间被称之为 RTT (Round Trip Time – 往返时间),当客户端需要在一个批处理中执行多次请求时很容易看到这是如何影响性能的(例如添加许多元素到同一个list,或者用很多Keys填充数据库)。例如,如果RTT时间是250毫秒(在一个很慢的连接下),即使服务器每秒能处理100k的请求数,我们每秒最多也只能处理4个请求。如果采用loopback接口,RTT就短得多(比如我的主机ping 127.0.0.1只需要44毫秒),但它任然是一笔很多的开销在一次批量写入操作中。
这显示没有充分利用redis的处理能力。除了可以利用mget,mset之类的单条命令处理多个key的命令外我们还可以利用pipeline的方式从client打包多条命令一起发出,不需要等待单条命令的响应返回,而redis服务端会处理完多条命令后会将多条命令的处理结果打包到一起返回给客户端。
二、Redis管道(Pipelining)
Redis的pipeline(管道)功能在命令行中没有,但redis是支持pipeline的,而且在各个语言版的client中都有相应的实现。 由于网络开销延迟,即算redis server端有很强的处理能力,也由于收到的client消息少,而造成吞吐量小。当client使用pipelining发送命令时,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。Redis server必须部分请求放到队列中(使用内存)执行完毕后一次性发送结果。
管道(pipelining),是一种几十年来广泛使用的技术。例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis很早就支持管道(pipelining)技术,因此无论你运行的是什么版本,你都可以使用管道(pipelining)操作Redis。下面是一个使用的例子:
1 2 3 4 |
$ printf "PING\r\nPING\r\nPING\r\n"; sleep 1 | nc localhost 6379 PING PING PING |
这一次我们没有为每个命令都花费了RTT开销,而是只用了一个命令的开销时间。
用管道顺序操作的第一个例子如下:
1 2 3 4 5 6 7 8 |
Client: INCR X Client: INCR X Client: INCR X Client: INCR X Server: 1 Server: 2 Server: 3 Server: 4 |
重要说明: 使用管道发送命令时,服务器将被迫会用一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如10K的命令,读回复,然后再发送另一个10k的命令,等等。这样速度几乎是相同的,但是在回复这10k命令队列需要非常大量的内存用来组织返回数据内容。
三、Python测试
同时提交10000个command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#!/usr/bin/python import redis import time def without_pipeline(): r=redis.Redis(host='localhost', port=6379,db=0) for i in range(10000): r.ping() return def with_pipeline(): r=redis.Redis(host='localhost', port=6379,db=0) pipeline=r.pipeline() for i in range(10000): pipeline.ping() pipeline.execute() return def bench(desc): start=time.clock() desc() stop=time.clock() diff=stop-start print "%s has token %s" % (desc.func_name,str(diff)) if __name__=='__main__': bench(without_pipeline) bench(with_pipeline) |
测试结果
1 2 3 |
$ python redis_piple.py without_pipeline has token 1.11 with_pipeline has token 0.29 |
注:在本机测试,基本忽略网络延迟,pipeline还是有很高的性能的。