豌豆莢分布式redis設計與實現-劉奇(豌豆莢資深系統架構師)
從這個使用情況,現在國內很多的公司在測試,有一部分已經用到線上系統里面去了,現在我們目前沒有發現報的BUG,這個蠻容幸的,在開源之前我們很糾結,萬一被人噴了呢?現在噴子這么多,后來發布之后我們發現沒有人噴,這個好意外,當然也比較小驚喜一點。目前我是在豌豆莢的基礎架構部,Storage是里面小的部門,專門做存儲和緩存相關的事情。
豌豆莢是為了響應速度是高度依賴redis,除了一部分歷史原因存在的,大部分的請求都不會走到數據庫。我們這個場景里面有這樣一些Image-service,手機的轉碼很麻煩的,首次轉碼時間比較久,大概需要3點幾秒,那個庫不能做并行的,要轉成WEBP還是M的格式,這個不能放緩存里面,另外一個是好友關系,這個挺夸張的,所有的好友都是在很短的時間之內取到,大概是4點幾億,這個豌豆莢實際上是有它的用戶數量的公開的報道,然后還有一個applist,是豌豆莢里面最大的Codis用戶,是720g,32instances,這個規劃是當初按照靜態分片做的規劃,后來做了擴容,這個內存還是進一步省一點,后面會做一個收縮。我們現在主要的業務特點后端是Mysql和 hHbase,Mysql是相對簡單一點的,或者是對它有一點特性依賴的,可能還有少部分歷史遺留的代碼,緩存是memcached+redis。 Hbase是分成兩個小塊專門負責HBase,也會提出一些BUG的提示,最新的0.98的應該打進去了,后面應該會看到豌豆莢會進入到名單里面去。
然后介紹一下我們這個使用redis的歷程,很多都是會經過這個類似的歷程,使用redis的用戶舉手?這個還是很夸張的,基本上很多的用戶都在用這個 redis,可能大部分都是用在這個程度,單實例就可以解決所有的問題,然后當單實例解決不了的時候,采用在代碼當中做Sharding然后,如果你的要求是一年有5個9,是不敢隨便重啟程序的。再進一步使用Twemproxy,流量沒有壓力的時候你會使用單個的,但是單點的問題很嚴重,如果掛了很嚴重的問題,當它流量不夠的時候會進一步使用多個Twemproxy,目前豌豆莢絕大多數的都跑到Codis上面。然后使用redis是痛苦的地方,單機內存不夠,帶寬不夠,最惡心的就是動態擴容,對于寫服務的人來說只要用這個東西不擔心后面的擴容怎么搞,跟運維提出一個要求,這個時候業務肯定沒有辦法接受的,但是運維也很痛苦,他說你現在擴容沒有辦法,有一個傳統的辦法就是說先把整個數據復制一份,復制兩份一模一樣的,這個時候改配置,如果這種情況下是不能這樣干,本身達到單機沒有辦法承受,不能再把兩個復制一份復制成完整的這個不太可能。這個時候就需要解決問題,還有一個磁盤損壞的時候數據搶救,是會修改多少個K,或者是每秒強制flush一次,但是數據不能丟,你直接打到數據庫,不管是什么都是立刻掛,如果緩存MISS掉,對于很多的公司來說他們先做一個預熱,每秒的處理能力就是那么幾千,Mysql就很快掛了。
Twemproxy的用戶兩很大的,在Codis之前沒有解決方案的,官方的Cluster發布一個beta版本,后面會提一下即使是正式版也有很大的風險。
Twemproxy的問題就是根本沒有辦法運維,當你裝了這個之后業務的特點是什么?運維是不知道,沒有辦法通過觀測Twemproxy觀測到什么特點,主要是用什么指令,推ter甚至做的更過一點,我知道你的top 10 的KEY是什么,因為對運維的要求很高,這個需要一個好一點的工具做這件事情,官方的Cluster,首先一個無中心的設計寫程序寫起來是很痛苦的,老許說狀態機的問題,大家看那個是單線程,到處都是狀態,然后這個Cluster的實現我們看了一下,這個函數有426行代碼,這個是很要命的,還有狀態的切換,覺得腦子沒有轉過來,狀態切換的時候很難保證大腦里面能想到所有的情況,這個太難了,作者還是沒有想清楚,10年的時候他會說年底會出一個集群的版本,我們等了4年,大話西游都可以重播一次了,即使是剛剛出來的時候我們也是不敢踩坑的畢竟是線上的環境,我們也需要有人列出來注意事項有10條,所有的東西差不多了之后才會考慮一下這個Cluster是不是可以用了?最大的問題是這樣,整個的設計是不合理的,因為整個系統是高度耦合的,它把集群和存儲全部放進去了,試想一下,如果有BUG你怎么辦?整個集群怎么辦?怎么升級?
為什么一定要redis?現在有很多的方案,Tair,Couchbase,一個是快,二是數據結構非常好,數據結構是更能描述你邏輯的東西,所以 redis是高度的依賴,還有很多的公司為了追求響應速度都把數據放redis里面。終于到了我們這個Codis。這個可能大家有關注我微博我會提了一下,又一個解決方案,為什么是又一個解決方案呢?我們沒有好意思說取代官方的,這個會被人噴死的,作者畢竟粉絲很多,所以說我們是替換Twemproxy 的,實際上我們是完全不考慮官方的Cluster,和一些其他的同仁聊了一下,他們覺得官方的設計是不太合理的,我們提供一個HA,這部分是用的 redis自帶的同步,然后我們最重要的幾個問題就是Scalability沒有單點,這個是GO和C的,這個是幾百行的C代碼,還有一部分是不實用的,就是用于DEBUG。Codis解決這些問題,動態擴容,我第一次說我們動態縮容的時候別人震驚了一下,我們上線之后確實有這個需求,修一個機器磁盤壞了,我們不得不把這個數據倒到另外的機器上面去,縮容對于遷移機器的時候是非常有幫助的,有可能有一些別的情況下需要縮容,預先分配是10臺,后來發現8 臺就可以,你就很正常可以縮成8臺,下次需要可以再加一臺變成9臺,這樣可以省成本,甚至是可以做的再夸張一點,有的業務是有峰值的,內存需要的比較大,但是這部分機器可以共用的,并不是所有的業務峰值都同時發生,需要的時候動態擴一下,業務高峰期一過馬上縮回來,這樣可以省成本。還有可以解決下面的這個單點故障帶寬不足的問題,還有就是我們公司之前會大量使用Twemproxy,為了推動業務的發展我們就寫了工具從Twemproxy工具上面無縫倒到這個Codis來,我們有一個很漂亮的運維和監控的頁面,就是可以監控里面是可以看到這一個用戶的業務使用的哪些指令,以及這些指令使用了多少次,以及響應時間是多少,我們線上觀測的數據是所有服務的指令,都是5毫秒內得到響應,整個項目沒有遇到任何的go的gc問題,最新的go1.4我們測下來這個gc還是不行,我們線上用的版本是1.3.1,我們整個是Pre-sharding,最大的擴容是1023個實例,整個集群會分成那么多的SLOT,會做一些中間狀態的存儲,Proxy無狀態,增加這個是運維操作,用戶這邊是沒有感知的,使用非常平滑,我們現在基本上是要擴容直接給運維發一個申請要擴容就可以了,整個中間所有的業務都不需要動。我們做第一個版本花了一周的時間,但是我們之前想的比較多,大概想了兩周的時間,為什么呢?因為這個分布式系統是比較復雜,而且在我們這個之前是沒有能夠擴容的方案的,我們要做的就是第一個,那第一個就是你要考慮的問題比較多,而且在做設計的時候當時做了一個決定,這個東西一定要開源的,所以會想的比較多也很正常,然后國內的環境大家也清楚的。首先一個就是分布式系統是復雜的,這個是做分布式系統的人都比較同意,然后我們開發人員不足,盡量拆分,簡化每個模塊,這個非常重要,為什么我說官方的不足的地方就是這個,他是沒有辦法處理升級的你的Cluster的管理和存儲是綁定一起,如果有BUG,分布式這邊有BUG你整個升級怎么升?要把所有的東西停下來,再全部升成新版本再啟動組成新集群,這樣整個業務要停下來,這個沒有辦法接受,大家知道redis作為一個單機的存儲來講質量是非常高的,使用也很穩定,我們希望如果存儲這一塊保持穩定我們希望一直穩定,如果分布式有 BUG我們就需要升級這個模塊就可以了,但是官方不可能做到這一點的,整個就是一個應用程序。
每個組件只負責自己的事情,每個組件都可以單獨測試,分布式的也是可以單獨測試,對 Proxy的管理和狀態都是可以單獨測試的,對于整個分布式系統整個實現來說可以做極大的簡化,這個質量也是很有保障的。另外redis做存儲引擎,最后 Proxy的狀態真正實際的東西一定是有狀態的,然后狀態實際上是存儲在ZK上面的,你只要放這個上面其他客戶端會發現新增的他們會做一個自動的均衡,還有一個考量的地方是說,因為公司有自己的運維系統,然后本身也是比較復雜的,我們不希望做一套系統和運維系統是隔離的,我們整個的redis是不是掛掉的是由外部的系統判定,他認為這個已經掛了另外一個還活著會把另外一個提升為管理者,這樣會作為一個很好的結合,但是開源之后發現別的公司不愿意自己再寫一個和自己的運維系統結合調用我們的API,可能后面要做一個新的模塊把這個事情自動做了,因為對很多的公司來說,他們還是沒有精力開發自己的運維系統調用我們的API的,但是這樣的一個設計確實讓整個系統變的非常的可控。
還有一個就是分布式系統里面的當前系統的狀態,是很糾結的問題,怎么確定當前整個集群的狀態是什么樣?是不是穩定的?有哪些掛的哪些沒有掛的怎么拿到一致性的視圖?這個是很糾結的事情,然后我們在這個地方考慮的方案就是說,所有的這個狀態的變更在ZK上面統統都是有記錄的,只要是我們能夠訪問ZK整個狀態都是清楚的。當前的狀態是通過P2P的做慢慢的同步,很難拿到一致的狀態,所以我們在這個設計上面就把這個很多的信息放在ZK上面,比如說SLOT的狀態,Proxy的狀態,group、lock的狀態。你所有的行為就是比如說這個時候做擴容的行為,一個命令,ZK上面也有歷史記錄,這樣一來有一個好處,只要能看到ZK你這個行為是可以復現出來的,就相當于ZK做了一個很好分布式的 LOCK,本身擴容和縮容的行為很少,所以用這個ZK控制是很好的。
記得去年的時候和剛剛上一場的張虎同學聊了一下,到底是proxy的還是 smart,如果是性能的話會選擇一個smart client,有一點惡心的是,smart client會發布自己的客戶端,你發現自己的客戶端有BUG找他升級的時候就比較痛苦,別人不想升級,而且你這個升級的時候又要重新發布。反正是根本看不到過程,因為全部是透明的,還有一個就是說基于proxy監控很好做,任何時候要禁掉某一條指令這個很好做,但是你的smart client發布出去就沒有辦法禁止了,不會經過你這邊,另外一個就是說后端信息這個是不暴露的,現在給你一個集群后面有多少臺是不知道的,你也不應該關心,我告訴你總容量有100g,200g,現在需要擴到300g,你不需要關心后面任何的細節。
這個是一個架構圖,顯示的效果有點問題,我大概說一下,最上面的話是直接可以用 redis-client,中間是做一個負載均衡,你也可以直接去連我們其中任何的一個proxy,至少有一個管理者,如果你對業務的要求沒有那么高,可以不配slave,建議一個Group一個maste,一個Slave。
這個是正常的流程,當你一個讀寫過來的時候,首先算一下屬于那個Slot,給我們結果我們回客戶端,那么這個流程不一樣,因為大家知道這個系統里面比較難做的是一致性。
前面那個是SLOT這個有一個狀態,這個會標記成正在遷移的狀態,就有這樣的記錄,如果是從A遷移到B,那么這個請求是應該發給A還是B呢?這個時候是不知道的,先給A發一個命令要求把這個key從A遷移到B,再把請求發給B,這個K已經過去了,B給我們回一個請求,然后我們再回客戶端,這樣數據是一致的,這個中間需要保證的就是說遷移這個操作必須是原子的,你如果還有別的并發的操作是有問題的,但是redis是很好的保證這一點,你做這個遷移的時候必須遷移完成之后才會處理下一條命令。
我們把Slot狀態標記為pre_migrate,然后再標記成正在遷移,大家看這三個是很標準的一個2PC的基本的流程,然后我們后臺會有一個遷移進程不斷的發送這個命令,把這個遷移到另外一個機器上去,直到遷移完了然后再走正常的流程,如果沒有就走剛剛的遷移流程。
我們詳細細節,這個中間必須要求所有的Proxies都要響應然后提交,這個沒有用到傳統的比如說我是一個管理程序給Proxies一角,我們會創建一個結點,會描述你的信息,有哪些需要響應,這個Proxies會監聽這個結點的變化,發現 需要自己會去響應,于是在原來的結點再創建子結點,表示自己響應,自己響應的狀態是什么。然后這樣的好處就是說我們剛剛說的任何時候如果這個程序掛了,那這個時候在上面看就知道你做到那個一階段掛掉了,到底什么程度沒有響應?然后下面就是一個原子操作,我們就給redis打了幾個很小的,大概幾百行的代碼,這個也是很經典的2PC的過程。
然后這個是codis-redis和原生的redis有甚么差別,這個KEY是怎么操作的,這個會計算,然后就可以跟遷移SLOT的時候,就是根據這個查找他原來的有那些KEY需要遷移然后會隨機遷移。這個是我們在ZK上面的結構,大家有興趣可以用這個客戶端連一下看一下他的結構是什么,然后這個是我們內部的一些事例,這個是Applist的業務,這個是在N是group的ID,后面會跟一個詳細的server為addr,同時存儲一個信息描述一個更詳細的情況,大家可以直接ls一下就可以看到詳細的信息,這里有一個詳細的事例,路由表是什么樣的。
我們對它的修改就是這么一點,就是一個最核心的就是一個Slot的Support,內部增加了一個TAGS,比如我希望一個UID的對應的數據落到同一個 SLOT上面去,就可以用一個腳本當成這個存儲的過程記錄詳細的變化,如果沒有這個TAG的支持就不需要打,本身是DB號做一個SLOT號就可以了,我們提供了一個管理工具,這些有rebalance,直接點一下按紐就自動的做,還提供了一個Dashborrd,我們在網頁上面有展示圖片,我們現在對它的擴充都是通過API的調用做的,基本不會改變原代碼。這樣一來就是你這個東西還行,我們能夠直接用嗎?我說數據已經在Twemproxy上面了,其實是可以直接用的,我們介紹一個我們自己寫的一個東西。
CodisPort具體的實現很簡單,對于每個replication做一個數據協議,這樣直接同步會到一個redis上面去,假設你原來的集群是這樣的,一個Twemproxy后面跟了三個redis,這三個CodisProt指向三個不同的redis,業務做切換,重新指向我們新的 CodisProxy,通常是幾秒的時間,基本是中斷幾秒的時間,如果遷移到上面的話。所以整個遷移的成本是很低的。
因為中間我們提到當它訪問一個KEY的時候,正在是遷移狀態,我們會發一個 LIST,但是不要太大,如果是上億個這樣壓力會很大,遷移時間會很長,然后這個時候如果你這個時候去說我有一個100M的KEY,你遷移的時候是1點幾秒,如果是1G的KEY就沒有辦法了,基本上不會有VALUE特別大,除非是設計上有問題,當然沒有遇到這樣的場景,所以我們大概提一下,我們發現有一些用戶對Codis的功能有誤解。
所以其實我們內部在做一個版本,用來做這個事情,我們把leveldb或者是rocksdb集成過來,所以在storage為就不是一個 Migration的了,這個后面看實際的需求情況,或者是看社區的要求吧,然后我們剛剛也提出過,有一些公司單獨拿這個東西跑的沒有很完善的運維系統,也不想自己調API。
然后最后一個大家關心的問題,就是Codis和Twemproxy相比性能怎么樣,大概會慢20%左右,不同的指令級不一樣,mget指令Codis比 Twemproxy慢很多,這個原因是因為為了保證migreate,仍然是非常快,大家可以把自己的業務測試一下,如果大家的業務需要遷移到Codis 的話,需要一個什么樣流程,我建議是用先把流量倒過來,確保OK之后再做切換,當然我們這個是肯定開源了的,https://github.com /wandoulabs/codis如果你不愿意敲網址可以掃一下二維碼。
PPT:http://qiniuppt.qiniudn.com/liuqi.pdf
視頻:http://78rg1k.com1.z0.glb.clouddn.com/ecug-2014-liuqi.mov
來自:http://blog.qiniu.com/?p=871