首頁>技術>

在“雲原生”、“容器化”、“微服務”、“服務網格”等概念大行其道的今天,一提到叢集管理、容器工作負載排程,人們首先想到的是Kubernetes。

Kubernetes經過多年的發展,目前已經成為了雲原生計算平臺的事實標準,得到了諸如谷歌、微軟、紅帽、亞馬遜、IBM、阿里等大廠的大力支援,各大雲計算提供商也都提供了專屬Kubernetes叢集服務。開發人員可以一鍵在這些大廠的雲上建立k8s叢集。對於那些不願被cloud provider繫結的組織或開發人員,Kubernetes也提供了諸如Kubeadm這樣的k8s叢集引導工具,幫助大家在裸金屬機器上搭建自己的k8s叢集,當然這樣做的門檻較高(如果您想學習自己搭建和管理k8s叢集,可以參考我在慕課網上釋出的實戰課《高可用叢集搭建、配置、運維與應用》)。

Kubernetes的學習曲線是公認的較高,尤其是對於應用開發人員。再加上Kubernetes發展很快,越來越多的概念和功能加入到k8s技術棧,這讓人們不得不考慮建立和維護這樣一套叢集所要付出的成本。人們也在考慮是否所有場景都需要部署一個k8s叢集,是否有輕量級的且能滿足自身需求的叢集管理和微服務部署排程方案呢?外國朋友Matthias Endler就在其文章《也許你不需要Kubernetes》中給出一個輕量級的叢集管理方案 – 使用hashicorp開源的nomad工具。

這讓我想起了去年寫的《基於consul實現微服務的服務發現和負載均衡》一文。文中雖然實現了基於consul的服務註冊、發現以及負載均衡,但是缺少一個環節:那就是整個叢集管理以及工作負載部署排程自動化的缺乏。nomad應該恰好可以補足這一短板,並且它足夠輕量。本文我們就來探索和實踐一下使用nomad實現叢集管理和微服務部署排程。

一. 安裝nomad叢集

nomad是Hashicorp公司出品的叢集管理和工作負荷排程器,支援多種驅動形式的工作負載排程,包括Docker容器、虛擬機器、原生可執行程式等,並支援跨資料中心排程。Nomad不負責服務發現或金鑰管理等 ,它將這些功能分別留給了HashiCorp的Consul和Vault。HashiCorp的創始人認為,這會使得Nomad更為輕量級,排程效能更高。

nomad使用Go語言實現,因此其本身僅僅是一個可執行的二進位制檔案。和Hashicorp其他工具產品(諸如:consul等)類似,nomad一個可執行檔案既可以以server模式執行,亦可以client模式執行,甚至可以啟動一個例項,既是server,也是client。

下面是nomad叢集的架構圖(來自hashicorp官方):

一個nomad叢集至少要包含一個server,作為叢集的控制平面;一個或多個client則用於承載工作負荷。通常生產環境nomad叢集的控制平面至少要有5個及以上的server才能在高可用上有一定保證。

建立一個nomad叢集有多種方法,包括手工建立、基於consul自動建立和基於雲自動建立。考慮到後續涉及微服務的註冊發現,這裡我們採用基於consul自動建立nomad叢集的方法,下面是部署示意圖:

我這裡的試驗環境僅有三臺hosts,因此這三臺host既承載consul叢集,也承載nomad叢集(包括server和client),即nomad的控制平面和工作負荷由這三臺host一併承擔了。

1. consul叢集啟動

在之前的《基於consul實現微服務的服務發現和負載均衡》一文中,我對consul叢集的建立做過詳細地說明,因此這裡只列出步驟,不詳細解釋了。注意:這次consul的版本升級到了consul v1.4.4了。

在每個node上分別下載consul 1.4.4:

# wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip# unzip consul_1.4.4_linux_amd64.zip# cp consul /usr/local/bin# consul -vConsul v1.4.4Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

啟動consul叢集:(每個node上建立~/.bin/consul-install目錄,並進入該目錄下執行)

dxnode1:# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=~/.bin/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=172.16.66.102 -datacenter=dc1 > consul-1.log & 2>&1dxnode2:# 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=172.16.66.103 -datacenter=dc1 -join 172.16.66.102 > consul-2.log & 2>&1dxnode3: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=172.16.66.104 -datacenter=dc1 -join 172.16.66.102 > consul-3.log & 2>&1

consul叢集啟動結果檢視如下:

# consul membersNode      Address             Status  Type    Build  Protocol  DC   Segmentconsul-1  172.16.66.102:8301  alive   server  1.4.4  2         dc1  <all>consul-2  172.16.66.103:8301  alive   server  1.4.4  2         dc1  <all>consul-3  172.16.66.104:8301  alive   server  1.4.4  2         dc1  <all># consul operator raft list-peersNode      ID                                    Address             State     Voter  RaftProtocolconsul-3  d048e55b-5f6a-34a4-784c-e6607db0e89e  172.16.66.104:8300  leader    true   3consul-1  160a7a20-f177-d2f5-0765-e6d1a9a1a9a4  172.16.66.102:8300  follower  true   3consul-2  6795cd2c-fad5-9d4f-2531-13b0a65e0893  172.16.66.103:8300  follower  true   3
2. DNS設定(可選)

如果採用基於consul DNS的方式進行服務發現,那麼在每個nomad client node上設定DNS則很必要。否則如果要是基於consul service catalog的API去查詢service,則可忽略這個步驟。設定步驟如下:

在每個node上,建立和編輯/etc/resolvconf/resolv.conf.d/base,填入如下內容:

nameserver {consul-1-ip}nameserver {consul-2-ip}

然後重啟resolvconf服務:

#  /etc/init.d/resolvconf restart[ ok ] Restarting resolvconf (via systemctl): resolvconf.service.

新的resolv.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 OVERWRITTENnameserver {consul-1-ip}nameserver {consul-2-ip}nameserver 100.100.2.136nameserver 100.100.2.138options timeout:2 attempts:3 rotate single-request-reopen

這樣無論是在host上,還是在新啟動的container裡就都可以訪問到xx.xx.consul域名的服務了:

# ping -c 3 consul.service.dc1.consulPING consul.service.dc1.consul (172.16.66.103) 56(84) bytes of data.64 bytes from 172.16.66.103: icmp_seq=1 ttl=64 time=0.227 ms64 bytes from 172.16.66.103: icmp_seq=2 ttl=64 time=0.158 ms^C--- consul.service.dc1.consul ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 999msrtt min/avg/max/mdev = 0.158/0.192/0.227/0.037 ms# docker run busybox ping -c 3 consul.service.dc1.consulPING consul.service.dc1.consul (172.16.66.104): 56 data bytes64 bytes from 172.16.66.104: seq=0 ttl=64 time=0.067 ms64 bytes from 172.16.66.104: seq=1 ttl=64 time=0.061 ms64 bytes from 172.16.66.104: seq=2 ttl=64 time=0.076 ms--- consul.service.dc1.consul ping statistics ---3 packets transmitted, 3 packets received, 0% packet lossround-trip min/avg/max = 0.061/0.068/0.076 ms
3. 基於consul叢集引導啟動nomad叢集

按照之前的拓撲圖,我們需先在每個node上分別下載nomad:

# wget -c https://releases.hashicorp.com/nomad/0.8.7/nomad_0.8.7_linux_amd64.zip# unzip nomad_0.8.7_linux_amd64.zip.zip# cp ./nomad /usr/local/bin# nomad -vNomad v0.8.7 (21a2d93eecf018ad2209a5eab6aae6c359267933+CHANGES)

我們已經建立了consul叢集,因為我們將採用基於consul叢集引導啟動nomad叢集這一建立nomad叢集的最Easy方式。同時,我們每個node上既要執行nomad server,也要nomad client,於是我們在nomad的配置檔案中,對server和client都設定為”enabled = true”。下面是nomad啟動的配置檔案,每個node上的nomad均將該配置檔案作為為輸入:

// agent.hcldata_dir = "/root/.bin/nomad-install/nomad.d"server {  enabled = true  bootstrap_expect = 3}client {  enabled = true}

下面是在各個節點上啟動nomad的操作步驟:

dxnode1:# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  > nomad-1.log & 2>&1dxnode2:# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  > nomad-2.log & 2>&1dxnode3:# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  > nomad-3.log & 2>&1

檢視nomad叢集的啟動結果:

#  nomad server membersName            Address        Port  Status  Leader  Protocol  Build  Datacenter  Regiondxnode1.global  172.16.66.102  4648  alive   true    2         0.8.7  dc1         globaldxnode2.global  172.16.66.103  4648  alive   false   2         0.8.7  dc1         globaldxnode3.global  172.16.66.104  4648  alive   false   2         0.8.7  dc1         global# nomad operator raft list-peersNode            ID                  Address             State     Voter  RaftProtocoldxnode1.global  172.16.66.102:4647  172.16.66.102:4647  leader    true   2dxnode2.global  172.16.66.103:4647  172.16.66.103:4647  follower  true   2dxnode3.global  172.16.66.104:4647  172.16.66.104:4647  follower  true   2# nomad node-statusID        DC   Name     Class   Drain  Eligibility  Status7acdd7bc  dc1  dxnode1  <none>  false  eligible     readyc281658a  dc1  dxnode3  <none>  false  eligible     ready9e3ef19f  dc1  dxnode2  <none>  false  eligible     ready

以上這些命令的結果都顯示nomad叢集工作正常!

nomad還提供一個ui介面(http://nomad-node-ip:4646/ui),可以讓運維人員以視覺化的方式直觀看到當前nomad叢集的狀態,包括server、clients、工作負載(job)的情況:

nomad ui首頁

nomad server列表和狀態

nomad client列表和狀態

二. 部署工作負載

引導啟動成功nomad集群后,我們接下來就要向叢集中新增“工作負載”了。

在Kubernetes中,我們可以透過建立deployment、pod等向叢集新增工作負載;在nomad中我們也可以透過類似的宣告式的方法向nomad叢集新增工作負載。不過nomad相對簡單許多,它僅提供了一種名為job的抽象,並給出了job的specification。nomad叢集所有關於工作負載的操作均透過job描述檔案和nomad job相關子命令完成。下面是透過job部署工作負載的流程示意圖:

從圖中可以看到,我們需要做的僅僅是將編寫好的job檔案提交給nomad即可。

Job spec定義了:job -> group -> task的層次關係。每個job檔案只有一個job,但是一個job可能有多個group,每個group可能有多個task。group包含一組要放在同一個叢集中排程的task。一個Nomad task是由其驅動程式(driver)在Nomad client節點上執行的命令、服務、應用程式或其他工作負載。task可以是短時間的批處理作業(batch)或長時間執行的服務(service),例如web應用程式、資料庫伺服器或API。

Tasks是在用HCL語法的宣告性job規範中定義的。Job檔案提交給Nomad服務端,服務端決定在何處以及如何將job檔案中定義的task分配給客戶端節點。另一種概念化的理解是:job規範表示工作負載的期望狀態,Nomad服務端建立並維護其實際狀態。

透過job,開發人員還可以為工作負載定義約束和資源。約束(constraint)透過核心型別和版本等屬性限制了工作負載在節點上的位置。資源(resources)需求包括執行task所需的記憶體、網路、CPU等。

有三種類型的job:system、service和batch,它們決定Nomad將用於此job中task的排程器。service 排程器被設計用來排程永遠不會宕機的長壽命服務。batch作業對短期效能波動的敏感性要小得多,壽命也很短,幾分鐘到幾天就可以完成。system排程器用於註冊應該在滿足作業約束的所有nomad client上執行的作業。當某個client加入到nomad叢集或轉換到就緒狀態時也會呼叫它。

Nomad允許job作者為自動重新啟動失敗和無響應的任務指定策略,並自動將失敗的任務重新排程到其他節點,從而使任務工作負載具有彈性。

如果對應到k8s中的概念,group更像是某種controller,而task更類似於pod,是被真實排程的實體。Job spec對應某個k8s api object的spec,具體體現在某個yaml檔案中。

下面我們就來真實地在nomad叢集中建立一個工作負載。我們使用之前在《基於consul實現微服務的服務發現和負載均衡》一文中使用過的那幾個demo image,這裡我們先使用httpbackendservice映象來建立一個job。

下面是httpbackend的job檔案:

// httpbackend-1.nomadjob "httpbackend" {  datacenters = ["dc1"]  type = "service"  group "httpbackend" {    count = 2    task "httpbackend" {      driver = "docker"      config {        image = "bigwhite/httpbackendservice:v1.0.0"        port_map {          http = 8081        }        logging {          type = "json-file"        }      }      resources {        network {          mbits = 10          port "http" {}        }      }      service {        name = "httpbackend"        port = "http"      }    }  }}

這個檔案基本都是自解釋的,重點提幾個地方:

job type: service : 說明該job建立和排程的是一個service型別的工作負載;count = 2 : 類似於k8s的replicas欄位,期望在nomad叢集中執行2個httpbackend服務例項,nomad來保證始終處於期望狀態。關於port:port_map指定了task中容器的監聽埠。network中的port “http” {}沒有指定靜態IP,因此將採用動態主機埠。service中的port則指明使用”http”這個tag的動態主機埠。這和k8s中service中port使用名稱匹配的方式對映到具體pod中的port的方法類似。

我們使用nomad job子命令來建立該工作負載。正式建立之前,我們可以先透過nomad job plan來dry-run一下,一是看job檔案格式是否ok;二來檢查一下nomad叢集是否有空餘資源建立和排程新的工作負載:

# nomad job plan httpbackend-1.nomad+/- Job: "httpbackend"+/- Stop: "true" => "false"    Task Group: "httpbackend" (2 create)      Task: "httpbackend"Scheduler dry-run:- All tasks successfully allocated.Job Modify Index: 4248To submit the job with version verification run:nomad job run -check-index 4248 httpbackend-1.nomadWhen running the job with the check-index flag, the job will only be run if theserver side version matches the job modify index returned. If the index haschanged, another user has modified the job and the plan's results arepotentially invalid.

如果plan的輸出結果沒有問題,則可以用nomad job run正式建立和排程job:

# nomad job run httpbackend-1.nomad==> Monitoring evaluation "40c63529"    Evaluation triggered by job "httpbackend"    Allocation "6b0b83de" created: node "9e3ef19f", group "httpbackend"    Allocation "d0710b85" created: node "7acdd7bc", group "httpbackend"    Evaluation status changed: "pending" -> "complete"==> Evaluation "40c63529" finished with status "complete"

接下來,我們可以使用nomad job status命令檢視job的建立情況以及某個job的詳細狀態資訊:

# nomad job statusID                  Type     Priority  Status   Submit Datehttpbackend         service  50        running  2019-03-30T04:58:09+08:00# nomad job status httpbackendID            = httpbackendName          = httpbackendSubmit Date   = 2019-03-30T04:58:09+08:00Type          = servicePriority      = 50Datacenters   = dc1Status        = runningPeriodic      = falseParameterized = falseSummaryTask Group   Queued  Starting  Running  Failed  Complete  Losthttpbackend  0       0         2        0       0         0AllocationsID        Node ID   Task Group   Version  Desired  Status    Created    Modified6b0b83de  9e3ef19f  httpbackend  11       run      running   8m ago     7m50s agod0710b85  7acdd7bc  httpbackend  11       run      running   8m ago     7m39s ago

前面說過,nomad只是叢集管理和負載排程,服務發現它是不管的,並且服務發現的問題早已經被consul解決掉了。所以httpbackend建立後,要想使用該服務,我們還得走consul提供的路線:

DNS方式(前面已經做過鋪墊了):

# dig SRV httpbackend.service.dc1.consul; <<>> DiG 9.10.3-P4-Ubuntu <<>> SRV httpbackend.service.dc1.consul;; global options: +cmd;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7742;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 5;; WARNING: recursion requested but not available;; OPT PSEUDOSECTION:; EDNS: version: 0, flags:; udp: 4096;; QUESTION SECTION:;httpbackend.service.dc1.consul.    IN    SRV;; ANSWER SECTION:httpbackend.service.dc1.consul.    0 IN    SRV    1 1 23578 consul-1.node.dc1.consul.httpbackend.service.dc1.consul.    0 IN    SRV    1 1 22819 consul-2.node.dc1.consul.;; ADDITIONAL SECTION:consul-1.node.dc1.consul. 0    IN    A    172.16.66.102consul-1.node.dc1.consul. 0    IN    TXT    "consul-network-segment="consul-2.node.dc1.consul. 0    IN    A    172.16.66.103consul-2.node.dc1.consul. 0    IN    TXT    "consul-network-segment=";; Query time: 471 msec;; SERVER: 172.16.66.102#53(172.16.66.102);; WHEN: Sat Mar 30 05:07:54 CST 2019;; MSG SIZE  rcvd: 251# curl http://172.16.66.102:23578this is httpbackendservice, version: v1.0.0# curl http://172.16.66.103:22819this is httpbackendservice, version: v1.0.0

或http api方式(可透過官方API查詢服務):

# curl http://127.0.0.1:8500/v1/health/service/httpbackend[    {        "Node": {"ID":"160a7a20-f177-d2f5-0765-e6d1a9a1a9a4","Node":"consul-1","Address":"172.16.66.102","Datacenter":"dc1","TaggedAddresses":{"lan":"172.16.66.102","wan":"172.16.66.102"},"Meta":{"consul-network-segment":""},"CreateIndex":7,"ModifyIndex":10},        "Service": {"ID":"_nomad-task-5uxc3b7hjzivbklslt4yj5bpsfagibrb","Service":"httpbackend","Tags":[],"Address":"172.16.66.102","Meta":null,"Port":23578,"Weights":{"Passing":1,"Warning":1},"EnableTagOverride":false,"ProxyDestination":"","Proxy":{},"Connect":{},"CreateIndex":30727,"ModifyIndex":30727},        "Checks": [{"Node":"consul-1","CheckID":"serfHealth","Name":"Serf Health Status","Status":"passing","Notes":"","Output":"Agent alive and reachable","ServiceID":"","ServiceName":"","ServiceTags":[],"Definition":{},"CreateIndex":7,"ModifyIndex":7}]    },    {        "Node": {"ID":"6795cd2c-fad5-9d4f-2531-13b0a65e0893","Node":"consul-2","Address":"172.16.66.103","Datacenter":"dc1","TaggedAddresses":{"lan":"172.16.66.103","wan":"172.16.66.103"},"Meta":{"consul-network-segment":""},"CreateIndex":5,"ModifyIndex":5},        "Service": {"ID":"_nomad-task-hvqnbklzqr6q5mpspqcqbnhxdil4su4d","Service":"httpbackend","Tags":[],"Address":"172.16.66.103","Meta":null,"Port":22819,"Weights":{"Passing":1,"Warning":1},"EnableTagOverride":false,"ProxyDestination":"","Proxy":{},"Connect":{},"CreateIndex":30725,"ModifyIndex":30725},        "Checks": [{"Node":"consul-2","CheckID":"serfHealth","Name":"Serf Health Status","Status":"passing","Notes":"","Output":"Agent alive and reachable","ServiceID":"","ServiceName":"","ServiceTags":[],"Definition":{},"CreateIndex":8,"ModifyIndex":8}]    }]
三. 將服務暴露到外部以及負載均衡

叢集內部的東西向流量可以透過consul的服務發現來實現,南北向流量則需要我們將部分服務暴露到外部才能實現流量匯入。在《基於consul實現微服務的服務發現和負載均衡》一文中,我們是透過nginx實現服務暴露和負載均衡的,但是需要consul-template的協助,並且自己需要實現一個nginx的配置模板,門檻較高也比較複雜。

nomad的官方文件推薦了fabio這個反向代理和負載均衡工具。fabio最初由位於荷蘭的“eBay Classifieds Group”開發,它為荷蘭(marktplaats.nl),澳大利亞(gumtree.com.au)和義大利(www.kijiji.it)的一些最大網站提供支援。自2015年9月以來,它為這些站點提供23000個請求/秒的處理能力(效能應對一般中等流量是沒有太大問題的),沒有發現重大問題。

與consul-template+nginx的組合不同,fabio無需開發人員做任何二次開發,也不需要自定義模板,它直接從consul讀取service list並生成相關路由。至於哪些服務要暴露在外部,路由形式是怎樣的,是需要在服務啟動時為服務設定特定的tag,fabio定義了一套靈活的路由匹配描述方法。

下面我們就來部署fabio,並將上面的httpbackend暴露到外部。

1. 部署fabio

fabio也是nomad叢集的一個工作負載,因此我們可以像普通job那樣部署fabio。我們先來使用nomad官方文件中給出fabio.nomad:

//fabio.nomadjob "fabio" {  datacenters = ["dc1"]  type = "system"  group "fabio" {    task "fabio" {      driver = "docker"      config {        image = "fabiolb/fabio"        network_mode = "host"        logging {          type = "json-file"        }      }      resources {        cpu    = 200        memory = 128        network {          mbits = 20          port "lb" {            static = 9999          }          port "ui" {            static = 9998          }        }      }    }  }}

這裡有幾點值得注意:

fabio job的型別是”system”,也就是說該job會被部署到job可以匹配到(透過設定的約束條件)的所有nomad client上,且每個client上僅部署一個例項,有些類似於k8s的daemonset控制下的pod;network_mode = “host” 告訴fabio的驅動docker:fabio容器使用host網路,即與主機同網路namespace;static = 9999和static = 9998,說明fabio在每個nomad client上監聽固定的靜態埠而不是使用動態埠。這也要求了每個nomad client上不允許存在與fabio埠衝突的應用啟動。

我們來plan和run一下這個fabio job:

# nomad job plan fabio.nomad+ Job: "fabio"+ Task Group: "fabio" (3 create)  + Task: "fabio" (forces create)Scheduler dry-run:- All tasks successfully allocated.Job Modify Index: 0To submit the job with version verification run:nomad job run -check-index 0 fabio.nomadWhen running the job with the check-index flag, the job will only be run if theserver side version matches the job modify index returned. If the index haschanged, another user has modified the job and the plan's results arepotentially invalid.# nomad job run fabio.nomad==> Monitoring evaluation "97bfc16d"    Evaluation triggered by job "fabio"    Allocation "1b77dcfa" created: node "c281658a", group "fabio"    Allocation "da35a778" created: node "7acdd7bc", group "fabio"    Allocation "fc915ab7" created: node "9e3ef19f", group "fabio"    Evaluation status changed: "pending" -> "complete"==> Evaluation "97bfc16d" finished with status "complete"

檢視一下fabio job的執行狀態:

# nomad job status fabioID            = fabioName          = fabioSubmit Date   = 2019-03-27T14:30:29+08:00Type          = systemPriority      = 50Datacenters   = dc1Status        = runningPeriodic      = falseParameterized = falseSummaryTask Group  Queued  Starting  Running  Failed  Complete  Lostfabio       0       0         3        0       0         0AllocationsID        Node ID   Task Group  Version  Desired  Status   Created    Modified1b77dcfa  c281658a  fabio       0        run      running  1m11s ago  58s agoda35a778  7acdd7bc  fabio       0        run      running  1m11s ago  54s agofc915ab7  9e3ef19f  fabio       0        run      running  1m11s ago  58s ago

透過9998埠,可以檢視fabio的ui頁面,這個頁面主要展示的是fabio生成的路由資訊:

由於尚未暴露任何服務,因此fabio的路由表為空。

fabio的流量入口為9999埠,不過由於沒有配置路由和upstream service,因此如果此時向9999埠傳送http請求,將會得到404的應答。

2. 暴露HTTP服務到外部

接下來,我們就將上面建立的httpbackend服務透過fabiolb暴露到外部,使得特定條件下透過fabiolb進入叢集內部的流量可以被準確路由到叢集中的httpbackend例項上面。

下面是fabio將nomad叢集內部服務暴露在外部的原理圖:

我們看到原理圖中最為關鍵的一點就是service tag,該資訊由nomad在建立job時寫入到consul叢集;fabio監聽consul叢集service資訊變更,讀取有新變動的job,解析job的service tag,生成路由規則。fabio關注所有帶有”urlprefix-”字首的service tag。

fabio啟動時監聽的9999埠,預設是http接入。我們修改一下之前的httpbackend.nomad,為該job中的service增加tag欄位:

// httpbackend.nomad... ...     service {        name = "httpbackend"        tags = ["urlprefix-mysite.com:9999/"]        port = "http"        check {          name     = "alive"          type     = "http"          path     = "/"          interval = "10s"          timeout  = "2s"        }      }

對於上面httpbackend.nomad中service塊的變更,主要有兩點:

1) 增加tag:匹配的路由資訊為:“mysite.com:9999/”

2) 增加check塊:如果沒有check設定,該路由資訊將不會在fabio中生效

更新一下httpbackend:

# nomad job run httpbackend-2.nomad==> Monitoring evaluation "c83af3d3"    Evaluation triggered by job "httpbackend"    Allocation "6b0b83de" modified: node "9e3ef19f", group "httpbackend"    Allocation "d0710b85" modified: node "7acdd7bc", group "httpbackend"    Evaluation status changed: "pending" -> "complete"==> Evaluation "c83af3d3" finished with status "complete"

檢視fabio的route表,可以看到增加了兩條新路由資訊:

我們透過fabio來訪問一下httpbackend服務:

# curl http://mysite.com:9999/      --- 注意:事先已經在/etc/hosts中添加了 mysite.com的地址為127.0.0.1this is httpbackendservice, version: v1.0.0

我們看到httpbackend service已經被成功暴露到lb的外部了。

四. 暴露HTTPS、TCP服務到外部1. 定製fabio

我們的目標是將https、tcp服務暴露到lb的外部,nomad官方文件中給出的fabio.nomad將不再適用,我們需要讓fabio監聽多個埠,每個埠有著不同的用途。同時,我們透過給fabio傳入適當的命令列引數來幫助我們檢視fabio的詳細access日誌資訊,並讓fabio支援TRACE機制。

fabio.nomad調整如下:

job "fabio" {  datacenters = ["dc1"]  type = "system"  group "fabio" {    task "fabio" {      driver = "docker"      config {        image = "fabiolb/fabio"        network_mode = "host"        logging {          type = "json-file"        }        args = [          "-proxy.addr=:9999;proto=http,:9997;proto=tcp,:9996;proto=tcp+sni",          "-log.level=TRACE",          "-log.access.target=stdout"        ]      }      resources {        cpu    = 200        memory = 128        network {          mbits = 20        }      }    }  }}

我們讓fabio監聽三個埠:

9999: http埠9997: tcp埠9996: tcp+sni埠

後續會針對這三個埠暴露的不同服務做細緻說明。

我們將fabio的日誌級別調低為TRACE級別,以便能檢視到fabio日誌中輸出的trace資訊,幫助我們進行路由匹配的診斷。

重新nomad job run fabio.nomad後,我們來看看TRACE的效果:

//訪問後端服務,在http header中新增"Trace: abc":# curl -H 'Trace: abc' 'http://mysite.com:9999/'this is httpbackendservice, version: v1.0.0//檢視fabio的訪問日誌:2019/03/30 08:13:15 [TRACE] abc Tracing mysite.com:9999/2019/03/30 08:13:15 [TRACE] abc Matching hosts: [mysite.com:9999]2019/03/30 08:13:15 [TRACE] abc Match mysite.com:9999/2019/03/30 08:13:15 [TRACE] abc Routing to service httpbackend on http://172.16.66.102:23578/127.0.0.1 - - [30/Mar/2019:08:13:15 +0000] "GET / HTTP/1.1" 200 44

