美文网首页
探究Redis 07:使用管道加速请求

探究Redis 07:使用管道加速请求

作者: wzhwizard | 来源:发表于2020-08-18 16:55 被阅读0次

    请求/响应 协议与往返时延

    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倍与常规通讯模型的效率,如下图:

    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
    }
    
    

    在我的Mac OS X 系统中,上面的测试代码显示如下结果, (由于运行时采用本地回环地址,RTT开销已经相当小了):

    without pipelining 1.185238 seconds
    with pipelining 0.250783 seconds
    
    

    可以很明显的看出,采用管道技术,性能提升了5倍。

    管道还是脚本

    使用Redis scripting (Redis2.6之后支持) 功能可以更高效的完成一些管道适用的用户场景,但在服务端需要更复杂的编程工作。
    脚本的好处是可以支持延时更低的读写数据操作,使得 read, compute, write 操作更快, (在写入前需要读取操作的情况下,管道无法提供更多效率提升)。

    相关文章

      网友评论

          本文标题:探究Redis 07:使用管道加速请求

          本文链接:https://www.haomeiwen.com/subject/pmkkjktx.html