Server Sent Events (SSE) jako konkurencja do technologii WebSocket

Długo zbierałem się omówieniem tego tematu, głównie z powodu wciąż trwających przeze mnie testów technologi SSE. Pomimo, że w dalszym ciągu nie mam w pełni wyrobionego zdania to postanowiłem coś napisać i spróbować porównać obie technologie wymiany informacji między klientem a serwerem. Temat jest obszerny, mamy wiele sposobów na wykorzystanie tych rozwiązań, podobnie jak narzędzi, bibliotek czy już nawet frameworków, nie mówiąc o różnych edge casach im towarzyszących. Dlatego też tym wpisem chciałbym jedynie nakreślić ogólną problematykę i przedstawić mój przykład implementacji SSE w miejsce dotychczas używania WebSocket, zapraszam do lektury.

Co skłoniło mnie do porzucenia websocketów? Odpowiedź jest prosta, ich problematyczna natura. Niestabilność połączenia, wiele różnych sposobów na tak zwany „handshake”, niekompatybilność bibliotek czy samych przeglądarek. Momentami myślałem, że strzelam z armaty do muchy, zatem pomysł by uprościć komunikacje,uczynić ją jednokierunkową i wykorzystać ustrukturyzowane API EventSource wydaje się całkiem obiecujący.

Moje testy przeprowadziłem wspomagając się otwarto źródłowym oprogramowaniem Mercure. Stanowi on gotowe rozwiązanie do obsługi komunikatów, które serwer może wysłać do przeglądarki. Serwer łączy się zatem z Mercure Hub’em – aplikacją, która kolejkuje komunikaty od serwera i wysyła je do odpowiednich subskrybentów (klientów) danego tematu. Tworząc komunikat – wiadomość, określamy z jakim tematem jest ona związana. Co istotne, Mercure Hub potrafi buforować wiadomości i odesłać je do subskrybenta w kolejności nadania nawet jeśli klient połączy się po jakimś dłuższym czasie od pierwotnego czasu nadania wiadomości. Cała komunikacja może być szyfrowana i wymagać uwierzytelnienia. Możemy też określić jacy subskrybenci i nadawcy mają dostęp do danych tematów. Ciekawe, że Mercure dostarcza standard do implementacji hub’a, zatem pojawiły się implementacje nawet na procesory wykorzystane w Raspberry czy Qnap. By było jeszcze bardziej słodko Symfony posiada bundle integrujący się z naszym hubem. Ułatwia to nam publikowanie wiadomości i ich ewentualne debugowanie, a wszystko to przy minimalnej wymaganej konfiguracji. Większe wyzwanie stanowi część kliencka, jeśli planujemy uwierzytelnianie to musimy zrezygnować z wbudowanego w przeglądarki api Event Source i wykorzystać EventSourcePolyfill (co z kolei pozbawi nas wygodnego wbudowanego w przeglądarce debuggera komunikatów). Pamiętajmy, że technologia SSE jest komunikacją w jedną stronę, zatem po odebraniu wiadomości z serwera, gdy zajdzie konieczność otrzymania dodatkowych danych to musimy wykonać do serwera reguest XHR.

Projektem dla którego zdecydowałem się podmienić WebSockety na SSE stanowi prosta aplikacja do kasowania duplikatów zdjęć, w której pomocne jest monitorować zadania skanowania danych katalogów. Owe zadania są wykonywane równolegle przez workery, więc śledzenie ich pracy, aby miało jakiś sens, musi odbywać się w czasie rzeczywistym. Komunikat jaki serwer przesyła to informacja, jakie zadanie zmieniło swój status i w jakim procencie jest ono gotowe. Klient nie musi wysyłać do serwera dodatkowych żądań, oprócz wysłania zadania do kolejki czy gotowość na nasłuchiwanie tych statusów. W tej roli SSE wypada znakomicie. Co może stanowić problem? W moim przypadku ograniczenia w rozmiarze wiadomości, choć można przesłać cały obiekt zserializowany do formatu JSON to należy mieć na uwadzę parametry pracy danego Hub serwera. Warte zastanowienia jest czy wiadomość wysłana przez SSE powinna się ograniczać jedynie do typów prostych? Ja postanowiłem wysyłać jedynie identyfikatory zmienionych zadań, następnie klient wykonuje żądanie o dodatkowe informacje do serwera wówczas.

Jakie wady można zaobserwować po wdrożeniu SSE? Ograniczenia w formacie przesyłanych danych (tylko UTF-8), w liczbie dozwolonych równoległych połączeń (zależy od przeglądarki) czy jednokierunkowość komunikacji, ale jak dla mnie to ostatnie jest zaletą. Nie mniej w roli wysyłania powiadomień do klienta, to rozwiązanie sprawdza się znakomicie.

Wykrywanie duplikatów plików

Zabrałem się za porządki w swoich albumach zdjęciowych i szybko zauważyłem, że masa zdjęć się niepotrzebnie powtarza. By wykryć duplikaty potrzebowałem narzędzia, które przeskanuje dziesiątki gigabajtów plików. Niestety nie znalazłem żadnej takiej aplikacji, która to właściwie zrobi na moim serwerze plików QNAP. Dlatego postanowiłem napisać takie narzędzie, przy użyciu PHP i Symfony. Dodatkowo by trochę utrudnić sobie to zadanie użyłem mechanizmu kolejek, by skanowanie duplikatów mogło odbywać się równolegle przez kilka workerów, czyli wykorzystałem do tego Symfony Messenger.

Aplikacja składa się z trzech elementów: komendy pobierającej podstawową strukturę katalogów w celu odpowiedniego zakolejkowania zadań, obsługi skanowania i części analizującej zebrane wyniki. Zakolejkowanie zadań polega na połączeniu się z serwerem plików poprzez protokół smb (Windows / Samba) do wskazanego zasobu udostępniającego zdjęcia. Taki zasób składa się z kilku folderów / albumów, dla których zostanie stworzone osobne zadanie skanowania. Zadania skanowania zostają umieszczone na kolejce przez mechanizm wiadomości (Symfony Messenger) i w bazie danych, tutaj sqlite tak by wygodnie móc śledzić postęp analizy.

Skanowanie jest prostym zadaniem realizowanym przez worker, który odbiera wiadomość z adresem katalogu do analizy. Polega to na pobraniu wszystkich plików graficznych (mających odpowiednie rozszerzenie), w tym także składowanych w podkatalogach, a następnie wygenerowanie odpowiedniego hasha, przy użyciu algorytmu md5. Hash taki zostaje zapamiętany w bazie danych i stanowi rezultat skanowania.

Ostatnia część aplikacji, czyli analiza wyników sprowadza się do pobrania z bazy danych tych hashy, które występują częściej niż raz, a więc są duplikatami zdjęć (prosta komenda GROUP BY i HAVING). Komendę taką najlepiej wykonać gdy wszystkie zadania zostaną zakończone. Natomiast kasowania duplikatów pozostawiam już użytkownikowi.

Zachęcam do wypróbowania i podzielenia się opinią https://github.com/domino91/dduplicate

Subsrybowanie usług w Symfony

Jest wiele sposobów rejestrowania usług i dostępu do nich w Symfony Framework. W dzisiejszym wpisie przedstawię mój ulubiony oparty o tagi, ale przy wykorzystaniu dobrodziejstw z PHP 8 czyli atrybutów. Zanim jednak o TaggedLocator, który robi taką magię słów kilka o ServiceSubscriber.

ServiceSubscriber w Symfony służy do wstrzykiwania zależności w obiekty, które nie są usługami. Oznacza to, że ServiceSubscriber pozwala na wstrzyknięcie usług do obiektów, które same w sobie nie są usługami, ale wymagają dostępu do innych usług w systemie.

Aby skorzystać z ServiceSubscribera, klasa musi zaimplementować interfejs ServiceSubscriberInterface i zdefiniować metodę “getSubscribedServices“, która zwraca tablicę z nazwami usług wymaganych przez klasę.

Przykładowo, jeśli klasa potrzebuje dostępu do obiektu EntityManager, można wstrzyknąć tę usługę, korzystając z metody get() z obiektu Kontenera Serwisów Symfony:

use Symfony\Contracts\Service\{ServiceSubscriberInterface, ServiceLocatorTrait};

class MyClass implements ServiceSubscriberInterface
{
    use ServiceLocatorTrait;

    public static function getSubscribedServices()
    {
        return [
            'doctrine.orm.entity_manager' => EntityManagerInterface::class,
        ];
    }

    public function myMethod()
    {
        $entityManager = $this->get('doctrine.orm.entity_manager');
        // ...
    }
}

Dzięki temu wstrzykiwaniu zależności za pomocą ServiceSubscribera klasa MyClass może skorzystać z usługi EntityManager bez potrzeby wstrzykiwania jej w konstruktorze lub metodzie setEntityManager().

Jeszcze inną motywacją w używaniu ServiceSubscribera jest sytuacja, gdy chcemy zainicjalizować tylko niektóre serwisy, bo wiemy że tylko one będą nam potrzebne. Takie zarządzanie może odbywać się więc “w locie”, przez nasz serwis pełniący rolę w tym przypadku takiego menadżera / planisty / proxy.

Przejdźmy do TaggedLocator, bo to kolejny sposób na wstrzykiwanie zależności w Symfony, który jest podobny do ServiceSubscribera, ale zapewnia większą elastyczność i dostępność dla usług. W odróżnieniu od ServiceSubscribera, TaggedLocator pozwala na dynamiczne wstrzykiwanie wielu usług o określonym tagu, który jest definiowany w plikach konfiguracyjnych aplikacji. Dzięki temu TaggedLocator umożliwia wstrzykiwanie różnych usług w zależności od potrzeb i konfiguracji, co jest szczególnie przydatne w przypadku aplikacji z dużą liczbą usług.

