Consul

consul 是一個能夠讓團隊在服務與跨預置和多雲環境中安全管理網絡連接的解決方案,它提供了服務發現,服務網格,流量治理,網絡基礎設施自動更新等一系列功能。
官方文檔:Consul by HashiCorp
開源地址:hashicorp/consul
Consul 是 HashiCorp 公司開源的一款服務發現與注冊工具,采用 Raft 選舉算法,工具本身使用 Go 語言進行開發,因此部署起來十分的輕便。Consul 總共有以下特點:
- 服務發現
- 服務注冊
- 健康檢查
- 鍵值存儲
- 多數據中心
實際上 consul 能做的事情不止服務發現,還可以做分布式配置中心,同類型的開源工具也有很多,比如 zookeeper,nacos,這裡就不再做過多的介紹。
安裝
對於 Ubuntu 而言的話,執行下面的命令使用 apt 安裝即可
$ 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 開發的,安裝包本身也就只有一個二進制可執行文件,安裝起來也相當的方便,安裝成功後,執行如下命令查看版本。
$ 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 單節點,一般單節點是在開發期間測試用的,如果單節點使用起來沒有問題,大概率多節點集群也不會由問題。單節點搭建起來十分的簡單,只需要一行命令即可
$ 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 的運行模式,總共有三種dev,client,serverbind,局域網通信地址,端口默認 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 vsphereui,運行 Web 後台node,執行節點名稱,必須在集群中保持唯一。
TIP
關於更多的 agent 參數釋義,前往Agents - CLI Reference | Consul | HashiCorp Developer ,需要注意的是有些參數只有企業版能用。
當成功運行後,訪問127.0.0.1:8500,就可以瀏覽 Web 界面。

dev01 的圖標是一個星星,就說明它是 leader 節點。
退出時,為了能其他節點感知到當前節點的退出,不建議強制殺死進程,可以使用命令
consul leave或者
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
consul agent -server -bind=vm_address -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent_name -ui對於 client 而言,運行如下命令,創建 client agent
consul agent -client=0.0.0.0 -bind=vm_address -data-dir=/tmp/consul/ -node=agent_name -ui執行的命令分別如下
# 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一些參數釋義
client,0.0.0.0即放行所有來源的請求,如果只有 client 參數,沒有 server 參數,代表著 agent 將以 client 模式運行。
所有的 agent 運行好後,其中retry-join的作用等於自動執行 join 命令,失敗後會不斷嘗試,默認重試時間 30s
$ consul join 192.168.48.138join 完成後,各個節點都知曉了對方的存在,由於 vm00 指定了 bootstrap 模式,所以它就是默認的 leader,如果沒有指定 bootstrap 模式,所有節點在 join 時指定的節點為默認 leader, 在 leader 沒有選舉出來之前,集群無法正常工作,訪問 web 界面會返回 500,一些命令也無法正常工作 。如果集群中有節點制定了 bootstrap 模式,那麼集群中其他節點就不應該再有其他節點指定 bootstrap 模式,同時其他節點也不應該再使用bootstrap-expect 參數,如果使用了會自動禁用。
這時在 leader 節點上(實際上這時無論哪個節點都可以查看)運行查看 data center 的成員信息,運行如下命令
$ 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 虛擬機中添加如下數據
$ consul kv put sys_confg {"name":"consul"}
Success! Data written to: sys_confg保存後,通過 HTTP API 訪問其他節點會發現數據同樣存在(其中的 value 是 base64 編碼)
$ 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
$ 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 節點
$ 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
# 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
$ consul join -wan 192.168.48.141
Successfully joined cluster by contacting 1 nodes.join 成功後,執行命令查看廣域網 members
$ 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 數據
$ consul kv put name consul
Success! Data written to: name在 vm01 節點嘗試讀取數據,可以看到同一數據中心的數據是同步的
$ consul kv get name
consul然後再去不同數據中心的 vm03 嘗試讀取數據,會發現不同數據中心的數據是不同步的。
$ 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 的依賴
go get github.com/hashicorp/consul/api在服務啟動時向 consul 主動注冊服務,在服務關閉時,向 consul 注銷服務,下面是一個示例。
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 自定義解析器,來向注冊中心查詢對應的服務,解析成真實地址。
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() {
}客戶端在啟動時注冊解析器
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 實現服務注冊與發現的簡單案例。
