Em um datacenter tradicional, servidores têm IPs fixos e a localização de um serviço é hardcoded em configuração. Em ambientes de nuvem e contêineres, instâncias são efêmeras: o Kubernetes pode reiniciar um pod em um nó diferente, o auto-scaling pode criar dez instâncias em segundos e destruí-las cinco minutos depois, uma implantação blue-green pode trocar o conjunto ativo de instâncias sem downtime. O IP de ontem não é o IP de hoje. Service discovery é o mecanismo pelo qual serviços encontram uns aos outros nesse ambiente dinâmico.
O problema tem dois lados: registro (como uma instância anuncia sua presença e localização) e descoberta (como um cliente encontra uma instância saudável de um serviço). As soluções diferem em onde cada responsabilidade reside — no cliente, no proxy, no DNS, ou em um registro dedicado.
DNS como service discovery
DNS é o mecanismo de discovery mais simples e universal. Em vez de IPs, clientes resolvem nomes de domínio. O DNS retorna um ou mais registros A (IPv4) ou AAAA (IPv6) apontando para instâncias do serviço. É o padrão em Kubernetes: todo Service tem um nome DNS interno (payment-service.production.svc.cluster.local) que o kube-dns resolve para o ClusterIP do Service, que por sua vez balanceia para os pods saudáveis.
; Kubernetes DNS — resolução interna
; payment-service.production.svc.cluster.local → 10.96.0.15 (ClusterIP)
; O kube-proxy mantém as regras iptables/IPVS que distribuem
; o tráfego para os pods selecionados pelo Service
; DNS com múltiplos A records (round-robin DNS)
; api.example.com → 10.0.0.1, 10.0.0.2, 10.0.0.3
; Cada resolução DNS retorna os registros em ordem diferente
; O cliente usa o primeiro — primitive load balancing
Limitações do DNS puro: TTL alto causa problemas quando instâncias mudam (clientes continuam usando IPs stale até o TTL expirar), round-robin não considera a saúde ou carga de cada instância, e clientes que cacheiam resoluções DNS agressivamente ignoram mudanças. Para microsserviços em Kubernetes, DNS é suficiente para a maioria dos casos — o kube-proxy mantém a tabela atualizada e o ClusterIP é estável.
Client-side discovery
No modelo client-side, o cliente é responsável por consultar o registro de serviços, obter a lista de instâncias saudáveis e escolher uma usando um algoritmo de load balancing local. O cliente tem controle total sobre a seleção — pode usar round-robin, least connections, weighted, ou qualquer algoritmo customizado.
┌──────────────┐ 1. query instances ┌─────────────────┐
│ Client │ ─────────────────────────► │ Service Registry│
│ │ ◄───────────────────────── │ (Consul/Eureka) │
│ │ 2. [10.0.0.1:8080, └─────────────────┘
│ │ 10.0.0.2:8080]
│ │
│ │ 3. choose + call
│ │ ─────────────────────────► 10.0.0.2:8080
└──────────────┘
Prós: lógica de balanceamento customizável, sem hop extra (sem proxy intermediário), o cliente pode observar a latência de cada instância e redirecionar. Contras: cada cliente precisa implementar a lógica de discovery e balanceamento — e cada linguagem precisa de sua própria biblioteca. Quando a lógica de balanceamento precisa mudar, todos os clientes precisam ser atualizados.
Exemplos: Spring Cloud Netflix Ribbon + Eureka (Java), Finagle (Scala), e qualquer cliente gRPC com resolver customizado que consulta o Consul.
Server-side discovery
No modelo server-side, o cliente envia a requisição para um router (load balancer ou proxy) que resolve e encaminha para uma instância saudável. O cliente não sabe nada sobre as instâncias individuais — só sabe o endereço do router.
┌──────────────┐ 1. call service-a ┌──────────────────┐
│ Client │ ─────────────────────────► │ Load Balancer │
│ │ │ (ALB / nginx / │
│ │ │ Envoy / LB) │
│ │ └────────┬─────────┘
│ │ │ 2. query + route
│ │ ┌────────┴─────────┐
│ │ │ Service Registry │
│ │ └────────┬─────────┘
│ │ │ 3. forward
│ │ ┌────────▼─────────┐
│ │ │ Instance 10.0.0.2│
└──────────────┘ └──────────────────┘
Prós: cliente simples (sem lógica de discovery), atualização de política de balanceamento sem mudar clientes, ponto único para observabilidade. Contras: hop extra de rede, o load balancer é um ponto de falha e gargalo potencial (mitigado com redundância e design stateless).
Exemplos: AWS ALB + Target Groups, Kubernetes Service + kube-proxy, Nginx com upstream dinâmico via Consul Template, Envoy com xDS.
Registros de serviço — Consul e etcd
Consul
Consul é o registro de serviços mais completo — combina service registry, health checking distribuído, configuração distribuída (KV store) e service mesh básica. É baseado no protocolo Raft para consistência.
# Registro de serviço no Consul (via HTTP API)
curl -X PUT http://localhost:8500/v1/agent/service/register \
-H "Content-Type: application/json" \
-d '{
"ID": "payment-service-1",
"Name": "payment-service",
"Address": "10.0.0.5",
"Port": 8080,
"Tags": ["v2", "production"],
"Check": {
"HTTP": "http://10.0.0.5:8080/health",
"Interval": "10s",
"Timeout": "5s",
"DeregisterCriticalServiceAfter": "30s"
}
}'
# Descoberta via DNS (Consul DNS interface, porta 8600)
dig @localhost -p 8600 payment-service.service.consul SRV
# Descoberta via HTTP API
curl http://localhost:8500/v1/health/service/payment-service?passing=true
# Retorna apenas instâncias com health check passando
Health checking no Consul: cada agente Consul local verifica os serviços registrados naquele nó (HTTP, TCP, gRPC, script, TTL). Instâncias com health check falhando são removidas automaticamente das respostas de discovery. DeregisterCriticalServiceAfter remove automaticamente serviços que ficaram em estado crítico por muito tempo — evita acúmulo de instâncias mortas no registro.
etcd
etcd é um key-value store distribuído consistente, criado para ser o backend do Kubernetes. Não é um service registry pronto — você implementa discovery em cima do KV e do mecanismo de watch. O Kubernetes usa etcd para armazenar todo o estado do cluster, incluindo Endpoints de Services.
# etcd como registro manual (padrão de chave por instância)
# Registro:
etcdctl put /services/payment/10.0.0.5:8080 '{"weight":1,"version":"v2"}'
# Watch para mudanças (stream de eventos):
etcdctl watch /services/payment/ --prefix
# Descoberta (listar todas as instâncias):
etcdctl get /services/payment/ --prefix
# Com lease (TTL): instância deve renovar ou é removida automaticamente
LEASE=$(etcdctl lease grant 30 | awk '{print $2}')
etcdctl put /services/payment/10.0.0.5:8080 '{"weight":1}' --lease=$LEASE
# Renovação:
etcdctl lease keep-alive $LEASE
Service discovery em Kubernetes
Kubernetes resolve service discovery em múltiplas camadas, sem necessidade de Consul ou etcd manual para a maioria dos casos:
ClusterIP Service: IP virtual estável dentro do cluster. kube-proxy mantém regras iptables/IPVS que fazem DNAT do ClusterIP para os pods selecionados pelo selector. DNS resolve o nome do Service para o ClusterIP. É o mecanismo padrão para comunicação intra-cluster.
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: production
spec:
selector:
app: payment # seleciona pods com este label
ports:
- port: 80
targetPort: 8080
type: ClusterIP
# DNS gerado:
# payment-service.production.svc.cluster.local → ClusterIP
# payment-service.production → ClusterIP (dentro do mesmo namespace)
# payment-service → ClusterIP (dentro do namespace production)
Headless Service: clusterIP: None — o DNS resolve diretamente para os IPs dos pods, sem ClusterIP intermediário. Útil para StatefulSets (Kafka, Cassandra) onde o cliente precisa endereçar instâncias específicas, ou para client-side load balancing.
apiVersion: v1
kind: Service
metadata:
name: kafka
spec:
clusterIP: None # headless
selector:
app: kafka
ports:
- port: 9092
# DNS resolve para múltiplos A records — um por pod:
# kafka.default.svc.cluster.local → 10.0.0.1, 10.0.0.2, 10.0.0.3
# kafka-0.kafka.default.svc.cluster.local → 10.0.0.1 (StatefulSet)
# kafka-1.kafka.default.svc.cluster.local → 10.0.0.2
EndpointSlices: Kubernetes mantém EndpointSlices que listam os pods saudáveis de cada Service. Quando um pod falha no readiness probe, é removido do EndpointSlice — e o kube-proxy e o Envoy (se usando service mesh) atualizam suas tabelas. Isso é o mecanismo de health-aware routing do Kubernetes.
Padrões de registro — self-registration vs third-party
No modelo self-registration, a instância se registra no service registry ao inicializar e se desregistra ao encerrar. Simples de implementar, mas coloca responsabilidade na aplicação — e se a aplicação travar sem encerrar gracefully, a instância fica no registro sem health check passando até o TTL expirar.
No modelo third-party registration, um sistema externo (Kubernetes controller, Nomad, Consul agent) monitora a infraestrutura e mantém o registro atualizado. A aplicação não sabe que existe um registro. É mais robusto: o orquestrador tem visibilidade de todo o ciclo de vida da instância, incluindo falhas de nó.
Stale endpoints e graceful shutdown
O problema mais frequente em service discovery em produção: um pod está sendo encerrado, mas o cliente ainda tenta enviar requisições para ele porque o registro ou o DNS ainda apontam para aquele IP. Isso causa erros de conexão recusada.
A solução é um graceful shutdown que espera o tempo de propagação antes de parar de aceitar conexões:
# Kubernetes: configuração que garante propagação antes do kill
spec:
containers:
- name: payment-service
lifecycle:
preStop:
exec:
# Aguarda a propagação do endpoint removal pelo kube-proxy
# antes de realmente encerrar o processo
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 30
# O fluxo:
# 1. Pod recebe sinal de encerramento
# 2. preStop executa: sleep 15s
# 3. Durante esses 15s, o Kubernetes remove o pod do EndpointSlice
# 4. kube-proxy e envoys propagam a remoção (~alguns segundos)
# 5. Após preStop, SIGTERM é enviado ao processo
# 6. Processo encerra gracefully (drena conexões em andamento)
# 7. Após terminationGracePeriodSeconds, SIGKILL se ainda rodando
Decisões de engenharia
DNS puro (Kubernetes Services, Route 53, AWS Cloud Map) é suficiente quando o ambiente é Kubernetes ou cloud-native e instâncias mudam via deployments controlados. O kube-proxy mantém a tabela de endpoints atualizada, o ClusterIP é estável, e o DNS interno funciona sem infraestrutura adicional. É simples de operar e não tem dependência extra.
Service Registry dedicado (Consul, etcd, Eureka) faz sentido em ambientes híbridos (VMs + Kubernetes + bare metal), quando você precisa de funcionalidades além do DNS (health checks ricos, KV store, watches para configuração dinâmica), ou quando múltiplas regiões ou datacenters precisam de discovery unificado. Consul é a escolha mais comum — suporta multi-datacenter, tem ACLs e integra com Kubernetes via consul-k8s. O custo é operacionar outro sistema distribuído crítico.
Client-side discovery: o cliente consulta o registro e escolhe a instância. Exemplos: Ribbon (Spring Cloud), implementação manual com Consul API. Vantagem: o cliente tem controle total do algoritmo de seleção (weighted, affinity, circuit breaker por instância). Desvantagem: cada cliente precisa implementar a lógica de discovery e load balancing; acoplamento ao service registry específico.
Server-side discovery: o cliente faz requisição a um endpoint fixo (DNS/VIP/load balancer); a infraestrutura roteia para uma instância saudável. Exemplos: Kubernetes Service + kube-proxy, AWS ALB com target groups, Envoy como proxy. Vantagem: clientes são simples — não sabem de discovery. Desvantagem: o load balancer é um SPOF (mitigado com HA); o algoritmo de seleção é menos customizável por cliente. Em Kubernetes, server-side discovery via Services é o padrão — prefira-o a menos que precise de algoritmos de seleção sofisticados por cliente.
ClusterIP Service (padrão): cria um IP virtual estável; kube-proxy distribui tráfego para pods saudáveis via iptables/IPVS. O cliente faz DNS e obtém o ClusterIP — transparente. Ideal para a maioria dos serviços stateless onde a instância específica não importa.
Headless Service (clusterIP: None): sem IP virtual. O DNS resolve diretamente para os IPs dos pods individuais (múltiplos A records). Use quando o cliente precisa endereçar instâncias específicas: StatefulSets com estado por pod (cada instância de Kafka, Cassandra, ou Postgres tem um nome DNS estável pod-0.service.namespace.svc.cluster.local), ou quando você quer implementar load balancing no client side dentro do Kubernetes. Headless + StatefulSet é o padrão para databases e message brokers em Kubernetes.
Self-registration: a própria instância do serviço se registra no registry ao iniciar e se desregistra ao parar (via shutdown hook). Comum com Consul agent sidecar, Eureka client, ou diretamente via API. Vantagem: a instância conhece seu próprio estado. Desvantagem: acoplamento entre o serviço e o registry; se o shutdown não for graceful, o registro persiste stale até o TTL expirar.
Third-party registration: um componente externo (Kubernetes controller, Registrator, Consul Catalog Sync) é responsável pelo registro. O serviço não sabe que existe um registry. Em Kubernetes, este é o modelo nativo — o Endpoints controller registra/desregistra pods baseado em labels e liveness probes automaticamente. Prefira third-party registration em Kubernetes — é mais confiável porque o runtime (kubelet) detecta falhas de pod antes do próprio serviço conseguir se desregistrar.
-
Configure um cluster Kubernetes local (kind ou minikube) com 3 replicas de um serviço simples. Use
kubectl get endpointsekubectl get endpointslicespara observar os IPs dos pods. Mate um pod comkubectl delete pode observe em tempo real a atualização dos endpoints. Meça o intervalo entre a morte do pod e a remoção do endpoint.
Critério: tempo de remoção do endpoint medido e documentado; diferença entreendpoints(legado) eendpointslices(moderno) explicada; comportamento com e sem readiness probe demonstrado. -
Implemente client-side discovery manual: sem Kubernetes Service, use a API do Kubernetes (
kubectl proxy+ HTTP) para listar os pods de um Deployment por label e implemente round-robin no cliente. Compare a latência e o comportamento de falha com a abordagem DNS via ClusterIP Service.
Critério: o cliente lista pods via API e distribui carga em round-robin; quando um pod morre, o cliente remove da lista em até 2 ciclos de polling; benchmark de latência comparado com DNS/Service documentado. -
Configure um headless Service para um StatefulSet com 3 instâncias. Verifique com
nslookupdentro do cluster que o DNS resolve para múltiplos A records e que cada pod tem nome DNS estável (pod-0.service.namespace.svc.cluster.local). Implemente um cliente que conecta apod-0especificamente para operações de escrita (Kafka leader) e distribui leituras entre todos os pods.
Critério: DNS resolve para 3 IPs distintos; cada pod acessível por nome DNS estável; cliente demonstra roteamento write→pod-0 e read→round-robin. -
Demonstre o problema de stale endpoints: encerre um pod com
kill -9(simulando crash sem graceful shutdown) e usecurlem loop com--failpara medir quantas requisições retornam 502 antes do endpoint ser removido. AdicionepreStop: sleep 10e compare o número de erros.
Critério: sem preStop: número de erros 502 medido; com preStop sleep 10: zero ou próximo de zero erros durante o shutdown; latência do teardown documentada. -
Instale o Consul em um cluster Kubernetes usando consul-k8s. Registre um serviço externo (um servidor Go simples rodando fora do Kubernetes em um processo local) no Consul com health check HTTP. Acesse-o de dentro do cluster via DNS do Consul (
external-service.service.consul). Simule falha do serviço externo e observe a remoção automática via health check.
Critério: serviço externo acessível via DNS do Consul dentro do cluster; após parar o servidor externo, o Consul remove o endpoint em até 2× o intervalo de health check; acesso ao serviço falha de forma limpa após remoção.
Perguntas de entrevista
Qual a diferença entre client-side e server-side service discovery? Qual você prefere e por quê?
Client-side discovery: o cliente consulta o registry (Consul, Eureka) e escolhe a instância usando algoritmo de load balancing local. A lógica de seleção pode ser sofisticada (weighted, circuit breaker por instância, affinity). O custo é acoplamento ao registry no cliente e duplicação da lógica em cada linguagem/serviço.
Server-side discovery: o cliente usa um endpoint estável (ClusterIP, ALB); a infraestrutura roteia. O cliente não sabe de discovery — apenas faz HTTP/gRPC para um endereço fixo. Simples para desenvolver, mas o load balancer é um componente crítico (deve ser HA).
Preferência: em Kubernetes, server-side via Services é o padrão e a escolha certa para 95% dos casos. O kube-proxy faz load balancing L4 eficientemente via iptables/IPVS. Para casos onde você precisa de algoritmos sofisticados (circuit breaker por instância, affinity de sessão no cliente) ou está fora de Kubernetes, considere client-side com sidecar (Envoy) em vez de implementar no próprio serviço — é a abordagem do Service Mesh.
Por que o DNS puro tem limitações em microsserviços? Como o Kubernetes contorna essas limitações?
Limitações do DNS puro: (1) TTL alto — mudanças de IP demoram a propagar; clientes que cacheiam agressivamente (JVM é famosa por isso) continuam usando IPs stale; (2) round-robin ingênuo — não considera saúde ou carga das instâncias; (3) sem health check nativo — DNS retorna todos os registros, mesmo para instâncias mortas; (4) sem metadados — DNS retorna IPs, não versões, regiões, ou outros atributos para seleção.
Como o Kubernetes contorna: em vez de expor IPs de pods diretamente, o Kubernetes usa um ClusterIP virtual estável como destino do DNS. O kube-proxy mantém regras iptables/IPVS que mapeiam o ClusterIP para os IPs dos pods saudáveis (via Readiness Probe). O DNS do serviço nunca muda — o ClusterIP é fixo durante a vida do Service. A atualização dos endpoints é feita pelo Endpoints controller, não pelo DNS. Assim o TTL do DNS não importa.
Quando o DNS ainda é limitante em Kubernetes: load balancing L4 via kube-proxy não funciona bem com gRPC (que mantém uma única conexão HTTP/2 long-lived — todas as requisições vão para a mesma instância). Para gRPC, use headless Service + client-side load balancing ou Envoy/Service Mesh.
O que é um headless Service no Kubernetes e quando ele é necessário?
Headless Service: um Kubernetes Service com clusterIP: None. Sem VIP. O kube-dns resolve o nome DNS diretamente para os IPs dos pods (múltiplos A records), em vez de para um ClusterIP virtual. O cliente recebe todos os IPs e escolhe (ou usa o primeiro, dependendo da implementação DNS do SO).
Quando é necessário: (1) StatefulSets — cada pod precisa de identidade estável e endereçamento individual. O headless Service cria entradas DNS pod-name.service-name.namespace.svc.cluster.local para cada pod. Kafka, Cassandra, Elasticsearch e bancos de dados em Kubernetes usam isso; (2) gRPC — clientes gRPC precisam dos IPs individuais para distribuir chamadas entre conexões (cada stream HTTP/2 vai para uma instância); (3) client-side load balancing dentro do cluster — quando o cliente quer controlar a seleção e precisa de todos os IPs disponíveis.
Diferença chave: ClusterIP Service → transparente, sem estado, stateless load balancing. Headless Service → o cliente vê as instâncias individuais, adequado para stateful workloads que precisam de identidade.
O que é o problema de stale endpoints em Kubernetes e como resolvê-lo corretamente?
O problema: quando um pod é encerrado (por scale down, rolling update, ou crash), existe uma janela de tempo entre o pod parar de receber tráfego e o kube-proxy/envoy terem propagado a remoção do endpoint. Nessa janela, requisições são roteadas para o pod morto e recebem 502/connection refused.
Por que acontece: o encerramento do pod (SIGTERM) e a remoção do endpoint do kube-proxy são operações assíncronas e paralelas no Kubernetes. O pod pode terminar antes do kube-proxy ter atualizado as regras iptables/IPVS em todos os nós.
Solução correta: preStop hook com sleep 5-15s. O preStop executa antes do SIGTERM ser enviado ao processo. Durante o sleep, o Endpoints controller já removeu o pod do EndpointSlice e o kube-proxy propagou a remoção. Quando o SIGTERM chega, o pod já não está mais recebendo novas conexões e pode drenar as existentes até o terminationGracePeriodSeconds expirar. Combine com readiness probe que falha imediatamente no shutdown para acelerar a remoção do endpoint.
Como health checks se integram ao service discovery e qual o risco de health checks mal configurados?
Integração com discovery: o service registry usa health checks para saber quais instâncias estão aptas a receber tráfego. Em Kubernetes, o readinessProbe controla se o pod aparece nos Endpoints do Service. Em Consul, health checks HTTP/TCP são executados periodicamente; instâncias que falham são marcadas como critical e removidas do DNS. Sem health checks, instâncias mortas ficam no pool de discovery até o registro expirar manualmente.
Riscos de má configuração: (1) initialDelaySeconds muito curto — o pod é marcado como pronto antes de estar realmente inicializado (conexões ao banco ainda sendo estabelecidas, caches aquecendo); (2) timeout muito longo — instâncias lentas mas não mortas ficam no pool e recebem tráfego; (3) health check que depende de dependências externas — se o banco estiver lento, o serviço é removido do pool mesmo funcionando, causando avalanche de remoções desnecessárias; (4) liveness probe que mata o pod em vez de readiness probe que o remove do pool — livenessProbe deve ser reservada apenas para deadlocks irrecuperáveis.
Regra prática: readinessProbe checa se o serviço consegue responder requisições corretamente (é saudável o suficiente para receber tráfego). livenessProbe checa se o processo está vivo e não em deadlock (critério muito mais conservador para reiniciar).
Referências
- artigo Pattern: Client-side service discovery — Chris Richardson, microservices.io.
- docs Kubernetes Services — DNS for Services and Pods — Kubernetes Documentation.
- docs Consul Service Discovery — HashiCorp Developer.
- artigo Kubernetes best practices: terminating with grace — Google Cloud Blog.
- livro Production Kubernetes — Josh Rosso et al. (O'Reilly, 2021).
- artigo Pattern: Server-side service discovery — Chris Richardson, microservices.io.
- docs Kubernetes EndpointSlices — Kubernetes Documentation.
- docs Envoy Service Discovery Service (xDS API) — Envoy Proxy Documentation.
- paper ZooKeeper: Wait-free Coordination for Internet-scale Systems — Hunt et al. (USENIX ATC, 2010).
- blog A Guide to the Kubernetes Networking Model — Kevin Sookocheff.
- artigo CAP Theorem and Service Discovery — HashiCorp Blog.
- docs Kubernetes Ingress Controllers — Comparison — Kubernetes Documentation.