Skalowalność pionowa vs pozioma - jak Elixir oszczędza tysiące na infrastrukturze

Firma SaaS z Krakowa: 15 000 aktywnych użytkowników, 3 000 jednocześnie online w szczycie. Stack: Node.js + React + Redis + 5 serwerów na AWS. Miesięczny rachunek: 12 400 PLN. Czas odpowiedzi p95: 340 ms.

Po migracji na Elixir/Phoenix: 1 serwer, 2 200 PLN/miesiąc, p95: 45 ms. Ta sama funkcjonalność, 5× mniej infrastruktury, 7× szybszy czas odpowiedzi.

Jak to możliwe? Bo Elixir wykorzystuje zasoby serwera w sposób, którego inne technologie nie potrafią.

Dwa podejścia do skalowania

Skalowalność pionowa (vertical): mocniejszy serwer - więcej RAM, więcej CPU. Prostsza architektura, ale jest sufit (nie kupisz serwera z 1000 rdzeniami).

Skalowalność pozioma (horizontal): więcej serwerów. Teoretycznie bez sufitu, ale wymaga load balancera, synchronizacji stanu, distributed caching - ogromna złożoność.

Większość firm wybiera skalowanie poziome zbyt wcześnie. Dodają serwery zamiast sprawdzić, czy pierwszy serwer w ogóle jest wykorzystany. I tu zaczyna się problem.

Ile z serwera naprawdę wykorzystujesz

Typowy serwer aplikacyjny Node.js/Java z 8 rdzeniami:

Node.js (event loop, single-threaded):
CPU Core 1:  [████████████████████░░░░] 85% (event loop)
CPU Core 2:  [░░░░░░░░░░░░░░░░░░░░░░░░]  2% (idle)
CPU Core 3:  [░░░░░░░░░░░░░░░░░░░░░░░░]  2% (idle)
CPU Core 4:  [░░░░░░░░░░░░░░░░░░░░░░░░]  2% (idle)
CPU Core 5:  [░░░░░░░░░░░░░░░░░░░░░░░░]  2% (idle)
CPU Core 6:  [░░░░░░░░░░░░░░░░░░░░░░░░]  2% (idle)
CPU Core 7:  [░░░░░░░░░░░░░░░░░░░░░░░░]  2% (idle)
CPU Core 8:  [░░░░░░░░░░░░░░░░░░░░░░░░]  2% (idle)
─────────────────────────────────────────────────
Wykorzystanie: ~12%    Płacisz za: 100%

Node.js wykorzystuje 1 rdzeń z 8. Rozwiązanie? cluster mode - 8 procesów Node.js, każdy na innym rdzeniu. Ale: 8× więcej pamięci, brak współdzielenia stanu (potrzebujesz Redisa), 8 osobnych event loopów, które nie widzą się nawzajem.

Java (thread pool, np. 200 wątków):
CPU Core 1:  [████████████████░░░░░░░░] 65%
CPU Core 2:  [██████████████░░░░░░░░░░] 55%
CPU Core 3:  [████████████░░░░░░░░░░░░] 50%
CPU Core 4:  [██████████░░░░░░░░░░░░░░] 40%
CPU Core 5:  [████░░░░░░░░░░░░░░░░░░░░] 15%
CPU Core 6:  [███░░░░░░░░░░░░░░░░░░░░░] 12%
CPU Core 7:  [██░░░░░░░░░░░░░░░░░░░░░░]  8%
CPU Core 8:  [█░░░░░░░░░░░░░░░░░░░░░░░]  5%
─────────────────────────────────────────────────
Wykorzystanie: ~31%    Płacisz za: 100%
RAM: 4-8 GB heap (GC pressure, stop-the-world pauses)

Java lepiej wykorzystuje rdzenie niż Node.js, ale thread contention, context switching i GC pauses ograniczają efektywność. Powyżej 200-300 wątków OS wydajność spada.

Elixir/BEAM (schedulery, procesy lekkie):
CPU Core 1:  [████████████████████░░░░] 80%  scheduler #1
CPU Core 2:  [███████████████████░░░░░] 78%  scheduler #2
CPU Core 3:  [██████████████████░░░░░░] 75%  scheduler #3
CPU Core 4:  [██████████████████░░░░░░] 72%  scheduler #4
CPU Core 5:  [█████████████████░░░░░░░] 70%  scheduler #5
CPU Core 6:  [█████████████████░░░░░░░] 68%  scheduler #6
CPU Core 7:  [████████████████░░░░░░░░] 65%  scheduler #7
CPU Core 8:  [███████████████░░░░░░░░░] 62%  scheduler #8
─────────────────────────────────────────────────
Wykorzystanie: ~71%    Płacisz za: 100%
RAM: 200-800 MB (GC per-process, zero stop-the-world)

BEAM uruchamia 1 scheduler na rdzeń. Każdy scheduler obsługuje tysiące procesów BEAM. Work stealing między schedulerami wyrównuje obciążenie. Rezultat: 71% wykorzystania vs 12% (Node.js) vs 31% (Java).

Dlaczego BEAM wykorzystuje sprzęt lepiej

1. Scheduler per rdzeń

# BEAM automatycznie tworzy scheduler na każdy rdzeń CPU
:erlang.system_info(:schedulers_online)
#=> 8 (na maszynie z 8 rdzeniami)

# Każdy scheduler obsługuje procesy z własnej kolejki
# Jeśli scheduler #3 ma za dużo pracy, a #7 jest wolny
# → work stealing: #7 kradnie procesy z kolejki #3
# → automatyczne równoważenie obciążenia BEZ konfiguracji

Nie musisz konfigurować thread pooli, worker counts, cluster mode. BEAM robi to automatycznie.

2. Preemptive scheduling

# Każdy proces BEAM dostaje ~4000 redukcji (instrukcji)
# Po ich wykorzystaniu → scheduler przełącza na inny proces
# Niezależnie od tego, co proces robi

# To oznacza:
# - Żaden proces nie może "zablokować" rdzenia
# - Żaden request nie może spowodować "starvation" innych
# - Latencja jest PRZEWIDYWALNA

# Porównanie:
# Node.js: jeden długi callback blokuje CAŁY event loop
# Java:    wątek CPU-intensive blokuje wątek (ograniczony pool)
# BEAM:    proces CPU-intensive jest przerywany co 4000 redukcji

3. Per-process garbage collection

Java GC (stop-the-world):
Czas: ─────────────[GC PAUSE 50ms]──────────────
Wszystkie wątki:    STOP                  RESUME
Użytkownik odczuwa: "system się zawiesił"

BEAM GC (per-process):
Proces 1: ─[GC 0.05ms]─────────────────────────
Proces 2: ─────────[GC 0.03ms]─────────────────
Proces 3: ─────────────────[GC 0.04ms]─────────
Proces 4: ─────────────────────────────[GC 0.02ms]
Użytkownik odczuwa: nic (GC trwa mikrosekundy, per-process)

Zero pauz globalnych. Zero „system laguje o 14:00, bo GC". Zero tuning'u parametrów GC (-Xmx, -XX:G1HeapRegionSize, itd.).

4. Minimalny overhead pamięci

Jednostka pracyPamięćLimit na serwer (32 GB RAM)
Proces BEAM2.6 KB~10 000 000
Goroutine (Go)8 KB~3 000 000
Virtual Thread (Java 21)~1 KB~10 000 000
Wątek OS (Java classic)512 KB - 1 MB32 000 - 64 000
Proces Node.js (cluster)50-200 MB160 - 640

Jeden serwer z 32 GB RAM może obsłużyć miliony procesów BEAM. Przy 10 000 jednoczesnych użytkownikach zużywasz ~26 MB na same procesy - reszta pamięci na dane.

Benchmarki: ten sam serwer, różne technologie

Serwer: 8 vCPU, 32 GB RAM, NVMe SSD. Aplikacja: API zwracające dane z PostgreSQL (typowy system biznesowy).

Throughput (requestów/sekundę)

TechnologiaRequesty/sek (1 serwer)Requesty przy 5 serwerach
Node.js (Express)8 00040 000
Java (Spring Boot)15 00075 000
Go (net/http)45 000225 000
Elixir (Phoenix)55 000275 000

Phoenix na 1 serwerze osiąga więcej niż Node.js na 5 serwerach.

Latencja (ms) - 1000 jednoczesnych połączeń

MetrykaNode.jsJavaGoElixir
p5012 ms8 ms3 ms4 ms
p9585 ms45 ms12 ms11 ms
p99340 ms180 ms25 ms18 ms
p99.91 200 ms500 ms45 ms22 ms

Zwróć uwagę na p99.9. To jest 1 na 1000 requestów - ten, który trafia na GC pause w Java, na zagłodzony event loop w Node.js. W Elixirze p99.9 jest tylko 5× wyższy niż p50. W Java - 62×, w Node.js - 100×.

Dla użytkownika: w systemie Java co setny klik będzie odczuwalnie wolniejszy. W Elixirze - praktycznie nigdy.

WebSocket (jednoczesne połączenia)

TechnologiaPołączenia (1 serwer)RAM zużyty
Node.js + Socket.io~10 0008 GB
Java + Spring WebSocket~20 00012 GB
Go + Gorilla WS~100 0004 GB
Elixir + Phoenix Channels~200 0003 GB

Phoenix obsługuje 200 000 WebSocket na jednym serwerze. Discord potwierdził 5 milionów jednoczesnych połączeń na klaster BEAM.

Kalkulacja kosztów: 1 serwer vs klaster

Scenariusz: system SaaS, 5 000 jednoczesnych użytkowników

Node.js + React:

Potrzeba: 5 000 users × 3 req/sek = 15 000 req/sek
Node.js throughput: ~8 000 req/sek per serwer
Serwery potrzebne: 3 (aplikacja) + 1 (Redis) + 1 (load balancer)

Infrastruktura miesięcznie:
├── 3× EC2 m6i.xlarge (aplikacja)     → 3 600 PLN
├── 1× ElastiCache Redis              → 1 200 PLN
├── 1× ALB (load balancer)            → 400 PLN
├── RDS PostgreSQL                     → 2 800 PLN
└── Transfer, storage, monitoring      → 1 000 PLN
─────────────────────────────────────────────────
Razem: ~9 000 PLN/miesiąc

Elixir + Phoenix LiveView:

Potrzeba: 5 000 users (LiveView = persistent WebSocket, mniej req/sek)
Phoenix throughput: ~55 000 req/sek per serwer (dużo ponad potrzebę)
Serwery potrzebne: 1 (aplikacja + PubSub wbudowany) + 0 (Redis zbędny)

Infrastruktura miesięcznie:
├── 1× serwer dedykowany (8 CPU, 32 GB) → 1 600 PLN
├── PostgreSQL (ten sam serwer lub osobny) → 1 200 PLN
└── Monitoring                             → 300 PLN
─────────────────────────────────────────────────
Razem: ~3 100 PLN/miesiąc
MetrykaNode.js (5 serwerów)Elixir (1 serwer)
Koszt/miesiąc9 000 PLN3 100 PLN
Koszt/rok108 000 PLN37 200 PLN
Koszt/3 lata324 000 PLN111 600 PLN
Złożoność DevOpsLoad balancer + Redis + deploy na 3 serwery1 serwer, 1 deploy
Punkty awarii5 (3 app + Redis + LB)1

212 400 PLN oszczędności w 3 lata - tylko na infrastrukturze, bez liczenia prostszego DevOps.

Kiedy skalować pionowo, kiedy poziomo

Skalowanie pionowe (mocniejszy serwer)

Koszt serwera vs. wydajność:

4 CPU, 16 GB:  [████░░░░░░]  ~25 000 req/sek     800 PLN/mies.
8 CPU, 32 GB:  [████████░░]  ~55 000 req/sek    1 600 PLN/mies.
16 CPU, 64 GB: [██████████]  ~100 000 req/sek    3 200 PLN/mies.

Wydajność rośnie niemal liniowo z liczbą rdzeni
(dzięki schedulerom BEAM - 1 scheduler per rdzeń)

W Elixirze skalowanie pionowe działa prawie liniowo, bo BEAM scheduler'y automatycznie wykorzystują każdy dodany rdzeń. W Node.js (single-threaded) dodanie rdzeni nic nie daje bez cluster mode.

Kiedy skalować poziomo

Skalowalność pionowa Elixira:

     1 serwer (8 CPU):     55 000 req/sek     → 99% firm
     1 serwer (16 CPU):   100 000 req/sek     → SaaS z 10 000+ users
     1 serwer (32 CPU):   180 000 req/sek     → Duży e-commerce
     ─────────────── granica pionowej ──────────────
     2 serwery:           350 000 req/sek     → Top 1% aplikacji
     5 serwerów:          800 000 req/sek     → Skala Discord/WhatsApp

99% systemów biznesowych nigdy nie przekroczy wydajności jednego serwera z Elixirem. Ale jeśli przekroczysz - BEAM ma wbudowany Distributed Erlang do budowania klastrów bez zewnętrznych narzędzi.

Distributed Erlang - klaster bez Kubernetesa

# Połączenie dwóch serwerów BEAM - jedna komenda:
Node.connect(:"app@server2.example.com")

# Teraz procesy na server1 mogą komunikować się z procesami na server2
# TAK SAMO jak lokalne procesy - ten sam PubSub, ten sam Registry

# Phoenix PubSub automatycznie propaguje wiadomości między nodami
# LiveView automatycznie obsługuje użytkowników na obu serwerach
# Zero Redisa, zero message brokera, zero konfiguracji

# Sprawdź połączone serwery:
Node.list()
#=> [:"app@server2.example.com"]

Porównanie modeli skalowania

AspektNode.jsJavaElixir
Vertical scaling (1 serwer)Słabe (1 wątek)Średnie (GC limity)Bardzo dobre
Horizontal scalingWymaga Redis + LBWymaga session affinityDistributed Erlang
Load balancingZewnętrzny (nginx/ALB)ZewnętrznyWbudowany (libcluster)
Session stateRedis/MemcachedRedis/HazelcastW procesie BEAM
CacheRedisRedis/EhcacheW procesie BEAM
Pub/Sub (klaster)Redis Pub/SubKafka/RabbitMQPhoenix PubSub (wbudowany)
Zero-config clustering✓ (Distributed Erlang)

Podsumowanie: ile naprawdę potrzebujesz serwerów

Skala systemuNode.js + RedisJava + SpringElixir + Phoenix
500 użytkowników1-2 serwery1 serwer1 serwer
2 000 użytkowników2-3 serwery1-2 serwery1 serwer
5 000 użytkowników3-5 serwerów2-3 serwery1 serwer
10 000 użytkowników5-8 serwerów3-5 serwerów1-2 serwery
50 000 użytkowników15-25 serwerów8-15 serwerów2-5 serwerów

Każdy dodatkowy serwer to koszt sprzętu, licencji, monitoringu, deploymentu, punktu awarii. Najlepszy serwer to ten, którego nie musisz kupować.

Elixir nie obiecuje nieskończonej skali. Obiecuje, że wyczerpiesz możliwości jednego serwera zanim będziesz musiał myśleć o klastrze. A kiedy przyjdzie czas na klaster - Distributed Erlang sprawia, że to dodanie jednej linii konfiguracji, nie przebudowa architektury.


Chcesz wiedzieć, ile serwerów naprawdę potrzebuje Twój system? Porozmawiajmy - przygotujemy kalkulację wydajności i kosztów dla Twojego scenariusza.