1. 程式人生 > >基於consul實現微服務的服務發現和負載均衡

基於consul實現微服務的服務發現和負載均衡

一. 背景

隨著2018年年初國務院辦公廳聯合多個部委共同釋出了《國務院辦公廳關於促進“網際網路+醫療健康”發展的意見(國辦發〔2018〕26號)》,國內醫療IT領域又迎來了一波網際網路醫院建設的高潮。不過網際網路醫院多基於實體醫院建設,雖說掛了一個“網際網路”的名號,但網際網路醫院系統也多與傳統的院內系統,比如:HISLISPACSEMR等共享院內的IT基礎設施。

如果你略微瞭解過國內醫院院內IT系統的現狀,你就知道目前的多數醫院的IT系統相比於網際網路行業、電信等行業來說是相對“落伍”的,這種落伍不僅體現在IT基礎設施的專業性和數量上,更體現在對新概念、新技術、新設計理念等應用上。雖然國內醫院IT系統在技術層面呈現出“多樣性”的特徵,但整體上偏陳舊和保守 – - 你可以在全國範圍內找到10-15年前的各種主流語言(

VBdelphic#等實現的IT系統,並且系統架構多為兩層C/S結構的。

近幾年“網際網路+醫療”的興起的確在一些方面提升了醫院的服務效率和水平,但這些網際網路醫療系統多部署於院外,並主要集中在“做入口”。它們並不算是醫院的核心繫統:即沒有這些網際網路系統,醫院的業務也是照常進行的(患者可以在傳統的視窗辦理所有院內業務,就是效率低罷了)。因此,雖然這些網際網路醫療系統採用了先進的網際網路系統設計理念和技術,但並沒有真正提升院內系統的技術水平,它們也只能與院內那些“陳舊”的、難於擴充套件的系統做對接。

不過網際網路醫院與這些系統有所不同,雖然它依然“可有可無”,但它卻是部署在院內IT基礎設施上的系統,同時也受到了院內IT基礎設施條件的限制

。在我們即將上線的一個針對醫院集團的網際網路醫院版本中,我們就遇到了“被限制”的問題。我們本想上線的Kubernetes叢集因為院方提供的硬體“不足”而無法實施,只能“降級”為手工打造的基於consul的微服務服務發現和負載均衡平臺,初步滿足我們的系統需要。而從k8sconsul的實踐過程,總是讓我有一種從工業時代回到的農業時代或是“消費降級”的趕腳^_^。

本文就來說說基於當前較新版本的consul實現微服務的服務發現和負載均衡的過程。

二. 實驗環境

這裡有三臺阿里雲的ECS,即用作部署consul叢集,也用來承載工作負載的節點(這點與真實生產環境還是蠻像的,醫院也僅能提供類似的這點兒可憐的裝置):

  • consul-1: 192.168.0.129
  • consul-2: 192.168.0.130
  • consul-3: 192.168.0.131

作業系統:Ubuntu server 16.04.4 LTS
核心版本:4.4.0-117-generic

實驗環境安裝有:

實驗所用的樣例程式映象:

三. 目標及方案原理

本次實驗的最基礎、最樸素的兩個目標:

  • 所有業務應用均基於容器執行
  • 某業務服務容器啟動後,會被自動註冊服務,同時其他服務可以自動發現該服務並呼叫,並且到達這個服務的請求會負載均衡到服務的多個例項。

這裡選擇了與程式語言技術棧無關的、可搭建微服務的服務發現和負載均衡的Hashicorpconsul。關於consul是什麼以及其基本原理和應用,可以參見我多年前寫的這篇有關consul的文章

但是光有consul還不夠,我們還需要結合consul-template、gliderlab的registrator以及nginx共同來實現上述目標,原理示意圖如下:

img{512x368}

原理說明:

  • 對於每個biz node上啟動的容器,位於每個node上的Registrator例項會監聽到該節點上容器的建立和停止的event,並將容器的資訊以consul service的形式寫入consul或從consul刪除。
  • 位於每個nginx node上的consul-template例項會watch consul叢集,監聽到consul service的相關event,並將需要expose到external的service資訊獲取,按照事先定義好的nginx conf template重新生成nginx.conf並reload本節點的nginx,使得nginx的新配置生效。
  • 對於內部服務來說(不通過nginx暴露到外部),在被registrator寫入consul的同時,也完成了在consul DNS的註冊,其他服務可以通過特定域名的方式獲取該內部服務的IP列表(A地址)和其他資訊,比如埠(SRV),並進而實現與這些內部服務的通訊。

參考該原理,落地到我們實驗環境的部署示意圖如下:

img{512x368}

四. 步驟

下面說說詳細的實驗步驟。

1. 安裝consul叢集

首先我們先來安裝consul叢集。consul既支援二進位制程式直接部署,也支援Docker容器化部署。如果consul叢集單獨部署在幾個專用節點上,那麼consul可以使用二種方式的任何一種。但是如果consul所在節點還承載工作負載,考慮consul作為整個分散式平臺的核心,降低它與docker engine引擎的耦合(docker engine可能會因各種情況經常restart),還是建議以二進位制程式形式直接部署在物理機或vm上。這裡的實驗環境資源有限,我們採用的是以二進位制程式形式直接部署的方式。

consul最新版本是1.2.2(截至發稿時),consul 1.2.x版本與consul 1.1.x版本最大的不同在於consul 1.2.x支援service mesh了,這對於consul來說可是革新性的變化,因此這裡擔心其初期的穩定性,因此我們選擇consul 1.1.0版本。

我們下載consul 1.1.0安裝包後,將其解壓到/usr/local/bin下。

在$HOME下建立consul-install目錄,並在其下面存放consul叢集的執行目錄consul-data。在consul-install目錄下,執行命令啟動節點consul-1上的consul:

consul-1 node:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=192.168.0.129 -datacenter=dc1 > consul-1.log & 2>&1

# tail -100f consul-1.log
bootstrap_expect > 0: expecting 3 servers
==> Starting Consul agent...
==> Consul agent running!
           Version: 'v1.1.0'
           Node ID: 'd23b9495-4caa-9ef2-a1d5-7f20aa39fd15'
         Node name: 'consul-1'
        Datacenter: 'dc1' (Segment: '<all>')
            Server: true (Bootstrap: false)
       Client Addr: [0.0.0.0] (HTTP: 8500, HTTPS: -1, DNS: 53)
      Cluster Addr: 192.168.0.129 (LAN: 8301, WAN: 8302)
           Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false

==> Log data will now stream in as it occurs:

    2018/09/10 10:21:09 [INFO] raft: Initial configuration (index=0): []
    2018/09/10 10:21:09 [INFO] raft: Node at 192.168.0.129:8300 [Follower] entering Follower state (Leader: "")
    2018/09/10 10:21:09 [INFO] serf: EventMemberJoin: consul-1.dc1 192.168.0.129
    2018/09/10 10:21:09 [INFO] serf: EventMemberJoin: consul-1 192.168.0.129
    2018/09/10 10:21:09 [INFO] consul: Adding LAN server consul-1 (Addr: tcp/192.168.0.129:8300) (DC: dc1)
    2018/09/10 10:21:09 [INFO] consul: Handled member-join event for server "consul-1.dc1" in area "wan"
    2018/09/10 10:21:09 [INFO] agent: Started DNS server 0.0.0.0:53 (tcp)
    2018/09/10 10:21:09 [INFO] agent: Started DNS server 0.0.0.0:53 (udp)
    2018/09/10 10:21:09 [INFO] agent: Started HTTP server on [::]:8500 (tcp)
    2018/09/10 10:21:09 [INFO] agent: started state syncer
==> Newer Consul version available: 1.2.2 (currently running: 1.1.0)
    2018/09/10 10:21:15 [WARN] raft: no known peers, aborting election
    2018/09/10 10:21:17 [ERR] agent: failed to sync remote state: No cluster leader

我們的三個節點的consul都以server角色啟動(consul agent -server),consul叢集初始有三個node( -bootstrap-expect=3),均位於dc1 datacenter(-datacenter=dc1),服務bind地址為192.168.0.129(-bind=192.168.0.129 ),允許任意client連線( -client=0.0.0.0)。我們啟動了consul ui(-ui),便於以圖形化的方式檢視consul叢集的狀態。我們設定了consul DNS服務的埠號為53(-dns-port=53),這個後續會起到重要作用,這裡先埋下小伏筆。

這裡我們使用nohup+&符號的方式將consul運行於後臺。生產環境建議使用systemd這樣的init系統對consul的啟停和配置更新進行管理。

從consul-1的輸出日誌來看,單節點並沒有選出leader。我們需要繼續在consul-2和consul-3兩個節點上也重複consul-1上的操作,啟動consul:

consul-2 node:

#nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=192.168.0.130 -datacenter=dc1 -join 192.168.0.129 > consul-2.log & 2>&1

consul-3 node:

# nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=192.168.0.131 -datacenter=dc1 -join 192.168.0.129 > consul-3.log & 2>&1

啟動後,我們檢視到consul-3.log中的日誌:

    2018/09/10 10:24:01 [INFO] consul: New leader elected: consul-3
    2018/09/10 10:24:01 [WARN] raft: AppendEntries to {Voter a215865f-dba7-5caa-cfb3-6850316199a3 192.168.0.130:8300} rejected, sending older logs (next: 1)
    2018/09/10 10:24:01 [INFO] raft: pipelining replication to peer {Voter a215865f-dba7-5caa-cfb3-6850316199a3 192.168.0.130:8300}
    2018/09/10 10:24:01 [WARN] raft: AppendEntries to {Voter d23b9495-4caa-9ef2-a1d5-7f20aa39fd15 192.168.0.129:8300} rejected, sending older logs (next: 1)
    2018/09/10 10:24:01 [INFO] raft: pipelining replication to peer {Voter d23b9495-4caa-9ef2-a1d5-7f20aa39fd15 192.168.0.129:8300}
    2018/09/10 10:24:01 [INFO] consul: member 'consul-1' joined, marking health alive
    2018/09/10 10:24:01 [INFO] consul: member 'consul-2' joined, marking health alive
    2018/09/10 10:24:01 [INFO] consul: member 'consul-3' joined, marking health alive
    2018/09/10 10:24:01 [INFO] agent: Synced node info
==> Newer Consul version available: 1.2.2 (currently running: 1.1.0)

consul-3 node上的consul被選為初始leader了。我們可以通過consul提供的子命令檢視叢集狀態:

#  consul operator raft list-peers
Node      ID                                    Address             State     Voter  RaftProtocol
consul-3  0020b7aa-486a-5b44-b5fd-be000a380a89  192.168.0.131:8300  leader  true   3
consul-1  d23b9495-4caa-9ef2-a1d5-7f20aa39fd15  192.168.0.129:8300  follower  true   3
consul-2  a215865f-dba7-5caa-cfb3-6850316199a3  192.168.0.130:8300  follower    true   3

我們還可以通過consul ui以圖形化方式檢視叢集狀態和叢集記憶體儲的各種配置資訊:

img{512x368}

至此,consul叢集就搭建ok了。

2. 安裝Nginx、consul-template和Registrator

根據前面的“部署示意圖”,我們在consul-1和consul-2上安裝nginx、consul-template和Registrator,在consul-3上安裝Registrator。

a) Nginx的安裝

