Sprawa bazy, która wyglądała na zajętą

Krótka historia detektywistyczna o martwym serwerze, który nie przestawał mówić, wykresie, który mówił prawdę, tylko źle, i wiadomości, której nikt nie dostał.

 ┌────────────────────────────────────────────────────────────┐
 │                     TABLICA ŚLEDCZA                        │
 ├────────────────────────────────────────────────────────────┤
 │                                                            │
 │   [ usunąć stary klaster? NIEODWRACALNE ]                  │
 │                  │                                         │
 │        ┌─────────┴─────────┐                               │
 │        ▼                   ▼                               │
 │  ┌───────────┐       ┌───────────┐                         │
 │  │   DOWÓD   │       │   DOWÓD   │                         │
 │  │  dzienny: │       │ godzinowy:│                         │
 │  │ „nadal    │       │ „martwy   │                         │
 │  │  zajęty”  │       │   od      │                         │
 │  │           │       │   08:00”  │                         │
 │  └─────┬─────┘       └─────┬─────┘                         │
 │        │ te same dane      │ te same dane                  │
 │        └─────────┬─────────┘                               │
 │                  ▼                                         │
 │        ┌───────────────────────┐                           │
 │        │  JEDEN Z NICH MIAŁ    │                           │
 │        │  ZDECYDOWAĆ O USUNIĘ- │                           │
 │        │  CIU NA PRODUKCJI     │                           │
 │        └───────────────────────┘                           │
 └────────────────────────────────────────────────────────────┘

Zgłoszenie

Zaczęło się od najnudniejszego rodzaju zgłoszenia: ustalenia zgodności.

Firma uruchamiała klaster wyszukiwania katalogu produktów — zarządzaną domenę AWS OpenSearch — a skaner bezpieczeństwa dostawcy chmury oznaczył go jako Critical z jednolinijkową skargą:

OpenSearch domains should not be publicly accessible.

Klaster miał publiczny endpoint. Był odgrodzony polityką ograniczenia IP, więc w praktyce nic niechcianego nie mogło do niego dotrzeć, ale ta kontrola nie ocenia z taryfą ulgową: endpoint istniał w internecie, więc nie przeszedł.

Poprawka jest dobrze przetarta i lekko irytująca. AWS nie przeniesie działającej domeny do VPC w miejscu — API po prostu odmawia. Stawiasz obok drugi klaster, tylko w VPC, migrujesz, kierujesz aplikację na nowy, a potem usuwasz stary.

Ta ostatnia część zdania to cała historia. Usuwasz stary. Trzy środowiska — development, staging, produkcja — każde z własnym działającym klastrem, każde z prawdziwymi indeksami, każde usunięcie nieodwracalne i każde zabierające ze sobą wszystkie indeksy w środku.

Wszystko przed tym krokiem da się odwrócić. Ten krok to urwisko.

Rozdział 1 — Cicha dobra decyzja

Zanim padły jakiekolwiek ciekawe błędy, ktoś podjął decyzję wartą skopiowania.

Jedna baza kodu Terraforma obsługiwała wszystkie trzy środowiska. Oczywistym sposobem na wycofanie starego klastra jest usunięcie jego bloku modułu z kodu. Zrób to, a następny apply w każdym środowisku zniszczy jego klaster — łącznie z dwoma, które jeszcze nie zmigrowały.

Zamiast więc usuwać blok, postawiono przed nim przełącznik:

# was: count = var.enable_search ? 1 : 0
count = var.enable_search && var.enable_search_legacy ? 1 : 0

Teraz wycofanie klastra to jedna linia w pliku konfiguracji jednego środowiska. Każde środowisko idzie we własnym tempie.

Potem przyszła część, którą łatwo zrobić na odwrót. Jaka powinna być domyślna wartość tego przełącznika?

Konwencja obowiązująca w repozytoriach Firmy była stanowcza: flagi funkcji domyślnie mają false, opt-in, bez niespodzianek. Każda inna flaga w pliku jej przestrzegała.

Ta miała domyślnie true, celowo, wbrew konwencji — bo te dwa kierunki zawodzą bardzo różnie:

  DOMYŚLNIE             KTOŚ ZAPOMNI USTAWIĆ   WYNIK
  ──────────────────────────────────────────────────────────────────
  false  (konwencja)    wychodzi false         działający klaster
                                               produkcyjny zostaje
                                               PO CICHU ZNISZCZONY
  ──────────────────────────────────────────────────────────────────
  true   (wybrane)      wychodzi true          klaster zostaje;
                                               wycofanie po prostu
                                               jeszcze nie następuje

Zwykłe flagi decydują, czy coś zostanie utworzone, i false to nieszkodliwa wartość domyślna. Ta decydowała, czy coś zostanie zniszczone, więc nieszkodliwa wartość domyślna odwróciła się razem z nią.

Zasada pod spodem: domyślną wartością flagi logicznej powinna być ta, która nic nie robi, kiedy ktoś o niej zapomni. Spójność to dobry cel aż do chwili, gdy zaczyna coś usuwać. Uzasadnienie wpisano w opis zmiennej, żeby następna osoba, która przyjdzie „naprawić niespójność”, zastała czekający na nią argument.

Development poszedł pierwszy. Jedna linia, jeden apply, klaster zniknął. Potem staging. Oba bez wydarzeń.

Potem produkcja.

Rozdział 2 — Trup z pulsem

Zanim przełączono przełącznik na produkcji, rozsądny ruch: sprawdzić, czy nic nadal nie używa rzeczy, którą zaraz usuniesz. Runbook migracji mówił, że produkcję przełączono kilka dni wcześniej. Ale runbook mówiący, że środowisko zmigrowało, to twierdzenie, a koszt błędu w tym twierdzeniu to awaria produkcji, a nie niedogodność.

Więc: pobrać metryki ruchu dla obu klastrów, starego i nowego, i potwierdzić, że stary jest martwy.

Nie był martwy. W ciągu poprzedniego tygodnia klaster, który miał zostać wycofany, obsługiwał dziennie więcej ruchu wyszukiwania niż jego następca.

Czytane wprost, to znak stop. Nie usuwa się bardziej zajętej z dwóch baz danych.

I to jest punkt, w którym historia mogła źle się skończyć w obu kierunkach. Uwierz liczbie, a wycofanie utknie bezterminowo na klastrze, który faktycznie jest martwy. Zignoruj liczbę, bo runbook mówi inaczej, a usuwasz działającą bazę produkcyjną na wyczucie.

Liczba nie była zła. Była czytana w złej rozdzielczości.

Rozdział 3 — Przybliżenie

Wartości dzienne były dziennymi sumami — jedna liczba na klaster na dzień. Te same dane pobrane godzinowo opowiadały zupełnie inną historię.

   STARY KLASTER (na godzinę)          NOWY KLASTER (na godzinę)

   przed przełączeniem:                przed przełączeniem:
   26 . 26 . 26 . 26   (noc)           2 . 2 . 2 . 2 . 2 . 2
   60 . 77 . 95 .140   (dzień pracy)   2 . 2 . 2 . 2 . 2 . 2
  108 . 62 . 26 . 26   (wieczór)       2 . 2 . 2 . 2 . 2 . 2
        ▲ dzień w ludzkim kształcie          ▲ nikogo w domu

   ──────────── 08:00, przełączenie ─────────────

   potem:                              potem:
   26 . 26 . 26 . 26                  54 . 47 . 27 .126
   26 . 26 . 26 . 26                  28 . 63 . 19 . 23
   26 . 26 . 26 . 26                   2 .  2 . 33 .  2
        ▲ płasko. cały dzień. całą noc.     ▲ dzień w ludzkim kształcie

I jest. O jednej konkretnej godzinie oba klastry zamieniły się kształtami.

Dobowy rytm starego klastra — cicho w nocy, ruch przez dzień pracy, wygasanie wieczorem — podniósł się i przeniósł do nowego klastra. Zostawiona została płaska linia: dokładnie ta sama liczba, co godzinę, przez popołudnie, przez noc, do następnego ranka.

To nie jest użycie. Nic, co robi człowiek, nie jest tak regularne. Krzywa dnia pracy to ludzie; płaska linia to maszyna rozmawiająca sama ze sobą.

Sumy dzienne całkowicie to ukryły, bo suma dzienna dodaje płaską linię i ludzką krzywą i daje jedną liczbę. Płaska linia starego klastra biegła przez wszystkie dwadzieścia cztery godziny; prawdziwy ruch nowego klastra odbywał się tylko w godzinach pracy. Zsumuj oba z całego dnia, a trup wychodzi na prowadzenie.

Porównuj kształt, a nie sumę.

Rozdział 4 — Bezczynność to nie zero

Co rodzi oczywiste pytanie: dlaczego klaster bez użytkowników w ogóle cokolwiek robi?

Bo zarządzane magazyny danych nigdy nie są naprawdę ciche. Wewnętrzne indeksy systemowe, health checki, porządki, które usługa robi we własnym imieniu — wszystko to rejestruje się jako ruch na dokładnie tych samych metrykach, których używa twoja aplikacja. Nie ma flagi oddzielającej „to zrobił mój kod” od „usługa odetchnęła”.

Zaskoczeniem była skala. Sprawdzenie już wycofanych środowisk dało coś, co wyglądało na literówkę:

  operacje indeksowania dziennie, po przełączeniu, bez użytkowników:

    środowisko development ........ 92
    środowisko staging ............ 92
                                    ▲
                    dwa różne konta AWS.
                    dwa różne klastry.
                    identycznie co do cyfry.

Dziewięćdziesiąt dwie dziennie, w obu, na zawsze. Tymczasem podłoga po stronie wyszukiwania wynosiła około 26 na godzinę — i ta liczba pojawiła się znowu na produkcji, na trzecim koncie.

To nie są zbiegi okoliczności, to tętno spoczynkowe usługi. Co po cichu burzy najbardziej naturalną zasadę, po jaką ktokolwiek by tu sięgnął:

„Jeśli metryka nie jest zerowa, coś nadal tego używa.”

Ta zasada zablokowałaby wszystkie trzy wycofania. Na zawsze. Metryka nigdy nie będzie zerowa, bo klaster nigdy nie przestanie oddychać, dopóki go nie zabijesz.

Zanim użyjesz metryki do udowodnienia nieobecności, sprawdź, co ta metryka pokazuje, kiedy niczego tam nie ma.

Rozdział 5 — Grupa kontrolna, którą już masz

Nawet wiedząc, że płaska linia to poziom bazowy, jedno pytanie pozostało niewygodne: czy ta konkretna płaska linia jest na tyle mała, żeby było bezpiecznie?

Tak postawione nie ma dobrej odpowiedzi. Czy 58 dziennie to za dużo? 634? W porównaniu z czym? Można się przekonać do każdego werdyktu i żaden nie przetrwa recenzenta pytającego „skąd wiesz?”.

Wyjściem było przestać pytać w kategoriach bezwzględnych.

Klaster development został już zbadany, uznany za bezpieczny, usunięty i nikt za nim nie tęsknił. Ten werdykt zapadł, został przyjęty i dowiedziony. Pytanie można więc było zadać ponownie jako porównanie: czy resztkowa podłoga stagingu jest większa czy mniejsza niż ta już zatwierdzona?

Mniejsza. Wyraźnie. Co znaczyło, że staging był bezpieczny według standardu, na który zespół już się zgodził i z którym już żył — bez potrzeby nowej oceny.

To niedoceniany bonus wdrażania czegokolwiek środowisko po środowisku. Wszyscy robią stopniowe wdrożenia, żeby ograniczyć zasięg rażenia. Ale środowisko, które poszło pierwsze, staje się też grupą kontrolną — przerobionym przykładem „to było w porządku”, dostępnym za darmo, który zamienia pytania bezwzględne bez odpowiedzi w względne, na które da się odpowiedzieć.

Produkcja przeszła to samo i zdała tak samo.

Przełącznik przełączono. Plan wyszedł dokładnie zgodnie z przewidywaniem: jeden klaster zniszczony, dwie polityki dostępu przycięte o uprawnienie wskazujące na coś, co przestanie istnieć, i — twierdzenie, które naprawdę miało znaczenie — zero zmian w następcy. Przy rozbiórce znaczącą kontrolą bezpieczeństwa nie jest to, co plan robi. Jest nią to, czego plan nie dotyka.

Rozdział 6 — Wiadomość, której nikt nie dostał

Gdy usuwanie trwało, zostało tylko opisać to w zgłoszeniu: co zweryfikowano, co pokazały dowody i jedno naprawdę otwarte pytanie do zespołu — dane produkcyjne nie są jednorazowe, więc czy robimy snapshot przed usunięciem, czy stare indeksy to naprawdę martwy balast?

To pytanie musiały zobaczyć dwie osoby. Obie zostały oznaczone, z imienia, w komentarzu.

Komentarz się opublikował. Renderował się pięknie — nagłówki, tabele, bloki kodu, dwa nazwiska w znajomym podświetlonym stylu, który znaczy zostałeś wspomniany.

Tyle że nie zostali.

Narzędzie użyte do opublikowania komentarza potrafiło idealnie zapisać format rich-text trackera dla wszystkiego poza wzmiankami. Odczytanie istniejącego komentarza pokazało wzmianki w czymś, co wyglądało na składnię wielokrotnego użytku — więc ta sama składnia poszła z powrotem. Ale identyfikator w środku wcale nie był ID konta użytkownika; był pozycyjnym placeholderem, artefaktem tego, jak tracker przetłumaczył własne dane przy wyjściu. Odesłanie go z powrotem zapisało zwykły tekst przebrany za wzmiankę.

