追求效能極致:Redis6.0的多執行緒模型

2022-10-10 18:02:48

Redis系列1:深刻理解高效能Redis的本質
Redis系列2:資料持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 叢集模式

背景

我們在第一篇《Redis系列1:深刻理解高效能Redis的本質》中就已經提到了,Redis 的網路 IO 以及鍵值對指令讀寫是由單個執行緒來執行的,避免了不必要的contextswitch和資源競爭,對於效能提升有很大的幫助。
而到了2020年的5月份,Redis官方 推出了 令人矚目的 Redis 6.0,提出很多新特性,包含 多執行緒網路IO 的概念,如下:

新特性 核心優化 應用優化 其他
ACL細粒度許可權管控(包括ACL LOG) 過期Key回收優化,增加設定引數 新版本Module API 全面支援SSL協定、並新增TSL協定
使用者端快取(Client side caching) Resp3協定,相容Resp2,更加簡單、高效 disque訊息佇列模組 Redis-benchmark支援叢集模式
多執行緒處理網路 IO(Threaded I/O) 優化了INFO命令,效率更高 新增設定,支援Del命令如unlink執行 Systemd支援重寫
Redis叢集代理(Cluster proxy) 優化阻塞命令,複雜度從O(n)到O(1) XINFO STREAM FULL流命令 新增設定引數來刪除用於在非永續性範例中進行復制的RDB檔案
支援linux/bsd系統的CPU和執行緒(包括子執行緒如aof、dbIO執行緒)親和力系結 RDB載入速度優化 CLIENT KILL USER username命令 無磁碟複製副本(Diskless replication on replicas),從測試版優化,目前無磁碟複製在load rdb仍是測試版。
叢集Slots命令優化
Psync2優化,修復了5.0的鏈式複製不一致問題。
defrag優化,從試驗版到正式版

這其中比較引人注意的就是 Threaded I/O Client side caching 這兩項了。
這時候我們不免疑問,為什麼6.0之前是單執行緒模式的,是基於什麼考慮。而現在為什麼又要優化成 多執行緒網路IO模式,主要解決了哪些問題 ,帶來了那些變化?
這一篇咱們就詳細就來聊下這個 Threaded I/O。

6.0之前的單執行緒模式

瞭解單執行緒模式之前,大家可以先回顧一下Redis系列第一篇 Redis系列1:深刻理解高效能Redis的本質
就會明白,Redis所謂的單執行緒並不是所有工作都是隻有一個執行緒在執行,而是指Redis的網路IO和鍵值對讀寫是由一個執行緒來完成的,Redis在處理使用者端的請求時包括獲取 (socket 讀)、解析、執行、內容返回 (socket 寫) 等都由一個順序序列的主執行緒處理。
這就是所謂的「單執行緒」。這也是Redis對外提供鍵值儲存服務的主要流程。
由於Redis在處理命令的時候是單執行緒作業的,所以會有一個Socket佇列,每一個到達的伺服器端命令來了之後都不會馬上被執行,而是進入佇列,然後被執行緒的事件分發器逐個執行。如下圖:

至於Redis的其他功能, 比如持久化、非同步刪除、叢集資料同步等等,其實是由額外的執行緒執行的。 可以這麼說,Redis工作執行緒是單執行緒的。但是在4.0之後,對於整個Redis服務來說,還是多執行緒運作的。

那麼問題來了,6.0之前為什麼要使用單執行緒,通過 Redis官方的檔案 ,我們看到他們有給出了說明:

  • 在使用 Redis 時,Redis 主要受限是在記憶體和網路上,CPU 幾乎沒有效能瓶頸的問題。
  • 以Linux 系統為例子,在Linux系統上Redis 通過 pipelining 可以處理 100w 個請求每秒,而應用程式的計算複雜度主要是 O(N) 或 O(log(N)) ,不會消耗太多 CPU。
  • 使用了單執行緒後,提高了可維護性。多執行緒模型在某些方面表現優異,卻增加了程式執行順序的不確定性,並且帶來了並行讀寫的一系列問題,增加了系統複雜度。同時因為執行緒切換、加解鎖,甚至死鎖,造成一定的效能損耗。
  • Redis 通過 AE 事件模型以及 IO 多路複用等技術,擁有超高的處理效能,因此沒有使用多執行緒的必要。

