Skip to content

Consul

consul 是一個能夠讓團隊在服務與跨預置和多雲環境中安全管理網絡連接的解決方案,它提供了服務發現,服務網格,流量治理,網絡基礎設施自動更新等一系列功能。

官方文檔:Consul by HashiCorp

開源地址:hashicorp/consul

Consul 是 HashiCorp 公司開源的一款服務發現與注冊工具,采用 Raft 選舉算法,工具本身使用 Go 語言進行開發,因此部署起來十分的輕便。Consul 總共有以下特點:

  • 服務發現
  • 服務注冊
  • 健康檢查
  • 鍵值存儲
  • 多數據中心

實際上 consul 能做的事情不止服務發現,還可以做分布式配置中心,同類型的開源工具也有很多,比如 zookeeper,nacos,這裡就不再做過多的介紹。

安裝

對於 Ubuntu 而言的話,執行下面的命令使用 apt 安裝即可

sh
$ wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
$ sudo apt update && sudo apt install consul

或者也可以在官網下載Install Consul 對應的安裝包,由於 consul 是由 go 開發的,安裝包本身也就只有一個二進制可執行文件,安裝起來也相當的方便,安裝成功後,執行如下命令查看版本。

sh
$ consul version

正常輸出就沒有問題

Consul v1.16.1
Revision e0ab4d29
Build Date 2023-08-05T21:56:29Z
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

快速開始

下面介紹如何快速搭建一個 consul 單節點,一般單節點是在開發期間測試用的,如果單節點使用起來沒有問題,大概率多節點集群也不會由問題。單節點搭建起來十分的簡單,只需要一行命令即可

sh
$ consul agent -dev -bind=192.168.48.141 -data-dir=/tmp/consul -ui -node=dev01

一般會有如下輸出

==> Starting Consul agent...
               Version: '1.16.1'
            Build Date: '2023-08-05 21:56:29 +0000 UTC'
               Node ID: 'be6f6b8d-9668-f7ff-8709-ed57c72ffdec'
             Node name: 'dev01'
            Datacenter: 'dc1' (Segment: '<all>')
                Server: true (Bootstrap: false)
           Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, gRPC-TLS: 8503, DNS: 8600)
          Cluster Addr: 192.168.48.141 (LAN: 8301, WAN: 8302)
     Gossip Encryption: false
      Auto-Encrypt-TLS: false
           ACL Enabled: false
     Reporting Enabled: false
    ACL Default Policy: allow
             HTTPS TLS: Verify Incoming: false, Verify Outgoing: false, Min Version: TLSv1_2
              gRPC TLS: Verify Incoming: false, Min Version: TLSv1_2
      Internal RPC TLS: Verify Incoming: false, Verify Outgoing: false (Verify Hostname: false), Min Version: TLSv1_2
==> Log data will now stream in as it occurs:
2023-08-25T17:23:33.763+0800 [DEBUG] agent.grpc.balancer: switching server: target=consul://dc1.be6f6b8d-9668-f7ff-8709-ed57c72ffdec/server.dc1 from=<none> to=<none>
2023-08-25T17:23:33.767+0800 [INFO]  agent.server.raft: initial configuration: index=1 servers="[{Suffrage:Voter ID:be6f6b8d-9668-f7ff-8709-ed57c72ffdec Address:192.168.48.141:8300}]"

簡單講解一下釋義

  • agent是子命令,是 consul 的核心命令,consul agent就是運行一個新的 consul 代理,每一個 node 都是一個代理。

  • dev,是 agent 的運行模式,總共有三種devclientserver

  • bind,局域網通信地址,端口默認 8301,一般此值為服務器的內網地址

  • advertise,廣域網通信地址,端口默認 8302,一般此值為服務器的外網地址

  • data-dir,數據存放目錄

  • config-dir,配置存放目錄,consul 會讀取目錄下所有的 json 文件

  • bootstrap,標注當前 server 進入引導模式,在 raft 選舉時會給自己投票,集群中處於該模式的 server 不能超過一個

  • bootstrap-expect,即集群中期望的 server 數量,在沒有達到指定數量之前,集群不會開始選舉投票,不能與bootstrap同時使用。

  • retry-join,agent 啟動後,會不斷嘗試加入指定的節點,還支持以下的一些服務商發現方法

    aliyun aws azure digitalocean gce hcp k8s linode mdns os packet scaleway softlayer tencentcloud triton vsphere
  • ui,運行 Web 後台

  • node,執行節點名稱,必須在集群中保持唯一。

TIP

關於更多的 agent 參數釋義,前往Agents - CLI Reference | Consul | HashiCorp Developer ,需要注意的是有些參數只有企業版能用。

當成功運行後,訪問127.0.0.1:8500,就可以瀏覽 Web 界面。

dev01 的圖標是一個星星,就說明它是 leader 節點。

退出時,為了能其他節點感知到當前節點的退出,不建議強制殺死進程,可以使用命令

sh
consul leave

或者

sh
consul force-leave

也可以ctrl+c,讓 consul agent 優雅退出。

概念

這是一張 consul 集群的示意圖,圖中分為了兩部分,控制面和數據面。consul 只負責控制面,分為服務集群和客戶端,服務集群中又分為追隨者和領導者,總體而言,圖中 consul 集群就構成了一個數據中心。下面對一些術語進行講解

  • Agent(代理):或者稱為節點更合適,每一個 agent 都是一個長時間運行的守護進程,它們對外暴露 HTTP 和 DNS 接口,負責健康檢查和服務同步。
  • Server(服務代理):作為一個 consul server,它的職責主要有參與 Raft 選舉,維護集群狀態,響應查詢,與其他數據中心交換數據,以及向領導者和其他數據中心轉發查詢。
  • Client(客戶代理):client 相對 server 來說是無狀態的,它不參與 Raft 選舉,所做的事情僅僅只是將所有的請求轉發給 server,它唯一參與的與後台有關的事情就是局域網流言轉發(LAN gossip pool)。
  • Leader(領導者):leader 是所有 server 的領導,而且領導只能有一個,leader 是通過 Raft 選舉算法進行選舉的,每一個 leader 有自己的任期,在任期內,其他的 server 收到了不管什麼請求都要告訴 leader,所以 leader 的數據是最及時最新的。
  • Gossi(流言):Consul 是基於 Serf(該公司其下的另一個產品)而構建的,它使用 gossip 協議,該協議專用於節點間的隨機通信,類似 UDP,consul 使用此協議來在服務集群間進行互相通知。
  • Data Center(數據中心):一個局域網內的 consul 集群被稱為一個數據中心,consul 支持多數據中心,多數據中心的溝通方式就是 WAN gossip。

TIP

更多詞匯和術語可以前往Glossary | Consul | HashiCorp Developer 進行了解。

在 consul 集群中,server 的數量應該嚴格控制,因為它們直接參與到 LAN gossip 和 WAN gossip,raft 選舉,並且要存儲數據,server 越多,通信成本越高。而 client 的數量多一點沒什麼問題,它只負責轉發,不參與選舉,佔用資源很低,在圖中的集群中,各個服務通過 client 將自身注冊到 server 中,如果有 server 掛了的話,client 會自行尋找其他可用的 server。

集群搭建示例

下面搭建一個簡單的 consul 多節點集群示例,先准備四台虛擬機

四台虛擬機中,三個 server,一個 client,官方建議 server 的數量最好是奇數,並且最好大於等於三個。這裡將 vm00-vm02 作為 server,vm03 作為 client,

對於 server 而言,運行如下命令,創建 server agent

sh
consul agent -server -bind=vm_address -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent_name -ui

對於 client 而言,運行如下命令,創建 client agent

sh
consul agent -client=0.0.0.0  -bind=vm_address -data-dir=/tmp/consul/ -node=agent_name -ui

執行的命令分別如下

sh
# vm00
consul agent -server -bind=192.168.48.138 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent01 -ui -bootstrap

# vm01
consul agent -server -bind=192.168.48.139 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent02 -ui -retry-join=192.168.48.138

# vm02
consul agent -server -bind=192.168.48.140 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent03 -ui -retry-join=192.168.48.138

# vm03
consul agent -bind=192.168.48.140 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent03 -ui -retry-join=192.168.48.138

一些參數釋義

  • client0.0.0.0即放行所有來源的請求,如果只有 client 參數,沒有 server 參數,代表著 agent 將以 client 模式運行。

所有的 agent 運行好後,其中retry-join的作用等於自動執行 join 命令,失敗後會不斷嘗試,默認重試時間 30s

sh
$ consul join 192.168.48.138

join 完成後,各個節點都知曉了對方的存在,由於 vm00 指定了 bootstrap 模式,所以它就是默認的 leader,如果沒有指定 bootstrap 模式,所有節點在 join 時指定的節點為默認 leader, 在 leader 沒有選舉出來之前,集群無法正常工作,訪問 web 界面會返回 500,一些命令也無法正常工作 。如果集群中有節點制定了 bootstrap 模式,那麼集群中其他節點就不應該再有其他節點指定 bootstrap 模式,同時其他節點也不應該再使用bootstrap-expect 參數,如果使用了會自動禁用。

這時在 leader 節點上(實際上這時無論哪個節點都可以查看)運行查看 data center 的成員信息,運行如下命令

sh
$ consul members
Node      Address              Status  Type    Build   Protocol  DC   Partition  Segment
agent01   192.168.48.138:8301  alive   server  1.16.1  2         dc1  default    <all>
agent02   192.168.48.139:8301  alive   server  1.16.1  2         dc1  default    <all>
agent03   192.168.48.140:8301  alive   server  1.16.1  2         dc1  default    <all>
client01  192.168.48.141:8301  alive   client  1.16.1  2         dc1  default    <default>
  • Node,即節點名稱
  • Address,通信地址
  • Status,alive表示存活,left表示下線
  • Type,agent 種類,server 和 client 兩種模式
  • Build,該節點使用的 consul 版本,consul 可以在一定范圍內兼容不同版本的節點進行工作
  • Protocol,指的是使用的 Raft 協議版本,這個協議應當所有節點一致
  • DC,Data Center,數據中心,輸出中的所有節點都屬於 dc1 數據中心
  • Partition,節點隸屬的分區,屬於企業版功能,每個節點只能與同一分區的節點進行通信
  • Segment,節點隸屬的網段,屬於企業版功能

同樣的,如果想要一個節點退出,應該使用consul leave讓節點優雅退出,並通知其他節點自己將要退出,對於多節點的情況下,節點的優雅退出尤為重要,因為這關系到數據的一致性。

TIP

虛擬機在演示的時候關閉了所有的防火牆,在實際生產環境中為了安全考慮應該開啟,為此應該關注下 consul 使用到的所有端口:Required Ports | Consul | HashiCorp Developer

接下來簡單測試一下數據一致性,在 vm00 虛擬機中添加如下數據

sh
$ consul kv put sys_confg {"name":"consul"}
Success! Data written to: sys_confg

保存後,通過 HTTP API 訪問其他節點會發現數據同樣存在(其中的 value 是 base64 編碼)

sh
$ curl http://192.168.48.138:8500/v1/kv/sys_confg
[{"LockIndex":0,"Key":"sys_confg","Flags":0,"Value":"ewogICJuYW1lIjoiY29uc3VsIgp9","CreateIndex":2518,"ModifyIndex":2518}]
$ curl http://192.168.48.139:8500/v1/kv/sys_confg
[{"LockIndex":0,"Key":"sys_confg","Flags":0,"Value":"ewogICJuYW1lIjoiY29uc3VsIgp9","CreateIndex":2518,"ModifyIndex":2518}]
$ curl http://192.168.48.140:8500/v1/kv/sys_confg
[{"LockIndex":0,"Key":"sys_confg","Flags":0,"Value":"ewogICJuYW1lIjoiY29uc3VsIgp9","CreateIndex":2518,"ModifyIndex":2518}]

事實上,consul 提供的服務發現與注冊功能,通過 gossip 協議廣播給其他節點,並且當任意一個節點加入當前數據中心時,所有的節點都會感知到此變化。

多數據中心搭建示例

准備五台虛擬機,vm00-vm02 是上一個示例的集群,屬於 dc1 數據中心,不去動它,vm03-vm04 屬於 dc2 數據中心,數據中心在 agent 啟動時,默認為 dc1。

TIP

這裡為了演示,只搭建 server,省掉了 client。

首先分別啟動 vm03,將其作為默認的 leader

sh
$ consul agent -server -datacenter=dc2 -bind=192.168.48.141 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent04 -ui -bootstrap

啟動啟動 vm04,讓其自動 join 到 vm03 節點

sh
$ consul agent -server -datacenter=dc2 -bind=192.168.48.142 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent05 -ui -retry-join=192.168.48.141

此時分別在 vm00 和 vm03 查看 members

sh
# vm00-vm02
$ consul members
Node      Address              Status  Type    Build   Protocol  DC   Partition  Segment
agent01   192.168.48.138:8301  alive   server  1.16.1  2         dc1  default    <all>
agent02   192.168.48.139:8301  alive   server  1.16.1  2         dc1  default    <all>
agent03   192.168.48.140:8301  alive   server  1.16.1  2         dc1  default    <all>

# vm03-vm04
$ consul members
Node     Address              Status  Type    Build   Protocol  DC   Partition  Segment
agent04  192.168.48.141:8301  alive   server  1.16.1  2         dc2  default    <all>
agent05  192.168.48.142:8301  alive   server  1.16.1  2         dc2  default    <all>

可以看到 DC 字段不同,因為這裡是虛擬機演示,所以都是在同一個網段中,現實中兩個數據中心可能是異地的服務器集群。接下來讓 dc1 的任意一個節點 join 到 dc2 的任意一個節點,這裡讓 vm01 join vm03

sh
$ consul join -wan 192.168.48.141
Successfully joined cluster by contacting 1 nodes.

join 成功後,執行命令查看廣域網 members

sh
$ consul members -wan
Node         Address              Status  Type    Build   Protocol  DC   Partition  Segment
agent01.dc1  192.168.48.138:8302  alive   server  1.16.1  2         dc1  default    <all>
agent02.dc1  192.168.48.139:8302  alive   server  1.16.1  2         dc1  default    <all>
agent03.dc1  192.168.48.140:8302  alive   server  1.16.1  2         dc1  default    <all>
agent04.dc2  192.168.48.141:8302  alive   server  1.16.1  2         dc2  default    <all>
agent05.dc2  192.168.48.142:8302  alive   server  1.16.1  2         dc2  default    <all>

$ consul catalog datacenters
dc2
dc1

只要 dc1 的隨便一個節點 join 到 dc2 的任意一個節點,兩個數據中心的所有節點都會感知到此變化,查看 members 的時候也可以看到兩個數據中心的節點。

接下來嘗試在 vm00 節點添加一個 KV 數據

sh
$ consul kv put name consul
Success! Data written to: name

在 vm01 節點嘗試讀取數據,可以看到同一數據中心的數據是同步的

sh
$ consul kv get name
consul

然後再去不同數據中心的 vm03 嘗試讀取數據,會發現不同數據中心的數據是不同步的。

sh
$ consul kv get name
Error! No key exists at: name

如果想要多數據中心數據同步的話,可以了解hashicorp/consul-replicate: Consul cross-DC KV replication daemon

服務注冊與發現

consul 服務注冊的方式有兩種,配置文件注冊和 API 注冊。為了方便進行測試,這裡事先准備一個 Hello World 服務(gRPC 文章中的示例),部署兩份,分別在不同的位置。配置文件注冊的方式可以前往Register external services with Consul service discovery | Consul | HashiCorp Developer 了解,這裡只介紹通過 HTTP API 進行注冊。

TIP

對於本地服務(和 consul client 在一塊)而言,可以直接使用 agent service 注冊,否則的話應該使用 catalog register 來進行注冊。

consul 提供了 HTTP API 的 SDK,其他語言的 SDK 前往Libraries and SDKs - HTTP API | Consul | HashiCorp Developer 了解。這裡下載 go 的依賴

sh
go get github.com/hashicorp/consul/api

在服務啟動時向 consul 主動注冊服務,在服務關閉時,向 consul 注銷服務,下面是一個示例。

go
package main

import (
  consulapi "github.com/hashicorp/consul/api"
  "google.golang.org/grpc"
  "google.golang.org/grpc/credentials/insecure"
  pb "grpc_learn/helloworld/hello"
  "log"
  "net"
)

var (
  server01 = &consulapi.AgentService{
        // 必須保持唯一
    ID:      "hello-service1",
    Service: "hello-service",
        // 部署兩份,一份的端口是8080,一份的端口是8081
    Port:    8080,
  }
)

// 注冊服務
func Register() {
  client, _ := consulapi.NewClient(&consulapi.Config{Address: "192.168.48.138:8500"})
  _, _ = client.Catalog().Register(&consulapi.CatalogRegistration{
    Node:    "hello-server",
    Address: "192.168.2.10",
    Service: server01,
  }, nil)
}

// 注銷服務
func DeRegister() {
  client, _ := consulapi.NewClient(&consulapi.Config{Address: "192.168.48.138:8500"})
  _, _ = client.Catalog().Deregister(&consulapi.CatalogDeregistration{
    Node:      "hello-server",
    Address:   "192.168.2.10",
    ServiceID: server01.ID,
  }, nil)
}

func main() {
  Register()
  defer DeRegister()

  // 監聽端口
  listen, err := net.Listen("tcp", ":8080")
  if err != nil {
    panic(err)
  }
  // 創建gprc服務器
  server := grpc.NewServer(
    grpc.Creds(insecure.NewCredentials()),
  )
  // 注冊服務
  pb.RegisterSayHelloServer(server, &HelloRpc{})
  log.Println("server running...")
  // 運行
  err = server.Serve(listen)
  if err != nil {
    panic(err)
  }
}

客戶端代碼使用 consul 自定義解析器,來向注冊中心查詢對應的服務,解析成真實地址。

go
package myresolver

import (
    "fmt"
    consulapi "github.com/hashicorp/consul/api"
    "google.golang.org/grpc/resolver"
)

func NewConsulResolverBuilder(address string) ConsulResolverBuilder {
    return ConsulResolverBuilder{consulAddress: address}
}

type ConsulResolverBuilder struct {
    consulAddress string
}

func (c ConsulResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
    consulResolver, err := newConsulResolver(c.consulAddress, target, cc)
    if err != nil {
       return nil, err
    }
    consulResolver.resolve()
    return consulResolver, nil
}

func (c ConsulResolverBuilder) Scheme() string {
    return "consul"
}

func newConsulResolver(address string, target resolver.Target, cc resolver.ClientConn) (ConsulResolver, error) {
    var reso ConsulResolver
    client, err := consulapi.NewClient(&consulapi.Config{Address: address})
    if err != nil {
       return reso, err
    }
    return ConsulResolver{
       target: target,
       cc:     cc,
       client: client,
    }, nil
}

type ConsulResolver struct {
    target resolver.Target
    cc     resolver.ClientConn
    client *consulapi.Client
}

func (c ConsulResolver) resolve() {
    service := c.target.URL.Opaque
    services, _, err := c.client.Catalog().Service(service, "", nil)
    if err != nil {
       c.cc.ReportError(err)
       return
    }
    var adds []resolver.Address
    for _, catalogService := range services {
       adds = append(adds, resolver.Address{Addr: fmt.Sprintf(fmt.Sprintf("%s:%d", catalogService.Address, catalogService.ServicePort))})
    }

    c.cc.UpdateState(resolver.State{
       Addresses: adds,
       // 輪詢策略
       ServiceConfig: c.cc.ParseServiceConfig(
          `{"loadBalancingPolicy":"round_robin"}`),
    })
}

func (c ConsulResolver) ResolveNow(options resolver.ResolveNowOptions) {
    c.resolve()
}

func (c ConsulResolver) Close() {

}

客戶端在啟動時注冊解析器

go
package main

import (
    "context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/resolver"
    "grpc_learn/helloworld/client/myresolver"
    hello2 "grpc_learn/helloworld/hello"
    "log"
    "time"
)

func init() {
    // 注冊builder
    resolver.Register(
       // 注冊自定義的consul解析器
       myresolver.NewConsulResolverBuilder("192.168.48.138:8500"),
    )
}

func main() {

    // 建立連接,沒有加密驗證
    conn, err := grpc.Dial("consul:hello-service",
       grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
       panic(err)
    }
    defer conn.Close()
    // 創建客戶端
    client := hello2.NewSayHelloClient(conn)
    for range time.Tick(time.Second) {
       // 遠程調用
       helloRep, err := client.Hello(context.Background(), &hello2.HelloReq{Name: "client"})
       if err != nil {
          panic(err)
       }
       log.Printf("received grpc resp: %+v", helloRep.String())
    }

}

先啟動服務端,再啟動客戶端,服務端是有兩個的,提供同一個服務,只是地址不一樣,客戶都的負載均衡策略是輪詢,從服務端的日志間隔時間就能看出來策略生效了。

2023/08/29 17:39:54 server running...
2023/08/29 21:03:46 received grpc req: name:"client"
2023/08/29 21:03:48 received grpc req: name:"client"
2023/08/29 21:03:50 received grpc req: name:"client"
2023/08/29 21:03:52 received grpc req: name:"client"
2023/08/29 21:03:54 received grpc req: name:"client"
2023/08/29 21:03:56 received grpc req: name:"client"
2023/08/29 21:03:58 received grpc req: name:"client"
2023/08/29 21:04:00 received grpc req: name:"client"

以上就是一個簡單的使用 consul 結合 gRPC 實現服務注冊與發現的簡單案例。

Golang學習網由www.golangdev.cn整理維護