使用pipeline加速Redis

面試官:怎麼快速刪除10萬個key?
某廠面試題:prod環境,如何快速刪除10萬個key?
帶着思考,我們一來研究Redis pipeline。

why pipeline ?

Redis客戶端與server的請求/響應模型

前面的文章 Redis底層協議RESP詳解 ,介紹到redis客戶端與redis-server交互通信,採用的TCP請求/響應模型;
我們通過Redis客戶端執行命令,如set key value,客戶端遵循RESP協議,將命令的協議串發送給redis-server執行,redis-server執行完成後再同步返回結果。
手寫Redis客戶端-實現自己的Jedis 對這一過程進行了重點分析,並遵循RESP實現了自己簡易版的Redis客戶端。

Redis客戶端與server通信,使用的是客戶端-服務器(CS)模式;每次交互,都是完整的請求/響應模式。
這意味着通常情況下一個請求會遵循以下步驟:

  • 客戶端連接服務端,基於特定的端口,發送一個命令,並監聽Socket返回,通常是以阻塞模式,等待服務端響應。
  • 服務端處理命令,並將結果返回給客戶端。

很顯然,我們使用jedis或lettuce執行Redis命令,每次都是建立socket連接,並等待返回。

每個命令底層建立TCP連接的時間是省不掉的,即使我們都是在內網使用Redis,內網快但請求/響應的往返時間是不會減少的。
當需要對一組kv進行批量操作時,這組命令的耗時=sum(N*(建立連接時間+發送命令、返回結果的往返時間RTT)),隨批量操作的key越多,時間累加呈線性增長。

順理成章的,就出現了像數據庫連接池等池化思想的衍生,redis連接也進行“池化”,如JedisPool。

JedisPool就足夠了?

池化connection後,每次執行命令都從池子裏“借”,用完之後再將connection“還”到池子。只是節省了創建TCP連接的時間;
當需要對一組kv進行批量操作時,JedisPool池子裏的connection連接、極端情況都被用完了,怎麼辦?
——需要等待JedisPool池裏有可複用的connection才能繼續執行;

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
…
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)

如果在指定的等待時間內沒有等到idle空閒連接,就報異常了。

儘管使用了池化、將connection進行復用,但不可避免的帶來其他問題:
https://jjlu521016.github.io/2018/12/09/JedisPool常見問題.html

除了池化的connection會被瞬間用完,Redis官網還給出了另外一個性能損耗的原因:

It’s not just a matter of RTT
https://redis.io/topics/pipelining

雖然池化的connection,節省了建立連接的時間,但多條命令(發送命令到sever、server返回結果)分別執行多次socket網絡IO,涉及到read()和write() syscall系統調用,這意味着從用戶態到內核態。上下文切換是巨大的速度損失。

如果能將多條命令“合併”到一起,進行一次網絡IO,性能會提高不少吧。
有沒有一種方式,佔用極少的connection連接,且不浪費請求/響應的往返時間,提高整體吞吐量呢?
這就是今天的主角——Redis pipeline

pipeline不僅是一種減少往返時間的延遲成本的方法,它實際上還可以極大地提高Redis服務器中每秒可執行的總操作量。

由於網絡開銷延遲,就算redis-server端有很強的處理能力,也會由於收到的client命令少,而造成吞吐量小。
當client 使用pipeline 發送命令時,redis-server必須將部分請求放到隊列中(使用內存),執行完畢後一次性發送結果。

對pipeline的支持
pipeline(管道)功能在命令行CLI客戶端redis-cli中沒有提供,也就是我們不能通過終端交互的方式使用pipeline;
redis的客戶端,如jedis,lettuce等都實現了對pipeline的支持。

pipeline爲我們節省了哪部分時間?

pipeline在某些場景下非常有用,比如有多個command需要被“及時的”提交,而且他們對相應結果沒有互相依賴,對結果響應也無需立即獲得,那麼pipeline就可以充當這種“批處理”的工具;而且在一定程度上,可以較大的提升性能:

  • 我們使用JedisPool連接池,節省了建立連接connection的時間;
  • pipeline節省了多條命令的(發送命令到server、server返回結果)往返時間RTT,包括多次網絡IO、系統調用的消耗。

pipeline是萬金油?

1、pipeline“獨佔”connection,直到pipeline結束
pipeline期間將“獨佔”connection,此期間將不能進行非“管道”類型的其他操作,直到pipeline關閉;如果你的pipeline的指令集很龐大,爲了不干擾鏈接中的其他操作,你可以爲pipeline操作新建Client連接,讓pipeline和其他正常操作分離在2個client連接中。

2、使用pipeline,如果發送的命令很多的話,建議對返回的結果加標籤,當然這也會增加使用的內存;

