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 406PID 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/fd0 -> /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 406SYN-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 406strace: Process 406 attached
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("10.255.255.1")}, 16Parado 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");def primeira_linha(caminho: str) -> str:
# O bloco with fecha o arquivo ao sair, com ou sem exceção
with open(caminho, encoding="utf-8") as arquivo:
return arquivo.readline().rstrip("\n")package arquivos
import (
"bufio"
"os"
)
func PrimeiraLinha(caminho string) (string, error) {
arquivo, err := os.Open(caminho)
if err != nil {
return "", err
}
defer arquivo.Close() // executado ao sair da função, por qualquer caminho
leitor := bufio.NewScanner(arquivo)
if leitor.Scan() {
return leitor.Text(), nil
}
return "", leitor.Err()
}import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class Arquivos {
public static String primeiraLinha(Path caminho) throws IOException {
// try-with-resources fecha o leitor ao sair do bloco
try (BufferedReader leitor = Files.newBufferedReader(caminho, StandardCharsets.UTF_8)) {
String linha = leitor.readLine();
return linha == null ? "" : linha;
}
}
}public static class Arquivos
{
public static async Task<string> PrimeiraLinha(string caminho)
{
// using fecha o leitor ao sair do escopo, com ou sem exceção
using var leitor = new StreamReader(caminho);
return await leitor.ReadLineAsync() ?? "";
}
}O que muda de uma linguagem para outra
| Construção | Também serve para | |
|---|---|---|
| Node | try/finally, ou await using nas versões recentes | Conexões de banco tiradas de um pool (client.release() no finally) |
| Python | with | Conexões, travas, transações, arquivos temporários |
| Go | defer logo depois de abrir | Corpos de resposta HTTP (defer resp.Body.Close()), travas, linhas de consulta |
| Java | try-with-resources, para tudo que implementa AutoCloseable | Conexões JDBC, PreparedStatement, ResultSet |
| C# | using e await using, para tudo que implementa IDisposable | Conexõ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 abertosO ú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çoNo 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éricoNo 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 killerNo 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 oufinally? - 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]