
聯席作者:吳毅挺 任浩軍 童子龍
鄭重鳴謝:Nacos - 彥林,Spring Cloud Alibaba - 小馬哥、洛夜,Nacos 社群 - 張龍(pader)、春少(chuntaojun)
相關文章推薦:
前言
在高速發展的時候,公司規模越來越大,老師人數越來越多,這時候公司不能鋪太多人去做營運與服務,必須提高每個人效,這就需要技術驅動。是以掌門教育轉變成一家技術驅動型的公司,如果被迫成為一家靠資金驅動的公司就活不下去了。
-- 張翼(掌門教育創始人兼 CEO)
掌門教育自 2014 年正式轉型線上教育以來,秉承“讓教育共享智能,讓學習高效快樂”的宗旨和願景,經曆雲計算、大資料、人工智能、
AR
/
VR
MR
以及現今最火的
5G
,一直堅持用科技賦能教育。掌門教育的業務近幾年得到了快速發展,特别是今年的疫情,使線上教育成為了新的風口,也給掌門教育新的機遇。
随着業務規模進一步擴大,流量進一步暴增,微服務數目進一步增長,使老的微服務體系所采用的注冊中心
Eureka
不堪重負,同時
Spring Cloud
體系已經演進到第二代,第一代的
Eureka
注冊中心已經不大适合現在的業務邏輯和規模,同時它目前被
Spring Cloud
官方置于維護模式,将不再向前發展。如何選擇一個更為優秀和适用的注冊中心,這個課題就擺在了掌門人的面前。經過對
Alibaba Nacos
、
HashiCorp Consul
等開源注冊中心做了深入的調研和比較,最終標明
Alibaba Nacos
做微服務體系
Solar
中的新注冊中心。
背景故事
兩次
Eureka
引起業務服務大面積崩潰後,雖然通過更新硬體和優化配置參數的方式得以解決,
Eureka
伺服器目前運作平穩,但我們依舊擔心此類事故在未來會再次發生,最終選擇落地
Alibaba Nacos
作為掌門教育的新注冊中心。
Nacos 開發篇
Nacos Eureka Sync 方案演進
① Sync 官方方案
經過研究,我們采取了官方的
Nacos Eureka Sync
方案,在小範圍試用了一下,效果良好,但一部署到
FAT
環境後,發現根本不行,一台同步伺服器無法抗住将近 660 個服務(非執行個體數)的頻繁心跳,同時該方案不具備高可用特點。
② Sync 高可用一緻性 Hash + Zookeeper 方案
既然一台不行,那麼就多幾台,但如何做高可用呢?
我們率先想到的是一緻性
Hash
方式。當一台或者幾台同步伺服器挂掉後,采用
Zookeeper
臨時節點的
Watch
機制監聽同步伺服器挂掉情況,通知剩餘同步伺服器執行
reHash
,挂掉服務的工作由剩餘的同步伺服器來承擔。通過一緻性
Hash
實作被同步的業務服務清單的平均配置設定,基于對業務服務名的二進制轉換作為
Hash
的
Key
實作一緻性
Hash
的算法。我們自研了這套算法,發現平均配置設定的很不理想,第一時間懷疑是否算法有問題,于是找來
Kafka
自帶的算法(見
Utils.murmur2
),發現效果依舊不理想,原因還是業務服務名的本身分布就是不平均的,于是又回到自研算法上進行了優化,基本達到預期,下文會具體講到。但說實話,直到現在依舊無法做到非常良好的絕對平均。
③ Sync 高可用主備 + Zookeeper 方案
這個方案是個小插曲,當一台同步伺服器挂掉後,由它的“備”頂上,當然主備切換也是基于
Zookeeper
Watch
機制來實作的。後面讨論下來,主備方案,機器的成本很高,實作也不如一緻性
Hash
優雅,最後沒采用。
④ Sync 高可用一緻性 Hash + Etcd 方案
折騰了這麼幾次後,發現同步業務服務清單是持久化在資料庫,同步伺服器挂掉後
reHash
通知機制是由
Zookeeper
來負責,兩者能否可以合并到一個中間件上以降低成本?于是我們想到了
Etcd
方案,即通過它實作同步業務服務清單持久化 + 業務服務清單增減的通知 + 同步伺服器挂掉後
reHash
通知。至此方案最終确定,即兩個注冊中心(
Eureka
和
Nacos
)的雙向同步方案,通過第三個注冊中心(
Etcd
)來做橋梁。
⑤ Sync 業務服務名清單定時更新優化方案
解決了一緻性
Hash
的問題後,還有一個潛在風險,即官方方案每次定時同步業務服務的時候,都會去讀取全量業務服務名清單,對于業務服務數較少的場景應該沒問題,但對于我們這種場景下,這麼頻繁的全量去拉業務服務清單,會不會對
Nacos
伺服器的性能有所沖擊呢?接下去我們對此做了優化,取消全量定時讀取業務服務名清單,通過
DevOps
的釋出系統平台實施判斷,如果是遷移過來的業務服務或者新上
Nacos
的業務服務,由釋出平台統一調用
Nacos
接口來增加新的待同步業務服務
Job
,當該業務服務全部遷移完畢後,在官方同步界面上删除該同步業務服務
Job
即可。
⑥ Sync 伺服器兩次擴容
方案實作後,上了
FAT
環境上後沒發現問題(此環境,很多業務服務隻部署一個執行個體),而在
PROD
環境上發現存在雙向同步丢心跳的問題,原因是同步伺服器來不及執行排隊的心跳線程,導緻
Nacos
伺服器無法及時收到心跳而把業務服務踢下來。我們從 8 台
4C8G
同步伺服器擴容到 12 台,情況好了很多,但觀察下來,還是存在一天記憶體在一些業務服務丢失心跳的情況,于是我們再次從 12 台
4C8G
同步伺服器擴容到 20 台,情況得到了大幅改善,但依舊存在某個同步伺服器上個位數丢失心跳的情況,觀察下來,那台同步伺服器承受的某幾個業務服務的執行個體數特别多的情況,我們在那台同步伺服器調整了最大同步線程數,該問題得到了修複。我們将繼續觀察,如果該問題仍舊複現,不排除更新機器配置到
8C16G
來確定
PROD
環境的絕對安全。
至此,經過 2 個月左右的努力付出,
Eureka
Nacos
同步運作穩定,
PROD
環境上同步将近 660 個服務(非執行個體數),情況良好。
非常重要的提醒:一緻性 Hash 的虛拟節點數,在所有的 Nacos Sync Server 上必須保持一緻,否則會導緻一部分業務服務同步的時候會被遺漏。
Nacos Eureka Sync 落地實踐
① Nacos Eureka Sync 目标原則
- 注冊中心遷移目标
- 過程并非一蹴而就的,業務服務逐漸遷移的過程要保證線上調用不受影響,例如,
業務服務注冊到A
上,Eureka
業務服務遷移到B
,Nacos
業務服務和A
業務服務的互相調用必須正常;B
- 過程必須保證雙注冊中心都存在這兩個業務服務,并且目标注冊中心的業務服務執行個體必須與源注冊中心的業務服務執行個體數目和狀态保持實時嚴格一緻。
- 過程并非一蹴而就的,業務服務逐漸遷移的過程要保證線上調用不受影響,例如,
- 注冊中心遷移原則
- 一個業務服務隻能往一個注冊中心注冊,不能同時雙向注冊;
- 一個業務服務無論注冊到
或者Eureka
,最終結果都是等效的;Nacos
- 一個業務服務在絕大多數情況下,一般隻存在一個同步任務,如果是注冊到
的業務服務需要同步到Eureka
,那就有一個Nacos
的同步任務,反之亦然。在平滑遷移中,一個業務服務一部分執行個體在Eureka -> Nacos
上,另一部分執行個體在Eureka
上,那麼會産生兩個雙向同步的任務;Nacos
- 一個業務服務的同步方向,是根據業務服務執行個體中繼資料(
)的标記Metadata
來決定。syncSource
② Nacos Eureka Sync 問題痛點
-
同步節點需要代理業務服務執行個體和Nacos Eureka Sync
間的心跳上報。Nacos Server
将心跳上報請求放入隊列,以固定線程消費,一個同步業務服務節點處理的服務執行個體數超過一定的門檻值會造成業務服務執行個體的心跳發送不及時,進而造成業務服務執行個體的意外丢失。Nacos Eureka Sync
-
節點當機,上面處理的心跳任務會全部丢失,會造成線上調用大面積失敗,後果不堪設想。Nacos Eureka Sync
-
已經開始工作的時候,從Nacos Eureka Sync
Eureka
上,新上線或者下線一個業務服務(非執行個體),都需要讓Nacos
實時感覺。Nacos Eureka Sync
③ Nacos Eureka Sync 架構思想
- 從各個注冊中心擷取業務服務清單,初始化業務服務同步任務清單,并持久化到
叢集中;Etcd
- 後續遷移過程增量業務服務通過
接口持久化到API
叢集中,業務服務遷移過程整合Etcd
釋出平台。整個遷移過程全自動化,規避人為操作造成的遺漏;DevOps
- 同步服務訂閱
叢集擷取任務清單,并監聽同步叢集的節點狀态;Etcd
- 同步服務根據存活節點的一緻性
算法,找到處理任務節點,後端接口通過Hash
負載均衡,删除任務指令輪詢到的節點。如果是自己處理任務則移除心跳,否則找到處理節點,代理出去;SLB
- 同步服務監聽源注冊中心每個業務服務執行個體狀态,将正常的業務服務執行個體同步到目标注冊中心,保證雙方注冊中心的業務服務執行個體狀态實時同步;
- 業務服務所有執行個體從
到Eureka
後,需要業務部門通知基礎架構部手動從Nacos
同步界面摘除該同步任務。Nacos Eureka Sync
④ Nacos Eureka Sync 方案實作
基于官方的
Nacos Sync
做任務分片和叢集高可用,目标是為了支援大規模的注冊叢集遷移,并保障在節點當機時,其它節點能快速響應,轉移故障。技術點如下,文中隻列出部分源碼或者以僞代碼表示:
詳細代碼,請參考: https://github.com/zhangmen-tech/nacos
服務一緻性 Hash
分片路由
Hash
- 根據如圖 1 多叢集部署,為每個節點設定可配置的虛拟節點數,使其在
環上能均勻分布;Hash
// 虛拟節點配置
sync.consistent.hash.replicas = 1000;
// 存儲虛拟節點
SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
// 循環添加所有節點到容器,建構Hash環
replicas for loop {
// 為每個實體節點設定虛拟節點
String nodeStr = node.toString().concat("##").concat(Integer.toString(replica));
// 根據算法計算出虛拟節點的Hash值
int hashcode = getHash(nodeStr);
// 将虛拟節點放入Hash環中
circle.put(hashcode, node);
}
// 異步監聽節點存活狀态
etcdManager.watchEtcdKeyAsync(REGISTER_WORKER_PATH, true, response -> {
for (WatchEvent event : response.getEvents()) {
// 删除事件,從記憶體中剔除此節點及Hash中虛拟節點
if (event.getEventType().equals(WatchEvent.EventType.DELETE)) {
String key = Optional.ofNullable(event.getKeyValue().getKey()).map(bs -> bs.toString(Charsets.UTF_8)).orElse(StringUtils.EMPTY);
//擷取Etcd中心跳丢失的節點
String[] ks = key.split(SLASH);
log.info("{} lost heart beat", ks[3]);
// 自身節點不做判斷
if (!IPUtils.getIpAddress().equalsIgnoreCase(ks[3])) {
// 監聽心跳丢失,更顯存貨節點緩存,删除Hash環上節點
nodeCaches.remove(ks[3]);
try {
// 心跳丢失,清除etcd上該節點的處理任務
manager.deleteEtcdValueByKey(PER_WORKER_PROCESS_SERVICE.concat(SLASH).concat(ks[3]), true);
} catch (InterruptedException e) {
log.error("clear {} process service failed,{}", ks[3], e);
} catch (ExecutionException e) {
log.error("clear {} process service failed,{}", ks[3], e);
}
}
}
}
- 根據業務服務名的
算法計算每個業務服務的哈希值,計算該FNV1_32_HASH
值順時針最近的節點,将任務代理到該節點。Hash
// 計算任務的Hash值
int hash = getHash(key.toString());
if (!circle.containsKey(hash)) {
SortedMap<Integer, T> tailMap = circle.tailMap(hash);
// 找到順勢針最近節點
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
// 得到Hash環中的節點位置
circle.get(hash);
// 判斷任務是否自己的處理節點
if (syncShardingProxy.isProcessNode(taskDO.getServiceName())) {
//如果任務屬于該節點,則進行心跳同步處理
processTask(Task);
}
// 删除心跳同步任務
if (TaskStatusEnum.DELETE.getCode().equals(taskUpdateRequest.getTaskStatus())) {
// 通過Etcd存活節點的一緻性Hash算法,擷取此任務所在的處理節點
Node processNode = syncShardingProxy.fetchProcessNode(Task);
if (processNode.isMyself()) {
// 如果是自己的同步任務,釋出删除心跳事件
eventBus.post(new DeleteTaskEvent(taskDO));
} else {
// 如果是其他節點,則通過Http代理到此節點處理
httpClientProxy.deleteTask(targetUrl,task);
}
}
同步節點當機故障轉移
- 節點監聽:監聽其它節點存活狀态,配置
叢集租約Etcd
TTL
内至少發送 5 個續約心跳以保證一旦出現網絡波動避免造成節點丢失;TTL
// 心跳TTL配置
sync.etcd.register.ttl = 30;
// 擷取租約TTL配置
String ttls = environment.getProperty(ETCD_BEAT_TTL);
long ttl = NumberUtils.toLong(ttls);
// 擷取租約ID
long leaseId = client.getLeaseClient().grant(ttl).get().getID();
PutOption option = PutOption.newBuilder().withLeaseId(leaseId).withPrevKV().build();
client.getKVClient().put(ByteSequence.from(key, UTF_8), ByteSequence.from(value, UTF_8), option).get();
long delay = ttl / 6;
// 定時續約
scheduledExecutorService.schedule(new BeatTask(leaseId, delay), delay, TimeUnit.SECONDS);
// 續約任務
private class BeatTask implements Runnable {
long leaseId;
long delay;
public BeatTask(long leaseId, long delay) {
this.leaseId = leaseId;
this.delay = delay;
}
public void run() {
client.getLeaseClient().keepAliveOnce(leaseId);
scheduledExecutorService.schedule(new BeatTask(this.leaseId, this.delay), delay, TimeUnit.SECONDS);
}
}
- 節點當機:其中某個節點當機,其任務轉移到其它節點,因為有虛拟節點的緣故,是以此節點的任務會均衡
到其它節點,那麼,叢集在任何時候,任務處理都是分片均衡的,如圖 2 中,ReSharding
節點當機,B
##1
虛拟節點的任務會分别轉移到##2
C
節點,這樣避免一個節點承擔當機節點的所有任務造成剩餘節點連續雪崩;A
- 節點恢複:如圖 3,節點的虛拟節點重新添加到
環中,Hash
規則變更,恢複的節點會根據新的Sharding
環規則承擔其它節點的一部分任務。心跳任務一旦在節點産生都不會自動消失,這時需要清理其它節點的多餘任務(即重新配置設定給複蘇節點的任務),給其它節點減負(這一步非常關鍵,不然也可能會引發叢集的連續雪崩),保障叢集恢複到最初正常任務同步狀态;Hash
// 找到此節點處理的心跳同步任務
Map<String, FinishedTask> finishedTaskMap = skyWalkerCacheServices.getFinishedTaskMap();
// 存儲非此節點處理任務
Map<String, FinishedTask> unBelongTaskMap = Maps.newHashMap();
// 找到叢集複蘇後,Rehash後不是此節點處理的任務
if (!shardingEtcdProxy.isProcessNode(taskDO.getServiceName()) && TaskStatusEnum.SYNC.getCode().equals(taskDO.getTaskStatus())) {
unBelongTaskMap.put(operationId, entry.getValue());
}
unBelongTaskMap for loop {
// 删除多餘的節點同步
specialSyncEventBus.unsubscribe(taskDO);
// 删除多餘的節點處理任務數
proxy.deleteEtcdValueByKey(PER_WORKER_PROCESS_SERVICE.concat(SLASH).concat(IPUtils.getIpAddress()).concat(SLASH).concat(taskDO.getServiceName()), false);
// 根據不同的同步類型,删除多餘的節點心跳
if (ClusterTypeEnum.EUREKA.getCode().equalsIgnoreCase(clusterDO.getClusterType())) {
syncToNacosService.deleteHeartBeat(taskDO);
}
if (ClusterTypeEnum.NACOS.getCode().equalsIgnoreCase(clusterDO.getClusterType())) {
syncToEurekaService.deleteHeartBeat(taskDO);
}
// 删除多餘的finish任務
finishedTaskMap.remove(val.getKey());
}
- 節點容災:如果
叢集連接配接不上,則存活節點從配置檔案中擷取,叢集正常運作,但是會失去容災能力。Etcd
// 配置所有處理節點的機器IP,用于建構Hash環
sync.worker.address = ip1, ip2, ip3;
// 從配置檔案擷取所有處理任務節點IP
List<String> ips = getWorkerIps();
ConsistentHash<String> consistentHash = new ConsistentHash(replicas, ips);
// 如果從Etcd中擷取不到目前處理節點,則建構Hash環用配置檔案中的IP清單,且清單不會動态變化
if (CollectionUtils.isNotEmpty(nodeCaches)) {
consistentHash = new ConsistentHash(replicas, nodeCaches);
}
return consistentHash;
Nacos Eureka Sync 保障手段
① Nacos Eureka Sync 同步界面
從如下界面可以保證,從
Eureka
Nacos
上,新上線或者下線一個業務服務(非執行個體),都能讓
Nacos Eureka Sync
實時感覺。但我們做了更進一層的智能化和自動化:
- 新增同步:結合
釋出平台,當一個業務服務(非執行個體)新上線的時候,智能判斷它是從哪個注冊中心上線的,然後回調DevOps
接口,自動添加同步接口,例如,Nacos Eureka Sync
A
Eureka
釋出平台會自動添加它的DevOps
的同步任務,反之亦然。當然從如下界面的操作也可實作該功能;Eureka -> Nacos
- 删除同步:由于
釋出平台無法判斷一個業務服務(非執行個體)下線,或者已經遷移到另一個注冊中心,已經全部完畢(有同學會反問,可以判斷的,即檢視那個業務服務的執行個體數是否是零為标準,但我們應該考慮,執行個體數為零在網絡故障的時候也會發生,即心跳全部丢失,是以這個判斷依據是不嚴謹的),交由業務人員來判斷,同時配合釘釘機器人告警提醒,由基礎架構部同學從如下界面的操作實作該功能;DevOps
② Nacos Eureka Sync Etcd 監控
從如下界面可以監控到,業務服務清單是否在同步服務的叢集上呈現一緻性
Hash
均衡分布。
③ Nacos Eureka Sync 告警
- 業務服務同步完畢告警
Nacos Eureka Sync 更新演練
- 7 月某天晚上 10 點開始,
環境進行演練,通過自動化運維工具FAT
兩次執行一鍵更新和復原均沒問題;Ansible
- 晚上 11 點 30 開始,執行災難性操作,觀察智能恢複狀況, 9 台
挂掉 3 台的操作,隻丢失一個執行個體,但 5 分鐘後恢複(經調查,問題定位在Nacos Eureka Sync
上某個業務服務執行個體狀态異常);Eureka
- 晚上 11 點 45 開始,繼續挂掉 2 台,隻剩 4 台,故障轉移,同步正常;
- 晚上 11 點 52 開始,恢複 2 台,
叢集重新均衡Nacos Eureka Sync
,同步正常;ReHash
- 晚上 11 點 55 開始,全部恢複,
Nacos Eureka Sync
ReHash
- 12 點 14 分,極限災難演練, 9 台挂掉 8 台,剩 1 台也能抗住,故障轉移,同步正常;
- 淩晨 12 點 22 分,更新
環境順利;UAT
- 淩晨 1 點 22,更新
PROD
- 容災恢複中的
時間小于 1 分鐘,即ReHash
服務大面積故障發生時,恢複時間小于 1 分鐘。Nacos Eureka Sync
作者介紹
- 吳毅挺,掌門技術副總裁,負責技術中台和少兒技術團隊。曾就職于百度、
、攜程,曾任攜程進階研發總監,負責從零打造攜程私有雲、容器雲、桌面雲和eBay
平台。PaaS
- 任浩軍,掌門基礎架構部負責人。曾就職于平安銀行、萬達、惠普,曾負責平安銀行平台架構部
平台PaaS
基礎服務架構研發。10 多年開源經曆,Halo
ID:@HaojunRen,Github
開源社群創始人,Nepxion
Group Member,Nacos
&Spring Cloud Alibaba
Nacos
Sentinel
Committer。OpenTracing
參與
Nacos
落地的基礎架構部成員,包括:
- 童子龍,張彬彬,廖夢鴿,張金星,胡振建,謝璐,謝慶芳,伊安娜
“ 阿裡巴巴雲原生 關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公衆号。”