標籤:

服務註冊發現與調度

遠程服務依賴

依賴分為兩種,本地的lib依賴,遠程的服務依賴。

本地的依賴其實是很複雜的問題。從操作系統的apt-get,到各種語言的pip, npm。包管理是無窮無盡的問題。但是所有的本地依賴已經被docker終結了。無論是依賴了什麼,全部給你打包起來,從操作系統開始。除了你依賴的cpu指令集沒法給你打包成鏡像了,其他都給打包了。

docker之後,依賴問題就只剩遠程服務依賴的問題。這個問題就是服務註冊發現與調度需要解決的問題。從軟體工程的角度來說,所有的解耦問題都可以通過抽取lib的方式解決。lib也可以實現獨立的發布周期,良好定義的IDL介面。所以如果非必要,請不要把lib依賴升級成網路服務依賴的角度。除非是從非功能性需求的角度,比如獨立的擴縮容,支持scale out這些。很多時候微服務是因為基於lib的工具鏈支持不全,使得大家義無反顧地走上了拆分網路服務的不歸路。

名字服務

服務名又稱之為Service Qualifier,是一個人類可理解的英文標識。所謂的服務註冊和發現就是在一個Service Qualifier下註冊一堆Endpoint。一個Endpoint就是一個ip+埠的網路服務。就是一個非常類似DNS的名字服務,其實DNS本身就可以做服務的註冊和發現,用SRV類型記錄。

名字服務的存在意義是簡化服務的使用方,也就是主調方。過去在使用方的代碼里需要填入一堆ip加埠的配置,現在有了名字服務就可以只填一個服務名,實際在運行時用服務名找到那一堆endpoint。

從名字服務的角度來講並不比DNS要強多少。可能也就是通過「服務發現的lib」幫你把ip和埠都獲得了。而DNS默認lib(也就是libc的getHostByName)只支持host獲取,並不能獲得port。當然既然你都外掛了一個服務發現的lib了,和libc做對比也就優勢公平了。

lib提供的介面類似

$endpoints = listServiceEnpoints(redis);necho($endpoints[0][ip]);n

甚至可以直接提供拼接url的介面

$url = getServiceUrl(order, /newOrder); # http://xxx:yyy/newOrdern

比DNS更快的廣播速度

傳統DNS的服務發現機制是緩存加上TTL過期時間,新的endpoint要傳播到使用方需要各級緩存的刷新。而且即便endpoint沒有更新,因為TTL到期了也要去上游刷新。為了減少網路間定時刷新endpoint的流量,一般TTL都設得比較長。

而另外一個極端是gossip協議。所有人連接到所有人。一個服務的endpoint註冊了,可以通過gossip協議很快廣播到全部的節點上去。但是gossip的缺點是不基於訂閱的。無論我是不是使用這個服務,我都會被動地被gossip這個服務的endpoint。這樣就造成了無謂的網路間帶寬的開銷。

比較理想的更新方式是基於訂閱的。如果業務對某個服務進行了發現,那麼緩存伺服器就保持一個訂閱關係獲得最新的endpoint。這樣可以比定時刷新更及時,也消耗更小。這個方面要黑一下etcd 2.0,它的基於http連接的watch方案要求每個watch獨佔一個tcp連接,嚴重限制了watch的數量。而etcd 3.0基於gRPC的實現就修復了這個問題。而consul的msgpack rpc從一開始就是復用tcp連接的。

圖中的observer是類似的zookeeper的observer角色,是為了幫權威伺服器分擔watch壓力的存在。也就是說服務發現的核心其實是一個基於訂閱的層級消息網路。服務註冊和發現並不承諾任何的一致性,它只是儘力地進行分發,並不保證所有的節點對一個服務的endpoint是哪些有一致的view,因為這並沒有價值。因為一個qualifier下的多個endpoint by design 就是等價的,只要有足夠的endpint能夠承擔負載,對於abc三個endpoint具體是讓ab可見,還是bc可見,並無任何影響。

服務發現agent的高可用

DNS的方案是在每台機器上裝一個dnsmasq做為緩存伺服器。服務發現也是類似的,在每台機器上有一個agent進程。如果dnsmasq掛了,dns域名就會解析失敗,這樣的可用性是不夠的。服務發現的agent會把服務的配置和endpoint dump一份成本機的文件,服務發現的lib在無法訪問agent的時候會降級去讀取本機的文件,從而保證足夠的可用性。當然你要願意搞什麼共享內存,也沒人阻攔。

無法實現對dns伺服器的降級。因為哪怕是降級到 /etc/hosts 的實現,其一個巨大的缺陷是 /etc/hosts 對於一個域名只能填一個ip,無法滿足擴展性。而如果這一個ip填的是代理伺服器的話,則失去了做服務發現的意義,都有代理了那就讓代理去發現服務好了。

