1. 程式人生 > 實用技巧 >微服務架構之服務發現:選型與思考

微服務架構之服務發現:選型與思考

導語:本文圍繞服服務呼叫模式、一致性取捨、服務提供者的健康檢查模式等方面,討論了服務發現的技術選型和設計的各種優缺點,希望能夠幫助大家在選擇或者使用服務發現系統的時候更加順暢。

01

服務發現系統的背景

不知道大家剛接觸微服務治理的時候是否有這樣的疑惑:為什麼一定需要一個服務發現系統呢?服務啟動的時候直接讀取一個本地配置,然後通過遠端配置系統,動態推送下來不行嗎?實際上,當服務節點規模較小時,該方案也行得通,但如果遇到以下的場景呢?

1. 在微服務的世界中,服務節點的擴縮容、服務版本的迭代是常態,服務消費端需要能夠快速及時的感知到節點資訊的變更(網路地址、節點數量)。

2. 當服務節點規模巨大時,節點的不可用也會變成常態,服務提供者要能夠及時上報自己的健康狀態,從而做到及時剔除不健康節點(或降低權重)。

3. 當服務部署在多個可用區時,需要將多個可用區的服務節點資訊互相同步,當某個可用區的服務不可用時,服務消費者能夠及時切換到其他可用區(通過負載均衡演算法自動切換或手動緊急切換),從而做到多活和高可用。

4. 服務發現背後的儲存應該是分散式的,這樣當部分服務發現節點不可用的時候,也能提供基本的服務發現功能

5. 除了ip、port我們需要更多的資訊,比如節點權重、路由標籤資訊等等。

使用檔案配置或DNS等傳統方式無法同時滿足上述幾點要求,因此我們需要重新設計一個能夠匹配上微服務架構的服務發現系統。

02

服務間呼叫模式

客戶端發現模式

由客戶端負責向服務發現系統(可以認為是一個數據庫,儲存了所有服務提供者的所有節點位置資訊)詢問某個服務提供者的所有例項的ip、port資訊,並採用某種負載均衡策略,直接發起對服務例項的訪問。

其中一個經典代表就是Netflix提供的解決方案:Netflix Eureka 提供服務發現功能, Netflix Ribbon 作為一個通訊SDK庫與客戶端整合在一起提供負載均衡與故障轉移。

這種模式去除了對中心化單點(API Gateway or Load Balancer)的依賴,可以避開單點造成的效能瓶頸與故障問題,同時由於負載均衡的邏輯在客戶端,它可以根據自身的配置選擇負載均衡演算法,比如一致性Hash演算法。不過這種模式也存在缺陷,由於客戶端的負載均衡邏輯是分散式的,各自為政,沒有全域性統一視角,在某些情景下會因為客戶端的高度競爭而導致後端服務提供者節點的負載不均衡。同時客戶端的業務邏輯和服務發現的邏輯耦合在一起,不同的服務使用了不同的程式語言,那麼就需要有不同語言的SDK,如果未來某天服務發現的邏輯變更了,也需要重新發布所有的客戶端節點。

服務端發現模式

把原本客戶端執行的服務列表拉取&負載均衡&熔斷&故障轉移這部分邏輯抽象變成一個專屬的服務。不過跟傳統的 load balancer 不大一樣的地方是: 這個的 load balancer會跟服務發現系統密切的配合,實時訂閱服務發現系統中服務提供者節點列表資訊,扮演反向代理的角色,將請求分發到合適的 Endpoint。

這塊的一個代表是kubernetes的服務發現解決方案:執行在每個Node節點的kube-proxy會實時的watch Services和 Endpoints物件。每個執行在Node節點的kube-proxy感知到Services和Endpoints的變化後,會在各自的Node節點設定相關的iptables或IPVS規則,方便後面使用者通過Service的ClusterIP去訪問該Service下的服務。當kube-proxy把需要的規則設定完成之後,使用者便可以在叢集內的Node或客戶端Pod上通過ClusterIP經過iptables或IPVS設定的規則進行路由和轉發,最終將客戶端請求傳送到真實的後端Pod。

這種模式對於客戶端來說是透明的,所有細節都被隔離在 load balancer 跟服務發現系統之間, 因此也沒有前面跨語言等相關問題,更新相關邏輯也只要統一部署 load balancer & service registry 就足夠了。很明顯,這種模式下服務的架構等於多了一層轉發,延遲事件會增加;整個系統也多了一個故障點,整體系統的運維難度會提高;另外這個load balancer 也可能會成為效能瓶頸。

基本上服務端發現模式我們平常接觸到的機會比較少,但是由於是無任何入侵的,比較適合舊系統上微服務架構的一個過渡方案。

03

服務發現的一致性取捨

我們先回顧一下CAP定律:在一個分散式系統中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分割槽容錯性),不能同時成立。

  • 一致性:它要求在同一時刻點,分散式系統中的所有資料備份都處於同一狀態。

  • 可用性:在系統叢集的一部分節點宕機後,系統依然能夠響應使用者的請求。

  • 分割槽容錯性:在網路區間通訊出現失敗,系統能夠容忍。

對基於raft、paxos演算法的CP服務發現系統比如Consul、Zookeeper、Etcd等,為了保證資料的線性強一致性(Linearizable),必然會犧牲掉高可用性,比如在網路分割槽的情況下心跳、註冊、反註冊這些操作都會超時並失敗。同時由於一致性演算法的要求,所有的寫請求都會重定向至leader節點,那麼這樣無法做到寫的水平擴充套件。而AP服務發現比如Eureka則強調最終一致性(在有限的時間內(例如3s內)將資料收斂到一致狀態),在犧牲資料一致性的情況下最大程度保障服務的可用性。

zookeeper服務發現

可以考慮上圖的跨機房容災的情景,此時滿足強一致要求的Zookeeper作為服務發現。如果機房 1 和機房 2 由於某些不穩定的原因發生網路斷開,provider B 去往 Zookeeper Follower 的註冊是無法實現的。因為 Zookeeper Follower 所有的請求是強一致,都有同步到 ZK Leader,這時機房 2 就無法註冊了,但此時其實 Consumer B 和 Provider B 之間的網路是正常的,互相呼叫沒有問題,可Provider B不能註冊導致Consumer B無法訪問Provider B。所以我們可以發現,服務發現系統首先應當保證的服務可用性,為了保證資料一致性卻不能提供註冊功能,在生產實踐中是不能接受的。 當然我們也可以在兩個機房獨立的部署兩套Zookeeper,然後再寫一個工具互相同步資料,使得兩個機房的Zookeeper互為Master Slave,但這樣不僅引入了新的複雜度,同時還得花大力氣保證資料同步的一致性。

那麼引入一個最終一致的Netflix Eruka的最終一致性設計是否就滿足所有的場景萬事大吉了呢?讓我們設想這種情景:

Eruka serverA和Eruka serverB之前互相同步資料,但此時Eruka serverC和Eruka serverB、Eruka serverA之間的網路發生了故障,無法順利同步資訊。

ProviderB向Eruka serverA註冊了服務資訊,並維持上報心跳,這樣服務節點ProvderB的資訊Eruka serverA和Eruka serverB中都是存在的,但是由於資訊複製的問題,沒辦法同步到Eruka serverC中。這樣當ConsumerA先向eruka serverA發起請求的時候,會得到一個正確的節點資訊,但是當下次訪問到Eruka serverC的時候又會得到一個錯誤的節點資訊,這樣之前正確的資訊就被覆蓋了。

那麼為了避免上述的情況,我們需要改造上面的邏輯,Client SDK需要同時去訪問三個eruka server節點,再拿到三個節點返回providerB的節點資訊中的的dirty time(dirty time由ProviderB維護,心跳上報的時候夾帶,這樣可以保證單調自增)後,通過比較選取dirty time最新的那個資訊,這樣就可以保證訪問到正確的資訊。當然上述情景是在生成環境中很難遇到,因為大多數情況下eruka server和Provider、Consumer都部署在同一個機房,如果eruka serverC和其他eruka server節點網路通訊有問題的話,ConsumerA大概率也是訪問不到eruka serverC的;又如果eruka serverC是跨機房部署的,那麼正常情況下ConsumerA也是不會主動跨機房訪問eruka serverC的。

04

服務提供者的健康檢查模式

客戶端心跳

  • 客戶端每隔一定時間主動傳送“心跳”的方式來向服務端表明自己的服務狀態正常,心跳可以是 TCP 的形式,也可以是 HTTP 的形式。

  • 也可以通過維持客戶端和服務端的一個 socket 長連線自己實現一個客戶端心跳的方式。

但是客戶端心跳中,長連線的維持和客戶端的主動心跳都只是表明鏈路上的正常,不一定是服務狀態正常。

服務端主動探測

  • 服務端呼叫服務釋出者某個 HTTP 介面來完成健康檢查。

  • 對於沒有提供 HTTP 服務的 RPC 應用,服務端呼叫服務釋出者的介面來完成健康檢查。

  • 可以通過執行某個指令碼的形式來進行綜合檢查。

服務端主動呼叫服務進行健康檢查是一個較為準確的方式,返回結果成功表明服務狀態確實正常。但是服務端主動探測也存在問題。服務註冊中心主動呼叫 RPC 服務的某個介面無法做到通用性;在很多場景下服務註冊中心到服務釋出者的網路是不通的,服務端無法主動發起健康檢查,那麼往往需要在宿主機器上部署一個agent來代替服務端的介面探測,比如Consul的健康檢查機制就是這麼實現的。

05

消費端的訂閱機制

  • Push推送:Push 的經典實現有兩種,基於socket長連線的推送,典型的實現如 zookeeper;另一種為HTTP連線所使用的 Long Polling,這兩種形式都保證了訊息變更能夠第一時間送達。但是基於 socket 長連線的推送和基於 HTTP 協議的 Long Polling 都會存在notify訊息丟失的問題和程式碼實現複雜度過高的問題。

  • 定時輪詢:比如eruka,客戶端每隔一段時間(預設30秒)會去服務端拉取登錄檔資訊,保證登錄檔是最新的,這樣的基於http短連結的訂閱模式實現起來是最簡單、最通用的。但也很容易導致一個問題,就是服務節點資訊會有30s的延遲,在這30s內有可能會有請求打到已下線的節點上去。

  • 推拉結合的方式:比如Consul,客戶端和consul server之間會建立起一個最長30s的http長連結,如果期間有任何變更,則會立即推送,如果沒有變更等到30s過後,客戶端又會立即建立起新的連線,繼續開始新的一輪訂閱。這種模式的既吸收了http短連結方便通用的好處,又享受到訊息即時推送的優勢。

06

服務的上線與下線

優雅上線

1. 服務提供一個通用的Health check介面(比如spring boot actuator模組自帶/actuator/health 介面,grpc也提供了health checking的標準模型),服務發現的sdk通過檢查該介面來確定服務是否準備好接流,只有準備好節流才可將該節點註冊上去。

2. SDK也可以提供一個回撥介面,服務一切都準備就緒後再呼叫這個介面通知sdk去註冊。

優雅下線

服務發現SDK接收到系統發出的SigTerm或者SigInt訊號後,需要先主動反註冊本身的例項,此時如果服務框架提供了graceful shutdown能力,就可以直接呼叫該方法,此時會阻塞住直到當前的所有inflight請求都處理完成或者超時才真正退出(不通)(grpc server提供了直接graceful shutdown方法,spring web應用則可以通過java提供的ThreadPoolExecutor.awitTermination來實現此能力)。如果沒有graceful shutdown的能力,則需要主動sleep一定時間以確保所有http、rpc請求都處理完成後再退出。

07

服務發現的容災與高可用

服務端

  • 服務節點資訊原本是分散式儲存的,少數節點掛了,不會影響整體可用性。

  • 當大多數節點掛了的時候,如果是強一致的系統此時會進入只讀不可寫的模式(比如Zookeeper和開啟了stale read的consul。如果是最終一致的系統,此時客戶端 sdk會自動重試並切換到正常節點上去,讀和寫都不受影響。(缺少後括號,但不知道在哪加)。

  • 當服務端所有的節點都掛了時候,此時需要服務端能夠持久化儲存之前註冊的Provider節點資訊,並在重啟之後進入保護模式一段時間,在此期間先不剔除不健康的Provider節點(因為宕機過程中心跳沒辦法成功上報),否則可能會導致在一個ttl內大量Provider節點失效。

  • 網路閃斷保護,監測到大面積出現服務提供者節點心跳沒有上報,則自動進入保護模式,該模式下不會剔除因為心跳上報失敗的服務提供者節點

客戶端

  • 客戶端SDK需要有不可用節點剔除能力,當服務端某個節點不可用的時候,能夠立即切換到下一個節點嘗試(切換的時候隨機sleep 0-3s防止重試風暴打垮某個節點)。這裡要注意客戶端SDK每次請求的超時時間是否設定正確,我們發現部分服務發現官方SDK的預設超時時間過長,比如java的consul sdk中預設超時是10分鐘,在生產實踐中如果發生了網路閃斷導致response包回不來就會導致sdk的心跳請求一致阻塞住,沒辦法進行下次的心跳上報,從而導致節點從註冊中心中異常下線。

  • 當所有的服務端節點都不可用的時候,SDK能夠使用記憶體中的快取繼續提供服務

  • 如果客戶端重啟了,記憶體中的資料不存在了,則走本地配置降級。

服務註冊的Metadata

服務註冊的時候除了攜帶serviceName、ip、port這些資訊就足夠了呢?在一個大型為微服務系統中,服務支援的協議、服務的標籤(比如Abtest、藍綠髮布的時候需要篩選這些tag作為服務路由資訊)、服務的健康狀態、服務的排程權重等資訊可能都需要傳遞給消費者感知到。不過在生產實踐中,一般不推薦將過多的資訊放入註冊中心,以免導致效能下降,比如swagger生成的api資訊最好單獨儲存。

08

總結

以上一些淺見便是我們團隊在騰訊雲微服務框架TSF中的服務發現系統開發和維護時所踩過的坑以及留下的經驗和總結,如果大家不想再淌這些坑,可以直接使用騰訊雲微服務框架TSF,其中提供了服務發現等微服務治理功能。

參考閱讀:

本文授權轉載自騰訊雲中間件,技術原創及架構實踐文章,歡迎通過公眾號選單「聯絡我們」進行投稿。

高可用架構

改變網際網路的構建方式


長按二維碼 關注「高可用架構」公眾號