天天看點

聊聊微服務的服務注冊與發現

引言

聊起微服務的服務注冊與發現,很多人立馬就會脫口而出 zk、etcd、consul、eureka 這些元件,進而聊到 CAP 如何取舍,性能如何,高可用和容災是怎麼實作的。

在這之前,站在元件使用者的角度,我想先問這麼幾個問題:

  • 注冊的 IP 和端口怎麼确定 ?
  • 實作服務治理還需要注冊哪些資訊 ?
  • 如何進行優雅的服務注冊與服務下線 ?
  • 注冊服務的健康檢查是如何做的 ?
  • 當服務有節點退出或新的節點加入時,訂閱者能不能及時收到通知 ?
  • 我能友善地檢視某個應用釋出和訂閱了哪些服務,以及所訂閱的服務有哪些節點嗎 ?

看完這些問題後,您也許會發現,對于服務注冊與發現,首先應該關注的是服務注冊發現本身的功能,然後才是性能和高可用。

一個好的服務注冊發現中間件,應該是能完整地滿足服務開發和治理的基礎功能,然後才是性能和高可用。如果沒有想清楚前面的功能,再高的可用性和性能都是浮雲。最後,安全也同樣重要。

  • 服務端的性能如何 ?
  • 服務發現的容災政策是怎樣的 ?
  • 當我的應用和服務發現中心的網絡連接配接出現問題時,會對我的調用産生什麼影響 ?
  • 服務注冊中心某台機器當機或者全部當機時,會對我的調用産生什麼影響 ?
  • 服務注冊和發現的鍊路安全嗎,有沒有做好權限控制 ?

下面将從 服務注冊、服務發現、容災和高可用三個大方面來回答這些問題的主流做法。

最後會介紹一下 ANS(Alibaba Naming Service) , ANS 綜合了這些解決方案中的優點,并在 EDAS(阿裡巴巴企業級分布式應用服務) 中輸出,目前完全免費!

服務注冊

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,由于每次寫操作需要過半的節點确認。水準擴容不能提升整個叢集的寫性能,隻能提升整個叢集的讀性能。
  • 而對于采用最終一緻性的元件來說,水準擴容可以同時提升整個叢集的寫性能和讀性能。

用戶端容災政策

  1. 首先,本地記憶體緩存,當運作時與服務注冊中心的連接配接丢失或服務注冊中心完全當機,仍能正常地調用服務。
  2. 然後,本地緩存檔案,當應用與服務注冊中心發生網絡分區或服務注冊中心完全當機後,應用進行了重新開機操作,記憶體裡沒有資料,此時應用可以通過讀取本地緩存檔案的資料來擷取到最後一次訂閱到的内容。
  3. 最後,本地容災檔案夾。正常的情況下,容災檔案夾内是沒有内容的。當服務端完全當機且長時間不能恢複,同時服務提供者又發生了很大的變更時,可以通過在容災檔案夾内添加檔案的方式來開啟本地容災。此時用戶端會忽略原有的本地緩存檔案,隻從本地容災檔案中讀取配置。

服務端容災與高可用

  • 當有新節點加入叢集時,節點啟動後能自動添加到位址伺服器中,并通過位址伺服器找到其他節點,自動從其他節點同步資料,以達到資料的最終一緻性。
  • 當某個節點當機時,此服務注冊中心節點的資訊會自動位址伺服器中摘除,用戶端能及時感覺到此節點已下線。

服務端的無狀态性保證了服務的容災和高可用可以做的很薄。

服務端安全是如何做的 ?

鍊路安全,對于使用 HTTP 連接配接的服務注冊中心,保護鍊路安全的最好方式是使用 HTTPS。而使用 TCP 連接配接的服務注冊中心來說,由于應用層協定一般使用的是私有協定,不一定存在現成的 TLS 支援方案。

在業務安全方面,應該在每一次的釋出、訂閱、心跳,都帶上鑒權的資訊就行驗簽和鑒權,確定業務資訊的安全性。

Alibaba Naming Service

ANS (Alibaba Naming Service) 是阿裡巴巴中間件團隊将多年業務實踐沉澱打磨的開源産品。在服務注冊與發現方面,ANS 綜合了上述解決方案中的優點,是最适合雲原生應用的服務注冊與發現元件。

ANS 服務已經在 EDAS(阿裡巴巴企業級分布式應用服務) 上線,目前已經提供 Spring Cloud Ans Starter 友善 Spring Cloud 使用者直接使用一個安全的可靠的商業版服務注冊與發現功能。ANS 能完美地支援 Eureka 的特性,而且目前完全免費!更多資訊參見

EDAS 幫助文檔