我們使用ubuntu 16.04.4預設源中的nginx版本:1.10.3,通過apt-get install nginx安裝nginx,這個無須贅述了。

b) consul-template的安裝

consul-template是一個將consul叢集中儲存的資訊轉換為檔案形式的工具。常用的場景是監聽consul叢集中資料的變化,並結合模板將資料持久化到某個檔案中,再執行某一關聯的action。比如我們這裡通過consul-template監聽consul叢集中service資訊的變化,並將service資訊資料與nginx的配置模板結合,生成nginx可用的nginx.conf配置檔案,並驅動nginx重新reload配置檔案,使得nginx的配置更新生效。因此一般來說,哪裡部署有nginx,我們就應該有一個配對的consul-template部署。

在我們的實驗環境中consul-1和consul-2兩個節點部署了nginx,因此我們需要在consul-1和consul-2兩個節點上部署consul-template。我們直接安裝comsul-template的二進位制程式(我們使用0.19.5版本),下載安裝包並解壓後,將consul-template放入/usr/local/bin目錄下:

# wget -c https://releases.hashicorp.com/consul-template/0.19.5/consul-template_0.19.5_linux_amd64.zip

# unzip consul-template_0.19.5_linux_amd64.zip
# mv consul-tempate /usr/local/bin
# consul-template -v
consul-template v0.19.5 (57b6c71)

