Como aplicar o padrão priority queue para processamento diferenciado por SLA

1. Fundamentos do Padrão Priority Queue e sua Relação com SLA

O padrão priority queue em sistemas distribuídos consiste em filas que organizam mensagens por níveis de urgência, onde cada elemento recebe um valor de prioridade que determina sua posição de processamento. Diferente de filas FIFO tradicionais, as priority queues ordenam as mensagens de forma que as de maior prioridade sejam consumidas primeiro, independentemente da ordem de chegada.

O SLA (Service Level Agreement) define métricas contratuais como tempo máximo de resposta, throughput mínimo e disponibilidade do serviço. O mapeamento entre prioridades e classes de SLA segue tipicamente esta estrutura:

  • Crítico: SLA < 100ms (transações financeiras, autenticação)
  • Alto: SLA < 1s (notificações em tempo real, atualizações de status)
  • Normal: SLA < 10s (processamento de dados, relatórios)
  • Baixo: sem garantia de SLA (logs, auditoria batch)

A relação entre prioridade e SLA é direta: quanto mais restritivo o SLA, maior deve ser a prioridade da fila correspondente.

2. Arquitetura Típica de uma Priority Queue para Diferenciação por SLA

A arquitetura padrão envolve três componentes principais:

  1. Produtores: sistemas que enviam mensagens com metadados de prioridade
  2. Filas múltiplas: uma fila independente por nível de SLA (critical, high, normal, low)
  3. Consumidores: workers que processam mensagens, alocados proporcionalmente às prioridades

Existem duas estratégias principais de implementação:

Fila única com ordenação: utiliza estruturas como Redis Sorted Sets, onde a pontuação (score) define a prioridade. Exemplo:

ZADD task_queue 10 "mensagem_critica"  # prioridade 10 (maior)
ZADD task_queue 1 "mensagem_baixa"     # prioridade 1 (menor)
ZPOPMIN task_queue                     # remove a de menor score (maior prioridade)

Múltiplas filas independentes: cada nível de SLA tem sua própria fila (RabbitMQ, Kafka topics). O scheduler distribui workers proporcionalmente:
- 50% workers para fila crítica
- 30% para fila alta
- 15% para fila normal
- 5% para fila baixa

3. Estratégias de Roteamento e Classificação de Mensagens por SLA

Os critérios para definir prioridade incluem:
- Origem da requisição: API pública (crítico) vs. job interno (baixo)
- Tipo de operação: pagamento (crítico) vs. relatório diário (normal)
- Cliente/tenant: clientes premium (alto) vs. gratuitos (baixo)

Implementação de classificador no produtor com regras dinâmicas:

def classify_message(message):
    if message['type'] == 'payment':
        return 'critical', 9
    elif message['client_tier'] == 'premium':
        return 'high', 7
    elif message['deadline'] < 5:  # minutos
        return 'normal', 5
    else:
        return 'low', 1

Configuração de filas com prioridade no RabbitMQ:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# Declara filas com suporte a prioridade
channel.queue_declare(queue='critical_sla', durable=True, arguments={'x-max-priority': 10})
channel.queue_declare(queue='high_sla', durable=True, arguments={'x-max-priority': 10})
channel.queue_declare(queue='normal_sla', durable=True, arguments={'x-max-priority': 10})
channel.queue_declare(queue='low_sla', durable=True, arguments={'x-max-priority': 10})

# Publica mensagem com prioridade
channel.basic_publish(
    exchange='',
    routing_key='critical_sla',
    body='{"order_id": 12345, "amount": 250.00}',
    properties=pika.BasicProperties(priority=9, delivery_mode=2)  # delivery_mode=2 persiste
)
connection.close()

4. Mecanismos de Consumo com Prioridade e Controle de Concorrência

Para evitar starvation, o consumo deve ser ponderado. Exemplo de consumidor que verifica filas em ordem de prioridade:

import pika
import time

def process_message(body):
    print(f"Processando: {body}")
    time.sleep(0.1)  # simula processamento

def consume_with_priority():
    connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
    channel = connection.channel()

    queues = ['critical', 'high', 'normal', 'low']

    while True:
        for queue in queues:
            method_frame, header_frame, body = channel.basic_get(queue=queue)
            if method_frame:
                process_message(body)
                channel.basic_ack(method_frame.delivery_tag)
                break  # processa uma mensagem por ciclo para evitar monopolização
        time.sleep(0.01)  # pequena pausa para evitar busy waiting

# Para controle de concorrência, use prefetch_count por fila
channel.basic_qos(prefetch_count=5)  # máximo 5 mensagens não confirmadas por worker

