[ 翻譯 ] Docker結合Consul實現的服務發現(一)
在過去的一年里,我開始變得熱衷于使用Consul來實現一切和服務發現相關的東西。如果你正在做微服務的話,你可能會碰到一個問題,那就是當你創建的服務數量越多時,那便越難管理這些服務之間的通信。針對這個問題,Consul給出了一份完美的答卷。它提供了一個易于使用,基于開放標準(個人見解)的服務發現解決方案(并且還提供了一大批其他的功能)。
我最近就如何 在一個微服務的架構下使用Consul來實現服務發現 做了一次演講,然后有很多人請求我講解下關于它的更多細節。因此在這篇文章,以及后續的幾篇系列文章里,我將會為你介紹具體該如何使用Consul。我將不會純粹只關注在Consul提供的服務發現部分,還會為你展示一系列Consul或者某些圍繞它建立的工具所提供的其他特性。請注意文章里所涉及的所有案例,Docker文件等。都可以在如下倉庫里找到: https://github.com/josdirksen/next-build-consul .
因此,與其從本文里"拷貝 & 復制",還不如直接克隆該倉庫。
在這第一篇文章里,我們將創建一個簡單的基于Docker的基礎設施,這里面一系列的服務將會使用簡單的HTTP調用形式與其他服務通信,并且使用Consul來發現其他服務實體。
我們最開始的目標架構有點像這個樣子:
為了實現這一切,我們首先需要通過以下步驟來建立一個我們可以在里面運行服務的基本環境:
注意:這一切我是通過在Mac上的Docker-machine實現的。如果你運行的是Windows或者Linux的話,鍵入的命令內容可能就會略有不同。我們只能寄望于支持Mac(以及Windows)的DOcker版本盡快正式發布( https://blog.docker.com/2016/03/docker-for-mac-windows-beta/ ),這樣一來的話我們便無需再這樣麻煩了。
-
創建四個Docker-machine:一個將用來運行Consul服務,而另外三個將用來運行單獨的服務以及Consul客戶端。
-
啟動主要的Consul服務端:我們將使用一個單一的Consul服務端(以及多個Consul客戶端,以后會更多)來追蹤正在運行的服務以及一些docker相關的東西。
- 配置Docker Swarm :為了避免不得不一個個地部署我們的服務,我們將使用Docker Swarm來管理這三個將運行我們服務的節點。在本文的余下部分,我們將使用Docker-Compose來啟停單獨的服務。
- 配置Docker覆蓋網絡 :如果我們想讓我們的服務和其他服務以簡單的方式通信的話,我們可能需要為此創建一個覆蓋網絡。這將使得我們部署到Docker的組件能夠很輕松地同其他服務通信(因為他們是共享一個子網)
- 啟動Consul客戶端 :每個節點都會有它自己的consul客戶端,它會在該節點上監控服務的健康性然后和Consul服務端通信。
因此我們要做的第一件事情便是創建一些Docker-machine。首先,我們將創建一臺支撐我們Consul服務端運行的Docker-machine。我們率先運行它的原因在于這樣一來我們便可以讓其他的Docker-machine配置成指向這臺容器里運行的Consul服務,并且使用它來管理Docker-swarm以及我們想要使用的覆蓋網絡。
docker-machine create nb-consul --driver virtualbox
而在我們啟動Consul服務之前,先讓我們快速看一下Consul背后的架構設計。
在這張圖里,你可以看到Consul可以運行的兩種模式。它能夠運行在服務端模式或者客戶端模式。所有的服務端節點會相互通信并且決定誰是領導者(leader)。一個客戶端節點則會與服務端節點里的其中一個交互,然后如常地運行在同樣還運行著應用服務的節點上。需要注意的同一個集群里的所有服務端和客戶端節點之間的狀態均是共享的。因此,當一個服務在其中一個客戶端上注冊本身時,它的相應信息也就對所有和其他節點連通的服務端以及客戶端可見。
在這個系列文章里,我們將不會去配置服務端節點的集群,而是簡單的只用一個節點。如今我們已經成功讓我們的Docker-machine跑了起來,我們可以啟動consul服務了。在我們開始之前請允許我先給你展示一個簡單的腳本,它可以幫助我們更加容易地在不同的Docker-machine之間切換,并且設置了別名以避免鍵入繁瑣的"Docker-machine"。
# 快速切換環境,例如: . dm-env nb-consul $ cat ~/bin/dm-env eval `docker-machine env $2 $1` # 避免太多不必要的輸入 $ alias dm dm=docker-machine
所以,有了這些替代的別名,首先,我們可以使用"dm-env nb-consul"來選擇合適的Docker-machine。
其次,我們拿到了這臺服務器的IP地址,然后我們可以使用類似如下的方式來啟動我們的Consul服務。
# 獲取IP地址 $ 192.168.99.106 # 在advertise里使用該IP地址 docker run -d --restart always -p 8300:8300 -p 8301:8301 -p 8301:8301/udp -p 8302:8302/udp \ -p 8302:8302 -p 8400:8400 -p 8500:8500 -p 53:53/udp -h server1 progrium/consul \ -server -bootstrap -ui-dir /ui -advertise $(dm ip nb-consul)
此刻,我們便成功地運行起來了我們的consul服務容器。現在,讓我們再來創建其他三個將要運行我們應用服務的服務器吧。
正如你所能看到的,在如下命令里,我們將會在同一時間內創建一個Docker swarm集群,并且"nb1"節點即swarm的master。
docker-machine create -d virtualbox --swarm --swarm-master \ --swarm-discovery="consul://$(docker-machine ip nb-consul):8500" \ --engine-opt="cluster-store=consul://$(docker-machine ip nb-consul):8500" \ --engine-opt="cluster-advertise=eth1:2376" nb1 docker-machine create -d virtualbox --swarm \ --swarm-discovery="consul://$(docker-machine ip nb-consul):8500" \ --engine-opt="cluster-store=consul://$(docker-machine ip nb-consul):8500" \ --engine-opt="cluster-advertise=eth1:2376" nb2 docker-machine create -d virtualbox --swarm \ --swarm-discovery="consul://$(docker-machine ip nb-consul):8500" \ --engine-opt="cluster-store=consul://$(docker-machine ip nb-consul):8500" \ --engine-opt="cluster-advertise=eth1:2376" nb3
這時候,我們已經創建了四臺Docker-machine并且成功地運行了起來。一個運行了Consul的master節點,其他的則暫時還沒做什么事情。
$ dm ls NAME ACTIVE DRIVER STATE URL SWARM nb1 - virtualbox Running tcp://192.168.99.110:2376 nb1 (master) nb2 - virtualbox Running tcp://192.168.99.111:2376 nb1 nb3 - virtualbox Running tcp://192.168.99.112:2376 nb1 nb-consul * virtualbox Running tcp://192.168.99.106:2376
在我們繼續配置slave節點之前,這里還有一個實用腳本可以派上用場:
$ cat addToHost #!/usr/bin/env bash update-docker-host(){ # 從/etc/hosts里清理現有的docker.local記錄 sudo sed -i "/"${1}"\.local$/d" /etc/hosts # 獲取運行中的docker machine的IP地址 export DOCKER_IP="$(docker-machine ip $1)" # 將docker machine的IP地址信息更新到/etc/hosts里 && sudo /bin/bash -c "echo \"${DOCKER_IP} $1.local\" >> /etc/hosts" } update-docker-host nb1 update-docker-host nb2 update-docker-host nb3 update-docker-host nb-consul
這個腳本將幾個Docker-machine的IP地址紛紛添加到了你的本地"hosts"文件里。這意味著我們可以在docker宿主機上簡單地通過類似 " http://nb-consul.local:8500 " 這樣的形式直接訪問服務。
在我們的場景里,我們希望我們所有的服務都能夠互相通信。我們擁有多臺Docker宿主機,因此我們需要找到一個簡便的方式使得運行在"nb1"節點上的服務能夠和"nb2"通信。想要達成這一點的話,最簡單的方法是創建一個單一網絡來承載所有運行在Docker容器里的服務。為此我們創建了一個像這樣的簡單"覆蓋"網絡:
# 選擇swarm master $dm-env nb1 --swarm # 創建一個名為my-net的覆蓋網絡
而自從我們在swarm master上創建了這個覆蓋網絡以后,該網絡便可用于swarm集群里的所有成員。在我們后面創建出應用服務后,我們將會讓它們均連接到這個網絡,如此它們便都可以共享同一個子網。
為了啟動consul客戶端,我們打算利用一下Docker-compose。Docker-Compose文件非常直截了當,并且算是一個不用鍵入全部啟動命令(除非當你在做現場demo)的簡便方案。
version: '2' services: agent-1: image: progrium/consul container_name: consul_agent_1 ports: - 8300:8300 - 8301:8301 - 8301:8301/udp - 8302:8302 - 8302:8302/udp - 8400:8400 - 8500:8500 - 53:53/udp environment: - "constraint:node==nb1" command: -ui-dir /ui -join 192.168.99.106 -advertise 192.168.99.110 networks: default: aliases: - agent-1 agent-2: image: progrium/consul container_name: consul_agent_2 ports: - 8300:8300 - 8301:8301 - 8301:8301/udp - 8302:8302 - 8302:8302/udp - 8400:8400 - 8500:8500 - 53:53/udp environment: - "constraint:node==nb2" command: -ui-dir /ui -join 192.168.99.106 -advertise 192.168.99.111 networks: default: aliases: - agent-2 agent-3: image: progrium/consul container_name: consul_agent_3 ports: - 8300:8300 - 8301:8301 - 8301:8301/udp - 8302:8302 - 8302:8302/udp - 8400:8400 - 8500:8500 - 53:53/udp environment: - "constraint:node==nb3" command: -ui-dir /ui -join 192.168.99.106 -advertise 192.168.99.112 networks: default: aliases: - agent-3 networks: default: external: name: my-net
這個文件并沒有什么特別之處。唯一你可能需要關注的地方便是我們在啟動Consul客戶端的命令里帶上了確切的IP地址。我們可以,簡單地,只使用一個環境變量來指代它,而這個也可以通過一個簡單的bash腳本來設置。然而,在本文中我們就只需要指定相關的Docker-machine的IP地址即可。
確保你的"DOCKER_HOST"已經指向了Docker swarm master,然后類似如下方式啟動客戶端:
# 啟動客戶端 $ docker-compose -f docker-compose-agents.yml up -d Creating consul_agent_3 Creating consul_agent_2 Creating consul_agent_1 # 檢查運行狀態 $ docker ps --format '{{ .ID }}\t{{ .Image }}\t{{ .Command }}\t{{ .Names}}' bf2000882dccprogrium/consul"/bin/start -ui-dir /"nb1/consul_agent_1 a1bc26eef516progrium/consul"/bin/start -ui-dir /"nb2/consul_agent_2 eb0d1c0cc075progrium/consul"/bin/start -ui-dir /"nb3/consul_agent_3
此時,我們成功在docker-machine "nb-consul"上跑起來了一個consul服務節點,然后我們在其余節點上運行了三個客戶端服務。為了驗證我們的配置,我們不妨打開Consul服務端的接口看看: http://nb-consul.local:8500
然后,正如你所見,我們跑起來了一個服務端節點(我們的consul服務),以及三個客戶端。所以在這之后我們便可以開始添加我們的應用服務,搭建完整的架構:
這些服務,在這個例子里,只是一些簡單的Golang應用。我創建了一個簡單的應用,它能以前端或者后端模式運行。在前端模式下,它提供一個極簡的UI,帶有一個按鈕可以用來調用后端服務。而在后端模式下它提供了一個簡單的API,它會返回一些信息給調用方并且還提供了一個簡單的UI展現一些統計數據。為了方便起見,我將這個鏡像推送到了Docker hub( https://hub.docker.com/r/josdirksen/demo-service/ ) 上,因此你可以簡單地直接使用它而無需再從Github上的源碼倉庫構建編譯。
正如你在之前的架構概述里所看到的那樣,我們打算在每個節點上均啟動一個前端以及一個后端服務。我們將會手工執行這一操作,但是由于我們配置了Docker-swarm,因此我們可以很輕松地通過一個單一的Docker-compose文件來實現這一點。如果你想看看這個文件具體內容的話,你可以看看這里的源碼( https://github.com/josdirksen/next-build-consul )
那么,讓我們先把服務運行起來,然后我們就可以看到它們是如此通過consul來注冊自己的:
# 確認你選擇了swarm master $ . dm-env nb1 --swarm # 現在可以使用docker-compose拉起后端服務 $ docker-compose -f docker-compose-backend.yml up -d Creating Backend2 Creating Backend3 Creating Backend1 # 然后用docker-compose拉起前端服務 $ docker-compose -f docker-compose-frontend.yml up -d Creating Frontend1 Creating Frontend3 Creating Frontend2 # 檢查docker是否一切都跑起來了 $ docker ps --format '{{ .ID }}\t{{ .Image }}\t{{ .Command }}\t{{ .Names}}' 65846be2e367 josdirksen/demo-service "/entrypoint.sh --typ" nb2/Frontend2 aedd80ab0889 josdirksen/demo-service "/entrypoint.sh --typ" nb3/Frontend3 d9c3b1d83b5e josdirksen/demo-service "/entrypoint.sh --typ" nb1/Frontend1 7c860403b257 josdirksen/demo-service "/entrypoint.sh --typ" nb1/Backend1 80632e910d33 josdirksen/demo-service "/entrypoint.sh --typ" nb3/Backend3 534da0670e13 josdirksen/demo-service "/entrypoint.sh --typ" nb2/Backend2 bf2000882dcc progrium/consul "/bin/start -ui-dir /" nb1/consul_agent_1 a1bc26eef516 progrium/consul "/bin/start -ui-dir /" nb2/consul_agent_2 eb0d1c0cc075 progrium/consul "/bin/start -ui-dir /" nb3/consul_agent_3
正如你在"Docker ps"后面的輸出所看到的那樣,我們擁有正在運行的三個前端,三個后端,以及三個consul客戶端服務。這正是我們所預期的架構。我們還可以打開Consul的界面看看情況
如你所見,我們在Consul注冊了三個前端和三個后端服務。如果我們打開其中一個后端的話我們將可以看到一些通用的信息:
然后我們可以在前端的UI上,調用其中一個后端服務的接口:
然而這里有一些問題我們需要去解答:
-
服務的注冊:當我們啟動一個后端或者前端服務時,我們可以看到它在consul里出現。我們怎么辦到的?
-
服務發現:再者說,當我們在前端服務上按下按鈕時,它會觸發一次對一個后端服務的調用。那個前端服務怎么知道它應該調用哪個服務呢?
在下一節里,我們將一步步揭開這些問題的答案。
第一關便是服務的注冊。為了在consul里注冊一個服務,我們得先給我們的本地consul客戶端發送一個非常簡單的REST調用,它也許看上去會是這個樣子:
{ "Name": "service1", "address": "10.0.0.12", "port": 8080, "Check": { "http": "http://10.0.0.12:8080/health", "interval": "5s" } }
如你所見,我們指定了可以用來發現服務的名字,地址,以及端口,然后我們添加了一個額外的健康檢查。當健康檢查返回的是某些在200范圍內的狀態碼時,該服務會被標記為是健康的,然后它便可以被其他服務檢索到。那么,我們如何讓我們的服務做到這一點?如果你看過這個例子里的源碼的話,你可以找到一個"script/entrypoint.sh"的文件,它看上去會是這個樣子:
#!/usr/bin/env bash IP=`ip addr | grep -E 'eth0.*state UP' -A2 | tail -n 1 | awk '{print $2}' | cut -f1 -d '/'` NAME="$2-service" read -r -d '' MSG << EOM { "Name": "$NAME", "address": "$IP", "port": $PORT, "Check": { "http": "http://$IP:$PORT", "interval": "5s" } } EOM curl -v -XPUT -d "$MSG" http://consul_agent_$SERVER_ID:8500/v1/agent/service/register && /app/main "$@"
這個腳本的功能便是,它會創建出一個將要發送給consul客戶端的JSON,然后在啟動主程序前,它會用"curl"命令將該請求發送出去。因此一旦服務啟動了,它便會自動將自己注冊到本地的consul客戶端(注意,你也可以通過一個更加自動的方法來辦到這一點,例如,使用 Consul Registrator )。
這的確奏效了,因為它們在同一個容器里,我們只要簡單地把它的名字關聯到本地客戶端即可。如果你看的更仔細些的話,你也許會發現我們在這里用到了一些環境變量。它們通過我們采用的Docker-compose文件傳送到了容器里:
... frontend-1: image: josdirksen/demo-service container_name: Frontend1 ports: - 8090:8090 environment: - "constraint:node==nb1" - SERVER_ID=1 - SERVERNAME=Server1 - PORT=8090 command: /entrypoint.sh --type frontend dns: 192.168.99.106 dns_search: service.consul ...
這里面最有趣的部分莫過于DNS記錄。你也許還記得192.168.99.106正是我們的consul服務端的IP地址。這就意味著我們可以對consul執行DNS查找操作(我們也可以指向一個consul客戶端)。
有了這個配置,我們便可以通過名稱來引用服務,然后使用DNS來解析它。如下操作即展示它是如何工作的:
# 從容器外部檢查被調用的后端服務所注冊的IP地址 $ dig @nb-consul.local backend-service.service.consul +short 10.0.9.7 10.0.9.8 10.0.9.6 # 如果從容器內部的話,我們可以單純的只做這一件事情 docker exec -ti nb2/Frontend2 ping backend-service PING backend-service.service.consul (10.0.9.8): 56 data bytes 64 bytes from 10.0.9.8: icmp_seq=0 ttl=64 time=0.809 ms
看上去很酷,不是嗎?我們只用DNS便可以發現該服務。這同樣也意味著將此集成到我們現有應用會是一件異常簡單的事情,因為我們只需要基本的DNS解析服務即可。舉個例子,在前端服務里,我們通過如下代碼調用后端:
resp, err := http.Get("http://backend-service:8081") if err != nil { // handle error fmt.Println(err) } else { defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) w.Header().Set("Content-Type",resp.Header.Get("Content-Type")) w.Write(body) }
這里通過DNS服務調用其中一個后端服務。我們如今也擁有了一些簡單的故障轉移,因為consul的DNS TTL被設置成了0。應用程序也許在它們這層仍舊做了一些緩存,然而這意味著我們至少擁有一些基本的故障轉移功能:
$ curl -s backend-service:8081 {"result" : { "servername" : "Server1", "querycount" : 778 } } # 關閉服務器1然后再做一次, curl會有1分鐘的DNS緩存, # 因此,你可能需要等上一會兒 $ curl -s backend-service:8081 {"result" : { "servername" : "Server2", "querycount" : 770 } } $ curl -s backend-service:8081 {"result" : { "servername" : "Server2", "querycount" : 771 } }
當然,這對我們的前端Golang應用同樣奏效:
在后面的文章里,我們還將介紹一些更先進的故障轉移,通過引入HAProxy作為中間件來實現更為高級的故障切換技術。以上便是這篇文章的全部內容。那么,不妨總結一下,我們做過的一些事情:
- 我們已經建立了一套擁有4個Docker節點的簡單架構。1個用作consul的服務端,其他三個則是我們的服務。
- 這些服務會在啟動時自動在Consul里注冊自己。
- 我們并不需要顯式地做些什么來開啟服務發現的功能。我們只要使用標準的DNS來查找服務即可。
- Consul檢索DNS時用的TTL為0,并且返回可用服務時采取的策略是輪詢(round-robin)。正如你之前所了解到的那樣,當一個DNS查找操作失敗時,你本身就可以用它來完成基本的故障轉移。
請繼續關注預期在未來幾周內出爐的后續文章。
來自: http://devopstarter.info/-fan-yi-dockerjie-he-consulshi-xian-de-fu-wu-fa-xian/