Przykładowo, można zdefiniować kilka usług implementujących interfejs NotificationInterface, o różnych tagach:

# services.yaml
services:
    app.email_notification:
        class: App\Notification\EmailNotification
        tags: ['app.notification']

    app.sms_notification:
        class: App\Notification\SmsNotification
        tags: ['app.notification']

    app.push_notification:
        class: App\Notification\PushNotification
        tags: ['app.notification']

Następnie, można wstrzyknąć te usługi w innej klasie za pomocą TaggedLocatora:

use Symfony\Component\DependencyInjection\ServiceLocator;

class NotificationManager
{
    private $notificationLocator;

    public function __construct(ServiceLocator $notificationLocator)
    {
        $this->notificationLocator = $notificationLocator;
    }

    public function notify(string $message, string $type)
    {
        $notifications = $this->notificationLocator->getTaggedServices('app.notification');
        foreach ($notifications as $notification) {
            $notification->send($message, $type);
        }
    }
}

Wszystko fajnie, ale jak to uprościć by nie pisać tyle kodu? Tutaj przychodzą właśnie wspomniane atrybuty. Jeśli stworzymy wspólny interfejs dla notyfikacji, nie będziemy musieli ich tagować (tagiem może stać się interfejs). Przykładowo:

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag]
interface NotificationInterface
{
    public function send(string $message, string $type): void;
}

Wstrzyknięcie również możemy ułatwić sobie przez atrybuty. Albo wybieramy konkretny serwis wstrzykując sobie ServiceLocator w konstruktorze

#[TaggedLocator(NotificationInterface::class)]
        private ServiceLocator $notificationLocator,

albo bezpośrednio wstrzykujemy sobie wszystkie usługi:

use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;

class NotificationManager
{
    public function __construct( 
#[TaggedIterator(NotificationInterface::class)] private readonly iterable $notifications)
    {
    }

    public function notify(string $message, string $type)
    {
       foreach ($this->notifications as $notification) {
            $notification->send($message, $type);
        }
    }
}

normalnie czary ;-)…

Refaktoryzacja baz danych na produkcji

Jako że jestem na świeżo po lekturze książki Refactoring Databases, która stanowiła główny materiał do mojej pracy magisterskiej dot. oceny przekształceń to też postanowiłem podzielić się z wami kilkoma ciekawostkami o procesie refaktoryzacji. Właściwie w tym wpisie poruszę wyłącznie kwestię w jaki sposób wdrażać zmiany w schemacie bazy danych na środowisku produkcyjnym bazując na sugestiach autorów książki.

Pierwsza rzecz, która rzuciła mi się w oczy po lekturze to to, że wcale ten żmudny i długotrwały proces refaktoryzacji wcale nie musi być jakoś niezwykle trudny i ryzykowny. Oczywiście wprowadzanie zmian na produkcji, a już w szczególności tam gdzie trzymamy dane nie jest sielanką i zawsze niesie pewne niebezpieczeństwa. Niemniej stosując się do opisanych tutaj wskazówek można to ryzyko zminimalizować. Zacznijmy od tego, że zanim wykonamy refaktoryzacje to musimy ją odpowiednio przetestować i to w dodatku nie raz. Potrzebujemy w tym celu kilku środowisk z bazą danych. Programista opracowujący refaktoryzacje potrzebuje własnego serwera bazy danych na wyłączność, to jasne. Osobnego środowiska będzie potrzebował zespół takiego dewelopera, który z kolei dokonuje integracji całej aplikacji z taką poprawką. Podobnie jak zespoły QA, product owner na UAT itp. Zatem kolejna istotna sprawa to automatyzacja wdrażania takiej bazy danych, każde środowisko musi instalować najnowsze poprawki bazy danych całkowicie niezależnie od użytkownika. Gdy przy tym jesteśmy to warto wspomnieć o wersjonowaniu bazy danych, tj. posiadaniu zestawu narzędzi, które będą w stanie nam zapewnić monitorowanie zmian na bazie oraz zdolność do ich wycofywania (patrz np. Doctrine Migrations). Posiadać powinniśmy również szereg różnego rodzaju automatycznych testów regresyjnych czy integracyjnych. Natomiast samo opracowanie refaktoryzacji zdaniem autorów książki warto wykonać w duchu TDD, gdzie np. opracowujemy test sprawdzający istnienie refaktoryzowanych kolumn czy kluczy obcych działając przy tym iteracyjnie. Całość tego procesu dopełniają wyzwalacze, których zadaniem jest utrzymywać produkcyjną bazę danych jednocześnie w dwóch stanach (przed i po refaktoryzacji). W momencie, w którym uznamy że wszystkie dane poprawnie się przeniosły do nowego schematu, można stary schemat i wyzwalacze usunąć. Poniżej przykład takiego wyzwalacza dla przekształcenia podziału na kolumny (zmieniamy kolumnę nazwa na imię i nazwisko).

CREATE OR REPLACE TRIGGER SynchronizacjaNazwyUzytkownika
BEFORE INSERT OR UPDATE
ON Uzytkownicy
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
IF :NEW.Imie IS NULL THEN
:NEW.Imie := pobierzImie(Nazwa);
END IF;
IF :NEW.Nazwisko IS NULL THEN
:NEW.Nazwisko := pobierzNazwisko(Nazwa);
END IF;
END;

W tej sytuacji, gdy będzie dodawany lub aktualizowany rekord z nazwą użytkownika jednocześnie zostaną zaktualizowane nowo wprowadzone kolumny imie i nazwisko (o ile poprawnie zaimplementujemy funkcje wyłuskujące takie dane, tutaj pobierzImie, pobierzNazwisko). W kolejnej iteracji będzie można usunąć kolumnę nazwa.

Ostatnią istotną kwestią jest znalezienie właściwego “okna” czasowego na przeprowadzenie zmian na bazie produkcyjnej i poinformowanie o takim terminie wszystkich zainteresowanych. Najlepiej by to był czas względnego spokoju, gdy baza nie jest nadzwyczaj obciążona, w szczególności gdy używamy chmurowych środowisk bazodanowych w klastrze 😉

aaa i pamiętajcie, zawsze warto mieć kopie! Powodzenia!

Flagi funkcji

Dzisiaj na blogu o podejściu do zarządzania nowymi funkcjami w aplikacji. W jaki sposób je wdrażać, wycofywać, katalogować w zależności od różnych strategii biznesowych. Z pomocą tutaj przychodzi nam koncepcja flag funkcji (Feature Toggles / Feature Flags). Pozwala ona zmieniać zachowanie aplikacji bez zmian w kodzie. Wprowadzamy tutaj rozróżnienie na punkty przełączania, konfiguracje przełączania, kontekst przełączania i jego routing. Punkty przełączania stanowią rozwidlenia w kodzie przy użyciu instrukcji if lub strategi / polimorfizmu. Konfiguracja to to zbiór zasad określających, które flagi są włączone / wyłączone dla poszczególnych funkcji, ale też w zależności od dostarczonego kontekstu. Możemy ją trzymać w pliku płaskim, w bazie danych czy nawet w zewnętrznych serwisach jak Zookeeper, etcd, Consul. Zalecane jest jednak by konfiguracja była zapisana w kodzie źródłowym, gdzie będzie podlegać ewidencji. Programista będzie wiedział kto i dlaczego włączył daną funkcję, ale też analizując kod z przed kilku miesięcy jakie w danym czasie funkcje były uruchomione. Kolejnym elementem jest kontekst przełączania, określa specyficzne parametry potrzebne do obsługi żądania jak np. posiadanie odpowiedniego ciasteczka w przeglądarce. Ostatnim elementem jest routing, to serce całego systemu. Na podstawie punktów przełączania, dostarczonej konfiguracji i kontekstu odpowiednio prowadzi użytkownika po funkcjach aplikacji. Poniżej przykład implementacji tego wzorca przy użyciu biblioteki flagception-bundle.

Zaleca się by stosować osobne flagi w zależności od czasu ich użycia i częstotliwości. Osobne dla mendażerów a osobne dla działu devops. Flagi powinny być włączone w cały proces CI/CD i testować wszystkie możliwe ścieżki. Należy publikować co wydanie informacje, które funkcje są włączone/wyłączone, w tym również trzeba ewidencjonować kto, co i dlaczego dokonał zmiany. Narzędzie do zarządzania konfiguracją powinno być możliwie proste w obsłudze i łatwo dostępne.

Źródło:
https://martinfowler.com/articles/feature-toggles.html
https://github.com/bestit/flagception-bundle

Lazy Doctrine

Po ostatnich moich przeprawach z doctrinem i dociąganiem danych z bazy dodatkowym żądaniem, stwierdziłem że muszę napisać tą notkę bo to po prostu musi zostać gdzieś utrwalone!

Domyślnie Doctrine nie dociąga wszystkich relacji danej encji, stosuje strategie “lazy“. Polega ona na tym, że nasz ORM trzyma obiekt proxy zamiast właściwych danych. W momencie gdy po te dane sięgniemy, zostaną pobrane z bazy. Oczywiście możemy takie zachowanie zmienić, by mieć dane zawsze pod ręką. Wystarczy użyć w np. adnotacji OneToMany parametru fetch=”EAGER” i już..co jednak gdy nie chcemy modyfikować encji?

Innym sposobem jest stworzenie własnego zapytania o dane, gdzie jawnie określimy, które relacje są nam potrzebne. Dla przykładu:

$qb = $em->getRepository('Ksiazka')
           ->createQueryBuilder('k');

$qb->select('k');

$qb->leftJoin('k.autor', 'autor');
$qb->addSelect('autor');

$qb->where('k.id = :id');
$qb->setParameter('id', 1);

$q = $qb->getQuery();

$ksiazka = $q->getSingleResult();

