1. 程式人生 > >一篇文章瞭解Consul服務發現實現原理

一篇文章瞭解Consul服務發現實現原理

從 2016 年起就開始接觸 Consul,使用的主要目的就是做服務發現,後來逐步應用於生產環境,並總結了少許使用經驗。

 

一篇文章瞭解Consul服務發現實現原理

 

 

最開始使用 Consul 的人不多,這兩年微服務越來越火,使用 Consul 的人也越來越多。

經常有人會問一些問題,比如:

  • 服務註冊到節點後,其他節點為什麼沒有同步?
  • Client 是幹什麼的?(Client 有什麼作用?)
  • 能不能直接註冊到 Server?(是否只有 Server 節點就夠了?)
  • 服務資訊是儲存在哪裡的?
  • 如果節點掛了,健康檢查能不能轉移到別的節點?

有些人可能對服務註冊和發現還沒有概念,有些人可能使用過其他服務發現的工具,比如 ZooKeeper,etcd,會有一些先入為主的經驗。

這篇文章將結合 Consul 的官方文件和自己的實際經驗,談一下 Consul 做服務發現的方式,文中儘量不依賴具體的框架和開發語言,從原理上進行說明,希望能夠講清楚上邊的幾個問題。

為什麼使用服務發現

防止硬編碼、容災、水平擴縮容、提高運維效率等等,只要你想使用服務發現總能找到合適的理由。

一般的說法是因為使用微服務架構。傳統的單體架構不夠靈活不能很好的適應變化,從而向微服務架構進行轉換。

而伴隨著大量服務的出現,管理運維十分不便,於是開始搞一些自動化的策略,服務發現應運而生。所以如果需要使用服務發現,你應該有一些對服務治理的痛點。

但是引入服務發現就可能引入一些技術棧,增加系統總體的複雜度,如果你只有很少的幾個服務,比如 10 個以下,並且業務不怎麼變化,吞吐量預計也很穩定,可能就沒有必要使用服務發現。

Consul 內部原理

下面這張圖來源於 Consul 官網,很好的解釋了 Consul 的工作原理,先大致看一下:

 

一篇文章瞭解Consul服務發現實現原理

 

 

首先 Consul 支援多資料中心,在上圖中有兩個 DataCenter,他們通過 Internet 互聯,同時請注意為了提高通訊效率,只有 Server 節點才加入跨資料中心的通訊。

在單個數據中心中,Consul 分為 Client 和 Server 兩種節點(所有的節點也被稱為 Agent),Server 節點儲存資料,Client 負責健康檢查及轉發資料請求到 Server。

Server 節點有一個 Leader 和多個 Follower,Leader 節點會將資料同步到 Follower,Server 的數量推薦是 3 個或者 5 個,在 Leader 掛掉的時候會啟動選舉機制產生一個新的 Leader。

叢集內的 Consul 節點通過 gossip 協議(流言協議)維護成員關係,也就是說某個節點了解叢集內現在還有哪些節點,這些節點是 Client 還是 Server。

單個數據中心的流言協議同時使用 TCP 和 UDP 通訊,並且都使用 8301 埠。跨資料中心的流言協議也同時使用 TCP 和 UDP 通訊,埠使用 8302。

叢集內資料的讀寫請求既可以直接發到 Server,也可以通過 Client 使用 RPC 轉發到 Server,請求最終會到達 Leader 節點。

在允許資料輕微陳舊的情況下,讀請求也可以在普通的 Server 節點完成,叢集內資料的讀寫和複製都是通過 TCP 的 8300 埠完成。

Consul 服務發現原理

下面這張圖是自己畫的,基本描述了服務發現的完整流程,先大致看一下:

 

一篇文章瞭解Consul服務發現實現原理

 

 

首先需要有一個正常的 Consul 叢集,有 Server,有 Leader。這裡在伺服器 Server1、Server2、Server3 上分別部署了 Consul Server。

假設他們選舉了 Server2 上的 Consul Server 節點為 Leader。這些伺服器上最好只部署 Consul 程式,以儘量維護 Consul Server 的穩定。

然後在伺服器 Server4 和 Server5 上通過 Consul Client 分別註冊 Service A、B、C,這裡每個 Service 分別部署在了兩個伺服器上,這樣可以避免 Service 的單點問題。

服務註冊到 Consul 可以通過 HTTP API(8500 埠)的方式,也可以通過 Consul 配置檔案的方式。

Consul Client 可以認為是無狀態的,它將註冊資訊通過 RPC 轉發到 Consul Server,服務資訊儲存在 Server 的各個節點中,並且通過 Raft 實現了強一致性。

最後在伺服器 Server6 中 Program D 需要訪問 Service B,這時候 Program D 首先訪問本機 Consul Client 提供的 HTTP API,本機 Client 會將請求轉發到 Consul Server。

Consul Server 查詢到 Service B 當前的資訊返回,最終 Program D 拿到了 Service B 的所有部署的 IP 和埠,然後就可以選擇 Service B 的其中一個部署並向其發起請求了。

如果服務發現採用的是 DNS 方式,則 Program D 中直接使用 Service B 的服務發現域名,域名解析請求首先到達本機 DNS 代理,然後轉發到本機 Consul Client,本機 Client 會將請求轉發到 Consul Server。

Consul Server 查詢到 Service B 當前的資訊返回,最終 Program D 拿到了 Service B 的某個部署的 IP 和埠。

圖中描述的部署架構筆者認為是最普適最簡單的方案,從某些預設配置或設計上看也是官方希望使用者採用的方案,比如 8500 埠預設監聽 127.0.0.1,當然有些同學不贊同,後邊會提到其他方案。

Consul 實際使用

為了更快的熟悉 Consul 的原理及其使用方式,最好還是自己實際測試下。

Consul 安裝十分簡單,但是在一臺機器上不方便搭建叢集進行測試,使用虛擬機器比較重,所以這裡選擇了 Docker。

這裡用了 Windows 10,需要是專業版,因為 Windows 上的 Docker 依賴 Hyper-V,而這個需要專業版才能支援。

這裡對於 Docker 的使用不會做過多的描述,如果遇到相關問題請搜尋一下。

安裝 Docker

通過這個地址下載安裝:

https://store.docker.com/editions/community/docker-ce-desktop-windows 

安裝完成後開啟 Windows PowerShell,輸入 docker –version,如果正常輸出 Docker 版本就可以了。

啟動 Consul 叢集

在 Windows PowerShell 中執行命令拉取最新版本的 Consul 映象:

docker pull consul 

然後就可以啟動叢集了,這裡啟動 4 個 Consul Agent,3 個 Server(會選舉出一個 Leader),1 個 Client。

#啟動第1個Server節點,叢集要求要有3個Server,將容器8500埠對映到主機8900埠,同時開啟管理介面 
docker run -d --name=consul1 -p 8900:8500 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --bootstrap-expect=3 --client=0.0.0.0 -ui

#啟動第2個Server節點,並加入叢集
docker run -d --name=consul2 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.2

#啟動第3個Server節點,並加入叢集
docker run -d --name=consul3 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.2

#啟動第4個Client節點,並加入叢集
docker run -d --name=consul4 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=false --client=0.0.0.0 --join 172.17.0.2

第 1 個啟動容器的 IP 一般是 172.17.0.2,後邊啟動的幾個容器 IP 會排著來:172.17.0.3、172.17.0.4、172.17.0.5。

這些 Consul 節點在 Docker 的容器內是互通的,他們通過橋接的模式通訊。但是如果主機要訪問容器內的網路,需要做埠對映。

在啟動第一個容器時,將 Consul 的 8500 埠對映到了主機的 8900 埠,這樣就可以方便的通過主機的瀏覽器檢視叢集資訊。

 

一篇文章瞭解Consul服務發現實現原理

 

 

進入容器 consul1:

docker exec -it consul1 /bin/sh 
#執行ls後可以看到consul就在根目錄
ls

輸入 exit 可以跳出容器。服務註冊自己寫一個 Web 服務,用最熟悉的開發語言就好了,不過需要在容器中能夠跑起來,可能需要安裝執行環境。

比如 Python、Java、.net core等的 sdk 及 Web 伺服器,需要注意的是 Consul 的 Docker 映象基於 alpine 系統,具體執行環境的安裝請搜尋一下。

這裡寫了一個 hello 服務,通過配置檔案的方式註冊到 Consul,服務的相關資訊如下:

  • name:hello,服務名稱,需要能夠區分不同的業務服務,可以部署多份並使用相同的 name 註冊。
  • id:hello1,服務 id,在每個節點上需要唯一,如果有重複會被覆蓋。
  • address:172.17.0.5,服務所在機器的地址。
  • port:5000,服務的埠。
  • 健康檢查地址:http://localhost:5000/,如果返回 HTTP 狀態碼為 200 就代表服務健康,每 10 秒 Consul 請求一次,請求超時時間為 1 秒。

請將下面的內容儲存成檔案 services.json,並上傳到容器的 /consul/config 目錄中:

{ 
"services": [
{
"id": "hello1",
"name": "hello",
"tags": [
"primary"
],
"address": "172.17.0.5",
"port": 5000,
"checks": [
{
"http": "http://localhost:5000/",
"tls_skip_verify": false,
"method": "Get",
"interval": "10s",
"timeout": "1s"
}
]
}
]
}

複製到 consul config 目錄:

docker cp {這裡請替換成services.json的本地路徑} consul4:/consul/config 

重新載入 consul 配置:

consul reload 

然後這個服務就註冊成功了。可以將這個服務部署到多個節點,比如部署到 consul1 和 consul4,並同時執行。

一篇文章瞭解Consul服務發現實現原理

 

 

服務發現

服務註冊成功以後,呼叫方獲取相應服務地址的過程就是服務發現。Consul 提供了多種方式。

HTTP API 方式

curl http://127.0.0.1:8500/v1/health/service/hello?passing=true 

返回的資訊包括註冊的 Consul 節點資訊、服務資訊及服務的健康檢查資訊。

這裡用了一個引數 passing=false,會自動過濾掉不健康的服務,包括本身不健康的服務和不健康的 Consul 節點上的服務,從這個設計上可以看出 Consul 將服務的狀態繫結到了節點的狀態。

如果服務有多個部署,會返回服務的多條資訊,呼叫方需要決定使用哪個部署,常見的可以隨機或者輪詢。

為了提高服務吞吐量,以及減輕 Consul 的壓力,還可以快取獲取到的服務節點資訊,不過要做好容錯的方案,因為快取服務部署可能會變得不可用。具體是否快取需要結合自己的訪問量及容錯規則來確定。

上邊的引數 passing 預設為 false,也就是說不健康的節點也會返回,結合獲取節點全部服務的方法,這裡可以做到獲取全部服務的實時健康狀態,並對不健康的服務進行報警處理。

DNS 方式

hello 服務的域名是:hello.service.dc1.consul,後邊的 service 代表服務,固定;dc1 是資料中心的名字,可以配置;最後的 consul 也可以配置。

官方在介紹 DNS 方式時經常使用 dig 命令進行測試,但是 alpine 系統中沒有 dig 命令,也沒有相關的包可以安裝,但是有人實現了,下載下來解壓到 bin 目錄就可以了。

curl -L https://github.com/sequenceiq/docker-alpine-dig/releases/download/v9.10.2/dig.tgz|tar -xzv -C /usr/local/bin 

然後執行 dig 命令:

dig @127.0.0.1 -p 8600 hello.service.dc1.consul. ANY 

如果報錯:parse of /etc/resolv.conf failed ,請將 resolv.conf 中的 search 那行刪掉。

正常的話可以看到返回了服務部署的 IP 資訊,如果有多個部署會看到多個,如果某個部署不健康了會自動剔除(包括部署所在節點不健康的情況)。需要注意這種方式不會返回服務的埠資訊。

使用 DNS 的方式可以在程式中整合一個 DNS 解析庫,也可以自定義本地的 DNS Server。

自定義本地 DNS Server 是指將 .consul 域的請求全部轉發到 Consul Agent,Windows 上有 DNS Agent,Linux 上有 Dnsmasq。

對於非 Consul 提供的服務則繼續請求原 DNS;使用 DNS Server 時 Consul 會隨機返回具體服務的多個部署中的一個,僅能提供簡單的負載均衡。

DNS 快取問題:DNS 快取一般存在於應用程式的網路庫、本地 DNS 客戶端或者代理,Consul Sever 本身可以認為是沒有快取的(為了提高叢集 DNS 吞吐量,可以設定使用普通 Server 上的陳舊資料,但影響一般不大)。

DNS 快取可以減輕 Consul Server 的訪問壓力,但是也會導致訪問到不可用的服務。使用時需要根據實際訪問量和容錯能力確定 DNS 快取方案。

Consul Template

Consul Template 是 Consul 官方提供的一個工具,嚴格的來說不是標準的服務發現方式。

這個工具會通過 Consul 監聽資料變化然後替換模板中使用的標籤,併發布替換後的檔案到指定的目錄。在 Nginx 等 Web 伺服器做反向代理和負載均衡時特別有用。

Consul 的 Docker 映象中沒有整合這個工具,需要自己安裝,比較簡單:

curl -L https://releases.hashicorp.com/consul-template/0.19.5/consul-template_0.19.5_linux_amd64.tgz|tar -xzv -C /usr/local/bin 

然後建立一個檔案 in.tpl,內容為:

{{ range service "hello" }} 
server {{ .Name }}{{ .Address }}:{{ .Port }}{{ end }}

這個標籤會遍歷 hello 服務的所有部署,並按照指定的格式輸出。在此檔案目錄下執行:

nohup consul-template -template "in.tpl:out.txt" & 

現在你可以 cat out.txt 檢視根據模板生產的內容,新增或者關閉服務,檔案內容會自動更新。

此工具我沒有用在生產環境,詳細使用請訪問:

https://github.com/hashicorp/consul-template 

節點和服務登出

節點和服務的登出可以使用 HTTP API:

登出任意節點和服務:/catalog/deregister

登出當前節點的服務:/agent/service/deregister/:service_id

注意:如果登出的服務還在執行,則會再次同步到 catalog 中,因此應該只在 Agent 不可用時才使用 catalog 的登出 API。

節點在宕機時狀態會變為 failed,預設情況下 72 小時後會被從叢集移除。

如果某個節點不繼續使用了,也可以在本機使用 consul leave 命令,或者在其他節點使用 consul force-leave 節點 id,則節點上的服務和健康檢查全部登出。

Consul 的健康檢查

Consul 做服務發現是專業的,健康檢查是其中一項必不可少的功能,其提供 Script/TCP/HTTP+Interval,以及 TTL 等多種方式。

服務的健康檢查由服務註冊到的 Agent 來處理,這個 Agent 既可以是 Client 也可以是 Server。

很多同學都使用 ZooKeeper 或者 etcd 做服務發現,使用 Consul 時發現節點掛掉後服務的狀態變為不可用了,所以有同學問服務為什麼不在各個節點之間同步?這個根本原因是服務發現的實現原理不同。

Consul 與 ZooKeeper、etcd 的區別

後邊這兩個工具是通過鍵值儲存來實現服務的註冊與發現:

ZooKeeper 利用臨時節點的機制,業務服務啟動時建立臨時節點,節點在服務就在,節點不存在服務就不存在。

etcd 利用 TTL 機制,業務服務啟動時建立鍵值對,定時更新 TTL,TTL 過期則服務不可用。

ZooKeeper 和 etcd 的鍵值儲存都是強一致性的,也就是說鍵值對會自動同步到多個節點,只要在某個節點上存在就可以認為對應的業務服務是可用的。

Consul 的資料同步也是強一致性的,服務的註冊資訊會在 Server 節點之間同步,相比 ZK、etcd,服務的資訊還是持久化儲存的,即使服務部署不可用了,仍舊可以查詢到這個服務部署。

但是業務服務的可用狀態是由註冊到的 Agent 來維護的,Agent 如果不能正常工作了,則無法確定服務的真實狀態。

並且 Consul 是相當穩定了,Agent 掛掉的情況下大概率伺服器的狀態也可能是不好的,此時遮蔽掉此節點上的服務是合理的。

Consul 也確實是這樣設計的,DNS 介面會自動遮蔽掛掉節點上的服務,HTTP API 也認為掛掉節點上的服務不是 passing 的。

鑑於 Consul 健康檢查的這種機制,同時避免單點故障,所有的業務服務應該部署多份,並註冊到不同的 Consul 節點。

部署多份可能會給你的設計帶來一些挑戰,因為呼叫方同時訪問多個服務例項可能會由於會話不共享導致狀態不一致,這個有許多成熟的解決方案,可以去查詢,這裡不做說明。

