antonio leandro

estruturas e algoritmos

Cold-RL: Learning Cache Eviction with Offline Reinforcement Learning for NGINX

paper · Aayush Gupta, Arpit Bhayani · · ~14 min de leitura do original

a tese

dá para aprender a política de eviction dentro do nginx sem estourar o orçamento de microssegundos: um modelo de 10 mil parâmetros olha a cauda do lru, tem 500 µs para responder, e cai de volta no lru quando falha

o que fica

  1. O ganho de uma política aprendida só aparece sob pressão: com cache de 400 MB, Cold-RL empata com ARC (0,918 contra 0,919), e com 25 MB sobe de 0,144 para 0,354.
  2. Eviction é o evento raro do cache — dezenas de vezes contra milhões de hits — e é exatamente por isso que cabe gastar centenas de microssegundos de rede neural nele.
  3. O fallback determinístico para LRU é o que torna o modelo deployável: o p99 da eviction dá 710 µs, estoura o orçamento, e 0,02% das decisões voltam para o caminho nativo.
  4. Avaliar só os K objetos mais frios da cauda dá custo O(K), mas amarra a política: um objeto ruim parado no meio da lista nunca chega a ser considerado.
  5. Na ablação, tamanho é a feature que mais pesa — removê-la derruba o hit ratio em 31%, contra 18% do inter-arrival time e 12% do TTL restante.
  6. O benchmark mais impressionante do paper foi desenhado pelos próprios autores para quebrar heurísticas clássicas, o que limita o que 0,421 contra 0,056 diz sobre tráfego real.

o problema

LRU está em produção em todo lugar não porque seja bom, mas porque é O(1) e porque cabe na cabeça de quem está debugando às três da manhã. Os autores enumeram três cegueiras dessa escolha. LRU é cego a tamanho: um vídeo de 120 MB expulsa milhares de assets de 20 KB. É cego a periodicidade: rajadas diárias causam thrashing justo antes do pico previsível. E parte da premissa de que recência prevê reuso — premissa que workload real e adversário violam de rotina.

O exemplo do paper: uma notícia estoura às 14h, um vídeo de 15 MB vira o objeto mais pedido, e a entrada dele empurra para fora CSS, JavaScript e respostas de API que eram consistentemente populares. Às 16h o interesse morreu, mas o estrago fica: os assets expulsos precisam ser buscados de novo na origem, e a cascata de miss dura horas. Nenhuma política clássica aprende com isso. ARC, LIRS e GreedyDual-Size adaptam por regra fixa, não por experiência. O trabalho que aprende de verdade — LRB, HALP, HR-Cache — opera em escala de milissegundos ou só em simulação, e eviction num proxy é decisão de microssegundo.

a ideia

Eviction é um problema de predição disfarçado de contabilidade de recência. Manter um objeto é uma aposta em ele ser lido de novo antes do TTL expirar, condicionada a tamanho, pressão do cache e padrão temporal. Heurística congela essa aposta numa regra; um modelo pode estimá-la.

Duas restrições operacionais moldam o resto. Primeira: eviction é raro. São milhões de hits, milhares de inserções, dezenas de evictions — então dá para concentrar computação ali, e só ali. Segunda: SRE não aceita caixa-preta sem freio. Daí o desenho: modelo minúsculo, prazo duro, fallback instantâneo para o LRU nativo, kill switch de uma flag.

como funciona

Um módulo dinâmico em C engancha ngx_http_file_cache_forced_expire, interceptando o caminho de expiração forçada antes do LRU. Quando dispara, ele pega os K objetos mais frios da cauda do LRU (K entre 8 e 32; a ablação aponta 16 como melhor troca) e extrai seis features por candidato: age, size, hit_count, inter-arrival time, TTL remaining e último RTT de origem. Para K=8 isso serializa em 192 bytes, que vão por IPC síncrono em Unix domain socket até um sidecar. A resposta é um bitmask de vítimas. Prazo: 500 µs. Estourou, resposta inválida, sidecar mudo — cai no LRU.

O sidecar é C++ com ONNX Runtime. Ring buffer lock-free, modelo carregado uma vez com hot-swap por SIGHUP, e um caminho de inferência que não faz syscall, não aloca e não pega lock. O modelo é uma dueling DQN: Linear128 → ReLU → Linear64 → ReLU, e daí duas cabeças, V escalar e A com K saídas, combinadas como Q = V + (A - mean(A)). Dez mil parâmetros no total, quantizados em int8 — cabe em L2.

O treino é offline. Access log do próprio NGINX passa por um simulador de cache que reproduz admissão e expiração, gerando trajetórias. A recompensa é quase constrangedora de simples: objeto retido ganha +1 se for lido de novo antes do TTL expirar.

Os números de latência: inferência com p50 de 127 µs e p95 de 342 µs; eviction completa com p50 de 216 µs e p95 de 498 µs. Menos de 2% de CPU a 50 mil req/s.

o que isso custou

Os autores declaram três limites: cold start exige de 24 a 48 horas de log, mudança brusca de workload obriga a retreinar, e a decisão nunca sai da cauda K. Esse último é o mais estrutural — objeto que envelheceu mal no meio da lista não é avaliado.

O p99 da eviction total dá 710 µs, ou seja, passa do orçamento; é para isso que existe o fallback, acionado em 0,02% das decisões. E o ganho evapora quando não há pressão: a 400 MB, Cold-RL faz 0,918 contra 0,919 do ARC. Se o working set cabe no cache, isso não é para você.

Vale ler o resto com a régua alta. O benchmark “trap”, onde Cold-RL faz 0,421 contra 0,056 do LRU, foi construído pelos próprios autores para violar as premissas clássicas — ele mede tanto a política quanto a armadilha. Os números de produção (três meses, 100 milhões de requisições por dia, 23% menos tráfego de origem) vêm sem descrição do deployment, sem intervalo de confiança e sem baseline rodando em paralelo. E a extrapolação de custo, de 2,2 a 3,3 milhões de dólares por ano em 50 nós, é aritmética sobre preço de tabela, não medição.

onde isso aparece hoje

O paper é de 2025 e ainda não tem descendência; o que ele ocupa é o vão entre a literatura de cache aprendido, que roda em simulação, e o que aguenta ficar num proxy. A régua de comparação continua sendo ARC, que os autores tratam como o melhor clássico e é quem eles precisam bater. O contramovimento existe e é forte: SIEVE vai na direção oposta — menos mecanismo, não mais — e mostra que ainda há ganho em heurística simples. Na direção de colocar modelo dentro de software de sistema, o parente próximo é AlphaDev, com a diferença de que lá o aprendizado sai antes do deploy, virando código estático, enquanto aqui ele fica no caminho quente e precisa de um relógio apontado para ele.

lido na íntegra por pipeline de llm, revisado por antonio leandro antes de publicar ·