聊聊微服務的服務註冊與發現
引言
聊起微服務的服務註冊與發現,很多人立馬就會脫口而出 zk、etcd、consul、eureka 這些組件,進而聊到 CAP 如何取舍,性能如何,高可用和容災是怎麽實現的。
在這之前,站在組件使用者的角度,我想先問這麽幾個問題:
註冊的 IP 和端口怎麽確定 ?
實現服務治理還需要註冊哪些信息 ?
如何進行優雅的服務註冊與服務下線 ?
註冊服務的健康檢查是如何做的 ?
當服務有節點退出或新的節點加入時,訂閱者能不能及時收到通知 ?
我能方便地查看某個應用發布和訂閱了哪些服務,以及所訂閱的服務有哪些節點嗎 ?
看完這些問題後,您也許會發現,對於服務註冊與發現,首先應該關註的是服務註冊發現本身的功能,然後才是性能和高可用。
一個好的服務註冊發現中間件,應該是能完整地滿足服務開發和治理的基礎功能,然後才是性能和高可用。如果沒有想清楚前面的功能,再高的可用性和性能都是浮雲。最後,安全也同樣重要。
服務端的性能如何 ?
服務發現的容災策略是怎樣的 ?
當我的應用和服務發現中心的網絡連接出現問題時,會對我的調用產生什麽影響 ?
服務註冊中心某臺機器宕機或者全部宕機時,會對我的調用產生什麽影響 ?
服務註冊和發現的鏈路安全嗎,有沒有做好權限控制 ?
下面將從 服務註冊、服務發現、容災和高可用三個大方面來回答這些問題的主流做法。
最後會介紹一下 ANS(Alibaba Naming Service) , ANS 綜合了這些解決方案中的優點,並在 EDAS(阿裏巴巴企業級分布式應用服務) 中輸出,目前完全免費!
服務註冊
註冊的 IP 和端口怎麽確定 ?
IP 如何確定
主流的 IP 獲取有這幾種方法。
最簡單粗暴的方式,手動配置需要註冊的IP。當然這種方式基本無法在生產環境使用,因為微服務基本都是支持水平擴容多機部署的,在配置中寫死 IP 地址的方式無法支持一份代碼水平擴容,會給運維帶來極大的成本。
通過遍歷網卡的方式去獲取,找到第一個不為本地環回地址的 IP 地址。絕大多數情況下,這個方式比較好用,dubbo 等框架采用的就是這種方法。
在一些網絡規劃比較好的標準化機房中,我們還可以通過手動指定網卡名,即 interfaceName 的方式來指定使用哪一塊網卡所對應的 IP 地址進行註冊。
當上述三種方式都不能有效解決問題的時候,有一個方法就是直接與服務註冊中心建立 socket 連接,然後通過
socket.getLocalAddress()
這種方式來獲取本機的 IP。
端口如何確定
端口的獲取,沒有標準化的方案。
如果是 RPC 應用,啟動的時候都有一個配置來指定服務監聽的端口, 註冊的時候直接使用配置項的端口值。
傳統的 WEB 容器所提供的 HTTP 的應用,同樣也存在一個配置文件來配置容器的監聽端口,註冊時候直接使用配置項的端口值。
特別的,在 Java 應用的 Spring Boot 框架中,可以通過
EmbeddedServletContainerInitializedEvent. getEmbeddedServletContainer().getPort()
來獲取。(Spring Boot 版本為 1.x)。
實現服務治理還需要註冊哪些信息 ?
簡單地將 IP 和 port 信息註冊上去,可以滿足基本的服務調用的需求,但是在業務發展到一定程度的時候,我們還會有這些需求:
想知道某個 HTTP 服務是否開啟了 TLS。
對相同服務下的不同節點設置不同的權重,進行流量調度。
將服務分成預發環境和生產環境,方便進行AB Test功能。
不同機房的服務註冊時加上機房的標簽,以實現同機房優先的路由規則。
這些高級功能的實現,本質上是依賴於客戶端調用時候的負載均衡策略和調用策略,但是如果服務元數據沒有註冊上來,也只能是巧婦難為無米之炊。一個良好的服務註冊中心在設計最初就應該支持這些擴展字段。
如何進行優雅的服務註冊與服務下線 ?
優雅發布
雖然服務註冊一般發生在服務的啟動階段,但是細分的話,服務註冊應該在服務已經完全啟動成功,並準備對外提供服務之後才能進行註冊。
有些 RPC 框架自身提供了方法來判斷服務是否已經啟動完成,如 Thrift ,我們可以通過 Server.isServing() 來判斷。
有一些 RPC 框架本身沒有提供服務是否啟動完成的方式,這時我們可以通過檢測端口是否已經處於監聽狀態來判斷。
而對於 HTTP 服務,服務是否啟動完畢也可以通過端口是否處於監聽狀態來判斷。
特別的,在 Java 應用的 Spring Boot 框架中,可以通過事件通知的形式來通知容器已經啟動完畢, EmbeddedServletContainerInitializedEvent 事件來通知容器已經啟動完成 (Spring Boot 版本為 1.x)。
優雅下線
絕大多數的服務註冊中心都提供了健康檢查功能,在應用停止後會自動摘除服務所對應的節點。但是我們也不能完全依賴此功能,應用應該在停止時主動調用服務註冊中心的服務下線接口。
在 Java 應用中,通用的服務下線接口調用一般使用 JVM Shutdown Hook 的方式來實現。
特別的,在 Java 應用中的 Spring 框架中,可以通過 Spring Bean LifeCycle 來實現應用停止時主動調用服務下線接口。
當然上述兩種方式還不夠優雅,因為不能確保不出現 kill -9 這種粗暴的停止方式,而且應用調用服務下線接口也是嘗試去調用,對於網絡不通等異常場景並沒有做異常處理。因此,調用客戶端仍應該做好負載均衡與 failover 的處理。
更優雅的方式,先將即將停止的應用所對應的權重調成 0,此時上遊將不再調用此應用。這時候的停止應用的操作對服務訂閱者完全沒有影響,當然這種場景需要訂閱者實現按權重的負載均衡和運維部署工具深度結合。
服務的健康檢查是如何做的 ?
健康檢查分為客戶端心跳和服務端主動探測兩種方式。
客戶端心跳
客戶端每隔一定時間主動發送“心跳”的方式來向服務端表明自己的服務狀態正常,心跳可以是 TCP 的形式,也可以是 HTTP 的形式。
也可以通過維持客戶端和服務端的一個 socket 長連接自己實現一個客戶端心跳的方式。
ZooKeeper 並沒有主動的發送心跳,而是依賴了組件本身提供的臨時節點的特性,通過 ZooKeeper 連接的 session 來維持臨時節點。
但是客戶端心跳中,長連接的維持和客戶端的主動心跳都只是表明鏈路上的正常,不一定是服務狀態正常。
服務端主動調用服務進行健康檢查是一個較為準確的方式,返回結果成功表明服務狀態確實正常。
服務端主動探測
服務端調用服務發布者某個 HTTP 接口來完成健康檢查。
對於沒有提供 HTTP 服務的 RPC 應用,服務端調用服務發布者的接口來完成健康檢查。
可以通過執行某個腳本的形式來進行綜合檢查。
服務端主動探測也存在問題。服務註冊中心主動調用 RPC 服務的某個接口無法做到通用性;在很多場景下服務註冊中心到服務發布者的網絡是不通的,服務端無法主動發起健康檢查。
所以如何取舍,還是需要根據實際情況來決定,根據不同的場景,選擇不同的策略。
服務發現
怎麽找到服務發現服務端的地址?
在應用的配置文件中指定服務註冊中心的地址,類似於 zookeeper 和 eureka。
指定一個地址服務器的地址,然後通過這個地址服務器來獲取服務註冊中心的地址,地址服務器返回的結果會隨著服務註冊中心的擴縮容及時更新。
當服務有節點退出或新的節點加入時,訂閱者如何及時收到通知 ?
很經典的 Push 和 Pull 問題。
Push 的經典實現有兩種,基於 socket 長連接的 notify,典型的實現如 zookeeper;另一種為 HTTP 連接所使用 Long Polling。
但是基於 socket 長連接的 notify 和基於 HTTP 協議的 Long Polling 都會存在notify消息丟失的問題。
所以通過 Pull 的方式定時輪詢也必不可少,時間間隔的選擇也很關鍵,頻率越高服務註冊中心所承受的壓力也越大。需要結合服務端的性能和業務的規模進行權衡。
還有一種方式,真實的 Push,客戶端開啟一個 UDP server,服務註冊中心通過 UDP 的方式進行數據推送,當然這個也受限於網絡的連通性。
我能方便地查看我發布和訂閱了哪些服務,訂閱的服務有哪些節點嗎 ?
一個好的產品,用戶使用體驗和運維體驗必須是優雅的,如果查看本機發布和訂閱的服務,只能通過查看日誌,甚至是 jmap 的方式來獲取,顯然體驗非常糟糕。
服務註冊中心應該提供了豐富的接口,支持根據應用名、IP、訂閱服務名、發布服務名,來進行多層次的組合查詢。
同時,客戶端的內存裏,同樣也應該保留服務發布與訂閱的各種信息,並提供方式供人方便地查詢。
比如在 Java 中的 Spring Boot 的應用,可以結合 actuator endpoint,通過 HTTP 的方式來提供本機服務查詢功能,查詢此應用發布的服務,以及訂閱的服務及各服務的對應節點。
容災和高可用
性能如何
當服務節點數越來越多時,服務註冊中心的性能會成為瓶頸,這時候就需要通過水平擴容來提升服務註冊中心集群的性能。
對於那些采用了類 Paxos 協議的強一致性的組件,如ZooKeeper,由於每次寫操作需要過半的節點確認。水平擴容不能提升整個集群的寫性能,只能提升整個集群的讀性能。
而對於采用最終一致性的組件來說,水平擴容可以同時提升整個集群的寫性能和讀性能。
客戶端容災策略
首先,本地內存緩存,當運行時與服務註冊中心的連接丟失或服務註冊中心完全宕機,仍能正常地調用服務。
然後,本地緩存文件,當應用與服務註冊中心發生網絡分區或服務註冊中心完全宕機後,應用進行了重啟操作,內存裏沒有數據,此時應用可以通過讀取本地緩存文件的數據來獲取到最後一次訂閱到的內容。
最後,本地容災文件夾。正常的情況下,容災文件夾內是沒有內容的。當服務端完全宕機且長時間不能恢復,同時服務提供者又發生了很大的變更時,可以通過在容災文件夾內添加文件的方式來開啟本地容災。此時客戶端會忽略原有的本地緩存文件,只從本地容災文件中讀取配置。
服務端容災與高可用
當有新節點加入集群時,節點啟動後能自動添加到地址服務器中,並通過地址服務器找到其他節點,自動從其他節點同步數據,以達到數據的最終一致性。
當某個節點宕機時,此服務註冊中心節點的信息會自動地址服務器中摘除,客戶端能及時感知到此節點已下線。
服務端的無狀態性保證了服務的容災和高可用可以做的很薄。
服務端安全是如何做的 ?
鏈路安全,對於使用 HTTP 連接的服務註冊中心,保護鏈路安全的最好方式是使用 HTTPS。而使用 TCP 連接的服務註冊中心來說,由於應用層協議一般使用的是私有協議,不一定存在現成的 TLS 支持方案。
在業務安全方面,應該在每一次的發布、訂閱、心跳,都帶上鑒權的信息就行驗簽和鑒權,確保業務信息的安全性。
Alibaba Naming Service
ANS (Alibaba Naming Service) 是阿裏巴巴中間件團隊將多年業務實踐沈澱打磨的開源產品。在服務註冊與發現方面,ANS 綜合了上述解決方案中的優點,是最適合雲原生應用的服務註冊與發現組件。
ANS 服務已經在 EDAS(阿裏巴巴企業級分布式應用服務) 上線,目前已經提供 Spring Cloud Ans Starter 方便 Spring Cloud 用戶直接使用一個安全的可靠的商業版服務註冊與發現功能。ANS 能完美地支持 Eureka 的特性,而且目前完全免費!更多信息參見 EDAS 幫助文檔。
原文鏈接
聊聊微服務的服務註冊與發現