Pular para o conteúdo
caua.tolentino
Todos os cases
DesempenhoArquitetura

Feed XML que quebrava em catálogos grandes

Feeds de produtos com alguns milhares de itens saíam truncados, e aumentar a memória só adiava o problema. Diagnostiquei a causa e redesenhei a geração: em lotes, fora do request, com trava de concorrência e escrita atômica, para qualquer tamanho de catálogo.

Papel
Desenvolvedor responsável, do diagnóstico à entrega
Contexto
Plataforma de e-commerce multi-tenant
Stack
PHP, Symfony, Doctrine, Redis, Twig, XML
Time
Solo
Período
2026

Case anonimizado: nomes de empresa, cliente e pessoas foram omitidos ou trocados.

01. Situação

Acima de alguns milhares de produtos, o feed saía cortado

As lojas da plataforma publicam feeds XML de produtos para serviços como comparadores de preço e plataformas de avaliação. Cada loja tem o seu template, guardado no banco. Numa loja com cerca de 7.000 produtos, o feed de uma categoria com pouco mais de mil itens funcionava, mas o de uma categoria com 3.331 saía truncado: tags sem fechar, nenhum arquivo de cache e estouro de memória no log.

Reproduzi o problema com categorias de tamanhos diferentes para achar o ponto em que ele aparecia. O contorno óbvio era dar mais memória ao PHP, e eu testei: de 512 MB para 2 GB, o feed de 3.331 passou, muito lento, e o feed com todos os produtos voltou a quebrar.

produtos: feed gerado
1.055
produtos: feed truncado
3.331
produtos: quebra mesmo com 2 GB
~7.000

02. Investigação

O consumo crescia com o catálogo por três caminhos ao mesmo tempo

O teste com 2 GB me mostrou que o problema não era um limite mal configurado: o consumo crescia junto com o volume, então qualquer limite seria atingido por um catálogo maior. Fui atrás de onde a memória ia. Li o fluxo inteiro da geração, contei as consultas por feed e mapeei o padrão de uso de memória.

O controller buscava todos os produtos da categoria em uma consulta e mantinha todas as entidades do ORM na memória até o fim.

  1. Pedido do feed
  2. Busca todos os produtos de uma vez
  3. Template monta o XML inteiro em memória, com consultas por produto
  4. Resposta e cache gravados só no final
  5. memória estoura no meio: XML truncado e nenhum cache gravado

Esse desenho tinha um segundo defeito: como o cache só era gravado no final, um feed que quebrava nunca gerava cache, e cada novo pedido repetia o trabalho inteiro e quebrava de novo.

03. Solução

Gerar em lotes, fora do request, e publicar só o arquivo completo

Decidi não tratar o sintoma. Em vez de ajustar limites, redesenhei o fluxo de geração para que a memória usada deixasse de depender do tamanho do catálogo.

A busca passou a trazer só os ids. A renderização acontece em lotes de 200 produtos: cada lote é carregado, renderizado, escrito direto no arquivo e descartado, com a memória do ORM limpa antes do lote seguinte. Para isso funcionar com templates escritos pelas lojas, implementei a detecção do cabeçalho e do rodapé do template, que são escritos uma vez só, em volta dos lotes.

Feeds acima de mil produtos saem do request: são gerados por um comando em segundo plano, e o pedido HTTP só entrega o que já está pronto.

  1. Pedido do feed
  2. Cache válido: entrega o arquivo
  3. Feed grande: pega a trava e dispara a geração em segundo plano
  4. Lotes de 200 escritos em um arquivo temporário
  5. Arquivo completo substitui o cache
  6. trava ocupada: entrega o cache anterior ou uma resposta provisória
produtos por lote
200
produtos: geração em segundo plano
1.000+
validade do cache
6 h

04. Concorrência

Uma geração por vez, e nenhum arquivo pela metade

Tirar a geração do request criou problemas novos, e eu tratei cada um antes de entregar.

Escolha

Trava distribuída no Redis

Vários pedidos do mesmo feed não podem disparar várias gerações. Usei uma trava por feed, com validade de 10 minutos, que funciona entre servidores e só pode ser liberada por quem a pegou.

Escolha

Escrita atômica do cache

O arquivo é escrito em um temporário e só troca de nome quando está completo. Quem pede o feed recebe a versão anterior ou a nova, nunca um XML pela metade.

Escolha

Resposta provisória na primeira geração

Enquanto o primeiro arquivo de um feed grande não existe, o pedido recebe uma resposta provisória em vez de ficar preso esperando.

Cuidado

Corrida e arquivos órfãos

Nos testes, encontrei e corrigi uma condição de corrida e arquivos temporários que ficavam órfãos.

05. Compatibilidade

Os templates das lojas continuam funcionando sem alteração

Cada loja escreve o próprio template, então eu não podia assumir um formato. Mantive a mudança dentro do fluxo de XML, sem tocar nos componentes de template compartilhados com o resto da plataforma, e cobri os casos em que a geração em lotes não se aplica.

Trade-off

Não reescrever as funções do template

Reduzir as consultas por produto exigiria mexer em componentes usados pela loja inteira. Resolvi o consumo de memória no fluxo do XML, com risco restrito a ele.

Cuidado

Nomes de feed que colidem

Dois nomes diferentes podiam resultar no mesmo arquivo de cache. Adicionei a validação no cadastro do feed, com testes.

06. Resultado

O feed grande deixou de depender da memória do request. Ele é gerado por partes, em segundo plano, com uma geração por vez, e servido do cache por seis horas. O feed que saía truncado passou a sair completo, e o tamanho do catálogo deixou de ser um limite.

Além do código, entreguei a análise técnica do módulo de XML documentada para o time, logs próprios da geração e um comando de geração de cache que pode ser reaproveitado.