服务发现技术选型那点事儿
作者 | 張羽辰(同昭)
引子——什么是服務發現
近日來,和很多來自傳統行業、國企、政府的客戶在溝通技術細節時,發現云原生所代表的技術已經逐漸成為大家的共識,從一個虛無縹緲的概念漸漸變成這些客戶的下一個技術戰略。自然,應用架構就會提到微服務,以及其中最重要的分布式協作的模式——服務發現。模式(pattern)是指在特定上下文中的解決方案,很適合描述服務發現這個過程。不過相對于 2016 年,現在我們最少有十多種的方式能實現服務發現,這的確是個好時機來進行回顧和展望,最終幫助我們進行技術選型與確定演進方向。
微服務脫胎于 SOA 理論,核心是分布式,但單體應用中,模塊之間的調用(比如讓消息服務給客戶發送一條數據)是通過方法,而所發出的消息是在同一塊內存之中,我們知道這樣的代價是非常小的,方法調用的成本可能是納秒級別,我們從未想過這樣會有什么問題。但是在微服務的世界中,模塊與模塊分別部署在不同的地方,它們之間的約束或者協議由方法簽名轉變為更高級的協議,比如 RESTful 、PRC,在這種情況下,調用一個模塊就需要通過網絡,我們必須要知道目標端的網絡地址與端口,還需要知道所暴露的協議,然后才能夠編寫代碼比如使用 HttpClient 去進行調用,這個“知道”的過程,往往被稱為服務發現。
分布式的架構帶來了解耦的效果,使得不同模塊可以分別變化,不同的模塊可以根據自身特點選擇編程語言、技術棧與數據庫,可以根據負載選擇彈性與運行環境,使得系統從傳統的三層架構變成了一個個獨立的、自治的服務,往往這些服務與業務領域非常契合,比如訂單服務并不會關心如何發送郵件給客戶,司機管理服務并不需要關注乘客的狀態,這些服務應該是網狀的,是通過組合來完成業務。解耦帶來了響應變化的能力,可以讓我們大膽試錯,我們希望啟動一個服務的成本和編寫一個模塊的成本類似,同時編寫服務、進行重構的成本也需要降低至于代碼修改一般。在這種需求下,我們也希望服務之間的調用能夠簡單,最好能像方法調用一樣簡單。
但是 Armon(HashiCorp 的創始人)在他的技術分享中提到,實現分布式是沒有免費午餐的,一旦你通過網絡進行遠程調用,那網絡是否可達、延遲與帶寬、消息的封裝以及額外的客戶端代碼都是代價,在此基礎上,有時候我們還會有負載均衡、斷路器、健康檢查、授權驗證、鏈路監控等需求,這些問題是之前不需要考慮的。所以,我們需要有“產品”來幫助我們解決這類問題,我們可以先從 Eureka 開始回顧、整理。
一個單體應用部署在多臺服務器中,模塊間通過方法直接調用。
分布式的情況下,模塊之間的調用通過網絡,也許使用 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 加入,也能讓代碼修改的成本變低。
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 在不改變代碼的情況下注入某些功能,比如服務注冊發現、比如日志記錄、比如服務之間的授信。
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,如果是不同的編程語言或框架,我們就必須要編寫自己的實現。
像蜘蛛網一樣的互相調用過程,并且每個服務都必須有 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 來判斷哪個節點是否健康。
邏輯上還是 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 上的應用進行遷移、重構還是可以負擔的起的,但我非常擔心幾年后,改造的成本就會變的非常高,最終導向重寫的境地。
我們將調用方式分為“同步”與“異步”兩種情況,在異步調用時,使用 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 等技術領域、聚焦云原生流行技術趨勢、云原生大規模的落地實踐,做最懂云原生開發者的公眾號。”
總結
以上是生活随笔為你收集整理的服务发现技术选型那点事儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何无缝迁移 SpringCloud/D
- 下一篇: Dubbo 3.0 前瞻之:常用协议对比