Declaração do Problema
Precisa de conceber e implementar uma arquitetura de microservices escalável e fácil de manter, capaz de lidar com o aumento de tráfego, suportar deployments independentes e manter a consistência de dados entre serviços.
Pré-requisitos
- Compreensão de contentorização (Docker)
- Um cluster Kubernetes ou uma plataforma de orquestração de contentores
- Um message broker (RabbitMQ, Apache Kafka)
- Um API Gateway ou Ingress Controller
Visão Geral da Arquitetura
┌─────────────────────────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway / Ingress │
└─────────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Service │ │ Service │ │ Service │
│ A │ │ B │ │ C │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌──────────────▼──────────────┐
│ Message Broker │
│ (RabbitMQ / Kafka) │
└─────────────────────────────┘
Passo 1: Definir Limites de Serviço
Abordagem de Domain-Driven Design
Cada microservice deve:
- Ser proprietário dos seus dados (padrão «database per service»)
- Ter um limite de negócio claro
- Ser implementável de forma independente
- Comunicar através de APIs bem definidas
Exemplo de Estrutura de Serviço
/project
├── services/
│ ├── user-service/
│ │ ├── Dockerfile
│ │ ├── src/
│ │ └── k8s/
│ ├── order-service/
│ │ ├── Dockerfile
│ │ ├── src/
│ │ └── k8s/
│ └── notification-service/
│ ├── Dockerfile
│ ├── src/
│ └── k8s/
├── shared/
│ ├── proto/ # Definições gRPC
│ └── events/ # Esquemas de eventos
└── infrastructure/
├── docker-compose.yml
└── k8s/
Passo 2: Contentorizar Serviços com Builds Multi-Stage
Otimize as suas imagens Docker para produção:
# Fase de build
FROM node:alpine as builder
WORKDIR '/app'
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
# Fase de execução: imagem mínima
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Para serviços backend com linguagens compiladas (Rust, Go):
# Etapa de build
FROM rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
# Etapa de runtime
FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myservice /usr/local/bin/
EXPOSE 8080
CMD ["myservice"]
Passo 3: Implementar Comunicação Orientada a Eventos
Seleção do Message Broker
- RabbitMQ: Melhor para message queuing tradicional; fácil de utilizar
- Apache Kafka: Melhor para event streaming de elevado throughput e cenários intensivos em dados
Definição do Esquema de Eventos
{
"eventType": "OrderCreated",
"eventId": "uuid-v4",
"timestamp": "2024-01-15T10:30:00Z",
"version": "1.0",
"payload": {
"orderId": "12345",
"userId": "user-789",
"items": [],
"total": 99.99
}
}
Padrões-Chave
- Event Sourcing: Armazenar alterações de estado como uma sequência de eventos
- CQRS: Separar modelos de leitura e escrita para escalabilidade
- Saga Pattern: Gerir transações distribuídas entre serviços
Passo 4: Fazer Deploy para Kubernetes
Configuração Mínima de Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: myregistry/user-service:v1.0.0
ports:
- containerPort: 8080
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
Configuração de Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-gateway
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: api.example.com
http:
paths:
- path: /users(/|$)(.*)
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
- path: /orders(/|$)(.*)
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
Passo 5: Implementar Service Discovery e Comunicação
Comunicação Interna entre Serviços
Os serviços dentro de Kubernetes podem comunicar usando DNS:
http://user-service.default.svc.cluster.local/api/users
Circuit Breaker Pattern
Implemente resiliência com circuit breakers para evitar falhas em cascata:
const CircuitBreaker = require('opossum');
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000
};
const breaker = new CircuitBreaker(callExternalService, options);
breaker.fallback(() => cachedResponse);
breaker.on('open', () => console.log('Circuit opened'));
breaker.on('halfOpen', () => console.log('Circuit half-opened'));
Passo 6: Configurar Segurança da Comunicação entre Pods
Network Policies
Limite quais os pods que podem comunicar:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: user-service-policy
spec:
podSelector:
matchLabels:
app: user-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: user-database
Service Mesh (Opcional)
Para gestão avançada de tráfego, utilize Istio:
- Encriptação Mutual TLS (mTLS) entre pods
- Traffic shaping e canary deployments
- Observability e tracing
Definir Limites de Serviço: A Parte Difícil
A parte mais difícil de microservices é decidir onde traçar os limites. Limites mal definidos criam monólitos distribuídos — toda a complexidade de microservices sem nenhum dos benefícios.
Diretrizes para Bons Limites
- Alinhar com capacidades de negócio: Os serviços devem mapear para funções de negócio, não para camadas técnicas. Não crie um «database service» — crie um «order service» que seja proprietário dos dados de encomendas.
- Minimizar transações entre serviços: Se as operações frequentemente abrangem vários serviços, os seus limites podem estar errados.
- Considerar a estrutura da equipa: A Lei de Conway sugere que a arquitetura do sistema espelha a estrutura organizacional. Os serviços devem ser propriedade de equipas únicas.
- Começar de forma mais abrangente, dividir mais tarde: Comece com serviços maiores e decomponha quando a complexidade o justificar. A decomposição prematura cria overhead desnecessário.
Estratégias de Gestão de Dados
Cada microservice deve ser proprietário dos seus dados, mas isto cria desafios:
Database per Service: Cada serviço tem a sua própria instância de base de dados. Isolamento máximo, mas aumenta o overhead operacional.
Schema per Service: Os serviços partilham um cluster de base de dados, mas têm schemas isolados. Mais fácil de operar, mas exige disciplina para evitar queries entre schemas.
Data Replication: Serviços que precisam de dados de outros serviços mantêm cópias locais, sincronizadas através de eventos. Melhora o desempenho de leitura e a resiliência, ao custo de consistência eventual.
Modos de Falha e Resiliência
Sistemas distribuídos falham de formas que monólitos não conseguem. Conceba a pensar na falha:
Circuit Breakers: Pare de chamar serviços em falha para evitar falhas em cascata. Após um limiar de falhas, o circuito abre e as chamadas falham rapidamente.
Timeouts: Todas as chamadas externas precisam de um timeout. Sem eles, serviços lentos consomem recursos indefinidamente.
Retries with Backoff: Falhas transitórias devem desencadear retries, mas com backoff exponencial para evitar «thundering herds».
Bulkheads: Isole falhas para evitar que um serviço com mau comportamento consuma todos os recursos. Os limites de recursos de Kubernetes fornecem bulkheading básico.
A Mentalidade de Microservices de um Senior
Um monólito bem desenhado frequentemente supera uma arquitetura de microservices mal desenhada. Escolha microservices quando os benefícios de deployment independente, scaling e autonomia das equipas superarem a complexidade operacional que introduzem.
Comece com limites de serviço claros alinhados com domínios de negócio. Use comunicação assíncrona através de message brokers sempre que possível. Implemente health checks abrangentes e limites de recursos. Automatize tudo através de pipelines de CI/CD.
Deixe a sua arquitetura evoluir com base em pontos de dor reais, em vez de problemas antecipados.
Checklist de Boas Práticas
- [ ] Cada serviço tem a sua própria base de dados
- [ ] Os serviços comunicam via eventos para baixo acoplamento
- [ ] Todos os serviços têm endpoints de health e readiness
- [ ] Estão definidos limites de recursos para todos os contentores
- [ ] As network policies restringem comunicação desnecessária
- [ ] Estão configurados logging e monitoring centralizados
- [ ] Os secrets são geridos de forma segura (não no código)
- [ ] Pipelines de CI/CD permitem deployments independentes
- [ ] Está definida uma estratégia de versionamento de API
- [ ] Está implementado distributed tracing
- [ ] Circuit breakers e timeouts estão configurados para todas as chamadas externas
- [ ] Os limites de serviço estão alinhados com capacidades de negócio
Artigos Relacionados na Wiki