使用管道提升Redis查询速度

请求/响应网关和RTT

Redis是一个使用被称为请求/响应网关的客户机-服务器(client-server)模型的TCP服务器。

这意味着一个请求通常通过以下步骤完成的:

  • 客户端像服务器端发送一个请求,然后从套接字中读取服务器端的响应,通常以阻塞方式读取。
  • 服务器端执行完命令,然后发送响应返回给客户端。

例如,一个四个指令的序列是这样的:

  • Client: INCR X
  • Server: 1
  • Client: INCR X
  • Server: 2
  • Client: INCR X
  • Server: 3
  • Client: INCR X
  • Server: 4

客户端和服务器端通过网络连接起来。网络可能非常快(一个环回接口),或者非常慢(两个主机通过Internet建立起来的多跳连接)。不管延迟是多少,将包从客户端发送到服务端,然后携带服务器端的回复返回到客户端,都需要时间。

这个时间被称为RTT(往返时间 Round Trip Time)。当客户端需要连续执行许多请求时(例如,向同一个列表中添加许多元素,或使用很多键填充一个数据库),能够非常容易的看到它是如何影响性能的。例如,如果RTT时间是250毫秒(在这个例子中是一个非常慢的Internet连接),即使服务器能够每秒处理100K请求,我们也只能最多每秒处理4个请求。

如果使用的接口是环回接口,RTT将会非常短(例如,ping 127.0.0.1我的主机报告的0,044毫秒),但是当你执行大量的连续写入操作时RTT仍然是很多的。

幸运的是,有一个方法可以改进此用例。

Redis Pipelining Redis管道

可以实现一个请求/响应服务器,这样即使客户端还没有准备好读取旧的响应时,它都能够处理新的请求。这种方式能够给服务器发送多命令而不用等待响应,并且最后在单个步骤里读取应答。

这被称为管道,一个数十年前就被广泛使用的技术。例如,许多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服务的每秒最大处理数量提升一大截。这个事实的结果是,不使用管道时,从为每个命令提供访问数据结构和产生回复的视角看,提供服务的成本非常低,但是socket接口I/O的视角看则是非常值得的。这个涉及到调用read()write()系统调用,这意味着从用户态转换到系统态。这个上下文切换会导致速度大幅降低。

当管道被使用时,许多命令通常可以从一次read()系统调用读取,并且许多回复可以通过一次write()系统调用来传递。由于这个原因,一开始,每秒钟执行的查询总数随着管道的延长几乎是直线增加,最终能达到不使用管道为基数的10倍以上,就像你在下图中看到的那样。

Pipeline size and IOPs

一些真实的代码示例

在下面的基础测试中,我们将会使用支持管道的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
}

Running the above simple script will provide the following figures in my Mac OS X system, running over the loopback interface, where pipelining will provide the smallest improvement as the RTT is already pretty low:

在我的Mac OSX系统上运行上面的简单脚本,将会提供如下的图表,运行在环回接口上,管道将会提供最小的改进,因为RTT已经是相当的低了:

without pipelining 1.185238 seconds
with pipelining 0.250783 seconds

如你所见,使用管道,我们将传输提高了5倍。

管道 VS 脚本

有大量的用例在使用Redis脚本(Redis2.6及更高的版本可用)可以解决管道的问题,当服务器端有大量的任务需要执行时,使用脚本可以让管道变得更高效。脚本的一大优势是,可以使用最小化的延迟读写数据,以非常快的速度读,计算,写(在这个场景下管道可能没有太多用,因为客户端在可以调用写命令之前,需要获得读命令的回复)。

有时应用也可能要在管道中发送EVALEVALSHA命令。这是完全有可能的,Redis中可以使用SCRIPT LOAD命令(它确保EVALSHA可以被没有失败风险的调用)。

附录:为什么即使在环回接口上一个忙碌的循环也会慢

即使了解了本页的所有背景,你可能仍然想要知道为什么Redis基准测试会像下面这样(伪代码),展示了在环回接口中执行,服务器端和客户端运行在同样的物理机器上:

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

毕竟,当Redis进程和基准测试都运行在同样的盒子里,难道这不应该仅是从内存的一个地方复制到另外一个地方,而没有延迟和不涉及到网络吗?

原因在于,在内存中进程并不总是在运行中,实际上是内核的调度让它运行,因此发生了,例如,基准测试允许运行时从Redis服务器读取回复(关联到最近执行的命令),然后写入一个新的命令。现在命令在换回网络接口缓存上,但是为了从服务器读取,内核应该调度服务端进程运行(目前被系统调用阻塞),等等。因此在实际操作中,由于内存调度工作方式的原因,环回接口任务仍然涉及类网络延迟(network-alike)。

基本上,在测量服务端性能的时候,一个忙碌的循环基准测试是最笨的事情。聪明的方式是避免使用这种方式做基准测试。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容