
Dzień, w którym nasza aplikacja zDDoSowała samą siebie
Zaległość oczekujących uploadów, wypuszczona naraz przy starcie, zamieniła każdego klienta GeekBye w mały atak denial-of-service na nasze własne serwery. Naprawa — i drabina liveness połączenia, którą wymusiła — to jedna z najbardziej przydatnych rzeczy, jakich nauczyła nas v2.
Każdy inżynier systemów rozproszonych prędzej czy później spotyka thundering herd (stado spłoszonych klientów): masę klientów robiących to samo w tej samej chwili, a współdzielony zasób za nimi ugina się pod naporem. Zwykle to czyjeś klienci. W GeekBye v2.0.1 to byli nasi, a atakującym była nasza własna aplikacja.
Jak notatnik atakuje sam siebie
GeekBye potrafi wgrywać nagrania na Twój Google Drive. Jeśli nagranie nie może wgrać się od razu — byłeś offline, aplikacja się zamknęła, Drive się zaciął — trafia do zaległości, by ponowić próbę później. Rozsądne.
Awaria tkwiła w tym, kiedy nadchodziło to „później". Przy następnym uruchomieniu aplikacja próbowała opróżnić całą zaległość naraz. Jeden użytkownik z tygodniem oczekujących nagrań stawał się serią jednoczesnych żądań wgrania w chwili, gdy otwierał aplikację. Pomnóż to przez każdego użytkownika uruchamiającego aplikację rano, a nasz backend był proszony o uwierzytelnienie, sprawdzenie limitów i przetworzenie ściany ruchu w tych samych kilku sekundach — od klientów, którzy wszyscy, technicznie rzecz biorąc, zachowywali się poprawnie.
Nasz backend zrobił to, co należy, i ograniczył ten zalew (rate-limit). I tu zaczęła się szkoda drugiego rzędu. Odpowiedź o przekroczeniu limitu to HTTP 429, a 429 wygląda bardzo podobnie do innych awarii, jeśli nie jesteś ostrożny:
- Startowe pobranie profilu dostało
429, a aplikacja potraktowała to jako nieudane uwierzytelnienie — na chwilę wylogowując użytkowników z całkowicie ważnej sesji. - Połączenie transkrypcji dostało generyczne
429i pokazało monit o upgrade z powodu wyczerpania limitu audio — mówiąc płacącym użytkownikom, że osiągnęli limit, którego nie osiągnęli.
Więc jedna główna przyczyna — nieopanowana zaległość — dała trzy widoczne objawy: obciążenie serwera, fałszywe wylogowania i fałszywe upselle. Klasyczny kształt self-DoS: obciążenie jest złe, ale to błędna interpretacja objawów obciążenia jest tym, co użytkownicy faktycznie odczuwają.
Naprawa, w dwóch warstwach
Zatrzymaj panikę stada. Zaległość wgrywania jest teraz rozłożona w czasie — żądania rozproszone z backoffem, a po każdym 429 następuje cooldown, żeby klient wycofał się od obciążonego serwera, zamiast napierać jeszcze mocniej. Thundering herd staje się uporządkowaną kolejką.
Zatrzymaj błędną interpretację. Przejściowe 429 lub 5xx podczas startu już cię nie wylogowuje — klient odróżnia „serwer jest chwilowo zajęty" od „twoja sesja jest nieważna". A generyczne 429 o przekroczeniu limitu na ścieżce transkrypcji już nie udaje błędu wyczerpania limitu audio; tylko prawdziwa odpowiedź o limicie pokazuje monit o upgrade. Lekcja, która zapadła w pamięć: kod błędu to nie znaczenie błędu. 429 znaczy „zwolnij", a nie „jesteś nieautoryzowany" i nie „wyczerpałeś limit" — a klient musi znać tę różnicę.
Drabina liveness, która potem powstała
Opanowanie stada odsłoniło cichszy problem: gdy połączenie rzeczywiście padało pod obciążeniem, jak szybko to zauważaliśmy? Wolniej, niż byśmy chcieli. Więc v2.0.4 zbudował drabinę liveness połączenia dla transkrypcji w czasie rzeczywistym:
- Heartbeaty — gniazdo transkrypcji jest pingowane w stałym interwale, więc po cichu martwe połączenie jest wykrywane w sekundy, zamiast czekać, aż zawiedzie kolejny fragment audio.
- Kopniak ponownego łączenia przy powrocie sieci — gdy system operacyjny zgłasza, że sieć wróciła, aplikacja proaktywnie odtwarza połączenie, zamiast czekać, aż natknie się na to odkrycie.
- Liveness na poziomie ping/pong kontrolnego — round-trip na poziomie aplikacji, który potwierdza nie tylko „gniazdo jest otwarte", ale „druga strona faktycznie odpowiada".
- Bramka ponownego łączenia należąca do serwisu — jedno miejsce decyduje, czy się łączyć ponownie, zamiast kilku części aplikacji ścigających się, by to zrobić, i wchodzących sobie w drogę.
Nic z tego nie jest efektowne. Wszystko to jest powodem, dla którego sesja w v2 sprawia wrażenie, że po prostu... zostaje połączona.
Zdarzyło się znowu w tym tygodniu — piętro niżej
Oto część, która czyni z tego coś więcej niż opowieść wojenną. Ta sama klasa awarii wróciła kilka dni temu, o poziom niżej od nas. Pojedynczy klient wystrzelił 21 startów sesji transkrypcji w niecałe 400 milisekund — i przekroczył obowiązujący na całe konto limit naszego dostawcy mowy: 15 jednoczesnych żądań. Panika stada wobec współdzielonej infrastruktury, dokładnie jak zaległość wgrywania, tyle że wymierzona w zależność, a nie w nasz własny backend.
Kształt jest identyczny, i tak samo naprawa: napierający klient potrzebuje strażnika single-flight, żeby nie mógł wystrzelić dwudziestu startów dla jednej intencji, a współdzielony zasób potrzebuje limitu per-użytkownik, żeby jeden klient nie mógł pochłonąć całej puli. Zbudowaliśmy już wcześniej wersję tego po stronie klienta — pacer wgrywania jest tym wzorcem. Teraz stosujemy go do startów sesji. Thundering herd to nie błąd, który naprawiasz raz; to kształt, który uczysz się rozpoznawać wszędzie tam, gdzie klienci spotykają wspólny limit.
Trzy rzeczy do zapamiętania
- Twoi właśni klienci to test obciążeniowy, którego nie zaplanowałeś. Wszystko, co grupuje-przy-starcie, ponawia-przy-uruchomieniu albo łączy-się-po-wybudzeniu, to thundering herd czekający na wystarczającą liczbę użytkowników. Rozłóż to w czasie, zanim ich zdobędziesz.
- Odróżniaj kod od znaczenia.
429to najczęściej źle odczytywany status, jaki istnieje. „Zwolnij" to nie „wyloguj się" i nie „zapłać nam". Skieruj każdą awarię do tego, co naprawdę znaczy. - Liveness to drabina, a nie flaga. „Czy połączenie żyje?" ma kilka uczciwych odpowiedzi na różnych warstwach — gniazdo otwarte, bajty płyną, druga strona odpowiada, sieć obecna. Solidna aplikacja sprawdza więcej niż jedną.
To jest ta nieefektowna maszyneria pod spokojem GeekBye v2. O funkcjach niezawodności, które umożliwiła, przeczytaj dlaczego Twój notatnik AI zatrzymuje się przy słabym Wi-Fi oraz transkrypcja na żywo, gdy firewall blokuje WebSockety (v2.0.8). O sąsiednich wydaniach z tej serii — dlaczego Twój notatnik AI przerywa nagrywanie w trakcie spotkania (v2.0.9).