DEV Community

Denis Bonate
Denis Bonate

Posted on

Arquitetura Magento (MageOS) no EKS com KEDA. Do Monolito à Elasticidade Extrema:

Migrar uma plataforma robusta como o Magento (MageOS) para Kubernetes é apenas o começo. O verdadeiro desafio é fazer essa arquitetura ser eficiente, escalável e barata.

Neste post, vou compartilhar como migramos nossa operação de e-commerce da Adobe Cloud para o Amazon EKS e, principalmente, como resolvemos o maior gargalo de performance do Magento — o processamento de filas — utilizando KEDA (Kubernetes Event-driven Autoscaling).

O Desafio

O Magento é tradicionalmente uma aplicação monolítica voraz. Nossos principais problemas eram:

Processamento de Filas Lento: O auto-scaling nativo do Kubernetes (HPA) baseado em CPU/Memória é lento demais para reagir a picos de mensagens no RabbitMQ.

Desperdício de Recursos: Manter servidores de consumers ligados 24/7 com 16GB de RAM para processar filas que ficam vazias na madrugada.

Deploy Lento e Frágil: Dependências de banco de dados durante o build da imagem.
Enter fullscreen mode Exit fullscreen mode

Aqui está como resolvemos isso, camada por camada.

1. A Fundação: Imagem Base e Build

Optamos por uma estratégia de imagem "All-in-One", onde a mesma imagem Docker contém o Nginx, o PHP-FPM e o código da aplicação. O comportamento do pod é definido apenas no momento da inicialização (runtime).

A Imagem Base

Criamos uma imagem base otimizada contendo apenas as dependências do sistema. Aqui ajustamos o php.ini e o nginx.conf para alta performance:

PHP Tuning: Ajustamos opcache.validate_timestamps=0 (para performance máxima em prod) e aumentamos limites críticos como upload_max_filesize e post_max_size para 10GB (necessário para nosso gerenciador de vídeos).

Nginx: Configuramos timeouts agressivos e aumentamos o client_max_body_size.
Enter fullscreen mode Exit fullscreen mode

O Build da Aplicação (CI)

No nosso pipeline (GitLab CI), o Dockerfile herda da imagem base e foca apenas na construção dos artefatos estáticos.

Dockerfile simplificado

FROM nossa-imagem-base:latest as builder

pasta 'vendor'
USER root
WORKDIR /app
COPY composer.json composer.lock ./

RUN composer install --no-dev --no-interaction --optimize-autoloader
COPY . .
RUN chown -R www-data:www-data .

FROM nossa-imagem-base:latest

WORKDIR /var/www/html
USER root


COPY --chown=www-data:www-data --from=builder /app .

# Copia o env.php, precisa conter no diretorio
COPY --chown=www-data:www-data env.php app/etc/env.php

USER www-data

# Agrupamos todos os comandos de deploy em um único RUN para otimização
RUN set -ex && \
    # Sincroniza o banco de dados com o código
    echo "Atualizando schema do banco de dados..." && \
    php -d memory_limit=-1 bin/magento setup:upgrade --keep-generated && \
    # Gera os arquivos estáticos (CSS/JS)
    echo "Gerando conteúdo estático..." && \
    php -d memory_limit=-1 bin/magento setup:static-content:deploy pt_BR en_US -f --jobs 4 && \
    # Compila o código de Injeção de Dependência
    echo "Compilando DI..." && \
    php -d memory_limit=-1 bin/magento setup:di:compile && \
    # Limpa o cache
    echo "Limpando cache..." && \
    php -d memory_limit=-1 bin/magento cache:flush

USER root
# Usa o entrypoint da imagem base, que inicia o supervisor (Nginx + PHP-FPM)
Enter fullscreen mode Exit fullscreen mode

2. Segregação de Workloads no Kubernetes

Em vez de tratar todos os pods como iguais, dividimos o Magento em "perfis" usando Argo Rollouts. Usamos a mesma imagem Docker, mas mudamos o command e args de inicialização.

