Architektura przetwarzania strumieniowego danych w Elixir: od Hello, World! do luźnego sprzężenia
Rozwój systemu współpracujących silników w Elixir wymaga wyraźnego podziału warstw i integracji z zewnętrznymi zależnościami. W tym artykule omówiono implementację podstawowych węzłów do przetwarzania strumieniowego danych, w tym źródeł, ujść i centralnego dyspozytora, z naciskiem na zasady architektoniczne i praktyczne rozwiązania.
Systematyzacja węzłów sprzętowych i klasyfikacja
W przetwarzaniu strumieniowym danych kluczową rolę odgrywają końcowe węzły grafu, które współdziałają ze sprzętem. Na dolnej warstwie znajdują się źródła danych, które można sklasyfikować według typu wprowadzania:
- Klawiatura (Setpoint): do wprowadzania informacji alfanumerycznych, np. wartości nastaw w systemach sterowania.
- Regularne bramki (R_gateway): sterowniki szyn z regularnie napływającymi danymi, takie jak Modbus, stosowane w automatyce przemysłowej.
- Nieregularne bramki (Ir_gateway): adaptery do spontanicznie napływających danych, np. szyna Can w nowoczesnych samochodach podłączona do centralnej jednostki sterującej.
Na górnej warstwie znajdują się konsumenci danych, zaliczani do klasy Stock:
- Wyświetlacze do prezentacji informacji.
- Systemy plików do rejestrowania zdarzeń i błędów.
- Bazy danych lub repozytoria jako pośrednie magazyny.
- Systemy wyższego poziomu, umożliwiające łączenie silników w łańcuchy przez węzły bramkowe.
Taka klasyfikacja pozwala utworzyć graf przepływu danych, gdzie węzły dolnego poziomu przekazują informacje węzłom górnego poziomu, pomijając pośrednią warstwę silników na początkowym etapie.
Implementacja podstawowych węzłów i imitacja strumieni
W celu zademonstrowania działania systemu zaimplementowano trzy węzły-źródła i jeden węzeł-ujście bez użycia wzorca GenServer OTP. Te procesy opierają się na zapętlanych funkcjach loop, co upraszcza architekturę i redukuje złożoność:
- Moduł wprowadzania danych z terminala: umożliwia ręczne wprowadzanie informacji alfanumerycznych, np. frazy "Witaj, Świecie!".
- Imitator regularnych danych: bez końca wysyła do strumienia replikę "Hello, " z okresowością 1 sekundy.
- Imitator spontanicznych danych: bez końca wysyła do strumienia replikę "World! " z okresowością do 1 sekundy.
- Terminal wyjścia: wyświetla informacje alfanumeryczne na ekran, przyjmując dane od wszystkich źródeł.
Pośrednia warstwa silników pozostaje pusta, co odpowiada zamiarowi wykluczenia silników z początkowej implementacji. Kod po dodaniu roboczych silników zostanie udostępniony na GitHubie, zapewniając przejrzystość i możliwość dalszego rozwoju.
Rozproszenie terminali i centralny dyspozytor
Jednym z kluczowych zadań była organizacja wejścia/wyjścia terminala, przypiętego do lokalnego procesu. Standardowo Erlang zbiera wejście/wyjście ze wszystkich węzłów na jeden terminal do scentralizowanego zarządzania, co nie zawsze odpowiada wymaganiom systemów rozproszonych. Rozwiązanie znaleziono przy użyciu operatora:
:global.register_name(:stock_ldr, :erlang.group_leader)
To pozwoliło rozproszyć system na wyspecjalizowane terminale, chociaż poszukiwanie rozwiązania zajęło znaczny czas z powodu niewystarczającej dokumentacji na ten temat.
Do załadowania i uruchomienia systemu stworzono centralny dyspozytor. W kontekście integracji z zewnętrznymi bibliotekami w Elixir zaleca się tworzenie jednego wewnętrznego modułu do interakcji z zewnętrznymi zależnościami, co sprzyja luźnemu sprzężeniu. Architektura heksagonalna sugeruje wynoszenie zewnętrznych zależności na peryferia aplikacji, oddzielając logikę biznesową od efektów ubocznych. Jednak w praktyce osiągnięcie tej zasady może być trudne, szczególnie przy obecności wielu zewnętrznych modułów, takich jak sprzętowe bramki i konsumenci danych.
Zasady architektoniczne i rozszerzalność systemu
Projektowany system współpracujących silników jest otwarty na rozszerzenia, gdzie każdy końcowy węzeł grafu pełni rolę bramki (gateway) do integracji ze światem zewnętrznym. To umożliwia swobodne doładowywanie niestandardowych komponentów programowych, rozwijając system w dziedziny aplikacyjne. Ważna uwaga: system silników rośnie nie "w górę", ale przerasta "w dół" w obszary aplikacyjne, co zmienia stereotypowe spojrzenie na rozwój architektoniczny.
Kluczowe aspekty dla deweloperów:
- Użycie prostych procesów z loop zamiast GenServer OTP dla podstawowych węzłów może uprościć implementację, ale wymaga przemyślanej obsługi błędów.
- Rozproszenie terminali przez :global.register_name rozwiązuje problem scentralizowanego wejścia/wyjścia w Erlang.
- Luźne sprzężenie z zewnętrznymi zależnościami pozostaje złożonym zadaniem, pomimo zaleceń dotyczących architektury heksagonalnej.
Co jest ważne:
- Klasyfikacja węzłów na R_gateway, Ir_gateway, Setpoint i Stock zapewnia wyraźną strukturę przetwarzania strumieniowego.
- Implementacja imitatorów danych demonstruje działanie systemu bez pośrednich silników.
- Rozproszenie terminali wymaga nietrywialnych rozwiązań w Erlang/Elixir.
- Centralny dyspozytor i integracja z zewnętrznymi zależnościami podążają za zasadami luźnego sprzężenia.
- System jest otwarty na rozszerzenia przez węzły bramkowe, rozwijając się w dziedziny aplikacyjne.
— Editorial Team
Brak komentarzy.