總結10點提升Redis效能的小技巧

2022-03-07 19:00:58
本篇文章給大家帶來了關於的相關知識,其中主要介紹了一些讓redis效能提升的小技巧,包括了pipeline、開啟IO多執行緒、避免big key等等,希望對大家有幫助。

推薦學習:

01 使用 pipeline

Redis 是基於請求-響應模型的 TCP 伺服器。意味著單次請求 RTT(往返時間),取決於當前網路狀況 。這會導致單個 Redis 請求可能非常快,比如通過本地環路網路卡。可能非常慢,比如處於網路狀況不佳的環境。

另一方面,Redis 每次請求-響應,都涉及到 read 和 write 系統呼叫。甚至會觸發多次 epoll_wait 系統呼叫(Linux 平臺)。這導致 Redis 不斷在使用者態和核心態進行切換。

static int connSocketRead(connection *conn, void *buf, size_t buf_len) {
    // read 系統呼叫
    int ret = read(conn->fd, buf, buf_len);}static int connSocketWrite(connection *conn, const void *data, size_t data_len) {
    // write 系統呼叫
    int ret = write(conn->fd, data, data_len);}int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    // 事件觸發,Linux 下為 epoll_wait 系統呼叫
    numevents = aeApiPoll(eventLoop, tvp);}

那麼,如何節省往返時間和系統呼叫次數呢?批次處理是一個好的辦法。

為此,Redis 提供了 「pipeline」。pipeline 的原理很簡單,將多個命令打包成「一個命令」傳送。Redis 收到後,解析成多個命令執行。最終將多個結果打包返回。

「pipeline 可以有效的提升 Redis 效能」

但是,使用 pipeline 有幾點需要你留意

  1. 「pipeline 不能保證原子性」。在一次 pipeline 命令執行期間,可能會執行其它 client 發起的命令。請記住,pipeline 只是批次處理命令。想要保證原子性,使用 MULTI 或者 Lua 指令碼。

  2. 「單次 pipeline 命令不宜過多」。當使用 pipeline 時,Redis 會將 pipeline 命令的響應結果,暫存在記憶體 Reply buffer 中,等待所有命令執行完畢後返回。如果 pipeline 命令過多,可能會導致佔用較多記憶體。可以將單個 pipeline 拆分成多個 pipeline。


02 開啟 IO 多執行緒

在「Redis 6」版本以前,Redis 是 「單執行緒」 讀取、解析、執行命令的。Redis 6 開始,引入了 IO 多執行緒。

IO 執行緒負責讀取命令、解析命令、返回結果。開啟後可以有效提升 IO 效能。

我畫了一張示意圖供你參考

在這裡插入圖片描述
如上圖所示,主執行緒和 IO 執行緒會共同參與命令的讀取、解析以及結果響應。

但執行命令的,為 「主執行緒」

IO 執行緒預設關閉,你可以修改 redis.conf 以下設定開啟。

io-threads 4
io-threads-do-reads yes

「io-threads」 是 IO 執行緒數(包含主執行緒),我建議你根據機器,設定不同值進行壓測,取最優值。


03 避免 big key

Redis 執行命令是單執行緒的,這意味著 Redis 操作「big key」有阻塞的風險。

big key 通常指的是 Redis 儲存的 value 過大。包括:

  • 單個 value 過大。如 200M 大小的 String。
  • 集合元素過多。如 List、Hash、Set、ZSet 中有幾百、上千萬資料。

舉個例子,假設我們有一個 200M 大小的 String key,名稱為「foo」。

執行如下命令

127.0.0.1:6379> GET foo

當返回結果時,Redis 會分配 200m 的記憶體,並執行 memcpy 拷貝。

void _addReplyProtoToList(client *c, const char *s, size_t len) {
    ...
    if (len) {
        /* Create a new node, make sure it is allocated to at
         * least PROTO_REPLY_CHUNK_BYTES */
        size_t size = len < PROTO_REPLY_CHUNK_BYTES? PROTO_REPLY_CHUNK_BYTES: len;
        // 分配記憶體(例子中為 200m)
        tail = zmalloc(size + sizeof(clientReplyBlock));
        /* take over the allocation's internal fragmentation */
        tail->size = zmalloc_usable_size(tail) - sizeof(clientReplyBlock);
        tail->used = len;
        // 記憶體拷貝
        memcpy(tail->buf, s, len);
        listAddNodeTail(c->reply, tail);
        c->reply_bytes += tail->size;

        closeClientOnOutputBufferLimitReached(c, 1);
    }}