這裡先不啟動consul-template,後續在註冊不同服務的場景中,我們再啟動consul-template。

c) Registrator的安裝

Registrator是另外一種工具,它監聽Docker引擎上發生的容器建立和停止事件,並將啟動的容器資訊以consul service的形式儲存在consul叢集中。因此,Registrator和node上的docker engine對應,有docker engine部署的節點上都應該安裝有對應的Registator。因此我們要在實驗環境的三個節點上都部署Registrator。

Registrator官方推薦的就是以Docker容器方式執行,但這裡我並不使用lastest版本,而是用master版本,因為只有最新的master版本才支援service meta資料的寫入,而當前的latest版本是v7版本,年頭較長,並不支援service meta資料寫入。

在所有實驗環境節點上執行:

 # docker run --restart=always -d \
    --name=registrator \
    --net=host \
    --volume=/var/run/docker.sock:/tmp/docker.sock \
    gliderlabs/registrator:master\
      consul://localhost:8500

我們看到registrator將node節點上的/var/run/docker.sock對映到容器內部的/tmp/docker.sock上,通過這種方式registrator可以監聽到node上docker引擎上的事件變化。registrator的另外一個引數:consul://localhost:8500則是Registrator要寫入資訊的consul地址(當然Registrator不僅僅支援consul,還支援etcd、zookeeper等),這裡傳入的是本node上consul server的地址和服務埠。

