DevGuideTK
← Trilha Backend

Linux e linha de comando

Processos, permissões, sinais e as ferramentas para investigar um servidor em produção.

As saídas desta página vêm de testes em containers Debian e Alpine.

Investigar um processo "travado"

O cenário: um serviço parou de responder, sem erro no log. Para o teste, o "serviço" é um programa Python que tenta se conectar a um endereço que não responde, sem tempo limite.

1. O processo existe e em que estado está?

ps -o pid,stat,etime,cmd -p 406
PID STAT ELAPSED CMD
406 S      00:02 python -c import socket; s=socket.socket(); s.connect(("10.255.255.1", 80))

Estado S: está esperando alguma coisa.

2. Que recursos ele tem abertos?

ls -l /proc/406/fd
0 -> /dev/null
1 -> pipe:[86162956]
2 -> pipe:[86162957]
3 -> socket:[86166627]

Além das três saídas padrão, um socket: uma conexão de rede.

3. Que conexão é essa?

ss -tnp | grep 406
SYN-SENT 0 1 172.17.0.4:45146 10.255.255.1:80 users:(("python",pid=406,fd=3))

SYN-SENT: o processo enviou o primeiro pacote da conexão TCP e está esperando a resposta, que não vem.

4. O que ele está fazendo exatamente?

strace -p 406
strace: Process 406 attached
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("10.255.255.1")}, 16

Parado dentro da chamada connect(), no descritor 3. A causa está confirmada: uma conexão sem tempo limite com um destino que não responde. A correção está no código, com tempo limite de conexão, como mostram os tópicos de HTTP e de TCP.

Quatro comandos, sem nenhuma mudança no código, foram do sintoma à causa.

Fechar recursos sempre

O erro Too many open files quase sempre vem de recursos abertos que não são fechados quando ocorre uma exceção. Cada linguagem tem uma construção que garante o fechamento, mesmo com erro:

import { open } from "node:fs/promises";

export async function primeiraLinha(caminho: string): Promise<string> {
  const arquivo = await open(caminho, "r");
  try {
    for await (const linha of arquivo.readLines()) {
      return linha;
    }
    return "";
  } finally {
    await arquivo.close(); // fecha mesmo se a leitura lançar erro
  }
}

// Em versões recentes do Node e do TypeScript:
// await using arquivo = await open(caminho, "r");

O que muda de uma linguagem para outra

ConstruçãoTambém serve para
Nodetry/finally, ou await using nas versões recentesConexões de banco tiradas de um pool (client.release() no finally)
PythonwithConexões, travas, transações, arquivos temporários
Godefer logo depois de abrirCorpos de resposta HTTP (defer resp.Body.Close()), travas, linhas de consulta
Javatry-with-resources, para tudo que implementa AutoCloseableConexões JDBC, PreparedStatement, ResultSet
C#using e await using, para tudo que implementa IDisposableConexões, comandos, fluxos

Em Go, esquecer defer resp.Body.Close() numa resposta HTTP é um vazamento clássico: a conexão não volta ao pool e o descritor fica aberto. Em Java, o mesmo acontece com um ResultSet ou Statement fora de um try-with-resources.

O coletor de lixo não resolve esse problema. Ele pode demorar a coletar o objeto, e enquanto isso o descritor continua ocupado. No nosso teste, os arquivos abertos e não fechados continuavam referenciados por uma lista, e o coletor não podia fazer nada: o limite chegou em 98 aberturas.

Comandos para cada problema

Descritores

ulimit -n                          # limite da sessão atual
cat /proc/<pid>/limits | grep files   # limite de um processo em execução
ls /proc/<pid>/fd | wc -l          # quantos o processo tem abertos
ls -l /proc/<pid>/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head
                                   # o que mais aparece entre os abertos

O último comando agrupa os descritores pelo destino. Centenas de entradas socket: sugerem conexões vazando; o mesmo arquivo repetido sugere um arquivo aberto em laço sem fechamento.

Disco