而 Redis 輸出 buf 為 16k

// server.h#define PROTO_REPLY_CHUNK_BYTES (16*1024) /* 16k output buffer */typedef struct client {
    ...
    char buf[PROTO_REPLY_CHUNK_BYTES];} client;

這意味著 Redis 無法單次返回響應資料,需要註冊「可寫事件」,從而觸發多次 write 系統呼叫。

這裡有兩個耗時點:

  • 分配大記憶體(也可能釋放記憶體,如 DEL 命令)
  • 觸發多次可寫事件(頻繁執行系統呼叫,如 write、epoll_wait)

那麼,如何找出 big key 呢?

如果 slow log 出現了簡單命令,如 GET、SET、DEL,大概率是出現了 big key。

127.0.0.1:6379> SLOWLOG GET
    3) (integer) 201323  // 單位微妙
    4) 1) "GET"
       2) "foo"

其次,可以通過 Redis 分析工具來查詢 big key。

$ redis-cli --bigkeys -i 0.1
...
[00.00%] Biggest string found so far '"foo"' with 209715200 bytes

-------- summary -------

Sampled 1 keys in the keyspace!
Total key length in bytes is 3 (avg len 3.00)

Biggest string found '"foo"' has 209715200 bytes

1 strings with 209715200 bytes (100.00% of keys, avg size 209715200.00)
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)

對於 big key,有以下幾點建議:

1.業務中儘量避免 big key 出現。當出現 big key 時,你要判斷這樣設計是否合理,又或者是出現了 bug。

2.將 big key 拆分為多個小 key。

3.使用替代命令。

  • 如果 Redis 版本大於 4.0,可使用 UNLINK 命令替代 DEL。Redis 版本大於 6.0,可開啟 lazy-free 機制。將釋放記憶體操作,放到後臺執行緒執行。

  • LRANGE、HGETALL 等替換為 LSCAN、HSCAN 分次獲取。

但我還是建議在業務中避免 big key。


04 避免執行時間複雜度高的命令

我們知道 Redis 是「單執行緒」執行命令的。執行時間複雜度高的命令,很可能會阻塞其它請求。

複雜度高的命令和元素數量有關。通常有以下兩種場景。

  1. 元素太多,消耗 IO 資源。如 HGETALL、LRANGE,時間複雜度為 O(N)。

  2. 計算過於複雜,消費 CPU 資源。如 ZUNIONSTORE,時間複雜度為 O(N)+O(M log(M))

Redis 官方手冊,標記了命令執行的時間複雜度。建議你在使用不熟悉的命令前,先檢視手冊,留意時間複雜度。

實際業務中,你應該儘量避免時間複雜度高的命令。如果必須要用,有兩點建議

  1. 保證操作的元素數量,儘可能少。

  2. 讀寫分離。複雜命令通常是讀請求,可以放到「slave」結點執行。


05 使用惰性刪除 Lazy free

key 過期或是使用 DEL 刪除命令時,Redis 除了從全域性 hash 表移除物件外,還會將物件分配的記憶體釋放。當遇到 big key 時,釋放記憶體會造成主執行緒阻塞。

為此,Redis 4.0 引入了 UNLINK 命令,將釋放物件記憶體操作放入 bio 後臺執行緒執行。從而有效減少主執行緒阻塞。

Redis 6.0 更進一步,引入了 Lazy-free 相關設定。當開啟設定後,key 過期和 DEL 命令內部,會將「釋放物件」操作「非同步執行」。

void delCommand(client *c) {
    delGenericCommand(c,server.lazyfree_lazy_user_del);}void delGenericCommand(client *c, int lazy) {
    int numdel = 0, j;

    for (j = 1; j < c->argc; j++) {
        expireIfNeeded(c->db,c->argv[j]);
        // 開啟 lazy free 則使用非同步刪除
        int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
                              dbSyncDelete(c->db,c->argv[j]);
        ...
    }}

建議至少升級到 Redis 6,並開啟 Lazy-free。


06 讀寫分離

Redis 通過副本,實現「主-從」執行模式,是故障切換的基石,用來提高系統執行可靠性。也支援讀寫分離,提高讀效能。

你可以部署一個主結點,多個從結點。將讀命令分散到從結點中,從而減輕主結點壓力,提升效能。

