說說 Redis pipeline

更多技術文章,請關注我的個人部落格 www.immaxfang.com 和小公眾號 Max的學習札記

Redis 客戶端和服務端之間是採用 TCP 協議進行通訊的,是基於 Request/Response 這種一問一答的模式,即請求一次響應一次。

普通模式

我們先來看下普通模式下,一條 Redis 命令的簡要執行過程:

  • 客戶端發送一條命令給 redis-server,阻塞等待 redis-server 應答
  • redis-server 接收到命令,執行命令
  • redis-server 將結果返回給客戶端

image.png
下面我們來簡要了解下一個完整請求的交互過程。
image.png

  1. 客戶端調用 write() 將消息寫入作業系統為 socket 分配的 send buffer 中
  2. 作業系統將 send buffer 中的內容發送到網卡,網卡通過網關路由把內容發送到伺服器網卡
  3. 伺服器網卡將接受到的消息寫入作業系統為 socket 分配的 recv buffer
  4. 伺服器進程調用 read() 從 recv buffer 中讀取消息進行處理
  5. 處理完成之後,伺服器調用 write() 將響應內容發送的 send buffer 中
  6. 伺服器將 send buffer 中的內容通過網卡,發送到客戶端
  7. 客戶端作業系統將網卡中的內容放入 recv buffer 中
  8. 客戶端進程調用 read() 從 recv buffer 中讀取消息

普通模式的問題

我們來想一下,這種情況下可能導致什麼問題。
如果同時執行大量的命令,那對於每一個命令,都要按上面的流程走一次,當前的命令需要等待上一條命令執行應答完畢之後,才會執行。這個過程中會有多次的 RTT ,也還會伴隨著很多的 IO 開銷,發送網路請求等。每條命令的發送和接收的過程都會佔用兩邊的網路傳輸。
簡單的來說,每個命令的執行時間 = 客戶端發送耗時 + 伺服器處理耗時 + 伺服器返回耗時 + 一個網路來回耗時。
在這裡,一個 網路來回耗時(RTT) 是不好控制的,也是不穩定的。它的影響因素很多,比如客戶端到伺服器的網路線路是否擁堵,經過了多少跳。還有就是 IO 系統調用也是耗時的,一個 read 系統調用,需要從用戶態,切換到內核態。上文我們講述一個命令的請求過程時多次降到 read 和 write 系統調用。
可以說一個命令的執行時間,很大程度上受到它們的限制。

pipeline 模式

有沒有什麼方法來解決這種問題呢。
第一種方法,就是利用多執行緒機制,並行執行命令。
第二種方法,調用批量命令,例如 mget等,一次操作多個鍵。
很多時候我們要執行的命令並不是一樣的命令,而是一組命令,這個時候就無法使用類似 mget這樣的批量命令了。那還有其他的方法嗎?
回想一下,我們初學編程的時候,老手都會告訴我們,不要在循環裡面做查詢。我有一個 books 列表數據,要根據 book_id 查詢它們的 price,如果我們循環 books 列表,在每次循環裡面取查詢單個 book_id 的 price,那性能肯定是不理想的。一般我們的優化方式是將多個 book_id 取出來,一次性去查多個 book_id 的 price,這樣性能就有明顯的提示。即將多次小命令中的耗時操作合併到一次,從而減少總的執行時間。
類似的,Redis pipeline 出現了,一般稱之為管道。它允許客戶端一次可以發送多條命令,而不用像普通模式那樣每次執行一個小命令都要等待前一個小命令執行完,伺服器在接收到一堆命令後,會依次執行,然後把結果打包,再一次性返回給客戶端。
這樣可以避免頻繁的命令發送,減少 RTT,減少 IO 調用次數。前面已經介紹了,IO 調用會涉及到用戶態和內核態之間的切換,在高性能的一些系統中,我們都是儘可能的減少 IO 調用。
簡要流程如下圖:
image.png

  • pipeline 的優點
    • 減少 RTT
    • 減少 IO 調用次數
  • 基本使用
Pipeline pipeline =jedis.pipelined();

for(int i = 0; i < 100; i++){
    pipeline.rpush("rediskey", i + "");
}

pipeline.sync()

總結一下 pipeline 的核心,就是客戶端將一組 Redis 命令進行組裝,通過一次 RTT 發送給伺服器,同時伺服器再將這組命令的執行結果按照順序一次返回給客戶端

pipeline 注意問題

雖然 pipeline 在某些情況下會帶來不小的性能提升,但是,我們在使用的時候也需要注意。

  • pipeline 中的命令數量不宜過多。

客戶端會先將多個命令寫入記憶體 buffer 中(打包),命令過多,如果是超過了客戶端設置的 buffer 上限,被客戶端的處理策略處理了(不同的客戶端實現可能會有差異,比如 jedis pipeline ,限制每次最大的發送位元組數為 8192,緩衝區滿了就發送,然後再寫緩衝,最後才處理 Redis 伺服器的應答)。如果客戶端沒有設置 buffer 上限或不支援上限設置,則會佔用更多的客戶端機器記憶體,造成客戶端癱瘓。官方推薦是每次 10k 個命令。
建議做好規範,遇到一次包含大量命令的 pipeline,可以拆分成多個稍小的 pipeline 來完成。

  • pipeline 一次只能運行在一個 Redis 節點上,一些集群或者 twemproxy 等中間件使用需要注意。

在集群環境下,一次 pipeline 批量執行多個命令,每個命令需要根據 key 計算槽位,然後根據槽位去特定的節點上去執行命令,這樣一次 pipeline 就會使用多個節點的 redis 連接,這種當前也是不支援的。

  • pipeline 不保證原子性,如要求原子性,不建議使用 pipeline

它僅是將多個命令打包發送出去而已,如果中間有命令執行異常,也會繼續執行剩餘命令。

pipeline 與批量操作 mget 等區別

其實 meget和 pipeline 優化的方向是一致的,即多個命令打包一次發送,減少網路時間。但是也是有區別的。

  • mget等的場景是一個命令對應多個鍵值對,而 pipeline 一般是多條命令(不同的命令)
  • mget操作是一個原子操作,而 pipeline 不是原子操作
  • mget是服務端實現,而 pipeline 是客戶端和服務端共同實現

pipeline 與事務的區別

這兩者關注和解決的問題不是一個東西,原理也不一樣。

  • pipeline 是一次請求,服務端順序執行,一次返回。而事務是多次請求(先 multi,再多個操作命令,最後 exec),服務端順序執行,一次返回
  • pipeline 關注的是 RTT 時間和 IO 調用,事務關注的是一致性問題

總結

本文主要講了多命令執行時耗時問題,以及 pipeline 的解決方法,和其簡單的原理,以及注意點。今天的學習就到這裡,改天我們接著肝。

Tags: