1. 程式人生 > 實用技巧 >服務發現技術選型那點事兒

服務發現技術選型那點事兒

1009頭圖.png

作者 | 張羽辰(同昭)

引子——什麼是服務發現

近日來,和很多來自傳統行業、國企、政府的客戶在溝通技術細節時,發現雲原生所代表的技術已經逐漸成為大家的共識,從一個虛無縹緲的概念漸漸變成這些客戶的下一個技術戰略。自然,應用架構就會提到微服務,以及其中最重要的分散式協作的模式——服務發現。模式(pattern)是指在特定上下文中的解決方案,很適合描述服務發現這個過程。不過相對於 2016 年,現在我們最少有十多種的方式能實現服務發現,這的確是個好時機來進行回顧和展望,最終幫助我們進行技術選型與確定演進方向。

微服務脫胎於 SOA 理論,核心是分散式,但單體應用中,模組之間的呼叫(比如讓訊息服務給客戶傳送一條資料)是通過方法,而所發出的訊息是在同一塊記憶體之中,我們知道這樣的代價是非常小的,方法呼叫的成本可能是納秒級別,我們從未想過這樣會有什麼問題。但是在微服務的世界中,模組與模組分別部署在不同的地方,它們之間的約束或者協議由方法簽名轉變為更高階的協議,比如 RESTful 、PRC,在這種情況下,呼叫一個模組就需要通過網路,我們必須要知道目標端的網路地址與埠,還需要知道所暴露的協議,然後才能夠編寫程式碼比如使用 HttpClient 去進行呼叫,這個“知道”的過程,往往被稱為服務發現。

分散式的架構帶來了解耦的效果,使得不同模組可以分別變化,不同的模組可以根據自身特點選擇程式語言、技術棧與資料庫,可以根據負載選擇彈性與執行環境,使得系統從傳統的三層架構變成了一個個獨立的、自治的服務,往往這些服務與業務領域非常契合,比如訂單服務並不會關心如何傳送郵件給客戶,司機管理服務並不需要關注乘客的狀態,這些服務應該是網狀的,是通過組合來完成業務。解耦帶來了響應變化的能力,可以讓我們大膽試錯,我們希望啟動一個服務的成本和編寫一個模組的成本類似,同時編寫服務、進行重構的成本也需要降低至於程式碼修改一般。在這種需求下,我們也希望服務之間的呼叫能夠簡單,最好能像方法呼叫一樣簡單。

但是 Armon(HashiCorp 的創始人)在他的技術分享中提到,實現分散式是沒有免費午餐的,一旦你通過網路進行遠端呼叫,那網路是否可達、延遲與頻寬、訊息的封裝以及額外的客戶端程式碼都是代價,在此基礎上,有時候我們還會有負載均衡、斷路器、健康檢查、授權驗證、鏈路監控等需求,這些問題是之前不需要考慮的。所以,我們需要有“產品”來幫助我們解決這類問題,我們可以先從 Eureka 開始回顧、整理。

1.png

一個單體應用部署在多臺伺服器中,模組間通過方法直接呼叫。

2.png

分散式的情況下,模組之間的呼叫通過網路,也許使用 HTTP 或者其他 RPC 協議。

Spring Cloud Eureka

從 Netflix OSS 發展而來的 Spring Cloud 依舊是目前最流行的實現微服務架構的方式,我們很難描述 Spring Cloud 是什麼,它是一些獨立的應用程式、特定的依賴與註解、在應用層實現的一攬子的微服務解決方案。由於是應用層解決方案,那就說明了 Spring Cloud 很容易與執行環境解耦,雖然限定了程式語言為 Java 但是也可以接受,因為在網際網路領域 Java 佔有絕對的支配地位,特別是在國內。所以服務發現 Eureka、斷路器 Hystrix、閘道器 Zuul 與負載均衡 Ribbon 非常流行直至今日,再加上 Netflix 成功的使用這些技術構建了一個龐大的分散式系統,這些成功經驗使得 Spring Cloud 一度是微服務的代表。

