4000餘字爲你講透Codis內部工作原理

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

作者 | 個推運維工程師  無名


一、引言

Codis是一個分佈式 Redis 解決方案,可以管理數量巨大的Redis節點。個推作爲專業的第三方推送服務商,多年來專注於爲開發者提供高效穩定的消息推送服務。每天通過個推平臺下發的消息數量可達百億級別。基於個推推送業務對數據量、併發量以及速度的要求非常高,實踐發現,單個Redis節點性能容易出現瓶頸,綜合考慮各方面因素後,我們選擇了Codis來更好地管理和使用Redis。


二、選擇Codis的原因

隨着公司業務規模的快速增長,我們對數據量的存儲需求也越來越大,實踐表明,在單個Redis的節點實例下,高併發、海量的存儲數據很容易使內存出現暴漲。


此外,每一個Redis的節點,其內存也是受限的,主要有以下兩個原因:


一是內存過大,在進行數據同步時,全量同步的方式會導致時間過長,從而增加同步失敗的風險;

二是越來越多的redis節點將導致後期巨大的維護成本。


因此,我們對Twemproxy、Codis和Redis Cluster  三種主流redis節點管理的解決方案進行了深入調研。

推特開源的Twemproxy最大的缺點是無法平滑的擴縮容。而Redis Cluster要求客戶端必須支持cluster協議,使用Redis Cluster需要升級客戶端,這對很多存量業務是很大的成本。此外,Redis Cluster的p2p方式增加了通信成本,且難以獲知集羣的當前狀態,這無疑增加了運維的工作難度。


而豌豆莢開源的Codis不僅可以解決Twemproxy擴縮容的問題,而且兼容了Twemproxy,且在Redis Cluster(Redis官方集羣方案)漏洞頻出的時候率先成熟穩定下來,所以最後我們使用了Codis這套集羣解決方案來管理數量巨大的redis節點。


目前個推在推送業務上綜合使用Redis和Codis,小業務線使用Redis,數據量大、節點個數衆多的業務線使用Codis。


我們要清晰地理解Codis內部是如何工作的,這樣才能更好地保證Codis集羣的穩定運行。下面我們將從Codis源碼的角度來分析Codis的Dashboard和Proxy是如何工作的。


三、Codis介紹

Codis是一個代理中間件,用GO語言開發而成。Codis 在系統的位置如下圖所示 :

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

Codis是一個分佈式Redis解決方案,對於上層應用來說,連接Codis Proxy和連接原生的Redis Server沒有明顯的區別,有部分命令不支持;


Codis底層會處理請求的轉發、不停機的數據遷移等工作,對於前面的客戶端來說,Codis是透明的,可以簡單地認爲客戶端(client)連接的是一個內存無限大的Redis服務。


Codis分爲四個部分,分別是:

Codis  Proxy (codis-proxy)

Codis  Dashboard

Codis Redis (codis-server)

ZooKeeper/Etcd


Codis架構 


四、Dashboard的內部工作原理


Dashboard介紹

Dashboard是Codis的集羣管理工具,所有對集羣的操作包括proxy和server的添加、刪除、數據遷移等都必須通過dashboard來完成。Dashboard的啓動過程是對一些必要的數據結構以及對集羣的操作的初始化。


Dashboard啓動過程

Dashboard啓動過程,主要分爲New()和Start()兩步。


New()階段

⭕ 啓動時,首先讀取配置文件,填充config信息。coordinator的值如果是"zookeeper"或者是"etcd",則創建一個zk或者etcd的客戶端。根據config創建一個Topom{}對象。Topom{}十分重要,該對象裏面存儲了集羣中某一時刻所有的節點信息(slot,group,server等),而New()方法會給Topom{}對象賦值。


⭕ 隨後啓動18080端口,監聽、處理對應的api請求。


⭕ 最後啓動一個後臺線程,每隔一分鐘清理pool中無效client。


下圖是dashboard在New()時內存中對應的數據結構。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


