從我們的直覺感受來講,對于任何服務,隻要在中間增加了一層,肯定會對服務性能造成影響。那麼到底會影響什麼呢?在考察一個服務性能的時候,有兩個最重要的名額,那就是吞吐和延遲。吞吐定義為服務端機關時間内能處理的請求數,延遲定義為用戶端從送出請求到收到請求的耗時。中間環節的引入我們首先想到的就是那會增加處理時間,這就會增加服務的延遲,于是順便我們也會認為吞吐也會下降。從單個使用者的角度來講,事實确實如此,我完成一個請求的時間增加了,那麼我機關時間内所能完成的請求量必定就減少了。然而站在服務端的角度來看,雖然單個請求的處理時間增加了,但是總的吞吐就一定會減少嗎?
接下來我們就來對redis來進行一系列的測試,利用redis自帶的redis-benchmark,分别對set和get指令;單個發送和批量發送;直連redis和連接配接redis代理predixy。這樣組合起來總共就是八種情況。redis-benchmark、redis是單線程的,predixy支援多線程,但是我們也隻運作一個線程,這三個程式都運作在一台機器上。
項目 | 内容 |
---|---|
CPU | AMD Ryzen 7 1700X Eight-Core Processor 3.775GHz |
記憶體 | 16GB DDR4 3000 |
OS | x86_64 GNU/Linux 4.10.0-42-generic #46~16.04.1-Ubuntu |
redis | 版本3.2.9,端口7200 |
predixy | 版本1.0.2,端口7600 |
八個測試指令
測試指令 | 指令行 |
---|---|
redis set | redis-benchmark -h xxx -p 7200 -t set -r 3000 -n 40000000 |
predixy set | redis-benchmark -h xxx -p 7600 -t set -r 3000 -n 40000000 |
redis get | redis-benchmark -h xxx -p 7200 -t get -r 3000 -n 40000000 |
predixy get | redis-benchmark -h xxx -p 7600 -t get -r 3000 -n 40000000 |
redis 批量set | redis-benchmark -h xxx -p 7200 -t set -r 3000 -n 180000000 -P 20 |
predixy 批量set | redis-benchmark -h xxx -p 7600 -t set -r 3000 -n 180000000 -P 20 |
redis 批量get | redis-benchmark -h xxx -p 7200 -t get -r 3000 -n 420000000 -P 20 |
predixy 批量get | redis-benchmark -h xxx -p 7600 -t get -r 3000 -n 220000000 -P 20 |
以上8條指令采取redis-benchmark預設的50個并發連接配接,資料大小為2位元組,指定3000個key,批量測試時一次發送20個請求。依次間隔2分鐘執行以上指令,每一個測試完成時間大約4分鐘。最後得到下圖的總體結果:

