4000余字為你講透Codis內部工作原理


一、引言
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 在系統的位置如下圖所示 :

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()時內存中對應的數據結構。


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中




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循環,其中一個用於具體處理主從同步問題。

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

⭕ 寫進zk中的group信息:


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:



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的槽位變化,並及時同步槽位信息。


總結一下,啟動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予以維護 。





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目錄。


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,與大家共同探討如何在生產環境中更好地對其進行使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM