
Денят, в който приложението ни направи DDoS само на себе си
Backlog от чакащи качвания, освободен наведнъж при стартиране, превърна всеки GeekBye клиент в малка denial-of-service атака срещу собствените ни сървъри. Фиксът — и стълбата за connection-liveness, която ни принуди да построим — е едно от най-полезните неща, на които ни научи v2.
Всеки инженер на разпределени системи рано или късно среща thundering herd (втурналото се стадо): маса от клиенти, всички правещи едно и също нещо в един и същи миг, и споделеният ресурс зад тях се огъва. Обикновено са нечии чужди клиенти. В GeekBye v2.0.1 бяха нашите, а нападателят беше собственото ни приложение.
Как един notetaker напада сам себе си
GeekBye може да качва записи в твоя Google Drive. Ако един запис не може да се качи веднага — бил си офлайн, приложението се е затворило, Drive е засякъл — той влиза в backlog, за да се опита отново по-късно. Разумно.
Провалът беше в това кога се случваше това „по-късно". При следващото стартиране приложението се опитваше да изпразни целия backlog наведнъж. Един потребител със седмица чакащи записи ставаше залп от едновременни заявки за качване в мига, в който отвори приложението. Умножи по всеки потребител, който стартира сутринта, и от нашия backend се искаше да автентикира, да rate-провери и да обработи стена от трафик в едни и същи няколко секунди — от клиенти, които всички, технически, се държаха коректно.
Нашият backend направи правилното нещо и rate-лимитира потопа. И тук започна щетата от втори ред. Отговорът от rate-лимит е HTTP 429, а 429 доста прилича на други провали, ако не внимаваш:
- Profile fetch-ът при стартиране получи
429и приложението го третира като провалена автентикация — излогвайки за кратко потребители от напълно валидна сесия. - Връзката за транскрипция получи общ
429и показа подкана за upgrade заради достигнат аудио лимит — казвайки на платещи потребители, че са ударили квота, която не са.
Така една първопричина — неритмуван backlog — произведе три видими симптома: натоварване на сървъра, фалшиви излогвания и фалшиви upsell-и. Класическата форма на self-DoS: натоварването е лошо, но погрешното тълкуване на симптомите на натоварването е това, което потребителите всъщност усещат.
Фиксът, в два слоя
Спри блъсканицата. Backlog-ът за качвания сега е ритмуван — заявките са разпределени във времето с backoff, плюс cooldown след всеки 429, така че клиентът се отдръпва от натоварен сървър, вместо да се опира по-силно на него. Един thundering herd става подредена опашка.
Спри погрешното тълкуване. Преходен 429 или 5xx по време на стартиране вече не те излогва — клиентът различава „сървърът е зает за кратко" от „сесията ти е невалидна". А общ 429 от rate-лимит по пътя на транскрипцията вече не се маскира като грешка за аудио квота; само истински отговор за квота показва upgrade подканата. Урокът, който остана: код на грешка не е значение на грешка. 429 означава „забави", не „ти си неоторизиран" и не „свърши ти квотата" — и клиентът трябва да знае разликата.
Стълбата за liveness, която последва
Ритмуването на стадото извади наяве по-тих проблем: когато една връзка наистина се разваляше под натоварване, колко бързо забелязвахме? По-бавно, отколкото искахме. Затова v2.0.4 изгради стълба за connection-liveness за транскрипция в реално време:
- Heartbeat-и — socketът за транскрипция се ping-ва на фиксиран интервал, така че безшумно мъртва връзка се засича за секунди, вместо да чака следващото парче аудио да се провали.
- Kick за повторно свързване при връщане на мрежата — когато операционната система докладва, че мрежата е обратно, приложението проактивно възстановява връзката, вместо да чака да се спъне в откритието.
- Liveness чрез control ping/pong — round-trip на ниво приложение, който потвърждава не просто „socketът е отворен", а „другият край наистина отговаря".
- Порта за повторно свързване, притежавана от услугата — едно място решава дали да се свърже отново, вместо няколко части на приложението да се надпреварват да го правят и да си пречат взаимно.
Нищо от това не е бляскаво. Всичко от него е причината една v2 сесия да усещаш все едно просто... си остава свързана.
Случи се пак тази седмица — едно ниво по-надолу
Ето частта, която прави това повече от военна история. Същият клас провал изникна отново преди дни, едно ниво под нас. Един-единствен клиент изстреля 21 старта на сесия за транскрипция за под 400 милисекунди — и задейства лимита на ниво акаунт на нашия speech доставчик от 15 конкурентни заявки. Блъсканица срещу споделена инфраструктура, точно като backlog-а за качвания, само насочена към зависимост вместо към собствения ни backend.
Формата е идентична, и фиксът също: щурмуващият клиент има нужда от single-flight guard, за да не може да изстрелва двайсет старта за едно намерение, а споделеният ресурс има нужда от таван на потребител, за да не може един клиент да изконсумира целия пул. Строили сме версията от страна на клиента на това и преди — pacer-ът за качвания е точно този модел. Сега го прилагаме към стартовете на сесия. Thundering herd не е бъг, който поправяш веднъж; той е форма, която се научаваш да разпознаваш навсякъде, където клиенти срещат споделен лимит.
Три неща за отнасяне
- Собствените ти клиенти са тест за натоварване, който не си планирал. Всичко, което групира-при-стартиране, опитва-отново-при-пускане или се свързва-отново-при-събуждане, е thundering herd, чакащ достатъчно потребители. Ритмувай го, преди да ги имаш.
- Различавай кода от значението.
429е най-погрешно разчитаният статус в съществуването. „Забави" не е „излез" и не е „плати ни". Насочвай всеки провал към това, което той наистина означава. - Liveness-ът е стълба, не флаг. „Жива ли е връзката?" има няколко честни отговора на различни слоеве — socketът е отворен, байтове текат, другият край отговаря, мрежата присъства. Едно устойчиво приложение проверява повече от един.
Това е неефектната машинария под спокойствието на GeekBye v2. За функциите за надеждност, които тя позволи, виж защо твоят AI notetaker спира при лош Wi-Fi и транскрипция на живо, когато защитната стена блокира WebSocket-ите (v2.0.8). За съседните издания в тази серия, защо твоят AI notetaker спира записа по средата на срещата (v2.0.9).