Hot code reload - jak Erlang aktualizuje system bez wyłączania go nawet na milisekundę
Jest rok 1998. Ericsson uruchamia centralę telefoniczną AXD 301. Obsługuje miliony jednoczesnych rozmów. System musi działać bez przerwy - dosłownie. Nie "99.9% uptime". Nie "5 minut przerwy miesięcznie". 99.9999999% uptime - to 31 milisekund przestoju na rok. Trzydzieści jeden milisekund. Mrugnięcie oka trwa dłużej.
Jak to możliwe? Jak aktualizujesz oprogramowanie systemu, który nie może się wyłączyć nawet na sekundę? Jak naprawiasz buga w kodzie, który w tej chwili obsługuje milion rozmów telefonicznych?
Odpowiedź: hot code reload - mechanizm wbudowany w BEAM, maszynę wirtualną Erlanga, który pozwala wymieniać kod w działającym systemie jak silnik w lecącym samolocie. Bez lądowania. Bez przestoju. Bez utraty stanu.
To nie jest teoria. To nie jest prototyp. To jest technologia produkcyjna używana od ponad 25 lat w systemach, gdzie downtime oznacza zerwane rozmowy telefonów alarmowych.
Jak to działa
Dwie wersje modułu jednocześnie
BEAM może trzymać w pamięci dwie wersje tego samego modułu jednocześnie: starą (current) i nową (new). To jest klucz do wszystkiego.
Gdy ładujesz nową wersję modułu:
- BEAM ładuje nowy kod do pamięci obok starego
- Nowe wywołania funkcji z zewnątrz idą do nowego modułu
- Procesy, które aktualnie wykonują kod starego modułu, kontynuują na starym - bo są w środku funkcji i nie mogą nagle przeskoczyć
- Gdy proces zakończy bieżące wywołanie i zrobi kolejne wywołanie kwalifikowane (fully qualified call) - przeskakuje na nowy moduł
- Gdy żaden proces nie korzysta ze starego modułu - BEAM usuwa go z pamięci
Czas →
Proces A: [stary moduł]──────[nowy moduł]──────────────→
Proces B: [stary moduł]──[nowy moduł]──────────────────→
Proces C: ──────[stary moduł]────────[nowy moduł]──────→
↑
Załadowanie nowej wersji.
Procesy przechodzą na nową
w momencie kolejnego wywołania.
Żaden proces nie jest przerywany.Nie ma momentu, w którym system nie działa. Nie ma momentu, w którym proces jest "w połowie starego i w połowie nowego kodu". Przejście jest atomowe na poziomie wywołania funkcji.
Wywołanie kwalifikowane - mechanizm przejścia
W Erlangu/Elixirze istnieją dwa typy wywołań funkcji:
Wywołanie lokalne - funkcja(args) - wywołuje bieżącą wersję modułu. Proces zostaje na tej wersji, na której jest.
Wywołanie kwalifikowane - Modul.funkcja(args) - wywołuje najnowszą wersję modułu. To jest punkt, w którym proces przeskakuje na nowy kod.
defmodule MojSerwer do
def loop(state) do
receive do
{:request, data} ->
new_state = handle(state, data) # lokalne - ta sama wersja
loop(new_state) # lokalne - ta sama wersja
:upgrade ->
MojSerwer.loop(state) # kwalifikowane - NOWA wersja!
end
end
endProces działa w pętli loop/1. Gdy dostanie wiadomość :upgrade, wywołuje MojSerwer.loop(state) z pełną nazwą modułu. To sprawia, że BEAM przeskakuje na najnowszą wersję MojSerwer. Stan (state) jest przekazany - nic nie ginie.
W praktyce GenServer (standardowy wzorzec w Elixirze) robi to automatycznie. Programista nie musi pisać :upgrade - BEAM i OTP zarządzają tym za niego.
Zachowanie stanu
To jest najtrudniejsza część: co się dzieje, gdy nowa wersja modułu ma inną strukturę stanu niż stara?
Przykład: stara wersja trzyma stan jako %{name: "Jan", email: "jan@firma.pl"}. Nowa wersja dodaje pole phone. Co z procesami, które mają stary stan bez phone?
OTP (Open Telecom Platform - standardowa biblioteka Erlanga) ma na to mechanizm code_change/3:
defmodule MojSerwer do
use GenServer
# Stara wersja stanu: %{name: ..., email: ...}
# Nowa wersja stanu: %{name: ..., email: ..., phone: nil}
def code_change(_old_vsn, old_state, _extra) do
new_state = Map.put_new(old_state, :phone, nil)
{:ok, new_state}
end
endBEAM wywołuje code_change/3 na każdym procesie, który używa tego modułu. Proces dostaje szansę na transformację swojego stanu ze starego formatu na nowy. Bez restartu. Bez utraty danych. Proces dalej działa, ale teraz na nowym kodzie z nowym stanem.
Gdzie to jest używane w produkcji
Telekomunikacja (Ericsson)
To jest rodowód Erlanga. Centrale telefoniczne Ericssona obsługują ponad 40% światowego ruchu telefonicznego. Systemy działają latami bez restartu, aktualizowane hot code reloadem. Rozmowa telefoniczna nie może zostać przerwana, bo inżynier wdraża poprawkę.
AXD 301 - ta legenda z 99.9999999% uptime - to 3 miliony linii kodu Erlanga, aktualizowanych w ruchu.
WhatsApp w 2014 roku obsługiwał 450 milionów aktywnych użytkowników na zespole 32 inżynierów. Backend napisany w Erlangu. Aktualizacje wdrażane bez przerywania dostarczania wiadomości. Gdy kupił ich Facebook za 19 miliardów dolarów, infrastruktura Erlanga była jednym z kluczowych assetów.
2 miliony jednoczesnych połączeń na jeden serwer. Hot code reload pozwalał aktualizować serwery jeden po jednym, bez utraty ani jednej wiadomości.
RabbitMQ
Ironia: RabbitMQ - broker wiadomości, którego wiele firm używa zamiast PostgreSQL + Oban - sam jest napisany w Erlangu. Aktualizacje RabbitMQ w klastrze wykorzystują hot code reload: węzły klastra są aktualizowane po kolei, bez utraty wiadomości w kolejkach.
Discord
Discord używa Elixira do obsługi milionów jednoczesnych użytkowników na czatach głosowych i tekstowych. Aktualizacje deployu nie przerywają trwających sesji głosowych. Użytkownik rozmawiający na kanale nie zauważa, że pod spodem zmienił się kod serwera.
Systemy finansowe
Klarna (szwedzki fintech, "buy now pay later") używa Erlanga do przetwarzania transakcji płatniczych. Transakcja w trakcie przetwarzania nie może być przerwana przez wdrożenie. Hot code reload gwarantuje, że nowa wersja kodu przejmuje nowe transakcje, a stare kończą się na starym kodzie.
Czym to się różni od "zwykłego" zero downtime
Inne technologie też osiągają zero downtime - ale innymi (gorszymi) sposobami:
Blue-green deployment
Dwa środowiska. Przełączasz ruch z jednego na drugie. Problem: połączenia są zrywane. Użytkownik na starym serwerze traci sesję. W aplikacji HTTP (request-response) to OK - następny request idzie do nowego serwera. Ale w aplikacji WebSocket/real-time - połączenie jest zerwane, stan utracony, użytkownik musi się reconnectować.
W BEAM: połączenie WebSocket trwa. Proces obsługujący użytkownika przechodzi na nowy kod bez zrywania połączenia. Stan sesji jest zachowany. Użytkownik niczego nie zauważa.
Rolling deployment
Serwery aktualizowane po kolei za load balancerem. Problem: przez czas aktualizacji część serwerów ma starą wersję, część nową. Jeśli wersje mają niekompatybilne API między sobą - chaos. Load balancer musi wiedzieć, który serwer jest "gotowy", a który "w trakcie". Proces jest złożony i podatny na race conditions.
W BEAM: jeden serwer, dwie wersje kodu współistnieją. Nie ma niezgodności między serwerami, bo jest jeden serwer. Przejście jest atomowe na poziomie procesu.
Container restart (Docker/Kubernetes)
Zabij kontener, uruchom nowy. Czas restartu: 5-30 sekund. Problem: wszystkie połączenia zerwane, cały stan utracony, cold start (ładowanie danych do cache, rozgrzewanie connection pool, JIT compilation).
W BEAM: zero restartu. Kod zmienia się "w locie". Dane w ETS (cache) pozostają. Połączenia z bazą danych pozostają. Connection pool pozostaje. Nic nie wymaga "rozgrzania".
Porównanie
| Cecha | Blue-green | Rolling | Docker restart | BEAM hot reload |
|---|---|---|---|---|
| Downtime | ~0 (przełączenie LB) | ~0 (za LB) | 5-30 sekund | 0 (dosłownie) |
| Połączenia WebSocket | Zerwane | Zerwane na aktualizowanym serwerze | Zerwane | Zachowane |
| Stan sesji | Utracony | Utracony | Utracony | Zachowany |
| Cache (RAM) | Zimny start | Częściowo zimny | Zimny start | Ciepły (zachowany) |
| Connection pool DB | Nowy | Nowy na aktualizowanym | Nowy | Zachowany |
| Złożoność infra | Wysoka (2 środowiska) | Średnia (LB + health) | Średnia (orchestrator) | Niska (jeden serwer) |
| Kompatybilność wersji | Nie wymaga | Wymaga (2 wersje live) | Nie wymaga | Obsługiwana (code_change) |
Hot code reload w Phoenix LiveView
LiveView to idealny scenariusz dla hot code reload: każdy użytkownik ma trwałe połączenie WebSocket z serwerem. Zerwanie tego połączenia oznacza migotanie interfejsu, utratę stanu formularza, reset pozycji scrolla.
Scenariusz: poprawka na żywo
200 użytkowników pracuje w systemie ERP. Ktoś zgłasza buga: rabat 10% jest liczony od kwoty brutto zamiast netto. Poprawka to zmiana jednej linijki:
# Było (błędnie):
rabat = zamowienie.brutto * Decimal.new("0.10")
# Jest (poprawnie):
rabat = zamowienie.netto * Decimal.new("0.10")Bez hot code reload (Docker/Java/Node):
- Deploy nowej wersji
- Restart serwera (5-30 sekund)
- 200 użytkowników traci połączenie WebSocket
- 200 użytkowników widzi "Reconnecting..." w LiveView
- Formularze w trakcie wypełniania - zresetowane
- Długie tabele - scroll wraca na górę
- Filtry i ustawienia widoku - zresetowane
- Po reconnect: użytkownicy muszą odtworzyć swoją pracę
Z hot code reload (BEAM/Elixir):
- Deploy nowej wersji
- BEAM ładuje nowy moduł
- 200 użytkowników kontynuuje pracę bez przerwy
- Następne wywołanie funkcji rabatu używa nowego kodu
- Formularze, scrolle, filtry - nienaruszone
- Nikt niczego nie zauważa
Różnica jest dramatyczna w systemach, gdzie ludzie pracują cały dzień z otwartą aplikacją. Dashboard zarządu, terminal kasowy, panel magazyniera - to nie są strony do przeglądania, to narzędzia pracy. Każde "Reconnecting..." to przerwana praca.
Hot code reload a LiveView reconnect
LiveView ma wbudowany mechanizm reconnect - jeśli połączenie WebSocket zostanie zerwane, klient automatycznie się łączy ponownie. To jest plan B. Hot code reload to plan A: nie zrywaj połączenia wcale.
Z hot code reload LiveView:
- Nowy kod jest załadowany
- Procesy LiveView (każdy użytkownik = osobny proces BEAM) przechodzą na nowy kod przy następnym evencie
- Nowy render jest wysyłany do przeglądarki jako diff HTML
- Interfejs aktualizuje się płynnie, bez migotania, bez "Reconnecting..."
Kiedy hot code reload jest szczególnie ważny
Systemy 24/7
System ERP dla firmy z 3 zmianami. System logistyczny obsługujący dostawy nocne. E-commerce działający w każdej strefie czasowej. Nie ma "okna serwisowego", bo nie ma godziny, w której nikt nie pracuje.
Tradycyjne podejście: wdrożenie o 3:00 w nocy, "bo wtedy najmniej użytkowników". W firmie globalnej 3:00 w Warszawie to 9:00 w Tokio. Nie ma dobrej godziny.
Z hot code reload: wdrażasz o 14:00 w wtorek. W godzinach pracy. Przy pełnym obciążeniu. Nikt nie zauważa.
Systemy real-time
Monitoring IoT, trading, sterowanie procesami przemysłowymi, komunikatory. Systemy, w których ciągłość strumienia danych jest krytyczna.
Zerwanie połączenia z czujnikiem temperatury na 10 sekund? To 10 sekund bez danych. W procesie chemicznym to 10 sekund, w których nie wiesz, czy reaktor nie przegrzewa się.
Z hot code reload: strumień danych nie jest przerywany. Proces odbierający dane z czujnika przechodzi na nowy kod bez utraty ani jednego odczytu.
Systemy z długimi sesjami
Wideo konferencja trwająca 2 godziny. Edycja współdzielonego dokumentu. Gracz w grze online w trakcie sesji. Zerwanie sesji to utrata kontekstu, czasu, cierpliwości użytkownika.
Discord wdraża aktualizacje na serwerach obsługujących aktywne rozmowy głosowe. Użytkownicy rozmawiają dalej. To jest niemożliwe w architekturze opartej na restartach.
Ograniczenia i uczciwość
Hot code reload nie jest magią bez ograniczeń. Uczciwie o wadach:
Złożoność migracji stanu
Jeśli nowa wersja zmienia strukturę stanu procesu, musisz napisać code_change/3. Dla prostych zmian (dodanie pola) to trywialne. Dla fundamentalnych zmian (zmiana struktury danych z mapy na drzewo) to może być skomplikowane.
W praktyce 90% wdrożeń nie wymaga migracji stanu - bo zmiany są w logice, nie w strukturze danych.
Nie dla zmian architektonicznych
Hot code reload działa świetnie dla zmian w logice: poprawki bugów, nowe reguły biznesowe, optymalizacje. Nie działa dobrze dla zmian architektonicznych: zmiana drzewa supervisionowego, dodanie nowej aplikacji OTP, zmiana schematu bazy danych.
Dla dużych zmian architektonicznych nadal potrzebujesz rolling restart - ale BEAM robi nawet restart szybciej niż inne platformy (czas startu aplikacji Elixir: 1-3 sekundy vs Java: 15-60 sekund).
Kompatybilność wersji w klastrze
Jeśli masz klaster BEAM (wiele węzłów połączonych siecią), musisz pamiętać o kompatybilności wersji między węzłami. Węzeł A ma nowy kod, węzeł B jeszcze stary - czy komunikacja między nimi działa?
OTP ma na to mechanizmy (appup/relup files), ale wymagają planowania. W praktyce większość zespołów używa rolling restart w klastrze + hot code reload na pojedynczym węźle.
Testowanie hot upgrade'u
Musisz testować nie tylko "czy nowa wersja działa", ale "czy przejście ze starej na nową działa". To dodatkowa warstwa testów, której nie masz w systemie z restartami.
Hot code reload w kontekście naszego stacku
W naszym stacku hot code reload działa na kilku poziomach:
Phoenix LiveView - użytkownicy nie tracą połączeń przy wdrożeniach. Formularze, filtry, stan sesji - zachowane.
Oban workers - zadania w trakcie wykonywania kończą się na starym kodzie. Nowe zadania startują na nowym kodzie. Żadne zadanie nie jest przerwane w połowie.
GenServery (procesy tła) - synchronizacje, monitoringi, cache'e - przechodzą na nowy kod przez code_change/3. Stan jest zachowany.
Połączenia z PostgreSQL - connection pool (Ecto) nie jest restartowany. Nie ma "zimnego startu" puli połączeń.
Wdrożenie nowej wersji:
LiveView proces 1: ──[stary]──→[nowy]──→ (użytkownik nic nie widzi)
LiveView proces 2: ──[stary]────→[nowy]──→ (użytkownik nic nie widzi)
Oban worker: ──[stary - kończy]──→ (zadanie się kończy)
Nowy Oban worker: ─────────[nowy]──→ (nowe zadanie na nowym kodzie)
GenServer cache: ──[stary]──→[nowy]──→ (cache zachowany)
DB connection pool: ──────────────────────→ (bez zmian)Wszystko dzieje się jednocześnie, bez koordynacji, bez downtime'u, bez utraty danych.
Ile kosztuje downtime - perspektywa biznesowa
"Ale mój system może być offline 5 minut na wdrożenie" - powiedzą niektórzy. Policzmy:
Bezpośredni koszt przestoju
System ERP, 200 pracowników, wdrożenie raz w tygodniu:
- 5 minut przestoju × 200 osób × 52 tygodnie = 866 roboczogodzin rocznie
- Przy stawce 50 PLN/h = 43 000 PLN rocznie na czekanie na restart
System e-commerce, 500 transakcji/godzinę, wdrożenie raz w tygodniu:
- 5 minut × 500/60 = ~42 utracone transakcje × średni koszyk 150 PLN = 6 300 PLN
- × 52 tygodnie = 327 000 PLN rocznie utraconych transakcji
Pośredni koszt: rzadsze wdrożenia
Bo wdrożenie oznacza przestój, firma wdraża rzadko. Raz na tydzień albo raz na dwa tygodnie. Zmiany kumulują się. Większe wdrożenie = większe ryzyko. Większe ryzyko = więcej stresów. Więcej stresów = jeszcze rzadsze wdrożenia. Błędne koło.
Z hot code reload: wdrożenie nie ma kosztu. Nie ma powodu, żeby nie wdrażać kilka razy dziennie. Mała zmiana = małe ryzyko. Szybki feedback. Szybka naprawa.
Koszt "okna serwisowego"
Tomek wdraża w sobotę o 22:00, bo nie może w godzinach pracy (przestój). Godziny Tomka w sobotę wieczorem kosztują więcej (nadgodziny). Tomek jest zmęczony - ryzyko błędu wyższe. Jeśli coś pójdzie nie tak - naprawia w nocy, sam, pod presją.
Z hot code reload: wdrożenie we wtorek o 14:00. W godzinach pracy. Cały zespół jest dostępny. Tomek jest wypoczęty. Jeśli coś pójdzie nie tak - cały zespół reaguje.
Podsumowanie
Hot code reload to nie ciekawostka techniczna. To fundamentalna cecha architektoniczna BEAM, która ma realne konsekwencje biznesowe:
| Aspekt | Restart (Java/Node/Python) | Hot code reload (BEAM) |
|---|---|---|
| Downtime przy wdrożeniu | 5-60 sekund | 0 milisekund |
| Połączenia WebSocket | Zerwane | Zachowane |
| Stan sesji użytkownika | Utracony | Zachowany |
| Cache w pamięci | Zimny start | Ciepły (zachowany) |
| Connection pool DB | Odbudowywany | Zachowany |
| Kiedy wdrażać | Okno serwisowe (noc/weekend) | Dowolna godzina |
| Częstotliwość wdrożeń | Raz/tydzień (bo ryzykowne) | Wiele razy/dzień |
| Systemy 24/7 | Przerwa nieunikniona | Przerwa niepotrzebna |
| Koszt dla e-commerce | ~327 000 PLN/rok | 0 PLN |
Ericsson zbudował Erlanga, bo nie mógł sobie pozwolić na sekundę przestoju w centralach telefonicznych. My budujemy systemy biznesowe na tej samej platformie - bo Twoja firma też nie powinna tracić ani sekundy.
Chcesz system, który aktualizuje się bez przerywania pracy Twoich ludzi? Porozmawiajmy - pokażemy, jak BEAM eliminuje problem "okna serwisowego" raz na zawsze.