Tym sposobem zobaczymy konkretną książkę wraz z obiektem autor, który ją napisał. Nie musimy stosować EAGER w adnotacji. Ważne jest tutaj by pamiętać o użyciu addSelect, tak by doctrine wiedział, że musi pobrać również i autora w tym samym żądaniu (można równie dobrze dopisać to wcześniej w select()). Nic też nie stoi na przeszkodzie by pobrać także inne encji w tym te, które są powiązane z autorem.

Jednakże w niektórych sytuacjach możemy mieć problem z hydracją i właśnie to jest pretekst do dzisiejszego wpisu.
Owy problem to sytuacja w której mimo, że encje i jej relacje poprawnie pobraliśmy to Doctrine ich nie mapuje. Przyczyn może być wiele jednakże w moim przypadku było to używanie w konstruktorze klasy agregującej inicjalizacji obiektu jako ArrayCollection a nie domyślnej wartości null. Gdy sami zainicjujemy dane, Doctrine potrzebuje wiedzieć, że może je nadpisać. Zatem do powyższego zapytania należałoby dorzucić jeszcze:

$qb->getQuery()
            ->setHint(Query::HINT_REFRESH, true); 

Tłumaczy to hydratorowi by wymusił odświeżanie wszystkich obiektów (nadpisał to co już jest)….i właściwie to tyle

Symulowanie sieci komputerowej w ns-3

Ostatnimi czasy intensywnie studiuję temat protokołów routingu w sieciach typu ad hoc. Zbudowanie odpowiedniej infrastruktury, posiadającej wiele komputerów, routerów oraz ich właściwe sparowanie znacznie wykracza poza moje możliwości logistyczno-finansowe, to też zdecydowałem się na ich symulacje. Użyłem w tym celu ns-3 Network Simulator, i właśnie o tym narzędziu oraz o pisaniu symulacji będzie dzisiejszy wpis.

Symulację piszemy w języku C++ (można też w Python), umieszczając pliki w folderze scratch programu. W przypadku języka C++ pliki muszą mieć rozszerzenie “cc“. Zalecane jest by w przypadku większej liczby plików pogrupować je w projekty poprzez utworzenie folderów. Należy wtedy jednak pamiętać o zdefiniowaniu funkcji main, która zostanie uruchomiona jako punkt startowy symulacji. Kompilacja plików robi się automatycznie podczas uruchomienia symulacji, sam program odpala się w następujący sposób:

./waf run --nazwa naszego pliku, ale bez rozszerzenia cc

Na czym polega filozofia pisania symulacji w ns-3?

Właściwie sprowadza się ona do odpowiedniego skonfigurowania topologi sieci, którą byśmy chcieli badać. Odpalenia zestawu aplikacji (symulowania ruchu), a następnie zebrania wyników i ich obrobienie. Ns-3 posiada gotowe komponenty, które znacznie przyspieszają całą pracę. Topologie sieci budujemy w oparciu o strukturę Node (węzeł). Może to być dowolne urządzenie w sieci (niekoniecznie tylko host). Do każdego węzła przypisujemy urządzenia, które ma posiadać (np. karta sieciowa i jej parametry), jak również zestaw aplikacji (można pisać własne, albo użyć już dostarczonych), które mają być na nim uruchomione. Dobrze to ilustruje obrazek poniżej (zapożyczony z prezentacji ns3 intro slides autorstwa Adil Alsuhaim).

Na szczęście do naszej dyspozycji jest masa helperów, które przyspieszą cały ten proces i skonfigurują ustaloną przez nas grupę węzłów. Poniżej przykład jak można utworzyć topologię sieci WIFI typu ad hoc, zaczerpnięty z http://www.lrc.ic.unicamp.br/ofswitch13/doc/html/wifi-hidden-terminal_8cc_source.html.

    // 1. Create 3 nodes 
    NodeContainer nodes;
    nodes.Create (3);

    // 2. Place nodes
    for (size_t i = 0; i < 4; ++i)
    {
        nodes.Get(i)-> AggregateObject (CreateObject<ConstantPositionMobilityModel> ());
    }

    // 3. Create propagation loss matrix
    Ptr<MatrixPropagationLossModel> lossModel = CreateObject<MatrixPropagationLossModel> ();
    lossModel->SetDefaultLoss (200); // set default loss to 200 dB (no link)
    for (size_t i = 0; i < 3; ++i)
    {
        lossModel->SetLoss (nodes.Get (i)-> GetObject<MobilityModel>(), nodes.Get (i+1)->GetObject<MobilityModel>(), 50); // set symmetric loss i <-> i+1 to 50 dB
    }

    // 4. Create & setup wifi channel
    Ptr<YansWifiChannel> wifiChannel = CreateObject <YansWifiChannel> ();
    wifiChannel->SetPropagationLossModel (lossModel);
    wifiChannel->SetPropagationDelayModel (CreateObject <ConstantSpeedPropagationDelayModel> ());

    // 5. Install wireless devices
    WifiHelper wifi;
    wifi.SetStandard (WIFI_PHY_STANDARD_80211b);
    Config::SetDefault ("ns3::WifiRemoteStationManager::RtsCtsThreshold", StringValue ("2200"));
    wifi.SetRemoteStationManager ("ns3::ConstantRateWifiManager", 
                                  "DataMode",StringValue ("DsssRate11Mbps"));
    YansWifiPhyHelper wifiPhy =  YansWifiPhyHelper::Default ();
    wifiPhy.SetChannel (wifiChannel);
    WifiMacHelper wifiMac;
    wifiMac.SetType ("ns3::AdhocWifiMac");
    NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

    // 6. Install TCP/IP stack & assign IP addresses
    InternetStackHelper internet;
    internet.Install (nodes);
    Ipv4AddressHelper ipv4;
    ipv4.SetBase ("10.0.0.0", "255.0.0.0");
    ipv4.Assign (devices);

