18.4 Service Discovery
TL;DR Service Discovery é o mecanismo para achar microsserviços automaticamente num sistema distribuído. Tipos: Client-Side (o cliente acha o serviço no Registry) e Server-Side (o Load Balancer faz o discovery). Consul: registro via HTTP API, health checks, client-side load balancing. Kubernetes: DNS-based discovery automático. Health checks: liveness (vivo?) e readiness (pronto?). Fallback para config estático se o Registry cair.
Conteúdo
- O que é
- Tipos de Service Discovery
- Consul (HashiCorp)
- Kubernetes Service Discovery
- Eureka (Netflix)
- Laravel Package para Service Discovery
- Caching Service Discovery
- Graceful Degradation
- Health Checks
- Boas práticas
- Alternativas
- Exercícios práticos
O que é
Service Discovery: Mecanismo para achar automaticamente o endereço de rede dos serviços num sistema distribuído.
Problema: Em arquitetura de microsserviços os serviços sobem e descem o tempo todo. IP e porta mudam.
Antes (monolito):
Order Service → http://payment-service:8080
Agora (microsserviços):
Order Service → Payment Service (onde está?)
├ instance-1: 10.0.1.5:8080
├ instance-2: 10.0.1.7:8080 (autoscaling adicionou)
└ instance-3: 10.0.1.9:8080Para quê:
- Roteamento dinâmico
- Autoscaling (adicionar/remover instances)
- Health checks
- Load balancing
Tipos de Service Discovery
1. Client-Side Discovery
O cliente acha o serviço sozinho:
1. Client → Service Registry: "Onde está o Payment Service?"
2. Service Registry → Client: [10.0.1.5:8080, 10.0.1.7:8080]
3. Client escolhe a instance (client-side load balancing)
4. Client → Payment Service instanceExemplos: Consul, Eureka
2. Server-Side Discovery
O Load Balancer acha o serviço:
1. Client → Load Balancer
2. Load Balancer → Service Registry: "Onde está o Payment Service?"
3. Service Registry → Load Balancer: [instances]
4. Load Balancer escolhe a instance
5. Load Balancer → Payment Service instanceExemplos: Kubernetes Service, AWS ELB, Nginx
Consul (HashiCorp)
O que é: Service mesh com service discovery, health checks, KV store.
Instalação:
docker run -d --name=consul \
-p 8500:8500 \
-p 8600:8600/udp \
consul agent -server -ui -bootstrap-expect=1 -client=0.0.0.0Web UI: http://localhost:8500
Registro do serviço
Laravel Service Provider:
// app/Providers/ConsulServiceProvider.php
class ConsulServiceProvider extends ServiceProvider
{
public function boot()
{
$this->registerService();
}
private function registerService()
{
$serviceId = config('app.name') . '-' . gethostname();
$data = [
'ID' => $serviceId,
'Name' => config('app.name'),
'Address' => gethostname(),
'Port' => (int) config('app.port', 8000),
'Check' => [
'HTTP' => url('/health'),
'Interval' => '10s',
'Timeout' => '5s',
],
];
Http::put('http://consul:8500/v1/agent/service/register', $data);
// Deregister no shutdown
register_shutdown_function(function () use ($serviceId) {
Http::put("http://consul:8500/v1/agent/service/deregister/{$serviceId}");
});
}
}Health Check Endpoint:
// routes/web.php
Route::get('/health', function () {
// Checar banco, Redis, etc.
try {
DB::connection()->getPdo();
Redis::ping();
return response()->json(['status' => 'healthy']);
} catch (Exception $e) {
return response()->json(['status' => 'unhealthy'], 503);
}
});Service Discovery (obter o serviço)
class ConsulServiceDiscovery
{
public function getService(string $serviceName): ?string
{
// Pegar todas as instances healthy
$response = Http::get("http://consul:8500/v1/health/service/{$serviceName}", [
'passing' => true, // só healthy
]);
$instances = $response->json();
if (empty($instances)) {
throw new ServiceNotFoundException("Serviço {$serviceName} não encontrado");
}
// Client-side load balancing (random)
$instance = $instances[array_rand($instances)];
$address = $instance['Service']['Address'];
$port = $instance['Service']['Port'];
return "http://{$address}:{$port}";
}
}
// Uso
$discovery = new ConsulServiceDiscovery();
$paymentUrl = $discovery->getService('payment-service');
$response = Http::post("{$paymentUrl}/api/charge", [
'amount' => 100,
]);Load Balancing Strategies
Round Robin:
class ConsulServiceDiscovery
{
private array $counters = [];
public function getService(string $serviceName): string
{
$instances = $this->getInstances($serviceName);
// Round Robin
if (!isset($this->counters[$serviceName])) {
$this->counters[$serviceName] = 0;
}
$index = $this->counters[$serviceName] % count($instances);
$this->counters[$serviceName]++;
$instance = $instances[$index];
return "http://{$instance['address']}:{$instance['port']}";
}
}Weighted (por carga):
public function getService(string $serviceName): string
{
$instances = $this->getInstances($serviceName);
// Escolher a instance com menor carga
usort($instances, fn($a, $b) => $a['load'] <=> $b['load']);
$instance = $instances[0];
return "http://{$instance['address']}:{$instance['port']}";
}Kubernetes Service Discovery
Kubernetes DNS:
# payment-service deployment
apiVersion: v1
kind: Service
metadata:
name: payment-service
spec:
selector:
app: payment
ports:
- port: 80
targetPort: 8000DNS name: payment-service.default.svc.cluster.local
Laravel:
// Só use o nome do serviço
$response = Http::post('http://payment-service/api/charge', [
'amount' => 100,
]);
// Kubernetes resolve o DNS e faz o balance sozinhoComo funciona:
- Kubernetes cria o registro DNS do Service
- O registro DNS aponta para o ClusterIP
- kube-proxy faz o balance entre os Pods
Eureka (Netflix)
Ecossistema Spring Boot, para Java/Kotlin.
Não existe cliente PHP, mas dá para usar a REST API:
// Registro no Eureka
Http::post('http://eureka:8761/eureka/apps/PAYMENT-SERVICE', [
'instance' => [
'hostName' => gethostname(),
'app' => 'PAYMENT-SERVICE',
'ipAddr' => '10.0.1.5',
'port' => ['$' => 8000, '@enabled' => true],
'healthCheckUrl' => url('/health'),
],
]);
// Pegar as instances
$response = Http::get('http://eureka:8761/eureka/apps/PAYMENT-SERVICE');
$instances = $response->json()['application']['instance'];Laravel Package para Service Discovery
Composer:
composer require illuminate/http-clientService Provider:
class ServiceDiscoveryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(ServiceDiscovery::class, function () {
$driver = config('services.discovery.driver');
return match ($driver) {
'consul' => new ConsulServiceDiscovery(),
'kubernetes' => new KubernetesServiceDiscovery(),
'static' => new StaticServiceDiscovery(),
default => throw new Exception("Driver desconhecido: {$driver}"),
};
});
}
}
// config/services.php
'discovery' => [
'driver' => env('SERVICE_DISCOVERY_DRIVER', 'consul'),
'consul' => [
'host' => env('CONSUL_HOST', 'consul:8500'),
],
],Uso:
class PaymentService
{
public function __construct(private ServiceDiscovery $discovery) {}
public function charge(int $amount): Payment
{
$url = $this->discovery->getService('payment-service');
$response = Http::post("{$url}/api/charge", ['amount' => $amount]);
return new Payment($response->json());
}
}Caching Service Discovery
Problema: Cada request no Consul/Eureka deixa o sistema mais lento.
Solução: Cache
class CachedServiceDiscovery implements ServiceDiscovery
{
public function __construct(
private ServiceDiscovery $discovery,
private CacheInterface $cache
) {}
public function getService(string $serviceName): string
{
$cacheKey = "service_discovery:{$serviceName}";
return $this->cache->remember($cacheKey, 60, function () use ($serviceName) {
return $this->discovery->getService($serviceName);
});
}
}Graceful Degradation
Fallback quando o Service Registry está indisponível:
class ResilientServiceDiscovery implements ServiceDiscovery
{
public function getService(string $serviceName): string
{
try {
// Tenta pelo Consul
return $this->consulDiscovery->getService($serviceName);
} catch (ConsulUnavailableException $e) {
// Fallback: config estático
Log::warning("Consul indisponível, usando config estático");
return config("services.static.{$serviceName}");
}
}
}
// config/services.php
'static' => [
'payment-service' => 'http://payment.internal:8080',
'user-service' => 'http://user.internal:8080',
],Health Checks
Tipos:
1. Liveness Probe
Checagem: "o serviço está vivo?"
// /health/live
Route::get('/health/live', function () {
return response()->json(['status' => 'alive']);
});2. Readiness Probe
Checagem: "o serviço está pronto para receber requests?"
// /health/ready
Route::get('/health/ready', function () {
try {
// Checar as dependências
DB::connection()->getPdo();
Redis::ping();
return response()->json(['status' => 'ready']);
} catch (Exception $e) {
return response()->json(['status' => 'not ready'], 503);
}
});Boas práticas
✓ Health checks com dependências (banco, Redis, etc.)
✓ Graceful shutdown (deregister antes de parar)
✓ Client-side caching (não spammar o Service Registry)
✓ Fallback para config estático
✓ Retry com exponential backoff
✓ Load balancing strategy (round robin, weighted)
✓ Monitoring: eventos de registro/deregistration
✓ TTL nos health checksAlternativas
Para casos simples:
1. Env Variables
PAYMENT_SERVICE_URL=http://payment-service:8080
USER_SERVICE_URL=http://user-service:8080Http::post(config('services.payment.url') . '/api/charge');Prós:
- ✅ Simples
- ✅ Sem dependências
Contras:
- ❌ Sem autoscaling
- ❌ Sem health checks
- ❌ Precisa de redeploy quando muda
2. DNS Round Robin
payment-service.example.com → 10.0.1.5
payment-service.example.com → 10.0.1.7
payment-service.example.com → 10.0.1.9Http::post('http://payment-service.example.com/api/charge');Prós:
- ✅ Load balancing simples
Contras:
- ❌ Sem health checks (pode rotear para instance morta)
- ❌ Problemas de DNS caching
Exercícios práticos
Exercício 1: Consul Service Registration
Enunciado: Crie um Service Provider para registrar o app Laravel no Consul automaticamente na subida.
Solução:
// app/Providers/ConsulServiceProvider.php
namespace App\Providers;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
class ConsulServiceProvider extends ServiceProvider
{
public function boot()
{
if (!app()->environment('local')) {
$this->registerInConsul();
$this->deregisterOnShutdown();
}
}
private function registerInConsul(): void
{
$serviceId = config('app.name') . '-' . gethostname();
$port = config('app.port', 8000);
$data = [
'ID' => $serviceId,
'Name' => config('app.name'),
'Address' => gethostbyname(gethostname()),
'Port' => (int) $port,
'Check' => [
'HTTP' => url('/health/ready'),
'Interval' => '10s',
'Timeout' => '5s',
'DeregisterCriticalServiceAfter' => '30s',
],
'Tags' => ['laravel', 'api', config('app.env')],
];
try {
Http::put('http://consul:8500/v1/agent/service/register', $data);
logger()->info("Registrado no Consul como {$serviceId}");
} catch (\Exception $e) {
logger()->error("Falha ao registrar no Consul: {$e->getMessage()}");
}
}
private function deregisterOnShutdown(): void
{
$serviceId = config('app.name') . '-' . gethostname();
register_shutdown_function(function () use ($serviceId) {
try {
Http::put("http://consul:8500/v1/agent/service/deregister/{$serviceId}");
logger()->info("Deregistrado do Consul: {$serviceId}");
} catch (\Exception $e) {
logger()->error("Falha ao fazer deregister no Consul: {$e->getMessage()}");
}
});
}
}
// config/app.php — adicionar em providers
'providers' => [
// ...
App\Providers\ConsulServiceProvider::class,
],Exercício 2: Service Discovery com cache
Enunciado: Implemente um cliente de Service Discovery com cache dos resultados e round-robin load balancing.
Solução:
class ConsulServiceDiscovery
{
private array $counters = [];
public function getServiceUrl(string $serviceName): string
{
$cacheKey = "service_discovery:{$serviceName}";
// Cache de 60 segundos
$instances = Cache::remember($cacheKey, 60, function () use ($serviceName) {
return $this->fetchHealthyInstances($serviceName);
});
if (empty($instances)) {
throw new ServiceNotFoundException("Serviço {$serviceName} não encontrado");
}
// Round-robin load balancing
if (!isset($this->counters[$serviceName])) {
$this->counters[$serviceName] = 0;
}
$index = $this->counters[$serviceName] % count($instances);
$this->counters[$serviceName]++;
$instance = $instances[$index];
return "http://{$instance['address']}:{$instance['port']}";
}
private function fetchHealthyInstances(string $serviceName): array
{
try {
$response = Http::timeout(3)->get("http://consul:8500/v1/health/service/{$serviceName}", [
'passing' => true, // só instances healthy
]);
return collect($response->json())->map(function ($item) {
return [
'address' => $item['Service']['Address'],
'port' => $item['Service']['Port'],
];
})->toArray();
} catch (\Exception $e) {
logger()->error("Falha ao buscar o serviço no Consul: {$e->getMessage()}");
return [];
}
}
}
// Uso
$discovery = app(ConsulServiceDiscovery::class);
$url = $discovery->getServiceUrl('payment-service');
$response = Http::post("{$url}/api/charge", ['amount' => 100]);Exercício 3: Health Check Endpoints
Enunciado: Crie endpoints de health check para liveness e readiness.
Solução:
// routes/web.php
Route::get('/health/live', [HealthController::class, 'live']);
Route::get('/health/ready', [HealthController::class, 'ready']);
// app/Http/Controllers/HealthController.php
class HealthController extends Controller
{
// Liveness: o app está vivo?
public function live()
{
return response()->json([
'status' => 'alive',
'timestamp' => now()->toIso8601String(),
]);
}
// Readiness: pronto para receber requests?
public function ready()
{
$checks = [
'database' => $this->checkDatabase(),
'redis' => $this->checkRedis(),
'queue' => $this->checkQueue(),
];
$allHealthy = collect($checks)->every(fn($check) => $check === true);
return response()->json([
'status' => $allHealthy ? 'ready' : 'not ready',
'checks' => $checks,
'timestamp' => now()->toIso8601String(),
], $allHealthy ? 200 : 503);
}
private function checkDatabase(): bool
{
try {
DB::connection()->getPdo();
return true;
} catch (\Exception $e) {
logger()->error("Health check do banco falhou: {$e->getMessage()}");
return false;
}
}
private function checkRedis(): bool
{
try {
Redis::ping();
return true;
} catch (\Exception $e) {
logger()->error("Health check do Redis falhou: {$e->getMessage()}");
return false;
}
}
private function checkQueue(): bool
{
try {
// Checar se o queue worker está rodando
return Cache::store('redis')->get('queue:heartbeat', 0) > (time() - 60);
} catch (\Exception $e) {
return false;
}
}
}Na entrevista
"Service Discovery é o mecanismo para achar serviços automaticamente em microsserviços. Tipos: Client-Side (o cliente pega a lista de instances no Registry e escolhe) e Server-Side (o Load Balancer faz o discovery). Consul: registro via HTTP API, health checks, client-side load balancing (random, round robin, weighted). Kubernetes: DNS-based discovery, o Service resolve sozinho no ClusterIP. Laravel: Service Provider registra o serviço na subida, deregister no shutdown, cache nos requests de discovery. Health checks: liveness (está vivo?), readiness (pronto para receber requests?). Best practices: caching, fallback para config estático, graceful shutdown, monitoring."
Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate