Sprawa poda, który umarł dokładnie zgodnie z planem

Krótka historia detektywistyczna o siatce bezpieczeństwa, która okazała się narzędziem zbrodni, łagodnym wyłączeniu, które wszystko pogorszyło, i egzekucji zapowiedzianej dwa dni wcześniej publiczności złożonej z nikogo.

 ┌──────────────────────────────────────────────────────────────┐
 │                      TABLICA ŚLEDCZA                         │
 ├──────────────────────────────────────────────────────────────┤
 │                                                              │
 │   [ demo na żywo pada w trakcie rozmowy, ok. 06:00 ]         │
 │                     │                                        │
 │        ┌────────────┴────────────┐                           │
 │        ▼                         ▼                           │
 │  ┌────────────┐           ┌────────────┐                     │
 │  │ PODEJRZANY │           │ PODEJRZANY │                     │
 │  │ „węzeł     │           │ „ktoś      │                     │
 │  │  osiągnął  │           │  wdrażał   │                     │
 │  │  max wiek” │           │  o 3 w n.” │                     │
 │  └─────┬──────┘           └─────┬──────┘                     │
 │        │ niewinny               │ niewinny                   │
 │        ▼                         ▼                           │
 │            ┌────────────────────────────┐                    │
 │            │    FAKTYCZNY SPRAWCA       │                    │
 │            │  „okres karencji”, który   │                    │
 │            │  dodaliśmy, żeby *chronić* │                    │
 │            │  właśnie to obciążenie     │                    │
 │            └────────────────────────────┘                    │
 │                                                              │
 │   dowody: 5585 wpisów w logu mówiących „NIE zrobiłem tego”   │
 │           ...a potem jeden mówiący „ok, zrobiłem”            │
 └──────────────────────────────────────────────────────────────┘

Zgłoszenie

Firma uruchamia na Kubernetesie usługę multimediów w czasie rzeczywistym — z tych, które przenoszą sesje wideo i audio na żywo między zdalnym sprzętem a przeglądarką. Pewnego ranka, w trakcie demo na żywo dla zewnętrznej publiczności, multimedia przestały płynąć.

Zgłoszenie, które trafiło do zespołu platformowego, nie brzmiało „usługa leży”. Było dziwniejsze:

Dwa urządzenia brzegowe straciły połączenie z usługą multimediów w tym samym momencie. Demo na produkcji jest zepsute. Wysoki priorytet.

Nic nie było w pętli restartów. Żaden pod się nie restartował. Dashboardy były zielone. Usługa działała, odpowiadała, była zdrowa — a multimedia nie płynęły.

To są te dobre przypadki. Martwy proces mówi ci, czym jest. Zdrowy proces, który nie działa, zmusza cię do faktycznego myślenia.

Rozdział 1 — Podejrzany z idealnym alibi

Pody multimedialne miały dobrze znaną adnotację. Każdy autoskaler ma jakąś jej wersję: znacznik mówiący nie przerywaj tego poda. Zespół umieścił ją tam celowo, z latami instynktu za sobą: to coś trzyma sesje na żywo, nie ruszaj tego.

Pierwsza teoria była więc oczywista. Adnotacja musiała zawieść. Ktoś musiał umieścić ją w złym miejscu w konfiguracji — ten zespół popełnił już taki błąd przy innej usłudze, gdzie znacznik był przypięty do obiektu deployment zamiast do poda, gdzie autoskaler nigdy nie zagląda. Renderował się, pokazywał w describe i przez cały czas życia usługi był tylko dekoracją.

Tanie do sprawdzenia. Przeczytaj adnotację z działającego poda, a nie z pliku konfiguracji.

Była. Poprawnie umieszczona. Na podzie.

Potem przyszło alibi. Strumień zdarzeń samego klastra zapisywał decyzje autoskalera, a dla tego jednego węzła wpisywał tę samą linię, raz za razem, mniej więcej co siedemnaście minut, przez jedenaście dni:

DisruptionBlocked: Pod has "do-not-disrupt" annotation (Pod=<the media service>)
   ...occurrence 5,585

Pięć i pół tysiąca razy autoskaler podchodził do tego węzła, patrzył na chronionego poda i odchodził. Adnotacja nie była zepsuta. Adnotacja była najdokładniej udowodnionym działającym kawałkiem konfiguracji w całym klastrze.

Co nasunęło dużo lepsze pytanie: co zabiło poda, który został skutecznie ochroniony 5585 razy?

Rozdział 2 — Przyznanie się, złożone dwa dni wcześniej

Tu najpierw się pomyliłem i warto to przyznać, bo to błąd, do którego zachęca dokumentacja.

Moją pierwszą teorią był wiek węzła. Ten autoskaler ma ustawienie maksymalnego czasu życia węzła — domyślnie 30 dni — a wygaśnięcie to akcja wymuszona: nie obchodzi jej znacznik do-not-disrupt. Pasuje. To podręcznikowa odpowiedź.

Sprawdziłem więc wiek każdego węzła w klastrze. Najstarszy węzeł zarządzany przez autoskaler miał jedenaście dni. Żaden nie był nawet blisko trzydziestu. Teoria umarła na starcie, zabita jednym zapytaniem.

Złe zakręty są tanie, kiedy je sprawdzasz. Są drogie, kiedy wpisujesz je do raportu z incydentu.

Z powrotem do strumienia zdarzeń. Węzła, na którym działał pod, już nie było — usunięty, nie do odzyskania, nie został żaden obiekt do obejrzenia. Ale jego zdarzenia przetrwały i opowiadały całą historię ze znacznikami czasu. Czytane po kolei:

Day 0, 06:16:32   DisruptionTerminating: Disrupting Node: Drifted
Day 0, 06:16:32   FailedDraining: Failed to drain node, 19 pods are waiting to be evicted
Day 0, 06:16:32   TerminationGracePeriodExpiring:
                     All pods will be deleted by  Day 2, 06:16:32

Przeczytaj jeszcze raz tę trzecią linię.

Dwa dni przed awarią demo klaster zapisał dokładną sekundę, w której zamierzał zabić poda. Nie ostrzeżenie. Nie ryzyko. Zaplanowaną egzekucję, ze znacznikiem czasu, w strumieniu, którego nikt nie oglądał. Między tą linią a awarią ten sam węzeł wyemitował jeszcze 517 zdarzeń „failed to drain”. Na żadne z nich nie odpalił alert.

A powód był wprost w pierwszej linii: Drifted.

Rozdział 3 — Siatka bezpieczeństwa była pułapką

Drift oznacza, że węzeł nie zgadza się już z tym, czym według konfiguracji powinien być. Tutaj pula węzłów była skonfigurowana tak, żeby zawsze używać najnowszego bazowego obrazu systemu, więc za każdym razem, gdy dostawca chmury publikował nowy, każdy węzeł w puli natychmiast stawał się „zły” i kwalifikował do wymiany. Rutyna. Wręcz zdrowa — tak trafiają poprawki bezpieczeństwa.

Ale drift ma być operacją uprzejmą. Ma respektować znacznik do-not-disrupt. To cały sens znacznika.

Dokumentacja, czytana powoli, mówi coś bardziej precyzyjnego. Znacznik wyklucza poda z konsolidacji bezwarunkowo, a z driftu warunkowo. A oto warunek, dosłownie:

Configuring terminationGracePeriod affects disruption eligibility for Drift. When set, a node can be disrupted via drift even if pods have blocking disruption budgets or the do-not-disrupt annotation. (Ustawienie terminationGracePeriod wpływa na kwalifikację do zakłóceń przez Drift. Gdy jest ustawione, węzeł może zostać zakłócony przez drift, nawet jeśli pody mają blokujące budżety zakłóceń albo adnotację do-not-disrupt.)

Pula węzłów miała terminationGracePeriod równe 48 godzinom.

Ktoś dodał to ustawienie z najżyczliwszego możliwego powodu. Czyta się je dokładnie jak siatkę bezpieczeństwa: daj podom do dwóch dni na dokończenie tego, co robią, zanim je zabijesz. Kto nie chciałby tego dla usługi trzymającej rozmowy wideo na żywo?

W rzeczywistości robi dwie rzeczy i obie usuwają ochronę:

  1. Sprawia, że drift całkowicie ignoruje znacznik do-not-disrupt.
  2. Zamienia „ten drain jest zablokowany” na „ten drain ma termin”, po którym pody są usuwane niezależnie od jakiejkolwiek ochrony.

Ustawienie, które wyglądało jak pas bezpieczeństwa, było tym, co go odpinało. Przez jedenaście dni znacznik powstrzymywał konsolidację — ten drugi rodzaj zakłócenia, który blokuje bezwarunkowo — i ta nieprzerwana seria 5585 sukcesów to dokładnie to, co sprawiło, że wszyscy mu ufali.

Nic nie kłamie tak przekonująco jak zabezpieczenie, które przez tygodnie działa przeciwko niewłaściwemu zagrożeniu.

Rozdział 4 — Pięciogodzinny duch

W osi czasu jest jeszcze dziura. Egzekucję zaplanowano na Dzień 2, 06:16. Demo padło około 06:00. Blisko — ale pod zastępczy faktycznie wystartował pięć godzin wcześniej, o 01:16.

Dlaczego pod miałby zostać usunięty pięć godzin przed czasem?

Przez własny okres karencji. Autoskaler jest w tym staranny: jeśli pod prosi o czas na czyste wyłączenie, usuwanie zaczyna się na tyle wcześnie, żeby pod dostał pełny przydział przed twardym terminem. Więc:

   usunięcie poda  =  termin węzła  −  okres karencji poda
       01:16       =     06:16      −         5 godzin

Chart usługi multimediów dostarczał domyślny okres karencji równy 18 000 sekund — pięć godzin. Zapewne po to, żeby długa rozmowa mogła się spokojnie skończyć.

A oto drugi zwrot, ten, który zamienił restart poda w awarię.

W chwili, gdy pod zostaje oznaczony do usunięcia, Kubernetes usuwa go z listy zdrowych endpointów usługi. To poprawne zachowanie: przestań wysyłać nowy ruch, pozwól dokończyć. Ale do tej usługi multimediów nie dociera się zwykłą siecią klastra. Na brzegu siedzi bramka przekaźnikowa, która przekazuje multimedia bezpośrednio na adresy konkretnych podów — a poznaje te adresy właśnie z tej listy endpointów.

Więc o 01:16 pod zniknął z listy endpointów, a przekaźnik od razu przestał chcieć przenosić do niego multimedia. Tymczasem pod dalej działał. Zachował istniejące sesje. Współdzielony rejestr sesji dalej kierował do niego uczestników, bo z punktu widzenia aplikacji nadal był właścicielem tych pokojów.

   01:16 ──────────────────────────────────────────► 06:16
     │                                                 │
     │   pod: DZIAŁA, posiada sesje na żywo            │
     │   przekaźnik: „nie znam tego adresu”            │
     │   wynik: sesje istnieją i nie przenoszą mediów  │
     │                                                 │
     └──── pięć godzin żywy, ale nieosiągalny ─────────┘
                          ▲
                   tu było demo

Pięć godzin usługi, która działała, była zdrowa, trzymała prawdziwe sesje i strukturalnie nie mogła przenieść ani jednego pakietu multimediów. Okres karencji, który miał chronić trwające rozmowy, nie uratował żadnej. Wydłużył zepsuty stan z sekund do godzin i postawił demo prosto w tym oknie.

Łagodne wyłączenie jest łagodne tylko wtedy, gdy wszystko, co do ciebie kieruje ruch, zgadza się, że nadal istniejesz.

Rozdział 5 — Poprawka, która zrobiłaby to znowu

Poprawka wyglądała na łatwą: zdjąć terminationGracePeriod z puli węzłów, dać usłudze multimediów własne dedykowane węzły i skończyć z całą klasą problemu.

Jeszcze jeden szczegół prawie zamienił naprawę w powtórkę występu.

Zmiana szablonu węzłów puli — czasu życia, okresu karencji, typów maszyn, czegokolwiek — wprowadza w drift każdy węzeł już działający w tej puli. Dokumentacja mówi to wprost. A istniejące węzły zachowują własną kopię starego okresu karencji, niezmienną, przez cały czas życia.

Złóż te dwa fakty. Zmiana okresu karencji, żeby zatrzymać eksmisje przez drift, oznaczyłaby wszystkie dziesięć produkcyjnych węzłów jako dryfujące — podczas gdy wciąż rządziło nimi to samo ustawienie, które pozwala driftowi ominąć ochronę. Poprawka odtworzyłaby incydent na każdym węźle w klastrze, z 48-godzinnym lontem.

Wyjściem jest kolejność. Niektóre części tej konfiguracji to tożsamość węzła, a ich dotknięcie wymienia maszyny. Inne — same reguły zakłóceń — to czysta polityka i nic nie zmieniają w węzłach. Więc:

  1. Najpierw zamroź drift budżetem, który pozwala na zero wymian przez drift. To polityka, nie tożsamość: nic nie dryfuje, działa natychmiast.
  2. Utwórz nową pulę. Zupełnie nowa pula niczego nie wprowadza w drift.
  3. Przenieś obciążenie celowo, o wybranej godzinie, z kimś, kto patrzy.
  4. Potem edytuj starą pulę i odmroź drift w oknie serwisowym.

W części poprawki dotyczącej izolacji czaiła się jeszcze jedna pułapka. Dedykowana pula węzłów brzmi, jakby dedykowała węzły. Nie dedykuje — to szablon opisujący maszyny, które autoskaler może utworzyć. Nie ma żadnego zdania na temat tego, które pody tam lądują. Prawdziwa izolacja wymaga dwóch dopasowanych par: etykiety i taintu na puli oraz selektora i tolerancji na podzie. Pomiń selektor, a obciążenie radośnie zostanie na współdzielonych węzłach, a lśniąca nowa pula będzie stała pusta, wyglądając, jakby działała. Pomiń taint, a wprowadzi się tam wszystko inne.

I mała poezja w zgliszczach: zamrożenie driftu oznacza, że te węzły przestają dostawać poprawki systemu. Poprawką na „nasze węzły są wymieniane, kiedy tego nie chcemy” jest, tymczasowo, „nasze węzły nigdy nie są wymieniane”. To musi być zaplanowane okno, a nie stan stały, inaczej za pół roku to będzie następna historia.

Wskazówki dla czytelnika