Start()階段


⭕ Start()階段,將內存中model.Topom{}寫入zk,路徑是/codis3/codis-demo/topom。


⭕ 設置topom.online=true。


⭕ 隨後通過Topom.store從zk中重新獲取最新的slotMapping、group、proxy等數據填充到topom.cache中(topom.cache,這個緩存結構,如果爲空就通過store從zk中取出slotMapping、proxy、group等信息並填充cache。不是隻有第一次啓動的時候cache會爲空,如果集羣中的元素(server、slot等等)發生變化,都會調用dirtyCache,將cache中的信息置爲nil,這樣下一次就會通過Topom.store從zk中重新獲取最新的數據填充。)


⭕ 最後啓動4個goroutine for循環來處理相應的動作 。


創建group過程

創建分組的過程很簡單。

⭕ 首先,我們通過Topom.store從zk中重新拉取最新的slotMapping、group、proxy等數據填充到topom.cache中。


⭕ 然後根據內存中的最新數據來做校驗:校驗group的id是否已存在以及該id是否在1~9999這個範圍內。


⭕ 接着在內存中創建group{}對象,調用zkClient創建路徑/codis3/codis-demo/group/group-0001。


初始,這個group下面是空的。








{    "id": 1,    "servers": [],    "promoting": {},    "out_of_sync": false}



添加codis server

⭕接下來,向group中添加codis server。Dashboard首先會去連接後端codis server,判斷節點是否正常。


⭕ 接着在codis  server上執行slotsinfo命令,如果命令執行失敗則會導致cordis server添加進程的終結。


⭕ 之後,通過Topom.store從zk中重新拉取最新的slotMapping、group、proxy等數據填充到topom.cache中,根據內存中的最新數據來做校驗,判斷當前group是否在做主從切換,如果是,則退出;然後檢查group server在zk中是否已經存在。


⭕ 最後,創建一個groupServer{}對象,寫入zk。

當codis  server添加成功後,就像我們上面說的,Topom{}在Start時,有4個goroutine  for循環,其中RefreshRedisStats()就可以將codis server的連接放進topom.stats.redisp.pool中


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


tips

⭕ Topom{}在Start時,有4個goroutine for循環,其中RefreshRedisStats執行過程中會將codis server的連接放進topom.stats.redisp.pool中;


⭕ RefreshRedisStats()每秒執行一次,裏面的邏輯是從topom.cache中獲取所有的codis server,然後根據codis server的addr 去topom.stats.redisp.Pool.pool 裏面獲取client。如果能取到,則執行info命令;如果不能取到,則新建一個client,放進pool中,然後再使用client執行info命令,並將info命令執行的結果放進topom.stats.servers中。


Codis Server主從同步

當一個group添加完成2個節點後,要點擊主從同步按鈕,將第二個節點變成第一個的slave節點。


⭕ 首先,第一步還是刷新topom.cache。我們通過Topom.store從zk中重新獲取最新的slotMapping、group、proxy等數據並把它們填充到topom.cache中。


⭕然後根據最新的數據進行判斷:group.Promoting.State != models.ActionNothing,說明當前group的Promoting不爲空,即 group裏面的兩個cordis server在做主從切換,主從同步失敗;


group.Servers[index].Action.State       == models.ActionPending,說明當前作爲salve角色的節點,其狀態爲pending,主從同步失敗;


⭕ 判斷通過後,獲取所有codis server狀態爲ActionPending的最大的action.index的值+1,賦值給當前的codis server,然後設置當前作爲slave角色的節點的狀態爲:g.Servers[index].Action.State = models.ActionPending。將這些信息寫進zk。


⭕ Topom{}在Start時,有4個goroutine for循環,其中一個用於具體處理主從同步問題。


⭕ 頁面上點擊主從同步按鈕後,內存中對應的數據結構會發生相應的變化:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

⭕ 寫進zk中的group信息:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


tips


Topom{}在Start時,有4個goroutine for循環,其中一個便用於具體來處理主從同步。具體怎麼做呢?


首先,通過Topom.store從zk中重新獲取最新的slotMapping、group、proxy等數據填充到topom.cache中,待得到最新的cache數據後,獲取需要做主從同步的group server,修改group.Servers[index].Action.State        == models.ActionSyncing,寫入zk中。


其次,dashboard連接到作爲salve角色的節點上,開啓一個redis事務,執行主從同步命令:






c.Send(“MULTI”)        —> 開啓事務c.Send(“config”, “set”, “masterauth”, c.Auth)c.Send(“slaveof”, host, port)




c.Send(“config”, “rewrite")c.Send(“client”, “kill”, “type”, “normal")c.Do(“exec”)               —> 事物執行

⭕ 主從同步命令執行完成後,修改group.Servers[index].Action.State == “synced”並將其寫入zk中。至此,整個主從同步過程已經全部完成。


codis server在做主從同步的過程中,從開始到完成一共會經歷5種狀態:








""(ActionNothing)      --> 新添加的codis,沒有主從關係的時候,狀態爲空pending(ActionPending) --> 頁面點擊主從同步之後寫入zk中syncing(ActionSyncing) --> 後臺goroutine for循環處理主從同步時,寫入zk的中間狀態synced                 --> goroutine for循環處理主從同步成功後,寫入zk中的狀態synced_failed       --> goroutine for循環處理主從同步失敗後,寫入zk中的狀態



slot分配

上文給Codis集羣添加了codis server,做了主從同步,接下來我們把1024個slot分配給每個codis server。Codis給使用者提供了多種方式,它可以將指定序號的slot移到某個指定group,也可以將某個group中的多個slot移動到另一個group。不過,最方便的方式是自動rebalance。


通過Topom.store我們首先從zk中重新獲取最新的slotMapping、group、proxy等數據填充到topom.cache中,再根據cache中最新的slotMapping和group信息,生成slots分配計劃 plans = {0:1, 1:1, … , 342:3, …, 512:2, …, 853:2, …, 1023:3},其中key 爲 slot id,      value 爲 group id。接着,我們按照slots分配計劃,更新slotMapping信息:Action.State =      ActionPending和Action.TargetId = slot分配到的目標group id,並將更新的信息寫回zk中。


Topom{}在Start時,有4個goroutine   for循環,其中一個用於處理slot分配。


SlotMapping: 

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


tips

● Topom{}在Start時,有4個goroutine       for循環,其中ProcessSlotAction執行過程中就將codis server的連接放進topom.action.redisp.pool中了。


● ProcessSlotAction()每秒執行一次,待裏面的一系列處理邏輯執行之後,它會從topom{}.action.redisp.Pool.pool中獲取client,隨後在redis上執行SLOTSMGRTTAGSLOT命令。如果client能取到,則dashboard會在redis上執行遷移命令;如果不能取到,則新建一個client,放進pool中,然後再使用client執行遷移命令。


SlotMapping中action對應的7種狀態:

我們知道Codis是由ZooKeeper來管理的,當Codis的Codis Dashbord改變槽位信息時,其他的Codis Proxy節點會監聽到ZooKeeper的槽位變化,並及時同步槽位信息。 

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


總結一下,啓動dashboard過程中,需要連接zk、創建Topom這個struct,並通過18080這個端口與集羣進行交互,然後將該端口收到的信息進行轉發。此外,還需要啓動四個goroutine、刷新集羣中的redis和proxy的狀態,以及處理slot和同步操作。


五、Proxy的內部工作原理


proxy啓動過程

proxy啓動過程,主要分爲New()、Online()、reinitProxy()和接收客戶端請求()等4個環節。


New()階段

⭕ 首先,在內存中新建一個Proxy{}結構體對象,並進行各種賦值。

⭕ 其次,啓動11080端口和19000端口。

⭕ 然後啓動3個goroutine後臺線程,處理對應的操作:

