Redis系列(1) —— 流水線

寫在前面

去年下半年,出于學(xué)習(xí)Redis的目的,在看完《Redis in Action》一書后,開始嘗試翻譯Redis官方文檔。盡管Redis中文官方網(wǎng)站有了譯本,但是看別人翻譯好的和自己翻譯英文原文畢竟還是有很大的不同。這一系列文章之前發(fā)布在GitBook上,為了方便管理,跟其他文章一起放在同一個(gè)平臺,遂全部遷移至簡書。由于本人學(xué)習(xí)Redis時(shí)間不長,認(rèn)識有限,同時(shí)也缺少實(shí)戰(zhàn)經(jīng)驗(yàn),翻譯中有任何不恰當(dāng)之處,歡迎各位及時(shí)斧正,本人將不勝感激。對英文官方文檔感興趣的朋友也可以直接訪問https://redis.io/ 進(jìn)行獲取。

使用流水線來提升redis的查詢速度
請求/響應(yīng)協(xié)議和RTT

Redis是一個(gè)使用客戶端-服務(wù)端模型和請求/響應(yīng)協(xié)議的TCP服務(wù)。這意味著完成一次請求通常需要經(jīng)過以下步驟:

  • 客戶端向服務(wù)端發(fā)起一次查詢請求并讀取socket,這通常是以阻塞方式來等待服務(wù)端響應(yīng)。
  • 服務(wù)端處理命令并將響應(yīng)發(fā)回給客戶端。

例如下面是一個(gè)4條命令序列的執(zhí)行情況:

  • 客戶端:INCR x
  • 服務(wù)端:1
  • 客戶端:INCR x
  • 服務(wù)端:2
  • 客戶端:INCR x
  • 服務(wù)端:3
  • 客戶端:INCR x
  • 服務(wù)端:4

客戶端和服務(wù)端通過網(wǎng)絡(luò)來連接。這樣的連接可以很快(loopback接口)也可以很慢(兩臺主機(jī)之間建立的是一個(gè)經(jīng)過了多次跳轉(zhuǎn)的網(wǎng)絡(luò)連接)。不管網(wǎng)絡(luò)延遲如何,數(shù)據(jù)包從客戶端發(fā)往服務(wù)端,然后攜帶響應(yīng)從服務(wù)端發(fā)往客戶端總是會消耗時(shí)間的。

這個(gè)時(shí)間被稱為RTT(Round Trip Time)。當(dāng)客戶端需要一次性處理很多請求時(shí)很容易看到這是如何影響到性能的(比如說向一個(gè)列表中添加很多元素,或者用很多鍵值填充數(shù)據(jù)庫)。例如假設(shè)RTT時(shí)間為250毫秒(在網(wǎng)絡(luò)連接很慢的網(wǎng)絡(luò)條件下),那么即使服務(wù)端每秒可以處理100000個(gè)請求,我們每秒最多也只能處理4個(gè)請求。

如果使用loopback接口,RTT時(shí)間就會短很多(例如在我的機(jī)器上ping127.0.0.1只需要0.044毫秒),但是如果我們需要批量處理很多寫請求,這個(gè)時(shí)間仍然是很大的一筆開銷。

好在我們還有一種方式可以來改善這種狀況。

Redis流水線

即使客戶端舊的請求還沒有得到響應(yīng),一個(gè)請求/響應(yīng)服務(wù)器也可以處理新的請求。這樣一來我們就可以一次向服務(wù)端發(fā)送多條命令而根本不用等待響應(yīng),最后在一個(gè)步驟中讀取所有回復(fù)。

這就是流水線,這是一種幾十年來被廣泛采用的技術(shù)。例如很多POP3協(xié)議的實(shí)現(xiàn)已經(jīng)支持了這種特性,它極大地加快了從服務(wù)器下載新郵件的過程。

Redis很早就支持了流水線功能,所以無論你正在使用的是哪個(gè)版本,你都可以使用Redis的流水線技術(shù)。下面是一個(gè)使用這種原生能力的例子:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379

+PONG
+PONG
+PONG

這一次我們沒有為每次調(diào)用都消耗RTT,而是4個(gè)命令只消耗一次時(shí)間。

更明確地說,通過使用流水線技術(shù),我們第一個(gè)例子的操作順序?qū)窍旅孢@個(gè)樣子:

  • 客戶端: INCR x
  • 客戶端: INCR x
  • 客戶端: INCR x
  • 客戶端: INCR x
  • 服務(wù)端:1
  • 服務(wù)端:2
  • 服務(wù)端:3
  • 服務(wù)端:4

重要提示:當(dāng)客戶端使用流水線技術(shù)來發(fā)送命令時(shí),服務(wù)端將不得不使用內(nèi)存來排隊(duì)答復(fù)。所以如果你需要使用流水線來發(fā)送很多命令,最好是將他們按照合理的數(shù)量來分批處理,比如先發(fā)送10000條命令,讀取響應(yīng),再發(fā)送另外10000條命令等等。速度幾乎是一樣的,但是將需要大量額外的內(nèi)存來存儲這10000條命令的答復(fù)。

這不僅僅關(guān)乎RTT

