Skip to content

Consul

Consul — это решение, позволяющее командам безопасно управлять сетевыми соединениями между сервисами в различных средах и мультиоблачных окружениях. Оно предоставляет обнаружение сервисов, service mesh, управление трафиком, автоматическое обновление сетевой инфраструктуры и другие функции.

Официальная документация: 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, каждый узел является агентом.

  • dev — режим работы агента, всего три режима: dev, client, server

  • bind — адрес связи в локальной сети, порт по умолчанию 8301, обычно это внутренний адрес сервера

  • advertise — адрес связи в глобальной сети, порт по умолчанию 8302, обычно это внешний адрес сервера

  • data-dir — каталог хранения данных

  • config-dir — каталог конфигурации, Consul читает все JSON-файлы в этом каталоге

  • bootstrap — указывает, что текущий server входит в режим bootstrap, во время выбора Raft голосует за себя, в кластере не должно быть более одного server в этом режиме

  • bootstrap-expect — ожидаемое количество server в кластере, пока не достигнуто указанное количество, кластер не начнёт выбор, нельзя использовать одновременно с bootstrap

  • retry-join — после запуска агент будет постоянно пытаться присоединиться к указанным узлам, также поддерживает следующие методы обнаружения сервисов:

    aliyun aws azure digitalocean gce hcp k8s linode mdns os packet scaleway softlayer tencentcloud triton vsphere
  • ui — запуск веб-интерфейса

  • node — имя выполняемого узла, должно быть уникальным в кластере.

TIP

Более подробное объяснение параметров агента см. в Agents - CLI Reference | Consul | HashiCorp Developer. Обратите внимание, что некоторые параметры доступны только в корпоративной версии.

После успешного запуска访问 127.0.0.1:8500, можно浏览 веб-интерфейс.

Звёздочка рядом с dev01 указывает, что это узел leader.

При выходе, чтобы другие узлы могли感知 о выходе текущего узла, не рекомендуется принудительно завершать процесс, можно использовать команду:

sh
consul leave

Или

sh
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:

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

Объяснение некоторых параметров:

  • client, 0.0.0.0 означает разрешение всех запросов, если есть только параметр client без server, это означает, что agent будет работать в режиме client.

Все agent работают хорошо, retry-join эквивалентен автоматическому выполнению команды join, при неудаче будет постоянно пытаться, время повторной попытки по умолчанию 30с:

sh
$ consul join 192.168.48.138

После завершения join все узлы знают о существовании друг друга. Поскольку vm00 указан в режиме bootstrap, он является leader по умолчанию. Если режим bootstrap не указан, все узлы при join указывают узел как leader по умолчанию. До выбора leader кластер не может нормально работать,访问 веб-интерфейс вернёт 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, чтобы он автоматически присоединился к узлу 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

В этот момент分别 проверим members на vm00 и vm03:

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 присоединится к любому узлу dc2, здесь пусть vm01 присоединится к 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 присоединится к любому узлу dc2, все узлы обоих дата-центров感知 об этом изменении, при просмотре members также можно увидеть узлы обоих дата-центров.

Далее попробуем добавить KV-данные на узле vm00:

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 предоставляет SDK для HTTP API, 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)
  }
  // Создание 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 для запроса соответствующего сервиса в центре регистрации и преобразования в реальный адрес:

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 by www.golangdev.cn edit