Jak widać powyżej tworzymy 3 węzły, na których konfigurujemy sieć WIFI typu ad hoc i ustawiamy adresy ip dla sieci 10.0.0.0. By uczynić symulację bardziej realną używamy tak zwanej tablicy utraty propagacji sygnału na wskutek np. działania warunków pogodowych. Ważna kwestią, o której jeszcze nie wspomniałem to kwestia kanałów. Powyżej używamy jednego z nich tj. YansWifiChannel. Kanały pozwalają określić relacje, połączyć poszczególne węzły. W przypadku sieci typu ad hoc, YansWifiChannel musi połączyć węzły na zasadzie “każdy z każdym“. Gdy już mamy skonfigurowaną topologie naszej symulowanej sieci, pozostaje nam zasymulować jakiś ruch przy użyciu aplikacji. Poniżej przykład żądania i odpowiedzi na ping, który zostanie wysłany w ściśle określonym przez nas przedziale czasowym.

  ApplicationContainer serverApps = echoServer.Install (nodes.Get (3));
  serverApps.Start (Seconds (1.0));
  serverApps.Stop (Seconds (10.0));
   
  UdpEchoClientHelper echoClient (csmaInterfaces.GetAddress (3), 9);
  echoClient.SetAttribute ("MaxPackets", UintegerValue (1));
  echoClient.SetAttribute ("Interval", TimeValue (Seconds (1.0)));
  echoClient.SetAttribute ("PacketSize", UintegerValue (1024));
   
  ApplicationContainer clientApps = 
  echoClient.Install (wifiStaNodes.Get (nWifi - 1));
  clientApps.Start (Seconds (2.0));
  clientApps.Stop (Seconds (10.0));

Jeśli ktoś z Was złapał bakcyla, lub potrzebuje coś sobie potestować to polecam filmik poniżej, który bardziej szczegółowo to wszystko omawia.

Kubernetes na Debian, szybkie wprowadzenie

