追求性能極致:Redis6.0的多線程模型


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 的概念,如下:
image

新特性 內核優化 應用優化 其他
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隊列,每一個到達的服務端命令來了之后都不會馬上被執行,而是進入隊列,然后被線程的事件分發器逐個執行。如下圖:

image

至於Redis的其他功能, 比如持久化、異步刪除、集群數據同步等等,其實是由額外的線程執行的。 可以這么說,Redis工作線程是單線程的。但是在4.0之后,對於整個Redis服務來說,還是多線程運作的。

那么問題來了,6.0之前為什么要使用單線程,通過 Redis官方的文檔 ,我們看到他們有給出了說明:
image

  • 在使用 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,就會形成瓶頸,影響整體吞吐能力。
image

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

  • 提高網絡 IO 性能,典型實現方式比如使用 DPDK 來替代內核網絡棧的方式
  • 使用多線程,這樣可以充分利用多核CPU,同類實現案例比如 Memcached。

協議棧優化的這種方式跟 Redis 關系不大,所以最便捷高效的方式就是支持多線程。總結起來,redis支持多線程就是以下兩個原因:

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

6.0版本優化之后,主線程和多線程網絡IO的執行流程如下:

image

具體步驟如下:

  • 主線程建立連接,並接受數據,並將獲取的 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 多線程模式的時候,線程數一定要小於機器核數,否着意義不大。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM