Ещё одна история о распределённых системах
Давайте перенесёмся в апрель 2018 года. Тогда я работал на стартапе, который собирался выпустить очень востребованную новую функцию. Мы уже сформировали лист ожидания, но о дате запуска не распространялись (в основном потому, что не знали, когда закончится разработка).
После доставки последних функций, чтобы считать, что минимально жизнеспособная версия продукта (MVP) готова, мы провели небольшое тестирование, чтобы убедиться, что все пользовательские пути работают так, как мы рассчитывали. Все выглядело хорошо, и, поговорив с продукт-менеджером мы решили включить эту функцию для всех клиентов. Конечно, вся компания с волнением ожидала этого события — первое большое обновление за долгое время — и не успели мы оглянуться, как всем клиентам были отправлены push-уведомления, а лист ожидания был закрыт.
Мы не ожидали такого наплыва входящего трафика, и вскоре стали поступать предупреждения о падении сервисов из-за неконтролируемой паники, которую мы не учли. После исправления мы повторно развернули систему, на мгновение поток предупреждений прекратился, и мы могли расслабиться. Однако это было недолго, так как через 5 минут начал появляться совершенно другой набор предупреждений (высокая задержка, высокое использование ЦП). Пришло время снова поработать в аврале.
К сожалению, мы только что на собственном опыте столкнулись с проблемой «грохочущего стада».
Инцидент с «грохочущим стадом» на API обычно возникает, когда большое количество клиентов или сервисов одновременно отправляют запросы к API после некоторого периода недоступности или задержки. Это может быть вызвано как собственными, так и сторонними сервисами, повторно отправляющими запросы после некоторого простоя или нестабильности.
Эти запросы часто не являются злоумышленными, просто инженеры пытались сделать все правильно; один из первых уроков, который нам преподают при работе над распределенными системами, заключается в том, что сети не являются достаточно надежными, и следует исходить из того, что они могут дать сбой. Это хороший совет, но важно то, как мы справимся с этим сбоем.
Чтобы решить эту проблему, нам нужно либо уменьшить количество запросов, либо увеличить количество запросов, которые может обрабатывать наш API (а лучше и то, и другое). Для этого можно:
Мы увеличили масштаб нашего API, уменьшили масштаб других сервисов, и теперь вновь работаем в обычном режиме. Однако стресс не прошел полностью, поскольку мы знаем, что подобное может повториться в любой момент. Что можно сделать, чтобы предотвратить это? Мы должны подойти к этому вопросу с нескольких сторон. Давайте рассмотрим их, начав с изменений, необходимых для API.
Во-первых, можно попробовать добавить ограничения скорости для каждого клиента. Это реально сделать это на уровне инфраструктуры (многие балансировщики нагрузки или обратные прокси поддерживают эту функцию), а можете сделать это в своем приложении. В любом случае, предусмотреть для каждого клиента ограничение на количество запросов — действенный способ защиты вашего API. Принимать решение об ограничении скорости для каждого приложения – это искусство, а не точная наука, поэтому потребуются некоторые эксперименты. Я рекомендую, чтобы ваш API возвращал заголовок Retry-After, и клиенты могли использовать его для определения времени «отката». Этот подход хорош тем, что вы можете поощрять экспоненциальное поведение клиентов.
Возможно, получится реализовать кэширование в вашем API, чтобы избежать необходимости выполнять дорогостоящие запросы в БД или вычисления. Этот метод подходит не для всех API, и есть поговорка, что «если вы решаете проблему с помощью кэширования, у вас теперь есть одна проблема», которую всегда стоит иметь в виду, когда вы обращаетесь к ней как к решению.
Можно попробовать добавить в ваш API автоматический выключатель (предохранитель). В таком случае, обнаружив увеличение количества запросов, размыкать цепь и предотвращать обработку большего количества запросов. В частности, в Go для этой цели есть несколько библиотек с открытым исходным кодом, например, sony/gobreaker или go-kit.
В описанном выше случае у нас действительно были настроены оповещения, которые позволили нам выявить проблему, но ущерб уже был нанесен. Более раннее обнаружение возникающих проблем могло бы привести к принятию мер по вертикальному или горизонтальному масштабированию, чтобы предотвратить их возникновение. Еще лучше, если бы мы могли использовать эти ранние предупреждения как сигнал для начала масштабирования и автоматического устранения инцидента до того, как он произойдет!
Клиентам, обращающимся к вашему API, придется поработать с ним, чтобы убедиться, что они успешно реагируют на ограничение скорости. Более того, в случае получения отказа (код состояния из категории 5xx), вы можете попросить реализовать экспоненциальный откат с небольшим джиттером, чтобы гарантировать, что повторные запросы будут «распределены» в случае простоя. Сэм Роуз отлично и наглядно показал шаблоны повторных запросов, эта схема помогает понять,, как различные стратегии могут повлиять на ваш API.
Как и в случае с API, здесь стоит подумать о кэшировании. Нужна ли клиенту самая свежая информация? Может ли хранение некоторых ответов API локально снизить нагрузку на API и улучшить качество обслуживания клиентов?
Вы можете взять на вооружение некоторые уроки, извлеченные нами из описанного инцидента, и внести изменения в свою инфраструктуру, чтобы уберечь другие сервисы от подобной участи. Вы можете рассмотреть возможность введения ограничений скорости и снижения нагрузки на ваш API-шлюз. Мне особенно нравится этот пост в блоге Stripe. Здесь рассказывается о том, как авторам удалось решить такую проблему. Как уже говорилось выше, вы также можете реализовать ограничение скорости на более высоком уровне. Разрыв цепи — еще один паттерн, который можно рассмотреть на уровне инфраструктуры.
С проблемой «грохочущего стада» сложно разобраться, когда она уже возникла, если у вас не предусмотрено готовых стратегий противодействия ей. Как и при возникновении большинства проблем в области распределенных систем, важно действовать прозорливо. Если вы еще не обсуждали, какие пользовательские пути для вас наиболее важны, самое время этим заняться.
Кроме того, если вы убедитесь, что ваш API позволяет накладывать ограничения на скорость и что клиенты используют какую-либо форму экспоненциального отката, это обеспечит вам успех, если вы когда-нибудь окажетесь в ситуации, когда ваш API будет перегружен.
Надеюсь, эта статья была вам полезна, и она поможет вам приручить ваше стадо!
P.S Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.
Давайте перенесёмся в апрель 2018 года. Тогда я работал на стартапе, который собирался выпустить очень востребованную новую функцию. Мы уже сформировали лист ожидания, но о дате запуска не распространялись (в основном потому, что не знали, когда закончится разработка).
После доставки последних функций, чтобы считать, что минимально жизнеспособная версия продукта (MVP) готова, мы провели небольшое тестирование, чтобы убедиться, что все пользовательские пути работают так, как мы рассчитывали. Все выглядело хорошо, и, поговорив с продукт-менеджером мы решили включить эту функцию для всех клиентов. Конечно, вся компания с волнением ожидала этого события — первое большое обновление за долгое время — и не успели мы оглянуться, как всем клиентам были отправлены push-уведомления, а лист ожидания был закрыт.
Мы не ожидали такого наплыва входящего трафика, и вскоре стали поступать предупреждения о падении сервисов из-за неконтролируемой паники, которую мы не учли. После исправления мы повторно развернули систему, на мгновение поток предупреждений прекратился, и мы могли расслабиться. Однако это было недолго, так как через 5 минут начал появляться совершенно другой набор предупреждений (высокая задержка, высокое использование ЦП). Пришло время снова поработать в аврале.
Проблема «грохочущего стада»
К сожалению, мы только что на собственном опыте столкнулись с проблемой «грохочущего стада».
Инцидент с «грохочущим стадом» на API обычно возникает, когда большое количество клиентов или сервисов одновременно отправляют запросы к API после некоторого периода недоступности или задержки. Это может быть вызвано как собственными, так и сторонними сервисами, повторно отправляющими запросы после некоторого простоя или нестабильности.
Эти запросы часто не являются злоумышленными, просто инженеры пытались сделать все правильно; один из первых уроков, который нам преподают при работе над распределенными системами, заключается в том, что сети не являются достаточно надежными, и следует исходить из того, что они могут дать сбой. Это хороший совет, но важно то, как мы справимся с этим сбоем.
Непосредственное разрешение происшествия
Чтобы решить эту проблему, нам нужно либо уменьшить количество запросов, либо увеличить количество запросов, которые может обрабатывать наш API (а лучше и то, и другое). Для этого можно:
- Масштабировать API горизонтально: При выборе этого варианта обязательно следите за состоянием базы данных, особенно если в прошлом вы не масштабировали свой API горизонтально. Если вы не разделяете запросы на чтение и запись или не используете пул соединений, вы можете столкнуться с проблемой, связанной с базой данных!
- Масштабировать API вертикально: Это не всегда работает, но обычно существует определенная зависимость между процессором, памятью и количеством запросов, которые может обработать ваше приложение.
- Урезать менее критичные рабочие задачи: Возможно, есть какие-то пользовательские пути, на которые мы можем целенаправленно повлиять в краткосрочной перспективе, пока мы не вернемся в более стабильное состояние. Например, вы можете решить, что не разрешать новым клиентам регистрироваться в этот период — это оправданно. Для других компаний это может быть самым худшим вариантом, который только можно себе представить, а регистрация клиентов — это тот критически важный пользовательский путь, который мы должны обеспечить. Обязательно обсудите такие вещи в своей компании, если еще не сделали этого.
Убедитесь, что это не повторится
Мы увеличили масштаб нашего API, уменьшили масштаб других сервисов, и теперь вновь работаем в обычном режиме. Однако стресс не прошел полностью, поскольку мы знаем, что подобное может повториться в любой момент. Что можно сделать, чтобы предотвратить это? Мы должны подойти к этому вопросу с нескольких сторон. Давайте рассмотрим их, начав с изменений, необходимых для API.
Ограничение скорости
Во-первых, можно попробовать добавить ограничения скорости для каждого клиента. Это реально сделать это на уровне инфраструктуры (многие балансировщики нагрузки или обратные прокси поддерживают эту функцию), а можете сделать это в своем приложении. В любом случае, предусмотреть для каждого клиента ограничение на количество запросов — действенный способ защиты вашего API. Принимать решение об ограничении скорости для каждого приложения – это искусство, а не точная наука, поэтому потребуются некоторые эксперименты. Я рекомендую, чтобы ваш API возвращал заголовок Retry-After, и клиенты могли использовать его для определения времени «отката». Этот подход хорош тем, что вы можете поощрять экспоненциальное поведение клиентов.
Кэширование
Возможно, получится реализовать кэширование в вашем API, чтобы избежать необходимости выполнять дорогостоящие запросы в БД или вычисления. Этот метод подходит не для всех API, и есть поговорка, что «если вы решаете проблему с помощью кэширования, у вас теперь есть одна проблема», которую всегда стоит иметь в виду, когда вы обращаетесь к ней как к решению.
Разрыв цепи
Можно попробовать добавить в ваш API автоматический выключатель (предохранитель). В таком случае, обнаружив увеличение количества запросов, размыкать цепь и предотвращать обработку большего количества запросов. В частности, в Go для этой цели есть несколько библиотек с открытым исходным кодом, например, sony/gobreaker или go-kit.
Оповещения
В описанном выше случае у нас действительно были настроены оповещения, которые позволили нам выявить проблему, но ущерб уже был нанесен. Более раннее обнаружение возникающих проблем могло бы привести к принятию мер по вертикальному или горизонтальному масштабированию, чтобы предотвратить их возникновение. Еще лучше, если бы мы могли использовать эти ранние предупреждения как сигнал для начала масштабирования и автоматического устранения инцидента до того, как он произойдет!
Изменения клиента
Клиентам, обращающимся к вашему API, придется поработать с ним, чтобы убедиться, что они успешно реагируют на ограничение скорости. Более того, в случае получения отказа (код состояния из категории 5xx), вы можете попросить реализовать экспоненциальный откат с небольшим джиттером, чтобы гарантировать, что повторные запросы будут «распределены» в случае простоя. Сэм Роуз отлично и наглядно показал шаблоны повторных запросов, эта схема помогает понять,, как различные стратегии могут повлиять на ваш API.
Как и в случае с API, здесь стоит подумать о кэшировании. Нужна ли клиенту самая свежая информация? Может ли хранение некоторых ответов API локально снизить нагрузку на API и улучшить качество обслуживания клиентов?
Изменения в инфраструктуре
Вы можете взять на вооружение некоторые уроки, извлеченные нами из описанного инцидента, и внести изменения в свою инфраструктуру, чтобы уберечь другие сервисы от подобной участи. Вы можете рассмотреть возможность введения ограничений скорости и снижения нагрузки на ваш API-шлюз. Мне особенно нравится этот пост в блоге Stripe. Здесь рассказывается о том, как авторам удалось решить такую проблему. Как уже говорилось выше, вы также можете реализовать ограничение скорости на более высоком уровне. Разрыв цепи — еще один паттерн, который можно рассмотреть на уровне инфраструктуры.
Подведение итогов
С проблемой «грохочущего стада» сложно разобраться, когда она уже возникла, если у вас не предусмотрено готовых стратегий противодействия ей. Как и при возникновении большинства проблем в области распределенных систем, важно действовать прозорливо. Если вы еще не обсуждали, какие пользовательские пути для вас наиболее важны, самое время этим заняться.
Кроме того, если вы убедитесь, что ваш API позволяет накладывать ограничения на скорость и что клиенты используют какую-либо форму экспоненциального отката, это обеспечит вам успех, если вы когда-нибудь окажетесь в ситуации, когда ваш API будет перегружен.
Надеюсь, эта статья была вам полезна, и она поможет вам приручить ваше стадо!
P.S Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.
vhlv
У вас теперь при покупке бумажных книг электронные версии не прилагаются?
ph_piter Автор
Смотря какие книги, везде где можем - написано в карточке товара.
vhlv
https://www.piter.com/collection/all/product/chistaya-arhitektura-iskusstvo-razrabotki-programmnogo-obespecheniya
https://www.piter.com/collection/all/product/ekstremalnoe-programmirovanie-razrabotka-cherez-testirovanie-2
Тут не написано, но электронные версии прилагаются.