眼花缭亂是不是?左邊的縱軸表示CPU使用率,右邊的縱軸表示吞吐。其中redis used表示redis總的CPU使用率,redis user表示redis CPU使用者态使用率,redis sys表示redis CPU核心态使用率,其它類推。先别擔心分不清裡面的内容,下面我們會一一标出數值來。在這圖中總共可以看出有八個凸起,依次對應我們上面提到的八個測試指令。
1 redis set測試
2 predixy set測試
3 redis get測試
4 predixy get測試
5 redis 批量set測試
6 predixy 批量set測試
7 redis 批量get測試
8 predixy 批量get測試
圖檔還是不友善看,我們總結為表格:
測試\名額 | redis used | redis user | redis sys | predixy used | predixy user | predixy sys | redis qps | predixy qps |
---|---|---|---|---|---|---|---|---|
redis set | 0.990 | 0.247 | 0.744 | 167000 | 3 | |||
predixy set | 0.475 | 0.313 | 0.162 | 0.986 | 0.252 | 0.734 | 174000 | 174000 |
redis get | 0.922 | 0.180 | 0.742 | 163000 | 3 | |||
predixy get | 0.298 | 0.195 | 0.104 | 0.988 | 0.247 | 0.741 | 172000 | 172000 |
redis批量set | 1.006 | 0.796 | 0.21 | 782000 | 3 | |||
predixy批量set | 0.998 | 0.940 | 0.058 | 0.796 | 0.539 | 0.256 | 724000 | 724000 |
redis批量get | 1 | 0.688 | 0.312 | 1708000 | 3 | |||
predixy批量get | 0.596 | 0.582 | 0.014 | 0.999 | 0.637 | 0.362 | 935000 | 935000 |
看到前四個的結果如果感到驚訝不用懷疑是自己看錯了或者是測試結果有問題,這個結果是無誤的。根據這個結果,那麼可以回答我們最初提出的疑問,增加了代理之後并不一定會降低服務整體的吞吐!雖然benchmark并不是我們的實際應用,但是redis的大部分應用場景都是這樣的,并發的接閱聽人多用戶端的請求,處理然後傳回。
為什麼會是這樣的結果,看到這個結果後我們肯定想知道原因,這好像跟我們的想象不太一樣。要分析這個問題,我們還是從測試的資料來入手,首先看測試1的資料,redis的CPU使用率幾乎已經達到了1,對于單線程程式來說,這意味着CPU已經跑滿了,性能已經達到了極限,不可能再提高了,然而這時redis的吞吐卻隻有167000。測試2的redis吞吐都比它高,并且我們明顯能看出測試2裡redis的CPU使用率還不如測試1的高,測試2裡redis CPU使用率隻有0.475。為什麼CPU使用率降低了吞吐反而卻還高了呢?仔細對比一下兩個測試的資料,可以發現在測試1裡,redis的CPU大部分都花在了核心态,高達0.744,而使用者态隻有0.247,CPU運作在核心态時雖然我們不能稱之為瞎忙活,但是卻無助于提升程式的性能,隻有CPU運作在使用者态才可能提升我們的程式性能,相比測試1,測試2的redis使用者态CPU使用率提高到了0.313,而核心态CPU則大幅下降至0.162。這也就解釋了為什麼測試2的吞吐比測試1還要高。當然了,我們還是要繼續刨根問底,為什麼測試2裡經過一層代理predixy後,redis的CPU使用情況發生變化了呢?這是因為redis接受一個連接配接批量的發送指令過來處理,也就是redis裡所謂的pipeline。而predixy正是利用這一特性,predixy與redis之間隻有一個連接配接(大多數情況下),predixy在收到用戶端的請求後,會将它們批量的通過這個連接配接發送給redis處理,這樣一來就大大降低了redis用于網絡IO操作的開銷,而這一部分開銷基本都是花費在核心态。
對比測試1和測試2,引入predixy不僅直接提高了吞吐,還帶來一個好處,就是redis的CPU使用率隻有一半不到了,這也就意味着如果我再把剩下的這一半CPU用起來還可以得到更高的吞吐,而如果沒有predixy這樣一層的話,測試1結果告訴我們redis的CPU使用率已經到頭了,吞吐已經不可能再提高。
測試3和測試4說明的問題與測試1和測試2一樣,如果我隻做了這四個測試,那麼看起來好像代理的引入完全有助于提升我們的吞吐嘛。正如上面所分析的那樣,predixy提升吞吐的原因是因為采用了批量發送手段。那麼如果用戶端的使用場景就是批量發送指令,那結果會如何呢?
于是有了後面四個測試,後面四個測試給我們的直接感受就是太震撼了,吞吐直接提升幾倍甚至10倍!其實也正是因為redis批量模式下性能非常強悍,才使得predixy在單指令情況下改進吞吐成為可能。當然到了批量模式,從測試結果看,predixy使得服務的吞吐下降了。
具體到批量set時,直連redis和通過predixy,redis的CPU使用率都滿了,雖然采用predixy使得redis的使用者态CPU從0.796提高到了0.940,但是吞吐卻不升反降,從782000到724000,大約下降了7.4%。至于為什麼使用者态CPU使用率提高了吞吐卻下降了,要想知道原因就需要分析redis本身的實作,這裡我們就不做詳細探讨。可以做一個粗糙的解釋,在redis CPU跑滿的情況下,不同的負載情況會使得使用者态和核心态的使用率不同,而這其中有一種配置設定情況會是吞吐最大,而使用者态使用率高于或者低于這種情況時都會出現吞吐下降的情況。
再來看批量get,直連redis時吞吐高達1708000,而通過predixy的話就隻有935000了,下降了45%!就跟納了個人所得稅上限一般。看到這,剛剛對predixy建立的美好形象是不是又突然覺得要坍塌了?先别急,再看看其它名額,直連redis時,redis CPU跑滿;而通過predixy時redis CPU隻用了0.596,也就是說redis還有四成的CPU等待我們去壓榨。
寫到這,既然上面提到批量get時,通過predixy的話redis并未發揮出全部功力,于是就想着如果全部發揮出來會是什麼情況呢?我們繼續增加兩個測試,既然單個predixy在批量的情況下造成了吞吐下降,但是給我們帶來了一個好處是redis還可以提升的餘地,那麼我們就增加predixy的處理能力。是以我們把predixy改為三線程,再來跑一遍測試6和測試8。
兩個測試的整體結果如下。
三線程predixy批量set
三線程predixy批量get
測試\名額 | redis used | redis user | redis sys | predixy used | predixy user | predixy sys | redis qps | predixy qps |
---|---|---|---|---|---|---|---|---|
predixy pipeline set | 1.01 | 0.93 | 0.07 | 1.37 | 0.97 | 0.41 | 762000 | 762000 |
predixy pipeline get | 0.93 | 0.85 | 0.08 | 2.57 | 1.85 | 0.72 | 1718000 | 1718000 |
原本在單線程predixy的批量set測試中,predixy和redis的CPU都已經跑滿了,我們覺得吞吐已經達到了極限,但是實際結果顯示在三線程predixy的批量set測試中,吞吐還是提高了,從原來的724000到現在的76200,與直連的782000隻有2.5%的差距。多線程和單線程的主要差别在于單線程時predixy與redis隻有一個連接配接,而三線程時有三個連接配接。
而對于三線程predixy的批量get測試,不出我們所料的吞吐得到了極大的提升,從之前的935000直接飙到1718000,已經超過了直連的1708000。
最後,我們來總結一下,我們整個測試的場景比較簡單,隻是單純的set、get測試,并且資料大小為預設的2位元組,實際的redis應用場景遠比這複雜的多。但是測試結果的資料依舊可以給我們一些結論。代理的引入并不一定會降低服務的吞吐,實際上根據服務的負載情況,有時候引入代理反而可以提升整個服務的吞吐,如果我們不計較代理本身所消耗的資源,那麼引入代理幾乎總是一個好的選擇。根據我們上面的分析,一個最簡單實用的判斷原則,看看你的redis CPU使用情況,如果花費了太多時間在核心态,那麼考慮引入代理吧。