轉(zhuǎn)載:從單機到2000萬QPS: 知乎Redis平臺發(fā)展與演進之路
導(dǎo)讀:知乎存儲平臺團隊基于開源Redis 組件打造的知乎 Redis 平臺满哪,經(jīng)過不斷的研發(fā)迭代,目前已經(jīng)形成了一整套完整自動化運維服務(wù)體系,提供很多強大的功能靠抑。本文作者是是該系統(tǒng)的負責(zé)人,文章深入介紹了該系統(tǒng)的方方面面,作為后端程序員值得仔細研究眷蜈。
作者簡介:陳鵬,現(xiàn)知乎存儲平臺組 Redis 平臺技術(shù)負責(zé)人沈自,2014 年加入知乎技術(shù)平臺組從事基礎(chǔ)架構(gòu)相關(guān)系統(tǒng)的開發(fā)與運維,從無到有建立了知乎 Redis 平臺辜妓,承載了知乎高速增長的業(yè)務(wù)流量枯途。
背景
知乎作為知名中文知識內(nèi)容平臺忌怎,每日處理的訪問量巨大,如何更好的承載這樣巨大的訪問量酪夷,同時提供穩(wěn)定低時延的服務(wù)保證榴啸,是知乎技術(shù)平臺同學(xué)需要面對的一大挑戰(zhàn)。
知乎存儲平臺團隊基于開源Redis 組件打造的 Redis 平臺管理系統(tǒng)晚岭,經(jīng)過不斷的研發(fā)迭代鸥印,目前已經(jīng)形成了一整套完整自動化運維服務(wù)體系,提供一鍵部署集群坦报,一鍵自動擴縮容, Redis 超細粒度監(jiān)控库说,旁路流量分析等輔助功能。
目前片择,Redis 在知乎規(guī)模如下:
●?機器內(nèi)存總量約70TB潜的,實際使用內(nèi)存約40TB;
●?平均每秒處理約1500萬次請求字管,峰值每秒約2000萬次請求啰挪;
●?每天處理約1萬億余次請求;
●?單集群每秒處理最高每秒約400萬次請求嘲叔;
●?集群實例與單機實例總共約800個亡呵;
●?實際運行約16000個Redis 實例;
●?Redis 使用官方3.0.7版本硫戈,少部分實例采用4.0.11版本锰什。
Redis at Zhihu
根據(jù)業(yè)務(wù)的需求,我們將實例區(qū)分為單機(Standalone)和集群(Cluster)兩種類型掏愁,單機實例通常用于容量與性能要求不高的小型存儲歇由,而集群則用來應(yīng)對對性能和容量要求較高的場景。
單機(Standalone)
對于單機實例果港,我們采用原生主從(Master-Slave)模式實現(xiàn)高可用沦泌,常規(guī)模式下對外僅暴露 Master 節(jié)點。由于使用原生 Redis辛掠,所以單機實例支持所有 Redis 指令谢谦。
對于單機實例,我們使用Redis 自帶的哨兵(Sentinel)集群對實例進行狀態(tài)監(jiān)控與 Failover萝衩。Sentinel 是 Redis 自帶的高可用組件回挽,將 Redis 注冊到由多個 Sentinel 組成的 Sentinel 集群后,Sentinel 會對 Redis 實例進行健康檢查猩谊,當(dāng) Redis 發(fā)生故障后千劈,Sentinel 會通過 Gossip 協(xié)議進行故障檢測,確認宕機后會通過一個簡化的 Raft 協(xié)議來提升 Slave 成為新的 Master牌捷。
通常情況我們僅使用1 個 Slave 節(jié)點進行冷備墙牌,如果有讀寫分離請求涡驮,可以建立多個Read only slave 來進行讀寫分離。
如圖所示喜滨,通過向Sentinel 集群注冊 Master 節(jié)點實現(xiàn)實例的高可用捉捅,當(dāng)提交 Master 實例的連接信息后,Sentinel 會主動探測所有的 Slave 實例并建立連接虽风,定期檢查健康狀態(tài)棒口。客戶端通過多種資源發(fā)現(xiàn)策略如簡單的 DNS 發(fā)現(xiàn) Master 節(jié)點辜膝,將來有計劃遷移到如 Consul 或 etcd 等資源發(fā)現(xiàn)組件 无牵。
當(dāng)Master 節(jié)點發(fā)生宕機時,Sentinel 集群會提升 Slave 節(jié)點為新的 Master内舟,同時在自身的 pubsub channel +switch-master 廣播切換的消息合敦,具體消息格式為:
switch-master
watcher 監(jiān)聽到消息后,會去主動更新資源發(fā)現(xiàn)策略验游,將客戶端連接指向新的 Master 節(jié)點充岛,完成 Failover,具體 Failover 切換過程詳見 Redis 官方文檔耕蝉。
Redis Sentinel Documentation?[1]
實際使用中需要注意以下幾點:
●?只讀Slave 節(jié)點可以按照需求設(shè)置?slave-priority?參數(shù)為0崔梗,防止故障切換時選擇了只讀節(jié)點而不是熱備 Slave 節(jié)點;
●?Sentinel 進行故障切換后會執(zhí)行?CONFIG REWRITE?命令將SLAVEOF 配置落地垒在,如果 Redis 配置中禁用了 CONFIG 命令蒜魄,切換時會發(fā)生錯誤,可以通過修改 Sentinel 代碼來替換 CONFIG 命令场躯;
●?Sentinel Group 監(jiān)控的節(jié)點不宜過多谈为,實測超過 500 個切換過程偶爾會進入?TILT?模式,導(dǎo)致Sentinel 工作不正常踢关,推薦部署多個 Sentinel 集群并保證每個集群監(jiān)控的實例數(shù)量小于 300 個伞鲫;
●?Master 節(jié)點應(yīng)與 Slave 節(jié)點跨機器部署,有能力的使用方可以跨機架部署签舞,不推薦跨機房部署 Redis 主從實例秕脓;
●?Sentinel 切換功能主要依賴?down-after-milliseconds?和failover-timeout?兩個參數(shù),down-after-milliseconds?決定了Sentinel 判斷 Redis 節(jié)點宕機的超時儒搭,知乎使用 30000 作為閾值吠架。而?failover-timeout?則決定了兩次切換之間的最短等待時間,如果對于切換成功率要求較高搂鲫,可以適當(dāng)縮短failover-timeout?到秒級保證切換成功傍药,具體詳見Redis 官方文檔[2];
●?單機網(wǎng)絡(luò)故障等同于機器宕機,但如果機房全網(wǎng)發(fā)生大規(guī)模故障會造成主從多次切換怔檩,此時資源發(fā)現(xiàn)服務(wù)可能更新不夠及時褪秀,需要人工介入蓄诽。
集群(Cluster)
當(dāng)實例需要的容量超過20G 或要求的吞吐量超過 20萬請求每秒時薛训,我們會使用集群(Cluster)實例來承擔(dān)流量。集群是通過中間件(客戶端或中間代理等)將流量分散到多個 Redis 實例上的解決方案仑氛。
知乎的Redis 集群方案經(jīng)歷了兩個階段:客戶端分片與 Twemproxy 代理
客戶端分片(before 2015)
早期知乎使用redis-shard 進行客戶端分片乙埃,redis-shard 庫內(nèi)部實現(xiàn)了?CRC32、MD5锯岖、SHA1三種哈希算法介袜,支持絕大部分Redis 命令。使用者只需把 redis-shard 當(dāng)成原生客戶端使用即可出吹,無需關(guān)注底層分片遇伞。
基于客戶端的分片模式具有如下優(yōu)點:
●?基于客戶端分片的方案是集群方案中最快的,沒有中間件捶牢,僅需要客戶端進行一次哈希計算鸠珠,不需要經(jīng)過代理,沒有官方集群方案的MOVED/ASK 轉(zhuǎn)向秋麸;
●?不需要多余的Proxy 機器渐排,不用考慮 Proxy 部署與維護;
●?可以自定義更適合生產(chǎn)環(huán)境的哈希算法灸蟆。
但是也存在如下問題:
●?需要每種語言都實現(xiàn)一遍客戶端邏輯驯耻,早期知乎全站使用Python 進行開發(fā),但是后來業(yè)務(wù)線增多炒考,使用的語言增加至 Python可缚,Golang,Lua斋枢,C/C++帘靡,JVM 系(Java,Scala杏慰,Kotlin)等测柠,維護成本過高;
●?無法正常使用MSET缘滥、MGET?等多種同時操作多個Key 的命令轰胁,需要使用 Hash tag 來保證多個 Key 在同一個分片上;
●?升級麻煩朝扼,升級客戶端需要所有業(yè)務(wù)升級更新重啟赃阀,業(yè)務(wù)規(guī)模變大后無法推動;
●?擴容困難,存儲需要停機使用腳本Scan 所有的 Key 進行遷移榛斯,緩存只能通過傳統(tǒng)的翻倍取模方式進行擴容观游;
●?由于每個客戶端都要與所有的分片建立池化連接,客戶端基數(shù)過大時會造成Redis 端連接數(shù)過多驮俗,Redis 分片過多時會造成 Python 客戶端負載升高懂缕。
具體特點詳見zhihu/redis-shard[3]。早期知乎大部分業(yè)務(wù)由Python 構(gòu)建王凑,Redis 使用的容量波動較小搪柑, redis-shard 很好地應(yīng)對了這個時期的業(yè)務(wù)需求,在當(dāng)時是一個較為不錯解決方案索烹。
Twemproxy 集群 (2015 - Now)
2015 年開始工碾,業(yè)務(wù)上漲迅猛,Redis 需求暴增百姓,原有的 redis-shard 模式已經(jīng)無法滿足日益增長的擴容需求渊额,我們開始調(diào)研多種集群方案,最終選擇了簡單高效的 Twemproxy 作為我們的集群方案垒拢。
由Twitter 開源的 Twemproxy 具有如下優(yōu)點:
●?性能很好且足夠穩(wěn)定旬迹,自建內(nèi)存池實現(xiàn)Buffer 復(fù)用,代碼質(zhì)量很高子库;
●?支持fnv1a_64舱权、murmur、md5?等多種哈希算法仑嗅;
●?支持一致性哈希(ketama)宴倍,取模哈希(modula)和隨機(random)三種分布式算法。
具體特點詳見twitter/twemproxygithub.com[4]
但是缺點也很明顯:
●?單核模型造成性能瓶頸仓技;
●?傳統(tǒng)擴容模式僅支持停機擴容鸵贬。
對此,我們將集群實例分成兩種模式脖捻,即緩存(Cache)和存儲(Storage):
如果使用方可以接收通過損失一部分少量數(shù)據(jù)來保證可用性阔逼,或使用方可以從其余存儲恢復(fù)實例中的數(shù)據(jù),這種實例即為緩存地沮,其余情況均為存儲嗜浮。
我們對緩存和存儲采用了不同的策略:
存儲
對于存儲我們使用fnv1a_64?算法結(jié)合modula?模式即取模哈希對Key 進行分片,底層 Redis 使用單機模式結(jié)合 Sentinel 集群實現(xiàn)高可用摩疑,默認使用 1 個 Master 節(jié)點和 1 個 Slave 節(jié)點提供服務(wù)危融,如果業(yè)務(wù)有更高的可用性要求,可以拓展 Slave 節(jié)點雷袋。
當(dāng)集群中Master 節(jié)點宕機吉殃,按照單機模式下的高可用流程進行切換,Twemproxy 在連接斷開后會進行重連,對于存儲模式下的集群蛋勺,我們不會設(shè)置?auto_eject_hosts, 不會剔除節(jié)點瓦灶。
同時,對于存儲實例抱完,我們默認使用noeviction?策略贼陶,在內(nèi)存使用超過規(guī)定的額度時直接返回OOM 錯誤,不會主動進行 Key 的刪除乾蛤,保證數(shù)據(jù)的完整性每界。
由于Twemproxy 僅進行高性能的命令轉(zhuǎn)發(fā),不進行讀寫分離家卖,所以默認沒有讀寫分離功能,而在實際使用過程中庙楚,我們也沒有遇到集群讀寫分離的需求上荡,如果要進行讀寫分離,可以使用資源發(fā)現(xiàn)策略在 Slave 節(jié)點上架設(shè) Twemproxy 集群馒闷,由客戶端進行讀寫分離的路由酪捡。
緩存
考慮到對于后端(MySQL/HBase/RPC 等)的壓力,知乎絕大部分業(yè)務(wù)都沒有針對緩存進行降級纳账,這種情況下對緩存的可用性要求較數(shù)據(jù)的一致性要求更高逛薇,但是如果按照存儲的主從模式實現(xiàn)高可用,1 個 Slave 節(jié)點的部署策略在線上環(huán)境只能容忍 1 臺物理節(jié)點宕機疏虫,N 臺物理節(jié)點宕機高可用就需要至少 N 個 Slave 節(jié)點永罚,這無疑是種資源的浪費。
所以我們采用了Twemproxy 一致性哈希(Consistent Hashing)策略來配合?auto_eject_hosts?自動彈出策略組建Redis 緩存集群卧秘。
對于緩存我們?nèi)匀皇褂檬褂胒nv1a_64?算法進行哈希計算呢袱,但是分布算法我們使用了ketama?即一致性哈希進行Key 分布。緩存節(jié)點沒有主從翅敌,每個分片僅有 1 個 Master 節(jié)點承載流量羞福。
Twemproxy 配置?auto_eject_hosts?會在實例連接失敗超過server_failure_limit?次的情況下剔除節(jié)點,并在server_retry_timeout?超時之后進行重試蚯涮,剔除后配合ketama?一致性哈希算法重新計算哈希環(huán)治专,恢復(fù)正常使用,這樣即使一次宕機多個物理節(jié)點仍然能保持服務(wù)遭顶。
在實際的生產(chǎn)環(huán)境中需要注意以下幾點:
●?剔除節(jié)點后张峰,會造成短時間的命中率下降,后端存儲如MySQL液肌、HBase 等需要做好流量監(jiān)測挟炬;
●?線上環(huán)境緩存后端分片不宜過大谤祖,建議維持在20G 以內(nèi),同時分片調(diào)度應(yīng)盡可能分散,這樣即使宕機一部分節(jié)點,對后端造成的額外的壓力也不會太多;
●?機器宕機重啟后,緩存實例需要清空數(shù)據(jù)之后啟動,否則原有的緩存數(shù)據(jù)和新建立的緩存數(shù)據(jù)會沖突導(dǎo)致臟緩存。直接不啟動緩存也是一種方法,但是在分片宕機期間會導(dǎo)致周期性server_failure_limit?次數(shù)的連接失斂舾;
●?server_retry_timeout?和server_failure_limit?需要仔細敲定確認,知乎使用10min 和 3 次作為配置,即連接失敗 3 次后剔除節(jié)點,10 分鐘后重新進行連接媒抠。
Twemproxy 部署
在方案早期我們使用數(shù)量固定的物理機部署Twemproxy,通過物理機上的 Agent 啟動實例锉桑,Agent 在運行期間會對 Twemproxy 進行健康檢查與故障恢復(fù)后裸,由于 Twemproxy 僅提供全量的使用計數(shù)因苹,所以 Agent 運行時還會進行定時的差值計算來計算 Twemproxy 的?requests_per_second?等指標(biāo)款筑。
后來為了更好地故障檢測和資源調(diào)度毛秘,我們引入了Kubernetes,將 Twemproxy 和 Agent 放入同一個 Pod 的兩個容器內(nèi)限煞,底層 Docker 網(wǎng)段的配置使每個 Pod 都能獲得獨立的 IP抹恳,方便管理。
最開始署驻,本著簡單易用的原則奋献,我們使用DNS A Record 來進行客戶端的資源發(fā)現(xiàn),每個 Twemproxy 采用相同的端口號旺上,一個 DNS A Record 后面掛接多個 IP 地址對應(yīng)多個 Twemproxy 實例瓶蚂。
初期,這種方案簡單易用宣吱,但是到了后期流量日益上漲窃这,單集群Twemproxy 實例個數(shù)很快就超過了 20 個。由于 DNS 采用的 UDP 協(xié)議有 512 字節(jié)的包大小限制征候,單個 A Record 只能掛接 20 個左右的 IP 地址杭攻,超過這個數(shù)字就會轉(zhuǎn)換為 TCP 協(xié)議,客戶端不做處理就會報錯疤坝,導(dǎo)致客戶端啟動失敗兆解。
當(dāng)時由于情況緊急,只能建立多個Twemproxy Group跑揉,提供多個 DNS A Record 給客戶端锅睛,客戶端進行輪詢或者隨機選擇,該方案可用畔裕,但是不夠優(yōu)雅衣撬。
如何解決Twemproxy 單 CPU 計算能力的限制
之后我們修改了Twemproxy 源碼, 加入?SO_REUSEPORT?支持扮饶。
Twemproxy with SO_REUSEPORT on Kubernetes
同一個容器內(nèi)由Starter 啟動多個 Twemproxy 實例并綁定到同一個端口具练,由操作系統(tǒng)進行負載均衡,對外仍然暴露一個端口甜无,但是內(nèi)部已經(jīng)由系統(tǒng)均攤到了多個 Twemproxy 上扛点。
同時Starter 會定時去每個 Twemproxy 的 stats 端口獲取 Twemproxy 運行狀態(tài)進行聚合哥遮,此外 Starter 還承載了信號轉(zhuǎn)發(fā)的職責(zé)。
原有的Agent 不需要用來啟動 Twemproxy 實例陵究,所以 Monitor 調(diào)用 Starter 獲取聚合后的 stats 信息進行差值計算眠饮,最終對外界暴露出實時的運行狀態(tài)信息。
為什么沒有使用官方Redis 集群方案
我們在2015 年調(diào)研過多種集群方案铜邮,綜合評估多種方案后仪召,最終選擇了看起來較為陳舊的 Twemproxy 而不是官方 Redis 集群方案與 Codis,具體原因如下:
●?MIGRATE 造成的阻塞問題
Redis 官方集群方案使用 CRC16 算法計算哈希值并將 Key 分散到 16384 個 Slot 中松蒜,由使用方自行分配 Slot 對應(yīng)到每個分片中扔茅,擴容時由使用方自行選擇 Slot 并對其進行遍歷,對 Slot 中每一個 Key 執(zhí)行?MIGRATE?命令進行遷移秸苗。
調(diào)研后發(fā)現(xiàn)召娜,MIGRATE 命令實現(xiàn)分為三個階段:
1.?DUMP 階段:由源實例遍歷對應(yīng) Key 的內(nèi)存空間,將 Key 對應(yīng)的 Redis Object 序列化惊楼,序列化協(xié)議跟 Redis RDB 過程一致玖瘸;
2.?RESTORE 階段:由源實例建立 TCP 連接到對端實例,并將?DUMP?出來的內(nèi)容使用RESTORE 命令到對端進行重建檀咙,新版本的 Redis 會緩存對端實例的連接雅倒;
3.?DEL 階段(可選):如果發(fā)生遷移失敗,可能會造成同名的 Key 同時存在于兩個節(jié)點弧可,
此時?MIGRATE?的REPLACE?參數(shù)決定是是否覆蓋對端的同名Key屯断,如果覆蓋,對端的 Key 會進行一次刪除操作侣诺,4.0 版本之后刪除可以異步進行,不會阻塞主進程氧秘。
經(jīng)過調(diào)研年鸳,我們認為這種模式并不適合知乎的生產(chǎn)環(huán)境。Redis 為了保證遷移的一致性丸相,?MIGRATE?所有操作都是同步操作搔确,執(zhí)行MIGRATE?時,兩端的Redis 均會進入時長不等的 BLOCK 狀態(tài)灭忠。
對于小Key膳算,該時間可以忽略不計,但如果一旦 Key 的內(nèi)存使用過大弛作,一個 MIGRATE 命令輕則導(dǎo)致 P95 尖刺涕蜂,重則直接觸發(fā)集群內(nèi)的 Failover,造成不必要的切換
同時映琳,遷移過程中訪問到處于遷移中間狀態(tài)的Slot 的 Key 時机隙,根據(jù)進度可能會產(chǎn)生 ASK 轉(zhuǎn)向蜘拉,此時需要客戶端發(fā)送?ASKING?命令到Slot 所在的另一個分片重新請求,請求時延則會變?yōu)樵瓉淼膬杀丁?/p>
同樣有鹿,方案初期時的Codis 采用的是相同的 MIGRATE 方案旭旭,但是使用 Proxy 控制 Redis 進行遷移操作而非第三方腳本(如 redis-trib.rb),基于同步的類似 MIGRATE 的命令葱跋,實際跟 Redis 官方集群方案存在同樣的問題持寄。
對于這種Huge Key 問題決定權(quán)完全在于業(yè)務(wù)方,有時業(yè)務(wù)需要不得不產(chǎn)生 Huge Key 時會十分尷尬娱俺,如關(guān)注列表稍味。一旦業(yè)務(wù)使用不當(dāng)出現(xiàn)超過 1MB 以上的大 Key 便會導(dǎo)致數(shù)十毫秒的延遲,遠高于平時 Redis 亞毫秒級的延遲矢否。有時仲闽,在 slot 遷移過程中業(yè)務(wù)不慎同時寫入了多個巨大的 Key 到 slot 遷移的源節(jié)點和目標(biāo)節(jié)點,除非寫腳本刪除這些 Key 僵朗,否則遷移會進入進退兩難的地步赖欣。
對此,Redis 作者在 Redis 4.2 的?roadmap[5]?中提到了Non blocking MIGRATE?但是截至目前验庙,Redis 5.0 即將正式發(fā)布顶吮,仍未看到有關(guān)改動,社區(qū)中已經(jīng)有相關(guān)的?Pull Request?[6]粪薛,該功能可能會在5.2 或者 6.0 之后并入 master 分支悴了,對此我們將持續(xù)觀望。
●?緩存模式下高可用方案不夠靈活
還有违寿,官方集群方案的高可用策略僅有主從一種湃交,高可用級別跟Slave 的數(shù)量成正相關(guān),如果只有一個 Slave藤巢,則只能允許一臺物理機器宕機搞莺, Redis 4.2 roadmap 提到了?cache-only mode,提供類似于Twemproxy 的自動剔除后重分片策略掂咒,但是截至目前仍未實現(xiàn)才沧。
●?內(nèi)置Sentinel 造成額外流量負載
另外,官方Redis 集群方案將 Sentinel 功能內(nèi)置到 Redis 內(nèi)绍刮,這導(dǎo)致在節(jié)點數(shù)較多(大于 100)時在 Gossip 階段會產(chǎn)生大量的 PING/INFO/CLUSTER INFO 流量温圆,根據(jù)?issue?中提到的情況,200 個使用 3.2.8 版本節(jié)點搭建的 Redis 集群孩革,在沒有任何客戶端請求的情況下岁歉,每個節(jié)點仍然會產(chǎn)生 40Mb/s 的流量,雖然到后期 Redis 官方嘗試對其進行壓縮修復(fù)嫉戚,但按照 Redis 集群機制刨裆,節(jié)點較多的情況下無論如何都會產(chǎn)生這部分流量澈圈,對于使用大內(nèi)存機器但是使用千兆網(wǎng)卡的用戶這是一個值得注意的地方。
●?slot 存儲開銷
最后帆啃,每個Key 對應(yīng)的 Slot 的存儲開銷瞬女,在規(guī)模較大的時候會占用較多內(nèi)存,4.x 版本以前甚至?xí)_到實際使用內(nèi)存的數(shù)倍努潘,雖然 4.x 版本使用 rax 結(jié)構(gòu)進行存儲诽偷,但是仍然占據(jù)了大量內(nèi)存,從非官方集群方案遷移到官方集群方案時疯坤,需要注意這部分多出來的內(nèi)存报慕。
總之,官方Redis 集群方案與 Codis 方案對于絕大多數(shù)場景來說都是非常優(yōu)秀的解決方案压怠,但是我們仔細調(diào)研發(fā)現(xiàn)并不是很適合集群數(shù)量較多且使用方式多樣化的我們眠冈,場景不同側(cè)重點也會不一樣,但在此仍然要感謝開發(fā)這些組件的開發(fā)者們菌瘫,感謝你們對 Redis 社區(qū)的貢獻蜗顽。
擴容
靜態(tài)擴容
對于單機實例,如果通過調(diào)度器觀察到對應(yīng)的機器仍然有空閑的內(nèi)存雨让,我們僅需直接調(diào)整實例的maxmemory?配置與報警即可雇盖。同樣,對于集群實例栖忠,我們通過調(diào)度器觀察每個節(jié)點所在的機器崔挖,如果所有節(jié)點所在機器均有空閑內(nèi)存,我們會像擴容單機實例一樣直接更新maxmemory?與報警庵寞。
動態(tài)擴容
但是當(dāng)機器空閑內(nèi)存不夠狸相,或單機實例與集群的后端實例過大時,無法直接擴容捐川,需要進行動態(tài)擴容:
●?對于單機實例卷哩,如果單實例超過30GB 且沒有如?sinterstore?之類的多Key 操作我們會將其擴容為集群實例;
●?對于集群實例属拾,我們會進行橫向的重分片,我們稱之為Resharding 過程冷溶。
Resharding 過程
原生Twemproxy 集群方案并不支持?jǐn)U容渐白,我們開發(fā)了數(shù)據(jù)遷移工具來進行 Twemproxy 的擴容,遷移工具本質(zhì)上是一個上下游之間的代理逞频,將數(shù)據(jù)從上游按照新的分片方式搬運到下游纯衍。
原生Redis 主從同步使用?SYNC/PSYNC?命令建立主從連接,收到SYNC?命令的Master 會 fork 出一個進程遍歷內(nèi)存空間生成 RDB 文件并發(fā)送給 Slave苗胀,期間所有發(fā)送至 Master 的寫命令在執(zhí)行的同時都會被緩存到內(nèi)存的緩沖區(qū)內(nèi)襟诸,當(dāng) RDB 發(fā)送完成后瓦堵,Master 會將緩沖區(qū)內(nèi)的命令及之后的寫命令轉(zhuǎn)發(fā)給 Slave 節(jié)點。
我們開發(fā)的遷移代理會向上游發(fā)送SYNC?命令模擬上游實例的Slave歌亲,代理收到 RDB 后進行解析菇用,由于 RDB 中每個 Key 的格式與 RESTORE 命令的格式相同,所以我們使用生成?RESTORE?命令按照下游的Key 重新計算哈希并使用 Pipeline 批量發(fā)送給下游陷揪。
等待RDB 轉(zhuǎn)發(fā)完成后惋鸥,我們按照新的后端生成新的 Twemproxy 配置,并按照新的 Twemproxy 配置建立 Canary 實例悍缠,從上游的 Redis 后端中取 Key 進行測試卦绣,測試 Resharding 過程是否正確,測試過程中的 Key 按照大小飞蚓,類型滤港,TTL 進行比較。
測試通過后,對于集群實例,我們使用生成好的配置替代原有Twemproxy 配置并 restart/reload Twemproxy 代理选浑,我們修改了 Twemproxy 代碼救鲤,加入了 config reload 功能,但是實際使用中發(fā)現(xiàn)直接重啟實例更加可控模叙。而對于單機實例,由于單機實例和集群實例對于命令的支持不同,通常需要和業(yè)務(wù)方確定后手動重啟切換缝龄。
由于Twemproxy 部署于 Kubernetes ,我們可以實現(xiàn)細粒度的灰度挂谍,如果客戶端接入了讀寫分離叔壤,我們可以先將讀流量接入新集群,最終接入全部流量口叙。
這樣相對于Redis 官方集群方案炼绘,除在上游進行?BGSAVE?時的fork 復(fù)制頁表時造成的尖刺以及重啟時造成的連接閃斷,其余對于 Redis 上游造成的影響微乎其微妄田。
這樣擴容存在的問題:
對上游發(fā)送SYNC?后俺亮,上游fork 時會造成尖刺;
對于存儲實例疟呐,我們使用Slave 進行數(shù)據(jù)同步脚曾,不會影響到接收請求的 Master 節(jié)點;
對于緩存實例启具,由于沒有Slave 實例本讥,該尖刺無法避免,如果對于尖刺過于敏感,我們可以跳過 RDB 階段拷沸,直接通過?PSYNC?使用最新的SET 消息建立下游的緩存色查。
切換過程中有可能寫到下游,而讀在上游撞芍;
對于接入了讀寫分離的客戶端秧了,我們會先切換讀流量到下游實例,再切換寫流量勤庐。
一致性問題示惊,兩條具有先后順序的寫同一個Key 命令在切換代理后端時會通過 1)寫上游同步到下游 2)直接寫到下游兩種方式寫到下游,此時愉镰,可能存在應(yīng)先執(zhí)行的命令卻通過 1)執(zhí)行落后于通過 2)執(zhí)行米罚,導(dǎo)致命令先后順序倒置。
這個問題在切換過程中無法避免丈探,好在絕大部分應(yīng)用沒有這種問題录择,如果無法接受,只能通過上游停寫排空Resharding 代理保證先后順序碗降;
官方Redis 集群方案和 Codis 會通過 blocking 的 migrate 命令來保證一致性隘竭,不存在這種問題。
實際使用過程中讼渊,如果上游分片安排合理动看,可實現(xiàn)數(shù)千萬次每秒的遷移速度,1TB 的實例 Resharding 只需要半小時左右爪幻。另外菱皆,對于實際生產(chǎn)環(huán)境來說,提前做好預(yù)期規(guī)劃比遇到問題緊急擴容要快且安全得多挨稿。
旁路分析
由于生產(chǎn)環(huán)境調(diào)試需要仇轻,有時會需要監(jiān)控線上Redis 實例的訪問情況,Redis 提供了多種監(jiān)控手段奶甘,如 MONITOR 命令篷店。
但由于Redis 單線程的限制,導(dǎo)致自帶的 MONITOR 命令在負載過高的情況下會再次跑高 CPU臭家,對于生產(chǎn)環(huán)境來說過于危險疲陕,而其余方式如?Keyspace Notify?只有寫事件,沒有讀事件钉赁,無法做到細致的觀察鸭轮。
對此我們開發(fā)了基于libpcap 的旁路分析工具,系統(tǒng)層面復(fù)制流量橄霉,對應(yīng)用層流量進行協(xié)議分析,實現(xiàn)旁路 MONITOR,實測對于運行中的實例影響微乎其微姓蜂。
同時對于沒有MONITOR 命令的 Twemproxy按厘,旁路分析工具仍能進行分析,由于生產(chǎn)環(huán)境中絕大部分業(yè)務(wù)都使用 Kubernetes 部署于 Docker 內(nèi) 钱慢,每個容器都有對應(yīng)的獨立 IP逮京,所以可以使用旁路分析工具反向解析找出客戶端所在的應(yīng)用,分析業(yè)務(wù)方的使用模式束莫,防止不正常的使用懒棉。
將來的工作
由于Redis 5.0 發(fā)布在即,4.0 版本趨于穩(wěn)定览绿,我們將逐步升級實例到 4.0 版本策严,由此帶來的如 MEMORY 命令、Redis Module 饿敲、新的 LFU 算法等特性無論對運維方還是業(yè)務(wù)方都有極大的幫助妻导。
最后
知乎架構(gòu)平臺團隊是支撐整個知乎業(yè)務(wù)的基礎(chǔ)技術(shù)團隊,開發(fā)和維護著知乎幾乎全量的核心基礎(chǔ)組件怀各,包括容器倔韭、Redis、MySQL瓢对、Kafka寿酌、LB、HBase 等核心基礎(chǔ)設(shè)施硕蛹,團隊小而精醇疼,每個同學(xué)都獨當(dāng)一面負責(zé)上面提到的某個核心系統(tǒng)。
隨著知乎業(yè)務(wù)規(guī)模的快速增長妓美,以及業(yè)務(wù)復(fù)雜度的持續(xù)增加僵腺,我們團隊面臨的技術(shù)挑戰(zhàn)也越來越大,歡迎對技術(shù)感興趣壶栋、渴望技術(shù)挑戰(zhàn)的小伙伴加入我們辰如,一起建設(shè)穩(wěn)定高效的知乎云平臺。有意向可移步知乎網(wǎng)站招聘頁投遞簡歷贵试。
參考資料
1.?Redis Official site?https://redis.io/
2.?Twemproxy Github Page?twitter/twemproxy
3.?Codis Github Page?CodisLabs/codis
4.?SO_REUSEPORT Man Page?socket(7) - Linux manual page
5.?Kubernetes?Production-Grade Container Orchestration
相關(guān)閱讀:
用最少的機器支撐萬億級訪問琉兜,微博6年Redis優(yōu)化歷程
首發(fā)丨360開源的類Redis存儲系統(tǒng):Pika
Redis實戰(zhàn):如何構(gòu)建類微博的億級社交平臺
Codis作者黃東旭細說分布式Redis架構(gòu)設(shè)計和踩過的那些坑
Redis架構(gòu)之防雪崩設(shè)計:網(wǎng)站不宕機背后的兵法
同程旅游緩存系統(tǒng)設(shè)計:如何打造Redis時代的完美體系(含PPT)