Zero downtime bez Kubernetes - jak BEAM robi hot code reload
Piątek, 16:47. Znalazłeś buga w produkcji. Krytyczny - użytkownicy nie mogą zapłacić. Fix jest prosty, trzy linie kodu. Ale wdrożenie oznacza:
- build (5 min)
- deploy (3 min)
- restart serwera (30 sekund niedostępności)
- smoke testy (5 min)
I to pod warunkiem, że wszystko pójdzie gładko. W między czasie 47 użytkowników dostanie błąd 502. Trzech z nich otworzy ticket w support. Jeden napisze na Twittera.
A co by było, gdybyś mógł zmienić kod bez restartu? Użytkownicy kontynuują pracę, a Ty podmieniasz logikę pod nimi.
To nie jest magia. To jest hot code reload w BEAM.
Co to jest hot code reload
Większość runtime'ów wymaga restartu procesu, żeby załadować nowy kod. Python, Ruby, Node.js, Java, Go - wszyscy musisz ubić proces i postawić go od nowa.
BEAM (maszyna wirtualna Erlanga/Elixira) jest inna. Może ładować nowy kod w locie, podczas gdy stare procesy nadal działają.
Jak to działa
BEAM trzyma w pamięci dwie wersje kodu jednocześnie:
┌─────────────────────────────────────┐
│ Pamięć BEAM │
├─────────────────┬───────────────────┤
│ Moduł v1 │ Moduł v2 │
│ (stary) │ (nowy) │
├─────────────────┼───────────────────┤
│ Procesy już │ Nowe wywołania │
│ działające │ idą tutaj │
└─────────────────┴───────────────────┘Stare procesy kończą pracę na starej wersji. Nowe wywołania trafiają do nowej wersji. Gdy ostatni proces zakończy się na starej wersji - garbage collector ją usuwa.
Zero restartu. Zero zerwanych połączeń.
Przykład z życia: naprawa buga w LiveView
Masz formularz płatności z błędem w walidacji:
# payment_live.ex - WERSJA Z BUGIEM
def handle_event("validate", %{"amount" => amount}, socket) do
# BUG: amount przychodzi jako string, porównanie z integerem zawsze false
if amount > 100 do
{:noreply, assign(socket, fee: 10)}
else
{:noreply, assign(socket, fee: 0)}
end
endUżytkownicy nie widzą opcji "przedpłać za grosze" bo warunek nigdy nie jest spełniony.
Fix w tradycyjnym stacku
- Napraw kod lokalnie
- Commit, push
- CI build (3-8 min)
- Deploy na staging
- Testy manualne
- Deploy na prod
- Restart serwera (downtime)
- Smoke testy
Czas: 30-60 minut. Downtime: 30 sekund - 2 minuty.
Fix z hot code reload
# payment_live.ex - WERSJA NAPRAWIONA
def handle_event("validate", %{"amount" => amount}, socket) do
# FIX: konwersja string → integer
amount = String.to_integer(amount)
if amount > 100 do
{:noreply, assign(socket, fee: 10)}
else
{:noreply, assign(socket, fee: 0)}
end
endW konsoli produkcji:
# Łączysz się z running node
iex --remsh myapp@prod-server --cookie secret
# Kompilujesz i ładujesz nowy kod
iex> c("payment_live.ex")
[PaymentLive]
# Gotowe. Nowy kod działa od razu.Czas: 2-3 minuty. Downtime: 0.
Użytkownicy, którzy mieli otwarty formularz? Działają dalej. Nowi użytkownicy? Dostają naprawiony kod.
Kiedy to ma sens w praktyce
Hot code reload nie jest do wszystkiego. Ma swoje miejsce w hierarchii narzędzi.
Poziom 1: Hot patch w nagłej sytuacji
Krytyczny bug, który blokuje użytkowników. Nie masz czasu na pełny deploy. Fix jest mały i lokalizowany.
Przykłady:
- błąd w warunku if/else
- zła wartość domyślna
- literówka w komunikacie
- logika rate limitingu
Czas oszczędności: 20-50 minut.
Poziom 2: Tuning w czasie rzeczywistym
Zmieniasz parametry, które nie wymagają pełnego release'a.
# Konsola produkcji - zmiana konfiguracji bez restartu
iex> Application.put_env(:myapp, :max_connections, 500)
:ok
# Zmiana log level dla konkretnego modułu
iex> Logger.configure(level: :debug)
:okPrzykłady:
- zmiana poziomu logowania dla debugu
- zwiększenie timeout'u
- włączenie feature flagi
Poziom 3: Pełne upgrade bez downtime
BEAM wspiera appup files - instrukcje jak przeprowadzić upgrade z wersji A do B bez zatrzymania systemu.
# appup example - instrukcje upgrade'u
{'myapp', '1.2.0', '1.1.0', [
% wersja 1.1.0 → 1.2.0
{load_module, 'PaymentLive'},
{update, 'PaymentWorker', {advanced, []}},
% wersja 1.2.0 → 1.1.0 (rollback)
{load_module, 'PaymentLive'},
{update, 'PaymentWorker', {advanced, []}}
]}.To pozwala na:
- zmianę struktury procesów
- migrację stanu między wersjami
- rollback w przypadku problemów
Używane przez: Ericsson (telekomunikacja), WhatsApp (miliardy połączeń).
Jak to porównać do innych technologii
| Aspekt | Node.js/Python/Go | Kubernetes rolling deploy | BEAM hot reload |
|---|---|---|---|
| Restart wymagany | Tak | Tak (ale graceful) | Nie |
| Downtime | Tak | Minimalny (nowe pody) | Zero |
| Zerwane połączenia | Tak | Tak (pod rotation) | Nie |
| Stan w pamięci | Utracany | Utracany | Zachowany |
| Czas wdrożenia | Minuty | Minuty | Sekundy |
| Złożoność setupu | Niska | Wysoka | Średnia |
| Rollback | Nowy deploy | Nowy deploy | Natychmiastowy |
Kubernetes rolling deployment - standard industryjny
Kubernetes robi "graceful" deployment:
- Nowy pod ze świeżym kodem
- Nowe requesty idą do nowego poda
- Stary pod dostaje SIGTERM
- Czeka na zakończenie requestów
- Stary pod jest ubijany
To działa, ale:
- WebSocket connections są zrywane - user musi się połączyć ponownie
- Stan w pamięci jest tracony - cache, sesje, drafty
- Minima 2x zasoby - potrzebujesz miejsce na stary i nowy pod
BEAM hot reload
- Ładujesz nowy kod
- Stare procesy kończą na starej wersji
- Nowe wywołania idą do nowej wersji
- Zero dodatkowych zasobów
Połączenia WebSocket żyją. Stan w pamięci żyje.
Przykład praktyczny: GenServer ze stanem
Masz proces, który trzyma stan w pamięci - np. licznik requestów:
defmodule RequestCounter do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, 0, name: __MODULE__)
def increment, do: GenServer.cast(__MODULE__, :increment)
def get, do: GenServer.call(__MODULE__, :get)
# Callbacks
def handle_cast(:increment, count), do: {:noreply, count + 1}
def handle_call(:get, _from, count), do: {:reply, count, {:noreply, count}}
endDziała w produkcji, licznik ma wartość 15 847.
Teraz chcesz dodać histogram - rozkład requestów per godzina:
defmodule RequestCounter do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, {0, %{}}, name: __MODULE__)
def increment, do: GenServer.cast(__MODULE__, :increment)
def get, do: GenServer.call(__MODULE__, :get)
# Zmieniony stan: {count, histogram}
def handle_cast(:increment, {count, histogram}) do
hour = DateTime.utc_now().hour
new_histogram = Map.update(histogram, hour, 1, &(&1 + 1))
{:noreply, {count + 1, new_histogram}}
end
def handle_call(:get, _from, state), do: {:reply, state, {:noreply, state}}
# HOT CODE RELOAD: konwersja starego stanu na nowy
def code_change(_old_vsn, old_state, _extra) do
# Stary stan to integer, nowy to tuple {int, map}
{:ok, {old_state, %{}}}
end
endKluczowa funkcja: code_change/3. Mówi BEAM jak przekształcić stan ze starej wersji na nową.
# W konsoli produkcji
iex> :sys.suspend(RequestCounter)
:ok
iex> :sys.change_code(RequestCounter, RequestCounter, 0, [])
:ok
iex> :sys.resume(RequestCounter)
:ok
# Stan = {15847, %{}} - licznik zachowany, histogram startujeLicznik 15 847 zachowany. Zero downtime. Stan zmigrowany.
Kiedy NIE używać hot code reload
Mimo że to potężne narzędzie, nie jest odpowiedzią na wszystko.
Zmiany w schema bazy danych
Hot code reload nie rusza bazy. Jeśli zmieniasz strukturę tabeli - potrzebujesz migracji, które mogą wymagać locków.
# To wymaga migracji DB - hot reload nie pomoże
defmodule User do
schema "users" do
field :email, :string
field :phone, :string # NOWE POLE - potrzebna migracja
end
endZmiany w zależnościach
Nowa biblioteka? Zaktualizowany Elixir? Nowa wersja Phoenixa?
# To wymaga restartu - nowe zależności
def deps do
[
{:phoenix, "~> 1.7"}, # zmiana na 1.8
{:ecto_sql, "~> 3.10"}, # nowa wersja
]
endKompleksowe refaktoryzacje
Jeśli zmieniasz interfejsy między 20 modułami - hot reload staje się ryzykowne. Łatwo o niespójność.
Reguła: jeśli zmiana dotyka >3 plików lub zmienia interfejsy - użyj normalnego deployu.
Brak testów
Hot patch bypassuje CI/CD. Jeśli nie masz pewności, że kod działa - nie rób hot reload.
Strategia deploymentu w BEAM
Większość projektów używa mieszanego podejścia:
┌─────────────────────────────────────────────────────────┐
│ Typ zmiany │
├─────────────────────┬───────────────────────────────────┤
│ Mały fix │ Pełny feature / release │
│ (1-2 pliki) │ (wiele plików, zależności) │
├─────────────────────┼───────────────────────────────────┤
│ Hot patch │ Standard deploy │
│ (nagłe przypadki) │ (CI/CD, testy, staging) │
├─────────────────────┼───────────────────────────────────┤
│ Czas: minuty │ Czas: 20-60 minut │
│ Downtime: 0 │ Downtime: 0 (rolling) │
└─────────────────────┴───────────────────────────────────┘Workflow produkcyjny
- Normalne zmiany → CI/CD → rolling deploy (dla nowych wersji zależności)
- Krytyczne hotfixy → hot reload → potem commit do repozytorium
- Tuning runtime → konsola iex → trwałe w configu w następnym release
Tabela porównawcza: hot reload vs standard deploy
| Czynnik | Hot Code Reload | Standard Deploy |
|---|---|---|
| Czas wdrożenia | 1-5 minut | 15-60 minut |
| Downtime | 0 | 0 (rolling) lub minimalny |
| Zerwane połączenia | 0 | Tak (WebSocket) |
| Stan w pamięci | Zachowany | Utracony |
| Testy automatyczne | Pominięte | Przechodzą |
| Rollback | Natychmiastowy | Nowy deploy |
| Zmiany w zależnościach | Nie | Tak |
| Zmiany w bazie | Nie | Tak |
| Złożoność zmiany | Mała | Dowolna |
| Ryzyko | Średnie (brak CI) | Niskie (z CI) |
Ericsson i historia hot reload
Hot code reload nie jest nowym featurem. Jest częścią DNA BEAM od 1986 roku.
Ericsson budował systemy telekomunikacyjne, gdzie:
- 99.9999999% uptime był wymagany (max 31 ms rocznie downtime)
- systemy działały latami bez restartu
- aktualizacje musiały być bez przerwy
Rozwiązanie: maszyna wirtualna, która wspiera hot reload natywnie.
AXD301 - switch Ericssona:
- 2 milionów linii kodu Erlanga
- 99.9999999% availability
- Upgrade w locie, bez zrywania połączeń głosowych
To jest powód, dla którego WhatsApp (19 miliardów wiadomości dziennie) wybrał Erlanga/BEAM. Nie dla szybkości kodu, ale dla niezawodności runtime'u.
Podsumowanie
Hot code reload w BEAM to nie "nice to have". To architekturalna różnica.
| W tradycyjnym stacku | W BEAM |
|---|---|
| Bug fix = deploy = restart | Bug fix = reload = kontynuacja |
| Stan w pamięci = tracony | Stan w pamięci = zachowany |
| WebSocket = zrywane | WebSocket = żywe |
| Rollback = nowy deploy | Rollback = sekunda |
Reguła: BEAM daje Ci opcję, której inni nie mają. Możesz z niej korzystać lub nie - ale warto wiedzieć, że istnieje.
Jeśli chcesz zobaczyć jak hot code reload działa w praktyce i jak wdrożyć strategię zero-downtime w Twoim systemie, porozmawiajmy - pokażemy demo na żywo i omówimy, czy i kiedy to ma sens w Twoim przypadku.