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.

https://youtu.be/2W5mdzQrwXI

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 ;-)