Alerty, które wysyłają się raz, a nie raz na serwer
Jeden z naszych alertów operacyjnych, „niskie kredyty” dla opublikowanej aplikacji twórcy, ma dotrzeć do nas co najwyżej raz dziennie. Pewnego popołudnia dotarł pięć razy. Przyczyna: każda kopia naszego API pamiętała w swojej pamięci, które alerty już wysłała, więc dwa serwery wysyłały go dwukrotnie, a każde wdrożenie czyściło tę pamięć i pozwalało wysłać go ponownie. Ten sam wzorzec powodował też, że zadanie w tle wykonywało swoją pracę dwa razy. Oba teraz koordynują się przez wspólny magazyn, a w razie jego niedostępności zawodzą po bezpiecznej stronie.
Objaw
Mythex wysyła zespołowi alerty, gdy coś wymaga uwagi: usługa nie działa, zadanie stanęło, albo saldo kredytów twórcy spada, a jego aplikacja jest opublikowana. Każdy alert ma okno deduplikacji, czyli czas, w którym ten sam alert wysyłany jest tylko raz. Dla alertu o niskich kredytach to 24 godziny.
Pięć kopii tego samego alertu w jedno popołudnie oznaczało, że okno nie działało. Co gorsza, przychodziły parami, mniej więcej 11 minut od siebie.
Dlaczego tak się działo
Pakiet alertów trzymał deduplikację w pamięci: tabelę „klucz alertu → kiedy ostatnio wysłano”, wewnątrz działającego procesu. To działa, gdy istnieje jeden proces, który nigdy się nie restartuje. Produkcja nie spełnia żadnego z tych warunków:
- Dwie repliki API. Produkcja uruchamia dwie kopie API. Każda miała własną tabelę, więc każda wysłała alert raz. To były te pary.
- Wdrożenia. Każde wdrożenie zastępuje obie repliki świeżymi procesami z pustymi tabelami. Po wdrożeniu alert wychodził ponownie, jakby nigdy nie został wysłany.
Odstęp ok. 11 minut między alertami w każdej parze wynikał z zadania, które podnosi ten alert. Działa na obu replikach i bierze blokadę, więc repliki przejmują ją naprzemiennie, a każda podnosiła alert, gdy przychodziła jej kolej.
W tym samym kodzie był drugi, cichszy błąd. Żeby tabela nie rosła w nieskończoność, czyściła się, gdy miała 500 kluczy, usuwając każdy wpis starszy niż 15 minut, bez względu na jego okno. Pod obciążeniem 24-godzinna deduplikacja mogła trwać kwadrans.
Poprawka
Jeden wspólny zapis na alert
Zanim API wyśle alert, rezerwuje teraz klucz deduplikacji w Redisie, wspólnym magazynie w pamięci, z którym rozmawiają obie repliki i który nie restartuje się, gdy restartuje się API. Rezerwacja to jedna operacja: „ustaw ten klucz tylko wtedy, gdy go nie ma, i wygaś po upływie okna”. Która replika zarezerwuje klucz pierwsza, wysyła alert, a druga widzi klucz zajęty i milczy. Nowy proces po wdrożeniu znajduje klucz nadal na miejscu.
Klucz i okno pochodzą z tej samej reguły, którą używa pakiet alertów, teraz współdzielonej przez obie warstwy, więc nie mogą się rozjechać co do tego, co uznaje się za „ten sam alert”.
Przy awarii wysyłaj, nie wstrzymuj
Najważniejsza decyzja dotyczyła tego, co robić, gdy Redis jest nieosiągalny. Wybraliśmy wysłanie alertu mimo to. Jeśli Redis nie działa albo nie odpowiada w ciągu 1,5 sekundy, alert idzie dalej. Zduplikowany alert to szum. Zgubiony alert oznacza, że ktoś nie został poinformowany.
Najpoważniejsze alerty, P0, w ogóle nie są wstrzymywane przez deduplikację.
Każdy wpis zachowuje własne okno
W deduplikacji w pamięci samego pakietu alertów każdy wpis zapisuje teraz, kiedy kończy się jego okno, a czyszczenie usuwa tylko wpisy, których okno już minęło. Klucz 24-godzinny istnieje 24 godziny, bez względu na to, ile innych alertów przychodzi.
Ten sam błąd, w innym miejscu
Ten sam wzorzec podwajał też część pracy w tle: zadanie uzgadniania, które utrzymuje nasz zapis statusu każdej przestrzeni roboczej zgodny z dostawcą piaskownic. Każde przejście listuje każdą piaskownicę na koncie (tysiące na produkcji, ok. pięciu sekund) oraz każdy uruchomiony lub uśpiony projekt. Obie repliki uruchamiały je co dwie minuty.
Jego zapisy są idempotentne, czyli wykonane dwa razy dają ten sam wynik co raz, więc nic się nie psuło. Po prostu wykonywano dwa razy więcej pracy. Teraz każde dwuminutowe okno jest rezerwowane w Redisie tak samo, jak nasze liczniki zużycia już rezerwowały swoje, i tylko replika, która je zarezerwuje, wykonuje przejście. Jeśli Redis jest nieosiągalny, przejście i tak się wykonuje: wykonanie pracy dwa razy jest nieszkodliwe, jej pominięcie już nie.
Jak to testowaliśmy
Testy odwzorowują prawdziwą konfigurację. Ładują moduł alertów dwa razy, jako dwie oddzielne „repliki”, wskazują obie na jeden wspólny magazyn, podnoszą z każdej ten sam alert, a potem ładują trzecią, świeżą kopię, by zastąpić „po wdrożeniu”, i podnoszą alert ponownie. Wysyłany jest dokładnie jeden alert. Inne testy sprawdzają, że alert innego twórcy nadal dociera, że P0 wysyłane jest z obu replik, i że alert jest wysyłany, gdy magazyn jest niedostępny albo przekracza limit czasu. Poprawka czyszczenia ma własny test: klucz 24-godzinny przeżywa 600 innych alertów, które przychodzą 20 minut później.
W przypadku zadania uzgadniania dwie repliki działające jednocześnie plus trzecie uruchomienie w tym samym oknie dają jedno listowanie.
Co z tego wynieśliśmy
- Stan w pamięci istnieje per proces. Wszystko, co musi być prawdą między serwerami, takie jak „wysłane raz” czy „wykonane raz”, musi żyć w miejscu, które serwery współdzielą.
- Wdrożenia to restarty. Stan, który przetrwa tylko do następnego restartu, nie przetrwa zwykłego dnia pracy.
- Zdecyduj, w którą stronę zawodzić. Dla alertów niedostępność wspólnego magazynu powinna oznaczać więcej alertów, a nie mniej.
- Szukaj wzorca, a nie tylko egzemplarza. Alert był głośnym przypadkiem. Podwojone zadanie uzgadniania było tym samym błędem, po cichu kosztującym pracę.
Nic z tego nie widać z poziomu Mythex. Więcej o tym, jak utrzymujemy platformę w stabilności, znajdziesz w tekstach jak doprowadziliśmy wdrożenia do zera błędów 502 i brakujący plik wymiany. Jeśli dopiero poznajesz ideę uruchamiania więcej niż jednej kopii usługi, nasz poradnik czym jest serwer wyjaśnia podstawy.