對於 Eureka 來說,我們知道不論是 Eureka Server 還是 Client 端都存在大量的快取以及 TTL 機制,因為 Eureka 並不傾向於維持系統中服務狀態的一致性,雖然我們的 Client 在註冊服務時,Server 會嘗試將其同步至其他 Server,但是並不能保證一致性。同時,Client 的下線或者某個節點的斷網也是需要有 timeout 來控制是否移除,並不是實時的同步給所有 Server 與 Client。的確,通過“最大努力的複製(best effort replication)” 可以讓整個模型變得簡單與高可用,我們在進行 A -> B 的呼叫時,服務 A 只要讀取一個 B 的地址,就可以進行 RESTful 請求,如果 B 的這個地址下線或不可達,則有 Hystrix 之類的機制讓我們快速失敗。

對於 Netflix 來說,這樣的模型是非常合理的,首先服務與 node 的關係相對靜態,一旦一個服務投入使用其使用的虛擬機器(我記得大多是 AWS EC2)也確定下來,node 的 IP 地址與網路也是靜態,所以很少會出現頻繁上線、下線的情況,即使在進行頻繁迭代時,也是更新執行的 jar,而不會修改執行例項。國內很多實現也是類似的,在我們參與的專案中,很多客戶的架構圖上總會清晰的表達:這幾臺機器是 xx 服務,那幾臺是 xx 服務,他們使用 Eureka 註冊發現。第二,所有的實現都是 Java Code,高階語言雖然在效率上不如系統級語言,但是易於表達與修改,使得 Netflix 能夠保持與雲環境、IDC 的距離,並且很多功能通過 annotation 加入,也能讓程式碼修改的成本變低。

3.png

Eureka 的邏輯架構很清楚的表達了 Eureka Client、Server 之間的關係,以及他們的 Remote Call 是呼叫的。

Eureka 的限制隨著容器的流行被逐漸的放大,我們漸漸的發現 Eureka 在很多場景下並不能滿足我們的需求。首先對於弱一致性的需求使得我們在進行彈性伸縮,或者藍綠髮布時就會出現一定的錯誤,因為節點下線的訊息是需要時間才能同步的。在容器時代,我們希望應用程式是無狀態的,可以優雅的啟動和終止,並且易於橫向擴充套件。由於容器提供了很好的封裝能力,至於內部的程式碼是 Java 還是 Golang 並不是呼叫者關心的事情,這就帶來了第二個問題,雖然使用 Java annotation 的方式方便使用,但是必須是 Java 語言而且需要一大堆 SDK,很多例如負載均衡的能力無法做到程序之外。Eureka 會讓系統變得很複雜,如果你有十幾個微服務,每個微服務都有四五個節點,那維護這麼多節點的地址就顯得非常臃腫,對於呼叫者來說它只需要關注自己所依賴的服務。

Hashicorp Consul

Consul 作為繼任者解決了很多問題,首先 Consul 使用了現在流行的 service mesh 模式,在一個“控制面”中提供了服務發現、配置管理與劃分等能力,與 Netflix OSS 套件一樣,任何的這些功能都是可以獨立使用的,也可以組合在一起去構建我們自己的 service mesh 實現。Service mesh 作為實現微服務架構的新模式,核心思想在於程序之外 out-of-process 的實現功能,也就是 sidecar,我們可以通過 proxy 實現 interceptor 在不改變程式碼的情況下注入某些功能,比如服務註冊發現、比如日誌記錄、比如服務之間的授信。

4.png

Consul 的架構更為全面並複雜,支援多 Data Center,使用了 GOSSIP 協議,有 Control Panel 提供 Mesh 能力,基本上解決為了 Eureka 的問題。

與 Eureka 不同,Consul 通過 Raft 協議提供了強一致性,支援各種型別的 health check,而且這些 health check 也是分散式的,也不需要使用大量的 SDK 來在程式碼中整合這些功能。由於 Consul 代理了流量,所以可以支援傳輸安全 TLS,在架構設計上 Consul 與 Istio 還是有所類似,但是的確還是有如下的不足:

  • 沒有提供 native 的方式去配置 circuit breaker,Netflix OSS suite 最大的優勢是,Eureka\Hystrix\Ribbon 能夠提供完整的分散式解決方案,特別是 Hystrix,能夠提供“快速失敗”的能力,但是 Consul 的話,目前還沒有提供原生的方案。
  • 同樣的,整合 Consul 也變得比較麻煩,agent 的啟動不是那麼簡單,特別是在 k8s 上我們需要多級 sidecar 時,同時其提供的 ACL 配置也難以理解和使用。相對於內部的實現,管控用的 GUI 介面也是大家吐槽比較多的地方。
  • 相對於服務發現,其他 Consul 所提供的功能就顯得不那麼誘人了,比如 Key-Value 資料庫以及多資料中心支援,當然我認為這也不是核心內容。
  • 政治因素,雖然是開源產品,但是其公司也參與了對中國企業的制裁,所以在國內是無法合法使用該產品的。

Alibaba Nacos

Nacos 已經是目前專案中的首選,特別是那些急需 Eureka 替代品的場景下,當然這不是因為我們無法使用 Consul,更多的是因為 Nacos 已經成為了穩定的雲產品,你無需自己部署、運維、管控一個 Consul 或者別的機制,直接使用 Nacos 即可

而且 Nacos 替代 Eureka 基本上是一行程式碼的事情,某些時候客戶並沒有足夠的預算和成本投入微服務的改造與升級,所以在進行微服務上雲的過程中,Nacos 是目前的首選。相對於 Consul 自己發明輪子的做法,Nacos 在協議的支援更全面,包括 Dubbo 與 gRPC,這對於廣泛使用 Dubbo 的國內企業是一個巨大的優勢。

在這裡筆者就不擴充套件 Nacos 的功能與內部實現了,Nacos 團隊所做的科普、示例以及深度的文章都已經足夠多了,已經所有的文件都可以在官網找到,程式碼也開源,有興趣的話請大家移步 Nacos 團隊的部落格:https://nacos.io/zh-cn/blog/index.html

SLB、Kubernetes Service 與 Istio

實際上,我們剛才提到的“服務發現”是“客戶端的服務發現(client-side service discovery)”,假設訂單系統執行在四個節點上,每個節點有不同的 IP 地址,那我們的呼叫者在發起 RPC 或者 HTTP 請求前,是必須要清楚到底要呼叫個節點的,在 Eureka 的過程中,我們會通過 Ribbon 使用輪詢或者其他方式找到那個地址與埠,並且發起請求。

這個過程是非常直接的,作為呼叫者,我有所有可用服務的列表,所以我可以很靈活的決定我該呼叫誰,我可以簡單的實現斷路器。但是缺點的話也很清楚,我們必須依賴 SDK,如果是不同的程式語言或框架,我們就必須要編寫自己的實現。

5.png

像蜘蛛網一樣的互相呼叫過程,並且每個服務都必須有 SDK 來實現客戶端的服務發現,比如 IP3 這臺機器,是由它來決定最終訪問 Service 2 的那個節點。同時,IP23 剛剛上線,但是還沒有流量過來。

但是在邏輯架構上,這個系統又非常簡單,serivce 1 -> service 2 -> service 3\4。對於研發或者運維人員,你是希望 order service 是這樣描述:

https://internal.order-service.some-company.com:8443/ - online

還是,這樣一大堆地址,並且不確定的狀態?

http://192.168.20.19:8080 - online
http://192.168.20.20:8080 - online
http://192.168.20.21:8080 - offline
http://192.168.20.22:8080 - offline

事實上斷路器所提供的快速失敗在客戶端的服務發現中非常重要,但是這個功能並不完美,我們想要的場景是呼叫的服務都是可用的,而不是等呼叫鏈路走到個節點後再快速失敗,而這時候另一個節點是可以提供服務的。

而且對於一個訂單服務,在外來看它就應該是“一個服務”,它內部的幾個節點是否可用並不是呼叫者需要關心的,這些細節我們並不想關心

在微服務世界,我們很希望每個服務都是獨立且完整的,就像面向物件程式設計一樣,細節應該被隱藏到模組內部。按照這種想法,服務端的服務發現(server-side serivce discovery)會更具有優勢,其實我們對這種模式並不陌生,在使用 NGINX 進行負載均衡的代理時,我們就在實踐這種模式,一旦流量到了 proxy,由 proxy 決定下發至哪個節點,而 proxy 可以通過 healthcheck 來判斷哪個節點是否健康。

6.png

邏輯上還是 serivce 1 -> service 2 -> service 3\4,但是 LB 或者 Service 幫助我們隱藏了細節,從 Service 1 看 Service 2,就只能看到一個服務,而不是一堆機器。

服務端服務發現的確有很多優勢,比如隱藏細節,讓客戶端無需關心最終提供服務的節點,同時也消除了語言與框架的限制。缺點也很明顯,每個服務都有這一層代理,而且如果你的平臺不提供這樣的能力的話,自己手動去部署與管理高可用的 proxy 元件,成本是巨大的。但是這個缺陷已經有很好的應對,你可以使用阿里雲的 SLB 實現,不論 client 使用 HTTP 還是 PRC 都可以通過 DNS 名稱來訪問 SLB,甚至實現全鏈路 TLS 也非常簡單,而 SLB 可以管理多個 ECS 例項,也支援例項的 health check 與彈性,這就像一個註冊中心一樣,每個例項的狀態實際上儲存在 SLB 之上。雲平臺本身就是利於管控和使用,加入更多的比如驗證、限流等能力。

Kubernetes Service 也具有同樣的能力,隨著容器化的逐漸成熟,在雲原生的落地中 ACK 是必不可少的執行環境,那通過 Service 去綜合管理一組服務的 pod 與之前提到的 SLB 的方式是一致的,當然相對於平臺繫結的 SLB + ECS 方案,k8s 的 service 更加開放與透明,也支持者企業進行混合雲的落地。

作為 service mesh 目前最流行的產品,Istio 使用了 virtual service 與 destination rule 來解決了服務註冊與發現的問題,virtual service 與其他 proxy 一樣,都非常強調與客戶端的解耦,除了我們日常使用的輪詢式的呼叫方式,virtual service 可以提供更靈活的流量控制,比如“20% 的流量去新版本”或者“來自某個地區的使用者使用版本 2”,實現金絲雀釋出也比較簡單。相對於 kubernetes serivce, virtual service 可控制的地方更多,比如通過 destination rule 可控制下游,也可以實現根據路徑匹配選擇下游服務,也可以加入權重,重試策略等等。你同樣可以通過 Istio 的能力實現服務間的傳輸安全,比如全鏈路的 TLS,也可以做到細粒度的服務授權,而這所有的一切都是不需要寫入業務程式碼中的,只要進行一些配置就好。但是這也不是免費的,隨著服務數量的上升,手動的管理這麼多的 proxy 與 sidecar,沒有自動化的報警和響應手段,都會造成效率的下降。

ZooKeeper 真的不適合做註冊發現嗎?

在微服務剛剛開始流行的時候,很多企業在探索的過程中開始使用 ZooKeeper 進行服務發現的實現,一方面是 ZooKeeper 的可靠、簡單、天然分散式的優勢可以說是直接的選擇,另一方面也是因為沒有其他的機制讓我們模仿。下面這篇釋出於 2014 年底的文章詳細的說明了為什麼在服務發現中,使用 Eureka 會是一個更好的解決方案。

https://medium.com/knerd/eureka-why-you-shouldnt-use-zookeeper-for-service-discovery-4932c5c7e764

在 CAP 理論中,ZooKeeper 是面向 CP 的,在可用性(available)與一致性(consistent)中,ZooKeeper 選擇了一致性,這是因為 ZooKeeper 最開始用於進行分散式的系統管理與協調(coordination),比如控制大資料的叢集或者 kafka 之類的,一致性在這類系統中是紅線。文章還提到了“如果我們自己為 ZooKeeper 加上一種客戶端快取的能力,快取了其他服務地址的話,這樣就能緩解在叢集不可用時,依舊可以進行服務發現的能力,並且 Pinterest 與 Airbnb 都有類似的實現”,的確,看起來這樣是修復了問題,但是在原理上和 Eureka 這種 AP 型的系統就沒有多少區別了,使用了 Cache 就必須要在一致性上進行妥協,必須要自己的實現才能快取失效、無法同步等問題。

使用 ZooKeeper 實現服務發現並沒有什麼問題,問題是使用者必須要想清楚在這樣一個分散式系統中,AP 還是 CP 是最終的目標,如果我們的系統是在劇烈變化,面向終端消費者,但是又沒有交易或者對一致性要求不高,那這種情況下 AP 是較為理想的選擇,如果是一個交易系統,一致性顯然更重要。其實實現一個自己的服務發現並沒有大多數人想的那麼難,如果有一個 KV Store 去儲存服務的狀態,再加上註冊、更新等機制,這也是很多服務註冊與發現和配置管理經常做在一起的原因,剩下的事情就是 AP 與 CP 的選擇了,下面這篇文章是一個很好的例子,也提到了其他的服務發現,請查閱。

https://dzone.com/articles/zookeeper-for-microservice-registration-and-discov

一些思考

進行技術選型的壓力是非常之大的,隨著技術的演進、人員的更替,很多系統逐漸變成了無法修改、無法移動的存在,作為技術負責人我們在進行這件工作時應該更加註意,選擇某項技術時也需要考慮自己能否負擔的起。Spring Cloud 提供的微服務方案在易用性上肯定好於自己在 Kubernetes 上發明新的,但是我們也擔心它尾大不掉,所以在我們現在接觸的專案中,對 Spring Cloud 上的應用進行遷移、重構還是可以負擔的起的,但我非常擔心幾年後,改造的成本就會變的非常高,最終導向重寫的境地。

7.png

我們將呼叫方式分為“同步”與“非同步”兩種情況,在非同步呼叫時,使用 MQ 傳輸事件,或者使用 Kafka 進行 Pub / Sub,事實上,Event Driven 的系統更有靈活性,也符合 Domain 的封閉。

服務與服務之前的呼叫不僅僅是同步式的,別忘了在非同步呼叫或者 pub-sub 的場景,我們會使用中介軟體幫助我們解耦。雖然中介軟體(middleware)這個詞很容易讓人產生困惑,它並不能很好的描述它的功能,但最少在實現訊息隊裡、Event Bus、Stream 這種需求時,現在已有的產品已經非常成熟,我們曾經使用 Serverless 實現了一個完整的 web service,其中模組的互相呼叫就是通過事件。但是這並不是完美的,“如無必要,勿增實體”,加入了額外的系統或者應用就得去運維與管理,就需要考慮失效,考慮 failure 策略,同時在這種場景下實現“exactly once”的目標更為複雜,果然在分散式的世界中,真是沒有一口飯是免費的。

參考

https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance
https://www.consul.io/docs/architecture
https://dzone.com/articles/zookeeper-for-microservice-registration-and-discov
https://medium.com/knerd/eureka-why-you-shouldnt-use-zookeeper-for-service-discovery-4932c5c7e764
https://istio.io/latest/docs/concepts/traffic-management/#why-use-virtual-services
https://microservices.io/patterns/server-side-discovery.html

作者簡介

**張羽辰(同昭)**阿里雲交付專家,有著近十年研發經驗,是一名軟體工程師、架構師、諮詢師,從 2016 年開始採用容器化、微服務、Serverless 等技術進行雲時代的應用開發。同時也關注在分散式應用中的安全治理問題,整理《微服務安全手冊》,對資料、應用、身份安全都有一定的研究。

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