在這裡插入圖片描述


07 繫結 CPU

Redis 6.0 開始支援繫結 CPU,可以有效減少執行緒上下文切換。

CPU 親和性(CPU Affinity)是一種排程屬性,它將一個程序或執行緒,「繫結」到一個或一組 CPU 上。也稱為 CPU 繫結。

設定 CPU 親和性可以一定程度避免 CPU 上下文切換,提高 CPU L1、L2 Cache 命中率。

早期「SMP」架構下,每個 CPU 通過 BUS 匯流排共用資源。CPU 繫結意義不大。

SMP 架構(簡版)
而在當前主流的「NUMA」架構下,每個 CPU 有自己的本地記憶體。存取本地記憶體有更快的速度。而存取其他 CPU 記憶體會導致較大的延遲。這時,CPU 繫結對系統執行速度的提升有較大的意義。

NUMA 架構(簡版)
現實中的 NUMA 架構比上圖更復雜,通常會將 CPU 分組,若干個 CPU 分配一組記憶體,稱為 「node」

你可以通過 「numactl -H 」 命令來檢視 NUMA 硬體資訊。

$ numactl -H
available: 2 nodes (0-1)node 0 cpus: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38
node 0 size: 32143 MB
node 0 free: 26681 MB
node 1 cpus: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39
node 1 size: 32309 MB
node 1 free: 24958 MB
node distances:
node 0 1
  0: 10 21
  1: 21 10

上圖中可以得知該機器有 40 個 CPU,分組為 2 個 node。

node distances 是一個二維矩陣,表示 node 之間 「存取距離」,10 為基準值。上述命令中可以得知,node 自身存取,距離是 10。跨 node 存取,如 node 0 存取 node 1 距離為 21。說明該機器「跨 node 存取速度」比「node 自身存取速度」慢 2.1 倍。

其實,早在 2015 年,有人提出 Redis 需要支援設定 CPU 親和性,而當時的 Redis 還沒有支援 IO 多執行緒,該提議擱置。

而 Redis 6.0 引入 IO 多執行緒。同時,也支援了設定 CPU 親和性

我畫了一張 Redis 6.0 執行緒家族供你參考。

在這裡插入圖片描述

上圖可分為 3 個模組

  • 主執行緒和 IO 執行緒:負責命令讀取、解析、結果返回。命令執行由主執行緒完成。
  • bio 執行緒:負責執行耗時的非同步任務,如 close fd。
  • 後臺程序:fork 子程序來執行耗時的命令。

Redis 支援分別設定上述模組的 CPU 親和度。你可以在 redis.conf 找到以下設定(該設定需手動開啟)。

# IO 執行緒(包含主執行緒)繫結到 CPU 0、2、4、6
server_cpulist 0-7:2
# bio 執行緒繫結到 CPU 1、3
bio_cpulist 1,3
# aof rewrite 後臺程序繫結到 CPU 8、9、10、11
aof_rewrite_cpulist 8-11
# bgsave 後臺程序繫結到 CPU 1、10、11
bgsave_cpulist 1,10-11

我在上述機器,針對 IO 執行緒和主執行緒,進行如下測試:

首先,開啟 IO 執行緒設定

io-threads 4 # 主執行緒 + 3 個 IO 執行緒io-threads-do-reads yes # IO 執行緒開啟讀和解析命令功能

測試如下三種場景:

  1. 不開啟 CPU 繫結設定。

  2. 繫結到不同 node。
    「server_cpulist 0,1,2,3」

  3. 繫結到相同 node。
    「server_cpulist 0,2,4,6」

通過 redis-benchmark 對 get 命令進行基準測試,每種場景執行 3 次。

$ redis-benchmark -n 5000000 -c 50 -t get --threads 4

結果如下:

1.不開啟 CPU 繫結設定

throughput summary: 248818.11 requests per second
throughput summary: 248694.36 requests per second
throughput summary: 249004.00 requests per second

2.繫結不同 node

throughput summary: 248880.03 requests per second
throughput summary: 248447.20 requests per second
throughput summary: 248818.11 requests per second

3.繫結相同 node

throughput summary: 284414.09 requests per second
throughput summary: 284333.25 requests per second
throughput summary: 265252.00 requests per second

根據測試結果,繫結到同一個 node,qps 大約提升 15%

