Service discovery com Consul ou etcd
1. Introdução ao Service Discovery em Microsserviços Go
1.1. Por que service discovery é essencial em sistemas distribuídos
Em arquiteturas de microsserviços, instâncias são criadas e destruídas dinamicamente devido a escalonamento horizontal, falhas ou deploys. Endereços IP e portas mudam constantemente, tornando inviável a configuração estática. O service discovery resolve esse problema mantendo um catálogo atualizado de serviços disponíveis, permitindo que consumidores encontrem produtores sem acoplamento rígido.
1.2. Abordagens: cliente-servidor vs. peer-to-peer
Existem duas abordagens principais:
- Cliente-servidor: Um servidor central (como Consul ou etcd) armazena o estado. Clientes registram e consultam serviços. Oferece consistência forte, mas introduz um ponto único de falha que pode ser mitigado com clusters.
- Peer-to-peer: Cada nó conhece seus pares (ex: Serf, memberlist). Escalável e tolerante a falhas, mas com consistência eventual.
1.3. Visão geral do Consul e etcd como soluções de descoberta
- Consul: Plataforma completa com service discovery, health checking, segmentação por datacenter e suporte a DNS. Ideal para ambientes híbridos (on-premise + cloud).
- etcd: Key-value store consistente e altamente disponível, usado como backbone do Kubernetes. Excelente para cenários que exigem forte consistência e integração com sistemas containerizados.
Ambos possuem bibliotecas Go maduras e são escolhas populares na comunidade.
2. Configuração e Integração Básica com Consul
2.1. Instalação local do Consul e configuração mínima
Para testar localmente, inicie o Consul em modo dev:
consul agent -dev -node=local -bind=127.0.0.1
2.2. Registro de serviço usando a biblioteca hashicorp/consul/api
package main
import (
"fmt"
"log"
consulapi "github.com/hashicorp/consul/api"
)
func registerService(client *consulapi.Client, name, address string, port int) error {
registration := &consulapi.AgentServiceRegistration{
ID: fmt.Sprintf("%s-%s", name, address),
Name: name,
Address: address,
Port: port,
Check: &consulapi.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/health", address, port),
Interval: "10s",
Timeout: "5s",
},
}
return client.Agent().ServiceRegister(registration)
}
func main() {
config := consulapi.DefaultConfig()
config.Address = "localhost:8500"
client, err := consulapi.NewClient(config)
if err != nil {
log.Fatal(err)
}
err = registerService(client, "user-service", "192.168.1.10", 8080)
if err != nil {
log.Fatal(err)
}
fmt.Println("Serviço registrado com sucesso no Consul")
}
2.3. Descoberta de serviços via consulta DNS e API HTTP
func discoverService(client *consulapi.Client, serviceName string) ([]string, error) {
services, _, err := client.Health().Service(serviceName, "", true, nil)
if err != nil {
return nil, err
}
var endpoints []string
for _, entry := range services {
endpoints = append(endpoints, fmt.Sprintf("%s:%d", entry.Service.Address, entry.Service.Port))
}
return endpoints, nil
}
3. Configuração e Integração Básica com etcd
3.1. Inicialização de um cluster etcd local (modo single-node)
etcd --listen-client-urls http://localhost:2379 --advertise-client-urls http://localhost:2379
3.2. Registro de serviço com leases usando clientv3 do etcd
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func registerServiceEtcd(cli *clientv3.Client, key, value string, ttl int64) error {
lease, err := cli.Grant(context.Background(), ttl)
if err != nil {
return err
}
_, err = cli.Put(context.Background(), key, value, clientv3.WithLease(lease.ID))
if err != nil {
return err
}
// Keep-alive da lease
keepAlive, err := cli.KeepAlive(context.Background(), lease.ID)
if err != nil {
return err
}
go func() {
for range keepAlive {
// Mantém a lease ativa
}
}()
return nil
}
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
key := "/services/user-service/192.168.1.10:8080"
value := `{"address":"192.168.1.10","port":8080,"status":"active"}`
err = registerServiceEtcd(cli, key, value, 10)
if err != nil {
log.Fatal(err)
}
fmt.Println("Serviço registrado no etcd com lease")
}
3.3. Watchers para descoberta dinâmica de endpoints
func watchService(cli *clientv3.Client, prefix string) {
watchChan := cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
for wresp := range watchChan {
for _, ev := range wresp.Events {
fmt.Printf("Tipo: %s, Chave: %s, Valor: %s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
}
4. Estratégias de Health Checking e Heartbeat
4.1. Health checks customizados no Consul (scripts, HTTP, TCP)
O Consul suporta múltiplos tipos de health check. O exemplo anterior usou HTTP. Para checks TCP:
Check: &consulapi.AgentServiceCheck{
TCP: fmt.Sprintf("%s:%d", address, port),
Interval: "10s",
}
4.2. Implementação de heartbeats com etcd (keep-alive de leases)
O mecanismo de leases do etcd já funciona como heartbeat. Se o serviço falhar ao renovar a lease dentro do TTL, a chave expira automaticamente.
4.3. Tratamento de falhas: remoção automática de serviços inativos
No Consul, checks com falha marcam o serviço como crítico. Após um número configurável de falhas, o serviço é removido da consulta de serviços saudáveis. No etcd, a expiração da lease remove automaticamente a chave.
5. Padrões Avançados de Descoberta em Go
5.1. Cache local de endpoints com atualização por watch/event
type ServiceCache struct {
mu sync.RWMutex
endpoints map[string][]string
consulClient *consulapi.Client
}
func (c *ServiceCache) Watch(serviceName string) {
go func() {
for {
endpoints, err := discoverService(c.consulClient, serviceName)
if err == nil {
c.mu.Lock()
c.endpoints[serviceName] = endpoints
c.mu.Unlock()
}
time.Sleep(5 * time.Second)
}
}()
}
5.2. Estratégias de resolução: round-robin, weighted, least-connections
type RoundRobin struct {
mu sync.Mutex
index int
endpoints []string
}
func (rr *RoundRobin) Next() string {
rr.mu.Lock()
defer rr.mu.Unlock()
if len(rr.endpoints) == 0 {
return ""
}
endpoint := rr.endpoints[rr.index]
rr.index = (rr.index + 1) % len(rr.endpoints)
return endpoint
}
5.3. Integração com gRPC resolvers e load balancers personalizados
O pacote google.golang.org/grpc/resolver permite criar resolvers customizados que consultam Consul ou etcd dinamicamente.
6. Comparação Prática: Consul vs. etcd para Service Discovery
6.1. Diferenças de consistência, latência e tolerância a falhas
| Característica | Consul | etcd |
|---|---|---|
| Consistência | Eventual (AP) | Forte (CP) |
| Latência | Menor (cache local) | Maior (consenso Raft) |
| Health checking | Nativo (HTTP, TCP, script) | Requer implementação externa |
| DNS integrado | Sim | Não |
| Interface web | Sim | Não (apenas API) |
6.2. Casos de uso recomendados
- Consul: Ambientes híbridos, múltiplos datacenters, necessidade de DNS, health checks sofisticados.
- etcd: Kubernetes, sistemas que já usam etcd, cenários que exigem forte consistência.
6.3. Exemplo de código: migração de Consul para etcd
A migração envolve substituir chamadas à API do Consul por operações equivalentes no etcd. O conceito de chave-valor com leases substitui o registro de serviço com TTL.
7. Boas Práticas e Troubleshooting
7.1. Gerenciamento de conexões e timeouts com retry e backoff
func connectWithRetry(endpoints []string, maxRetries int) (*clientv3.Client, error) {
var cli *clientv3.Client
var err error
for i := 0; i < maxRetries; i++ {
cli, err = clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err == nil {
return cli, nil
}
time.Sleep(time.Duration(i+1) * time.Second)
}
return nil, fmt.Errorf("falha ao conectar após %d tentativas: %v", maxRetries, err)
}
7.2. Monitoramento do service discovery (métricas e logs)
Registre métricas como:
- Número de serviços registrados
- Latência de consulta
- Taxa de expiração de leases
- Número de falhas de health check
7.3. Problemas comuns: split-brain, churn de registros, TTL mal configurado
- Split-brain: Em clusters etcd, garantir número ímpar de nós.
- Churn de registros: TTL muito curto causa registros constantes. Ajuste para 30-60 segundos.
- TTL mal configurado: TTL muito longo mantém serviços mortos; muito curto causa instabilidade.
Referências
- Consul API Documentation — Documentação oficial da API REST do Consul, incluindo registro e descoberta de serviços.
- etcd clientv3 Go Documentation — Documentação oficial da biblioteca Go para interagir com etcd.
- Service Discovery in Go with Consul — Capítulo do livro "Cloud Native Go" sobre service discovery com Consul.
- etcd: Service Discovery Patterns — Guia oficial do etcd sobre padrões de descoberta de serviços.
- gRPC Resolver with Consul in Go — Exemplo de integração do Consul com resolvedores gRPC no repositório oficial.
- Comparing Consul and etcd for Service Discovery — Artigo técnico comparando Consul e etcd em cenários reais de service discovery.