Redis性能剖析

Redis爲什麼這麼快?

序言

作爲企業級的存儲組件, Redis被用到很多的業務場景。

Redis經常被用作做緩存, 一致性要求不高場景,還可以當做存儲使用。

另外, Redis還提供了消息訂閱、事務、索引等特性。
我們還可以利用集羣特性搭建分佈式存儲服務,實現非強一致性的分佈式鎖服務。

Redis用到上述場景, 都有一個共同的優勢, 就是處理速度快(高性能)。

面試中,面試官經常會問到單線程的Redis爲什麼這麼快?
爲了闡明這個問題, 下面將分三部分講解:
(1) 第一部分: Redis到底有多快
(2) 第二部分: 詳細講解Redis高性能原因
(3) 第三部分: 影響Redis性能的因素

頭像
👁 關注微信公衆號:非典型理科男 回覆:redis獲取redis三本經典著作

Redis到底有多快

要了解Reids的到底有多麼快, 首先需要有相應的評估工具。 其次,需要Redis
在一些平臺經驗數據,來評估Redis性能數量級。 幸運的是Redis提供了這樣的工具,並給出了常用的硬件平臺一些經驗數據。

下面篇幅比較長,核心觀點如下:

  1. 可以使用redis-benchmark對Redis的性能進行評估,命令行提供了普通/流水線方式、不同壓力評估特定命令的性能的功能。
  2. redis性能卓越,作爲key-value系統最大負載數量級爲10W/s, set和get耗時數量級爲10ms和5ms。使用流水線的方式可以提升redis操作的性能。

不關心具體數據的小夥伴,可以直接跳到第二部分,直接瞭解redis性能卓越的原因。

Redis性能評估工具

Redis包含的redis-benchmark實用程序可模擬N個客戶端同時發送M個總查詢的運行命令(類似於Apache的ab實用程序)。可以使用redis-benchmark對redis的性能進行評估。

支持以下選項:

Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]

 -h <hostname>      服務器 hostname (默認 127.0.0.1)
 -p <port>          服務器 port (默認 6379)
 -s <socket>        服務器 socket (覆蓋host和port)
 -a <password>      服務器鑑權密鑰
 -c <clients>       啓動的客戶端數量(並行度) (默認 50)
 -n <requests>      總請求量(默認 100000)
 -d <size>          GET和SET請求數據大小(默認 2個字節)
 --dbnum <db>       選擇的db編號 (默認 0)
 -k <boolean>       1=keep alive 0=reconnect (默認 1)
 -r <keyspacelen>   在SET/GET/INCR使用隨機的key值, 在SADD使用隨機的va
 -P <numreq>        一個Pipeline包含的請求數. 默認值1 (不使用Pipeline).
 -q                 安靜模式. 僅僅展示QPS值
 --csv              以csv格式輸出
 -l                 生成循環 永久執行測試
 -t <tests>         制定測試命令的命令, 命令列表以逗號分隔
 -I                 Idle模式,僅打開N個idle連接並等待

啓動基準之前,您需要具有運行中的Redis實例。我在自己工作的筆記本上, 使用默認參數跑了一個例子:

D:\data\soft\redis-windows>redis-benchmark.exe
.....
====== SET ======
  100000 requests completed in 0.81 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

99.90% <= 1 milliseconds
99.93% <= 2 milliseconds
99.95% <= 78 milliseconds
99.96% <= 79 milliseconds
100.00% <= 79 milliseconds
123609.39 requests per second

====== GET ======
  100000 requests completed in 0.70 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

100.00% <= 0 milliseconds
142045.45 requests per second

====== INCR ======
  100000 requests completed in 0.71 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

99.95% <= 1 milliseconds
99.95% <= 2 milliseconds
100.00% <= 2 milliseconds
140252.45 requests per second
.....

上面的例子中,截取了SET/GET/INCR的測試結果。

測試結果包括測試的環境參數(請求量、client數量、有效載荷)以及請求耗時的TP值。

redis-benchmark默認使用10萬請求量, 50個clinet,有效載荷爲3字節進行測試。

返回結果可以看出SET/GET/INCR命令在10萬的請求量下,總的請求耗時均低於0.1s以內。 以QPS=10W爲例, 計算出來的平均耗時爲2ms左右(1/(10W/50))。

Reids基準測試經驗數據

Redis的性能跟很多因素相關, 在第三部分會詳細介紹。比如客戶端網絡狀況、是否使用流水,鏈接的客戶端。爲了說明Redis到底有多快,我們使用Reidis官網使用redis-benchmark測試的一組數據。

警告:請注意,以下大多數基準測試已有數年曆史,並且是與今天的標準相比使用舊硬件獲得的。該頁面應該進行更新,但是在很多情況下,使用硬硬件狀態,您會期望看到的數字是此數字的兩倍。此外,在許多工作負載中,Redis 4.0比2.6快

硬件環境和軟件配置

測試是由50個同時執行200萬個請求的客戶端完成的。
所有測試在Redis 2.6.14上運行。
使用迴環地址(127.0.0.1)執行了測試。
使用一百萬個鍵的鍵空間執行測試。
使用和不使用流水線(16條命令流水線)執行測試。
Intel(R) Xeon(R) CPU E5520 @ 2.27GHz

Redis系統負載

  1. 不使用流水線測試結果
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q
SET: 122556.53 requests per second
GET: 123601.76 requests per second
LPUSH: 136752.14 requests per second
LPOP: 132424.03 requests per second
  1. 使用流水線測試結果
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q -P 16
SET: 195503.42 requests per second
GET: 250187.64 requests per second
LPUSH: 230547.55 requests per second
LPOP: 250815.16 requests per second

從以上可以看出Redis作爲key-value系統讀寫負載大致在10W+QPS, 使用流水線技術能夠顯著提升讀寫性能。

耗時情況

  1. 不使用流水線測試結果
$ redis-benchmark -n 100000

====== SET ======
  100007 requests completed in 0.88 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

58.50% <= 0 milliseconds
99.17% <= 1 milliseconds
99.58% <= 2 milliseconds
99.85% <= 3 milliseconds
99.90% <= 6 milliseconds
100.00% <= 9 milliseconds
114293.71 requests per second

====== GET ======
  100000 requests completed in 1.23 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

43.12% <= 0 milliseconds
96.82% <= 1 milliseconds
98.62% <= 2 milliseconds
100.00% <= 3 milliseconds
81234.77 requests per second
....

所有set操作均在10ms內完成, get操作均在5ms以下。

Redis爲什麼那麼快

Redis是一個單線程應用,所說的單線程指的是Redis使用單個線程處理客戶端的請求。
雖然Redis是單線程的應用,但是即便不通過部署多個Redis實例和集羣的方式提升系統吞吐, 從官網給出的數據可以看出,Redis處理速度非常快。

Redis性能非常高的原因主要有以下幾點:

  • 內存存儲:Redis是使用內存(in-memeroy)存儲,沒有磁盤IO上的開銷
  • 單線程實現:Redis使用單個線程處理請求,避免了多個線程之間線程切換和鎖資源爭用的開銷
  • 非阻塞IO:Redis使用多路複用IO技術,在poll,epool,kqueue選擇最優IO實現
  • 優化的數據結構:Redis有諸多可以直接應用的優化數據結構的實現,應用層可以直接使用原生的數據結構提升性能

下面詳細介紹非阻塞IO和優化的數據結構

多路複用IO

在《unix網絡編程 卷I》中詳細講解了unix服務器中的5種IO模型。

一個IO操作一般分爲兩個步驟:

  1. 等待數據從網絡到達, 數據到達後加載到內核空間緩衝區
  2. 數據從內核空間緩衝區複製到用戶空間緩衝區

按照兩個步驟是否阻塞線程,分爲阻塞/非阻塞, 同步/異步。
在這裏插入圖片描述

五種IO模型分類:

阻塞 非阻塞
同步 阻塞IO 非阻塞IO,IO多路複用,信號驅動IO
異步IO 異步IO

阻塞IO

在linux中,默認情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:

阻塞IO

非阻塞IO

Linux下,可以通過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:
非阻塞IO

IO多路複用

IO multiplexing這個詞可能有點陌生,但是如果我說select/epoll,大概就都能明白了。有些地方也稱這種IO方式爲事件驅動IO(event driven IO)。我們都知道,select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:
IO多路複用

信號驅動IO

信號驅動IO

異步IO

Linux下的asynchronous IO其實用得不多,從內核2.6版本纔開始引入。先看一下它的流程:
異步IO

介紹完unix或者類unix系統IO模型之後, 我們看下redis怎麼處理客戶端連接的?

Reids的IO處理

總的來說Redis使用一種封裝多種(select,epoll, kqueue等)實現的Reactor設計模式多路複用IO處理客戶端的請求。

Reactor設計模式

Reactor設計模式常常用來實現事件驅動。除此之外, Redis還封裝了不同平臺多路複用IO的不同的庫。處理過程如下:

IO庫封裝

因爲 Redis 需要在多個平臺上運行,同時爲了最大化執行的效率與性能,所以會根據編譯平臺的不同選擇不同的 I/O 多路複用函數作爲子模塊。

具體選擇過程如下:

Redis 會優先選擇時間複雜度爲 O(1) 的 I/O 多路複用函數作爲底層實現,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的這些函數都使用了內核內部的結構,並且能夠服務幾十萬的文件描述符。

但是如果當前編譯環境沒有上述函數,就會選擇 select 作爲備選方案,由於其在使用時會掃描全部監聽的描述符,所以其時間複雜度較差 O(n),並且只能同時服務 1024 個文件描述符,所以一般並不會以 select 作爲第一方案使用。

豐富高效的數據結構

Redis提供了豐富的數據結構,並且不同場景下提供不同實現。

Redis作爲key-value系統,不同類型的key對應不同的操作或者操作對應不同的實現,相同的key也會有不同的實現。Redis對key進行操作時,會進行類型檢查,調用不同的實現。

爲了解決以上問題, Redis 構建了自己的類型系統, 這個系統的主要功能包括:

redisObject 對象。
基於 redisObject 對象的類型檢查。
基於 redisObject 對象的顯式多態函數。
對 redisObject 進行分配、共享和銷燬的機制。

redisObject定義:

/*
 * Redis 對象
 */
typedef struct redisObject {

    // 類型
    unsigned type:4;

    // 對齊位
    unsigned notused:2;

    // 編碼方式
    unsigned encoding:4;

    // LRU 時間(相對於 server.lruclock)
    unsigned lru:22;

    // 引用計數
    int refcount;

    // 指向對象的值
    void *ptr;

} robj;

type 、 encoding 和 ptr 是最重要的三個屬性。

Redis支持4種type, 8種編碼, 分別爲:

在這裏插入圖片描述

有了redisObject之後, 對於特定key的操作過程就可以很容易的實現:
在這裏插入圖片描述

Redis除了提供豐富的高效的數據結構外, 還提供瞭如HyperLogLog, Geo索引這樣高效的算法。

篇幅的原因,影響Redis性能的因素將在另外一篇文章中介紹。

參考文檔:

  1. Redis和IO多路複用
  2. 5種網絡IO模型(有圖,很清楚)

頭像
👁 關注微信公衆號:非典型理科男 回覆:redis獲取redis三本經典著作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章