Perfil 1: Web e Admin (Frontend)

Esses pods atendem o tráfego HTTP. O tráfego é roteado via AWS ALB (Ingress).

Inicialização: Usamos o supervisord para rodar Nginx e PHP-FPM juntos no mesmo container.

Comando: exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
Enter fullscreen mode Exit fullscreen mode

Perfil 2: Cron Daemon (Scheduler)

O Magento precisa do cron rodando a cada 6 minutos.
CronJob do Kubernetes: Que sobe e inicializa o script:

- |
              set -e
              echo "Preparando diretórios de log..."
              mkdir -p var/log
              touch var/log/cron.log

              echo "--------------------------------"
              echo "Executando Comando Magento CRON..."
              php -d memory_limit=-1 bin/magento cron:run >> var/log/magento.cron.log 2>&1

              sleep 240
              echo "--------------------------------"
              echo "--- LOGS CRON ---"
              cat var/log/cron.log
              echo "--- FIM DOS LOGS CRON ---"
Enter fullscreen mode Exit fullscreen mode
Truque: Configuramos 'cron_run' => false no env.php para impedir que este pod tente processar filas, evitando conflitos.
Enter fullscreen mode Exit fullscreen mode

Perfil 3: Consumers (Workers)

Aqui está a "cereja do bolo". Esses pods não recebem tráfego HTTP; eles apenas processam mensagens do RabbitMQ.

3. Auto-Scaling Inteligente com KEDA

O HPA padrão do Kubernetes não sabe quantas mensagens tem na fila. O KEDA sabe (duplicamos filas que contém no env.php para manifest do Keda).

Implementamos o KEDA para monitorar nossas filas no Amazon MQ (RabbitMQ) e escalar os pods de consumers, por exemplo de 0 a 20 réplicas automaticamente.

A Configuração do ScaledObject

O segredo para a estabilidade foi o ajuste fino do comportamento de escala (behavior):
YAML

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: NOME_SCALE_OBJECT
  lables:
    app: NOME_POD_CONSUMER_QUE_SERA_ESCALADO
spec:
  scaleTargetRef:
    apiVersion: argoproj.io/v1alpha1
    kind: Rollout
    name: NOME_POD_CONSUMER_QUE_SERA_ESCALADO
  # Configuração de Zero-Scaling (Dormir/Acordar)
  minReplicaCount: 0  # Escala a ZERO quando não há trabalho (Economia!)
  maxReplicaCount: 20

  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 300 # Espera 5 min vazio antes de desligar
          policies:
          - periodSeconds: 15
            type: Percent
            value: 100
        scaleUp:
          stabilizationWindowSeconds: 0 # Sobe imediatamente
          policies:
          - periodSeconds: 15
            type: Percent
            value: 100
          - periodSeconds: 15
            type: Pods
            value: 4 # Adiciona até 4 pods de uma vez no pico
  triggers:
  # FILA 1
  - type: rabbitmq
    authenticationRef:
      name: keda-trigger-auth-rabbitmq
    metadata:
      mode: QueueLength
      queueName: NOME_FILA_RABBITMQ
      value: "10000"
      activationThreshold: "1" # Acorda com 1 mensagem
      host: "amqps://rabbit.mq.us-east-1.on.aws:5671//vhost"
      tls: "enable"
  # FILA 2...

Enter fullscreen mode Exit fullscreen mode

O Desafio da Autenticação na AWS (O "Pulo do Gato")

O RabbitMQ na AWS usa strings de conexão complexas. Tivemos problemas com o KEDA não encontrando o Virtual Host. A Solução: Usar uma "barra dupla" na string de conexão do Secret (amqps://host:port//vhost) e remover a definição explícita de vhost do trigger. Foi a única forma de fazer a API de métricas e a conexão AMQP funcionarem juntas sem erros 403 ou 404.

4. Otimização e "Auto-Cura" dos Consumers

Consumers PHP de longa duração sofrem com memory leaks. Em nossa arquitetura antiga, os pods cresciam até estourar a memória (OOMKilled).

Resolvemos isso combinando a infraestrutura com a configuração nativa do Magento:

Limites no Pod: Definimos requests/limits iguais (QoS Guaranteed) de 6GB.

Limites no Processo: Limitamos cada processo PHP a 512MB (php -d memory_limit=512M).

Auto-Reciclagem: No comando de inicialização, usamos a flag --max-messages=10000.

    O consumer processa 10.000 mensagens e morre "de propósito".

    O Kubernetes percebe a morte e sobe um pod novo, "limpo" e rápido.
Enter fullscreen mode Exit fullscreen mode

Script de Inicialização dos Consumers:

Bash

- |
          set -e

          # Garante que o diretório e o arquivo de log existam
          mkdir -p var/log
          touch var/log/consumers.log

          # --- LÓGICA DE ENCERRAMENTO CONTROLADO ---
          # Função que será chamada quando o Kubernetes enviar o sinal de 'stop' (SIGTERM)
          shutdown() {
            echo "Desligando... Enviando SIGTERM para todos os processos de consumer..."
            # Envia o sinal de 'stop' para todos os processos 'php' filhos
            pkill -P $$ -TERM 'php'
            echo "Aguardando o término dos processos..."
            # Espera que todos os processos em background terminem
            wait
            echo "Todos os processos de consumer foram encerrados. Saindo..."
            exit 0
          }

          # O 'trap' "apanha" o sinal de encerramento do Kubernetes e chama a nossa função 'shutdown'
          trap 'shutdown' TERM INT

          echo "Iniciando todos os consumers do Magento em background..."

          PROCESS_COUNT=$(php -r '
              $config = include "app/etc/env.php";
              $consumers = $config["cron_consumers_runner"]["multiple_processes"] ?? [];
              # Conta o número de chaves (filas), não a soma dos valores
              echo count($consumers);
          ')
          echo "========================================================"
          echo "CONTADOR DE PROCESSOS: Forçando 1 processo por fila. Total de filas: $PROCESS_COUNT"
          echo "========================================================"

          # 2. Gera os comandos (ignora o $processCount do env.php)
          COMMANDS=$(php -r '
              $config = include "app/etc/env.php";
              $consumers = $config["cron_consumers_runner"]["multiple_processes"] ?? [];
              # Itera sobre cada fila (ignora a contagem de processos)
              foreach ($consumers as $consumerName => $processCount) {
                  # IGNORA o $processCount e inicia APENAS 1 processo
                  echo "echo \"Iniciando 1 processo (forçado) para o consumer: $consumerName\"; ";
                  # 1. memory_limit=768M (seguro)
                  # 2. SEM --max-messages (para rodar para sempre)
                  echo "php -d memory_limit=768M bin/magento queue:consumers:start $consumerName >> var/log/consumers.log 2>&1 & ";
              }
          ')

          # Executa todos os comandos gerados
          eval $COMMANDS

          echo "Todos os consumers foram iniciados. Exibindo logs em tempo real..."
          # Mantém o contêiner rodando e transmite o log
          tail -f var/log/consumers.log &

          wait
Enter fullscreen mode Exit fullscreen mode

Com essa arquitetura, saímos de um modelo estático e caro para uma infraestrutura elástica.

Economia: Durante a madrugada, temos zero pods de consumer rodando.

Performance: Em picos de atualização de catálogo, o cluster escala para 20 pods em segundos, drenando filas de milhões de mensagens rapidamente.

Estabilidade: A segregação de Cron e Consumers acabou com os deadlocks de banco de dados.
Enter fullscreen mode Exit fullscreen mode

O Magento no Kubernetes não precisa ser uma dor de cabeça. Com as ferramentas certas (KEDA, Argo CD) e os ajustes corretos no env.php e Dockerfile, ele se torna uma plataforma robusta e moderna.

Observação: Pode ter melhorias em PHP e seus processos, não sou especialista nessa linguagem.

Link: Completo

Link: Biografia

Top comments (0)