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 konfiguracjiNie 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 redukcji3. 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 pracy | Pamięć | Limit na serwer (32 GB RAM) |
|---|---|---|
| Proces BEAM | 2.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 MB | 32 000 - 64 000 |
| Proces Node.js (cluster) | 50-200 MB | 160 - 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ę)
| Technologia | Requesty/sek (1 serwer) | Requesty przy 5 serwerach |
|---|---|---|
| Node.js (Express) | 8 000 | 40 000 |
| Java (Spring Boot) | 15 000 | 75 000 |
| Go (net/http) | 45 000 | 225 000 |
| Elixir (Phoenix) | 55 000 | 275 000 |
Phoenix na 1 serwerze osiąga więcej niż Node.js na 5 serwerach.
Latencja (ms) - 1000 jednoczesnych połączeń
| Metryka | Node.js | Java | Go | Elixir |
|---|---|---|---|---|
| p50 | 12 ms | 8 ms | 3 ms | 4 ms |
| p95 | 85 ms | 45 ms | 12 ms | 11 ms |
| p99 | 340 ms | 180 ms | 25 ms | 18 ms |
| p99.9 | 1 200 ms | 500 ms | 45 ms | 22 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)
| Technologia | Połączenia (1 serwer) | RAM zużyty |
|---|---|---|
| Node.js + Socket.io | ~10 000 | 8 GB |
| Java + Spring WebSocket | ~20 000 | 12 GB |
| Go + Gorilla WS | ~100 000 | 4 GB |
| Elixir + Phoenix Channels | ~200 000 | 3 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ącElixir + 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| Metryka | Node.js (5 serwerów) | Elixir (1 serwer) |
|---|---|---|
| Koszt/miesiąc | 9 000 PLN | 3 100 PLN |
| Koszt/rok | 108 000 PLN | 37 200 PLN |
| Koszt/3 lata | 324 000 PLN | 111 600 PLN |
| Złożoność DevOps | Load balancer + Redis + deploy na 3 serwery | 1 serwer, 1 deploy |
| Punkty awarii | 5 (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/WhatsApp99% 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
| Aspekt | Node.js | Java | Elixir |
|---|---|---|---|
| Vertical scaling (1 serwer) | Słabe (1 wątek) | Średnie (GC limity) | Bardzo dobre |
| Horizontal scaling | Wymaga Redis + LB | Wymaga session affinity | Distributed Erlang |
| Load balancing | Zewnętrzny (nginx/ALB) | Zewnętrzny | Wbudowany (libcluster) |
| Session state | Redis/Memcached | Redis/Hazelcast | W procesie BEAM |
| Cache | Redis | Redis/Ehcache | W procesie BEAM |
| Pub/Sub (klaster) | Redis Pub/Sub | Kafka/RabbitMQ | Phoenix PubSub (wbudowany) |
| Zero-config clustering | ✗ | ✗ | ✓ (Distributed Erlang) |
Podsumowanie: ile naprawdę potrzebujesz serwerów
| Skala systemu | Node.js + Redis | Java + Spring | Elixir + Phoenix |
|---|---|---|---|
| 500 użytkowników | 1-2 serwery | 1 serwer | 1 serwer |
| 2 000 użytkowników | 2-3 serwery | 1-2 serwery | 1 serwer |
| 5 000 użytkowników | 3-5 serwerów | 2-3 serwery | 1 serwer |
| 10 000 użytkowników | 5-8 serwerów | 3-5 serwerów | 1-2 serwery |
| 50 000 użytkowników | 15-25 serwerów | 8-15 serwerów | 2-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.