服務(wù)發(fā)現(xiàn)技術(shù)選型那點事兒
作者 | 張羽辰(同昭)

引子 —— 什么是服務(wù)發(fā)現(xiàn)?
近日來,和很多來自傳統(tǒng)行業(yè)、國企、政府的客戶在溝通技術(shù)細節(jié)時,發(fā)現(xiàn)云原生所代表的技術(shù)已經(jīng)逐漸成為大家的共識,從一個虛無縹緲的概念漸漸變成這些客戶的下一個技術(shù)戰(zhàn)略。自然,應(yīng)用架構(gòu)就會提到微服務(wù),以及其中最重要的分布式協(xié)作的模式——服務(wù)發(fā)現(xiàn)。模式(pattern)是指在特定上下文中的解決方案,很適合描述服務(wù)發(fā)現(xiàn)這個過程。不過相對于 2016 年,現(xiàn)在我們最少有十多種的方式能實現(xiàn)服務(wù)發(fā)現(xiàn),這的確是個好時機來進行回顧和展望,最終幫助我們進行技術(shù)選型與確定演進方向。
微服務(wù)脫胎于 SOA 理論,核心是分布式,但單體應(yīng)用中,模塊之間的調(diào)用(比如讓消息服務(wù)給客戶發(fā)送一條數(shù)據(jù))是通過方法,而所發(fā)出的消息是在同一塊內(nèi)存之中,我們知道這樣的代價是非常小的,方法調(diào)用的成本可能是納秒級別,我們從未想過這樣會有什么問題。
但是在微服務(wù)的世界中,模塊與模塊分別部署在不同的地方,它們之間的約束或者協(xié)議由方法簽名轉(zhuǎn)變?yōu)楦呒壍膮f(xié)議,比如 RESTful 、PRC,在這種情況下,調(diào)用一個模塊就需要通過網(wǎng)絡(luò),我們必須要知道目標端的網(wǎng)絡(luò)地址與端口,還需要知道所暴露的協(xié)議,然后才能夠編寫代碼比如使用 HttpClient 去進行調(diào)用,這個“知道”的過程,往往被稱為服務(wù)發(fā)現(xiàn)。
分布式的架構(gòu)帶來了解耦的效果,使得不同模塊可以分別變化,不同的模塊可以根據(jù)自身特點選擇編程語言、技術(shù)棧與數(shù)據(jù)庫,可以根據(jù)負載選擇彈性與運行環(huán)境,使得系統(tǒng)從傳統(tǒng)的三層架構(gòu)變成了一個個獨立的、自治的服務(wù),往往這些服務(wù)與業(yè)務(wù)領(lǐng)域非常契合,比如訂單服務(wù)并不會關(guān)心如何發(fā)送郵件給客戶,司機管理服務(wù)并不需要關(guān)注乘客的狀態(tài),這些服務(wù)應(yīng)該是網(wǎng)狀的,是通過組合來完成業(yè)務(wù)。解耦帶來了響應(yīng)變化的能力,可以讓我們大膽試錯,我們希望啟動一個服務(wù)的成本和編寫一個模塊的成本類似,同時編寫服務(wù)、進行重構(gòu)的成本也需要降低至于代碼修改一般。在這種需求下,我們也希望服務(wù)之間的調(diào)用能夠簡單,最好能像方法調(diào)用一樣簡單。
但是 Armon(HashiCorp 的創(chuàng)始人)在他的技術(shù)分享中提到,實現(xiàn)分布式是沒有免費午餐的,一旦你通過網(wǎng)絡(luò)進行遠程調(diào)用,那網(wǎng)絡(luò)是否可達、延遲與帶寬、消息的封裝以及額外的客戶端代碼都是代價,在此基礎(chǔ)上,有時候我們還會有負載均衡、斷路器、健康檢查、授權(quán)驗證、鏈路監(jiān)控等需求,這些問題是之前不需要考慮的。所以,我們需要有“產(chǎn)品”來幫助我們解決這類問題,我們可以先從 Eureka 開始回顧、整理。
一個單體應(yīng)用部署在多臺服務(wù)器中,模塊間通過方法直接調(diào)用。

分布式的情況下,模塊之間的調(diào)用通過網(wǎng)絡(luò),也許使用 HTTP 或者其他 RPC 協(xié)議。

Spring Cloud Eureka
從 Netflix OSS 發(fā)展而來的 Spring Cloud 依舊是目前最流行的實現(xiàn)微服務(wù)架構(gòu)的方式,我們很難描述 Spring Cloud 是什么,它是一些獨立的應(yīng)用程序、特定的依賴與注解、在應(yīng)用層實現(xiàn)的一攬子的微服務(wù)解決方案。
由于是應(yīng)用層解決方案,那就說明了 Spring Cloud 很容易與運行環(huán)境解耦,雖然限定了編程語言為 Java 但是也可以接受,因為在互聯(lián)網(wǎng)領(lǐng)域 Java 占有絕對的支配地位,特別是在國內(nèi)。所以服務(wù)發(fā)現(xiàn) Eureka、斷路器 Hystrix、網(wǎng)關(guān) Zuul 與負載均衡 Ribbon 非常流行直至今日,再加上 Netflix 成功的使用這些技術(shù)構(gòu)建了一個龐大的分布式系統(tǒng),這些成功經(jīng)驗使得 Spring Cloud 一度是微服務(wù)的代表。
對于 Eureka 來說,我們知道不論是 Eureka Server 還是 Client 端都存在大量的緩存以及 TTL 機制,因為 Eureka 并不傾向于維持系統(tǒng)中服務(wù)狀態(tài)的一致性,雖然我們的 Client 在注冊服務(wù)時,Server 會嘗試將其同步至其他 Server,但是并不能保證一致性。同時,Client 的下線或者某個節(jié)點的斷網(wǎng)也是需要有 timeout 來控制是否移除,并不是實時的同步給所有 Server 與 Client。
的確,通過“最大努力的復(fù)制(best effort replication)” 可以讓整個模型變得簡單與高可用,我們在進行 A -> B 的調(diào)用時,服務(wù) A 只要讀取一個 B 的地址,就可以進行 RESTful 請求,如果 B 的這個地址下線或不可達,則有 Hystrix 之類的機制讓我們快速失敗。
對于 Netflix 來說,這樣的模型是非常合理的,首先服務(wù)與 node 的關(guān)系相對靜態(tài),一旦一個服務(wù)投入使用其使用的虛擬機(我記得大多是 AWS EC2)也確定下來,node 的 IP 地址與網(wǎng)絡(luò)也是靜態(tài),所以很少會出現(xiàn)頻繁上線、下線的情況,即使在進行頻繁迭代時,也是更新運行的 jar,而不會修改運行實例。國內(nèi)很多實現(xiàn)也是類似的,在我們參與的項目中,很多客戶的架構(gòu)圖上總會清晰的表達:這幾臺機器是 xx 服務(wù),那幾臺是 xx 服務(wù),他們使用 Eureka 注冊發(fā)現(xiàn)。第二,所有的實現(xiàn)都是 Java Code,高級語言雖然在效率上不如系統(tǒng)級語言,但是易于表達與修改,使得 Netflix 能夠保持與云環(huán)境、IDC 的距離,并且很多功能通過 annotation 加入,也能讓代碼修改的成本變低。

