使用pipeline加速Redis查询

Request/Response协议和RTT

Redis是一个使用client-server模型和Request/Response协议的TCP服务器。

这意味着一个请求通常包含以下步骤:

  • client向server发送一个查询命令,并以阻塞的方式从socket中读取服务器响应。
  • server处理命令并将响应发送回client。

例如:下面的四个命令:

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

ClientServer通过网络连接。这样的链接可以非常快(环回接口)或非常慢(通过因特网建立两个主机的连接)。无论网络延迟如何,数据包从Client传输到Server,再从Server传输到Client来进行回复,都需要时间。

这个时间被称为RTT(往返时间)。当Client需要连续执行多个请求(例如,向同一个列表中添加多个元素,或者用多个键填充数据库)时,很容易看出这会如何影响性能。例如,如果RTT时间是250毫秒(在Internet上的链接非常慢的情况下),即使服务器每秒能够处理100k个请求,我们每秒最多只能处理4个请求。

如果使用的接口是环回接口,则RTT要短得多(例如,我的主机ping 127.0.0.1延迟只有0.044毫秒),但如果您需要连续执行许多写入,则RTT仍然会很多。

幸运的是,有一种方式可以改进这种情况。

Redis Pipelining

可以实现一种Request/Response服务器,即使client还没有读取到旧的响应,它也可以处理新的请求。这样就可以在不等待回复的情况下向服务器发送多个命令,并最终在一个步骤中读取回复。

这被称为pipelining(流水线),是一种广泛使用了几十年的技术。例如,许多POP3协议的实现已经支持这个特性,大大加快了从服务器下载新电子邮件的过程。

Redis从很早的时候就支持pipelining,所以无论您运行的是什么版本,您都可以使用Redis的pipelining。这是一个使用原生Netcat实用程序的示例:

$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

这一次我们不会每一次调用都花费RTT,因为三个命令只花费了一个RTT。

非常明确地说,通过pipelining,我们第一个示例的操作顺序如下:

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

重要提示:client使用pipelining发送命令时,服务器将被迫使用内存对回复进行排队。因此,如果需要通过pipelining发送大量命令,最好将它们按照合理的数量分批发送,例如10k命令一批,读取回复,然后再次发送另一个10k命令,依此类推。速度将几乎相同,但所使用的额外内存只需要最大限度地满足对这些10k命令的回复的需要即可。

这不仅仅是RTT的问题

pipelining不仅仅是减少了往返时延,它实际上极大地提高了Redis服务器上每秒可以执行的操作数。这是因为,如果不使用pipelining,从访问数据结构和生成响应的角度来看,执行每个命令是非常廉价的,但是从执行套接字I/O的角度来看,却是非常昂贵的。这涉及到read()和write()的系统调用,这意味着从用户态到内核态。上下文切换是一个巨大的性能损耗。

当使用pipelining时,许多命令通常通过一个read()的系统调用读取,而多个响应则通过一个write()系统调用传回。因此,每秒执行的总查询数随着pipelining的增长几乎呈线性增加,最终达到没有pipelining的情况下的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非常低的情况下,pipelining只有很小的改进:

without pipelining 1.185238 seconds
with pipelining 0.250783 seconds

如您所见,使用pipelining,我们将传输提高了五倍。

Pipelining vs 脚本

许多pipelining的用例通过使用脚本(在Redis版本2.6或更高版本中提供,在服务端执行大量所需的工作)可以更加有效率。脚本的一大优点是,它能够以极小的延迟读取和写入数据,使得读取、计算和写入等操作非常快(在这种情况下,pipelining没有帮助,因为client需要读取命令的回复才能调用写入命令)。

有时应用程序可能还希望在pipeline中发送EVAL或EVALSHA命令。这是完全可能的,Redis通过SCRIPT LOAD命令显式支持它(它保证可以调用EVALSHA而不会有失败的风险)。

附录:为什么轮询即使在环回接口上也很慢?

即使本页介绍了所有的背景知识,您仍然可能想知道,当server和client在同一台物理机器上运行时,为什么像下面这样的Redis基准(伪代码)即使在环回接口中执行也很慢:

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

毕竟,如果Redis进程和基准都在同一个机器中运行,这不就是将内存中的消息从一个地方复制到另一个地方而不涉及任何实际延迟或网络吗?

原因是系统中的进程并不总是在运行,实际上是内核调度器让进程运行,所以发生的情况是,例如,基准被允许运行,读取来自Redis服务器的回复(与最后执行的命令相关),然后写入新的命令。命令现在在环回接口缓冲区中,但是为了被服务器读取,内核将会调度服务器进程(当前在系统调用中被阻塞)运行,以此类推。因此,实际上,由于内核调度器的工作方式,环回接口仍然涉及类似网络的延迟。

总的来说,在网络服务器中测量性能时,轮询基准测试是最愚蠢的事情。明智的做法是避免以这种方式进行基准测试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值