作者:極光高階工程師—包利
摘要
極光推送後臺標籤/別名系統儲存超過百億條資料, 高峰期 QPS 超過 50 萬, 且隨著業務的發展,儲存容量和訪問量還在不斷增加。之前系統存在的一些瓶頸也逐漸顯現,所以近一兩年持續做了很多的優化工作,最終達到非常不錯的效果。近期,經過積累和沉澱後,將這一部分的工作進行總結。
背景
當前的舊系統中主要儲存標籤/別名與註冊 ID 的相互對映關係, 使用 Key-Value 結構儲存, 考慮到一個註冊 ID 可能有多個標籤, 同時一個標籤也存在多個不同的註冊 ID, 這部分資料使用 Redis 儲存中的 Set 資料結構; 而一個註冊 ID 只有一個別名, 同時一個別名也存在多個不同的註冊 ID, 這部分資料使用 String/Set 資料結構。由於此部分資料量過大, 考慮到儲存成本, Redis 使用的單 Master 模式, 而最終資料的落地使用 Pika 儲存(一種類 Redis 的檔案儲存)。Pika 與 Redis 中的資料由業務方保持一致, Redis 正常可用時, 讀 Redis; 當 Redis 不可用時讀 Pika, Redis 恢復可用後, 從 Pika 恢復資料到 Redis, 重新讀 Redis。舊系統的儲存架構如下:
從上面的架構圖可以看到, Redis/Pika 均採用主從模式, 其中 Redis 只有 Master, 配置管理模組用來維護 Redis/Pika 叢集的主從關係, 配置寫入 ZooKeeper 中, 業務模組從 ZooKeeper 中讀取配置, 不做配置變更。所有的配置變更由配置管理模組負責. 當需要遷移, 擴容, 縮容的時候, 必須通過配置管理模組操作。這個舊系統的優缺點如下:
優點:
配置集中管理, 業務模組不需要分開單獨配置
讀取 Redis 中資料, 保證了高併發查詢效率
Pika 主從模式, 保證了資料落地, 不丟失
配置管理模組維護分片 slot 與例項的對映關係, 根據 Key 的 slot 值路由到指定的例項
缺點:
Redis 與 Pika 中儲存的資料結構不一致, 增加了程式碼複雜度
Redis 單 Master 模式, Redis 某個節點不可用時, 讀請求穿透到 Pika, 而 Pika 不能保證查詢效率, 會造成讀請求耗時增加甚至超時
Redis 故障恢復後, 需要從 Pika 重新同步資料, 增加了系統不可用持續時間, 且資料一致性需要更加複雜的計算來保證
當遷移/擴容/縮容時需要手動操作配置管理模組, 步驟繁瑣且容易出錯
Redis 中儲存了與 Pika 同樣多的資料, 佔用了大量的記憶體儲存空間, 資源成本很高
整個系統的可用性還有提升空間, 故障恢復時間可以儘量縮短
配置管理模組為單點, 非高可用, 當此服務 down 掉時, 整個叢集不是高可用, 無法感知 Redis/Pika 的心跳狀態
超大 Key 打散操作需要手動觸發. 系統中存在個別標籤下的註冊 ID 過多, 儲存在同一個例項上, 容易超過例項的儲存上限, 而且單個例項限制了該 Key 的讀效能
舊系統缺點分析
考慮到舊系統存在以上的缺點, 主要從以下幾個方向解決:
Redis 與 Pika 中儲存的資料結構不一致, 增加了程式碼複雜度
分析: 舊系統中 Redis 與 Pika 資料不一致主要是 Pika 早期版本 Set 資料結構操作效率不高, String 資料結構操作的效率比較高, 但獲取標籤/別名下的所有註冊 ID 時需要遍歷所有 Pika 例項, 這個操作非常耗時, 考慮到最新版本 Pika 已經優化 Set 資料結構, 提高了 Set 資料結構的讀寫效能, 應該保持 Redis 與 Pika 資料結構的一致性。
Redis 單 Master 模式, Redis 某個節點不可用時, 讀請求穿透到 Pika, 而 Pika 不能保證查詢效率, 會造成讀請求耗時增加甚至超時
分析: Redis 單 Master 模式風險極大。需要優化為主從模式, 這樣能夠在某個 Master 故障時能夠進行主從切換, 不再從 Pika 中恢復資料, 減少故障恢復時間, 減少資料不一致的可能性。
Redis 故障恢復後, 需要從 Pika 重新同步資料, 增加了系統不可用持續時間, 且資料一致性需要更加複雜的計算來保證
分析: 這個系統恢復時間過長是由於 Redis 是單 Master 模式, 且沒有持久化, 需要把 Redis 優化成主從模式且必須開啟持久化, 從而幾乎不需要從 Pika 恢復資料了, 除非 Redis 的主從例項全部同時不可用。不需要從 Pika 恢復資料後, 那麼 Redis 中的資料在 Redis 主從例項發生故障時, 就和 Pika 中的資料一致了。
當遷移/擴容/縮容時需要手動操作配置管理模組, 步驟繁瑣且容易出錯
分析: 配置管理模組手動干預操作過多, 非常容易出錯, 這部分應儘量減少手動操作, 考慮引入 Redis 哨兵, 能夠替換大部分的手動操作步驟。
Redis 中儲存了與 Pika 同樣多的資料, 佔用了大量的記憶體儲存空間, 資源成本很高
分析: 通過對 Redis 中的各個不同維度資料進行資料量和訪問量以及訪問來源分析(如下圖)。外部請求量(估算) 這欄的資料反應了各個不同 Key 的單位時間內訪問量情況。
Redis 的儲存資料主要分為標籤/別名到註冊 ID 和註冊 ID 到標籤/別名兩部分資料. 通過分析得知, 標籤/別名到註冊 ID 的資料約佔 1/3 左右的儲存空間, 訪問量佔到 80%; 註冊 ID 到標籤/別名的資料約佔 2/3 左右的儲存空間, 訪問量佔到 20%。可以看到, 紅色數字部分為訪問的 Pika, 黑色部分訪問的 Redis, 3.7%這項的資料可以優化成訪問 Redis, 那麼可以得出結論, 紅色的資料在 Redis 中是永遠訪問不到的。所以可以考慮將 Redis 中註冊 ID 到標籤/別名這部分資料刪掉, 訪問此部分資料請求到 Pika, 能夠節省約 2/3 的儲存空間, 同時還能保證整個系統的讀效能。
整個系統的可用性還有提升空間, 故障恢復時間可以儘量縮短
分析: 這部分主要由於其中一項服務為非高可用, 而且整個系統架構的複雜性較高, 以及資料一致性相對比較難保證, 導致故障恢復時間長, 考慮應將所有服務均優化為高可用, 同時簡化整個系統的架構。
配置管理模組為單點, 非高可用, 當此服務 down 掉時, 整個叢集不是高可用, 無法感知 Redis/Pika 的心跳狀態
分析: 配置手動管理風險也非常大, Pika 主從關係通過配置檔案手動指定, 配錯後將導致資料錯亂, 產生髒資料. 需要使用 Redis 哨兵模式, 用哨兵管理 Redis/Pika, 配置管理模組不再直接管理所有 Redis/Pika 節點, 而是通過哨兵管理, 同時再發生主從切換或者節點故障時通知配置管理模組, 自動更新配置到 Zookeeper 中, 遷移/擴容/縮容時也基本不用手動干預。
超大 Key 打散操作需要手動觸發。系統中存在個別標籤下的註冊 ID 過多, 儲存在同一個例項上, 容易超過例項的儲存上限, 而且單個例項限制了該 Key 的讀效能
分析: 這部分手動操作, 應該優化為自動觸發, 自動完成遷移, 減少人工干預, 節省人力成本。
Redis 哨兵模式
Redis 哨兵為 Redis/Pika 提供了高可用性, 可以在無需人工干預的情況下抵抗某些型別的故障, 還支援監視, 通知, 自動故障轉移, 配置管理等功能:
監視: 哨兵會不斷檢查主例項和從例項是否按預期工作
通知: 哨兵可以將出現問題的例項以 Redis 的 Pub/Sub 方式通知到應用程式
自動故障轉移: 如果主例項出現問題, 可以啟動故障轉移, 將其中一個從例項升級為主, 並將其他從例項重新配置為新主例項的從例項, 並通知應用程式要使用新的主例項
配置管理: 建立新的從例項或者主例項不可用時等都會通知給應用程式
同時, 哨兵還具有分散式性質, 哨兵本身被設計為可以多個哨兵程式協同工作, 當多個哨兵就給定的主機不再可用這一事實達成共識時, 將執行故障檢測, 這降低了誤報的可能性。 即使不是所有的哨兵程式都在工作, 哨兵仍能正常工作, 從而使系統能夠應對故障。
Redis 哨兵+主從模式能夠在 Redis/Pika 發生故障時及時反饋例項的健康狀態, 並在必要時進行自動主從切換, 同時會通過 Redis 的 sub/pub 機制通知到訂閱此訊息的應用程式。從而應用程式感知這個主從切換, 能夠短時間將連結切換到健康的例項, 保障服務的正常執行。
沒有采用 Redis 的叢集模式, 主要有以下幾點原因:
當前的儲存規模較大, 叢集模式在故障時, 恢復時間可能很長
叢集模式的主從複製通過非同步方式, 故障恢復期間不保證資料的一致性
叢集模式中從例項不能對外提供查詢, 只是主例項的備份
無法全域性模糊匹配 Key, 即無法遍歷所有例項來查詢一個模糊匹配的 Key
最終解決方案
綜上, 為了保證整個儲存叢集的高可用, 減少故障恢復的時間, 甚至做到故障時對部分業務零影響, 我們採用了 Redis 哨兵+Redis/Pika 主從的模式, Redis 哨兵保證整個儲存叢集的高可用, Redis 主從用來提供查詢標籤/別名到註冊 ID 的資料, Pika 主從用來儲存全量資料和一些註冊 ID 到標籤/別名資料的查詢。需要業務保證所有 Redis 與 Pika 資料的全量同步。新方案架構如下:
從上面架構圖來看, 當前 Redis/Pika 都是多主從模式, 同時分別由不同的多個哨兵服務監視, 只要主從例項中任一個例項可用, 整個叢集就是可用的。Redis/Pika 叢集內包含多個主從例項, 由業務方根據 Key 計算 slot 值, 每個 slot 根據配置管理模組指定的 slot 與例項對映關係。如果某個 slot 對應的 Redis 主從例項均不可用, 會查詢對應的 Pika, 這樣就保證整個系統讀請求的高可用。這個新方案的優缺點如下:
優點:
整個系統所有服務, 所有儲存元件均為高可用, 整個系統可用性非常高
故障恢復時間快, 理論上當有 Redis/Pika 主例項故障, 只會影響寫入請求, 故障時間是哨兵檢測的間隔時間; 當 Redis/Pika 從例項故障, 讀寫請求都不受影響, 讀服務可以自動切換到主例項, 故障時間為零, 當從例項恢復後自動切換回從例項
標籤/別名儲存隔離, 讀寫隔離, 不同業務隔離, 做到互不干擾, 根據不同場景擴縮容
減少 Redis 的記憶體佔用 2/3 左右, 只使用原有儲存空間的 1/3, 減少資源成本
配置管理模組高可用, 其中一個服務 down 掉, 不影響整個叢集的高可用, 只要其中一臺服務可用, 那麼整個系統就是可用
可以平滑遷移/擴容/縮容, 可以做到遷移時無需業務方操作, 無需手動干預, 自動完成; 擴容/縮容時運維同步好資料, 修改配置管理模組配置然後重啟服務即可
超大 Key 打散操作自動觸發, 整個操作對業務方無感知, 減少運維成本
缺點:
Redis 主從例項均可不用時, 整個系統寫入這個例項對應 slot 的資料均失敗, 考慮到從 Pika 實時同步 Redis 資料的難度, 並且主從例項均不可用的概率非常低, 選擇容忍這種情況
哨兵管理增加系統了的複雜度, 當 Redis/Pika 例項主從切換時通知業務模組處理容易出錯, 這部分功能已經過嚴格的測試以及線上長時間的功能檢驗
其他優化
除了通過以上架構優化, 本次優化還包括以下方面:
通過 IO 複用, 由原來的每個執行緒一個例項連結, 改為在同一個執行緒同時管理多個連結,提高了服務的 QPS, 降低高峰期的資源使用率(如負載等)
之前的舊系統存在多個模組互相呼叫情況, 減少模組間耦合, 減少部署及運維成本
之前的舊系統模組間使用 MQ 互動, 效率較低, 改為 RPC 呼叫, 提高了呼叫效率, 保證呼叫的成功率, 減少資料不一致, 方便定位問題
不再使用 Redis/Pika 定製化版本, 可根據需要, 升級到 Redis/Pika 官方的最新穩定版本
查詢模組在查詢大 Key 時增加快取, 快取查詢結果, 此快取過期前不再查詢 Redis/Pika, 提高了查詢效率
展望
未來此係統還可以從以下幾個方面繼續改進和優化:
大 Key 儲存狀態更加智慧化管理, 增加設定時的大 Key 自動化遷移, 使儲存更加均衡
制定合理的 Redis 資料過期機制, 降低 Redis 的儲存量, 減少服務儲存成本
增加集合操作服務, 實現跨 Redis/Pika 例項的交集/並集等操作, 並新增快取機制, 提高上游服務訪問效率, 提高推送訊息下發效率
總結
本次系統優化在原有儲存元件的基礎上, 根據服務和資料的特點, 合理優化服務間呼叫方式, 優化資料儲存的空間, 將 Redis 當作快取, 只儲存訪問量較大的資料, 降低了資源使用率。Pika 作為資料落地並承載訪問量較小的請求, 根據不同儲存元件的優缺點, 合理分配請求方式。同時將所有服務設計為高可用, 提高了系統可用性, 穩定性。最後通過增加快取等設計, 提高了高峰期的查詢 QPS, 在不擴容的前提下, 保證系統高峰期的響應速度。