Microservices com Laravel: comunicação entre serviços

1. Introdução à Arquitetura de Microservices com Laravel

Microservices é um estilo arquitetural onde uma aplicação é composta por pequenos serviços independentes, cada um executando um processo único e se comunicando através de mecanismos leves, como HTTP ou mensageria. No ecossistema PHP/Laravel, essa abordagem permite escalar equipes e funcionalidades de forma granular, evitando os gargalos de monolitos tradicionais.

Os principais desafios incluem:
- Comunicação: como serviços trocam dados de forma confiável
- Consistência de dados: manter integridade em operações distribuídas
- Descoberta de serviços: localizar dinamicamente instâncias de serviços

As estratégias de comunicação dividem-se em síncrona (HTTP/REST) e assíncrona (filas, eventos, message brokers). Cada uma tem trade-offs específicos que exploraremos a seguir.

2. Comunicação Síncrona via HTTP com Laravel

O Laravel fornece um cliente HTTP expressivo baseado no Guzzle, ideal para consumir APIs REST entre microservices.

use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.usuarios.token'))
    ->timeout(5)
    ->retry(3, 100)
    ->get('http://api-usuarios:8001/api/usuarios/'.$userId);

if ($response->successful()) {
    $usuario = $response->json();
} else {
    Log::error('Falha ao buscar usuário', [
        'status' => $response->status(),
        'body' => $response->body()
    ]);
    // Fallback ou retorno de erro
}

Para maior resiliência, podemos implementar um Circuit Breaker com o pacote laravel-http-circuit-breaker:

use App\CircuitBreaker\CircuitBreaker;

$cb = new CircuitBreaker('servico-pagamentos', 5, 30); // 5 falhas, 30s de abertura

if ($cb->isAvailable()) {
    try {
        $response = Http::post('http://pagamentos:8002/api/cobrar', $data);
        $cb->recordSuccess();
    } catch (\Exception $e) {
        $cb->recordFailure();
        throw $e;
    }
} else {
    // Retornar resposta de fallback ou cache
}

Autenticação entre serviços pode ser feita com:
- API Tokens: simples, usando Authorization: Bearer com tokens pré-compartilhados
- JWT: com Laravel Sanctum para tokens expiráveis e claims personalizadas
- OAuth2: com Laravel Passport para fluxos mais complexos (client credentials, password grant)

Exemplo com Sanctum:

// Serviço A (cliente)
$token = 'servico-a-token-secreto';
$response = Http::withToken($token)->post('http://servico-b/api/dados');

// Serviço B (servidor)
Route::middleware('auth:sanctum')->get('/api/dados', function () {
    return auth()->user();
});

3. Comunicação Assíncrona com Filas e Eventos

A comunicação assíncrona desacopla os serviços, aumentando a resiliência e a escalabilidade. O Laravel oferece suporte nativo a filas (Redis, SQS, RabbitMQ) e eventos.

Exemplo com Laravel Queues (Redis):

// Disparando um job para outro serviço
dispatch(new ProcessarPagamento($pedidoId))->onQueue('pagamentos');

// Job no serviço de pagamentos
class ProcessarPagamento implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle()
    {
        // Lógica de processamento
        $this->release(30); // Re-tentar após 30s em caso de falha
    }

    public function failed(\Throwable $e)
    {
        Log::error('Falha no pagamento', ['pedido' => $this->pedidoId]);
    }
}

Event-Driven Architecture com broadcasting:

// Serviço de Pedidos
class PedidoCriado
{
    public function __construct(public int $pedidoId, public array $dados) {}
}

event(new PedidoCriado($pedido->id, $pedido->toArray()));

// Serviço de Estoque (ouvinte)
class AtualizarEstoque implements ShouldQueue
{
    public function handle(PedidoCriado $event)
    {
        // Atualizar estoque baseado no pedido
    }
}

Para garantir idempotência, cada mensagem deve conter um identificador único e o serviço receptor deve verificar se já processou aquele ID:

public function handle(PedidoCriado $event)
{
    $messageId = $event->dados['message_id'] ?? uniqid();

    if (Cache::has("processed:{$messageId}")) {
        return; // Mensagem já processada
    }

    Cache::put("processed:{$messageId}", true, 3600);
    // Processar...
}

4. Message Brokers Avançados: RabbitMQ e Kafka no Laravel

Para cenários de alta throughput e garantia de entrega, utilizamos message brokers especializados.

RabbitMQ com vladimir-yuldashev/laravel-queue-rabbitmq:

// config/queue.php
'rabbitmq' => [
    'driver' => 'rabbitmq',
    'host' => env('RABBITMQ_HOST', '127.0.0.1'),
    'queue' => env('RABBITMQ_QUEUE', 'default'),
    'exchange' => 'laravel_exchange',
    'exchange_type' => 'topic',
    'routing_key' => 'pedidos.*',
],

// Enviar mensagem com routing key
dispatch(new PedidoCriado($data))->onConnection('rabbitmq')->onQueue('pedidos.criados');

Kafka com mateusjunges/laravel-kafka:

// Producer
use Junges\Kafka\Facades\Kafka;

Kafka::publishOn('pedidos-stream')
    ->withHeaders(['correlation-id' => (string) Str::uuid()])
    ->withBodyKey('pedido_id', $pedidoId)
    ->send();