Bez błędu. Bez ostrzeżenia. Dla osoby, która go napisała, komentarz wyglądał dokładnie jak komentarz, który powiadomił dwóch kolegów. Jedynym sposobem, żeby przekonać się, że jest inaczej, było odczytanie zapisanego komentarza w surowej ustrukturyzowanej formie i sprawdzenie, czy obiekt jest wzmianką, czy tylko tekstem.

Był tekstem.

I zauważ, co to jest. To ta sama porażka co wykres w Rozdziale 2, tylko w innym kapeluszu: coś, co wygląda na sygnał, a nim nie jest. Metryka, która czyta się jak użycie, a nim nie jest. Wzmianka, która czyta się jak doręczona, a nie jest.

Wersja na pół naprawiona jest przy tym gorsza od zepsutej. Brakująca wzmianka jest widoczna — ktoś zauważy, że pytanie nigdzie nie dotarło, i zapyta ponownie. Fałszywa wzmianka jest niewidoczna: komentarz czyta się tak, jakby właściwe osoby były w pętli, więc nikt tego nie goni, a otwarte pytanie o usunięcie danych produkcyjnych po cichu czeka bez odpowiedzi aż do dnia usunięcia.

Poprawka była mało efektowna: wpisać nazwiska jako zwykły tekst i oznaczyć je ręcznie.

Podsumowanie — Wskazówki dla kolegów śledczych

  1. Niech flaga destrukcyjna ma domyślnie bezpieczną wartość, nawet wbrew konwencji. Flaga decydująca o tworzeniu jest bezpieczna z domyślnym off. Flaga decydująca o niszczeniu nie jest. Wybierz wartość domyślną, która nic nie robi, kiedy ktoś zapomni — i zapisz dlaczego, inaczej ktoś później ją „naprawi” dla spójności.
  2. Nigdy nie pozwól, żeby suma dzienna o czymkolwiek decydowała. Agregacja sumuje poziom bazowy i prawdziwy sygnał w jedną liczbę i z pełną pewnością podaje złą odpowiedź. Przybliżaj, aż zobaczysz dobowy rytm.
  3. Porównuj kształt, a nie sumę. Płasko przez całą dobę to maszyna. Krzywa podążająca za godzinami pracy to ludzie. Spadek w sobotę to najbardziej przekonujący dowód, jaki dostaniesz, bo nic syntetycznego nie wie, że jest weekend.
  4. Sprawdź, co twoja metryka pokazuje, kiedy niczego tam nie ma. Bezczynność to nie zero. Usługi zarządzane mówią dalej po odejściu ostatniego klienta, a „niezerowe znaczy używane” zablokuje każde wycofanie, jakiego kiedykolwiek spróbujesz.
  5. Środowisko, które poszło pierwsze, to grupa kontrolna — korzystaj z niej. „Czy ta liczba jest wystarczająco mała?” nie ma odpowiedzi, której da się bronić. „Czy jest mniejsza niż ta, którą już zatwierdziliśmy i usunęliśmy?” ma. Stopniowe wdrożenia dają ci to za darmo, a prawie nikt z tego nie korzysta.
  6. Przy rozbiórce kontrolą bezpieczeństwa jest to, czego plan nie dotyka. Jedno zniszczenie łatwo ocenić okiem. Zerowy diff w następcy to twierdzenie, które naprawdę cię chroni.
  7. Runbook mówiący „to środowisko zmigrowano” to twierdzenie, a nie dowód. Weryfikuj dla każdego środowiska, a nie tylko przed pierwszym. Liczy się ta kontrola, którą najchętniej byś pominął, bo poprzednie dwie były w porządku.
  8. Weryfikuj doręczenie, a nie wygląd. Czy to metryka, adnotacja w konfiguracji, czy wiadomość do kolegi — tryb awarii, który cię kosztuje, nigdy nie jest tym, który wygląda na zepsuty. To ten, który wygląda idealnie.

   CO MÓWIŁ WYKRES DZIENNY                CO SIĘ FAKTYCZNIE DZIAŁO
  ┌────────────────────────────┐         ┌────────────────────────────┐
  │ stary klaster ... zajętszy │         │ stary klaster ... oddycha  │
  │ nowy klaster .... cichszy  │ ═══════►│ nowy klaster .. obsługuje  │
  │                            │         │                            │
  │ werdykt: NIE USUWAĆ        │         │ werdykt: można usunąć      │
  └────────────────────────────┘         └────────────────────────────┘
         te same dane, inna rozdzielczość ───────┘