java redis 流水线,Redis系列(1) —— 流水线
寫在前面
去年下半年,出于學習Redis的目的,在看完《Redis in Action》一書后,開始嘗試翻譯Redis官方文檔。盡管Redis中文官方網站有了譯本,但是看別人翻譯好的和自己翻譯英文原文畢竟還是有很大的不同。這一系列文章之前發布在GitBook上,為了方便管理,跟其他文章一起放在同一個平臺,遂全部遷移至簡書。由于本人學習Redis時間不長,認識有限,同時也缺少實戰經驗,翻譯中有任何不恰當之處,歡迎各位及時斧正,本人將不勝感激。對英文官方文檔感興趣的朋友也可以直接訪問https://redis.io/ 進行獲取。
使用流水線來提升redis的查詢速度
請求/響應協議和RTT
Redis是一個使用客戶端-服務端模型和請求/響應協議的TCP服務。這意味著完成一次請求通常需要經過以下步驟:
客戶端向服務端發起一次查詢請求并讀取socket,這通常是以阻塞方式來等待服務端響應。
服務端處理命令并將響應發回給客戶端。
例如下面是一個4條命令序列的執行情況:
客戶端:INCR x
服務端:1
客戶端:INCR x
服務端:2
客戶端:INCR x
服務端:3
客戶端:INCR x
服務端:4
客戶端和服務端通過網絡來連接。這樣的連接可以很快(loopback接口)也可以很慢(兩臺主機之間建立的是一個經過了多次跳轉的網絡連接)。不管網絡延遲如何,數據包從客戶端發往服務端,然后攜帶響應從服務端發往客戶端總是會消耗時間的。
這個時間被稱為RTT(Round Trip Time)。當客戶端需要一次性處理很多請求時很容易看到這是如何影響到性能的(比如說向一個列表中添加很多元素,或者用很多鍵值填充數據庫)。例如假設RTT時間為250毫秒(在網絡連接很慢的網絡條件下),那么即使服務端每秒可以處理100000個請求,我們每秒最多也只能處理4個請求。
如果使用loopback接口,RTT時間就會短很多(例如在我的機器上ping127.0.0.1只需要0.044毫秒),但是如果我們需要批量處理很多寫請求,這個時間仍然是很大的一筆開銷。
好在我們還有一種方式可以來改善這種狀況。
Redis流水線
即使客戶端舊的請求還沒有得到響應,一個請求/響應服務器也可以處理新的請求。這樣一來我們就可以一次向服務端發送多條命令而根本不用等待響應,最后在一個步驟中讀取所有回復。
這就是流水線,這是一種幾十年來被廣泛采用的技術。例如很多POP3協議的實現已經支持了這種特性,它極大地加快了從服務器下載新郵件的過程。
Redis很早就支持了流水線功能,所以無論你正在使用的是哪個版本,你都可以使用Redis的流水線技術。下面是一個使用這種原生能力的例子:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
這一次我們沒有為每次調用都消耗RTT,而是4個命令只消耗一次時間。
更明確地說,通過使用流水線技術,我們第一個例子的操作順序將會是下面這個樣子:
客戶端: INCR x
客戶端: INCR x
客戶端: INCR x
客戶端: INCR x
服務端:1
服務端:2
服務端:3
服務端:4
重要提示:當客戶端使用流水線技術來發送命令時,服務端將不得不使用內存來排隊答復。所以如果你需要使用流水線來發送很多命令,最好是將他們按照合理的數量來分批處理,比如先發送10000條命令,讀取響應,再發送另外10000條命令等等。速度幾乎是一樣的,但是將需要大量額外的內存來存儲這10000條命令的答復。
這不僅僅關乎RTT
流水線不僅僅是一種用來減少RTT延遲成本的方式,實際上對于一臺給定的Redis服務器,它極大地提高了每秒鐘你所能處理的操作數量。一個事實是,當不采用流水線技術時,從訪問數據結構并且產生響應的角度來看,每一條命令的時間消耗都是很少的,但是從處理socket IO的角度來看,這個時間消耗確是很大的。它涉及到調用read()和write()這些系統調用,這意味著要從用戶側到內核側。而上下文切換是一個巨大的時間開銷,會嚴重影響響應速度。
當使用流水線時,一個簡單的read()系統調用就可以讀取很多命令,同樣的,一個簡單的write()系統調用就可以將很多回復傳送出去。正因為如此,每秒鐘可以處理的查詢命令的數量幾乎隨著管道長度的增加而呈線性增長,最終可以達到不使用流水線這種基本情況時的10倍,正如你從下圖看到的那樣:
image.png
一些真實世界的代碼樣例
在下面這個基準測試中,我們將會使用基于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系統上執行上面這個簡單的腳本將會得到如下的數據,開啟流水線功能后,RTT已經被改善得相當低。
without pipelining 1.185238 seconds
with pipelining 0.250783 seconds
如你所見,開啟流水線后,我們把傳輸速度提升了5倍。
流水線 VS 腳本
使用Redis腳本(2.6及以上版本的redis可用),很多使用流水線的場景可以獲得更高效的處理,因為使用腳本可以在服務端執行大量工作。腳本的一大優勢是它可以使讀寫數據只需要很小的時延,使得讀、計算和寫操作變得很快(流水線在這種場景下做不到這一點,因為客戶端在調用寫命令之前需要讀命令的返回結果)
有時候應用也會需要向流水線發送EVAL或者EVALSHA命令。這完全是有可能的并且Redis已經通過SCRIPT LOAD命令明確支持了這一點(它保證EVALSHA命令會調用成功)。
附錄:為什么即使在loopback接口上一個忙碌的循環也很慢
即使在這個頁面的背景之下,你還是會想知道為什么即使在loopback接口上執行并且服務端和客戶端運行在同一臺物理機上時,一個一個像下面的Redis基準測試(偽代碼)還是會很慢:
FOR-ONE-SECOND:
Redis.SET("foo","bar")
END
畢竟如果Redis過程和基準測試運行在一起時,難道它不是僅僅將信息在內存上從一個地方復制到另一個地方,中間沒有任何真正的延時和網絡參與進來嗎?
原因在于系統上的過程不是一直在運行,實際上是內核調度器才讓過程運行起來,所以基準測試開始運行時,從Redis服務端讀取返回數據(跟最后一條執行的命令相關),并且寫了一條新命令。現在命令存在于loopback接口的緩存里,但是為了能夠被服務端讀取到,內核會通過調度讓服務端的過程(當前被阻塞在系統調用里)運行起來,等等。所以在實際場景下,因為內核調度器的工作機制,loopback接口還是涉及到了類網絡延時。
基本上在網絡服務器中測量性能時,一個忙碌的循環基準測試是最愚蠢的事情。明智的做法是避免使用這種方法進行基準測試。
總結
以上是生活随笔為你收集整理的java redis 流水线,Redis系列(1) —— 流水线的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有人强烈建议现在不要买房投资,为什么?买
- 下一篇: mysql etl工具有哪些_常见ETL