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

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

2、選擇Codis的緣由 隨着公司業務規模的快速增加,咱們對數據量的存儲需求也愈來愈大,實踐代表,在單個Redis的節點實例下,高併發、海量的存儲數據很容易使內存出現暴漲。算法

此外,每個Redis的節點,其內存也是受限的,主要有如下兩個緣由:後端

一是內存過大,在進行數據同步時,全量同步的方式會致使時間過長,從而增長同步失敗的風險; 二是愈來愈多的redis節點將致使後期巨大的維護成本。api

所以,咱們對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是如何工做的。併發

3、Codis介紹 Codis是一個代理中間件,用GO語言開發而成。Codis 在系統的位置以下圖所示 : Codis是一個分佈式Redis解決方案,對於上層應用來講,鏈接Codis Proxy和鏈接原生的Redis Server沒有明顯的區別,有部分命令不支持;app

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

Codis分爲四個部分,分別是: Codis Proxy (codis-proxy) Codis Dashboard Codis Redis (codis-server) ZooKeeper/Etcd

Codis架構

4、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和同步操做。

5、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進行處理。

6、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,並進行相應操做。

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

Codis的不足 ⭕ 欠缺安全考慮,codis fe頁面沒有登陸驗證功能; ⭕ 缺少自帶的多租戶方案; ⭕ 缺少集羣縮容方案。

個推使用上的改進 ⭕ 採用squid代理的方式來簡單限制fe頁面的訪問,後期基於fe進行二次開發來控制登陸; ⭕ 小業務經過在key前綴增長業務標識,複用相同集羣;大業務使用獨立集羣,獨立機器; ⭕ 採用手動遷移數據、騰空節點、下線節點的方法來縮容。

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

相關文章
相關標籤/搜索