// Consumer
$consumer = Kafka::consumer(['pedidos-stream'])
    ->withHandler(function (\Junges\Kafka\Message $message) {
        $correlationId = $message->getHeaders()['correlation-id'];
        $pedidoId = $message->getBody()['pedido_id'];
        // Processar...
    })
    ->build();
$consumer->consume();

Padrões importantes:
- Command Bus: cada mensagem representa um comando (ex: CriarPedido)
- Event Sourcing: armazenar eventos como fonte da verdade
- Headers de correlação: rastrear requisições através dos serviços

5. Service Discovery e Balanceamento de Carga

Em ambientes dinâmicos (Kubernetes, Docker Swarm), os serviços mudam de endereço frequentemente. Service Discovery resolve esse problema.

Implementação com Consul:

// Serviço de descoberta
use SensioLabs\Consul\ServiceFactory;

$consul = new ServiceFactory(['base_uri' => 'http://consul:8500']);
$agent = $consul->get('agent');

// Registrar serviço
$agent->registerService([
    'Name' => 'api-usuarios',
    'Address' => gethostbyname(gethostname()),
    'Port' => 8001,
    'Check' => [
        'HTTP' => 'http://localhost:8001/health',
        'Interval' => '10s'
    ]
]);

// Descobrir serviço
$health = $consul->get('health');
$services = $health->service('api-usuarios')->json();
$endpoint = $services[0]['Service']['Address'].':'.$services[0]['Service']['Port'];

Para balanceamento de carga, podemos usar laravel-service-discovery:

// config/service-discovery.php
return [
    'services' => [
        'api-usuarios' => [
            'driver' => 'consul',
            'endpoints' => [], // Preenchido dinamicamente
            'fallback' => ['http://api-usuarios-static:8001'],
            'cache_ttl' => 60, // segundos
        ],
    ],
];

// Uso
$url = ServiceDiscovery::resolve('api-usuarios');
$response = Http::get("{$url}/api/usuarios");

6. Monitoramento e Observabilidade da Comunicação

Logging centralizado com Laravel Telescope:

// Adicionar correlation ID a todas as requisições
public function boot()
{
    Request::macro('correlationId', function () {
        return $this->header('X-Request-ID') ?? (string) Str::uuid();
    });
}

// Repassar para serviços downstream
Http::macro('withCorrelation', function () {
    return $this->withHeader('X-Request-ID', request()->correlationId());
});

Métricas com Prometheus:

use Prometheus\CollectorRegistry;
use Prometheus\RenderTextFormat;

$registry = app(CollectorRegistry::class);
$counter = $registry->getOrRegisterCounter(
    'app',
    'http_requests_total',
    'Total de requisições HTTP',
    ['service', 'method', 'status']
);
$counter->incBy(1, ['api-usuarios', 'GET', '200']);

Distributed Tracing com OpenTelemetry:

use Spatie\OpenTelemetry\Facades\Tracer;

Tracer::startSpan('processar-pagamento')
    ->setAttribute('pedido.id', $pedidoId)
    ->setAttribute('servico.origem', 'api-pedidos');

try {
    // Lógica...
} finally {
    Tracer::endSpan();
}

7. Tratamento de Erros e Resiliência

Exponential Backoff com Jitter:

public function retryWithBackoff(callable $operation, int $maxRetries = 3): mixed
{
    $attempt = 0;
    while (true) {
        try {
            return $operation();
        } catch (\Exception $e) {
            $attempt++;
            if ($attempt > $maxRetries) throw $e;

            $delay = min(1000 * pow(2, $attempt) + random_int(0, 500), 10000);
            usleep($delay * 1000);
        }
    }
}

Sagas para transações distribuídas:

// Serviço de Pedidos
class CriarPedidoSaga
{
    public function execute(array $data)
    {
        DB::beginTransaction();
        try {
            $pedido = Pedido::create($data);

            // Chamada compensável
            $this->reservarEstoque($pedido);

            // Chamada compensável
            $this->cobrarPagamento($pedido);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            // Executar compensações
            $this->compensarReserva($pedido);
            $this->compensarPagamento($pedido);
            throw $e;
        }
    }
}

8. Boas Práticas e Padrões de Projeto

API First com Contratos:

// Interface compartilhada entre serviços (via pacote)
interface ServicoUsuariosContract
{
    public function buscarUsuario(int $id): array;
    public function criarUsuario(array $dados): array;
}

// Implementação no serviço de usuários
class ServicoUsuariosController implements ServicoUsuariosContract
{
    public function buscarUsuario(int $id): array
    {
        return Usuario::findOrFail($id)->toArray();
    }
}

Testes de integração com mocks:

public function test_pedido_integra_com_estoque()
{
    Http::fake([
        'api-estoque:8003/*' => Http::response(['reservado' => true], 200),
    ]);

    $response = $this->postJson('/api/pedidos', ['produto_id' => 1]);
    $response->assertStatus(201);
}

Documentação viva com OpenAPI:

/**
 * @OA\Post(
 *     path="/api/pedidos",
 *     @OA\RequestBody(
 *         @OA\JsonContent(ref="#/components/schemas/PedidoRequest")
 *     ),
 *     @OA\Response(response=201, ref="#/components/responses/PedidoResponse")
 * )
 */
public function store(Request $request) { ... }

Com essas práticas, sua arquitetura de microservices em Laravel será robusta, escalável e de fácil manutenção. A escolha entre comunicação síncrona e assíncrona deve considerar os requisitos de latência, consistência e tolerância a falhas de cada contexto.

Referências