Pull to refresh

Comments 32

Для корректной работы circuit breaker нужно отказаться от промежуточных балансировщиков, либо перенести circuit breaker на них. В случае клиентской балансировки, клиент видит все хосты и может выкидывать их точечно, а не весь балансер разом.

В статье это скомкано, но имелся ввиду кейс, когда определенная часть заказов или пользователей сбоит. Но скорее не по причине плохого хоста, а из-за выпадания одного шарда СУБД или поломанной логики для части заказов (например, только для Доставки или только за кэш).

Вася передает благодарность, в статье поправил

Спасибо за такое доступное и в то же время информативное изложение информации, очень круто написано, и видно, что вложено много сил. Прочел с пользой и удовольствием)

Денис, а не рассматривали внедрений сервис мешей для решения этих задач? Я стараюсь такие задачи на уровне инфры решать. Интересен ваш опыт.

Хорошее замечание. Пока у нас это реализовано внутри языковых фреймворков (userver, go, java), со временем хотим унести все в service mesh. Экспериментируем с envoy, у него есть в тч retry budget.

При этом на практике довольно важно понимать что это за техники вокруг ретраев и почему они существуют, а не просто использовать готовое.

Не будет ли полезным сделать короткий вывод о том, что ретраи это почти как криптография: "не разбираешься – не лезь, иначе сделаешь только хуже"?

есть сходство, да. Только в отличие от криптографии, довольно много популярных либ коннекторов/клиентов делает небезопасные ретраи по умолчанию (порой можно сконфигурить безопасно, но это надо явно указывать).

Прежде всего, спасибо за классную статью с большим количеством примеров, схем, графиков!

В статье есть такая фраза:

Вася внедрил exponential backoff и jitter. Также он вынес логику ретраев из сервиса в общую библиотеку http-клиентов во фреймворке userver.

Подскажите, пожалуйста, а в настоящем userver есть какие-то готовые механизмы для борьбы с ретраями?

Ретраи с exponential backoff c jitter в userver работают из коробки для клиентов (пример). Retry budget для клиентов сейчас не доступен в опенсорс, но есть возможность вручную выставлять на стороне сервера ограничение на RPS на "ручку"; и ограничение RPS на сервер целиком выставляется автоматически логикой Congestion Control. Плюс во все компоненты (сервер, клиенты, базы данных) проинтегрирован Deadline Propagation

Так что сервис на userver убить ретраями просто так не получится.

В добавок, сейчас есть Congestion Control для Монги (и скоро появится для PostgreSql!). Так что есть автоматика, которая осознаёт что база данных чувствует себя плохо даже без ретраев, и помогает выйти ей из MFS.

P.S.: Постараемся вынести retry budget для клиентов в опенсорс версию, чтобы userver понежнее относился к сервисам-клиентам

Постараемся вынести retry budget для клиентов в опенсорс версию, чтобы userver понежнее относился к сервисам-клиентам

Звучит здорово, спасибо! а подскажите еще, почему выбрали retry budget, а не retry circuit breaker?

Retry budget меньше нагружает сервер при отказе. В статье подробно рассказано, почему его выбрали

ИМХО проблема в том что нужна соответствующая инфраструктура для запуска микросервисом. И если говорить проще, то Вася был с самого начала прав, и автоскейлинг подов решил бы бы проблему storm retry.

Нужно правильно реализовать пробы, startup, readiness, liveness. Проба startup понятно, микросервис запустился, прочитал конфигурацию, прогрел кеш и сделал другие нужные действия. Сложнее дело обстоит с readiness и liveness - обработчик этих проб должен быть в своем потоке, и не блокироваться входящими соединениями. Если такие пробы будут в общем обработчике соединений, то шторм запросов просто приведет к тому что liveness не будет проходить, и контейнер будет циклически перезапускаться. В идеальной ситуации liveness проба должна всегда быть успешной если микросервис работает(это проба про работу контейнера, а не микросервиса), тоесть данная проба нужна только для перезапуска контейнера из-за фатальной ошибки. Проба readiness самая сложная, она должна проверять что микросервис работает в нужном режиме, образно говоря для примера с микросервисом цен, такая проба всегда должна делать запрос на получения цены (чтобы полностью проверить подключение к базе, подключение к редису и т.д.)

При этом есть одно но, автоскейлинг на основе % памяти/cpu работает только для простых случаев(или обычных cpu bound задач), для более сложных нужно чтобы микросервис собирал и отдавал телеметрию. И в этом случае автоскейлинг будет работать так, если например ответов с 500 больше 40% (не с пода, а со всех подов, статистику берем с сервера телеметрии), то начинаем скейлиться, например х2.

И когда нормально работает телеметрия, то ее можно уже вкрутить и в readiness пробу, образно говоря перед запросом на получения цены, проверить в данных телеметрии процент 500, и если он больше 80% например, то проба вернет fail, и соответственно на контейнер перестанет попадать трафик, контейнер всем ответит и вернется в нормальное состояние.

Понятно что цифры взял с потолка, и их надо брать из мониторинга и тюнить под свои нужды.

спасибо за идею, и по правде мы не использовали автоскейлинг в Такси. Но боюсь он намного сложнее чем retry budget:

  1. когда скейлишь stateless сервис до скейлинга его базы, то базу прикладывает коннектами или запросами. Наступали на граблю когда руками скейлили на факапах. Вместо базы может быть и другая незащищенная зависимость

  2. чтобы скейлить сервис выше какого-то числа подов порой нужно напилить мультиплексер коннектов к базе

  3. автоскейлинг надо дружить с load shedding и рейт-лимитерами если юзается только сигнал про cpu usage

  4. механика circuit breaker через readiness пробу уязвима к тем же проблемам что описаны в статье: либо порог будет слишком большой (50+%), либо будет на выпадании шарда базы отрубать все. С большим порогом есть шанс войти в состояние когда переходы слишком резкие, и система не может работать при полном возврате трафика, и работает по синусоиде. Мы такое тоже ловили.

не отрицаю что автоскейлинг делать нужно, и мы планируем, челленджу лишь его огромную сложность и риски относительно retry budget

Получается, что автоскейлинг исходит из того, что нет аварий, а есть недостаточное выделение ресурсов. А ретрай-бюджет исходит из того, что авария однажды случится.

Надо сказать, не в каждой крупной компании можно закрепить такое видение ("авария однажды случится") у руководства.

А как девопсы относятся к ретрай-бюджету? Настройки бюджета живут не на сервисе, а на потребителе, и потребителей может быть много. Получается, что нужно как-то синхронизировать настройки бюджетов между потребителями? Ведь иначе любой криво настроенный потребитель может создать амплификацию и снова привет ручной ступенчатый ввод нагрузки по 5%?

у нас по дефолту в инфре микросервисов (в userver) выставлено 10%. Во всех новых сервисах оно автоматом начинает работать так. При этом можно оверрайдить и отключать, и есть такие единичные случаи, но это отдельная проблема.

Я думаю так, что retry budget хорош в том случае если апи выставляетесь наружу, а внутри фиксированная инфраструктура без скейлинга. Внутри же как по мне лучше использовать знание инфраструктуры и профилей нагрузок для управления трафиком и скейлинга если надо.

1-е и должно решать использованием метрик перед обращением к внешним ресурсам, если в начале /get/price сделать запрос в кеш метрик и увидеть что коннекты к базе (или другому сервису) обрабатывают с задержкой, то нету смысла делать еще один запрос, надо отдать клиенту ошибку. Да вместо базы может быть все что угодно, и если для этого есть метрика, то ее лучше использовать. Да можно по таймеру отдавать ошибку клиенту, если ответ идет дольше чем надо от базы или другого сервиса, но это только в том случае, если нету метрик.

Идея как раз в том чтобы не ждать пока мониторинг сработает, а сработает он с задержкой и из-за этого накопиться кучу запросов в очереди, а сразу используя метрики с пода решать вопрос про деградацию сервиса и отдавать нужный ответ, и просто не заставлять клиента ловить таймаут.

2-е да это нужно, для меня прям почти идеальный пример такого мультиплексор cloud-sql-proxy, потому что он собирает метрики, есть ендпоинты для проб startup, liveness, readiness и можно держать коннекты к разным базам и из-за этого можно на localhost забиндить на разные порты например основную бд и реадонли реплики.

3-е и 4-е я как раз и против скейлинга на основе cpu usage для не cpu bounded задач, надо использовать метрики которые относятся к запросам (количество ошибок, среднее, перцентили). Ну и совмещение подхода, если микросервис перегружен запросами по внутренним метрикам, то микросервис на время(например 1 мин) вывести из обслуживания(через /readiness пробу). Микросервис вернется к обработке трафика как только перемелет все входящие запросы из очереди и метрики выровняться. А для того чтобы не было массового вывода таких микросервисов из работы, скейлинг должен сработать раньше.

Да и чтобы этот самый скейлинг работает, в идеальной ситуации надо чтобы один микросервис работал с одной БД, если же с одной БД работает много микросервисов, то очень сложно угадать скейлинг какого их них приведет к падению базы. Или же просто в таком случае собирать микросервисы в микромонолиты.

1 - да, и вот это довольно дорого. Интересно насколько дешево у вас удалось похожее сделать? Мы сначала смотрели на envoy adaptive concurrency чтобы эту проблему решить, там свои сложности всплыли после тестов. В итоге решили вместо envoy запилить самим адаптивные пулы коннектов к СУБД в userver (то что выше @antoshkka называет congestion control). И это довольно дорогая разработка вышла на месяцы. А retry budget для нового go фреймворка внутри мы например за пару дней сделали.

2 - у нас такие мультиплексоры были в Такси, мы от них из-за сложностей отказались, но снова думаем о них :) по опыту факапов с mongos и самописными мультиплексорами - сложно, рискованно, легко набажить. У вас все гладко тут было?

Ну с микросервисами оно все дорого.

1-е я делал так, в контейнере основным процессом был monit он собственно отвечал за запуск cloud_sql_proxy, haproxy и приложения (причем внутри конфигов monit были свои чеки для запуска/перезапуска нужных частей и что самое главное monit позволяет задавать зависимости, ну и не менее приятное что можно слать алерты для разных событий запуска/перезапуска, ну и прям как бонус, но я это не использовал, что monit умеет собирать статистику по использованию cpu/memory и с ней работать/слать алерты). Пробы были такие, startup - успешный вызов /bin/true(контейнер запустился), liveness проба pgrep -x /usr/bin/monit(главный процесс запустился). Проба readiness хитрая, сам haproxy отвечал всегда 200 кодом, но при этом внутри был чек на /healtz ендпоинт приложения (если чек не прошел то возвращаем ошибку, если прошел то ответ от приложения). Внутри healtz и была сделана проверка на работоспособность - запрос к бд с дефолтовыми параметрами (для вашего примера запрос цены всегда одного и того же маршрута), с таймером на получение ответа, если таймер сработал раньше ответа из бд, то healtz возвращает ошибку.

Если бы внутри приложения были метрики и возможность обращения к ним, тогда бы haproxy не был бы нужен и схема была чуть проще.

Пришлось городить огород с monit, потому что в кубере нету зависимостей и порядка запуска контейнеров, стратегия перезапуска странная (перезапуск всего пода, если один из контейнеров не здоров, поэтому нельзя так просто cloud_sql_proxy запустить в виде sidecar контейнера).

2-е да в с cloud-sql-proxy все было гладко, как раз из-за наличия проб, поэтому monit нормально мониторил эту проксю, конфигурацию такого плана

check process cloud_sql_proxy with pidfile "/var/run/app/cloud_sql_proxy.pid"
  start program = "/etc/init.d/start_cloud_sql_proxy"
  stop program = "/bin/sh -c 'kill -s SIGTERM `cat /var/run/app/cloud_sql_proxy.pid`'"
  if failed host 127.0.0.1 port 9090 protocol HTTP request "/startup" then restart
  if failed host 127.0.0.1 port 9090 protocol HTTP request "/liveness" then restart
  group proxy

сам /etc/init.d/start_cloud_sql_proxy не особо сложный и просто обертка для запуска с нужными параметрами, но можно было это все напрямую вставить в monit конфиг

#!/usr/bin/env bash

set -euo pipefail

SERVICE_CMD="/usr/local/bin/cloud-sql-proxy --health-check --private-ip ${INSTANCE_CONNECTION_NAME}"
SERVICE_LOG="/var/log/app/cloud_sql_proxy.log"
SERVICE_PID="/var/run/app/cloud_sql_proxy.pid"

${SERVICE_CMD} >${SERVICE_LOG} 2>&1 &
echo ${!} > ${SERVICE_PID}

понятно, спасибо за детали!

А у вас нет такой же статьи, только на английском?) С коллегами поделиться.

Пока нет, планирую. Когда переведу - в ответ на ваш комментарий прикреплю ссылку.

Я немного не понимаю, почему нельзя, в случае если сервер не справляется с нагрузкой, просто выдавать отдельный код ошибки, а клиент, в случае её получения, просто не будет делать повторы.

Это, впрочем, касается любой серверной ошибки (aka 500 - Internal Server Error). Если сервисы идемпотенты, то одинаковые данные приведут к одинаковым ошибкам. Если этот так, то зачем вообще повторять? Я такое видел, когда пытаются скомпенсировать багливость сервиса с помощью повторов, сродни заметанию пыли под ковёр. Надо ли говорить, что обычно это только усугубляет ситуацию, ибо нахождение ошибок становится более сложной задачей?

Ретраи нужны только для сетевых ошибок и больше ни для чего другого.

Кроме того, вызывает вызывает сомнение в пользе увеличивающих таймаутов в небатчевых сервисах. Если клиентский сервис может отвалиться по таймауту, то экспотенциальные повторы только увеличат вероятность таймаута

Кривой релиз, который приводит к 500й ошибке не является сетевой ошибкой, а также не является нормальным поведением системы.

Ну дык верно, здесь нам не надо повторять - это же всё равно ничего не изменит. Надо сервис фиксить, а не повторами заниматься.

на 1 под мог только что выехать канареечный релиз, который кидает 500 на 1% специфичных запросов. Хорошо бы его отретраить в другой под.

по сути ретраи в таких кейсах работают как естественная балансировка нагрузки между подами

>Если сервисы идемпотенты, то одинаковые данные приведут к одинаковым ошибкам. Если этот так, то зачем вообще повторять?

это чаще всего задержки in-memory очередей где-то в системе, те самые "флапы". Их норм ретраить. Происходят либо из-за случайностей (не повезло и на данном поде параллельно выполнялся очень тяжелый запрос, съевший CPU), либо когда одному поду плохонько.

>Ретраи нужны только для сетевых ошибок и больше ни для чего другого.

даже если такое правило принять, то его нереально выполнять: для этого нужно чтобы каждый сервис или СУБД при получении сетевой ошибки от зависимостей, наружу тоже выдавал сетевую ошибку (например, тайм-аутил или разрывал коннект). Так как ретраить могут на уровень выше. А если не выдать сетевую ошибку - не будет ретрая. Но так код не пишут обычно.

>сомнение в пользе увеличивающих таймаутов в небатчевых сервисах. Если клиентский сервис может отвалиться по таймауту, то экспотенциальные повторы только увеличат вероятность таймаута

не расшифруете мысль? тайм-ауты в exponential backoff не увеличиваются, растут лишь паузы между попытками.

это чаще всего задержки in-memory очередей где-то в системе, те самые "флапы". Их норм ретраить. Происходят либо из-за случайностей (не повезло и на данном поде параллельно выполнялся очень тяжелый запрос, съевший CPU), либо когда одному поду плохонько

С какой это стати корректность работы сервиса должна зависеть от случайностей? Для подов настраиваются ресурсы, а сервисы тестируют на производительность, чтобы такого не было. Да и какой смысл повторять, если можно просто подождать подольше ?? Повторы только увеличат задержки, но никак не уменьшат.

даже если такое правило принять, то его нереально выполнять: для этого нужно чтобы каждый сервис или СУБД при получении сетевой ошибки от зависимостей, наружу тоже выдавал сетевую ошибку (например, тайм-аутил или разрывал коннект)

С чего это вы так решили? Любая невосстановимая ошибка выдаёт 500. Да и зачем серверу имитировать сетевую ошибку. Это было бы странно, не находите?

 А если не выдать сетевую ошибку - не будет ретрая. Но так код не пишут обычно

Именно так его и пишут

тайм-ауты в exponential backoff не увеличиваются, растут лишь паузы между попытками.

Паузы увеличивают общее время недоступности сервера, а пока мы не определимся, доступен сервис или нет, мы не можем дать ответ последующим сервисам

на 1 под мог только что выехать канареечный релиз, который кидает 500 на 1% специфичных запросов. Хорошо бы его отретраить в другой под.

В чём тогда его канареечность, если запрос всё равно падает на другой инстанс?

  1. Что мешает выстроить очереди, потребление которых полностью контролируют потребители. Если даже клиенты "натуральные синхронисты" типа фронтэнда, то просто санитайз сервис делается, который изолирует синхронный от асинхронного контекста.

  2. Ну и даже если 1. не вариант прям вообще, то по мониторингу нагрузки делается просто вертикальное и горизонтальное скалирование и нет проблем. Можно прям прописаться, что бы подов было после деплоя в 5 раз больше, чем средний уровень и они после обработки ретраев сами свернуться - KEDA

неплохо было бы иметь список действующих лиц в самом начале :)

Sign up to leave a comment.