更進一步,很多基於zk的方案是把服務發現的agent和業務進程做到一個進程里去了。所以就不需要擔心外掛的進程是否還存活的問題了。

軟負載均衡

這點上和DNS是類似的。理論來說ttl設置為0的DNS伺服器也可以起到負載均衡的作用。通過把權重分發到服務發現的agent上,可以讓業務「每次發現」的endpoint都不一樣,從而達到均衡負載的作用。權重的實現通過簡單的隨機演算法就可以實現。

通過軟負載均衡理論上可以實現小流量,灰度地讓一個新的endpoint加入集群。也可以實現某一些endpoint承擔更大的調用量,以達到在線壓測的目的。

不要小瞧了這麼一點調權的功能。能夠中央調度,智能調度流量,是非常有用的。

故障檢測(減endpoint)

故障檢測其實是好做的。無非就是一個qualifier下掛了很多個endpoint,根據某種探活機制摘掉其中已經無法提供正常服務的endpoint。摘除最好是軟摘除,這樣不會出現一個閃失把所有endpoint全摘掉的問題。比如zookeeper的臨時節點就是硬摘除,不可取。

本地探活

在業務拿到endpoint之後,做完了rpc可以知道這個endpoint是否可用。這個時候對endpoint的健康狀態本地做一個投票累積。如果endpoint連續不可用則標記為故障,被臨時摘除。過一段時間之後再重新放出小黑屋,進行探活。這個過程和nginx對upstream的被動探活是非常類似的。

被動探活的好處是非常敏感而且真實可信(不可用就是我不能調你,就是不可用),本地投票完了立即就可以判定故障。缺陷是每個主調方都需要獨立去進行重複的判定。對於故障的endpoint,為了探活其是否存活需要以latency做為代價。

被動探活不會和具體的rpc機制綁定。無論是http還是thrift,無論是redis還是mysql,只要是網路調用都可以通過rpc後投票的方式實現被動探活。

主動探活

主動探活比較難做,而且效果也未必好:

  • 所有的主動探活的問題都在於需要指定如何去探測。不是tcp連接得上就算是能提供服務的。

  • 主動探活受到網路路由的影響,a可以訪問b,並不帶表c也可以訪問b

  • 主動探測帶來額外的網路開銷,探測不能過於頻繁

  • 主動探測的發起者過少則容易對發起者產生很大的探活壓力,需要很高的性能

本地主動探活

consul 的本機主動探活是一個很有意思的組合。避免了主動探活的一些缺點,可以是被動探活的一些補充。

心跳探活

無論是zookeeper那樣一來tcp連接的心跳(tcp連接的保持其實也是定時ttl發ip包保持的)。還是etcd,consul支持的基於ttl的心跳。都是類似的。

gossip探活

改進版本的心跳。減少整體的網路間通信量。

服務註冊(加endpoint)

服務endpoint註冊比endpoint摘除要難得多。

無狀態服務註冊

無狀態服務的註冊沒有任何約束。不管是中央管理服務註冊表,用web界面註冊。還是和部署系統聯動,在進程啟動時自動註冊都可以做。

有狀態服務的註冊

有狀態服務,比如redis的某個分片的master。其有兩個約束:

  • 一致性:同一個分片不能有兩個master

  • 可用性:分片不能沒有master,當master掛了,要自發選舉出新的master

除非是在數據層協議上做ack(paxos,raft)或者協議本身支持衝突解決(crdt),否則基於服務註冊來實現的分散式要麼犧牲一致性,要麼犧牲可用性。

有狀態服務的註冊需求,和普通的註冊發現需求是本質不同的。有狀態服務需要的是一個一致性決策機制,在consistency和availability之間取平衡。這個機制可以是外掛一個zookeeper,也可以是集群的數據節點自身做一個gossip的投票機制。

而普通的註冊和發現就是要給廣播渠道,提供visibility。儘可能地讓endpoint曝光到其使用方那。不同的問題需要的解決方案是不同的。對於有狀態服務的註冊表需要非常可靠的故障檢測機制,不能隨意摘除master。而用於廣播的服務註冊表則很隨意,故障檢測機制也可以做到儘可能錯殺三千不放過一個。廣播的機制需要解決的問題是大集群,怎麼讓服務可見。而數據節點的選主要解決的是相對小的集群,怎麼保持一致地情況下盡量可用。拿zookeeper的臨時節點這樣的機制放在大集群背景下,去做無狀態節點探活就是技術用錯了地方。

比如kafka,其有狀態服務部分的註冊和發現是用zookeeper實現的。而無狀態服務的註冊與發現是用data node自身提供集群的metadata來實現的。也就是消費者和生產者是不需要從zookeeper里去集群分片信息的(也就是服務註冊表),而是從data node拿。這個時候data node其是充當了一個服務發現的agent的作用。如果不用data node干這個活,我們把data node的內容放到DNS里去,其實也是可以work的。只是這些存儲的給業務使用的客戶端lib已經把這些邏輯寫好了,沒有人會去修改這個默認行為了。

但是廣播用途的服務註冊和發現,比如DNS不是只提供visibility而不能保證任何consistency嗎?那我讀到分片信息是舊的,把slave當master用了怎麼辦呢?所有做得好的存儲分片選主方案,在data node上自己是知道自己的角色的。如果你使用錯了,像redis cluster會回一個move指令,相當於http 302讓你去別的地方做這個操作。kafka也是類似的。

接入方式

libc只支持getHostByName,任何更高級的服務發現都需要挖空心思想怎麼簡化接入。反正操作系統和語言自身的工具鏈上是沒有標準的支持的。每個公司都有一套自己的玩法。行業嚴重缺乏標準。

無論哪種方式都是要修改業務代碼的。即便是用proxy方式接入,業務代碼里也得寫死固定的proxy ip才行。從可讀性的角度來說,固定proxy ip的可讀性是最差的,而用服務名或者域名是可讀性最好的。

給每種語言寫lib

最笨拙的方法,也是最保險的。業務代碼直接寫服務名,獲得endpoint。

探活也就是硬改各種rpc的lib,在調用後面加上投票的代碼。

配置文件替換

外掛式的服務發現。在配置文件中寫變數引用服務,運行時把endpoint取出來生成並替換。大部分通用工具都是這麼實現,比如consul-template。但是維護模板配置文件是很大的一個負擔。

復用libc的getHostByName

因為所有的語言基本上都支持DNS域名解析。利用這一層的介面,用鉤子換掉lib的實際實現。業務代碼里寫域名,埠固定。

socket的鉤子要難做得多,而且僅僅tcp4層探活也是不夠的(http 500了往往也要認為對方是掛了的)。

實際上考慮golang這種沒有libc的,java這種自己緩存域名結果的,鉤子的方案其實沒有想得那麼美好。

本地 proxy

proxy其實是一種簡化服務發現接入方式的手段。業務可以不用知道服務名,而是使用固定的ip和埠訪問。由proxy去做服務發現,把請求轉給對方。

http的proxy也很成熟,在proxy里對rpc結果進行跳票也有現成的工具(比如nginx)。很多公司都是這種本地proxy的架構,比如airbnb,yelp,eleme,uber。當用lib方式接業務接不動的時候,大家都會往這條路上轉的。

遠程 proxy

遠程proxy的缺陷是固定ip導致了路由是固定的。這條路由上的所有路由器和交換機都是故障點。無法做到多條網路路由冗餘容錯。而且需要用lvs做虛ip,也引入了運維成本。

而且遠程proxy無法支持分區部署多套環境。除非引入bgp anycast這樣妖孽的實現。讓同一個ip在不同的idc里路由到不同的伺服器。

分區部署

國內大部分的網遊都是分區分服的。這種架構就是一種簡化的存儲層數據分片。存儲層的數據分片一般都做得非常完善,可以做到key級別的搬遷(當你訪問key的時候告訴你我可以響應,還是告訴你搬遷到哪裡去了),可以做到訪問錯了shard告訴你正確的shard在哪裡。而分區部署往往是沒有這麼完善的。

所以為了支持分區部署。往往是給不同分區的服務區不同的服務名。比如模塊叫 chat,那麼給hb_set(華北大區)的chat模塊就命名為hb_set.chat,給hn_set(華南大區)的chat模塊就命名為hn_set.chat。當時如果我們是gamesvr模塊,需要訪問chat模塊,代碼都是同一份,我怎麼知道應該訪問hn_set.chat還是hb_set.chat呢?這個就需要讓gamesvr先知道自己所在的set,然後去訪問同set下的其他模塊。

again,這種分法也就是因為分區部署做為一個大的組合系統沒法像一個孤立地存儲做得那麼好。像kafka的broker,哪怕你訪問的不是它的本地分片,它可以幫你去做proxy連接到正確的分片上。而我們沒法要求一個組合出來的業務系統也做到這麼完備地程度。所以湊合著用吧。

但是這種分法也有問題。有一些模塊如果不是分區的,是全局的怎麼辦?這個時候服務發現就得起一個路由表的作用,把不同分區的服務通過路由串起來。

Netflix 工具棧

  • 服務發現和註冊:github.com/Netflix/eure

  • 服務調度:github.com/Netflix/ribb

  • 監控和降級:github.com/Netflix/hyst

  • Sidecar: github.com/Netflix/Pran

推薦閱讀:

沃爾瑪的數字化平台分析
這一篇文章帶你感受微服務的生和死,Spring Boot是生和死的主旋律。
微服務和容器:需要去防範的 5 個「坑」
微服務的模式語言
阿里Dubbo瘋狂更新,關Spring Cloud什麼事?

TAG:微服务架构 |