幾個方面談一下Nacos的設計(作為注冊中心,基於此時的develop分支)
原創博文,轉載請注明來源
客戶端與集群的交互
首先需要聲明的是Nacos Cluster雖然內部使用了Raft協議但是對於Nacos客戶端,Cluster實例是無狀態的。客戶端配置集群地址有兩種方式:
1.通過配置serverAddr列表,客戶端將訪問集群時,隨機從列表中選擇一個實例訪問:
NamingService configService = NacosFactory.createNamingService("10.22.0.137:30253,10.22.0.137:30254,10.22.0.137:30255");
當然,一般情況下我們並不會直接配置Nacos實例的IP,可用用域名,以便能動態發現。
2.通過Properties配置endpoint,定時訪問,感知集群變化,並隨機從接口返回的列表中選擇一個實例訪問,客戶端會與Endpoint創建LONG PULL。
Properties properties = new Properties();
properties.put(PropertyKeyConst.ENDPOINT,"10.18.90.16");
properties.put(PropertyKeyConst.ENDPOINT_PORT,"8850");
NamingService configService = NacosFactory.createNamingService(properties);
數據同步
實例信息同步
實例信息的由一個叫 Distro (com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl)的一致性協議維護,有如下幾個特點:
- 最終一致性,由實例間通過http同步(com.alibaba.nacos.naming.consistency.ephemeral.distro.TaskDispatcher.TaskScheduler)到除了自己的每個實例節點,且攜帶自己全量數據
- 數據不持久化,保存在內存(com.alibaba.nacos.naming.consistency.ephemeral.distro.DataStore)
- 每個節點通過算法,只接受一部分請求(com.alibaba.nacos.naming.web.DistroFilter),如果不屬於實例自己的請求過來則通過算法確定並轉發。
舉例解釋一下第3點:現有Nacos集群實例A,B,C 共3個。從客戶端與集群的交互知道,客戶端隨機從A,B,C中隨機選擇一個實例訪問,客戶端NACOS-DEMO選擇訪問B的注冊實例接口,如果NACOS-DEMO的請求應該屬於C處理的話,本次請求將會被B實例中的DistroFilter攔截掉,並由B轉發到C。理解起來,其實挺繞的,但是為什么這么設計呢?畢竟A,B,C的實例數據都會最終一致的,我隨機訪問任意一個實例就好了
我的解釋是:
由於數據是最終一致的,中間會存在同步過程,所以如果存在寫了馬上查的場景,則很有可能查不到的情況(客戶端寫和查兩次請求落在了兩個不同的實例)。但是如果通過算法,一個實例的增刪改查都在同一個確定的實例,就不會出現這種情況了。
服務集群信息
- 通過Open API服務創建接口創建,則直接通過raft協議 持久化到集群內所有節點
- 如果實例注冊時,如果服務集群不存在,則靜默創建,前面說了,實例注冊時,會通過Distro 協議通過實例數據到集群內其它節點,由於實例信息也附帶了服務集群等信息,所以實例上也順便同步了服務集群等信息。所以,默認情況下,靜默創建的服務集群等信息也是沒有持久化的。當實例注冊時ephemeral字段設置為false,除了上訴的同步方式外,還會調用raft協議,該協議會同步到其它節點並持久化(Raft協議中規定,只有leader才能處理客戶端請求,所有當發現自己不是leader時,會轉發請求,實現如下圖所示):
ephemeral字段的介紹
(Nacos 在 1.0.0版本 instance級別增加了一個ephemeral字段,該字段表示注冊的實例是否是臨時實例還是持久化實例。如果是臨時實例,則不會在 Nacos 服務端持久化存儲,需要通過上報心跳的方式進行包活,如果一段時間內沒有上報心跳,則會被 Nacos 服務端摘除。在被摘除后如果又開始上報心跳,則會重新將這個實例注冊。持久化實例則會持久化被 Nacos 服務端,此時即使注冊實例的客戶端進程不在,這個實例也不會從服務端刪除,只會將健康狀態設為不健康)
關於priv-raft協議
Raft協議第8節部分內容:
Clients of Raft send all of their requests to the leader.
When a client first starts up, it connects to a randomlychosen
server. If the client’s first choice is not the leader,
that server will reject the client’s request and supply information
about the most recent leader it has heard from
(AppendEntries requests include the network address of
the leader). If the leader crashes, client requests will time
out; clients then try again with randomly-chosen servers
Raft的客戶將所有請求發送給leader。當客戶機第一次啟動時,
它連接到隨機選擇的服務器。如果客戶機的首選不是leader,
服務器將拒絕客戶機的請求,並提供它最近聽到的leader的信息。
如果leader崩潰,客戶端請求將超時;然后,客戶端再次嘗試隨機選擇的服務器
Nacos官網說,自己實現的事一個輕量級的raft協議,原因我認為至少有如下兩點:
- 客戶端是隨機選擇並訪問Nacos實例的
- Nacos實例分為leader和flower,leader負責寫入,leader和flower都可以查詢,標准的raft協議,客戶端所有請求會發送給leader保證強一致性,nacos的實現是最終一致性。
Nacos集群在k8s中的實踐
Nacos在VM環境下,部署集群就比較簡單了,如下圖:
只需要在部署Nacos 實例時,在conf/Cluster.conf 中把自己和集群內其它實例的地址整合到一起即可,比如上圖的架構,Cluster.conf文件可以是這樣:
ip1:8848
ip2:8848
ip3:8848
這樣就構成了集群。Nacos實例會輪詢Cluster.conf 文件,以保證集群在有新的實例加入時能相互發現以實現實例的擴縮容,具體代碼實現在(com.alibaba.nacos.naming.cluster.ServerListManager.ServerListUpdater)。
所以在k8s環境下Nacos的集群應該怎么玩?毋庸置疑的是,我們不再需要手動去更改Cluster.conf文件來維護集群,必須使用k8s的機制去完成自動擴縮容,那么問題來了,k8s完成擴縮容,,集群自己怎么知道呢?
其實是peer-finder-plugin 和 上面說的自動輪詢Cluster.conf機制來達到的。
分析:
通過Helm使用官方的chart包部署的nacos集群,每一個pod有兩個容器:
- peer-finder-plugin-install : 是一個initContainers ,這個鏡像唯一的作用是安裝 peer-finder插件相關的腳本到指定目錄(plugins/peer-finder/),結束后就死亡。
- nacos-cluster:nacos本體鏡像,鏡像啟動時,除了啟動自身的服務外,還會同時調用peer-finder-plugin-install 安裝好的腳本啟動peer-finder(nacos dockerfile)
peer-finder這個插件很簡單,使用go語言寫的一個定時輪詢的程序,核心代碼:
peer-finder輪詢(1秒)指定的k8s service ,如果service下面的pod地址列表發生變化,則重新寫入Cluster.conf文件。這里的k8s service就必須是headless類型的了,因為只有解析headless提供的域名,才能獲取所有pod的地址列表。
pod啟動時序圖:
調用關系圖: