SDLC / Policy-as-Code

Proces jako łańcuch Markowa: stochastyka wydań i utrzymania

Do niektórych tekstów wraca się latami. Ja od dawna wracam do pracy magisterskiej o stochastycznym modelowaniu komórkowych szlaków sygnałowych - biochemia, enzymy, fosforylacja białek, teoretycznie zupełnie nie moja bajka. A jednak sporo procesów, które projektuję - wydania, utrzymanie, reakcję na incydenty - układam na jej fundamentach. Bo idea pod spodem jest ta sama: masz zbiór dyskretnych bytów, które zmieniają stan przez zdarzenia zachodzące z pewną szybkością. W biologii to molekuły i reakcje; w platformie to instancje usług, wdrożenia, żądania i awarie. Autor pisał wprost o paradygmacie „molekuła jako proces” - wystarczy podmienić molekułę na usługę, a cały ten sposób myślenia zaczyna opisywać utrzymanie i wydawanie aplikacji.

Ten artykuł bierze więc pomysł z tamtej pracy i przykłada go do procesów platformowych w duchu everything-as-code (EIAC - „wszystko jako kod”). Piszę go tak, żeby dało się go przeczytać bez ani jednego wzoru: cała argumentacja jest w słowach, a matematyka siedzi w wydzielonych, opcjonalnych ramkach „zapis formalny” - dla tych, którzy chcą zobaczyć, że to nie metafora, tylko dobrze opisana teoria. Jeśli wolisz, możesz te ramki pominąć i nic nie stracisz z głównej myśli.

Teza

Proces wydawania i utrzymania aplikacji jest z natury losowy: to układ dyskretnych zdarzeń (wdrożenie, awaria, przywrócenie) o mierzalnych szybkościach. Uśredniona, „deterministyczna" intuicja („zwykle działa") ukrywa ryzyko ogona - rzadkich, kosztownych zdarzeń - a to one generują większość kosztu. Kiedy zaczniesz myśleć o procesie jak o łańcuchu zdarzeń o pewnych szybkościach, a o swoich metrykach jak o tych szybkościach, przenosisz decyzje z przeczucia na rozkład prawdopodobieństwa. Everything-as-code i obserwowalność dostarczają stanów i szybkości jako danych, więc to podejście przestaje być akademickie, a staje się narzędziem.

Średnia kłamie: makro kontra mikro

W modelowaniu reakcji chemicznych klasyczne, „gładkie” równania (ciągłe i deterministyczne - dające zawsze ten sam wynik) działają świetnie w skali makro, gdzie molekuł są miliardy i fluktuacje się uśredniają. Ale w skali mikro, gdzie kluczowych cząsteczek są dziesiątki, uśrednianie kłamie: liczy się konkretna, losowa trajektoria. Dlatego do małych, ważnych układów stosuje się opis stochastyczny (czyli losowy - oparty na prawdopodobieństwie, a nie na jednej z góry ustalonej wartości).

Dokładnie ten sam podział jest w platformie. „Nasza dostępność to 99,9% średniorocznie” to opis makro - i bywa prawdziwy, a zarazem bezużyteczny. Bo to nie średnia Cię boli, tylko ten jeden apply w piątek o 17:00, który zbiegł się z awarią bazy i wywrócił produkcję na 40 minut. Zdarzenia rzadkie i skorelowane - ogon rozkładu - są z natury losowe, a to one dominują koszt incydentów. Deterministyczna średnia je zamiata pod dywan. Jeśli chcesz o nich rozmawiać serio, potrzebujesz języka rozkładów prawdopodobieństwa, nie jednej liczby „przeciętnie”.

Twój system jako łańcuch zdarzeń

Standardowym narzędziem do opisu takich układów jest łańcuch Markowa z czasem ciągłym (ang. continuous-time Markov chain, w skrócie CTMC). Brzmi poważnie, ale to po prostu formalna nazwa na „system, który przeskakuje między dyskretnymi stanami, a każde przejście ma swoją intensywność”. Ma trzy składniki, które w platformie mają dosłowne odpowiedniki:

  • Stany - dyskretne sytuacje, w których może być usługa lub wdrożenie. Np. Zdrowa → Zdegradowana → Awaria → Przywracanie. To nie metafora: to realne stany, które i tak widzisz w obserwowalności.
  • Przejścia - możliwe zdarzenia zmieniające stan (degradacja, awaria, samonaprawa, przywrócenie, wdrożenie, wycofanie).
  • Szybkości (ang. rate) - jak intensywnie zachodzi dane przejście. W chemii to stała szybkości reakcji; tu - częstość awarii, tempo naprawy, częstotliwość wdrożeń. Umownie oznacza się je greckimi literami: λ (lambda) dla „psucia się” i μ (mi) dla „naprawiania”.
Zdrowa Zdegradowana Awaria λ₁ λ₂ μ₁ μ₂ μ (pełne przywrócenie)
Cykl życia usługi jako łańcuch zdarzeń: λ (lambda) to szybkości degradacji i awarii, μ (mi) - szybkości naprawy. Dostępność to po prostu prawdopodobieństwo, że w losowo wybranej chwili proces jest w stanie „Zdrowa".

Najważniejsza cecha takiego łańcucha to brak pamięci: to, co stanie się dalej, zależy tylko od bieżącego stanu, a nie od tego, jak do niego doszliśmy. To potężne uproszczenie - i zaraz wrócę do tego, gdzie ono kłamie.

Zapis formalny (opcjonalny)

„Brak pamięci” znaczy, że czas do następnego zdarzenia ma rozkład wykładniczy: , gdzie to suma szybkości wszystkich możliwych w danym stanie przejść. Im większa łączna szybkość, tym krócej średnio czekamy na cokolwiek.

Twoje metryki to po prostu te szybkości

I tu jest pointa dla praktyka, którą można wypowiedzieć bez jednego symbolu: metryki, którymi już mierzysz dostarczanie, to zmierzone szybkości tego łańcucha. Nie musisz ich zgadywać - masz je w danych.

Weźmy cztery klasyczne metryki DORA (od zespołu badawczego DevOps Research and Assessment - nie mylić z unijnym rozporządzeniem DORA, czyli Digital Operational Resilience Act; ten sam akronim, zupełnie co innego). Mapują się wprost na elementy łańcucha:

Metryka DORARola w łańcuchu zdarzeń
Częstość wdrożeń (deployment frequency)szybkość przejść „wdrożenie” - tempo, w jakim wprowadzasz zmianę stanu
Odsetek nieudanych zmian (change failure rate)prawdopodobieństwo, że przejście „wdrożenie” prowadzi do stanu Zdegradowana/Awaria
Czas przywrócenia usługi (MTTR - Mean Time To Restore, średni czas do naprawy)odwrotność szybkości naprawy μ: krótki MTTR = duże μ
Czas realizacji zmiany (lead time for changes)jak długo zmiana czeka, zanim w ogóle wejdzie do układu

Innymi słowy: ponieważ w EIAC wszystko jest kodem i przechodzi przez obserwowalną platformę, każdy stan i każde przejście zostawiają ślad. Dostajesz λ i μ jako dane, nie jako przeczucie.

Zapis formalny (opcjonalny)

W modelu stochastycznym każdemu przejściu przypisuje się propensity (skłonność) - miarę tego, jak „chętnie” zdarzenie zachodzi tu i teraz: , czyli iloczyn stałej szybkości i liczby dostępnych kombinacji „substratów” w bieżącym stanie . W platformie „substratem” bywa gotowa do wdrożenia zmiana albo wolny worker autoskalera.

Dlaczego szybciej naprawiać, niż rzadziej się psuć

Najbardziej praktyczny wniosek z tego całego aparatu też da się powiedzieć jednym zdaniem: dostępność podniesiesz szybciej, skracając czas naprawy, niż polując na kolejną rzadką przyczynę awarii. Skrócenie przywracania z 40 do 10 minut robi dla dostępności więcej niż heroiczne tropienie jednego usterki-widma raz na kwartał.

To nie jest opinia - to wprost wynika z tego, jak liczy się dostępność. Reliability engineering (inżynieria niezawodności) to po prostu stosowana teoria procesów losowych, a znany wszystkim wzór na dostępność jest stanem ustalonym prostego łańcucha Markowa. To samo mówi pętla OODA z wprowadzenia (Observe-Orient-Decide-Act: obserwuj-orientuj się-decyduj-działaj): wygrywa ten, kto szybciej domyka pętlę - czyli ma większe μ.

