W pierwszym tekście pokazaliśmy, czym jest LinkSense — lekkie, open source narzędzie do synthetic monitoringu. Tutaj schodzimy o poziom niżej, do decyzji architektonicznych. Bo akurat w tym projekcie niemal każdy wybór — od modelu komunikacji, przez język programowania, po format bazy danych — wynika z konkretnego problemu, a nie z mody. Ten tekst jest dla osób, które chcą wiedzieć dlaczego to działa tak, jak działa: dla administratorów, zespołów bezpieczeństwa, architektów i devopsów.
Table of Contents
- Model agent–serwer: dlaczego agent łączy się z serwerem, a nie odwrotnie
- Bezpieczeństwo: zero otwartych portów po stronie agenta
- Masowa rekonfiguracja z poziomu serwera
- Local-first: dane przede wszystkim lokalnie
- Agregacja: dlaczego akurat co 60 sekund i zapis w paczkach
- Model danych: co dokładnie trafia do bazy
- Dlaczego Rust
- Dlaczego SQLite
- Konfiguracja jako kod: TOML
- Wdrożenie: binarki, CI i pułapka pinga na Linuksie
- Logi: dlaczego nie trafiają na standardowe wyjście
- Świadome „nie”: kontenery
- Podsumowanie
Model agent–serwer: dlaczego agent łączy się z serwerem, a nie odwrotnie
Punktem wyjścia było jedno założenie: chcemy mieć wielu agentów w różnych lokalizacjach. Z tego wynika reszta. Skoro agentów ma być wielu i mają być rozsiani po świecie, logiczną konsekwencją jest centralny serwer, do którego wysyłają dane.
Kluczowa decyzja dotyczy jednak kierunku połączenia. W LinkSense to agent nawiązuje połączenie z serwerem, czyli wykonuje połączenie wychodzące, a nie serwer „puka” do agentów, czyli inicjuje połączenie przychodzące. Wybór tego kierunku ma dwa uzasadnienia — bezpieczeństwo i łatwość zarządzania — i to one zdefiniowały całą architekturę.
Agent wykonuje zlecone testy, zapisuje wyniki lokalnie, a co 60 sekund tworzy zagregowane metryki i wypycha je, czyli wykonuje „push”, do centralnego serwera. Jeśli akurat nie może się połączyć, przy następnej próbie dodatkowo sprawdza, czy jego konfiguracja jest aktualna. Serwer odbiera metryki i również zapisuje je u siebie w lokalnej bazie.
Bezpieczeństwo: zero otwartych portów po stronie agenta
To najmocniejszy argument za takim modelem. Agent może działać na maszynie z firewallem ustawionym według klasycznej, restrykcyjnej polityki: „blokuj przychodzące, zezwalaj na wychodzące”, czyli deny incoming, allow outgoing. Skoro to agent inicjuje połączenie na zewnątrz, nie musi mieć żadnego otwartego portu nasłuchującego.
Dlaczego to istotne? Każdy otwarty port to potencjalne wejście dla atakującego. W żargonie bezpieczeństwa oznacza to powiększanie attack surface, czyli powierzchni ataku — sumy wszystkich miejsc, przez które ktoś mógłby próbować dostać się do systemu. Agenty monitorujące bywały już w przeszłości przejmowane, a przejęty agent z otwartym portem to poważne zagrożenie, bo zwykle znajduje się głęboko w infrastrukturze. Brak nasłuchującego portu po prostu zamyka tę drogę.
Drugi aspekt jest czysto operacyjny. Otwarte porty musi mieć tylko jedna maszyna — serwer, bo to on zbiera metryki. Zadbanie o bezpieczeństwo jednej, dobrze pilnowanej maszyny jest nieporównanie prostsze niż utrzymywanie otwartych portów na dziesiątkach czy setkach rozproszonych hostów.
Masowa rekonfiguracja z poziomu serwera
Skoro agent i tak regularnie odpytuje serwer, można to wykorzystać do zdalnej rekonfiguracji. Konfigurację agentów zmienia się centralnie, po stronie serwera — również w trybie zbiorczym, czyli bulk reconfigure, naraz dla wielu agentów.
Przykład z webinaru: wyobraź sobie statek z szesnastoma lub większą liczbą agentów na pokładzie. Chcesz dodać im wszystkim jedno nowe zadanie. Zamiast logować się na każdą maszynę z osobna, dodajesz jeden wpis w pliku konfiguracyjnym po stronie serwera.
Mechanizm działa jako „pull” inicjowany przez agenta i jest odporny na błędy:
- Edytujesz konfigurację na serwerze, na przykład odkomentowujesz zadania.
- Serwer odnotowuje, że dla danego agenta ma nowszą wersję konfiguracji niż ta, którą agent posiada.
- Agent przy kolejnej aktualizacji zgłasza: „moja konfiguracja jest nieaktualna, przyślij nowszą”.
- Serwer wysyła nową wersję, agent ją waliduje, podmienia u siebie i natychmiast zaczyna z niej korzystać.
Na żywej demonstracji ta zmiana propagowała się w ciągu kilku sekund — odkomentowane zadania od razu pojawiły się w surowych metrykach agenta.
Local-first: dane przede wszystkim lokalnie
Architektura jest „local-first”, czyli „najpierw lokalnie”: agent zapisuje wyniki u siebie, zanim cokolwiek wyśle. Konsekwencje są trzy:
- brak utraty danych, nawet gdy serwer chwilowo nie odpowiada;
- ciągłość monitoringu w trakcie problemów sieciowych — agent nie przestaje pracować tylko dlatego, że nie ma łączności z centralą; po jej przywróceniu dośle zebrane metryki;
- możliwość lokalnego odpytania danych bezpośrednio na agencie, co bardzo ułatwia debugowanie.
Z tego wynika też tryb standalone, czyli w pełni samodzielny: agent nie musi mieć centralnego serwera w ogóle. Jeśli używasz LinkSense jako małego, prywatnego projektu, wystarczy podejrzeć lokalną bazę dowolnym narzędziem i ewentualnie samodzielnie zbudować alerting na tych danych.
Agregacja: dlaczego akurat co 60 sekund i zapis w paczkach
Agent nie wysyła każdego pojedynczego pomiaru z osobna. Najpierw je agreguje, a do serwera trafia zagregowana porcja w interwale jednej minuty. Co ważne, to świadomy kompromis, a nie przypadek.
Rozważ scenariusz wysokiej częstotliwości: pingujesz coś co sekundę albo co 5 sekund, i to z dużej liczby agentów — powiedzmy setek. Gdyby każdy pomiar był wysyłany natychmiast, zalałbyś sieć i serwer ruchem. Agregacja co minutę okazała się najlepszym kompromisem między dokładnością a ilością przesyłanych i przechowywanych danych. Sama liczba „60 sekund” nie jest magiczna — to wartość dobrana praktycznie.
Agregacja nie gubi przy tym istotnych informacji. W zagregowanej porcji znajdują się między innymi: średnie czasy dla wszystkich mierzonych etapów, maksymalny czas całkowity, liczba próbek, czyli sample count, oraz procent skuteczności, czyli success rate. Dzięki wartości maksymalnej widać nie tylko, że „średnio było OK”, ale również najgorszy przypadek w danym oknie.
Dodatkowo zapis do bazy odbywa się w paczkach, czyli batch, a nie po jednym rekordzie. To kolejny zabieg pod wydajność: mniej operacji zapisu oznacza mniejszy narzut.
Model danych: co dokładnie trafia do bazy
Dla osób, które będą potem konsumować te dane, najciekawsze jest to, jak bogaty jest pojedynczy pomiar. Zadanie HTTP nie zwraca samego „działa/nie działa”. Rozbija odpowiedź na konkretne składniki:
- czasy TCP, czyli nawiązanie połączenia;
- czasy TLS, czyli zestawienie szyfrowania;
- time-to-first-byte, czyli czas do pierwszego bajtu odpowiedzi;
- całkowity czas pobrania;
- informację, czy połączenie było szyfrowane, czyli TLS, czy certyfikat jest ważny i ile dni zostało do jego wygaśnięcia;
- kody statusu HTTP, czyli informację, czy był sukces, czy błąd.
Do każdego zadania dochodzi też adres IP urządzenia, z którym faktycznie nawiązano połączenie. Dzięki temu widać na przykład, jak zmienia się rozwiązany adres IP w czasie.
Jest również opcjonalne, bardzo praktyczne pole meta: target_id. Pozwala oznaczyć tym samym identyfikatorem różne zadania dotyczące tego samego celu, na przykład ping, HTTP GET i TCP wskazujące na tę samą aplikację, aby później łatwo je grupować i filtrować. To pole istnieje wyłącznie po to, by pomóc uporządkować własny monitoring.
Dlaczego Rust
Wydajność, którą widać w liczbach — agent na poziomie około 31 MB pamięci i 0–1% CPU przy kilkudziesięciu zadaniach — nie wzięła się znikąd. Zespół włożył dużo pracy w to, aby program działał możliwie oszczędnie, i to przesądziło o wyborze Rusta jako języka.
Dwie cechy są tutaj kluczowe. Po pierwsze, asynchroniczne wykonywanie, czyli model async/await: zamiast blokować się na każdym pojedynczym teście i czekać na jego wynik, agent prowadzi wiele zadań współbieżnie, nie marnując czasu procesora na bezczynne oczekiwanie. Po drugie, wydajność i niskie zużycie pamięci — Rust nie ma garbage collectora i pozwala precyzyjnie panować nad zasobami.
Miało to również potwierdzenie w testach: oprogramowanie uruchomiono w długoterminowym przebiegu trwającym ponad miesiąc, pod sporym obciążeniem, w ramach stress testów na własnej infrastrukturze. Celem było wychwycenie ewentualnych wycieków pamięci lub degradacji działania. Testy zakończyły się bez ani jednego problemu.
Dlaczego SQLite
Dane trafiają do SQLite — zarówno po stronie agenta, jak i serwera. SQLite to lekka, plikowa baza danych: nie wymaga osobnego procesu ani serwera bazodanowego, a całość mieści się w jednym pliku.
Wybór SQLite — i właśnie takich, bardzo prostych struktur tabel — był celowy. Chodziło o to, aby:
- integracja z czymkolwiek była trywialna: żeby wyciągnąć dane „gdzie indziej”, wystarczy odczytać je z SQLite i skopiować do docelowego systemu;
- możliwy był tryb local-only: możesz podejrzeć bazę dowolną przeglądarką SQLite albo innym narzędziem do wizualizacji czy alertowania, bez stawiania dodatkowej infrastruktury.
Prostota schematu nie jest tutaj kosmetyką. To właśnie ona sprawia, że LinkSense łatwo „dokleić” do istniejącego stacku.
Konfiguracja jako kod: TOML
Cała konfiguracja to pliki tekstowe w formacie TOML — czytelnym formacie konfiguracyjnym opartym na parach klucz–wartość. Rozdzielono ją na warstwy:
- konfiguracja agenta — między innymi adres centralnego serwera i klucz, czyli app key, potrzebny do uwierzytelnienia; w trybie local-only te pola są zbędne;
- konfiguracja zadań — lista tego, co agent ma mierzyć; dodanie nowego zadania to skopiowanie wpisu i podmiana kilku wartości, a każdy typ zadania ma własne, krótkie README;
- konfiguracja serwera — jeszcze prostsza: adres nasłuchu, własny klucz, miejsce przechowywania konfiguracji agentów oraz rozmiar testu przepustowości.
Skoro wszystko jest tekstem, całość naturalnie poddaje się wersjonowaniu, na przykład w gicie. Masz historię zmian i możliwość przeglądu — czyli configuration as code.
Wdrożenie: binarki, CI i pułapka pinga na Linuksie
Możesz zbudować LinkSense ze źródeł, ale nie musisz. Projekt ma GitHub Actions, czyli mechanizm CI — automatycznego budowania i wydawania z poziomu repozytorium — które przygotowują gotowe wydania na wiele architektur. Pobierasz binarkę odpowiednią dla swojej platformy i uruchamiasz.
Jest jedna pułapka warta zapamiętania: uprawnienia pinga na Linuksie. Jeśli chcesz wykonywać wiele pingów równolegle, musisz dodać użytkownika lub proces do grupy ping. Powód jest techniczny: ping na Linuksie wymaga uprzywilejowanego dostępu, czyli tzw. raw socket — niskopoziomowego gniazda do wysyłania pakietów ICMP — a sposób, w jaki Linux to ogranicza, zmieniał się przez lata.
Twórcy mieli wybór: albo użyć systemowego polecenia ping jako zwykłej nakładki — prościej, ale ze stratą asynchroniczności i wydajności — albo zrealizować ping samodzielnie, asynchronicznie i wydajnie, co wymaga wspomnianego uprawnienia. Świadomie wybrali to drugie, w imię wydajności.
Logi: dlaczego nie trafiają na standardowe wyjście
Drobny, ale przemyślany szczegół: agent z założenia nie wypisuje logów na standardowe wyjście, czyli stdout. To efekt tego, że narzędzie ma działać „bezobsługowo”, również w odległych lokalizacjach. Logi są dostępne w plikach logów — i właśnie tam należy ich szukać, jeśli chcesz coś prześledzić.
Świadome „nie”: kontenery
Na pytanie o gotową wersję skonteneryzowaną odpowiedź brzmi: „i tak, i nie”. Pomysł jest rozważany, ale na razie zespół nie widzi w nim dużej wartości — i to także jest decyzja architektoniczna.
Logika jest spójna z resztą projektu: LinkSense to jedna prosta binarka, a wdrożenie sprowadza się do schematu „skopiuj binarkę i uruchom usługę”. Skonteneryzowanie tego samodzielnie jest trywialne — wystarczy własny Dockerfile lub docker compose, który kopiuje binarkę i uruchamia usługę.
Co więcej, w kontenerze zaczynasz tracić argument o minimalnym zużyciu zasobów, który jest jedną z głównych zalet narzędzia. Da się to ograniczyć, używając lekkiego obrazu — Alpine Linux i build pod musl, czyli alternatywną, lekką bibliotekę standardową C — choć tej ścieżki autorzy sami nie testowali, więc traktują ją jako „powinno działać”.
Podsumowanie
Architektura LinkSense to seria spójnych, świadomych decyzji: agent łączy się z serwerem, bo to bezpieczniejsze; dane są przechowywane lokalnie, bo nie chcemy ich tracić; agregacja jest minutowa, bo to rozsądny kompromis; baza to SQLite, bo narzędzie ma się łatwo integrować; język to Rust, bo ma być szybko i oszczędnie.
Nic tutaj nie jest przypadkowe — i dokładnie dlatego całość pozostaje tak lekka.
Chcesz dowiedzieć się więcej o LinkSense bezpośrednio od jego twórców? Obejrzyj dedykowany webinar z Maciejem Wilamowskim i Marcinem Kaźmierczakiem, w którym opowiadają o decyzjach architektonicznych stojących za projektem, pokazują, jak działa LinkSense w praktyce, i wyjaśniają, w jakich scenariuszach może realnie pomóc w monitorowaniu rozproszonych środowisk.