pipeline實現原理

管道(pipeline)可以一次性發送多條命令並在執行完後一次性將結果返回,pipeline通過減少客戶端與redis的通信次數來實現降低往返延時時間。
pipeline 底層實現是隊列,隊列的先進先出特性,保證了數據的順序性。 pipeline 的默認的同步的個數爲53個,也就是說arges中累加到53條數據時會把數據提交。

需要注意到是用 pipeline方式打包命令發送,redis必須在處理完所有命令前先緩存起所有命令的處理結果。打包的命令越多,緩存消耗內存也越多。所以並不是打包的命令越多越好。具體多少合適需要根據具體情況測試。

pipeline“打包命令”
客戶端將多個命令緩存起來,緩衝區滿了就發送(將多條命令打包發送);有點像“請求合併”。
服務端 接受一組命令集合,切分後逐個執行返回。

從Redis的RESP協議上看,pipeline並沒有什麼特殊的地方,只是把多個命令連續的發送給redis-server,然後一一解析返回結果。
手寫Redis客戶端-實現自己的Jedis 我們自己實現的Redis客戶端,遵循RESP協議拼裝了協議串,用socket將協議串發送給redis-server,以此實現和redis-server的通信。
pipeline並沒有什麼特殊的地方,只是一次性append追加了多條RESP指令,然後一次性發送出去而已。

1.pipeline減少了RTT,也減少了IO調用次數(IO調用涉及到用戶態到內核態之間的切換)
2.需要控制pipeline的大小,否則會消耗Redis的內存
Jedis客戶端緩存是8192,超過該大小則刷新緩存,或者直接發送。

當客戶端使用pipeline發送很多請求時,服務器將在內存中使用隊列存儲這些指令的響應。
所以批量發送的指令數量,最好在一個合理的範圍內,比如每次發1萬條指令,讀取完響應後再發送另外1萬條指令。2萬條指令,一次性發送和分2次發送,對客戶端來說速度是差不多的,但是對服務器來說,內存佔用差了1萬條響應的大小。

pipeline 的侷限性

pipeline 只能用於執行連續且無相關性的命令,當某個命令的生成需要依賴於前一個命令的返回時(或需要一起執行時),就無法使用 pipeline 了。通過 scripting 功能,可以規避這一侷限性。

有些系統可能對可靠性要求很高,每次操作都需要立馬知道這次操作是否成功,是否數據已經寫進redis了,如Redis實現分佈式鎖等,那這種場景就不適合了。

批量執行命令的其他方式

  • Redis事務
  • Scripting lua腳本

Redis支持使用multi命令,使用Redis事務。
但Redis事務屬於弱事務,並不像RDBMS一樣ACID的特性,詳見Redis事務,你真的瞭解嗎

pipeline與Redis事務(multi)

multi:標記一個事務塊的開始。 事務塊內的多條命令會按照先後順序被放進一個隊列當中,最後由 EXEC 命令原子性(atomic)地執行。
pipeline:客戶端將執行的命令寫入到緩衝中,最後由exec命令一次性發送給redis執行返回。

multi 是redis服務端一次性返回所有命令執行返回結果。
pipeline管道操作是需要客戶端與服務端的支持,客戶端將命令寫入緩衝,最後再通過exec命令發送給服務端,服務端通過命令拆分,逐個執行返回結果。

兩者的區別

  • pipeline選擇客戶端緩衝,multi選擇服務端隊列緩衝;
  • 請求次數的不一致,multi需要每個命令都發送一次給服務端,pipeline最後一次性發送給服務端,請求次數相對於multi減少
  • multi/exec可以保證原子性,而pipeline不保證原子性

pipeline和“事務”是兩個完全不同的概念,pipeline只是表達“交互”中操作的傳遞的方向性,pipeline也可以在事務中運行,也可以不在。
無論如何,pipeline中發送的每個command都會被server立即執行,如果執行失敗,將會在此後的相應中得到信息;也就是pipeline並不是表達“所有command都一起成功”的語義;
但是如果pipeline的操作被封裝在事務中,那麼將有事務來確保操作的成功與失敗。

Scripting lua腳本

Redis 從 2.6 開始內嵌了 Lua 環境來支持用戶擴展功能. 通過 Lua 腳本, 我們可以原子化地執行多條 Redis 命令.
在 Redis 中執行 Lua 腳本需要用到 eval 和 evalsha 和 script 這幾個命令。

Redis官方文檔:https://redis.io/topics/pipelining

本文首發於公衆號 架構道與術(ToBeArchitecturer),歡迎關注、學習更多幹貨~

發佈了208 篇原創文章 · 獲贊 609 · 訪問量 114萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章