df -h                              # espaço por sistema de arquivos
df -i                              # inodes: "cheio" com espaço sobrando
du -sh /var/log/* | sort -h | tail # as maiores pastas de log
lsof +L1                           # arquivos apagados que ainda ocupam espaço

No teste, o sistema de arquivos de 1 MB foi de 0% a 100% de uso, e a gravação passou a falhar com No space left on device.

Permissões

id                                 # usuário e grupos do processo atual
ls -ld /dados                      # dono, grupo e permissões da pasta
stat -c '%U:%G %a %n' /dados/*     # o mesmo, por arquivo, em formato numérico

No teste, id mostrou uid=1000 e ls -ld /dados mostrou drwxr-xr-x root root: o usuário 1000 não tem permissão de gravar numa pasta do root com 755. A correção certa é dar a posse da pasta ao usuário da aplicação, e não abrir a permissão para todos.

Sinais e processos

kill -0 <pid>          # só confere se o processo existe; não envia sinal nenhum
kill -TERM <pid>       # pede para terminar (o padrão do kill)
kill -KILL <pid>       # termina à força: último recurso
dmesg | grep -i "killed process"   # processos mortos pelo OOM killer

No teste, um processo que ignorava o SIGTERM continuou vivo depois dele e só terminou com SIGKILL. Na prática, kill -9 deve ser o último recurso: o processo não fecha conexões, não termina pedidos e não grava dados pendentes.

Lista de revisão

Código

  • Todo arquivo, conexão e corpo de resposta é fechado com with, defer, using, try-with-resources ou finally?
  • A aplicação grava só em pastas que pertencem ao usuário dela?
  • O processo termina com código de saída diferente de zero quando falha?
  • O SIGTERM é tratado para um encerramento limpo?

Servidor e container

  • Os logs têm rotação, e o disco tem alerta antes de encher?
  • O limite de descritores é adequado para o número de conexões esperado?
  • A aplicação roda sem root, sem chmod 777?
  • Existe uma forma de investigar containers sem ferramentas, como um container de depuração?

Investigação

  • A equipe conhece o roteiro por recurso: CPU, memória, disco, rede e descritores?
  • Mensagens do núcleo, como o OOM killer, são coletadas junto com os logs da aplicação?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Reproduzir falhas de sistema num container

    Quero reproduzir num container as falhas de sistema mais comuns em produção, para reconhecê-las depois.
    
    Pergunte qual linguagem eu uso.
    
    Vamos em etapas:
    1. Esgotar o limite de descritores com um programa que abre arquivos sem fechar, num container com ulimit baixo; depois corrigir fechando os arquivos.
    2. Encher um sistema de arquivos pequeno com logs e ver o erro.
    3. Rodar um processo sem root e tentar gravar num volume do root; corrigir do jeito certo.
    4. Mandar SIGTERM para um processo que trata o sinal e para um que o ignora, e comparar com SIGKILL.
    5. Ver os códigos de saída em cada caso.
    
    Em cada etapa, peça que eu preveja o resultado antes de rodar.
  • Investigar um processo travado com strace e /proc

    Quero aprender a descobrir o que um processo travado está esperando.
    
    Vamos montar o cenário num container: um programa que tenta se conectar a um endereço que não responde, sem tempo limite.
    
    Depois, me guie para investigar, um comando por vez, pedindo que eu interprete a saída antes de você explicar:
    1. ps para ver o estado do processo.
    2. /proc/PID/status e /proc/PID/fd para ver memória e descritores.
    3. ss para ver a conexão e o estado dela.
    4. strace para ver a chamada ao sistema em que ele está parado.
    
    No final, me mostre como corrigir no código da minha linguagem e como confirmar que a correção funcionou.
  • Revisar o uso de recursos do sistema no código

    Vou colar abaixo código e um Dockerfile. Revise com foco no uso de recursos do sistema operacional.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Arquivos, conexões e corpos de resposta são sempre fechados, inclusive quando há exceção?
    2. A aplicação grava só em pastas que pertencem ao usuário dela?
    3. O processo roda sem root?
    4. Logs vão para a saída padrão, ou para arquivos com rotação?
    5. O processo termina com código diferente de zero quando falha?
    6. O SIGTERM é tratado?
    
    Para cada problema, descreva a falha que ele causa em produção e mostre a correção. Não reescreva o que já está correto.
    
    Código e Dockerfile:
    [cole aqui]