Cześć w dzisiejszym wpisie podzielę się moimi notatkami z instalacji klastra Kubernetes na systemie Debian (chciałem na Ubuntu, ale było za ciężko ;-(…). Jako instalator Kubernetesa wybrałem Kubeadm, głównie ze względu na dużą liczbę materiałów w sieci jak i to, że idealnie poradzi sobie na serwerach bare-metal.

Przygotowanie systemu i instalacja

Zaczniemy od wyłączenia domyślnego SWAPU (Kubernetes sam musi zarządzać przestrzenią wymiany). Musimy w tym celu zakomentować odpowiedni UUID w naszym pliku z punktami montowań.

swapoff -a
nano /etc/fstab

Kolejny krok to instalacja składników sieciowych i konfiguracja zapory sieciowej w /etc/sysctl.conf.

apt-get install ebtables ethtool
cat <<EOF > /etc/sysctl.conf
net/bridge/bridge-nf-call-ip6tables = 1
net/bridge/bridge-nf-call-iptables = 1
net/bridge/bridge-nf-call-arptables = 1
EOF

Pozostaje nam zainstalowanie dockera (jeśli go jeszcze nie mamy), dodanie klucza i repozytorium do składników Kubernetes i wreszcie ich sama instalacja.

apt-get install -y docker.io apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -

cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet kubeadm kubectl

Tworzenie klastra

Sam proces jest dosyć prosty, ale jeśli macie tak jak ja kilka kart sieciowych w serwerze, który chcecie by stał się tak zwanym “master” to musicie przemyśleć adresacje ip. Jest tak ponieważ pody potrzebują komunikować się w swojej własnej przestrzeni adresowej, inna podsieć ip będzie użyta do komunikacji między węzłami a jeszcze inna może być użyte do sieci WAN. W przykładzie poniżej sieć 10.0.0.0/8 jest siecią węzłów a 192.168.1.0/16 siecią podów.

kubeadm init --pod-network-cidr=192.168.1.0/16 --apiserver-advertise-address 10.0.0.1

Gdy klaster zostanie zainstalowany otrzymamy na konsoli dalsze instrukcje. Trzeba będzie zainicjalizować konfigurację dla programu kubectl w zależności czy jesteśmy użytkownikiem root czy nie. Dla użytkownika bez superuprawnień wygląda to tak

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Kolejny istotny punkt to instalacja sterownika sieci dla naszych podów. Dla mnie był to najbardziej denerwujący moment w trakcie całej instalacji. Po nieowocnych bitwach z próbą instalacji sterownika Calico a potem jeszcze większym zmarnowaniem czasu z Weave, polecam Flannel – zadziałał niemalże out of the box.

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml 

Można sprawdzić czy sieć i pozostałe komponenty klastra zainstalowały się poprawnie poprzez wydanie komendy listującej nam wszystkie pody (w tym także właśnie te administracyjne):

kubectl get pods --all-namespaces

Pozostaje nam odblokowanie planisty

kubectl taint nodes --all node-role.kubernetes.io/master-

Dodanie węzłów do naszego klastra, przy użyciu wygenerowanego tokena (na etapie kubeadm init). Oczywiście robimy to na każdym serwerze typu węzeł oddzielnie, dla przykładu:

kubeadm join --token 354502.d6a9a425d5fa8f2e 10.0.0.1:6443 --discovery-token-ca-cert-hash sha256:ad7c5e8a0c909eda6a87452e65fa44b1c2a9729cef7285eb5C1e2f126a1d6a54

Jeśli zgubiliśmy token, możemy go pobrać wydając komendę

kubeadm token list 

Finalnie całość funkcjonowania systemu możemy sprawdzić poprzez

kubectl get cs

Przydatne do debugowania mogą być dla nas również poniższe komendy

kubectl get deployments
kubectl get nodes 
kubectl get pods --selector app=demo [-A dla wszystkich namespace)
kubectl describe pod
kubectl logs -n nazwa poda?? np. weave-net-67x28 lub -c by określić container
kubectl logs pod -n kube-system (jako namespace) --tail 100 (daj mi 100 logów ostatnich)
kubectl top nodes

uff..jeśli dotarłeś do tego miejsca, to możesz już śmiało dodawać swoje pierwsze pody na Twój klaster Kubernetes 😉

PHP 8 – co ciekawego?

Mimo, że od premiery najnowszej wersji PHP 8 za chwilę minie rok to ja dopiero teraz zdecydowałem się napisać co ciekawego ta wersja przynosi. Powód? Bardzo prosty, musiałem na własnej skórze sprawdzić, które ulepszenia są naprawdę przydatne w mojej codziennej pracy. Przyjrzyjmy się tej liście.

Na pierwszy ogień idą argumenty nazwane (named arguments), pozwalają one zmieniać kolejność przekazywania argumentów do funkcji/metody, a tym samym unikać konieczności podania wartości z zdefiniowaną domyślną wartością. Dla przykładu:

<?php

function rysuj(int $x, int $y = 25, string $figura = 'kwadrat') {
    printf ('Rysuje w punkcie x: %d, y: %d figure: %s', $x, $y, $figura);
}

rysuj(
 figura : 'trojkat', // tym samym unikamy podania parametru y - zostanie użyta wartość domyślna
  x: 12
);

Długo wyczekiwanym przez mnie usprawnieniem PHP są adnotacje (lub też inaczej atrybuty) wbudowane w język. Nie musimy już posiłkować się bibliotekami jak Doctrine\ORM. Pomimo, że wybór właściwej notacji był niezwykle burzliwy to osiągnięto kompromis definiując adnotacje przy użyciu Syntax #[…]. Należy też nadmienić, że twórcy w sposób staranny i przemyślany projektowali ten feature. Z tego też powodu będę chciał bardziej szczegółowo opisać adnotacje w osobnym wpisie.

Zdecydowanie najczęściej używaną przeze mnie nowością jaką zaoferowało PHP 8 to bezpieczny operator zerowy (nullsafe operator). Pozwala uniknąć całej ifozy sprawdzeń przy wypakowywaniu konkretnej wartości. Sprawdźmy to na przykładzie z trzema klasami, użytkownik posiada adres, adres posiada obiekt kraj.

<?php
class Country {
    private $code = null;
    
    public function getCode(): ?string
    {
        return $this->code;
    }
}

class Address {
    private ?Country $country;
    
    public function getCountry(): ?Country
    {
        return $this->country;
    }
}

class User {
    private ?Address $address = null;
    
    public function getAddress(): ?Address
    {
        return $this->address;
    }
}

$user = new User();

echo $user?->getAddress()?->getCountry()?->getCode();

W przypadku wystąpienia wartości null nie otrzymamy błędu, kod dalej będzie się wykonywać.

Jednakże nie wszystkie feature PHP 8 przypadły mi do gustu. Unie są dobrym przykładem tezy, że nie zawsze rozwój jest postępem. W tym przypadku mówimy o cofnięciu się w rozwoju języka do czasów z przed PHP 7 ponieważ unie to konstrukcja pozwalająca na definiowania kilku typów dla danej zmiennej (co praktycznie oznacza brak typu).

<?php

class samochod {
    private int|float|string $cena;
    
    public function ustawCena(int|float|string $cena)
    {
        $this->cena = $cena;
    }
    
    public function pobierzCena(): int|float|string
    {
        return $this->cena;
    }
}

$fiat = new samochod();
$fiat->ustawCena(99.99);

echo $fiat->pobierzCena();

Nie dostrzegam również korzyści z wyrażenia dopasowania (match expressions). Jak dla mnie to tylko nakładka na switch case z kilkoma wyjątkami.

<?php
$name = match(2) {
    1 => 'One',
    2 => 'Two',
};

echo $name; // "Two"

choć może to jednak kwestia przyzwyczajenia się do tego całego PHP 8?

CUDA, czyli wpis o programowaniu z użyciem kart graficznych

Z racji tego, że ostatnio na uczelni zderzyłem się z tematem współbieżnych programów opartych o procesor karty graficznej to postanowiłem podzielić się z Wami tymi rewalacjami. Postaram się tutaj po krótce opisać podstawowe elementy i koncepcje, którymi rządzi się takie podejście do programowania. Od razu zawęże, że mowa tu będzie tylko o programowaniu kart graficznych opartych o architekturze CUDA (Compute Unified Device Architecture).

Zacznijmy od scharakteryzowania głównej różnicy. Mamy tutaj dwa procesory. Procesor CPU zwany w terminologii hostem oraz procesor karty graficznej (GPU) zwany urządzeniem. Ogólna zasada programowania sprowadza się do:

  • uruchomienia programu na procesorze CPU,
  • skopiowaniu potrzebnych danych do pamięci karty graficznej (tak GPU posiada własną pamięć L2, DRAM),
  • wykonaniu kodu współbieżnego (z podziałem na bloki/wątki) przez procesor karty graficznej,
  • skopiowaniu wyników z urządzenia (GPU) do pamięci operacyjnej RAM i dalsze kroki np. prezentacja wyników.

Podział kodu na część, która ma być wykonywana przez GPU określamy za pomocą słowa kluczowego __global__ przy deklaracji funkcji. Taka funkcja zostanie wywołana przez procesor CPU, i wykonana przez procesor karty graficznej. Podczas wywołania w nawiasach ostrych <<< >>> określamy na ilu blokach i ilu wątkach ma się uruchomić nasz kod.

__global__ void mykernel(void) {

}

int main(void) {
   mykernel<<<1,1>>>();
   printf("Hello World!\n");
   return 0;
}

Pomówmy teraz o alokacji pamięci, czyli o wymianie danych między procesorami. W przypadku zarządzania pamięcią GPU dostajemy funkcje cudaMalloc(), cudaFree(), cudaMemcpy() będące odpowiednikiem z języka C do zarządzania pamięcią operacyjną – malloc(), free(), memcpy(). Przy czym należy pamiętać, że wskaźniki operujące na pamięci urządzenia mogą być przetworzone do i z kodu na hoście. Nie można natomiast wykonywać dereferencji (odwołania się do wartości danej zmiennej) przez kod na hoście. Analogicznie dla wskaźników operujących na pamięci hosta.

int main(void) {
int a, b, c; // host copies of a, b, c
int *d_a, *d_b, *d_c; // device copies of a, b, c
int size = sizeof(int);

// Allocate space for device copies of a, b, c
cudaMalloc((void **)&d_a, size);
cudaMalloc((void **)&d_b, size);
cudaMalloc((void **)&d_c, size);
// Setup input values
a = 2;
b = 7;


// Copy inputs to device
cudaMemcpy(d_a, &a, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, &b, size, cudaMemcpyHostToDevice);
// Launch add() kernel on GPU
add<<<1,1>>>(d_a, d_b, d_c);
// Copy result back to host
cudaMemcpy(&c, d_c, size, cudaMemcpyDeviceToHost);
// Cleanup
cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
return 0;
}

Do powyższego kodu można dopisać funkcje dodawania, która wykona się równolegle w ramach bloków. Koncepcja bloków, choć podobna do wątków ma zasadniczą różnice – bloki są od siebie odseparowane tj. nie mogą wpływać wzajemnie na swoją pamięć. W architekturze CUDA, bloki stanowią dodatkową abstrakcje tj. grupują wątki, które z kolei posiadają wspólną pamięć. Zmienna blockIdx.x dostarcza identyfikator konkretnego wątku. Stąd przykładowa definicja metody sumującej macierz do poprzedniego kodu mogła by wyglądać w następujący sposób.

__global__ void add(int *a, int *b, int *c) {
c[blockIdx.x] = a[blockIdx.x] + b[blockIdx.x];
}

Dla osób, które złapały bakcyla do programowania kart graficznych odsyłam po więcej informacji do strony producenta https://www.nvidia.com/docs/IO/116711/sc11-cuda-c-basics.pdf