kelvin@cluxnei:~/blog$ cat 2gb/01-a-maquina.md
Quanta coisa cabe em 2 GB
Você sabe a diferença entre Fargate e Cloud Run. Sabe quando uma fila gerenciada resolve e quando é overkill. Sabe estimar custo de egress. E nunca abriu um terminal numa máquina que é sua, sem console, sem painel, sem alguém garantindo que aquilo continua de pé. Esta série pega a VPS mais barata que existe e coloca coisa de verdade pra rodar nela. O primeiro passo não é instalar nada, é enxergar a máquina.
Neste texto
Uma VPS não é um serviço
O que você aluga é isto: um número de vCPUs, uma quantidade de RAM, um disco, e um limite de transferência por mês. Não vem health check. Não vem autoscale. Não vem ninguém reiniciando nada. Se o processo morrer às três da manhã, ele fica morto até você olhar.
Em troca você ganha o outro lado da moeda, que é justamente o que some quando tudo é gerenciado: não existe cold start, não existe cobrança por invocação, não existe fatura surpresa de egress, e a máquina inteira é sua o tempo todo, inclusive quando está parada.
Dos quatro recursos, um domina todos os outros. CPU você espera. Disco você compra. Transferência você otimiza. RAM é uma parede. Quando acaba, o kernel não avisa e não degrada com elegância: ele escolhe um processo e mata. Por isso esta primeira parte inteira é sobre memória.
Os dez primeiros minutos
Você recebeu um IP e uma senha de root. Antes de instalar qualquer coisa, existe uma sequência curta que separa uma máquina que é sua de uma máquina que vai ser de outra pessoa em duas semanas. Ela é curta de verdade:
# um usuário que não é root adduser deploy usermod -aG sudo deploy # sua chave, e só ela ssh-copy-id deploy@SEU_IP # em /etc/ssh/sshd_config.d/99-hardening.conf PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no systemctl restart ssh # firewall: nega tudo, libera o que você usa ufw default deny incoming ufw allow OpenSSH ufw allow 80,443/tcp ufw enable # patches de segurança sozinhos apt install unattended-upgrades dpkg-reconfigure --priority=low unattended-upgrades
Um detalhe sobre a ordem, porque a armadilha é clássica: teste a chave numa
segunda sessão SSH antes de desligar a senha. Deixe o terminal atual aberto.
Se a chave não funcionar e você já tiver reiniciado o sshd com
PasswordAuthentication no, a única saída é o console de recuperação
do provedor, que é sempre pior do que parece.
Isso é o mínimo, não é segurança. A parte que se mantém ao longo do tempo, que é rotação de log, atualização de kernel, o que realmente é atacado numa máquina exposta, fica na parte 5.
Você lê free -h errado
Este é o momento em que quase todo mundo que vem de plataforma gerenciada conclui que 2 GB não dá pra nada. Você sobe o servidor, roda o comando, e vê isto:
total used free shared buff/cache available Mem: 1971 612 158 14 1201 1203
Cento e cinquenta e oito megabytes livres. A leitura natural é pânico, e a leitura natural está errada.
Linux usa toda a RAM que sobra como cache de disco, de propósito. Todo arquivo
lido fica guardado na memória caso seja lido de novo. RAM livre é RAM
desperdiçada. Aquele 1201 de
buff/cache são arquivos, e arquivos podem ser descartados na hora em
que alguém precisar do espaço, sem perder nada, porque a cópia autoritativa está
no disco.
A coluna que responde a pergunta que você está fazendo chama-se
available: quanta memória um processo novo conseguiria pedir agora,
já contando o que o kernel devolveria se precisasse. São 1203 MB, não 158.
A regra: olhe available, nunca
free. Se available está confortável, a máquina está
bem, independente do que free mostra.
Memória anônima contra page cache
Vale separar os dois tipos, porque a distinção explica quase tudo que vem depois:
- Anônima é a memória que o processo pediu e preencheu: heap, stack, o
que veio de
malloc. Não tem cópia no disco. Se o kernel quiser esse espaço de volta, a única opção é mandar pro swap. - Page cache é memória que espelha arquivo. Tem cópia no disco por definição. O kernel joga fora quando quiser, de graça.
Quando você olha "quanto esse serviço está usando", você quase sempre está
olhando a soma dos dois. E a diferença entre eles é brutal. Estes são quatro
serviços reais, medidos com memory.stat do cgroup:
| serviço | anônima (real) | page cache | "usado" |
|---|---|---|---|
| fail2ban | 15,5 MB | 293,1 MB | 309,8 MB |
| systemd-journald | 0,6 MB | 66,1 MB | 68,9 MB |
| php-fpm | 33,9 MB | 41,4 MB | 77,3 MB |
| cron | 0,0 MB | 19,3 MB | 20,7 MB |
O fail2ban aparece em qualquer painel consumindo 310 MB. Ele usa 15,5. Os outros 293 são arquivos de log que ele leu e que o kernel resolveu manter em memória porque havia espaço sobrando. No dia em que faltar, eles evaporam sem que ninguém perceba.
O cron é o caso extremo: zero de memória própria, 19 MB
de arquivos em cache. Ele "usa" 20 MB e não usa nada.
Para ver isso em qualquer serviço da sua máquina:
cat /sys/fs/cgroup/system.slice/nome.service/memory.stat # as duas primeiras linhas são anon e file
E o top também mente
Resolvido o cache, ainda sobra um erro de contagem, e esse pega gente
experiente. O top, o htop e o ps mostram
RSS, que é toda a memória física que o processo está mapeando. O problema:
quando cinco processos compartilham a mesma libc, o mesmo OpenSSL e o mesmo
binário, o RSS conta essa cópia única cinco vezes.
Um nginx real, um master e quatro workers:
PID CMD PSS RSS 1656900 nginx: master process 1,7 10,5 1656903 nginx: worker process 3,7 12,7 1656904 nginx: worker process 3,6 12,6 1656905 nginx: worker process 3,5 12,5 1656907 nginx: worker process 3,6 12,6 soma 16,1 61,0
Somando o RSS dá 61 MB. O consumo real é 16,1 MB. A diferença de 45 MB é a mesma memória contada de novo a cada processo.
A métrica honesta chama-se PSS, de proportional set size: cada página compartilhada é dividida pelo número de processos que a mapeiam. Se quatro workers compartilham 8 MB de bibliotecas, cada um leva 2 MB na conta. A soma dos PSS de todos os processos da máquina bate com a memória realmente ocupada, coisa que a soma dos RSS nunca faz.
awk '/^Pss:/{s+=$2} END{printf "%.1f MB\n", s/1024}' /proc/PID/smaps_rollup
Isso importa mais do que parece no momento em que você for dimensionar
qualquer servidor que funciona por fork: php-fpm com vinte filhos,
gunicorn com oito workers, Puma em cluster. Pegar o RSS de um filho no
htop e multiplicar pelo número de filhos superestima a conta com
folga, e faz você comprar máquina que não precisava.
Swap não é derrota
Existe uma superstição de que ligar swap é admitir que a máquina é pequena demais. É o contrário: numa VPS de 2 GB, o swap é o que transforma um OOM killer às três da manhã em duzentos milissegundos de lentidão.
Swap é disco emprestado para páginas anônimas, que são justamente as que o kernel não consegue descartar de graça. E boa parte da memória anônima de um servidor é fria: código de inicialização de um daemon que rodou uma vez no boot, a ferramenta administrativa que você usou semana passada, tabelas que um serviço montou e nunca mais tocou. Mandar isso pro disco é lucro direto, porque a RAM liberada vira cache de coisa que está realmente sendo usada.
Dois mal-entendidos que vale desfazer:
vm.swappiness não é "porcentagem de RAM usada antes de começar a
usar swap". É o peso que o kernel dá a despejar página anônima contra despejar
page cache quando precisa recuperar memória. O padrão é 60. Em SSD isso é
perfeitamente razoável, e mexer nesse número deveria ser uma das últimas
coisas que você faz.
Swap existir e estar ocupado não é problema. O que importa é o fluxo,
não o estoque. Veja em vmstat 1, colunas si e
so, entrada e saída de swap por segundo:
vmstat 1 # si/so parados em 0, com swap ocupado → saudável, páginas frias # si/so batendo sem parar → falta RAM de verdade
Numa máquina de 2 GB, um swapfile de 2 GB é um padrão sensato, e não precisa ser partição:
fallocate -l 2G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab
Load average não é porcentagem
Os três números do uptime são médias de um minuto, cinco e
quinze. Eles não vão de 0 a 100 e não têm relação direta com uso de CPU.
Load 1,00 numa máquina de 1 vCPU significa exatamente ocupada. Load 1,00 numa
de 4 vCPUs significa um quarto ocupada. O número só quer dizer alguma coisa
dividido por nproc.
E no Linux há um detalhe a mais: a conta inclui processos em espera
ininterrupta, que é tipicamente espera de disco. Um disco com problema levanta o
load sem que a CPU esteja fazendo nada. Se o load está alto e o
top mostra CPU ociosa, o gargalo é I/O, não processamento.
Os três valores juntos são uma derivada. Um minuto muito acima de quinze minutos quer dizer que algo começou agora e está piorando. O inverso quer dizer que o pico já passou.
O OOM killer mata a coisa errada
Quando um processo pede memória anônima, não há nada pra recuperar e o swap acabou, o kernel entra em out of memory. Ele não pode devolver erro pra todo mundo, então escolhe um processo e mata.
A escolha é pelo oom_score, que é grosseiramente proporcional à
memória que o processo ocupa. Ou seja: ele mata o maior.
E o maior quase nunca é o script que vazou. É o seu banco de dados, ou a sua aplicação principal.
Esse é o sintoma que aparece nos fóruns como "minha app morreu sozinha e não tem nada no log". Não tem nada no log da aplicação porque a aplicação não decidiu morrer. Quem sabe o que aconteceu é o kernel:
journalctl -k | grep -i "killed process" # Out of memory: Killed process 4127 (postgres) total-vm:...
O conserto não é comprar RAM. O conserto é impedir que um processo sozinho possa levar a máquina toda, colocando um teto em cada serviço, e é exatamente isso que a parte 2 faz com cgroups. O objetivo é que o vazamento morra e mais nada morra junto.
O orçamento: quanto custa cada peça
Agora que os números fazem sentido, dá pra montar a conta que dá título a esta série. Tudo abaixo é memória real, medida com o processo parado, sem carga:
| peça | memória real |
|---|---|
| Ubuntu 26.04 LTS, 16 serviços de estoque | 24,8 MB |
| nginx, 1 master + 4 workers | 16,1 MB |
| PostgreSQL 17, ocioso | 19,6 MB |
| Redis 7, ocioso | 20,2 MB |
| PHP-FPM, master | 14,1 MB |
| PHP-FPM, por processo filho | 25 a 40 MB |
| Node.js, processo de aplicação | 48 a 96 MB |
Quatro leituras dessa tabela, em ordem de surpresa:
O Ubuntu custa 25 MB. Não 500. Os dezesseis serviços de estoque somados, incluindo SSH, systemd, rede, DNS, relógio e atualização automática, cabem em vinte e cinco megabytes de memória própria. A fama de pesado vem do disco e do tamanho do conjunto de pacotes. Em repouso, a RAM não tem nada com isso.
O nginx custa 16 MB. E dentro desses 16 MB ele aguenta milhares de conexões simultâneas, porque não usa uma thread por conexão. Esse modelo é o assunto da parte 3, e é metade da explicação de por que a conta de 2 GB fecha.
O Postgres ocioso custa 20 MB, que é o número que quase ninguém acredita.
Mas o Postgres tem um segundo número, e é esse que derruba máquina pequena. Cada
conexão é um processo separado. Medindo: o servidor saiu de 19,6 MB para 94,3 MB
ao abrir trinta conexões ociosas, que dá cerca de 2,5 MB por conexão sem
executar nada. O padrão de fábrica é max_connections = 100. São 250
MB de processos parados, antes de qualquer consulta, e antes do
work_mem que cada ordenação reserva por cima. É a causa número um de
morte por Postgres em VPS pequena, e o ajuste fica na parte 4.
O Node é o caro. Um processo de aplicação típico ficou entre 48 e 96 MB, e um mais pesado marcou 624 MB sozinho. Um único processo Node pode custar o mesmo que quarenta workers de nginx. Se você vem de serverless, essa é a conta que muda o seu planejamento: o custo é o runtime, não o framework.
Fechando a conta
Com esses números, uma pilha completa em 2 GB fica assim:
2048 MB total da máquina - 150 sistema e estruturas do kernel (reserva) - 16 nginx - 20 PostgreSQL ocioso - 150 60 conexões de banco, no pior caso - 20 Redis - 300 3 aplicações Node de ~100 MB ──────── 656 MB sobrando para cache e para pico
Proxy reverso, banco de dados, cache em memória e três aplicações. Na VPS mais barata que existe, com um terço da memória ainda livre.
A reserva de 150 MB para o kernel é o único número desta conta que não é medição direta. Estruturas de kernel (slab, tabelas de página, buffers de rede) escalam com o que a máquina faz, então medir isso numa caixa e aplicar noutra não teria sentido. Tratei como reserva, e reserva folgada.
Nada disso é uma máquina aguentando dez mil requisições por segundo. É uma máquina fazendo o que um produto pequeno faz. E o ponto da série é exatamente esse: a distância entre "não tenho nada" e "preciso de um cluster" é muito maior do que o catálogo de serviços deixa você imaginar.
Os seis comandos
Para fechar, a caixa de ferramentas mínima. Com esses seis você responde "o que essa máquina está fazendo agora", que é a pergunta que a plataforma gerenciada vinha respondendo por você:
| comando | a pergunta que ele responde |
|---|---|
free -h | tenho memória? olhe available |
vmstat 1 | estou sob pressão de verdade? colunas si e so |
systemd-cgtop | quem está gastando, por serviço |
df -h e du -xh -d1 / | o disco, que é a outra parede |
journalctl -u X -f | o que esse serviço está dizendo |
ss -tulpn | quem está escutando, em qual porta |
O systemd-cgtop é o mais subestimado da lista. Ele mostra consumo
agregado por serviço em vez de por processo, o que resolve sozinho o problema de
somar filhos de php-fpm ou workers de nginx na mão.
O que vem na parte 2
Você sabe ler a máquina. Ela ainda não roda nada seu, e o primeiro obstáculo é mais humilhante do que parece: você sobe a aplicação, fecha o terminal, e ela morre junto.
A parte 2 é sobre a virada de chave que falta pra quem vem de plataforma: sua aplicação não é um deploy, é um processo. Um processo que precisa sobreviver a um logout, voltar sozinho depois de um crash, subir no boot, gravar log onde alguém consiga achar, e principalmente ter um teto de memória, pra que o dia em que ele vazar seja o dia em que só ele morre.
kelvin@cluxnei:~/blog$