Consul

Consul — это решение, позволяющее командам безопасно управлять сетевыми соединениями между сервисами в различных средах и мультиоблачных окружениях. Оно предоставляет обнаружение сервисов, service mesh, управление трафиком, автоматическое обновление сетевой инфраструктуры и другие функции.
Официальная документация: 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, каждый узел является агентом.dev— режим работы агента, всего три режима:dev,client,serverbind— адрес связи в локальной сети, порт по умолчанию 8301, обычно это внутренний адрес сервераadvertise— адрес связи в глобальной сети, порт по умолчанию 8302, обычно это внешний адрес сервераdata-dir— каталог хранения данныхconfig-dir— каталог конфигурации, Consul читает все JSON-файлы в этом каталогеbootstrap— указывает, что текущий server входит в режим bootstrap, во время выбора Raft голосует за себя, в кластере не должно быть более одного server в этом режимеbootstrap-expect— ожидаемое количество server в кластере, пока не достигнуто указанное количество, кластер не начнёт выбор, нельзя использовать одновременно сbootstrapretry-join— после запуска агент будет постоянно пытаться присоединиться к указанным узлам, также поддерживает следующие методы обнаружения сервисов:aliyun aws azure digitalocean gce hcp k8s linode mdns os packet scaleway softlayer tencentcloud triton vsphereui— запуск веб-интерфейсаnode— имя выполняемого узла, должно быть уникальным в кластере.
TIP
Более подробное объяснение параметров агента см. в Agents - CLI Reference | Consul | HashiCorp Developer. Обратите внимание, что некоторые параметры доступны только в корпоративной версии.
После успешного запуска访问 127.0.0.1:8500, можно浏览 веб-интерфейс.

Звёздочка рядом с dev01 указывает, что это узел leader.
При выходе, чтобы другие узлы могли感知 о выходе текущего узла, не рекомендуется принудительно завершать процесс, можно использовать команду:
consul leaveИли
consul force-leaveТакже можно нажать ctrl+c, чтобы агент Consul корректно завершил работу.
Концепции
Это схема кластера Consul, разделённая на две части: плоскость управления и плоскость данных. Consul отвечает только за плоскость управления, разделённую на кластер сервисов и клиенты. Кластер сервисов разделён на followers и leaders. В целом, кластер Consul на схеме образует дата-центр. Ниже приведено объяснение некоторых терминов:
- Agent (агент): или более правильно называть узлом, каждый agent является долгоживущим демоном, они предоставляют HTTP и DNS интерфейсы, отвечают за проверку здоровья и синхронизацию сервисов.
- Server (серверный агент): как сервер Consul, его обязанности включают участие в выборе Raft, поддержание состояния кластера, обработку запросов, обмен данными с другими дата-центрами, а также пересылку запросов лидеру и другим дата-центрам.
- Client (клиентский агент): client относительно server не имеет состояния, он не участвует в выборе Raft, его задача — пересылать все запросы server, единственное, что он делает связанного с фоном, это локальная передача слухов (LAN gossip pool).
- Leader (лидер): leader является руководителем всех server, причём лидер может быть только один, leader выбирается через алгоритм выбора Raft, у каждого leader есть свой срок, в течение которого другие server, получив любой запрос, должны сообщить leader, поэтому данные leader самые актуальные.
- Gossip (слухи): 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, при неудаче будет постоянно пытаться, время повторной попытки по умолчанию 30с:
$ consul join 192.168.48.138После завершения join все узлы знают о существовании друг друга. Поскольку vm00 указан в режиме bootstrap, он является leader по умолчанию. Если режим bootstrap не указан, все узлы при join указывают узел как leader по умолчанию. До выбора leader кластер не может нормально работать,访问 веб-интерфейс вернёт 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, чтобы он автоматически присоединился к узлу 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В этот момент分别 проверим members на vm00 и vm03:
# 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 присоединится к любому узлу dc2, здесь пусть vm01 присоединится к 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 присоединится к любому узлу dc2, все узлы обоих дата-центров感知 об этом изменении, при просмотре members также можно увидеть узлы обоих дата-центров.
Далее попробуем добавить KV-данные на узле vm00:
$ 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 предоставляет SDK для HTTP API, 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)
}
// Создание gRPC сервера
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 для реализации регистрации и обнаружения сервисов.
