1. 程式人生 > 實用技巧 >掌門教育微服務體系 Solar | 阿里巴巴 Nacos 企業級落地中篇

掌門教育微服務體系 Solar | 阿里巴巴 Nacos 企業級落地中篇

聯席作者:吳毅挺 任浩軍 童子龍
鄭重鳴謝:Nacos - 彥林,Spring Cloud Alibaba - 小馬哥、洛夜,Nacos 社群 - 張龍(pader)、春少(chuntaojun)

相關文章推薦:

前言

在高速發展的時候,公司規模越來越大,老師人數越來越多,這時候公司不能鋪太多人去做運營與服務,必須提高每個人效,這就需要技術驅動。因此掌門教育轉變成一家技術驅動型的公司,如果被迫成為一家靠資金驅動的公司就活不下去了。

-- 張翼(掌門教育創始人兼 CEO)

掌門教育自 2014 年正式轉型線上教育以來,秉承“讓教育共享智慧,讓學習高效快樂”的宗旨和願景,經歷雲端計算、大資料、人工智慧、 AR

/ VR / MR 以及現今最火的 5G ,一直堅持用科技賦能教育。掌門教育的業務近幾年得到了快速發展,特別是今年的疫情,使線上教育成為了新的風口,也給掌門教育新的機遇。

隨著業務規模進一步擴大,流量進一步暴增,微服務數目進一步增長,使老的微服務體系所採用的註冊中心 Eureka 不堪重負,同時 Spring Cloud 體系已經演進到第二代,第一代的 Eureka 註冊中心已經不大適合現在的業務邏輯和規模,同時它目前被 Spring Cloud 官方置於維護模式,將不再向前發展。如何選擇一個更為優秀和適用的註冊中心,這個課題就擺在了掌門人的面前。經過對 Alibaba NacosHashiCorp 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 實現被同步的業務服務列表的平均分配,基於對業務服務名的二進位制轉換作為 HashKey 實現一致性 Hash 的演算法。我們自研了這套演算法,發現平均分配的很不理想,第一時間懷疑是否演算法有問題,於是找來 Kafka 自帶的演算法(見 Utils.murmur2 ),發現效果依舊不理想,原因還是業務服務名的本身分佈就是不平均的,於是又回到自研演算法上進行了優化,基本達到預期,下文會具體講到。但說實話,直到現在依舊無法做到非常良好的絕對平均。

③ Sync 高可用主備 + Zookeeper 方案

這個方案是個小插曲,當一臺同步伺服器掛掉後,由它的“備”頂上,當然主備切換也是基於 Zookeeper 臨時節點的 Watch 機制來實現的。後面討論下來,主備方案,機器的成本很高,實現也不如一致性 Hash 優雅,最後沒采用。

④ Sync 高可用一致性 Hash + Etcd 方案

折騰了這麼幾次後,發現同步業務服務列表是持久化在資料庫,同步伺服器掛掉後 reHash 通知機制是由 Zookeeper 來負責,兩者能否可以合併到一箇中間件上以降低成本?於是我們想到了 Etcd 方案,即通過它實現同步業務服務列表持久化 + 業務服務列表增減的通知 + 同步伺服器掛掉後 reHash 通知。至此方案最終確定,即兩個註冊中心( EurekaNacos )的雙向同步方案,通過第三個註冊中心( Etcd )來做橋樑。

⑤ Sync 業務服務名列表定時更新優化方案

解決了一致性 Hash 的問題後,還有一個潛在風險,即官方方案每次定時同步業務服務的時候,都會去讀取全量業務服務名列表,對於業務服務數較少的場景應該沒問題,但對於我們這種場景下,這麼頻繁的全量去拉業務服務列表,會不會對 Nacos 伺服器的效能有所衝擊呢?接下去我們對此做了優化,取消全量定時讀取業務服務名列表,通過 DevOps 的釋出系統平臺實施判斷,如果是遷移過來的業務服務或者新上 Nacos 的業務服務,由釋出平臺統一呼叫 Nacos 介面來增加新的待同步業務服務 Job,當該業務服務全部遷移完畢後,在官方同步介面上刪除該同步業務服務 Job 即可。

⑥ Sync 伺服器兩次擴容

方案實現後,上了 FAT 環境上後沒發現問題(此環境,很多業務服務只部署一個例項),而在 PROD 環境上發現存在雙向同步丟心跳的問題,原因是同步伺服器來不及執行排隊的心跳執行緒,導致 Nacos 伺服器無法及時收到心跳而把業務服務踢下來。我們從 8 臺 4C8G 同步伺服器擴容到 12 臺,情況好了很多,但觀察下來,還是存在一天記憶體在一些業務服務丟失心跳的情況,於是我們再次從 12 臺 4C8G 同步伺服器擴容到 20 臺,情況得到了大幅改善,但依舊存在某個同步伺服器上個位數丟失心跳的情況,觀察下來,那臺同步伺服器承受的某幾個業務服務的例項數特別多的情況,我們在那臺同步伺服器調整了最大同步執行緒數,該問題得到了修復。我們將繼續觀察,如果該問題仍舊復現,不排除升級機器配置到 8C16G 來確保 PROD 環境的絕對安全。

至此,經過 2 個月左右的努力付出,EurekaNacos 同步執行穩定, PROD 環境上同步將近 660 個服務(非例項數),情況良好。

非常重要的提醒:一致性 Hash 的虛擬節點數,在所有的 Nacos Sync Server 上必須保持一致,否則會導致一部分業務服務同步的時候會被遺漏。

Nacos Eureka Sync 落地實踐

① Nacos Eureka Sync 目標原則

  • 註冊中心遷移目標

    • 過程並非一蹴而就的,業務服務逐步遷移的過程要保證線上呼叫不受影響,例如, A 業務服務註冊到 Eureka 上, B 業務服務遷移到 NacosA 業務服務和 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 負載均衡,刪除任務指令輪詢到的節點。如果是自己處理任務則移除心跳,否則找到處理節點,代理出去;
  • 同步服務監聽源註冊中心每個業務服務例項狀態,將正常的業務服務例項同步到目標註冊中心,保證雙方註冊中心的業務服務例項狀態實時同步;
  • 業務服務所有例項從 EurekaNacos 後,需要業務部門通知基礎架構部手動從 Nacos Eureka Sync 同步介面摘除該同步任務。

④ Nacos Eureka Sync 方案實現

基於官方的 Nacos Sync 做任務分片和叢集高可用,目標是為了支援大規模的註冊叢集遷移,並保障在節點宕機時,其它節點能快速響應,轉移故障。技術點如下,文中只列出部分原始碼或者以偽程式碼表示:

詳細程式碼,請參考:https://github.com/zhangmen-tech/nacos

服務一致性 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 叢集租約 TTLTTL 內至少傳送 5 個續約心跳以保證一旦出現網路波動避免造成節點丟失;
// 心跳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);
    }
}
  • 節點宕機:其中某個節點宕機,其任務轉移到其它節點,因為有虛擬節點的緣故,所以此節點的任務會均衡 ReSharding 到其它節點,那麼,叢集在任何時候,任務處理都是分片均衡的,如圖 2 中, B 節點宕機, ##1##2 虛擬節點的任務會分別轉移到 CA 節點,這樣避免一個節點承擔宕機節點的所有任務造成剩餘節點連續雪崩;

  • 節點恢復:如圖 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 臺 Nacos Eureka Sync 掛掉 3 臺的操作,只丟失一個例項,但 5 分鐘後恢復(經調查,問題定位在 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 環境順利;
  • 容災恢復中的 ReHash 時間小於 1 分鐘,即 Nacos Eureka Sync 服務大面積故障發生時,恢復時間小於 1 分鐘。

作者介紹

  • 吳毅挺,掌門技術副總裁,負責技術中臺和少兒技術團隊。曾就職於百度、eBay 、攜程,曾任攜程高階研發總監,負責從零打造攜程私有云、容器雲、桌面雲和 PaaS 平臺。
  • 任浩軍,掌門基礎架構部負責人。曾就職於平安銀行、萬達、惠普,曾負責平安銀行平臺架構部 PaaS 平臺 Halo 基礎服務框架研發。10 多年開源經歷,Github ID:@HaojunRen,Nepxion 開源社群創始人,Nacos Group Member,Spring Cloud Alibaba & Nacos & Sentinel & OpenTracing Committer。

參與 Nacos 落地的基礎架構部成員,包括:

  • 童子龍,張彬彬,廖夢鴿,張金星,胡振建,謝璐,謝慶芳,伊安娜

阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公眾號。”