●Proxy啓動一個goroutine後臺線程,並對11080端口的請求進行處理;

●Proxy啓動一個goroutine後臺線程,並對19000端口的請求進行處理;

●Proxy啓動一個goroutine後臺線程,通過ping codis server對後端bc予以維護 。


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


Online()階段

⭕ 首先對model.Proxy{}的id進行賦值,Id = ctx.maxProxyId() +      1。若添加第一個proxy時,      ctx.maxProxyId() = 0,則第一個proxy的id 爲 0 + 1。


⭕ 其次,在zk中創建proxy目錄。


⭕之後,對proxy內存數據進行刷新reinitProxy(ctx, p, c)。


⭕ 第四,設置如下代碼:

online = true

proxy.online = true

router.online = true

jodis.online = true


⭕ 第五,zk中創建jodis目錄。


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

reinitProxy()

⭕Dashboard從zk[m1] 中重新獲取最新的slotMapping、group、proxy等數據填充到topom.cache中。根據cache中的slotMapping和group數據,Proxy可以得到model.Slot{},其裏面包含了每個slot對應後端的ip與port。建立每個codis server的連接,然後將連接放進router中。


⭕ Redis請求是由sharedBackendConn中取出的一個BackendConn進行處理的。Proxy.Router中存儲了集羣中所有sharedBackendConnPool和slot的對應關係,用於將redis的請求轉發給相應的slot進行處理,而Router裏面的sharedBackendConnPool和slot則是通過reinitProxy()來保持最新的值。


總結一下proxy啓動過程中的流程。首先讀取配置文件,獲取Config對象。其次,根據Config新建Proxy,並填充Proxy的各個屬性。這裏面比較重要的是填充models.Proxy(詳細信息可以在zk中查看),以及與zk連接、註冊相關路徑。


隨後,啓動goroutine監聽11080端口的codis集羣發過來的請求並進行轉發,以及監聽發到19000端口的redis請求並進行相關處理。緊接着,刷新zk中數據到內存中,根據models.SlotMapping和group在Proxy.router中創建1024個models.Slot。此過程中Router爲每個Slot都分配了對應的backendConn,用於將redis請求轉發給相應的slot進行處理。


六、Codis內部原理補充說明

Codis中key的分配算法是先把key進行CRC32,得到一個32位的數字,然後再hash%1024後得到一個餘數。這個值就是這個key對應着的槽,這槽後面對應着的就是redis的實例。 

slot共有七種狀態:nothing(用空字符串表示)、pending、preparing、prepared、migrating、finished。


如何保證slots在遷移過程中不影響客戶端的業務?

⭕ client端把命令發送到proxy, proxy會算出key對應哪個slot,比如30,然後去proxy的router裏拿到Slot{},內含backend.bc和migrate.bc。如果migrate.bc有值,說明slot目前在做遷移操作,系統會取出migrate.bc.conn(後端codis-server連接),並在codis       server上強制將這個key遷移到目標group,隨後取出backend.bc.conn,訪問對應的後端codis server,並進行相應操作。


七、Codis的不足與個推使用上的改進


Codis的不足

⭕ 欠缺安全考慮,codis fe頁面沒有登錄驗證功能;

⭕ 缺乏自帶的多租戶方案;

⭕ 缺乏集羣縮容方案。


個推使用上的改進

⭕ 採用squid代理的方式來簡單限制fe頁面的訪問,後期基於fe進行二次開發來控制登錄;

⭕ 小業務通過在key前綴增加業務標識,複用相同集羣;大業務使用獨立集羣,獨立機器;

⭕ 採用手動遷移數據、騰空節點、下線節點的方法來縮容。


八、全文總結

Codis作爲個推消息推送一項重要的基礎服務,性能的好壞至關重要。個推將Redis節點遷移到Codis後,有效地解決了擴充容量和運維管理的難題。未來,個推還將繼續關注Codis,與大家共同探討如何在生產環境中更好地對其進行使用。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章