5. Tratamento de Starvation e Garantias de SLA em Cenários de Alta Carga

Starvation ocorre quando filas de baixa prioridade nunca são processadas sob pressão. Soluções:

Aging de mensagens: aumenta gradualmente a prioridade conforme o tempo na fila:

def apply_aging(message, time_in_queue_seconds):
    aging_bonus = min(time_in_queue_seconds // 30, 5)  # +1 a cada 30s, máximo +5
    return min(message['base_priority'] + aging_bonus, 10)

Janelas de processamento garantido (time-slicing): reserva um percentual mínimo de workers para cada fila:

# Algoritmo de round-robin ponderado
weights = {'critical': 50, 'high': 30, 'normal': 15, 'low': 5}
total_weight = sum(weights.values())
current_weight = 0

while True:
    for queue in queues:
        current_weight += weights[queue]
        if current_weight >= total_weight:
            current_weight -= total_weight
            # processa uma mensagem desta fila

Métricas de monitoramento essenciais:
- Tempo médio na fila por prioridade (p50, p95, p99)
- Número de violações de SLA por hora
- Backlog (quantidade de mensagens não processadas) por fila

6. Integração com Estratégias de Retry e Dead Letter Queue (DLQ)

Políticas de retry diferenciadas por SLA:
- Crítico: retry imediato até 3 vezes, depois DLQ
- Normal: retry com backoff exponencial (1s, 4s, 16s)
- Baixo: sem retry automático, vai direto para DLQ

Configuração de DLQ com preservação de prioridade no RabbitMQ:

# Declara fila principal com DLQ configurada
channel.queue_declare(
    queue='critical_sla',
    durable=True,
    arguments={
        'x-max-priority': 10,
        'x-dead-letter-exchange': 'dlx_critical',
        'x-dead-letter-routing-key': 'dlq_critical',
        'x-message-ttl': 60000  # 60s timeout para mensagens críticas
    }
)

# Declara DLQ específica para crítico
channel.queue_declare(queue='dlq_critical', durable=True, arguments={
    'x-dead-letter-exchange': 'retry_exchange',
    'x-message-ttl': 300000  # 5 minutos antes de tentar reenvio
})

# Publica mensagem com header de retry
properties = pika.BasicProperties(
    priority=9,
    delivery_mode=2,
    headers={'x-retry-count': 0, 'x-original-priority': 9}
)
channel.basic_publish(exchange='', routing_key='critical_sla', body=message, properties=properties)

7. Monitoramento e Ajuste Dinâmico de Prioridades

Métricas essenciais para Prometheus/Grafana:

# Métrica: tempo na fila por prioridade (em segundos)
# Exported via middleware ou custom exporter
queue_time_seconds{queue="critical", priority="9"} 0.045
queue_time_seconds{queue="high", priority="7"} 0.230
queue_time_seconds{queue="normal", priority="5"} 2.150
queue_time_seconds{queue="low", priority="1"} 15.800

# Consulta para p99 de latência por fila
histogram_quantile(0.99, sum(rate(queue_time_seconds_bucket{queue="critical"}[5m])) by (le))

# Alerta de violação de SLA
ALERT SLAViolationCritical
  IF queue_time_seconds{queue="critical"} > 0.1
  FOR 1m
  LABELS { severity = "critical" }
  ANNOTATIONS { summary = "SLA violado para fila crítica" }

Técnicas de ajuste dinâmico:
- Rebalanceamento automático: aumenta workers da fila mais atrasada a cada 30s
- Escalonamento horizontal: adiciona novos consumidores quando backlog > threshold
- Degradação graceful: reduz prioridade de mensagens não críticas durante picos

8. Casos de Uso e Boas Práticas para Implementação em Produção

Casos reais:
- Processamento de pagamentos: fila crítica (SLA 200ms), com retry imediato e DLG para análise manual
- Notificações push: fila alta (SLA 2s), com prioridade baseada no tipo de dispositivo
- Relatórios batch: fila baixa (sem SLA), processado apenas quando há capacidade ociosa

Boas práticas:
- Limitar a 4-5 níveis de prioridade (mais que isso aumenta complexidade sem ganho)
- Testar starvation com carga sintética: envie 10.000 mensagens de baixa prioridade antes de uma crítica
- Documentar SLAs por fila em um arquivo de configuração centralizado
- Usar correlation IDs para rastrear mensagens entre filas e DLQ

Armadilhas comuns:
- Prioridade mal configurada em middlewares (verificar se o broker suporta prioridade nativa)
- Consumo sequencial bloqueante (usar async/await ou threading)
- Esquecer de tratar mensagens rejeitadas (configurar DLQ e monitorar)

Referências