Skip to content

Consul

consul é uma solução que permite às equipes gerenciar conexões de rede de forma segura entre serviços e em ambientes multi-cloud e multi-provisionamento, oferecendo funcionalidades como descoberta de serviços, service mesh, governança de tráfego, atualização automática de infraestrutura de rede e muito mais.

Documentação oficial: Consul by HashiCorp

Código fonte aberto: hashicorp/consul

Consul é uma ferramenta de descoberta e registro de serviços de código aberto da empresa HashiCorp, que utiliza o algoritmo de eleição Raft. A ferramenta em si é desenvolvida em Go, tornando sua implantação muito leve. O Consul possui as seguintes características:

  • Descoberta de serviços
  • Registro de serviços
  • Verificação de saúde
  • Armazenamento chave-valor
  • Multi-datacenter

Na verdade, o consul pode fazer mais do que apenas descoberta de serviços, também pode ser usado como centro de configuração distribuída. Existem muitas outras ferramentas de código aberto do mesmo tipo, como zookeeper, nacos, que não serão apresentadas em detalhes aqui.

Instalação

Para Ubuntu, execute os comandos abaixo para instalar usando 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

Ou você pode baixar no site oficial Install Consul o pacote de instalação correspondente. Como o consul é desenvolvido em go, o pacote de instalação em si é apenas um arquivo executável binário, e a instalação é bastante conveniente. Após a instalação bem-sucedida, execute o seguinte comando para verificar a versão.

sh
$ consul version

Se a saída for normal, não há problema

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)

Início Rápido

Abaixo está como configurar rapidamente um único nó consul. Geralmente, um único nó é usado para testes durante o desenvolvimento. Se um único nó funcionar sem problemas, é provável que um cluster de vários nós também não tenha problemas. A configuração de um único nó é muito simples, requer apenas uma linha de comando

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

Geralmente haverá a seguinte saída

==> 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}]"

Uma breve explicação dos significados

  • agent é um subcomando, é o comando principal do consul, consul agent executa um novo agente consul, cada nó é um agente.

  • dev é o modo de execução do agent, existem três tipos no total: dev, client, server

  • bind, endereço de comunicação da rede local, porta padrão 8301, geralmente este valor é o endereço interno do servidor

  • advertise, endereço de comunicação da rede de longa distância, porta padrão 8302, geralmente este valor é o endereço externo do servidor

  • data-dir, diretório de armazenamento de dados

  • config-dir, diretório de configuração, o consul lê todos os arquivos json no diretório

  • bootstrap, marca o servidor atual para entrar no modo de bootstrap, votando em si mesmo durante a eleição raft, não pode haver mais de um servidor neste modo no cluster

  • bootstrap-expect, o número esperado de servidores no cluster, até que o número especificado seja atingido, o cluster não iniciará a eleição e votação, não pode ser usado simultaneamente com bootstrap.

  • retry-join, após o agent iniciar, ele continuará tentando se juntar aos nós especificados, também suporta os seguintes métodos de descoberta de serviços de provedores

    aliyun aws azure digitalocean gce hcp k8s linode mdns os packet scaleway softlayer tencentcloud triton vsphere
  • ui, executa a interface Web

  • node, nome do nó de execução, deve ser único no cluster.

TIP

Para mais explicações sobre parâmetros do agent, vá para Agents - CLI Reference | Consul | HashiCorp Developer , note que alguns parâmetros só estão disponíveis na versão empresarial.

Após executar com sucesso, acesse 127.0.0.1:8500 para navegar na interface Web.

O ícone dev01 é uma estrela, indicando que é o nó líder.

Ao sair, para permitir que outros nós percebam a saída do nó atual, não é recomendado forçar a terminação do processo, use o comando

sh
consul leave

ou

sh
consul force-leave

Também pode usar ctrl+c para sair graciosamente do consul agent.

Conceitos

Este é um diagrama de um cluster consul, dividido em duas partes: plano de controle e plano de dados. O consul é responsável apenas pelo plano de controle, dividido em cluster de serviços e clientes, o cluster de serviços é dividido em seguidores e líderes, em geral, o cluster consul na figura constitui um datacenter. Abaixo está a explicação de alguns termos

  • Agent (Agente): ou seria mais apropriado chamar de nó, cada agent é um processo de longa duração que expõe interfaces HTTP e DNS, responsável por verificações de saúde e sincronização de serviços.
  • Server (Agente de Serviço): Como um servidor consul, suas responsabilidades incluem participar da eleição Raft, manter o estado do cluster, responder a consultas, trocar dados com outros datacenters e encaminhar consultas para líderes e outros datacenters.
  • Client (Agente Cliente): client é stateless em comparação com o server, não participa da eleição Raft, sua única função é encaminhar todas as solicitações para o server, a única coisa que participa relacionada ao background é o pool de gossip LAN (LAN gossip pool).
  • Leader (Líder): leader é o chefe de todos os servers, e só pode haver um líder, leader é eleito através do algoritmo de eleição Raft, cada líder tem seu próprio mandato, durante o mandato, qualquer solicitação recebida por outros servers deve ser passada ao leader, então os dados do leader são os mais atualizados.
  • Gossip (Rumor): Consul é construído sobre Serf (outro produto da mesma empresa), usa o protocolo gossip, que é especializado em comunicação aleatória entre nós, similar ao UDP, consul usa este protocolo para notificação mútua entre clusters de serviços.
  • Data Center (Datacenter): Um cluster consul dentro de uma rede local é chamado de datacenter, consul suporta múltiplos datacenters, o modo de comunicação entre múltiplos datacenters é WAN gossip.

TIP

Mais vocabulário e termos podem ser encontrados em Glossary | Consul | HashiCorp Developer

No cluster consul, o número de servers deve ser estritamente controlado, pois eles participam diretamente do LAN gossip e WAN gossip, eleição raft e precisam armazenar dados, quanto mais servers, maior o custo de comunicação. Enquanto o número de clients pode ser maior sem problemas, eles são apenas responsáveis pelo encaminhamento, não participam da eleição e ocupam poucos recursos, no cluster da figura, vários serviços se registram no server através do client, se um server falhar, o client procurará outros servers disponíveis.

Exemplo de Configuração de Cluster

Abaixo está um exemplo simples de configuração de cluster consul de múltiplos nós, primeiro prepare quatro máquinas virtuais

Das quatro máquinas virtuais, três servers e um client, a recomendação oficial é que o número de servers seja ímpar e preferencialmente maior ou igual a três. Aqui vm00-vm02 são usados como servers e vm03 como client.

Para o server, execute o seguinte comando para criar o agent server

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

Para o client, execute o seguinte comando para criar o agent client

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

Os comandos executados são respectivamente

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

Algumas explicações de parâmetros

  • client, 0.0.0.0 significa permitir solicitações de todas as origens, se houver apenas o parâmetro client sem o parâmetro server, representa que o agent executará no modo client.

Depois que todos os agents estiverem em execução, a função de retry-join é equivalente a executar automaticamente o comando join, e continuará tentando após falhar, o tempo de retry padrão é 30s

sh
$ consul join 192.168.48.138

Após a conclusão do join, cada nó conhece a existência do outro, como vm00 especificou o modo bootstrap, ele é o líder padrão, se o modo bootstrap não for especificado, o nó especificado por todos os nós durante o join é o líder padrão, antes que o líder seja eleito, o cluster não pode funcionar normalmente, acessar a interface web retornará 500, e alguns comandos também não funcionarão corretamente. Se um nó no cluster especificar o modo bootstrap, então nenhum outro nó no cluster deve especificar o modo bootstrap, e outros nós também não devem usar o parâmetro bootstrap-expect, se usado será automaticamente desabilitado.

Neste ponto, execute o comando para visualizar as informações dos membros do datacenter no nó líder (na verdade, neste momento qualquer nó pode visualizar)

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, nome do nó
  • Address, endereço de comunicação
  • Status, alive significa ativo, left significa offline
  • Type, tipo de agent, dois modos server e client
  • Build, versão do consul usada pelo nó, consul pode trabalhar com nós de diferentes versões dentro de um certo escopo
  • Protocol, refere-se à versão do protocolo Raft usada, este protocolo deve ser consistente em todos os nós
  • DC, Data Center, datacenter, todos os nós na saída pertencem ao datacenter dc1
  • Partition, partição à qual o nó pertence, funcionalidade da versão empresarial, cada nó só pode se comunicar com nós na mesma partição
  • Segment, segmento ao qual o nó pertence, funcionalidade da versão empresarial

Da mesma forma, se quiser que um nó saia, deve usar consul leave para sair graciosamente e notificar outros nós que está prestes a sair, para múltiplos nós, a saída graciosa do nó é particularmente importante, pois isso está relacionado à consistência dos dados.

TIP

As máquinas virtuais desativaram todos os firewalls durante a demonstração, em ambientes de produção reais, por razões de segurança, eles devem ser ativados, para isso deve-se prestar atenção a todas as portas usadas pelo consul: Required Ports | Consul | HashiCorp Developer.

Em seguida, teste brevemente a consistência dos dados, adicione os seguintes dados na máquina virtual vm00

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

Após salvar, acesse outros nós através da API HTTP e descobrirá que os dados também existem (o value é codificado em 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}]

De fato, a funcionalidade de descoberta e registro de serviços fornecida pelo consul é transmitida para outros nós através do protocolo gossip, e quando qualquer nó se junta ao datacenter atual, todos os nós perceberão essa mudança.

Exemplo de Configuração de Multi-Datacenter

Prepare cinco máquinas virtuais, vm00-vm02 são o cluster do exemplo anterior, pertencem ao datacenter dc1, não mexa nele, vm03-vm04 pertencem ao datacenter dc2, o datacenter é dc1 por padrão quando o agent é iniciado.

TIP

Aqui para demonstração, apenas o server é configurado, omitindo o client.

Primeiro inicie vm03 respectivamente, tornando-o o líder padrão

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

Inicie vm04, faça com que ele se junte automaticamente ao nó 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

Neste ponto, verifique members em vm00 e vm03 respectivamente

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>

Pode-se ver que o campo DC é diferente, como aqui é uma demonstração com máquina virtual, então estão todos na mesma sub-rede, na realidade dois datacenters podem ser clusters de servidores em locais diferentes. Em seguida, faça com que qualquer nó de dc1 se junte a qualquer nó de dc2, aqui faça vm01 se juntar a vm03

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

Após o join bem-sucedido, execute o comando para visualizar members da rede de longa distância

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

Basta que qualquer nó de dc1 se junte a qualquer nó de dc2, todos os nós de ambos os datacenters perceberão essa mudança, ao visualizar members também é possível ver os nós de ambos os datacenters.

Em seguida, tente adicionar um dado KV no nó vm00

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

Tente ler os dados no nó vm01, pode-se ver que os dados do mesmo datacenter estão sincronizados

sh
$ consul kv get name
consul

Depois vá tentar ler os dados em vm03 em um datacenter diferente, descobrirá que os dados de datacenters diferentes não estão sincronizados.

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

Se quiser sincronização de dados entre múltiplos datacenters, pode consultar hashicorp/consul-replicate: Consul cross-DC KV replication daemon.

Registro e Descoberta de Serviços

Existem duas maneiras de registrar serviços no consul: registro por arquivo de configuração e registro por API. Para facilitar o teste, prepare antecipadamente um serviço Hello World (exemplo do artigo gRPC), implante duas cópias em locais diferentes. Para o método de registro por arquivo de configuração, consulte Register external services with Consul service discovery | Consul | HashiCorp Developer, aqui será apresentado apenas o registro através da API HTTP.

TIP

Para serviços locais (junto com o consul client), pode-se usar diretamente o registro de serviço do agent, caso contrário, deve-se usar o catalog register para registro.

O consul fornece SDKs de API HTTP, SDKs de outras linguagens podem ser consultados em Libraries and SDKs - HTTP API | Consul | HashiCorp Developer. Aqui baixe a dependência go

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

Registre ativamente o serviço no consul quando o serviço for iniciado e cancele o registro do serviço no consul quando o serviço for desligado, abaixo está um exemplo.

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{
        // Deve ser único
    ID:      "hello-service1",
    Service: "hello-service",
        // Implante duas cópias, uma com porta 8080, outra com porta 8081
    Port:    8080,
  }
)

// Registrar serviço
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)
}

// Cancelar registro do serviço
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()

  // Escutar porta
  listen, err := net.Listen("tcp", ":8080")
  if err != nil {
    panic(err)
  }
  // Criar servidor grpc
  server := grpc.NewServer(
    grpc.Creds(insecure.NewCredentials()),
  )
  // Registrar serviço
  pb.RegisterSayHelloServer(server, &HelloRpc{})
  log.Println("server running...")
  // Executar
  err = server.Serve(listen)
  if err != nil {
    panic(err)
  }
}

O código do cliente usa o resolvedor personalizado do consul para consultar o serviço correspondente no centro de registro e resolvê-lo para o endereço real.

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,
       // Estratégia de round-robin
       ServiceConfig: c.cc.ParseServiceConfig(
          `{"loadBalancingPolicy":"round_robin"}`),
    })
}

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

func (c ConsulResolver) Close() {

}

O cliente registra o resolvedor ao iniciar

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() {
    // Registrar builder
    resolver.Register(
       // Registrar resolvedor consul personalizado
       myresolver.NewConsulResolverBuilder("192.168.48.138:8500"),
    )
}

func main() {

    // Estabelecer conexão, sem verificação de criptografia
    conn, err := grpc.Dial("consul:hello-service",
       grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
       panic(err)
    }
    defer conn.Close()
    // Criar cliente
    client := hello2.NewSayHelloClient(conn)
    for range time.Tick(time.Second) {
       // Chamada remota
       helloRep, err := client.Hello(context.Background(), &hello2.HelloReq{Name: "client"})
       if err != nil {
          panic(err)
       }
       log.Printf("received grpc resp: %+v", helloRep.String())
    }

}

Primeiro inicie o servidor, depois inicie o cliente, existem dois servidores, fornecendo o mesmo serviço, apenas com endereços diferentes, a estratégia de balanceamento de carga do cliente é round-robin, pode-se ver a partir do intervalo de tempo dos logs do servidor que a estratégia foi efetivada.

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"

Acima está um caso simples de uso do consul combinado com gRPC para implementar registro e descoberta de serviços.

Golang por www.golangdev.cn edit