Skip to content

Fix too many open files issue#1443

Merged
robertatakenaka merged 2 commits into
mainfrom
codex/too-many-openfile-issue
Jul 17, 2026
Merged

Fix too many open files issue#1443
robertatakenaka merged 2 commits into
mainfrom
codex/too-many-openfile-issue

Conversation

@rondinelisaad

@rondinelisaad rondinelisaad commented Jun 16, 2026

Copy link
Copy Markdown
Member

O que esse PR faz?

Este PR corrige um problema em que falhas na coleta de métricas de memória pelo sistema de profiling resultavam em erro HTTP 500 para o usuário final.

A leitura de memória realizada por meio da biblioteca psutil foi centralizada em funções auxiliares seguras em core/utils/profiling_tools.py. Quando ocorre uma exceção como:

OSError: [Errno 24] Too many open files: '/proc/16/statm'
ERROR 2026-06-15 22:35:00,539 log 16 139626005938016 Internal Server Error: /api/v2/pid/pid_provider/
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/app/core/utils/profiling_tools.py", line 589, in __call__
  File "/usr/local/lib/python3.11/site-packages/psutil/_pslinux.py", line 1921, in memory_info
  File "/usr/local/lib/python3.11/site-packages/psutil/_common.py", line 764, in open_binary
OSError: [Errno 24] Too many open files: '/proc/16/statm'

durante a leitura de informações do processo (/proc/<pid>/statm), o sistema passa a tratar a métrica de memória como indisponível (unavailable) e permite que a requisição continue normalmente.

Além disso:

  • Middleware e decoradores foram ajustados para tratar adequadamente situações em que a métrica de memória não está disponível;
  • Comparações entre valores de memória e limites configurados deixam de ser executadas quando não há métrica válida;
  • O cabeçalho HTTP X-Memory-Used passa a ser enviado apenas quando a informação de memória foi obtida com sucesso;
  • O script de start do container (compose/production/django/start) passa a ajustar o limite de arquivos abertos (ulimit -n, configurável via GUNICORN_NOFILE) e a reiniciar workers periodicamente (--max-requests/--max-requests-jitter), como mitigação adicional para o esgotamento de descritores de arquivo.

O objetivo é evitar que uma falha de observabilidade/profiling impacte a disponibilidade da aplicação.

Onde a revisão poderia começar?

Recomenda-se iniciar a revisão pelo arquivo:

core/utils/profiling_tools.py

Principais alterações:

  • Criação dos helpers seguros (_get_memory_usage_mb, _get_memory_delta_mb, _format_memory_delta, _is_high_memory) para obtenção e formatação de métricas de memória;
  • Tratamento de exceções OSError/psutil.Error nos pontos que chamam process.memory_info();
  • Ajuste dos decoradores (profile_endpoint, profile_classmethod, profile_method, profile_function, profile_staticmethod) e do LightweightProfilingMiddleware para usar os helpers em vez de acessar psutil diretamente.

Em seguida, revisar:

compose/production/django/start

  • Ajuste do ulimit -n antes de subir o Gunicorn;
  • Novos parâmetros configuráveis via variáveis de ambiente (GUNICORN_TIMEOUT, WEB_CONCURRENCY, GUNICORN_WORKER_CONNECTIONS, GUNICORN_WORKER_CLASS, GUNICORN_MAX_REQUESTS, GUNICORN_MAX_REQUESTS_JITTER).

Como este poderia ser testado manualmente?

Cenário normal

  1. Subir a aplicação normalmente;
  2. Acessar páginas da aplicação;
  3. Confirmar que as respostas continuam sendo retornadas normalmente;
  4. Verificar que o cabeçalho X-Memory-Used continua sendo enviado quando a coleta de memória é bem-sucedida (em ambiente com DEBUG=True).

Cenário de falha na coleta de memória

  1. Simular uma falha na chamada a psutil.Process(...).memory_info() (por exemplo, via mock lançando OSError);
  2. Realizar uma requisição para a aplicação;
  3. Confirmar que a resposta continua sendo retornada normalmente (sem HTTP 500);
  4. Confirmar que o cabeçalho X-Memory-Used não é enviado quando a métrica não está disponível;
  5. Verificar nos logs que a falha foi tratada adequadamente pelo profiling (mensagem memory: unavailable).

Cenário de limite de arquivos abertos

  1. Subir o container com a nova versão do script start;
  2. Verificar no log de inicialização a linha Open files limit: <valor>, confirmando que o ulimit foi aplicado;
  3. Opcionalmente, sobrescrever GUNICORN_NOFILE e confirmar que o valor configurado é refletido no log.

Algum cenário de contexto que queira dar?

Foi identificado um incidente em produção onde a aplicação retornava HTTP 500 devido a uma exceção originada no mecanismo de profiling (OSError: [Errno 24] Too many open files: '/proc/16/statm'), ocorrida durante a leitura de métricas de memória via psutil. Um componente de observabilidade acabava interrompendo o processamento normal da requisição.

Este PR atua como mecanismo de proteção (graceful degradation), impedindo que falhas de monitoramento provoquem indisponibilidade da aplicação, e complementa essa proteção com um ajuste operacional no limite de arquivos abertos e no ciclo de vida dos workers do Gunicorn.

Importante destacar que a causa raiz do erro (Too many open files) não é totalmente eliminada por este PR e deve continuar sendo monitorada. Possíveis causas remanescentes incluem:

  • Vazamento de descritores de arquivos;
  • Excesso de conexões abertas;
  • Limites de ulimit insuficientes em outros ambientes;
  • Limitações configuradas no container ou ambiente de execução.

Obs.: o comportamento mudou de "fail closed" para "fail open": se o profiling falhar, a aplicação continua atendendo a requisição. Isso deixa explícita a decisão arquitetural tomada e facilita a aprovação pelos revisores.

Screenshots

Não se aplica.

Quais são os tickets relevantes?

Não há ticket associado no momento.

Referências

  • Documentação do psutil sobre Process.memory_info();
  • Documentação do Gunicorn sobre --max-requests e --max-requests-jitter (mitigação de vazamento de memória/descritores por worker);
  • Tratamento de exceções para degradação graciosa de componentes de observabilidade;
  • Incidente de produção relacionado ao erro OSError: [Errno 24] Too many open files.

Segurança da informação (NSI.04)

Seção obrigatória. Marque as opções aplicáveis e justifique quando necessário. Referência: NSI.04 - Norma de Desenvolvimento Seguro.

Este PR manipula dados sensíveis ou pessoais (LGPD)?

  • Sim — descreva os controles de proteção aplicados (criptografia, mascaramento, anonimização, etc.):
  • Não — as alterações tratam apenas de métricas internas de memória/desempenho do processo (RSS), não de dados de usuários.

Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?

  • Sim — descreva o que mudou e por quê:
  • Não

Este PR introduz, atualiza ou remove dependências de terceiros?

  • Sim — as novas dependências foram verificadas no SBOM/Trivy sem vulnerabilidades críticas/altas em aberto?
    • Verificado e aprovado
    • Pendente / vulnerabilidade aceita com justificativa:
  • Não — psutil já era utilizado anteriormente; nenhuma dependência nova foi adicionada, atualizada ou removida.

Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?

  • Sim — link do job:
  • Não aplicável a este PR (justifique): alteração restrita a tratamento de exceções e configuração de infraestrutura (ulimit/gunicorn), sem introdução de dependências ou superfícies de ataque novas. Ajustar caso a política exija execução do pipeline para todo PR.

Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?

  • Sim — confirme que há sanitização/parametrização (prepared statements, escaping, etc.):
  • Não

Este PR expõe novos endpoints, telas ou serviços?

  • Sim — HTTPS obrigatório está garantido e o acesso segue o princípio de menor privilégio?
  • Não

Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?

  • Não, nenhum segredo foi commitado
  • Sim (bloquear merge e corrigir antes de prosseguir)