我們可以清晰的看到fabio收到請求後,匹配到一條路由:”mysite.com:9999/”,然後將http請求轉發到 172.16.66.102:23578這個httpbackend服務例項上去了。

2. https服務

接下來,我們考慮將一個https服務暴露在lb外部。

一種方案是fabiolb做ssl termination,然後再在與upstream https服務建立的ssl連線上傳遞資料。這種兩段式https通訊是比較消耗資源的,fabio要對資料進行兩次加解密。

另外一種方案是fabiolb將收到的請求透傳給後面的upsteam https服務,由client與upsteam https服務直接建立“安全資料通道”,這個方案我們在後續會提到。

第三種方案,那就是對外依舊暴露http,但是fabiolb與upsteam之間透過https通訊。我們先來看一下這種“間接暴露https”的方案。

// httpsbackend-upstreamhttps.nomadjob "httpsbackend" {  datacenters = ["dc1"]  type = "service"  group "httpsbackend" {    count = 2    restart {      attempts = 2      interval = "30m"      delay = "15s"      mode = "fail"    }    task "httpsbackend" {      driver = "docker"      config {        image = "bigwhite/httpsbackendservice:v1.0.0"        port_map {          https = 7777        }        logging {          type = "json-file"        }      }      resources {        network {          mbits = 10          port "https" {}        }      }      service {        name = "httpsbackend"        tags = ["urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true"]        port = "https"        check {          name     = "alive"          type     = "tcp"          path     = "/"          interval = "10s"          timeout  = "2s"        }      }    }  }}

我們將建立名為httpsbackend的job,job中Task對應的tag為:”urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true”。解釋為:路由mysite-https.com:9999/,上游upstream服務為https服務,fabio不驗證upstream服務的公鑰數字證書。

我們建立該job:

# nomad job run httpsbackend-upstreamhttps.nomad==> Monitoring evaluation "ba7af6d4"    Evaluation triggered by job "httpsbackend"    Allocation "3127aac8" created: node "7acdd7bc", group "httpsbackend"    Allocation "b5f1b7a7" created: node "9e3ef19f", group "httpsbackend"    Evaluation status changed: "pending" -> "complete"==> Evaluation "ba7af6d4" finished with status "complete"

我們來透過fabiolb訪問一下httpsbackend這個服務:

# curl -H "Trace: abc"  http://mysite-https.com:9999/this is httpsbackendservice, version: v1.0.0// fabiolb 日誌2019/03/30 09:35:48 [TRACE] abc Tracing mysite-https.com:9999/2019/03/30 09:35:48 [TRACE] abc Matching hosts: [mysite-https.com:9999]2019/03/30 09:35:48 [TRACE] abc Match mysite-https.com:9999/2019/03/30 09:35:48 [TRACE] abc Routing to service httpsbackend on https://172.16.66.103:29248127.0.0.1 - - [30/Mar/2019:09:35:48 +0000] "GET / HTTP/1.1" 200 45
3. 基於tcp代理暴露https服務

上面的方案雖然將https暴露在外面,但是client到fabio這個環節的資料傳輸不是在安全通道中。上面提到的方案2:fabiolb將收到的請求透傳給後面的upsteam https服務,由client與upsteam https服務直接建立“安全資料通道”似乎更佳。fabiolb支援tcp埠的反向代理,我們基於tcp代理來暴露https服務到外部。

我們建立httpsbackend-tcp.nomad檔案,考慮篇幅有限,我們僅列出差異化的部分:

job "httpsbackend-tcp" { ... ...    service {        name = "httpsbackend-tcp"        tags = ["urlprefix-:9997 proto=tcp"]        port = "https"        check {          name     = "alive"          type     = "tcp"          path     = "/"          interval = "10s"          timeout  = "2s"        }      }... ...}

從httpsbackend-tcp.nomad檔案,我們看到我們在9997這個tcp埠上暴露服務,tag為:“urlprefix-:9997 proto=tcp”,即凡是到達9997埠的流量,無論應用協議型別是什麼,都轉發到httpsbackend-tcp上,且透過tcp協議轉發。

我們建立並測試一下該方案:

# nomad job run httpsbackend-tcp.nomad# curl -k https://localhost:9997   //由於使用的是自簽名證書,所有告訴curl不校驗server端公鑰數字證書this is httpsbackendservice, version: v1.0.0
4. 多個https服務共享一個fabio埠

上面的基於tcp代理暴露https服務的方案還有一個問題,那就是每個https服務都要獨佔一個fabio listen的埠。那是否可以實現多個https服務使用一個fabio埠,並透過host name route呢?fabio支援tcp+sni的route策略。

SNI, 全稱Server Name Indication,即伺服器名稱指示。它是一個擴充套件的TLS計算機聯網協議。該協議允許在握手過程開始時透過客戶端告訴它正在連線的伺服器的主機名稱。這允許伺服器在相同的IP地址和TCP埠號上呈現多個證書,也就是允許在相同的IP地址上提供多個安全HTTPS網站(或其他任何基於TLS的服務),而不需要所有這些站點使用相同的證書。

接下來,我們就來看一下如何在fabio中讓多個後端https服務共享一個Fabio服務埠(9996)。我們建立兩個job:httpsbackend-sni-1和httpsbackend-sni-2。

//httpsbackend-tcp-sni-1.nomadjob "httpsbackend-sni-1" {... ...    service {        name = "httpsbackend-sni-1"        tags = ["urlprefix-mysite-sni-1.com/ proto=tcp+sni"]        port = "https"        check {          name     = "alive"          type     = "tcp"          path     = "/"          interval = "10s"          timeout  = "2s"        }      }.... ...}//httpsbackend-tcp-sni-2.nomadjob "httpsbackend-sni-2" {... ...   task "httpsbackend-sni-2" {      driver = "docker"      config {        image = "bigwhite/httpsbackendservice:v1.0.1"        port_map {          https = 7777        }        logging {          type = "json-file"        }    }    service {        name = "httpsbackend-sni-2"        tags = ["urlprefix-mysite-sni-2.com/ proto=tcp+sni"]        port = "https"        check {          name     = "alive"          type     = "tcp"          path     = "/"          interval = "10s"          timeout  = "2s"        }      }.... ...}

我們看到與之前的server tag不同的是:這裡proto=tcp+sni,即告訴fabio建立sni路由。httpsbackend-sni-2 task與httpsbackend-sni-1不同之處在於其使用image為bigwhite/httpsbackendservice:v1.0.1,為的是能透過https的應答結果,將這兩個服務區分開來。

除此之外,我們還看到tag中並不包含埠號了,而是直接採用host name作為路由匹配標識。

建立這兩個job:

# nomad job run httpsbackend-tcp-sni-1.nomad==> Monitoring evaluation "af170d98"    Evaluation triggered by job "httpsbackend-sni-1"    Allocation "8ea1cc8d" modified: node "7acdd7bc", group "httpsbackend-sni-1"    Allocation "e16cdc73" modified: node "9e3ef19f", group "httpsbackend-sni-1"    Evaluation status changed: "pending" -> "complete"==> Evaluation "af170d98" finished with status "complete"# nomad job run httpsbackend-tcp-sni-2.nomad==> Monitoring evaluation "a77d3799"    Evaluation triggered by job "httpsbackend-sni-2"    Allocation "32df450c" modified: node "c281658a", group "httpsbackend-sni-2"    Allocation "e1bf4871" modified: node "7acdd7bc", group "httpsbackend-sni-2"    Evaluation status changed: "pending" -> "complete"==> Evaluation "a77d3799" finished with status "complete"

我們來分別訪問這兩個服務:

# curl -k https://mysite-sni-1.com:9996/this is httpsbackendservice, version: v1.0.0# curl -k https://mysite-sni-2.com:9996/this is httpsbackendservice, version: v1.0.1

從返回的結果我們看到,透過9996,我們成功暴露出兩個不同的https服務。

五. 小結

到這裡,我們實現了我們的既定目標:

使用nomad實現了工作負載的建立和排程;東西向流量透過consul機制實現;透過fabio實現了http、https(through tcp)、多https(though tcp+sni)的服務暴露和負載均衡。

後續我們將進一步探索基於nomad實現負載的多種場景的升降級操作(滾動、金絲雀、藍綠部署)、對非host網路的支援(比如weave network)等。

本文涉及到的原始碼檔案在這裡可以下載。

六. 參考資料使用Nomad構建彈性基礎設施:nomad排程使用Nomad構建彈性基礎設施:重啟任務使用Nomad構建彈性基礎設施: job生命週期使用Nomad構建彈性基礎設施:容錯和自我修復fabio參考指南

15
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • C++基礎知識篇:日期 & 時間