Registrator的啟動日誌如下:

# docker logs -f registrator
2018/09/10 05:56:39 Starting registrator v7 ...
2018/09/10 05:56:39 Using consul adapter: consul://localhost:8500
2018/09/10 05:56:39 Connecting to backend (0/0)
2018/09/10 05:56:39 consul: current leader  192.168.0.130:8300
2018/09/10 05:56:39 Listening for Docker events ...
2018/09/10 05:56:39 Syncing services on 1 containers
2018/09/10 05:56:39 ignored: 6ef6ae966ee5 no published ports

在所有節點都啟動完Registrator後,我們來先檢視一下當前consul叢集中service的catelog以及每個catelog下的service的詳細資訊:

// consul-1:

# curl  http://localhost:8500/v1/catalog/services
{"consul":[]}

目前只有consul自己內建的consul service catelog,我們檢視一下consul這個catelog service的詳細資訊:

// consul-1:

# curl  localhost:8500/v1/catalog/service/consul|jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1189  100  1189    0     0   180k      0 --:--:-- --:--:-- --:--:--  193k
[
  {
    "ID": "d23b9495-4caa-9ef2-a1d5-7f20aa39fd15",
    "Node": "consul-1",
    "Address": "192.168.0.129",
    "Datacenter": "dc1",
    "TaggedAddresses": {
      "lan": "192.168.0.129",
      "wan": "192.168.0.129"
    },
    "NodeMeta": {
      "consul-network-segment": ""
    },
    "ServiceID": "consul",
    "ServiceName": "consul",
    "ServiceTags": [],
    "ServiceAddress": "",
    "ServiceMeta": {},
    "ServicePort": 8300,
    "ServiceEnableTagOverride": false,
    "CreateIndex": 5,
    "ModifyIndex": 5
  },
  {
    "ID": "a215865f-dba7-5caa-cfb3-6850316199a3",
    "Node": "consul-2",
    "Address": "192.168.0.130",
    "Datacenter": "dc1",
    "TaggedAddresses": {
      "lan": "192.168.0.130",
      "wan": "192.168.0.130"
    },
    "NodeMeta": {
      "consul-network-segment": ""
    },
    "ServiceID": "consul",
    "ServiceName": "consul",
    "ServiceTags": [],
    "ServiceAddress": "",
    "ServiceMeta": {},
    "ServicePort": 8300,
    "ServiceEnableTagOverride": false,
    "CreateIndex": 6,
    "ModifyIndex": 6
  },
  {
    "ID": "0020b7aa-486a-5b44-b5fd-be000a380a89",
    "Node": "consul-3",
    "Address": "192.168.0.131",
    "Datacenter": "dc1",
    "TaggedAddresses": {
      "lan": "192.168.0.131",
      "wan": "192.168.0.131"
    },
    "NodeMeta": {
      "consul-network-segment": ""
    },
    "ServiceID": "consul",
    "ServiceName": "consul",
    "ServiceTags": [],
    "ServiceAddress": "",
    "ServiceMeta": {},
    "ServicePort": 8300,
    "ServiceEnableTagOverride": false,
    "CreateIndex": 7,
    "ModifyIndex": 7
  }
]