流水線不僅僅是一種用來減少RTT延遲成本的方式,實(shí)際上對于一臺給定的Redis服務(wù)器,它極大地提高了每秒鐘你所能處理的操作數(shù)量。一個(gè)事實(shí)是,當(dāng)不采用流水線技術(shù)時(shí),從訪問數(shù)據(jù)結(jié)構(gòu)并且產(chǎn)生響應(yīng)的角度來看,每一條命令的時(shí)間消耗都是很少的,但是從處理socket IO的角度來看,這個(gè)時(shí)間消耗確是很大的。它涉及到調(diào)用read()和write()這些系統(tǒng)調(diào)用,這意味著要從用戶側(cè)到內(nèi)核側(cè)。而上下文切換是一個(gè)巨大的時(shí)間開銷,會嚴(yán)重影響響應(yīng)速度。

當(dāng)使用流水線時(shí),一個(gè)簡單的read()系統(tǒng)調(diào)用就可以讀取很多命令,同樣的,一個(gè)簡單的write()系統(tǒng)調(diào)用就可以將很多回復(fù)傳送出去。正因?yàn)槿绱?,每秒鐘可以處理的查詢命令的?shù)量幾乎隨著管道長度的增加而呈線性增長,最終可以達(dá)到不使用流水線這種基本情況時(shí)的10倍,正如你從下圖看到的那樣:


image.png
一些真實(shí)世界的代碼樣例

在下面這個(gè)基準(zhǔn)測試中,我們將會使用基于Ruby的redis客戶端,支持流水線操作,來測試流水線對于速度的提升效果:

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系統(tǒng)上執(zhí)行上面這個(gè)簡單的腳本將會得到如下的數(shù)據(jù),開啟流水線功能后,RTT已經(jīng)被改善得相當(dāng)?shù)汀?/p>

without pipelining 1.185238 seconds
with pipelining 0.250783 seconds

如你所見,開啟流水線后,我們把傳輸速度提升了5倍。

流水線 VS 腳本

使用Redis腳本(2.6及以上版本的redis可用),很多使用流水線的場景可以獲得更高效的處理,因?yàn)槭褂媚_本可以在服務(wù)端執(zhí)行大量工作。腳本的一大優(yōu)勢是它可以使讀寫數(shù)據(jù)只需要很小的時(shí)延,使得讀、計(jì)算和寫操作變得很快(流水線在這種場景下做不到這一點(diǎn),因?yàn)榭蛻舳嗽谡{(diào)用寫命令之前需要讀命令的返回結(jié)果)

有時(shí)候應(yīng)用也會需要向流水線發(fā)送EVAL或者EVALSHA命令。這完全是有可能的并且Redis已經(jīng)通過SCRIPT LOAD命令明確支持了這一點(diǎn)(它保證EVALSHA命令會調(diào)用成功)。

附錄:為什么即使在loopback接口上一個(gè)忙碌的循環(huán)也很慢

即使在這個(gè)頁面的背景之下,你還是會想知道為什么即使在loopback接口上執(zhí)行并且服務(wù)端和客戶端運(yùn)行在同一臺物理機(jī)上時(shí),一個(gè)一個(gè)像下面的Redis基準(zhǔn)測試(偽代碼)還是會很慢:

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

畢竟如果Redis過程和基準(zhǔn)測試運(yùn)行在一起時(shí),難道它不是僅僅將信息在內(nèi)存上從一個(gè)地方復(fù)制到另一個(gè)地方,中間沒有任何真正的延時(shí)和網(wǎng)絡(luò)參與進(jìn)來嗎?

原因在于系統(tǒng)上的過程不是一直在運(yùn)行,實(shí)際上是內(nèi)核調(diào)度器才讓過程運(yùn)行起來,所以基準(zhǔn)測試開始運(yùn)行時(shí),從Redis服務(wù)端讀取返回?cái)?shù)據(jù)(跟最后一條執(zhí)行的命令相關(guān)),并且寫了一條新命令?,F(xiàn)在命令存在于loopback接口的緩存里,但是為了能夠被服務(wù)端讀取到,內(nèi)核會通過調(diào)度讓服務(wù)端的過程(當(dāng)前被阻塞在系統(tǒng)調(diào)用里)運(yùn)行起來,等等。所以在實(shí)際場景下,因?yàn)閮?nèi)核調(diào)度器的工作機(jī)制,loopback接口還是涉及到了類網(wǎng)絡(luò)延時(shí)。

基本上在網(wǎng)絡(luò)服務(wù)器中測量性能時(shí),一個(gè)忙碌的循環(huán)基準(zhǔn)測試是最愚蠢的事情。明智的做法是避免使用這種方法進(jìn)行基準(zhǔn)測試。

2017-09-03
原文鏈接:https://redis.io/topics/pipelining

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文將從Redis的基本特性入手,通過講述Redis的數(shù)據(jù)結(jié)構(gòu)和主要命令對Redis的基本能力進(jìn)行直觀介紹。之后概...
    kelgon閱讀 61,663評論 23 625
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,569評論 19 139
  • 重點(diǎn)參考鏈接: http://www.cnblogs.com/wangyuyu/p/3786236.html Re...
    Kevin_Junbaozi閱讀 2,264評論 0 21
  • 《散》 花季怕蹉跎,光陰悄悄過。相逢于初秋,陌路形葉落。 《換》 別時(shí)容易重逢難,花敗新生又一凡。殘葉無語露珠干,...
    林裊衣閱讀 255評論 0 1
  • 說《史記》【第一部】||夏商與西周 【上一章】 【第二部】目錄 周莊王幫助衛(wèi)國打齊國,魯國,宋國,陳國,蔡國這幾個(gè)...
    李知一二閱讀 981評論 13 6

友情鏈接更多精彩內(nèi)容