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 多線程模式的時候,線程數一定要小於機器核數,否着意義不大。