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

Проблема

На диаграмме ниже в общих чертах показано, как работает сайт Teachers Pay Teachers.

Наши пользователи - преподаватели, просматривающие сайт в поисках ресурсов, которые они используют в классе. Их запросы направляются через интерфейсные серверы, на которых работает Node.js. Контент поддерживается API, написанным на Elixir с использованием Phoenix framework. API зависит от множества внешних систем, от баз данных и кеша до нескольких сторонних API.

В идеальном мире системы никогда не откажутся, а надежность сайта - не проблема, которую нужно решить. На самом деле мы не живем в идеальном мире, и дела идут плохо. Системы действительно выходят из строя. Например, запросы к нашей странице результатов поиска требуют выполнения тяжелого запроса к базе данных для получения некоторого вторичного контента. Когда база данных находится под нагрузкой, время ожидания этого запроса может истечь, что приведет к сбою всей страницы. Другой пример: домашняя страница нашего мобильного приложения использует Wordpress API для небольшой части контента. Когда был обновлен API Wordpress, вся наша мобильная домашняя страница не загрузилась. Было бы легко найти несколько других примеров, следующих аналогичному шаблону зависимости.

Обработка сбоев

Мы не контролируем наши внешние зависимости. Кроме того, при написании программного обеспечения невозможно предсказать все вариации времени выполнения. Неудачи неизбежны. Полностью предотвратить их нереально. Вопрос в том, как обрабатывать сбои. Что мы можем сделать, если система не работает или не отвечает? На очень высоком уровне есть два подхода к этой проблеме:

  • Мы отвечаем вручную после обнаружения сбоя. Здесь, безусловно, есть способы оптимизации - например, мы помещаем контент, поступающий из внешних систем, за флажками функций, которые мы можем быстро отключить. Вот как мы справились с проблемой Wordpress. В TpT развертывание флагов функций занимает менее 30 секунд. MTTR (среднее время ответа) здесь будет составлять 30 секунд плюс время, необходимое человеку, чтобы действовать.
  • Мы программируем нашу систему так, чтобы она автоматически реагировала и постепенно снижала производительность с помощью автоматических выключателей. Такой подход не требует вмешательства человека. Ниже мы остановимся на этом подходе.

Представляем автоматические выключатели

Шаблон автоматического выключателя автоматически обнаруживает неисправные службы и блокирует запросы к ним. Мы реализовали этот шаблон, расширив нашу существующую логику флагов функций. У нас есть флаги функций для переключения запросов к определенным службам. Чтобы обнаруживать нездоровые услуги, нам нужен способ хранить их состояние здоровья. Мы храним статус каждого запроса к сервису в Cachex в виде простого списка. Мы сохраняем 1 для успеха и 0 для неудачи, как показано ниже. По умолчанию TTL в кэше составляет 60 секунд. Этот тайм-аут настраивается для каждой функции.

if not circuit_break(service_name) do # if circuit is not broken 
  try do
    result = call_service()
    update_request_status(1, service_name) # set success in Cachex
    result
  rescue # service errored out
    err -> update_request_status(0, service_name) # set failure
  end 
end

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

def circuit_break(service_name) do
  request_statuses = Cachex.get(:api, service_name)
  history_size = length(request_statuses)
  # compute error rate
  error_rate = Enum.count(Enum.filter(request_statuses, &(&1 == 0))) / history_size
  if error_rate < error_threshold do
    false # don’t circuit break
  else
    true # circuit break
  end
end

Для каждой функции мы также определяем минимальный и максимальный размер истории статусов запросов, поскольку мы не хотим прерывать цепь слишком рано или переполнять кеш слишком большим объемом данных. Когда схема не нарушена, запрос продолжается и сохраняет его статус в кэше, как показано в первом блоке кода. При обрыве цепи все вызовы службы блокируются до истечения срока действия кеша. API чисто отправляет ответ клиентской части с пониженным содержанием. Фронтенд не ломается и умеет загружать страницу полученным контентом. Такое поведение называется постепенной деградацией.

Изящная деградация

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

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

Теперь, когда частота ошибок высока для спонсируемых элементов, мы просто прерываем цепь только для этой функции и возвращаем остальной контент на странице.

Другими словами, если пользователь, запросивший контент X, Y, Z и Y, обнаруживает высокую частоту ошибок, мы возвращаем X и Z вместо того, чтобы ничего не возвращать. Мы включили автоматический выключатель по запросам Wordpress, а также на нашей мобильной домашней странице и многие другие функции, которые имеют высокую вероятность отказа.

Для постепенной деградации не требуются автоматические выключатели, и она возможна при правильной обработке исключений. Шаблон автоматического выключателя - это оптимизация обработки исключений, которая заранее отключает ухудшенные функции. Это предотвращает поломку системы, которая уже находится под нагрузкой (в нашем примере, база данных), и позволяет ей быстрее восстанавливаться.

Компромиссы

Мы столкнулись с проблемами конкуренции за кеш с Cachex и позже перешли на ETS. Он по-прежнему требует, чтобы каждый сервер API локально сохранял данные о состоянии запроса, что может привести к несогласованному поведению на разных серверах. Использование внешнего хранилища, такого как Memcached или Redis, обеспечит синхронизацию серверов, но создаст еще одну точку отказа. Кроме того, это может привести к тому, что все серверы заблокируют функцию в случае, если только один сервер поврежден и порог ошибок низкий.

В настоящее время мы эвристически определяем пороги ошибок, которые могут быть слишком низкими или высокими. Альтернативой является установка пороговых значений динамических ошибок для каждой функции на основе ее исторического поведения. Мы смогли обнаружить аномальную частоту ошибок для сопоставимых прошлых периодов времени. Это приведет к меньшему количеству ложноотрицательных результатов, но потребует значительных затрат времени и ресурсов.

Последние мысли

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

Мы еще не находимся в идеальном состоянии, но описанная выше базовая схема автоматического выключателя оказалась эффективной для смягчения воздействия на пользователя во время непредвиденных сбоев. Особые благодарности моему коллеге Джеффу Мартину за то, что он работал со мной над реализацией.

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

Спасибо за прочтение!

Первоначально опубликовано на сайте engineering.teacherspayteachers.com 27 июня 2018 г.