Steven
Steven5 мин четене

Денят, в който приложението ни направи DDoS само на себе си

Backlog от чакащи качвания, освободен наведнъж при стартиране, превърна всеки GeekBye клиент в малка denial-of-service атака срещу собствените ни сървъри. Фиксът — и стълбата за connection-liveness, която ни принуди да построим — е едно от най-полезните неща, на които ни научи v2.

Надеждност
Мрежи
Инженерство
GeekBye издания
Денят, в който приложението ни направи DDoS само на себе си

Всеки инженер на разпределени системи рано или късно среща 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 не е бъг, който поправяш веднъж; той е форма, която се научаваш да разпознаваш навсякъде, където клиенти срещат споделен лимит.

Три неща за отнасяне

  1. Собствените ти клиенти са тест за натоварване, който не си планирал. Всичко, което групира-при-стартиране, опитва-отново-при-пускане или се свързва-отново-при-събуждане, е thundering herd, чакащ достатъчно потребители. Ритмувай го, преди да ги имаш.
  2. Различавай кода от значението. 429 е най-погрешно разчитаният статус в съществуването. „Забави" не е „излез" и не е „плати ни". Насочвай всеки провал към това, което той наистина означава.
  3. Liveness-ът е стълба, не флаг. „Жива ли е връзката?" има няколко честни отговора на различни слоеве — socketът е отворен, байтове текат, другият край отговаря, мрежата присъства. Едно устойчиво приложение проверява повече от един.

Това е неефектната машинария под спокойствието на GeekBye v2. За функциите за надеждност, които тя позволи, виж защо твоят AI notetaker спира при лош Wi-Fi и транскрипция на живо, когато защитната стена блокира WebSocket-ите (v2.0.8). За съседните издания в тази серия, защо твоят AI notetaker спира записа по средата на срещата (v2.0.9).

Свързани статии

Транскрипция на живо, когато защитната стена блокира WebSocket-ите
Steven
Steven5 мин четене

Транскрипция на живо, когато защитната стена блокира WebSocket-ите

Корпоративните мрежи обожават да разрешават HTTPS и тихо да убиват WebSocket upgrade-ите. Това счупва безшумно транскрипцията в реално време. GeekBye v2.0.8 автоматично преминава към чист HTTPS транспорт — а пускането му разкри бъг, който щеше да направи цялата функция безполезна.

Транскрипция
Мрежи
Инженерство
Защо твоят AI notetaker спира записа по средата на срещата
Steven
Steven5 мин четене

Защо твоят AI notetaker спира записа по средата на срещата

Собственото ни приложение прекрати две от нашите срещи, докато отсрещната страна беше по средата на изречение. Криминалистичната следа отведе до добронамерен idle таймер, който не чуваше никого освен теб — и до втори бъг, който можеше да заключи целия ти десктоп. И двата поправени в GeekBye v2.0.9.

Надеждност
Срещи
Инженерство
Защо AI транскрипцията греши техническите термини (и как го поправихме)
Steven
Steven6 мин четене

Защо AI транскрипцията греши техническите термини (и как го поправихме)

Сесия на живо чу "what is the pointer in C++" като "what is the point in life". Ето криминалистичната следа от този транскрипт до GeekBye v2.0.11 — keyterm biasing, състезание по време, което прекъсваше връзката, и денят, в който собственият ни фикс даде обратен ефект.

Транскрипция
Надеждност
Инженерство