A instância aguenta até não aguentar. Você sobe RAM, sobe CPU, o p95 continua feio no horário de almoço. Vertical tem teto. O próximo movimento é óbvio no slide e traiçoeiro no login: colocar um Nginx na frente e duas (depois N) cópias da aplicação.
Balancear carga é distribuir request entre processos ou máquinas para que nenhuma peça sozinha vire o gargalo. O Nginx faz isso bem, barato, com pouca mágica. O que quebra não é o upstream. É o estado que você esqueceu debaixo do tapete.
Por que uma caixa maior para
Uma máquina só concentra CPU, disco, rede e falha. Mesmo com recurso de sobra, o teto de conexões, o teto de workers do PHP-FPM e o teto de um único ponto de falha aparecem juntos. Incidente de kernel, de disco, de zona: tudo some junto.
Há também o perfil da carga. Muita request curta convive mal com poucas requests longas na mesma caixa. Relatório de seis segundos ocupa o worker que deveria servir o checkout. Horizontal permite, depois, separar papéis. Primeiro, já permite somar capacidade em fatias.
O teste de carga sincero mostra o platô. Requests por segundo param de crescer, latência sobe, CPU talvez nem esteja em 100%. Você não precisa de um servidor maior. Precisa de mais servidores e de um jeito de chegar neles.
Nginx como porta, não como aplicação
O padrão simples: o Nginx escuta 80/443, termina TLS, serve estático, e o upstream lista app1, app2, app3 — containers, VMs, sockets. least_conn ou round-robin. Health check ou max_fails. Request nova cai em um nó. Se um nó morre, os outros seguem.
Isso já aumenta a capacidade de trabalho stateless. Endpoint que só lê banco e devolve JSON escala quase linear até o banco reclamar. Endpoint que grava upload em /var/www/uploads da máquina local não escala: o arquivo está no disco que a próxima request talvez não veja.
O balanceador na AWS (ALB/NLB) é o mesmo desenho com menos Nginx para gerenciar. O princípio não muda: a porta não guarda o seu $_SESSION.
flowchart TD
A[Cliente] --> B[Nginx / LB]
B --> C[App A]
B --> D[App B]
B --> E[App C]
C --> F{Onde está a sessão?}
D --> F
E --> F
F -->|Disco local| G[Login some]
F -->|Sticky| H[Nó preso, escala falsa]
F -->|Token ou store| I[Qualquer nó atende]
classDef ouro fill:#FBF5E4,stroke:#C9A227,color:#0B1F18,stroke-width:1.4px
classDef verde fill:#F3FAF7,stroke:#1E7A57,color:#0B1F18,stroke-width:1.4px
classDef fim fill:#14513B,stroke:#0F3328,color:#FBFAF6,stroke-width:1.4px
class A,B ouro
class C,D,E,F verde
class G,H,I fim
Sticky session: o atalho que finge escala
Sessão em arquivo ou em memória do processo vive no nó que autenticou. A próxima request no outro nó não conhece o usuário. O “fix” clássico é sticky (afinidade): o balanceador manda o mesmo cliente sempre ao mesmo backend, por cookie ou IP.
Sticky reduz o sintoma e cria outro. O nó gordo fica gordo. O nó novo sobe vazio. Deploy que derruba o nó A desloga quem estava colado nele. Usuário atrás de NAT aparece como um IP só e concentra carga. Você comprou vários servidores e opera como se fosse um, com failover pior.
Há casos estreitos em que sticky é ponte: WebSocket mal desenhado, legado que você vai matar no trimestre. Como arquitetura, é dívida. O objetivo do horizontal é qualquer nó servir qualquer request autenticada.
Token ou store: o estado sai da máquina
Dois caminhos sadios.
Sessão em store compartilhado. Redis, Memcached, banco. O PHP (ou o framework) deixa de escrever em arquivo local. Qualquer nó lê a mesma sessão. Você ainda tem sessão server-side: logout central, dados maiores, revogação fácil. Paga-se com mais um componente e com a disciplina de não guardar objeto enorme no Redis.
Token. O cliente guarda um token opaco ou um JWT. O servidor valida assinatura ou consulta uma tabela de tokens. Não há afinidade. Escalar nós não mexe no login. O custo muda de lugar: rotação de chave, expiração, revogação, não colocar dado sensível em JWT que o cliente lê, HTTPS obrigatório.
Em API, token costuma ser o caminho mais simples para N servidores. Em app clássica com formulário e cookie de sessão, store compartilhado costuma ser a menor mudança. Os dois vencem sticky. Os dois exigem que arquivo, cache e upload também saiam do disco local — senão você só resolveu o login.
| Estado | No nó local | Com vários nós |
|---|---|---|
| Sessão em arquivo | Funciona | Login fantasma |
| Sticky session | — | Escala irregular, deploy doloroso |
| Sessão no Redis | — | Qualquer nó atende |
| Token (Sanctum, JWT, opaco) | — | Qualquer nó atende |
Upload em /tmp |
Funciona | Arquivo some |
| Cache em APCu só | Rápido | Inconsistente entre nós |
O que mais precisa sair do servidor
Lista curta e ignorada: arquivos de usuário, jobs em fila locais, cron que “roda em um dos nós” sem eleição, cache de opcode é ok (é por processo), cache de dado de negócio em memória local não é ok se a invalidação precisa ser global.
Log também muda. Três nós, três storage/logs. Sem agregar, o incidente vira SSH em roleta. Não é motivo para não balancear. É motivo para tratar observabilidade como parte do horizontal, não como extra.
Deploy: se você sobe código em um nó e o outro fica na versão antiga, o balanceador mistura contratos. Drain, health check, versão. Horizontal sem disciplina de release é incidente intercalado.
Por onde começar sem teatro
- Meça o platô da caixa única.
- Coloque o Nginx (ou o LB) na frente de duas cópias iguais.
- Quebre o login de propósito: acesse, troque o nó, veja a sessão sumir.
- Mova sessão para token ou Redis.
- Repita o teste de carga. Só então acrescente o terceiro nó.
O passo 3 é o que o tutorial pula. Sem ele, o time descobre sticky em produção, no horário em que o marketing ligou a campanha. Vários servidores não são “mais do mesmo”. São o mesmo código sem estado local. O Nginx só distribui. O desenho é que decide se a distribuição mente.
No PHP One esse ciclo aparece cedo: a request, o FPM, e o que deixa de caber num processo só.
Leia também: Escalabilidade vertical e horizontal e PHP na web: Nginx, FPM e o ciclo da request.