服務註冊發現與調度

原文地址:http://www.javashuo.com/article/p-dqtmqrjs-bv.htmljava

遠程服務依賴

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

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

docker以後,依賴問題就只剩遠程服務依賴的問題。這個問題就是服務註冊發現與調度須要解決的問題。從軟件工程的角度來講,全部的解耦問題均可以經過抽取lib的方式解決。lib也能夠實現獨立的發佈週期,良好定義的IDL接口。因此若是非必要,請不要把lib依賴升級成網絡服務依賴的角度。除非是從非功能性需求的角度,好比獨立的擴縮容,支持scale out這些。不少時候微服務是由於基於lib的工具鏈支持不全,使得你們義無反顧地走上了拆分網絡服務的不歸路。nginx

名字服務

服務名又稱之爲Service Qualifier,是一我的類可理解的英文標識。所謂的服務註冊和發現就是在一個Service Qualifier下注冊一堆Endpoint。一個Endpoint就是一個ip+端口的網絡服務。就是一個很是相似DNS的名字服務,其實DNS自己就能夠作服務的註冊和發現,用SRV類型記錄。git

名字服務的存在乎義是簡化服務的使用方,也就是主調方。過去在使用方的代碼裏須要填入一堆ip加端口的配置,如今有了名字服務就能夠只填一個服務名,實際在運行時用服務名找到那一堆endpoint。github

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

lib提供的接口相似web

$endpoints = listServiceEnpoints('redis');
echo($endpoints[0]['ip]);

甚至能夠直接提供拼接url的接口redis

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

比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 工具棧

相關文章
相關標籤/搜索