健康檢查能不能支援故障轉移?

上邊提到健康檢查是由服務註冊到的 Agent 來處理的,那麼如果這個 Agent 掛掉了,會不會有別的 Agent 來接管健康檢查呢?答案是否定的。

從問題產生的原因來看,在應用於生產環境之前,肯定需要對各種場景進行測試,沒有問題才會上線,所以顯而易見的問題可以遮蔽掉。

如果是新版本 Consul 的 Bug 導致的,此時需要降級;如果這個 Bug 是偶發的,那麼只需要將 Consul 重新拉起來就可以了,這樣比較簡單。

如果是硬體、網路或者作業系統故障,那麼節點上服務的可用性也很難保障,不需要別的 Agent 接管健康檢查。

從實現上看,選擇哪個節點是個問題,這需要實時或準實時同步各個節點的負載狀態。

而且由於業務服務執行狀態多變,即使當時選擇出了負載比較輕鬆的節點,無法保證某個時段任務又變得繁重,可能造成新的更大範圍的崩潰。

如果原來的節點還要啟動起來,那麼接管的健康檢查是否還要撤銷,如果要,需要記錄服務們最初註冊的節點,然後有一個監聽機制來觸發。

如果不要,通過服務發現就會獲取到很多冗餘的資訊,並且隨著時間推移,這種資料會越來越多,系統變的無序。

從實際應用看,節點上的服務可能既要被發現,又要發現別的服務,如果節點掛掉了,僅提供被發現的功能實際上服務還是不可用的。

當然發現別的服務也可以不使用本機節點,可以通過訪問一個 Nginx 實現的若干 Consul 節點的負載均衡來實現,這無疑又引入了新的技術棧。

如果不是上邊提到的問題,或者你可以通過一些方式解決這些問題,健康檢查接管的實現也必然是比較複雜的,因為分散式系統的狀態同步是比較複雜的。

同時不要忘了服務部署了多份,掛掉一個不應該影響系統的快速恢復,所以沒必要去做這個接管。

Consul 的其他部署架構

如果你實在不想在每個主機部署 Consul Client,還有一個多路註冊的方案可供選擇,這是交流群中獲得的思路。

如圖所示,在專門的伺服器上部署 Consul Client,然後每個服務都註冊到多個 Client。

這裡為了避免服務單點問題還是每個服務部署多份,需要服務發現時,程式向一個提供負載均衡的程式發起請求,該程式將請求轉發到某個 Consul Client。

這種方案需要注意將 Consul 的 8500 埠繫結到私網 IP 上,預設只有 127.0.0.1。

這個架構的優勢:

Consul 節點伺服器與應用伺服器隔離,互相干擾少。

不用每臺主機都部署 Consul,方便 Consul 的集中管理。

某個 Consul Client 掛掉的情況下,註冊到其上的服務仍有機會被訪問到。

但也需要注意其缺點:

引入更多技術棧:負載均衡的實現,不僅要考慮 Consul Client 的負載均衡,還要考慮負載均衡本身的單點問題。

Client 的節點數量:單個 Client 如果註冊的服務太多,負載較重,需要有個演算法(比如 hash 一致)合理分配每個 Client 上的服務數量,以及確定 Client 的總體數量。

服務發現要過濾掉重複的註冊:因為註冊到了多個節點會認為是多個部署(DNS 介面不會有這個問題)。

這個方案其實還可以優化,服務發現使用的負載均衡可以直接代理 Server 節點,因為相關請求還是會轉發到 Server 節點,不如直接就發到 Server。

是否可以只有 Server?

這個問題的答案還是有關服務數量的問題,首先 Server 的節點數量不是越多越好,3 個或者 5 個是推薦的數量,數量越多資料同步的處理越慢(強一致性)。

然後每個節點可以註冊的服務數量是有上限的,這個受限於軟硬體的處理能力。

所以如果你的服務只有 10 個左右,只有 Server 問題是不大的,但是這時候有沒有必要使用 Consul 呢?

因此正常使用 Consul 的時候還是要有 Client 才好,這也符合 Consul 的反熵設計。

大家可以將這個部署架構與前文提到的普世架構對比下,看看哪個更適合自己,或者你有更好的方案歡迎分享出來。