Plano: 100 mil utilizadores num só VPS, pronto para CDN

Versão do documento 2026-05-29 · Objetivo: o atual e único VPS deve aguentar até 100 mil MAU sem cair, e a aplicação mudar-se para uma CDN sem costuras quando chegar a altura.

Números alvo

MetricValorSource
MAU100,000o objetivo
DAU30,00030 % do MAU, normal numa aplicação utilitária
WS simultâneos10 000 (pico 15 mil)~10 % do MAU durante o dia, 15 % no pico
Ações por dia90,0003 aberturas ou chamadas por utilizador diário
Pedidos da API HTTP por segundo (média)3-5sobretudo key_create, host_sync e crash_report
Pedidos da API HTTP por segundo (pico)30-50após um lançamento em massa que atualiza os números

A arquitetura de hoje

Fase A — o cliente Android AGORA, v6.66

Goal: reduzir o número de pedidos supérfluos dos clientes para o servidor ter menos trabalho.

A1. Recuo exponencial das repetições HTTP para as chaves de anfitrião pendentes

File: HostService.kt:1424-1432. Hoje syncPendingHostKeys() é chamada a cada 60 s e bate em Api.keyCreate por cada chave pendente sem qualquer recuo.

Guardar em Prefs per-localId: attempt_at_id e attempt_count_id. Recuo: 30 s → 60 s → 2 min → 5 min → 15 min → 30 min (limite). Reposto em caso de sucesso.

A2. Recuo das repetições HTTP para os pacotes de convidado pendentes

File: Vault.kt:213. A mesma lógica.

A3. Comparar antes de incrementar overridesVersion

File: GuestConn.kt (5+ locais). Comparar o novo parseNumbers com o anterior, por hash ou diretamente. Se forem iguais, não incrementar AppState.overridesVersion. Isto elimina as ondas de recomposição — 80 leituras de SharedPreferences × 20 cartões — em cada inicialização do WebSocket.

Fase B — proteção do servidor e prontidão para CDN NOW

Goal: proteger o servidor de uma tempestade mesmo com um cliente defeituoso e desligar o endereço do APK do domínio.

B1. Um limite de frequência em host_hello e guest_hello

In server.php case 'host_hello' / 'guest_hello': se o mesmo device_id / user_key enviou um hello há menos de N segundos, fechar a ligação sem tocar na base. N = 5 s.

Proteção caso um cliente com a lógica partida envie cem hellos por segundo.

B2. Limite de pending_host_msgs por host_id

In enqueueHostMsg: se este host_id já tem mais de 200 linhas, apagar a mais antiga com DELETE FROM pending_host_msgs WHERE host_id=? ORDER BY id ASC LIMIT 1. Proteção contra o crescimento sem fim da fila perante atividade maliciosa.

B3. Prontidão para CDN: mover o endereço do APK para a configuração

In _config.php:

$GLOBALS['apk_url'] = 'https://entrixy.com/entrixy.apk';
$GLOBALS['download_page_url'] = 'https://entrixy.com/download';

In api/version_check.php: 'download_url' => $GLOBALS['apk_url'].

Quando a mudança para a CDN acontecer, muda exatamente uma linha em _config.php to https://apk.entrixy.com/entrixy.apk (Cloudflare R2 com domínio próprio). Não é preciso lançar a aplicação.

B4. O APK numa CDN — verificação de integridade

O cliente já verifica a assinatura do APK ao descarregar (keystore de depuração, ver build.gradle.kts:23-29). Para uma CDN isso basta: mesmo que um nó da CDN troque o ficheiro, a assinatura não confere e o Android recusa instalá-lo. No cliente não é preciso mais nada.

Fase C — escalar o Workerman QUANDO BATERMOS NO TETO

O atual count=1 O Workerman aguenta sem dificuldade cerca de 10 mil WebSockets em simultâneo — o server.php já arranca com ele na linha 40. O primeiro estrangulamento será o tempo de CPU do PHP no pico de troca: 10 mil pongs por segundo mais as ações.

C1. Workerman count=2–4 com Redis pub/sub para o estado partilhado

Today $hosts, $guests, $calls são vetores em memória dentro de um único processo. Para correr vários processos é preciso movê-los para o Redis:

Se uma mensagem chega ao trabalhador W1 mas o destinatário está em W2, encaminha-se por Redis pub/sub.

Effect: escalonamento linear até ao número de núcleos do VPS. Com quatro núcleos são cerca de 40 mil em simultâneo.

C2. Uma alternativa: uma máquina, nginx ou HAProxy à frente do Workerman, repartido por client_ip

Menos flexível e sem precisar de Redis. Cada utilizador cai sempre no mesmo trabalhador. Mas parte-se quando o remetente e o destinatário ficam em trabalhadores diferentes.

Fase D — quando mudar para uma CDN LATER

  1. Um bucket Cloudflare R2 → carregar entrixy.apk.
  2. Domínio próprio apk.entrixy.com → ligá-lo através da CDN da Cloudflare.
  3. In _config.php change $apk_url para o novo.
  4. No script de implantação, depois de assembleRelease add r2 cp app-release.apk apk-bucket/entrixy.apk.

O que NÃO fazer agora

O que está a ser feito agora

  1. A fase A completa → versão v6.66.
  2. A fase B completa, sem reiniciar o servidor WebSocket, para testar antes de confirmar.
  3. A fase C faz-se num ramo à parte, precisa de Redis no VPS e é uma tarefa própria.
  4. A fase D está descrita acima e executa-se quando for preciso.