作者 | 汪翰林
編輯 | 蔡芳芳
Redis 作為最受歡迎的 NoSQL 資料庫之一,具備高效能、高可用性、高擴充套件性等特點,在各網際網路業務中使用廣泛。目前業界針對 Redis 的效能最佳化主要針是配置項最佳化以及使用方式的最佳化。
本文介紹網易數帆嘗試撇開 Redis 本身,而從通用的協議棧層面來做最佳化,這種最佳化方式理論上可推廣到其他 Socket 類網際網路應用,如 Memcached、Ngnix、Envoy 等。
分 析
Redis-server 作為一個標準的 Socket 類應用,會透過監聽地址埠接收來自客戶端的連線,連線建立後會讀取連線上的客戶端請求,處理後再返回響應給客戶端,這其中的連線建立、請求讀取、響應返回都是透過核心的 TCP/IP 協議棧來處理的。可以透過火焰圖先看一下 Redis-server 在效能壓測下的 CPU 消耗情況。
圖中,是在客戶端讀請求壓測的時候抓取的火焰圖資訊。可見,核心態協議棧所佔用的 CPU 消耗較大,其中以 sys_write 為主,佔比 40% 左右。所以,如果能對這部分 CPU 佔用進行最佳化,收益還是非常可觀的。
那麼這部分 CPU 佔比如何進行最佳化呢?最好還能做到 Redis 應用本身完全無感知。
協議棧的處理完全省掉是不現實的,這樣底層 TCP 通訊就玩不轉了。但是我們可以考慮將這部分處理剝離出去,不佔用 Redis 的 CPU。
那剝離出去的協議棧實現放在哪兒呢?
可以放到一個單獨的程序中實現。那這樣是不是和剝離前沒有區別?
No!因為一臺機器上一般會啟動多個 Redis 例項,多個 Redis 例項在這種情況下就可以共享這個協議棧實現的程序。相當於將 Redis 和協議棧的 1:1 繫結部署關係,變為 N:1 的獨立部署關係。
那這個協議棧實現程序的效能就非常重要了,絕對不能成為瓶頸,否則會導致最終的效能沒有提升,甚至更糟。具體如何實現呢?
下面該輪到使用者態協議棧出場了!
優 化
使用者態協議棧介紹
顧名思義,使用者態協議棧是將原本在核心態實現的 TCP/IP 協議棧移到使用者態實現的技術。放到使用者態實現可以帶來幾大好處:
1. 高效能
Redis 本身是一個使用者態的應用程式,呼叫核心態的 TCP/IP 協議棧實現,不可避免地會帶來使用者態和核心態的上下文切換開銷。另外,最重要的一點,核心協議棧和應用繫結在一起,無法做到和應用在資源佔用上剝離,也就是前面所述的獨立部署。
2. 易調測
做過核心態開發的同學應該都知道,核心下程式的調測還是比較痛苦的,動不動給你來個 Oops 就會導致核心掛死。放到使用者態實現調測起來就會方便很多。
3. 易定製
核心協議棧隨著版本的迭代,歷史包袱越來越重,導致越來越臃腫。而且新特性的合入時依賴會越來越多,也會越來越謹慎,甚至 bug 的修復週期也越來越長。使用者態協議棧則不會有此類問題,可以在核心協議棧的基礎上做裁剪和定製,易調測也會讓試錯成本大大降低。
使用者態協議棧具體實現
使用者態協議棧我們採用開源 VPP+VCL 的方案:VPP 作為獨立程序在使用者態實現 TCP/IP 協議棧,VCL 作為動態庫實現 Socket 類介面劫持並和後端 VPP 完成互動。整個系統的架構如下圖所示:
其中:
VCL - 實現 Socket 類介面劫持並和後端 VPP 完成互動
FIFO - 是基於共享記憶體封裝的訊息佇列,用於 VCL 和 VPP 之間通訊
Session - 維持傳輸層和上層應用會話之間的對應
TCP/IP - 對應核心的 TCP/IP 協議棧實現
DPDK - 實現將網絡卡的報文收發解除安裝到使用者態
可見,VPP+VCL 分離式的部署模式將協議棧從應用端剝離,並透過 LD_PRELOAD 方式載入 VCL 動態庫實現對於 Redis 的無侵入加速。
最後,VPP 如何做到本身處理的高效而不會成為瓶頸呢?
VPP 主要基於 DPDK 實現報文的高效收發,再結合自身的向量化處理(減少 CPU Cache missing)來實現報文的高效處理。另外,graph node+ 外掛化也讓其非常易於擴充套件和定製。
rdbsave 動態程序問題
使用開源 VPP 加速 Redis 過程中,也遇到和解決了不少社群版本中的問題,比較典型的就是 rdbsave 動態程序引發的問題。
Redis 可以配置週期性的儲存快照,實現上會啟用一個動態的 rdbsave 程序來完成,rdbsave 程序非常駐程序,在完成工作後就會退出。配置檔案中可以指定儲存的週期以及觸發儲存的變化量,如果週期配置的比較短且觸發儲存的變化量比較小,則可能會導致 rdbsave 程序頻繁的建立和退出,實測過程中這也會導致目前社群中對於動態程序支援的一些問題很快速的就能暴露出來。
Session 同步問題
rdbsave 程序建立時會從主執行緒同步 socket 相關的 session 資源。目前社群中 epoll fd 相關的 session 資源沒有同步完全,主要是因為 session handle 中包含了各個程序的 worker_index 資訊,而 worker_index 是因程序 / 執行緒而異的,直接從主執行緒同步過來的 session handle 需要根據 worker_index 做轉換才能使用。相關的 patch 目前已經合入社群。
死鎖問題
rdbsave 程序退出時需要釋放和程序關聯的 session 資源,目前是透過主執行緒捕獲 SIGCHLD 訊號,在訊號處理函式中來釋放相關 session 資源。如果主執行緒在先獲取鎖 A 的情況下跳轉到訊號處理函式釋放資源,而釋放資源的時候也獲取了鎖 A,則會導致死鎖。當然我們可以針對鎖 A 的情況想辦法解決此問題,但是這種解決方式不徹底,因為主執行緒可能獲取了鎖 B 後再去執行訊號處理函式釋放資源,然後釋放資源的時候也獲取了鎖 B。根源是在於執行訊號處理函式之前的主執行緒狀態未知。
所以,我們可以考慮在訊號處理函式中不釋放資源,而僅僅將待釋放的資源索引進行儲存,等到後面合適的時機,如執行 epoll_wait 的時候再進行釋放。相關的 patch 目前也已經合入社群。
效 果
透過最佳化後的火焰圖看效果:
可見,核心的 socket 讀寫已經大大降低,還遺留的是使用者態協議棧實現中用來在 VCL 和 VPP 之間通知事件的 eventfd 通知。
基於 redis 4。0。9 以及 memtier_benchmark 1。2。17 測試的結果。
QPS 提升 31%,此時核心態 Redis CPU 佔用 99%,使用者態 Redis CPU 佔用 80% 左右。
延遲降低 23。2%,同樣此時核心態 Redis CPU 佔用 99%,使用者態 Redis CPU 佔用 80% 左右。
總 結
使用者態協議棧可以輕鬆做到針對 Redis 的無侵入加速,在佔用 CPU 資源更少的情況下,相較核心態協議棧可以取得 31% 的 QPS 加速效果,同時延遲降低 23%。
使用者態協議棧作為通用的加速元件,理論上可以支援所有 Socket 類應用的加速。目前基於使用者態協議棧對網易數帆輕舟微服務 API 閘道器中 Envoy 的加速已經產品化並在網易嚴選環境中落地,針對 Sidecar 的加速也相繼在內外部客戶完成測試,針對 Redis 的加速也完成了 PoC 測試。整個加速元件的資料面基於 Kubernetes 的 DaemonSet 部署,而管控面基於 Kubernetes 的 Operator 部署,部署簡單、運維方便。我們也會在後續工作中,持續探索基於使用者態協議棧的更多應用場景。
作者介紹:
汪翰林,網易數帆系統開發專家,16 年軟體開發老兵。曾就職於華三和華為,從事安全、影片監控、大資料和網路虛擬化等技術產品研發,目前在網易杭州研究院負責高效能網路技術預研和產品落地工作。