一、redis到底有多快?
官方文檔:https://redis.io/docs/management/optimization/benchmarks/
我們使用redis自帶的benchmark腳本測試:
D:\Redis-x64-3.2.100>redis-benchmark -t set, lpush -n 100000 -q
====== lpush -n 100000 -q ======
100000 requests completed in 0.89 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.90% <= 1 milliseconds
99.95% <= 5 milliseconds
99.96% <= 6 milliseconds
100.00% <= 6 milliseconds
111856.82 requests per second
我們發(fā)現(xiàn),每秒可以執(zhí)行11萬多次set、lpush命令。
D:\Redis-x64-3.2.100>redis-benchmark -n 100000 -q script load "redis.call('set', 'lua','666')"
script load redis.call('set', 'lua','666'): 105485.23 requests per second
執(zhí)行Lua腳本也能達到每秒10萬多次,按照這個測試結(jié)果,redis的10萬qps還是比較準確的,在高性能服務(wù)器上性能還能更強。
二、redis為什么這么快
總結(jié)起來主要是三點:
1、純內(nèi)存結(jié)構(gòu)
2、請求處理單線程
3、多路復(fù)用機制
1、內(nèi)存存儲
KV結(jié)構(gòu)的內(nèi)存數(shù)據(jù)庫,時間復(fù)雜度為O(1)。
(1)虛擬存儲器(虛擬內(nèi)存Virtual Memory)
計算機里面的內(nèi)存我們叫做主內(nèi)存,硬盤叫做輔存。
主存可看做一個很長的數(shù)組,一個字節(jié)一個單元,每個字節(jié)都有一個唯一的地址,這個地址叫做物理地址(Physical Address)。
早期的計算機中,如果CPU需要內(nèi)存,使用物理尋址,直接訪問主存儲器。
看起來是挺合乎情理的,但是這種方式有幾個弊端:
1、一般的操作系統(tǒng)都是多用戶多任務(wù)的,所有的進程共享主存。如果每個進程都獨立占一塊物理地址空間,主存很快就會被用完。我們希望在不同的時刻,不同的進程可以共用同一快物理地址空間。
2、如果所有進程都是直接訪問物理內(nèi)存,那么一個進程就可以修改其他進程的內(nèi)存數(shù)據(jù),導(dǎo)致物理地址空間被破壞,程序運行就會出現(xiàn)異常。
咋辦呢?對于物理內(nèi)存的使用,應(yīng)該有一個角色來協(xié)調(diào)和指揮。
在CPU和主存之間增加一個中間層。CPU不再使用物理地址訪問主存,而是訪問一個虛擬地址,由這個中間層把地址轉(zhuǎn)換成物理地址,最終獲得數(shù)據(jù)。這個中間層叫做MMU(Memory Management Unit),內(nèi)存管理單元。
具體的操作如下所示:
我們訪問MMU就跟訪問物理內(nèi)存一樣,所以把虛擬出來的地址叫做虛擬內(nèi)存(Virtual Memory)。
在每一個進程開始創(chuàng)建的時候,都會分配一段虛擬地址,然后通過虛擬地址和物理地址的映射來獲取真實數(shù)據(jù),這樣進程就不會直接接觸到物理地址,甚至不知道自己調(diào)用的哪塊物理地址的數(shù)據(jù)。
目前,大多數(shù)操作系統(tǒng)都使用了虛擬內(nèi)存,如windows系統(tǒng)的虛擬內(nèi)存、Linux系統(tǒng)的交換空間等等。Windows的虛擬內(nèi)存(pagefile.sys)是磁盤空間的一部分。
在32位的系統(tǒng)上,虛擬地址空間大小是2 ^ 32 = 4G。在64位系統(tǒng)上,最大虛擬地址空間理論上是2 ^ 64 = 1024 * 1024 TB,實際上沒有用到64位,因為用不到那么大的空間,而且會造成很大的系統(tǒng)開銷。Linux一般用低48位來表示虛擬地址空間,也就是2 ^ 48=256TB。
cat /proc/cpuinfo
address sizes : 42 bits physical, 48 bits virtual
實際的物理內(nèi)存可能遠遠小于虛擬內(nèi)存的大小。
總結(jié):引入虛擬內(nèi)存的作用:
- 1、通過把同一塊物理內(nèi)存映射到不同的虛擬地址空間實現(xiàn)內(nèi)存共享
- 2、對物理內(nèi)存進行隔離,不同的進程操作互不影響
- 3、虛擬內(nèi)存可以提供更大的地址空間,并且地址空間是連續(xù)的,使得程序編寫、鏈接更加簡單。
(2)用戶空間和內(nèi)核空間
Linux/GNU的虛擬內(nèi)存又進一步劃分成了兩塊:一部分是內(nèi)核空間(Kernel-space),一部分是用戶空間(User-space)。
在Linux系統(tǒng)中,虛擬地址布局如下:
這兩塊空間的區(qū)別是什么呢?
進程的用戶空間存放的是用戶程序的代碼和數(shù)據(jù),內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù)。不管是內(nèi)核空間還是用戶空間,它們都處于虛擬內(nèi)存空間中,都是對物理地址的映射。
當進程運行在內(nèi)核空間時就處于內(nèi)核態(tài),而進程運行在用戶空間時就處于用戶態(tài)。
進程在內(nèi)核空間可以訪問受保護的內(nèi)存空間,也可以訪問底層硬件設(shè)備。也就是可以執(zhí)行任意命令,調(diào)用系統(tǒng)的一切資源。在用戶空間只能執(zhí)行簡單的運算,不能直接調(diào)用系統(tǒng)資源,必須通過系統(tǒng)接口(又稱system call),才能向內(nèi)核發(fā)出指令。
所以,這樣劃分的目的是為了避免用戶進程直接操作內(nèi)核,保證內(nèi)核安全。
top命令:
us表示CPU消耗在User space的時間百分比;
sy表示CPU消耗在Kernel space的時間百分比。
2、單線程
按照正常思路來講,要實現(xiàn)這么高的并發(fā)性能,多線程理論上來說比單線程的性能要好很多,為什么Redis要用單線程?
這里說的單線程其實是指處理客戶端的請求是單線程的,可以把它叫做主線程。從4.0版本之后,還引入了一些線程處理其他的事情,比如清理臟數(shù)據(jù)、無用連接的釋放、大key的刪除等等。
把處理請求的主線程設(shè)置為單線程有什么好處呢?
- 沒有創(chuàng)建線程、銷毀線程帶來的消耗
- 避免了上下文切換導(dǎo)致的CPU消耗
- 避免了線程之間帶來的鎖競爭問題
Redis使用單線程確實有很多好處,但是不會白白浪費了多核CPU資源嗎?
官方是這樣解釋的:
在Redis中單線程已經(jīng)夠用了,CPU不是redis的瓶頸。Redis的瓶頸最有可能是機器內(nèi)存或者網(wǎng)絡(luò)帶寬。既然單線程容易實現(xiàn),又不需要處理線程并發(fā)的問題,那就順理成章地采用單線程的方案了。
注意,因為請求處理是單線程的,不要在生產(chǎn)環(huán)境運行長命令,比如keys、flushall、flushdb,否則會導(dǎo)致請求被阻塞。
(1)進程切換(上下文切換)
多任務(wù)操作系統(tǒng)是怎么實現(xiàn)運行遠大于CPU數(shù)量的任務(wù)個數(shù)的?當然,這些任務(wù)實際上并不是真的在同時運行,而是因為系統(tǒng)通過時間片分片算法,在很短的時間內(nèi),將CPU輪流分配給它們,造成多任務(wù)同時運行的錯覺。
在這個交替運行的過程里面,為了控制進程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運行的進程,以及恢復(fù)以前掛起的某個進程的執(zhí)行。這種行為被稱為進程切換。
什么叫上下文(Context)?
在每個任務(wù)運行前,CPU都需要知道任務(wù)從哪里加載、又從哪里開始運行。也就是說,需要系統(tǒng)事先幫它設(shè)置好CPU寄存器和程序計數(shù)器(Program Counter),這個叫做CPU的上下文。
而保存下來的上下文,會存儲在系統(tǒng)內(nèi)核
中,并在任務(wù)重新調(diào)度執(zhí)行時再次加載進來。這樣就能保證任務(wù)原來的狀態(tài)不受影響,讓任務(wù)看起來還是連續(xù)運行。
在切換上下文的時候,需要完成一系列的工作,這是一個很消耗資源的操作。
(2)進程的阻塞
正在運行的進程由于提出系統(tǒng)服務(wù)請求(如IO操作),但因為某種原因未得到操作系統(tǒng)的立即響應(yīng),該進程只能把自己變成阻塞狀態(tài),等待相應(yīng)的事件出現(xiàn)后才被喚醒。
進程在阻塞狀態(tài)不占用CPU資源。
(3)文件描述符 FD
Linux系統(tǒng)將所有設(shè)備都當做文件來處理,而Linux用文件描述符來標識每個文件對象。
文件描述符(File Descriptor)是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,用于指向被打開的文件,所有執(zhí)行IO操作的系統(tǒng)調(diào)用都通過文件描述符。
文件描述符是一個簡單的非負整數(shù),用以表明每個被進程打開的文件。Linux系統(tǒng)里面有三個標準文件描述符:0,標準輸入(鍵盤);1,標準輸出(顯示器);2,標準錯誤輸出(顯示器)。
3、同步非阻塞IO
Redis使用了同步非阻塞IO,多路復(fù)用機制處理并發(fā)連接。
(1)傳統(tǒng)IO數(shù)據(jù)拷貝
以讀操作為例:
當應(yīng)用程序執(zhí)行read系統(tǒng)調(diào)用讀取文件描述符(FD)的時候,如果這塊數(shù)據(jù)已經(jīng)存在于用戶進程的頁內(nèi)存中,就直接從內(nèi)存中讀取數(shù)據(jù)。如果數(shù)據(jù)不存在,則先將數(shù)據(jù)從磁盤加載數(shù)據(jù)到內(nèi)核緩沖區(qū)中,再從內(nèi)核緩沖區(qū)拷貝到用戶進程的頁內(nèi)存中。(兩次拷貝,兩次user和kernel的上下文切換)。
IO阻塞到底阻塞在哪里?一目了然。
(2)Blocking IO
當使用read或write對某個文件描述符進行過讀寫時,如果當前FD不可讀,系統(tǒng)就不會對其他的操作做出響應(yīng)。從硬件設(shè)備復(fù)制數(shù)據(jù)到內(nèi)核緩沖區(qū)是阻塞的,從內(nèi)核緩沖區(qū)拷貝到用戶空間,也是阻塞的,直到copy complete,內(nèi)核返回結(jié)果,用戶進程才解除block的狀態(tài)。
為了解決阻塞的問題,我們有幾個思路:
1、在服務(wù)端創(chuàng)建多個線程或者使用線程池——但是在高并發(fā)的情況下需要的線程會很多,系統(tǒng)無法承受,而且創(chuàng)建和釋放線程都需要消耗資源。
2、由請求方定期輪詢,在數(shù)據(jù)準備完畢后再從內(nèi)核緩存緩沖期復(fù)制數(shù)據(jù)到用戶空間(非阻塞式IO),這種方式會存在一定的延遲。
(3)IO多路復(fù)用(IO Multiplexing)
能不能用一個線程處理多個客戶端請求?答案是肯定的。
IO指的就是網(wǎng)絡(luò)IO,多路指的多個TCP連接(Socket或Channel),復(fù)用指的是復(fù)用一個或多個線程。
它的基本原理就是不再由應(yīng)用程序自己監(jiān)視連接,而是由內(nèi)核替應(yīng)用程序監(jiān)視文件描述符。
客戶端在操作的時候,會產(chǎn)生具有不同事件類型的socket。在服務(wù)端,IO多路復(fù)用程序(IO Multiplexing Module)會把消息放入隊列中,然后通過文件事件分派器(File event Dispatcher),轉(zhuǎn)發(fā)到不同的事件處理器中。
多路復(fù)用有很多的實現(xiàn),以select為例,當用戶進程調(diào)用了多路復(fù)用器,進程會被阻塞。內(nèi)核會監(jiān)視多路復(fù)用器負責(zé)的所有socket,當任何一個socket的數(shù)據(jù)準備好了,多路復(fù)用器就會返回。這時候用戶進程再調(diào)用read操作,把數(shù)據(jù)從內(nèi)核緩沖期拷貝到用戶空間。
所以,IO多路復(fù)用的特點是通過一種機制讓一個進程能同時等待多個文件描述符,而這些文件描述符其中的任意一個進入讀就緒(readable)狀態(tài),select()函數(shù)就可以返回。
多路復(fù)用需要操作系統(tǒng)的支持。Redis的多路復(fù)用,提供了select,epoll,evport,kqueue幾種選擇,在編譯的時候來選擇一種。源碼ae.c:
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
evport是Solaris系統(tǒng)內(nèi)核提供支持的;
epoll是Linux系統(tǒng)內(nèi)核提供支持的;
kqueue是Mac系統(tǒng)提供支持的;
select是POSIX提供支持的,一般的操作系統(tǒng)都有支撐(保底方案);
源碼:ae_evport.c、ae_epoll.c、ae_kqueue.c、ae_select.c
總結(jié)一下:
Redis抽象了一套AE事件模型,將IO事件和時間時間融入一起,同時借助多路復(fù)用機制的回調(diào)特性(Linux上用epoll),似的IO讀寫都是非阻塞的,實現(xiàn)高性能的網(wǎng)絡(luò)處理能力。
我們一直在說的Redis新版本多線程的 特性,意思并不是服務(wù)端接收客戶端請求變成多線程的了,它還是單線程的。
嚴格意義上來說,Redis從4.0之后就引入了多線程用來處理一些耗時長的工作和后臺工作,那不然的話,如果真的只有一個線程,那些耗時的操作肯定會導(dǎo)致客戶端請求被阻塞。我們這里說的多線程,確切的說,叫做多線程IO。
(4)多線程IO
回到多路復(fù)用的圖,服務(wù)端的數(shù)據(jù)返回給客戶端,需要從內(nèi)核空間copy數(shù)據(jù)到用戶空間,然后回寫到socket(write調(diào)用),這個過程使非常耗時的。所以多線程IO指的就是把結(jié)果寫到socket的這個環(huán)境是多線程的。處理請求依然是單線程的,所以不存在線程并發(fā)安全問題。
(5)select和epoll的區(qū)別
select:進程可以通過把一個或者多個 fd 傳遞給 select 系統(tǒng)調(diào)用,進程會阻塞在 select 操作上,這樣 select 可以幫我們檢測多個 fd 是否處于就緒狀態(tài)。
這個模式有二個缺點
1.由于他能夠同時監(jiān)聽多個文件描述符,假如說有 1000 個,這個時候如果其中一個 fd 處于就緒狀態(tài)了,那么當前進程需要線性輪詢所有的 fd,也就是監(jiān)聽的 fd 越多,性能開銷越大。
2.同時,select 在單個進程中能打開的 fd 是有限制的,默認是 1024,對于那些需要支持單機上萬的 TCP 連接來說確實有點少。
epoll:linux 還提供了 epoll 的系統(tǒng)調(diào)用,epoll 是基于事件驅(qū)動方式來代替順序掃描,因此性能相對來說更高,主要原理是,當被監(jiān)聽的 fd 中,有 fd 就緒時,會告知當前進程具體哪一個 fd 就緒,那么當前進程只需要去從指定的 fd 上讀取數(shù)據(jù)即可。
另外,epoll 所能支持的 fd 上線是操作系統(tǒng)的最大文件句柄,這個數(shù)字要遠遠大于 1024。
【由于 epoll 能夠通過事件告知應(yīng)用進程哪個 fd 是可讀的,所以我們也稱這種 IO 為異步非阻塞 IO,當然它是偽異步的,因為它還需要去把數(shù)據(jù)從內(nèi)核同步復(fù)制到用戶空間中,真正的異步非阻塞,應(yīng)該是數(shù)據(jù)已經(jīng)完全準備好了,我只需要從用戶空間讀就行】文章來源:http://www.zghlxwxcb.cn/news/detail-471903.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-471903.html
到了這里,關(guān)于Redis為什么會這么快?Redis到底有多快?的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!