@gitnnolabs

Copy link
Copy Markdown
Collaborator

What This PR Does

This PR implements graceful degradation for the profiling system to prevent monitoring failures from causing application outages. Specifically, it addresses an incident where OSError: [Errno 24] Too many open files exceptions from psutil during memory metric collection were causing HTTP 500 errors.

Key behavior change: From "fail closed" (profiling errors crash the request) to "fail open" (profiling errors are silently handled, memory metrics marked unavailable).

Business Impact

  • Problem: Production incident where profiling system failures resulted in HTTP 500s
  • Solution: Centralize memory metric collection with exception handling, gracefully degrade to "unavailable" state
  • Outcome: Application remains available even when profiling can't collect metrics

Core Changes

The PR introduces four new helper functions that safely wrap memory metric collection:

def _get_memory_usage_mb(process=None):
    """Returns RSS in MB or None if psutil fails"""
    try:
        if process is None:
            process = psutil.Process()
        return process.memory_info().rss / 1024 / 1024
    except (OSError, psutil.Error) as exc:
        profiling_logger.debug("Skipping profiling memory sample: %s", exc)
        return None

def _get_memory_delta_mb(process, start_memory):
    """Safely calculates memory delta, returns None if either value unavailable"""
    if start_memory is None:
        return None
    end_memory = _get_memory_usage_mb(process)
    if end_memory is None:
        return None
    return end_memory - start_memory

def _format_memory_delta(memory_used):
    """Formats memory delta or returns 'unavailable' string"""
    if memory_used is None:
        return "unavailable"
    return f"+{memory_used:.1f}MB"

def _is_high_memory(memory_used):
    """Safely checks if memory exceeded threshold"""
    return memory_used is not None and memory_used > PROFILING_LOG_HIGH_MEMORY

Why this matters:

  • Catches OSError (file descriptor exhaustion) and psutil.Error
  • Returns None instead of raising, allowing callers to handle gracefully
  • Single point of truth for memory collection logic

Other Changes

All profiling decorators and middleware are updated to use these helpers:

  1. profile_endpoint decorator - HTTP request profiling middleware

    • ✅ Uses _get_memory_usage_mb() for initial memory
    • ✅ Uses _get_memory_delta_mb() to compute delta
    • ✅ Only sends X-Memory-Used header when metric is available (if memory_used is not None)
  2. profile_classmethod, profile_method, profile_function, profile_staticmethod - All decorators updated

    • ✅ Replaced inline process.memory_info() calls with helpers
    • ✅ Updated memory comparisons to use _is_high_memory()
    • ✅ Updated logging to use _format_memory_delta()
  3. LightweightProfilingMiddleware - Global middleware

    • ✅ Uses helpers for safe memory collection
    • ✅ Logs "unavailable" instead of crashing on missing metrics

Consistency: All 7 locations that read memory metrics now follow the same safe pattern.


Merge Readiness and Risk Assessment

✅ Strengths

  1. Defensive Design: Centralized exception handling prevents profiling system failures from breaking the app
  2. Backward Compatible: No breaking changes to APIs or behaviors (only logging and response headers change)
  3. Well-Documented: Clear comments explain the failure mode and design decision ("fail open")
  4. Comprehensive: All profiling decorators and middleware updated consistently
  5. Minimal Scope: Single file, focused change with clear intent

⚠️ Considerations

  1. Root Cause Not Fixed: PR addresses symptoms (graceful degradation) not root cause (file descriptor exhaustion). Per the PR description, the underlying "too many open files" issue should continue to be investigated separately.

  2. Logging Clarity: When memory metrics fail, debug logs show profiling_logger.debug() but regular logs show "unavailable". This is acceptable since profiling failures are not critical, but ensures you won't see metrics gaps in normal logs.

  3. Header Behavior Change: X-Memory-Used header now omitted when metrics unavailable (was previously always sent). Any downstream systems relying on this header should handle its absence. This is correct behavior but worth noting.

  4. No Tests Visible: PR shows code changes but no visible unit/integration tests. Recommend:

    • Unit test for each helper function with mocked psutil failures
    • Integration test verifying requests succeed even when psutil.Process().memory_info() raises OSError

Risk Assessment: 🟢 Low

  • Isolated change with clear scope
  • Adds safety without removing functionality
  • Follows defensive programming principles
  • Only impacts profiling observability, not core app logic

Possible Improvements

  1. Add Unit Tests

    # Test that _get_memory_usage_mb handles OSError gracefully
    @patch('psutil.Process.memory_info', side_effect=OSError(24, 'Too many open files'))
    def test_get_memory_usage_mb_handles_oserror(mock_memory):
        result = _get_memory_usage_mb()
        assert result is None
  2. Consider Metrics Fallback: If psutil is unreliable, consider falling back to /proc/self/status parsing (for Linux) or os.getpid() + manual parsing as a secondary method.

  3. Metrics Reporting: Consider adding a metric (to APM/monitoring) when profiling sampling fails, so the ops team can track how often this degradation is triggered.

  4. Inline Comments: Add a comment above each decorator update explaining why None checks are needed, to future-proof the code.


O que esse PR faz?

Ajusta o script de inicializacao do Django em producao para elevar o limite de arquivos abertos antes de iniciar o Gunicorn, parametrizar os principais argumentos por variaveis de ambiente e habilitar reciclagem de workers com max-requests. Isso reduz a chance de erro Too many open files em pods Kubernetes executando gunicorn/gevent sob carga.

Onde a revisao poderia comecar?

compose/production/django/start

Como este poderia ser testado manualmente?

1. Gerar e publicar uma imagem com este script.

2. Implantar o pod Django no Kubernetes com GUNICORN_NOFILE=65536.

3. Validar no pod: cat /proc/<worker-pid>/limits | grep 'open files'.

4. Confirmar nos logs de startup a linha Open files limit: 65536.

5. Executar carga ou processamento que antes aproximava o limite e monitorar: ls -1 /proc/<worker-pid>/fd | wc -l.

Algum cenario de contexto que queira dar?

Os pods observados tinham hard limit alto, mas soft limit de apenas 1024 descritores. Ao mesmo tempo, gunicorn/gevent estava configurado com muitos worker-connections, deixando pouca margem para sockets, banco, logs, arquivos temporarios e leituras de /proc feitas por instrumentacao.

Screenshots

Nao aplicavel; mudanca operacional em script de inicializacao.

Quais sao tickets relevantes?

Nao informado.

Referencias

Diagnostico via /proc/<pid>/limits e /proc/<pid>/fd em Linux; configuracao de Gunicorn para worker-connections, max-requests e max-requests-jitter.
@rondinelisaad

rondinelisaad commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

Análise: limite de file descriptors vs. configuração do Gunicorn

Com gevent, cada worker pode manter muitas conexões simultâneas. Você configurou worker-connections=1000, mas cada processo worker só tem 1024 file descriptors disponíveis. Isso deixa quase nenhuma folga para:

  • sockets de clientes HTTP
  • conexões com banco
  • Redis/cache
  • arquivos estáticos/media/temp
  • logs
  • pipes internos
  • /proc/.../statm lido pelo psutil

Então, mesmo sem vazamento, um worker sob carga pode chegar perto do limite. Se houver qualquer vazamento pequeno, ele estoura rápido.

Com nofile=1024, worker-connections=1000 é apertado demais.

Timeout

Outro ponto: --timeout 1000 é muito alto. Isso permite que a request/processamento fique presa por quase 17 minutos segurando FDs. Para endpoints pesados, o ideal é mover o processamento longo para Celery/job assíncrono.

Se precisar manter esse timeout, eu colocaria também reciclagem de workers:

--max-requests 1000
--max-requests-jitter 100

Avaliação curta

A configuração atual explica bem o erro. O patch no profiling impede o 500 causado pelo psutil, mas eu ajustaria o ulimit ou reduziria worker-connections; senão o pod continua vulnerável a estourar FDs em carga.


Observação

Ao abrir o chamado será necessário pedir para adicionar as seguintes variáveis:

env:
  - name: GUNICORN_NOFILE
    value: "65536"
  - name: WEB_CONCURRENCY
    value: "3"
  - name: GUNICORN_WORKER_CONNECTIONS
    value: "1000"
  - name: GUNICORN_MAX_REQUESTS
    value: "1000"
  - name: GUNICORN_MAX_REQUESTS_JITTER
    value: "100"

@robertatakenaka robertatakenaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resumindo: O PR resolve de forma segura o sintoma imediato (HTTP 500 quando o psutil falha por falta de File Descriptors) centralizando a lógica em helpers com tratamento de erro e logs em debug. O código está aprovado para merge.

No entanto, a causa raiz (esgotamento de FDs por alta concorrência e requests longas) permanece latente e a aprovação fica condicionada a ações de infraestrutura e monitoramento pós-deploy.

⚠️ Riscos Importantes

  • Regressão de capacidade: o script start alterou o default de GUNICORN_WORKER_CONNECTIONS de 1000 para 100. Sem a env var explícita no deploy, a capacidade cairá 10x.
  • Falha silenciosa: se o hard limit do cgroup (ulimit -Hn) for menor que 65536, o script emitirá apenas um warning no stderr, que pode passar despercebido se a observabilidade não o capturar — o novo ulimit -n 65536 só entrega o headroom de FDs necessário para sustentar worker-connections=1000 se for de fato aplicado.
  • Débito técnico: o PR não inclui testes unitários para os novos helpers nem guard-rails (ex.: verificação estática/lint que bloqueie chamadas diretas a psutil.Process().memory_info() fora dos helpers, evitando reintrodução do bug no futuro).

📋 Próximos Passos

🚀 Infra & Deploy (Imediato)

  1. Merge do PR e aplicação obrigatória das seguintes env vars no deploy para evitar perda de capacidade:
GUNICORN_NOFILE: "65536"
WEB_CONCURRENCY: "3"
GUNICORN_WORKER_CONNECTIONS: "1000"
GUNICORN_MAX_REQUESTS: "1000"
GUNICORN_MAX_REQUESTS_JITTER: "100"
  1. Validar em Produção: confirmar se o ulimit -Hn (hard limit do cgroup) realmente permite 65536 FDs, se os logs de stderr (warning de falha do ulimit) estão sendo coletados pela observabilidade, e acompanhar a frequência de memory: unavailable nos logs como indicador direto de quão presente ainda está a falha do psutil mesmo após o ajuste.

📈 Melhorias & Investigação (Médio Prazo)

  • Testes: criar testes unitários para os helpers (falha do psutil, cálculo de delta com valor None, ausência do header X-Memory-Used) e adicionar trava/comentário contra o uso direto do psutil fora dos helpers.
  • Causa Raiz: investigar se o endpoint /api/v2/pid/pid_provider/ tem vazamento de conexões/arquivos e planejar a redução do --timeout 1000, migrando processamentos longos para Celery.
  • Monitorar: acompanhar métricas de FDs abertos por worker e a taxa de logs memory: unavailable após a virada, para avaliar se a mitigação foi suficiente ou se worker-connections/ulimit precisam de novo ajuste.

Concluindo: o merge do código pode seguir sem bloqueios, mas o incidente só será considerado de fato encerrado quando as ações de infraestrutura acima forem aplicadas e validadas em produção — o patch de profiling trata o sintoma, não a causa raiz do esgotamento de file descriptors.

@robertatakenaka
robertatakenaka merged commit e0bffad into main Jul 17, 2026
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants