速度不夠,管道來湊——Redis管道技術

Redis客戶端與服務器之間使用TCP協議進行通訊,而且很早就支持管道(pipelining)技術了。在某些高併發的場景下,網絡開銷成了Redis速度的瓶頸,因此須要使用管道技術來實現突破。java

在介紹管道以前,先來想一下單條命令的執行步驟:redis

  • 客戶端把命令發送到服務器,而後阻塞客戶端,等待着從socket讀取服務器的返回結果
  • 服務器處理命令並將結果返回給客戶端

按照這樣的描述,每一個命令的執行時間 = 客戶端發送時間+服務器處理和返回時間+一個網絡來回的時間bash

其中一個網絡來回的時間是不固定的,它的決定因素有不少,好比客戶端到服務器要通過多少跳,網絡是否擁堵等等。可是這個時間的量級也是最大的,也就是說一個命令的完成時間的長度很大程度上取決於網絡開銷。若是咱們的服務器每秒能夠處理10萬條請求,而網絡開銷是250毫秒,那麼實際上每秒鐘只能處理4個請求。最暴力的優化方法就是使客戶端和服務器在一臺物理機上,這樣就能夠將網絡開銷下降到1ms如下。可是實際的生產環境咱們並不會這樣作。並且即便使用這種方法,當請求很是頻繁時,這個時間和服務器處理時間比較仍然是很長的。服務器

Redis Pipelining

爲了解決這種問題,Redis在很早就支持了管道技術。也就是說客戶端能夠一次發送多條命令,不用逐條等待命令的返回值,而是到最後一塊兒讀取返回結果,這樣只須要一次網絡開銷,速度就會獲得明顯的提高。管道技術其實已經很是成熟而且獲得普遍應用了,例如POP3協議因爲支持管道技術,從而顯著提升了從服務器下載郵件的速度。網絡

在Redis中,若是客戶端使用管道發送了多條命令,那麼服務器就會將多條命令放入一個隊列中,這一操做會消耗必定的內存,因此管道中命令的數量並非越大越好(太大容易撐爆內存),而是應該有一個合理的值。併發

深刻理解Redis交互流程

管道並不僅是用來網絡開銷延遲的一種方法,它其實是會提高Redis服務器每秒操做總數的。在解釋緣由以前,須要更深刻的瞭解Redis命令處理過程。socket

圖片來源:掘金小冊《Redis 深度歷險:核心原理與應用實踐》

一個完整的交互流程以下:高併發

  1. 客戶端進程調用write()把消息寫入到操做系統內核爲Socket分配的send buffer中
  2. 操做系統會把send buffer中的內容寫入網卡,網卡再經過網關路由把內容發送到服務器端的網卡
  3. 服務端網卡會把接收到的消息寫入操做系統爲Socket分配的recv buffer
  4. 服務器進程調用read()讀取消息而後進行處理
  5. 處理完成後調用write()把返回結果寫入到服務器端的send buffer
  6. 服務器操做系統再將send buffer中的內容寫入網卡,而後發送到客戶端
  7. 客戶端操做系統將網卡內容讀到recv buffer中
  8. 客戶端進程調用read()從recv buffer中讀取消息並返回

如今咱們把命令執行的時間進一步細分:優化

命令的執行時間 = 客戶端調用write並寫網卡時間+一次網絡開銷的時間+服務讀網卡並調用read時間++服務器處理數據時間+服務端調用write並寫網卡時間+客戶端讀網卡並調用read時間spa

這其中除了網絡開銷,花費時間最長的就是進行系統調用write()read()了,這一過程須要操做系統由用戶態切換到內核態,中間涉及到的上下文切換會浪費不少時間。

使用管道時,多個命令只會進行一次read()wrtie()系統調用,所以使用管道會提高Redis服務器處理命令的速度,隨着管道中命令的增多,服務器每秒處理請求的數量會線性增加,最後會趨近於不使用管道的10倍。

圖片來源:Redis官方pipeline文檔

和Scripting對比

對於管道的大部分應用場景而言,使用Redis腳本(Redis2.6及之後的版本)會使服務器端有更好的表現。使用腳本最大的好處就是能夠以最小的延遲讀寫數據。

有時咱們也須要在管道中使用EVAL和EVALSHA命令,這是徹底有可能的。所以Redis提供了SCRIPT LOAD命令來支持這種狀況。

眼見爲實

多說無益,仍是眼見爲實。下面就來對比一下使用管道和不使用管道的速度差別。

public class JedisDemo {

    private static int COMMAND_NUM = 1000;
    
    private static String REDIS_HOST = "Redis服務器IP";

    public static void main(String[] args) {

        Jedis jedis = new Jedis(REDIS_HOST, 6379);
        withoutPipeline(jedis);
        withPipeline(jedis);
    }

    private static void withoutPipeline(Jedis jedis) {
        Long start = System.currentTimeMillis();
        for (int i = 0; i < COMMAND_NUM; i++) {
            jedis.set("no_pipe_" + String.valueOf(i), String.valueOf(i), SetParams.setParams().ex(60));
        }
        long end = System.currentTimeMillis();
        long cost = end - start;
        System.out.println("withoutPipeline cost : " + cost + " ms");
    }

    private static void withPipeline(Jedis jedis) {
        Pipeline pipe = jedis.pipelined();
        long start_pipe = System.currentTimeMillis();
        for (int i = 0; i < COMMAND_NUM; i++) {
            pipe.set("pipe_" + String.valueOf(i), String.valueOf(i), SetParams.setParams().ex(60));
        }
        pipe.sync(); // 獲取全部的response
        long end_pipe = System.currentTimeMillis();
        long cost_pipe = end_pipe - start_pipe;
        System.out.println("withPipeline cost : " + cost_pipe + " ms");
    }
}
複製代碼

結果也符合咱們的預期:

withoutPipeline cost : 11791 ms
withPipeline cost : 55 ms
複製代碼

總結

  1. 使用管道技術能夠顯著提高Redis處理命令的速度,其原理就是將多條命令打包,只須要一次網絡開銷,在服務器端和客戶端各一次read()write()系統調用,以此來節約時間。
  2. 管道中的命令數量要適當,並非越多越好。
  3. Redis2.6版本之後,腳本在大部分場景中的表現要優於管道。

擴展

前面咱們提到,爲了解決網絡開銷帶來的延遲問題,能夠把客戶端和服務器放到一臺物理機上。可是有時用benchmark進行壓測的時候發現這仍然很慢。

這時客戶端和服務端實際是在一臺物理機上的,全部的操做都在內存中進行,沒有網絡延遲,按理來講這樣的操做應該是很是快的。爲何會出現上面的狀況的呢?

實際上,這是由內核調度致使的。好比說,benchmark運行時,讀取了服務器返回的結果,而後寫了一個新的命令。這個命令就在迴環接口的send buffer中了,若是要執行這個命令,內核須要喚醒Redis服務器進程。因此在某些狀況下,本地接口也會出現相似於網絡延遲的延遲。實際上是內核特別繁忙,一直沒有調度到Redis服務器進程。

參考

Redis官方文檔

Redis源碼

掘金小冊:《Redis 深度歷險:核心原理與應用實踐》

相關文章
相關標籤/搜索