Steven
Steven5 хв читання

День, коли наш застосунок влаштував DDoS самому собі

Черга відкладених завантажень, випущена вся разом при запуску, перетворила кожен клієнт GeekBye на маленьку атаку denial-of-service на наші власні сервери. Виправлення — і драбина liveness з’єднання, яку воно змусило нас побудувати — одна з найкорисніших речей, яких навчив нас v2.

Надійність
Мережі
Інженерія
Релізи GeekBye
День, коли наш застосунок влаштував DDoS самому собі

Кожен інженер розподілених систем рано чи пізно зустрічає thundering herd (сполохане стадо): масу клієнтів, що роблять одне й те саме в ту саму мить, і спільний ресурс за ними прогинається. Зазвичай це чиїсь чужі клієнти. У GeekBye v2.0.1 це були наші, а нападником був наш власний застосунок.

Як нотатник атакує сам себе

GeekBye вміє завантажувати записи на ваш Google Drive. Якщо запис не може завантажитися одразу — ви були офлайн, застосунок закрився, Drive заїкнувся — він потрапляє в чергу, щоб повторити спробу пізніше. Розумно.

Збій був у тому, коли наставало це «пізніше». При наступному запуску застосунок намагався спорожнити всю чергу разом. Один користувач із тижнем відкладених записів перетворювався на сплеск одночасних запитів на завантаження в ту мить, коли відкривав застосунок. Помножте на кожного користувача, що запускає застосунок вранці, і наш backend просили автентифікувати, перевірити ліміти та обробити стіну трафіку в ті самі кілька секунд — від клієнтів, які всі, технічно, поводилися правильно.

Наш backend зробив правильну річ і обмежив цей потік (rate-limit). І ось тут почалася вторинна поломка. Відповідь про перевищення ліміту — це HTTP 429, а 429 дуже схоже на інші збої, якщо не бути обережним:

  • Стартове отримання профілю отримало 429, і застосунок сприйняв це як невдалу автентифікацію — на мить вийшовши з цілком дійсної сесії користувача.
  • З’єднання транскрипції отримало загальне 429 і показало моніт про апгрейд через вичерпання ліміту аудіо — кажучи платним користувачам, що вони досягли квоти, якої не досягали.

Тож одна першопричина — невпорядкована черга — дала три видимі симптоми: навантаження на сервер, хибні виходи із сесії та хибні апселли. Класична форма self-DoS: навантаження — це погано, але саме хибне тлумачення симптомів навантаження — це те, що користувачі реально відчувають.

Виправлення, у двох шарах

Зупини паніку стада. Черга завантажень тепер розтягнута в часі — запити розподілені з backoff, а після будь-якого 429 настає cooldown, щоб клієнт відступав від навантаженого сервера, а не налягав на нього ще дужче. Thundering herd стає впорядкованою чергою.

Зупини хибне тлумачення. Перехідне 429 чи 5xx при запуску більше не виводить вас із сесії — клієнт розрізняє «сервер ненадовго зайнятий» і «ваша сесія недійсна». А загальне 429 про перевищення ліміту на шляху транскрипції більше не видає себе за помилку вичерпання квоти аудіо; лише справжня відповідь про квоту показує моніт про апгрейд. Урок, що засів: код помилки — це не значення помилки. 429 означає «збав темп», а не «ти не авторизований» і не «у тебе скінчилася квота» — і клієнт мусить знати різницю.

Драбина liveness, що постала слідом

Упорядкування стада викрило тихішу проблему: коли з’єднання справді псувалося під навантаженням, як швидко ми це помічали? Повільніше, ніж хотілося. Тож v2.0.4 вибудував драбину liveness з’єднання для транскрипції в реальному часі:

  • Heartbeat’и — сокет транскрипції пінгується через фіксований інтервал, тож тихо померле з’єднання виявляється за секунди, а не в очікуванні, доки відмовить наступний фрагмент аудіо.
  • Копняк перепідключення при поверненні мережі — коли ОС повідомляє, що мережа повернулася, застосунок проактивно відновлює з’єднання, замість того щоб чекати, доки натрапить на це відкриття.
  • Liveness через контрольний ping/pong — round-trip на рівні застосунку, що підтверджує не просто «сокет відкритий», а «інший кінець справді відповідає».
  • Ворота перепідключення, що належать сервісу — одне місце вирішує, чи перепідключатися, замість кількох частин застосунку, які наввипередки намагаються це зробити й заважають одна одній.

Ніщо з цього не ефектне. Усе це — причина, чому сесія у v2 відчувається так, ніби вона просто... лишається на зв’язку.

Це сталося знову цього тижня — поверхом нижче

Ось частина, що робить це більшим, ніж воєнна байка. Той самий клас збою виринув кілька днів тому, рівнем нижче за нас. Один клієнт випустив 21 старт сесії транскрипції менш ніж за 400 мілісекунд — і спіткнувся об чинний на весь акаунт ліміт нашого провайдера мовлення: 15 одночасних запитів. Паніка стада проти спільної інфраструктури, точно як черга завантажень, тільки націлена на залежність, а не на наш власний backend.

Форма ідентична, і виправлення теж: клієнту, що налягає, потрібен single-flight guard, щоб він не міг випустити двадцять стартів на один намір, а спільному ресурсу потрібен ліміт на користувача, щоб один клієнт не міг поглинути весь пул. Ми вже будували клієнтську версію цього — pacer завантажень і є цим патерном. Тепер ми застосовуємо його до стартів сесій. Thundering herd — це не баг, який виправляєш одного разу; це форма, яку вчишся розпізнавати всюди, де клієнти зустрічають спільний ліміт.

Три речі на винос

  1. Ваші власні клієнти — це навантажувальний тест, який ви не планували. Усе, що групується-при-старті, повторюється-при-запуску чи перепідключається-при-пробудженні, — це thundering herd, що чекає на достатню кількість користувачів. Розтягніть це в часі, перш ніж вони у вас з’являться.
  2. Відрізняйте код від значення. 429 — найбільш неправильно прочитуваний статус на світі. «Збав темп» — це не «вийди із системи» і не «заплати нам». Спрямуйте кожен збій до того, що він реально означає.
  3. Liveness — це драбина, а не прапорець. «Чи живе з’єднання?» має кілька чесних відповідей на різних шарах — сокет відкритий, байти йдуть, інший кінець відповідає, мережа присутня. Надійний застосунок перевіряє більше ніж одну.

Це та неефектна машинерія під спокоєм GeekBye v2. Про функції надійності, які вона уможливила, читайте чому ваш ШІ-нотатник зупиняється на поганому Wi-Fi і транскрипція в реальному часі, коли файрвол блокує WebSocket (v2.0.8). Про сусідні релізи цієї серії — чому ваш ШІ-нотатник зупиняє запис посеред зустрічі (v2.0.9).

Схожі статті

Транскрипція в реальному часі, коли файрвол блокує WebSocket
Steven
Steven4 хв читання

Транскрипція в реальному часі, коли файрвол блокує WebSocket

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

Транскрипція
Мережі
Інженерія
Чого насправді варта версія 2: 206 комітів чесних станів
Steven
Steven6 хв читання

Чого насправді варта версія 2: 206 комітів чесних станів

GeekBye v2 не був релізом нових функцій. Це були 206 комітів, націлених на одну ідею: застосунок ніколи не повинен брехати про власний стан. Ось чого це коштує — включно з однорядковою помилкою в lockfile, яка ледь не завадила нам випустити хоч щось із цього.

Надійність
Інженерія
Реліз
Чому ваш ШІ-нотатник зупиняє запис посеред зустрічі
Steven
Steven5 хв читання

Чому ваш ШІ-нотатник зупиняє запис посеред зустрічі

Наш власний застосунок завершив дві наші зустрічі, поки співрозмовник був на середині фрази. Форензичний слід привів до таймера бездіяльності з добрими намірами, який чув лише вас, — і до другого бага, здатного заблокувати весь робочий стіл. Обидва виправлені в GeekBye v2.0.9.

Надійність
Зустрічі
Інженерія