可以看出,Redis對CPU計算力的要求並不迫切,相反單執行緒機制讓 Redis 內部實現的複雜度大大降低,同時降低了因為上下文切換和資源競爭造成的效能損耗。那既然單執行緒這麼好用,為什麼要引入多執行緒模式。

6.0之後的多執行緒主要解決什麼問題

我們知道, 近年來底層網路硬體效能越來越好,Redis 的效能瓶頸逐漸體現在網路 I/O 的讀寫上,單個執行緒處理網路 I/O 讀寫的速度跟不上底層網路硬體執行的速度。
從下圖我們可以看到,Redis 在處理網路資料時,呼叫 epoll 的過程是阻塞的,這個過程會阻塞執行緒。如果並行量很高,達到萬級別的 QPS,就會形成瓶頸,影響整體吞吐能力。

既然讀寫網路的 read/write 系統呼叫佔用了Redis 執行期間大部分CPU 時間,那麼要想真正做到提速,必須改善網路IO效能。我們可以從這兩個方面來優化:

  • 提高網路 IO 效能,典型實現方式比如使用 DPDK 來替代核心網路棧的方式
  • 使用多執行緒,這樣可以充分利用多核CPU,同類實現案例比如 Memcached。

協定棧優化的這種方式跟 Redis 關係不大,所以最便捷高效的方式就是支援多執行緒。總結起來,redis支援多執行緒就是以下兩個原因:

  • 可以充分利用伺服器CPU的多核資源,而主執行緒明顯只能利用一個
  • 多執行緒任務可以分攤 Redis 同步 IO 讀寫負荷,降低耗時

6.0版本優化之後,主執行緒和多執行緒網路IO的執行流程如下:

具體步驟如下:

  • 主執行緒建立連線,並接受資料,並將獲取的 socket 資料放入等待佇列;
  • 通過輪詢的方式將 socket讀取出來並分配給 IO 執行緒;
  • 之後主執行緒保持阻塞,一直等到 IO 執行緒完成 socket 讀取和解析;
  • I/O 執行緒讀取和解析完成之後,返回給主執行緒 ,主執行緒開始執行 Redis 命令;
  • 執行完Redis命令後,主執行緒阻塞,直到IO 執行緒完成 結果回寫到socket 的工作;
  • 主執行緒清空已完成的佇列,等待使用者端新的請求。

本質上是將主執行緒 IO 讀寫的這個操作 獨立出來,單獨交給一個I/O執行緒組處理。
這樣多個 socket 讀寫可以並行執行,整體效率也就提高了。同時注意 Redis 命令還是主執行緒序列執行。

開啟多執行緒的方式

Redis6.0的多執行緒預設是禁用的,只使用主執行緒。如需開啟需要修改redis.conf組態檔:

# io-threads-do-reads no
io-threads-do-reads yes

開啟多執行緒後,還需要設定執行緒數,否則是不生效的。同樣修改redis.conf組態檔。
關於執行緒數的設定,官方有一個建議:4 核的機器建議設定為 2 或 3 個執行緒,8核的建議設定為 6 個執行緒,執行緒數一定要小於機器核數。
執行緒數並不是越大越好,官方認為超過了 8 個就很難繼續提效了,沒什麼意義。

# 假設你的CPU核數是8核,儘量設定成 5~6
io-threads 5

總結

  • 6.0之前,Redis所謂的單執行緒並不是所有工作都是隻有一個執行緒在執行,而是指Redis的網路IO和讀寫是由一個執行緒來完成的。其他諸如持久化、非同步刪除、叢集資料同步等,其實是由額外的執行緒執行的。
  • 網際網路飛速發展,開發人員面臨的線上流量場景越來越大,再使用單執行緒模式會導致在網路 I/O 浪費太多時間,極大的降低吞吐量,而普遍多核的cpu又沒有得到有效的利用。
  • 使用多執行緒,這樣可以充分利用多核CPU,提高網路的 read/write 效率。
  • 設定 Threaded I/O 多執行緒模式的時候,執行緒數一定要小於機器核數,否著意義不大。