技術分享——zookeeper到nacos的遷移實踐

寫在前面:2020年面試必備的Java後端進階面試題總結了一份複習指南在Github上,內容詳細,圖文並茂,有需要學習的朋友可以Star一下!

GitHub地址:https://github.com/abel-max/Java-Study-Note

技術選型

公司的RPC框架是 dubbo ,配合使用的服務發現組件一直是 zookeeper ,長久以來也沒什麼大問題。至於爲什麼要考慮換掉zookeeper,並不是因爲它的性能瓶頸,而是考慮往 雲原生 方向演進。

雲原生計算基金會(CNCF)對雲原生的定義是:

雲原生技術有利於各組織在公有云、私有云和混合雲等新型動態環境中,構建和運行可彈性擴展的應用。雲原生的代表技術包括容器、服務網格、微服務、不可變基礎設施和聲明式API。這些技術能夠構建容錯性好、易於管理和便於觀察的松耦合系統。結合可靠的自動化手段,雲原生技術使工程師能夠輕鬆的對系統作出頻繁和可預測的重大變更。

想要落地雲原生,關鍵步驟就是 mesh化 ,當前主要討論 service mesh (服務網格),一句話形象描述service mesh

service mesh是微服務時代的TCP協議

服務網格相對於當前的微服務最明顯的優勢是下沉基礎設置,讓服務治理和業務開發分離。

舉個簡單的例子,如果在微服務架構中想要做好服務治理,不得不引入大量的第三方組件,如限流、熔斷,監控,服務發現,負載均衡、鏈路追蹤等等一系列組件,而這些組件大概率需要以jar包的形式被業務代碼依賴,甚至不同的開發語言還得維護不同的組件,業務和基礎設施耦合讓服務治理變得異常困難。

service mesh處理服務間的通信,通常由一系列網格代理組成,對應用程序透明。服務治理中的一切(包括但不限於上述的限流、熔斷、監控、服務發現等)都可以下沉到網格代理中。

說了這麼多,跟替換zookeeper有什麼關係呢?

dubbo的設計中有三大組件:服務提供者(provider),服務消費者(consumer),註冊中心(registry)。

provider啓動後往registry註冊它提供的ip、端口、服務名、方法名等信息,consumer發起調用時通過服務名、方法名從registry查找到對應的ip和端口發起遠程調用。

而在雲原生體系下的服務註冊與發現機制與dubbo的服務註冊發現機制有很大區別,雲原生是基於容器編排,主流的 k8s 服務註冊發現是基於 DNS ,也就是通過一個域名查找到一個對應的ip。

這樣一來,如果要遷移dubbo服務到雲原生體系中就很艱難,有沒有一款兼容兩種服務註冊發現的組件?經過調研nacos就是。

首先nacos和zookeeper一樣,提供了傳統微服務的註冊與發現方式,其次nacos還提供了基於coreDNS的插件DNS-F(DNS filter),DNS-F作爲一個代理攔截主機上的DNS請求,如果該服務在nacos上能找到則直接返回註冊的ip,否則繼續查找DNS。

完成後,我們的service mesh架構大致如下


網格內外的訪問策略如下:

網格外dubbo -> 網格內dubbo:註冊中心
網格外dubbo -> 網格外dubbo:註冊中心
網格內dubbo -> 網格外dubbo:域名 => dns-f => 註冊中心
網格內dubbo -> 網格內dubbo:域名 => dns-f => dns
異構語言(PHP、Node)可通過服務名直接發起調用,由DNS-F攔截,解析爲正確IP地址,且負載均衡策略可調整
同時nacosAP模式的高可用與寫的可擴展性、可對接CMDB等等特性也是選擇的考慮因素之一。

遷移方案

如果要從zookeeper平滑地遷移到nacos上,可選的方案有兩個:

改造dubbo應用,將服務註冊改爲雙註冊(同時註冊到zookeeper與nacos),等所有應用改造完成後再統一切換到nacos
使用遷移工具,將zookeeper上註冊的服務統一遷移到nacos,這時再慢慢修改應用,不必等完全遷移完即可享受nacos帶來的新特性
方案1實現上來說簡單,但改造成本較大,一些老舊無人維護的服務遷移起來困難,甚至公司層面還有PHP,node等服務也依賴了zookeeper,不可能一次性遷移完成。

方案2需要一款強大的遷移工具,增加了技術的複雜度,好在nacos提供了 nacosSync 。

當然,我們選擇了方案2,同時做了一點點優化。爲了降低遷移風險,基於dubbo優秀的擴展性,定製了一套 態註冊中心 ,動態註冊中心在服務啓動時從配置中心讀取配置,選擇是往zookeeper還是nacos註冊(或者都註冊),服務消費時是選擇nacos還是zookeeper,消費只能指定一個註冊中心。

默認情況下會往兩個註冊中心同時註冊,消費zookeeper,引入jar包後業務方無感知,切換時只需要變更配置,下次發佈即可改變註冊和消費的註冊中心,支持針對單個應用進行配置,便於灰度。

遷移工具優化

nacosSync的原理很簡單,如果是zookeeper同步數據到nacos,啓動時nacosSync作爲一個zookeeper客戶端,將zookeeper上的所有服務拉下來,解析爲nacos的服務格式,註冊到nacos上,同時監聽每個服務節點,有變化時對nacos數據進行更新。

單向同步策略
nacosSync可實現從zookeeper到nacos的雙向同步功能,但我們覺得雙向同步有風險,畢竟nacos是個新東西,穩定性不敢保證,如果nacos中的數據有誤,同步到zookeeper上就會帶來生產上的故障。於是採取了比較保守的zookeeper到nacos的單向同步策略。

高可用
作爲需要長期在線的遷移工具,需要保證它本身的穩定性和高可用,試想如果如果遷移工具宕機,導致所有的服務都將從nacos上掉線,這將是一個毀滅性的打擊。nacosSync將數據存儲下沉到數據庫,組件本身是無狀態的,可部署多臺,防止單點故障。但也帶來另外的問題,部署N臺對nacos服務端的壓力爲N倍,因爲一個服務會被註冊N次,修改後也會被更新N次,這塊的優化後面會說。

全量同步支持
nacosSync不支持全量的同步,只能單個服務單個服務的配置,對於有3k+服務來說,不可能一個服務一個服務地手動配置。於是開發一個全量的配置,這個倒是不難,但很有用。

zookeeper事件亂序處理

nacosSync在監聽zookeeper的節點後,當zookeeper節點發生變更,nacosSync將變更後的數據同步到nacos。

但在測試過程中我們發現一個問題,dubbo服務下線後,如果是沒有優雅地下線(如進程被kill -9),會在幾秒至幾分鐘內(取決於配置)被zookeeper踢掉節點,如果這時服務重新註冊,可能會存在節點remove事件較新節點add事件後到達,這會導致一個很重要的問題,新註冊上來的服務,被舊的remove事件下線。

解決方案也比較簡單,dubbo註冊節點的信息中存有毫秒級的 timestamp 信息,每次處理事件時將比較timestamp,如果大於等於當前值,則認爲此次事件有效,否則認爲此次事件是舊事件,丟棄不處理。

通過這樣的邏輯判斷後就再也沒有出現此類問題。

主動心跳檢測

由於nacosSync部署兩臺,萬一某服務下線時,其中一臺nacosSync發生了未知異常,會導致該服務不可用,但一直在nacos上存在,這時發起調用將會報錯。

爲了避免這種情況,在nacosSync中新增了對機器端口的檢測,每隔一段時間對所有機器進行建連接,如果失敗,再去看zookeeper中該節點是否存在,不存在再剔除該機器。

爲什麼不能在心跳檢測失敗後直接剔除?因爲有時候服務器會拒絕連接或者超時,但這時服務還在線,所以還是以zookeeper中爲準。至於爲什麼不直接掃描zookeeper,也是出於對zookeeper性能的擔心,萬一掃掛了zookeeper,可是個大故障。

nacos優化

遷移工具優化的差不多了,就開始將所有線上服務同步到nacos中。

起初我們搭建了包含3個節點的nacos集羣,結果由於服務數量太多,導致nacos機器的cpu長期處於非常高的值,超過50%。這裏給出一組數據作爲參考:

服務數:3k+
服務實例數:30k+
nacosSync節點數:2
nacos節點數:3(50%~80%cpu利用率)
監控完善
如何優化?首先找出性能瓶頸,nacos原生基於spring-boot做了監控,但是很雞肋,沒有想要的數據,於是從客戶端和服務端兩個維度對監控做了完善,這裏列出我認爲比較重要的幾個監控指標

nacos服務端:cpu佔比,服務數,實例數,接受請求數量(區分api),請求響應時間(區分api),心跳處理速度,推送耗時(原生),推送量(原生)

nacos客戶端:請求量(區分api),請求耗時(區分api),心跳發送速度

心跳優化

在上述監控完善之後,一眼就能看出瓶頸,心跳請求實在是太多了,99%的請求都是心跳請求。

這與nacos和dubbo的設計有關,dubbo註冊是服務維度,一個ip註冊了很多服務實例,而nacos的心跳以實例爲緯度,而且默認的是一個實例5秒一個心跳。

接近40k的實例,每5秒就有40k的心跳請求,換算成qps就是8k/s,而且使用了兩臺nacosSync,也就是雙倍的心跳請求16k/s,而且是http請求,節點內部還有數據同步的任務,cpu不高才怪。

於是我們想了一系列辦法來優化:

調整心跳間隔

心跳時間調整爲默認的兩倍,即10秒,同時也調整了節點無心跳下線時間(30s調整爲60s)犧牲了實例下線檢測的實時性。

擴容

將nacos服務端從3臺擴容到5臺,效果有,但不明顯。

減少心跳

由於我們是需要逐步遷移服務,遷移後的服務,如果服務本身發送心跳,2臺nacosSync也發送心跳,遷移後的服務就有三倍的心跳請求了,同時這樣也導致了服務下線後萬一有一方未剔除,服務仍在線的風險。

於是在前文提到的動態註冊中心中對往nacos上註冊的服務,增加了一條元數據信息 withNacos=true ,再修改nacosSync的邏輯,忽略zookeeper同步過來的帶withNacos=true的服務。這樣只要遷移過的服務只會由服務本身發送心跳,減少心跳請求。

合併心跳

nacosSync中註冊了大量的服務,通過前面的計算得知每秒約發送8k左右心跳,這部分心跳如果可以合併,將大量減少心跳的網絡消耗,服務端批處理也能加快速度。

在實現合併心跳前需要理解nacos AP模式下的distro協議,這部分可以參考 《nacos的一致性協議distro介紹 》

簡單概括一下單條心跳的處理路徑:

客戶端隨機找一臺nacos節點對某一個服務實例發生心跳,由於每個nacos服務端節點只負責部分服務,當它收到請求後判斷是否爲自己負責的服務,如果是則處理,如果不是則轉交給負責的節點。

單條心跳路由比較好處理,如果合併服務發送心跳,就需要在服務端將收到的請求按負責的節點進行分類,分類完只處理屬於自己的服務,對於不是自己的服務則批量轉交給其他節點。需要注意處理來自客戶端還是服務端的請求。

客戶端是將需要心跳的對象緩衝到隊列中,每秒鐘從隊列中讀取出一批進行批量發送,需要注意算好這個緩衝的大小設置,如果太小,可能會丟失心跳,太大就太消耗內存。

合併後的效果立竿見影,不僅服務端的cpu由50%+下降到10%以內,nacosSync的cpu消耗也下降了一半。

只保存了這張圖,當時cpu還在20%左右是因爲有個bug,解決後cpu就到10%以內了。

長連接

到這裏其實心跳問題只解決了一半,因爲在nacosSync中有大量服務時,批量心跳才效果比較明顯。如果是遷移後的服務,單機只有10個實例,一秒內也攢不了幾個心跳請求,所以效果肯定大打折扣。於是分析了可能是http請求的建立連接,以及每個請求都要走很長的web filter比較消耗性能,於是想看看修改爲長連接之後的效果。

爲了快速驗證猜想,只修改心跳接口,這個接口量最大,搞定它能解決80%的問題。

長連接使用什麼來實現,當初考慮的有netty和grpc兩個方案。爲了快速驗證,鎖定了 grpc ,同時上文中提到的DNS-F其實也是一個nacos的客戶端,它是go語言實現,剛好原生支持grpc,所以毫不猶豫就用grpc實現了一版。

使用配置來選擇使用原生心跳、批量心跳、grpc心跳。

實現長連接中遇到了一個問題是distro協議中將心跳轉發給負責的節點,原生是內部中轉,如果長連接也這樣實現就比較複雜,需要維持集羣內部的長連接。考慮邏輯寫進客戶端,和redis一樣,當不是自己負責的服務時,將請求 redirect 給負責的節點,客戶端重新發起請求。

初始時客戶端隨機挑一個節點發送,如果收到redirect,則緩存住該服務的目標節點,下次遇到redirect或者報錯時再清空緩存,這樣就能保證選擇節點的正確性,只要服務端節點沒有變化,客戶端就能一次命中節點,簡單高效,同時這個邏輯也不是很複雜,在DNS-F中實現也比較簡單。

最後的測量結果是,如果nacosSync全量使用grpc心跳,會比批量心跳cpu稍微高一點,沒有很多。這可是單個心跳發送,能這樣已經很不錯了,這樣就說明就算服務全部遷移,也可以接近批量發送的效率。

關鍵接口長連接

在嚐到了長連接優化心跳的甜頭後,針對幾個重要的接口,如服務註冊,服務拉取等都改爲了長連接,而且針對DNS-F也適配了長連接,效果很好,達到我們對nacos性能的預期。

優雅上下線

nacos提供了優雅下線的接口,即下線某個服務,但是是針對實例緯度的,對於公司內部的發佈系統不是很友好,發佈系統不知道機器中有什麼服務,於是需要提供一個ip緯度的下線接口,也可以理解爲一個批量下線接口,實現時同批量心跳接口,也需要注意處理distro協議。

DNS-F改進

長連接

這塊上面已經提到,不再贅述。

dubbo服務域名不合法
dubbo註冊到nacos上的服務爲:

providers:com.xx.yy.zz

通常我們將該服務名作爲域名發起dubbo調用,但引號在域名中是不合法的,通過這個域名直接訪問會報錯,於是修改了DNS-F的代碼,調用時使用

providers.com.xx.yy.zz

DNS-F內部將該域名的 providers. 替換爲 providers: ,這樣改動點最小。

高可用

因爲DNS-F本身作爲一個 agent 進程運行在機器上,所以高可用通過兩個手段來保證。

監控DNS-F進程,掛掉後及時拉起
搭建一個集中式的DNS-F集羣,在本地DNS-F不可用時DNS解析先過一遍DNS-F集羣,再走正常DNS集羣

最後

nacos作爲一個比較新的開源組件,使用時必然會遇到各種各樣的問題,本文重點介紹了筆者在遷移zookeeper到nacos中遇到的比較重要的坑點,希望對大家有所幫助,當然還有更多的細節限於篇幅未能羅列。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章