Eureka 的邏輯架構(gòu)很清楚地表達了 Eureka Client、Server 之間的關(guān)系,以及它們的 Remote Call 是調(diào)用的。
Eureka 的限制隨著容器的流行被逐漸放大,我們漸漸發(fā)現(xiàn) Eureka 在很多場景下并不能滿足我們的需求。
首先對于弱一致性的需求使得我們在進行彈性伸縮,或者藍綠發(fā)布時就會出現(xiàn)一定的錯誤,因為節(jié)點下線的消息是需要時間才能同步的。在容器時代,我們希望應(yīng)用程序是無狀態(tài)的,可以優(yōu)雅的啟動和終止,并且易于橫向擴展。由于容器提供了很好的封裝能力,至于內(nèi)部的代碼是 Java 還是 Golang 并不是調(diào)用者關(guān)心的事情,這就帶來了第二個問題,雖然使用 Java annotation 的方式方便使用,但是必須是 Java 語言而且需要一大堆 SDK,很多例如負載均衡的能力無法做到進程之外。Eureka 會讓系統(tǒng)變得很復(fù)雜,如果你有十幾個微服務(wù),每個微服務(wù)都有四五個節(jié)點,那維護這么多節(jié)點的地址就顯得非常臃腫,對于調(diào)用者來說它只需要關(guān)注自己所依賴的服務(wù)。

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

Consul 的架構(gòu)更為全面并復(fù)雜,支持多 Data Center,使用了 GOSSIP 協(xié)議,有 Control Panel 提供 Mesh 能力,基本上解決為了 Eureka 的問題。
與 Eureka 不同,Consul 通過 Raft 協(xié)議提供了強一致性,支持各種類型的 health check,而且這些 health check 也是分布式的,也不需要使用大量的 SDK 來在代碼中集成這些功能。
由于 Consul 代理了流量,所以可以支持傳輸安全 TLS,在架構(gòu)設(shè)計上 Consul 與 Istio 還是有所類似,但是的確還是有如下的不足:
沒有提供 native 的方式去配置 circuit breaker,Netflix OSS suite 最大的優(yōu)勢是,Eureka\Hystrix\Ribbon 能夠提供完整的分布式解決方案,特別是 Hystrix,能夠提供“快速失敗”的能力,但是 Consul 的話,目前還沒有提供原生的方案。
同樣的,集成 Consul 也變得比較麻煩,agent 的啟動不是那么簡單,特別是在 k8s 上我們需要多級 sidecar 時,同時其提供的 ACL 配置也難以理解和使用。相對于內(nèi)部的實現(xiàn),管控用的 GUI 界面也是大家吐槽比較多的地方。
相對于服務(wù)發(fā)現(xiàn),其他 Consul 所提供的功能就顯得不那么誘人了,比如 Key-Value 數(shù)據(jù)庫以及多數(shù)據(jù)中心支持,當(dāng)然我認為這也不是核心內(nèi)容。
政治因素,雖然是開源產(chǎn)品,但是其公司也參與了對中國企業(yè)的制裁,所以在國內(nèi)是無法合法使用該產(chǎn)品的。

Alibaba Nacos
Nacos 已經(jīng)是目前項目中的首選,特別是那些急需 Eureka 替代品的場景下,當(dāng)然這不是因為我們無法使用 Consul,更多的是因為 Nacos 已經(jīng)成為了穩(wěn)定的云產(chǎn)品,你無需自己部署、運維、管控一個 Consul 或者別的機制,直接使用 Nacos 即可。
而且 Nacos 替代 Eureka 基本上是一行代碼的事情,某些時候客戶并沒有足夠的預(yù)算和成本投入微服務(wù)的改造與升級,所以在進行微服務(wù)上云的過程中,Nacos 是目前的首選。相對于 Consul 自己發(fā)明輪子的做法,Nacos 在協(xié)議的支持更全面,包括 Dubbo 與 gRPC,這對于廣泛使用 Dubbo 的國內(nèi)企業(yè)是一個巨大的優(yōu)勢。
在這里筆者就不擴展 Nacos 的功能與內(nèi)部實現(xiàn)了,Nacos 團隊所做的科普、示例以及深度的文章都已經(jīng)足夠多了,已經(jīng)所有的文檔都可以在官網(wǎng)找到,代碼也開源,有興趣的話請大家移步 Nacos 團隊的博客:https://nacos.io/zh-cn/blog/index.html

SLB、Kubernetes Service 與 Istio
實際上,我們剛才提到的“服務(wù)發(fā)現(xiàn)”是“客戶端的服務(wù)發(fā)現(xiàn)(client-side service discovery)”,假設(shè)訂單系統(tǒng)運行在四個節(jié)點上,每個節(jié)點有不同的 IP 地址,那我們的調(diào)用者在發(fā)起 RPC 或者 HTTP 請求前,是必須要清楚到底要調(diào)用個節(jié)點的,在 Eureka 的過程中,我們會通過 Ribbon 使用輪詢或者其他方式找到那個地址與端口,并且發(fā)起請求。
這個過程是非常直接的,作為調(diào)用者,我有所有可用服務(wù)的列表,所以我可以很靈活的決定我該調(diào)用誰,我可以簡單的實現(xiàn)斷路器。但是缺點的話也很清楚,我們必須依賴 SDK,如果是不同的編程語言或框架,我們就必須要編寫自己的實現(xiàn)。
像蜘蛛網(wǎng)一樣的互相調(diào)用過程,并且每個服務(wù)都必須有 SDK 來實現(xiàn)客戶端的服務(wù)發(fā)現(xiàn),比如 IP3 這臺機器,是由它來決定最終訪問 Service 2 的那個節(jié)點。同時,IP23 剛剛上線,但是還沒有流量過來。
但是在邏輯架構(gòu)上,這個系統(tǒng)又非常簡單,serivce 1 -> service 2 -> service 3\4。對于研發(fā)或者運維人員,你是希望 order service 是這樣描述:
https://internal.order-service.some-company.com:8443/?-?online
還是這樣一大堆地址,并且不確定的狀態(tài)?
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
事實上斷路器所提供的快速失敗在客戶端的服務(wù)發(fā)現(xiàn)中非常重要,但是這個功能并不完美,我們想要的場景是調(diào)用的服務(wù)都是可用的,而不是等調(diào)用鏈路走到個節(jié)點后再快速失敗,而這時候另一個節(jié)點是可以提供服務(wù)的。
而且對于一個訂單服務(wù),在外來看它就應(yīng)該是“一個服務(wù)”,它內(nèi)部的幾個節(jié)點是否可用并不是調(diào)用者需要關(guān)心的,這些細節(jié)我們并不想關(guān)心。
在微服務(wù)世界,我們很希望每個服務(wù)都是獨立且完整的,就像面向?qū)ο缶幊桃粯樱毠?jié)應(yīng)該被隱藏到模塊內(nèi)部。按照這種想法,服務(wù)端的服務(wù)發(fā)現(xiàn)(server-side serivce discovery)會更具有優(yōu)勢,其實我們對這種模式并不陌生,在使用 NGINX 進行負載均衡的代理時,我們就在實踐這種模式,一旦流量到了 proxy,由 proxy 決定下發(fā)至哪個節(jié)點,而 proxy 可以通過 healthcheck 來判斷哪個節(jié)點是否健康。

邏輯上還是 serivce 1 -> service 2 -> service 3\4,但是 LB 或者 Service 幫助我們隱藏了細節(jié),從 Service 1 看 Service 2,就只能看到一個服務(wù),而不是一堆機器。
服務(wù)端服務(wù)發(fā)現(xiàn)的確有很多優(yōu)勢,比如隱藏細節(jié),讓客戶端無需關(guān)心最終提供服務(wù)的節(jié)點,同時也消除了語言與框架的限制。
缺點也很明顯,每個服務(wù)都有這一層代理,而且如果你的平臺不提供這樣的能力的話,自己手動去部署與管理高可用的 proxy 組件,成本是巨大的。但是這個缺陷已經(jīng)有很好的應(yīng)對,你可以使用阿里云的 SLB 實現(xiàn),不論 client 使用 HTTP 還是 PRC 都可以通過 DNS 名稱來訪問 SLB,甚至實現(xiàn)全鏈路 TLS 也非常簡單,而 SLB 可以管理多個 ECS 實例,也支持實例的 health check 與彈性,這就像一個注冊中心一樣,每個實例的狀態(tài)實際上保存在 SLB 之上。云平臺本身就是利于管控和使用,加入更多的比如驗證、限流等能力。
Kubernetes Service 也具有同樣的能力,隨著容器化的逐漸成熟,在云原生的落地中 ACK 是必不可少的運行環(huán)境,那通過 Service 去綜合管理一組服務(wù)的 pod 與之前提到的 SLB 的方式是一致的,當(dāng)然相對于平臺綁定的 SLB + ECS 方案,k8s 的 service 更加開放與透明,也支持者企業(yè)進行混合云的落地。
作為 service mesh 目前最流行的產(chǎn)品,Istio 使用了 virtual service 與 destination rule 來解決了服務(wù)注冊與發(fā)現(xiàn)的問題,virtual service 與其他 proxy 一樣,都非常強調(diào)與客戶端的解耦,除了我們?nèi)粘J褂玫妮喸兪降恼{(diào)用方式,virtual service 可以提供更靈活的流量控制,比如“20% 的流量去新版本”或者“來自某個地區(qū)的用戶使用版本 2”,實現(xiàn)金絲雀發(fā)布也比較簡單。
相對于 kubernetes serivce, virtual service 可控制的地方更多,比如通過 destination rule 可控制下游,也可以實現(xiàn)根據(jù)路徑匹配選擇下游服務(wù),也可以加入權(quán)重,重試策略等等。你同樣可以通過 Istio 的能力實現(xiàn)服務(wù)間的傳輸安全,比如全鏈路的 TLS,也可以做到細粒度的服務(wù)授權(quán),而這所有的一切都是不需要寫入業(yè)務(wù)代碼中的,只要進行一些配置就好。但是這也不是免費的,隨著服務(wù)數(shù)量的上升,手動的管理這么多的 proxy 與 sidecar,沒有自動化的報警和響應(yīng)手段,都會造成效率的下降。

ZooKeeper 真的不適合做注冊發(fā)現(xiàn)嗎?
在微服務(wù)剛剛開始流行的時候,很多企業(yè)在探索的過程中開始使用 ZooKeeper 進行服務(wù)發(fā)現(xiàn)的實現(xiàn),一方面是 ZooKeeper 的可靠、簡單、天然分布式的優(yōu)勢可以說是直接的選擇,另一方面也是因為沒有其他的機制讓我們模仿。下面這篇發(fā)布于 2014 年底的文章詳細的說明了為什么在服務(wù)發(fā)現(xiàn)中,使用 Eureka 會是一個更好的解決方案。
https://medium.com/knerd/eureka-why-you-shouldnt-use-zookeeper-for-service-discovery-4932c5c7e764
在 CAP 理論中,ZooKeeper 是面向 CP 的,在可用性(available)與一致性(consistent)中,ZooKeeper 選擇了一致性,這是因為 ZooKeeper 最開始用于進行分布式的系統(tǒng)管理與協(xié)調(diào)(coordination),比如控制大數(shù)據(jù)的集群或者 kafka 之類的,一致性在這類系統(tǒng)中是紅線。
文章還提到了“如果我們自己為 ZooKeeper 加上一種客戶端緩存的能力,緩存了其他服務(wù)地址的話,這樣就能緩解在集群不可用時,依舊可以進行服務(wù)發(fā)現(xiàn)的能力,并且 Pinterest 與 Airbnb 都有類似的實現(xiàn)”,的確,看起來這樣是修復(fù)了問題,但是在原理上和 Eureka 這種 AP 型的系統(tǒng)就沒有多少區(qū)別了,使用了 Cache 就必須要在一致性上進行妥協(xié),必須要自己的實現(xiàn)才能緩存失效、無法同步等問題。
使用 ZooKeeper 實現(xiàn)服務(wù)發(fā)現(xiàn)并沒有什么問題,問題是使用者必須要想清楚在這樣一個分布式系統(tǒng)中,AP 還是 CP 是最終的目標,如果我們的系統(tǒng)是在劇烈變化,面向終端消費者,但是又沒有交易或者對一致性要求不高,那這種情況下 AP 是較為理想的選擇,如果是一個交易系統(tǒng),一致性顯然更重要。其實實現(xiàn)一個自己的服務(wù)發(fā)現(xiàn)并沒有大多數(shù)人想的那么難,如果有一個 KV Store 去存儲服務(wù)的狀態(tài),再加上注冊、更新等機制,這也是很多服務(wù)注冊與發(fā)現(xiàn)和配置管理經(jīng)常做在一起的原因,剩下的事情就是 AP 與 CP 的選擇了,下面這篇文章是一個很好的例子,也提到了其他的服務(wù)發(fā)現(xiàn),請查閱。
https://dzone.com/articles/zookeeper-for-microservice-registration-and-discov

一些思考
進行技術(shù)選型的壓力是非常之大的,隨著技術(shù)的演進、人員的更替,很多系統(tǒng)逐漸變成了無法修改、無法移動的存在,作為技術(shù)負責(zé)人我們在進行這件工作時應(yīng)該更加注意,選擇某項技術(shù)時也需要考慮自己能否負擔(dān)的起。
Spring Cloud 提供的微服務(wù)方案在易用性上肯定好于自己在 Kubernetes 上發(fā)明新的,但是我們也擔(dān)心它尾大不掉,所以在我們現(xiàn)在接觸的項目中,對 Spring Cloud 上的應(yīng)用進行遷移、重構(gòu)還是可以負擔(dān)的起的,但我非常擔(dān)心幾年后,改造的成本就會變的非常高,最終導(dǎo)向重寫的境地。

我們將調(diào)用方式分為“同步”與“異步”兩種情況,在異步調(diào)用時,使用 MQ 傳輸事件,或者使用 Kafka 進行 Pub / Sub,事實上,Event Driven 的系統(tǒng)更有靈活性,也符合 Domain 的封閉。
服務(wù)與服務(wù)之前的調(diào)用不僅僅是同步式的,別忘了在異步調(diào)用或者 pub-sub 的場景,我們會使用中間件幫助我們解耦。
雖然中間件(middleware)這個詞很容易讓人產(chǎn)生困惑,它并不能很好的描述它的功能,但最少在實現(xiàn)消息隊里、Event Bus、Stream 這種需求時,現(xiàn)在已有的產(chǎn)品已經(jīng)非常成熟,我們曾經(jīng)使用 Serverless 實現(xiàn)了一個完整的 web service,其中模塊的互相調(diào)用就是通過事件。但是這并不是完美的,“如無必要,勿增實體”,加入了額外的系統(tǒng)或者應(yīng)用就得去運維與管理,就需要考慮失效,考慮 failure 策略,同時在這種場景下實現(xiàn)“exactly once”的目標更為復(fù)雜,果然在分布式的世界中,真是沒有一口飯是免費的。
參考鏈接
《Eureka at a glance》:https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance
《Consul Architecture》:https://www.consul.io/docs/architecture
《ZooKeeper for Microservice Registration and Discovery》:https://dzone.com/articles/zookeeper-for-microservice-registration-and-discov
《Eureka! Why You Shouldn’t Use ZooKeeper for Service Discovery》:https://medium.com/knerd/eureka-why-you-shouldnt-use-zookeeper-for-service-discovery-4932c5c7e764
《Why use virtual services?》:https://istio.io/latest/docs/concepts/traffic-management/#why-use-virtual-services
《Pattern: Server-side service discovery》:https://microservices.io/patterns/server-side-discovery.html
?
作者簡介
張羽辰(同昭)阿里云交付專家,有著近十年研發(fā)經(jīng)驗,是一名軟件工程師、架構(gòu)師、咨詢師,從 2016 年開始采用容器化、微服務(wù)、Serverless 等技術(shù)進行云時代的應(yīng)用開發(fā)。同時也關(guān)注在分布式應(yīng)用中的安全治理問題,整理《微服務(wù)安全手冊》,對數(shù)據(jù)、應(yīng)用、身份安全都有一定的研究。
K8S進階訓(xùn)練營,點擊下方圖片了解詳情