Zapis formalny (opcjonalny)

Dla najprostszego naprawialnego układu (dwa stany: Up/Down, awaria z szybkością , naprawa z ) dostępność w stanie ustalonym to:

gdzie MTBF (Mean Time Between Failures) to średni czas między awariami, a MTTR (Mean Time To Restore) to średni czas naprawy. Widać wprost: przy podobnym nakładzie łatwiej ruszyć (naprawę) niż (rzadką awarię). Pełny opis ewolucji rozkładu po stanach daje równanie master, którego ten wzór jest szczególnym, ustalonym przypadkiem.

Przećwicz okno wdrożeniowe, zanim je otworzysz

Prosty wzór wystarcza dla dwóch-trzech stanów. Przy realnej sieci przejść (wiele usług, zależności, okna wdrożeniowe) nie liczy się tego na kartce - symuluje się. Puszczasz tysiące losowych „przebiegów” swojego procesu i patrzysz na rozkład wyników. To zwykłe Monte Carlo (metoda szacowania przez wielokrotne losowanie - nazwa od kasyna), tyle że zastosowane do Twojego pipeline’u.

Kanonicznym sposobem takiego losowania trajektorii jest algorytm Gillespiego (ang. Stochastic Simulation Algorithm, SSA). W środku jest prosta pętla: policz, jak szybko może się wydarzyć cokolwiek, wylosuj kiedy zajdzie następne zdarzenie i które to będzie, zastosuj je, powtarzaj.

t = 0
while t < T_okna:
    a0 = Σ aᵢ                      # jak szybko może zajść cokolwiek (suma szybkości)
    τ  = Exp(a0)                   # kiedy zajdzie następne zdarzenie (losowy czas)
    j  = wylosuj zdarzenie ∝ aⱼ    # które zdarzenie zaszło (proporcjonalnie do szybkości)
    zastosuj_zdarzenie(j)          # zmień stan (wdrożenie, awaria, przywrócenie...)
    t += τ

Mając zmierzone szybkości, odpowiadasz na pytania, których średnia nie tknie: jakie jest prawdopodobieństwo dwóch równoczesnych awarii w oknie wdrożeniowym?, ile wdrożeń kanarkowych trzeba, żeby ryzyko regresji spadło poniżej progu?, czy autoskaler nadąży za szpilką ruchu? (to ostatnie to klasyczny model kolejkowy M/M/c - w notacji Kendalla: losowe, wykładnicze napływy / wykładnicza obsługa / c równoległych serwerów - też odmiana łańcucha Markowa). Zamiast wdrażać „na czuja w piątek”, przećwiczasz okno wdrożeniowe, zanim je otworzysz.

Cel poziomu usług jako własność, nie życzenie

Praca opisuje też formalne reprezentacje takich łańcuchów - stochastyczne sieci Petriego, algebry procesów PEPA (Performance Evaluation Process Algebra, formalny język do opisu wydajności systemów) - oraz weryfikację modelową: automatyczne sprawdzanie, czy model spełnia zadaną własność. Robi się to np. weryfikatorem probabilistycznym PRISM, a własności zapisuje w logice CSL (Continuous Stochastic Logic, stochastyczna logika temporalna - język do wyrażania zdań typu „z prawdopodobieństwem co najmniej X w czasie Y…”). Brzmi bardzo akademicko, dopóki nie zauważysz, że dokładnie o to chodzi w SLO.

SLO (ang. Service Level Objective, cel poziomu usług - mierzalny cel niezawodności, np. „99,9% udanych żądań w miesiącu”) to bliski krewny SLA (ang. Service Level Agreement, umowa o poziomie usług - zobowiązanie kontraktowe wobec klienta). Różnica jest praktyczna: SLA to obietnica w umowie, SLO to wewnętrzny cel, który faktycznie pilnujesz.

I teraz clou: zdanie „z prawdopodobieństwem co najmniej 0,999 system wraca do stanu Zdrowa w ciągu 5 minut” to nie slajd - to własność, którą można zweryfikować względem modelu. To ten sam ruch, co deterministyczny szkielet platformy agentowej i policy-as-code: przestajemy mieć nadzieję, że system się zachowa, a zaczynamy tworzyć specyfikację i sprawdzać jego własności. Policy-as-code egzekwuje reguły na pojedynczym apply; weryfikacja modelowa pyta o zachowanie całej dynamiki procesu w czasie.

Uczciwie: gdzie model kłamie

Nie chcę Ci tego wciskać, przemilczając haczyki - a te są istotne. Założenie o braku pamięci w rzeczywistości jest nieprawdziwe: model udaje, że każde zdarzenie zaczyna z czystą kartą, a rzeczywistość ma pamięć. Nieudane wdrożenie zwiększa szansę na kolejne nieudane. Po nocnym incydencie zespół jest zmęczony, więc następna naprawa idzie wolniej - μ spada dokładnie wtedy, gdy najbardziej go potrzebujesz. Awarie chodzą stadami, bo jedna pociąga za sobą następne. Do tego czasy naprawy rzadko układają się w gładki, „podręcznikowy” rozkład. A rzadkie zdarzenia - te najważniejsze - są z natury trudne do oszacowania, bo masz ich w danych mało.

Wniosek nie jest „odrzuć model”, tylko „używaj go jak modelu”: to narzędzie do myślenia i do zadawania właściwych pytań, nie wyrocznia. Prosty łańcuch zdarzeń, którego szybkości realnie zmierzyłeś, powie Ci o Twojej niezawodności więcej niż najładniejszy slajd o dostępności oparty na średniej. A tam, gdzie założenia bolą - świadomie sięgasz po bogatszy opis.

Jak to spina EIAC

Cała ta teoria staje się operacyjna dopiero, gdy proces jest policzalny - a to właśnie robi everything-as-code:

  • Stany i przejścia jawne - skoro infrastruktura, wdrożenia i polityki są kodem, cykl życia usługi (wdrożenie, degradacja, wycofanie, przywrócenie) jest zdefiniowany wprost, nie domyślany. Masz zbiór możliwych stanów za darmo.
  • Szybkości jako dane - obserwowalność i metryki dostarczania dają zmierzone λ i μ. Model karmisz faktami, nie zgadywaniem.
  • Własności jako kod - cel poziomu usług (SLO) zapisujesz jako sprawdzalną własność, a guardrails (policy-as-code) trzymają układ w tych stanach, które uznałeś za bezpieczne.

To domyka wątek, który ciągnie się przez całą serię: przewagą nie jest heroizm przy incydencie, lecz system produkcyjny, który zamienia losowość w policzalne ryzyko. Ten sposób myślenia daje temu język.

Podsumowanie

Wydawanie i utrzymanie aplikacji to nie ciąg deterministycznych kroków, lecz proces losowy - układ dyskretnych zdarzeń o mierzalnych szybkościach, czyli łańcuch Markowa. Deterministyczna średnia (dostępność „zwykle działa”) ukrywa ogon, który generuje koszt; język rozkładów prawdopodobieństwa go odsłania. Metryki DORA to zmierzone szybkości tego łańcucha, dostępność to jego stan ustalony (i rośnie szybciej przez skracanie naprawy μ niż przez ściganie awarii λ), symulacja pozwala przećwiczyć okno wdrożeniowe, a weryfikacja modelowa zamienia cel poziomu usług z życzenia w sprawdzalną własność. Model ma granice - założenie braku pamięci kłamie - ale prosty, zmierzony model bije przeczucie. A everything-as-code jest tym, co czyni proces policzalnym: podmieniasz „molekułę jako proces” na „usługę jako proces” i cały ten sposób myślenia rusza. Weź jeden swój pipeline, wypisz jego stany i zmierz dwie szybkości - jak często się psuje i jak szybko wraca do zdrowia. Reszta, jeśli zechcesz po nią sięgnąć, to już matematyka w opcjonalnych ramkach powyżej. :)

Inspiracja: Mikołaj Rybiński, „Stochastyczne modele komórkowych szlaków sygnałowych”, praca magisterska, Uniwersytet Warszawski, 2006.