3. 內部http服務的註冊和發現

對於微服務而言,有暴露到外面的,也有僅執行在內部,被內部服務呼叫的。我們先來看看內部服務,這裡以一個http服務為例。

對於暴露到外部的微服務而言,可以通過域名、路徑、埠等來發現。但是對於內部服務,我們怎麼發現呢?k8s中我們可以通過k8s叢集的DNS外掛進行自動域名解析實現,每個pod中container的DNS server指向的就是k8s dns server。這樣service之間可以通過使用固定規則的域名(比如:your_svc.default.svc.cluster.local)來訪問到另外一個service(僅需配置一個service name),再通過service實現該服務請求負載均衡到service關聯的後端endpoint(pod container)上。consul叢集也可以做到這點,並使用consul提供的DNS服務來實現內部服務的發現。

我們需要對三個節點的DNS配置進行update,將consul DNS server加入到主機DNS resolver(這也是之前在啟動consul時將consul DNS的預設監聽埠從8600改為53的原因),步驟如下:

  • 編輯/etc/resolvconf/resolv.conf.d/base,加入一行:
nameserver 127.0.0.1
  • 重啟resolveconf服務
 /etc/init.d/resolvconf restart

再檢視/etc/resolve.conf檔案:

# cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 100.100.2.136
nameserver 100.100.2.138
nameserver 127.0.0.1
options timeout:2 attempts:3 rotate single-request-reopen

我們發現127.0.0.1這個DNS server地址已經被加入到/etc/resolv.conf中了(切記:不要直接手工修改/etc/resolve.conf)。

好了!有了consul DNS,我們就可以發現consul中的服務了。consul給其叢集內部的service一個預設的域名:your_svc.service.{data-center}.consul. 之前我們查看了cluster中只有一個consul catelog service,我們就來訪問一下該consul service:

# ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.129) 56(84) bytes of data.
64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=2 ttl=64 time=0.025 ms
64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=3 ttl=64 time=0.031 ms

# ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.130) 56(84) bytes of data.
64 bytes from 192.168.0.130: icmp_seq=1 ttl=64 time=0.186 ms
64 bytes from 192.168.0.130: icmp_seq=2 ttl=64 time=0.136 ms
64 bytes from 192.168.0.130: icmp_seq=3 ttl=64 time=0.195 ms

# ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.131) 56(84) bytes of data.
64 bytes from 192.168.0.131: icmp_seq=1 ttl=64 time=0.149 ms
64 bytes from 192.168.0.131: icmp_seq=2 ttl=64 time=0.184 ms
64 bytes from 192.168.0.131: icmp_seq=3 ttl=64 time=0.179 ms

我們看到consul服務有三個例項,因此DNS輪詢在不同ping命令執行時返回了不同的地址。

現在在主機層面上,我們可以發現consul中的service了。如果我們的服務呼叫者跑在docker container中,我們還能找到consul服務麼?

# docker run busybox ping consul.service.dc1.consul
ping: bad address 'consul.service.dc1.consul'

事實告訴我們:不行!

那麼我們如何讓運行於docker container中的服務呼叫者也能發現consul中的service呢?我們需要給docker引擎指定DNS:

在/etc/docker/daemon.json中新增下面配置:

{
    "dns": ["node_ip", "8.8.8.8"] //node_ip: consul_1為192.168.0.129、consul_2為192.168.0.130、consul_3為192.168.0.131
}

重啟docker引擎後,再嘗試在容器內發現consul服務:

# docker run busybox ping consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.131): 56 data bytes
64 bytes from 192.168.0.131: seq=0 ttl=63 time=0.268 ms
64 bytes from 192.168.0.131: seq=1 ttl=63 time=0.245 ms
64 bytes from 192.168.0.131: seq=2 ttl=63 time=0.235 ms

這次就ok了!

接下來我們在三個節點上以容器方式啟動我們的一個內部http服務demo httpbackend:

# docker run --restart=always -d  -l "SERVICE_NAME=httpbackend" -p 8081:8081 bigwhite/httpbackendservice:v1.0.0

我們檢視一下consul叢集內的httpbackend service資訊:

# curl  localhost:8500/v1/catalog/service/httpbackend|jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1374  100  1374    0     0   519k      0 --:--:-- --:--:-- --:--:--  670k
[
  {
    "ID": "d23b9495-4caa-9ef2-a1d5-7f20aa39fd15",
    "Node": "consul-1",
    "Address": "192.168.0.129",
   ...
  },
  {
    "ID": "a215865f-dba7-5caa-cfb3-6850316199a3",
    "Node": "consul-2",
    "Address": "192.168.0.130",
   ...
  },
  {
    "ID": "0020b7aa-486a-5b44-b5fd-be000a380a89",
    "Node": "consul-3",
    "Address": "192.168.0.131",
   ...
  }
]

再訪問一下該服務:

# curl httpbackend.service.dc1.consul:8081
this is httpbackendservice, version: v1.0.0

內部服務發現成功!

4. 暴露外部http服務

說完了內部服務,我們再來說說那些要暴露到外部的服務,這個環節就輪到consul-template登場了!在我們的實驗中,consul-template讀取consul中service資訊,並結合模板生成nginx配置檔案。我們基於預設安裝的/etc/nginx/nginx.conf檔案內容來編寫我們的模板。我們先實驗暴露http服務到外面。下面是模板樣例:

//nginx.conf.template

.... ...

http {
        ... ...
        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

        #
        # http server config
        #

        {{range services -}}
        {{$name := .Name}}
        {{$service := service .Name}}
        {{- if in .Tags "http" -}}
        upstream {{$name}} {
          zone upstream-{{$name}} 64k;
          {{range $service}}
          server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1;
          {{end}}
        }{{end}}
        {{end}}

        {{- range services -}} {{$name := .Name}}
        {{- if in .Tags "http" -}}
        server {
          listen 80;
          server_name {{$name}}.tonybai.com;

          location / {
            proxy_pass http://{{$name}};
          }
        }
        {{end}}
        {{end}}

}

consul-template使用的模板採用的是go template的語法。我們看到在http block中,我們要為consul中的每個要expose到外部的catelog service定義一個server block(對應的域名為your_svc.tonybai.com)和一個upstream block。

對上面的模板做簡單的解析,弄明白三點,模板基本就全明白了:

  • {{- range services -}}: 標準的{{ range pipeline }}模板語法,services這個pipeline的呼叫相當於: curl localhost:8500/v1/catalog/services,即獲取catelog services列表。這個列表中的每項僅有Name和Tags兩個欄位可用。
  • {{- if in .Tags “http” -}}:判斷語句,即如果Tags欄位中有http這個tag,那麼則暴露該catelog service。
  • {{range $service}}: 也是標準的{{ range pipeline }}模板語法,$service這個pipeline呼叫相當於curl localhost:8500/v1/catalog/service/xxxx,即獲取某個service xxx的詳細資訊,包括Address、Port、Tag、Meta等。

接下來,我們在consul-1和consul-2上啟動consul-template:

consul-1:
# nohup  consul-template -template "/root/consul-install/templates/nginx.conf.template:/etc/nginx/nginx.conf:nginx -s reload" > consul-template.log & 2>&1

consul-2:
# nohup  consul-template -template "/root/consul-install/templates/nginx.conf.template:/etc/nginx/nginx.conf:nginx -s reload" > consul-template.log & 2>&1

檢視/etc/nginx/nginx.conf,你會發現http server config下面並沒有生成任何配置,因為consul叢集中還沒有滿足Tag條件的service(包含tag “http”)。現在我們就來在三個node上建立httpfront services。

# docker run --restart=always -d -l "SERVICE_NAME=httpfront" -l "SERVICE_TAGS=http" -P bigwhite/httpfrontservice:v1.0.0

檢視生成的nginx.conf:

upstream httpfront {
      zone upstream-httpfront 64k;

          server 192.168.0.129:32769 max_fails=3 fail_timeout=60 weight=1;

          server 192.168.0.130:32768 max_fails=3 fail_timeout=60 weight=1;

          server 192.168.0.131:32768 max_fails=3 fail_timeout=60 weight=1;

    }

    server {
      listen 80;
          server_name httpfront.tonybai.com;

      location / {
        proxy_pass http://httpfront;
      }
    }

測試一下httpfront.tonybai.com(可通過修改/etc/hosts),httpfront service會呼叫內部服務httpbackend(通過httpbackend.service.dc1.consul:8081訪問):

# curl httpfront.tonybai.com
this is httpfrontservice, version: v1.0.0, calling backendservice ok, its resp: [this is httpbackendservice, version: v1.0.0
]

可以在各個節點上檢視httpfront的日誌:(通過docker logs),你會發現到httpfront.tonybai.com的請求被均衡到了各個節點上的httpfront service上了:

{GET / HTTP/1.0 1 0 map[Connection:[close] User-Agent:[curl/7.47.0] Accept:[*/*]] {} <nil> 0 [] true httpfront map[] map[] <nil> map[] 192.168.0.129:35184 / <nil> <nil> <nil> 0xc0000524c0}
calling backendservice...
{200 OK 200 HTTP/1.1 1 1 map[Date:[Mon, 10 Sep 2018 08:23:33 GMT] Content-Length:[44] Content-Type:[text/plain; charset=utf-8]] 0xc0000808c0 44 [] false false map[] 0xc000132600 <nil>}
this is httpbackendservice, version: v1.0.0

5. 暴露外部tcp服務

我們的微服務可不僅僅有http服務的,還有直接暴露tcp socket服務的。nginx對tcp的支援是通過stream block支援的。在stream block中,我們來為每個要暴露在外面的tcp service生成server block和upstream block,這部分模板內容如下:

stream {
   {{- range services -}}
   {{$name := .Name}}
   {{$service := service .Name}}
     {{- if in .Tags "tcp" -}}
  upstream {{$name}} {
    least_conn;
    {{- range $service}}
    server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=30s weight=5;
    {{ end }}
  }
     {{end}}
  {{end}}

   {{- range services -}}
   {{$name := .Name}}
   {{$nameAndPort := $name | split "-"}}
    {{- if in .Tags "tcp" -}}
  server {
      listen {{ index $nameAndPort 1 }};
      proxy_pass {{$name}};
  }
    {{end}}
   {{end}}
}

和之前的http服務模板相比,這裡的Tag過濾詞換為了“tcp”,並且由於埠具有排他性,這裡用”名字-埠”串來作為service的name以及upstream block的標識。用一個例子來演示會更加清晰。由於修改了nginx模板,在演示demo前,需要重啟一下各個consul-template。

然後我們在各個節點上啟動tcpfront service(注意服務名為tcpfront-9999,9999是tcpfrontservice expose到外部的埠):

# docker run -d --restart=always -l "SERVICE_TAGS=tcp" -l "SERVICE_NAME=tcpfront-9999" -P bigwhite/tcpfrontservice:v1.0.0

啟動後,我們檢視一下生成的nginx.conf:

stream {

   upstream tcpfront-9999 {
    least_conn;
    server 192.168.0.129:32770 max_fails=3 fail_timeout=30s weight=5;

    server 192.168.0.130:32769 max_fails=3 fail_timeout=30s weight=5;

    server 192.168.0.131:32769 max_fails=3 fail_timeout=30s weight=5;

  }

   server {
      listen 9999;
      proxy_pass tcpfront-9999;
  }

}

nginx對外的9999埠對應到叢集內的tcpfront服務!這個tcpfront是一個echo服務,我們來測試一下:

# telnet localhost 9999
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
[v1.0.0]2018-09-10 08:56:15.791728641 +0000 UTC m=+531.620462772 [hello
]
tonybai
[v1.0.0]2018-09-10 08:56:17.658482957 +0000 UTC m=+533.487217127 [tonybai
]

基於暴露tcp服務,我們還可以實現將全透傳的https服務暴露到外部。所謂全透傳的https服務,即ssl證書配置在服務自身,而不是nginx上面。其實現方式與暴露tcp服務相似,這裡就不舉例了。

五. 小結

以上基於consul+consul-template+registrator+nginx實現了一個基本的微服務服務發現和負載均衡框架,但要應用到生產環境還需一些進一步的考量。

關於服務治理的一些功能,consul 1.2.x版本已經加入了service mesh的support,後續在成熟後可以考慮upgrade consul cluster。

consul-template在v0.19.5中還不支援servicemeta的,但在master版本中已經支援,後續利用新版本的consul-template可以實現功能更為豐富的模板,比如實現灰度釋出等。