Zaczynamy wchodzić w bardziej zaawansowane tematy z Elixir. Plan teraz jest dość prosty, dokończyć serię naukową i wejść w praktyczną – czyli napisać konkretną aplikację. Pomysł już jest :) Kwestia tylko omówienia kilku aspektów jeszcze i jazda :) Dziś więc na tapetę wchodzi maszyna wirtualna erlanga – BEAM (Bogdan/Björn’s Erlang Abstract Machine). Zrozumienie ogólne jak działa maszyna, umożliwi nam zrozumienie jak działają poszczególne elementy naszego systemu w Elixir.

Uwaga: słowo proces będzie używane dość często. Za każdym razem jak będę je używał to mam na myśli proces maszyny wirtualnej erlanga – stworzony i zarządzany przez nią, nie przez system operacyjny. Jeżeli będzie inaczej, to wspomnę o tym.

Zakładając wszystkie domyślne ustawienia, BEAM jest to pojedynczy proces systemu operacyjnego, który dla każdego rdzenia procesora tworzy scheduler odpowiedzialny zarządzaniem jednostkami pracy tak by jak najefektywniej wykorzystać dostępne procesory i jednostki pracy (procesy). Każdy taki scheduler działa jako osoby wątek w procesie systemu operacynego który został stworzony dla BEAM.

BEAM

CPU Cores, OS Process, BEAM, OS Thread, Schedulers, Processes
CPU Cores, OS Process, BEAM, OS Thread, Schedulers, Processes

Daje nam to kilka fajnych opcji (jak zawsze, na temat każdej można dyskutować):

  • Fault tolerant (aż się zdziwiłem, że po PL tak to piszą ;))
  • Skalowalność
  • Rozpraszalność

Fault Tolerant

Każdy proces jest zamkniętą w sobie w całością – jest odizolowany. Procesy nie mają nic wspólnego między sobą – nie dzielą pamięci, obiektów, zmiennych. Jedyną rzeczą wspólną dla procesów jest to, że zarządza nimi scheduler. Jeżeli coś się stanie, to proces można usunąć. Można też wystartować nowy proces w miejscu tego który uległ awarii.

Izolacja procesów umożliwia działanie garbage collectora na bazie procesu a nie całości systemu. To znaczy, że może następować optymalizacja w procesie czyszczenia pamięci – każdy proces ma własnego garbage collectora. Ogólnie taki proces, dostaje około 2KB pamięci – mało, ale o to chodzi, wiele małych procesów. Jeżeli proces wymaga więcej niż 2KB, to w tym momencie do okna czasu wykonania (process execution window) dodawany jest garbage collector na danym procesie. A więc zamiast jednego wielkiego, słusznego procesu czyszczenia pamięci, mamy masę, setki, tysiące małych.

Skalowalność

Z powodu izolacji procesów, komunikacja pomiędzy nimi musi następować na zasadzie wiadomości asynchronicznych – taki mały pub/sub. Dzięki czemu nie następuje problem synchronizacji aktualnego stanu – lock, mutex, semafor itp. A skoro procesy są małe, to można ich tworzyć dużo. Więc jak trzeba nad czymś mocniej popracować, można łatwo zwiększyć obroty za pomocą zrównoleglenia pracy i wykorzystać w pełni wszystkie dostępne rdzenie w procesorze.

Czyli, za pomocą na przykład dołożenia RAM lub nowego procesora, ten sam kod, zacznie pracować znacznie wydajniej gdyż w pełni wykorzysta dodatkowe zasoby które zostały dostarczone. I nie będziemy musieli nic z naszej programistycznej perspektywy zmieniać.

Rozpraszalność

I teraz tutaj wchodzi wszystko to co mówiliśmy wcześniej, procesy są odizolowane od siebie, komunikacja jest asynchroniczna. To więc czemu musimy się komunikować tylko z wykorzystaniem jednej maszyny? :) Skoro możemy skalować rozwiązanie dodając zasoby do serwera, to też powinniśmy móc skalować rozwiązanie (rozszerzać je) horyzontalnie, czyli za pomocą kolejnych serwerów.

I tak też jest. Nasze procesy za pomocą komunikacji asynchronicznej są nie tylko wstanie komunikować się pomiędzy sobą ale także pomiędzy różnymi instancjami BEAM na różnych komputerach.

Scheduler

Scheduler to narzędzie, które zarządza procesami, udostępniając im okno wykonania (execution window). Każdy scheduler działa w osobnym wątku systemu operacyjnego. W beam jest tyle schedulerów ile jest rdzeni w procesorze (jest to wartość domyślna, można też to modyfikować ustawieniami).

Każdy proces, który jest zarządzany przez pojedynczy schedulera, działa zamiennie – każdy proces dostaje takie samo okno wykonania. Jak czas się kończy, to proces jest wywłaszczany i kolejny proces dostaje swoją szansę. Dzięki czemu nie następuje blokada systemu kiedy jeden z procesów musi wykonać długotrwałą, ciężką pracę, która może trwać i trwać. Gdyby taki scheduler miał czekać na zakończenie pracy tego procesu to zarówno scheduler mógłby się przywiesić jak i byśmy stracili responsywność całego systemu – pozostałe procesy by czekały na skończenie pracy pierwszego procesu.

Proces

Proces to najmniejsza jednostka pracy. Działa ona zawsze sekwencyjnie – do póki nie skończy pracy, nie może podjąć się nowej. Jedynie instancje procesów mogą działać współbieżnie. Czyli jeżeli chcemy wykonać zadania równolegle, to nie zrobimy tego z wykorzystaniem jednego procesu. Musimy mieć dwa procesy (oraz oczywiście dwa rdzenie w procesorze).

Jedynie komunikacja pomiędzy procesami jest asynchroniczna. Czyli jeżeli mamy jeden proces, który jest odpowiedzialny za przechwytywanie komunikacji z tysiąca innych procesów, to ten nasz proces będzie naszym bottleneck. Ze względu na to, że cała komunikacja będzie kolejkowana do póki proces nie skończy swojej pracy. To znaczy, że jeżeli coś trwa u nas jedną sekundę a mamy 10 wiadomości już wysłanych, to ostatnia zostanie dopiero przetworzona po upływnie 10 sekundy.

Podsumowanie

Oki, trochę suchej teorii bez wchodzenie w zbytnio w szczegóły. Ale to da nam podstawy do następnego artykułu w którym pewne rzeczy z tego zostaną jeszcze bardziej rozwinięte.

side note: Pamiętacie jak cały czas chodziłem za tym jak to jest z tymi modułami? Kto i co jest odpowiedzialny za ich inicjalizację itp. ;) No to jest to tak, że kod źródłowy jest kompilowany do plików beam które są wykorzystywane przez maszynę wirtualną. Jeżeli coś nie zostało załadowane, to maszyna wirtualna szuka skompilowanej wersji modułu (każdy moduł ma osobny beam) i próbuje ją załadować i następnie wykonać funkcję. A więc częściowo sobie odpowiedziałem przy okazji jak to tak naprawdę jest szykując ten krótki suchy opis ;)

2 KOMENTARZE

Comments are closed.