使用繫結 CPU,你需要注意以下幾點:

  1. Linux 下,你可以使用 「numactl --hardware」 檢視硬體佈局,確保支援並開啟 NUMA。

  2. 執行緒要儘可能分佈在 「不同的 CPU,相同的 node」,設定 CPU 親和度才有效。否則會造成頻繁上下文切換和遠距離記憶體存取。

  3. 你要熟悉 CPU 架構,做好充分的測試。否則可能適得其反,導致 Redis 效能下降。


08 合理設定持久化策略

Redis 支援兩種持久化策略,RDB 和 AOF。

RDB 通過 fork 子程序,生成資料快照,二進位制格式。

AOF 是增量紀錄檔,文字格式,通常較大。會通過 AOF rewrite 重寫紀錄檔,節省空間。

除了手動執行「BGREWRITEAOF」命令外,以下 4 點也會觸發 AOF 重寫

  1. 執行「config set appendonly yes」命令

  2. AOF 檔案大小比例超出閾值,「auto-aof-rewrite-percentage」

  3. AOF 檔案大小絕對值超出閾值,「auto-aof-rewrite-min-size」

  4. 主從複製完成 RDB 載入

RDB 和 AOF,都是在主執行緒中觸發執行。雖然具體執行,會通過 fork 交給後臺子程序。但 fork 操作,會拷貝程序資料結構、頁表等,當範例記憶體較大時,會影響效能。

AOF 支援以下三種策略。

  1. appendfsync no:由作業系統決定執行 fsync 時機。 對 Linux 來說,通常每 30 秒執行一次 fsync,將緩衝區中的資料刷到磁碟上。如果 Redis qps 過高或寫 big key,可能導致 buffer 寫滿,從而頻繁觸發 fsync。

  2. appendfsync everysec: 每秒執行一次 fsync。

  3. appendfsync always: 每次「寫」會呼叫一次 fsync,效能影響較大。

AOF 和 RDB 都會對磁碟 IO 造成較高的壓力。其中,AOF rewrite 會將 Redis hash 表所有資料進行遍歷並寫磁碟。對效能會產生一定的影響。

線上業務 Redis 通常是高可用的。如果對快取資料丟失不敏感。考慮關閉 RDB 和 AOF 以提升效能。

如果無法關閉,有以下幾點建議:

  1. RDB 選擇業務低峰期做,通常為凌晨。保持單個範例記憶體不超過 32 G。太大的記憶體會導致 fork 耗時增加。

  2. AOF 選擇 appendfsync no 或者 appendfsync everysec

  3. AOF auto-aof-rewrite-min-size 設定大一些,如 2G。避免頻繁觸發 rewrite。

  4. AOF 可以僅在從節點開啟,減輕主節點壓力。

根據本地測試,不開啟 AOF,寫效能大約能提升 20% 左右。


09 使用長連線

Redis 是基於 TCP 協定,請求-響應式伺服器。使用短連線會導致頻繁的建立連線。

短連線有以下幾個慢速操作:

  1. 建立連線時,TCP 會執行三次握手、慢啟動等策略。

  2. Redis 會觸發新建/斷開連線事件,執行分配/銷燬使用者端等耗時操作。

  3. 如果你使用的是 Redis Cluster,新建連線時,使用者端會拉取 slots 資訊初始化。建立連線速度更慢。

所以,相對於效能快速的 Redis,建立連線是十分慢速的操作。

「建議使用連線池,併合理設定連線池大小」

但使用長連線時,需要留意一點,要有「自動重連」策略。避免因網路異常,導致連線失效,影響正常業務。


10 關閉 SWAP

SWAP 是記憶體交換技術。將記憶體按頁,複製到預先設定的磁碟空間上。

記憶體是快速的,昂貴的。而磁碟是低速的,廉價的。

通常使用 SWAP 越多,系統效能越低。

Redis 是記憶體資料庫,使用 SWAP 會導致效能快速下降

建議留有足夠記憶體,並關閉 SWAP


總結

以上就是今天為大家分享的 「提升 Redis 效能的 10 個手段」

我繪製了思維導圖,方便大家記憶。

在這裡插入圖片描述
可以看到,效能優化並不容易,需要我們瞭解很多底層知識,並做出充分測試。在不同機器、不同系統、不同設定下,Redis 都會有不同的效能表現。

推薦學習:

以上就是總結10點提升Redis效能的小技巧的詳細內容,更多請關注TW511.COM其它相關文章!