Хотите получать уведомления о новом контенте?
Возникновение сбоев
Когда какой-либо сервис (или система) вызывает другой, возможно возникновение сбоев. Причиной этих сбоев может послужить целый ряд факторов. Они включают в себя серверы, сети, балансировщики нагрузки, программное обеспечение, операционные системы или даже ошибки системных операторов. Мы проектируем наши системы так, чтобы снизить вероятность сбоя, но невозможно разработать системы, которые никогда не выходят из строя. Поэтому в Amazon мы проектируем наши системы с учетом возможных сбоев так, чтобы снизить их вероятность и не допустить превращение небольшого процента сбоев в полное отключение. Для разработки отказоустойчивых систем мы используем три основных инструмента: тайм-ауты, повторные попытки и отсрочку.
Многие виды сбоев обнаруживаются при более длительной обработке запросов, а также их потенциальном не выполнении. Когда клиент ожидает выполнения запроса дольше, чем обычно, в течении более длительного времени также удерживаются ресурсы, используемые для этого запроса. Когда несколько запросов удерживают ресурсы в течение длительного времени, сервер может столкнуться с недостатком этих ресурсов. К подобным ресурсам могут относиться память, потоки, соединения, динамические порты, а также какие-либо другие ограниченные ресурсы. Во избежание этой ситуации, клиенты устанавливают тайм-ауты. Тайм-ауты – это максимальное время, в течение которого клиент ожидает выполнения запроса.
Зачастую повторное выполнение одного и того же запроса приводит к его успешному выполнению. Это происходит потому, что типы разрабатываемых нами систем редко выходят из строя целиком. Чаще всего они испытывают частичные или временные сбои. Частичный сбой – это когда некоторое количество запросов успешно выполняется. Временный сбой – это когда запрос не выполняется в течение непродолжительного периода времени. Повторные попытки позволяют клиентам выдержать случайные частичные сбои и временные сбои с помощью повторной отправки запроса.
Повторная попытка не всегда безопасна. Повторная попытка может увеличить нагрузку на вызываемую систему, когда в системе уже есть сбои из-за приближающейся перегрузки. Чтобы избежать этой проблемы, мы внедряем в наши клиентские сервисы возможность использования отсрочки. Это увеличивает время между последующими повторными попытками, что позволяет поддерживать равномерную нагрузку на серверную часть. Еще одна проблема с повторными попытками заключается в том, что у некоторых удаленных вызовов есть побочные эффекты. При тайм-ауте или сбое возможны побочные эффекты. Если неоднократное возникновение побочных эффектов нежелательно, рекомендуется создать идемпотентные интерфейсы API, допускающие безопасные повторные попытки.
Наконец, у поступающего в сервисы Amazon трафика может не быть одних и тех же характеристик в любой из моментов времени. При поступлении запросов зачастую могут возникать пиковые ситуации. Эти пиковые ситуации могут быть вызваны поведением клиента, восстановлением после сбоя или даже регулярного задания планировщика. Если ошибки вызваны нагрузкой, повторные попытки могут быть неэффективными, когда все клиенты их выполняют одновременно. Чтобы избежать этой проблемы, мы используем джиттер. Это случайное количество времени перед выполнением или повтором запроса, которое помогает предотвратить пиковые ситуации благодаря распределению интенсивности входного потока.
Каждое из этих решений обсуждается в следующих разделах.
Тайм-ауты
• Увеличение малой задержки серверной части, приводящее к полному отключению, из-за повторения всех запросов.
• Этот подход также не работает с сервисами, которые имеют жесткие границы задержки, где перцентиль 99,9 приближается к перцентилю 50. В этих случаях смягчение жестких условий помогает нам избежать малого увеличения задержки, которое вызывает большое количество тайм-аутов.
• При реализации тайм-аутов мы столкнулись с распространенной проблемой. Параметр SO_RCVTIMEO для Linux достаточно мощный, но имеет некоторые недостатки, из-за которых он не подходит в качестве параметра установки тайм-аута для сквозного сокета. Некоторые языки, такие как Java, предоставляют такое средство контроля непосредственно. Другие языки, такие как Go, предоставляют более надежные механизмы тайм-аута.
• Существуют также варианты реализации, в которых тайм-аут не охватывает все удаленные вызовы (например, вызовы с DNS- или TLS-подтверждениями). Обычно мы предпочитаем использовать тайм-ауты, встроенные в проверенные клиенты. При реализации наших собственных тайм-аутов мы обращаем особое внимание на точное значение параметров сокетов тайм-аутов и выполнение задач.
Повторные попытки и отсрочки
Повторные попытки «эгоистичны». Другими словами, когда клиент повторяет попытку, он тратит больше времени сервера, чтобы добиться большей вероятности успеха. Когда сбои происходят редко или временно, это не проблема. Это объясняется тем, что общее количество повторных запросов невелико, а компромисс с увеличением видимой доступности довольно эффективен. Когда сбои вызваны перегрузкой, повторные попытки могут значительно ухудшить ситуацию, так как они увеличивают нагрузку. Кроме того, они могут привести к задержке восстановления, сохраняя высокую нагрузку после устранения исходной проблемы на протяжении длительного времени. Повторные попытки похожи на мощное лекарство – полезны в правильной дозировке, но при чрезмерном использовании могут нанести значительный ущерб. К сожалению, в распределенных системах почти невозможно координировать действия всех клиентов для достижения необходимого количества повторных попыток.
Предпочтительным решением, используемым нами в Amazon, являются отсрочки. Клиент не выполняет немедленные и агрессивные повторные попытки, а выжидает некоторое время. Наиболее распространенным шаблоном является экспоненциальный алгоритм отсрочки, при котором время ожидания после каждой попытки увеличивается экспоненциально. Экспоненциальный алгоритм отсрочки может значительно увеличить время отсрочки из-за стремительного экспоненциального роста. Чтобы избежать значительного увеличения времени повторных попыток, при реализации, как правило, устанавливают максимальное значение отсрочки. Это ожидаемо называется ограниченным экспоненциальным алгоритмом отсрочки. Однако это создает другую проблему. Теперь все клиенты постоянно повторяют попытки с использованием такого максимального значения. Практически во всех случаях наше решение заключается в ограничении количества повторных попыток клиента и более ранней обработке сбоя в сервис-ориентированной архитектуре. В большинстве случаев клиент прекратит попытки вызова, потому что следует своим тайм-аутам.
Существуют и другие проблемы повторных попыток:
• Распределенные системы часто имеют множество уровней. Рассмотрим систему, в которой вызов пользователя создает пятиуровневый стек вызовов сервиса. Он предусматривает отправку в итоге запроса к базе данных, а также три повторные попытки на каждом уровне. Что происходит, когда в базе данных начинают возникать ошибки запросов вследствие нагрузки? Если на каждом уровне выполняются свои повторные попытки, нагрузка на базу данных увеличится в 243 раза, что сделает ее восстановление маловероятным. Это происходит потому, что количество повторных попыток на каждом уровне увеличивается: сперва три попытки, затем девять попыток и т. д. Повторные попытки на самом верхнем уровне стека могут привести к потере данных предыдущих вызовов, что снизит эффективность. Как правило, для низкозатратных операций плоскости управления и плоскости данных рекомендуется выполнение повторных попыток в единой точке стека.
• Нагрузка. Даже когда уровень повторных попыток всего один, при возникновении ошибок трафик по-прежнему значительно увеличивается. Для решения этой проблемы широко используются автоматические выключатели, которые при превышении порогового значения ошибки полностью прекращают вызовы нисходящего сервиса. К сожалению, автоматические выключатели добавляют модальное поведение в системы, из-за чего возможны сложности тестирования и вероятно значительное увеличение времени восстановления. Мы обнаружили, что можем уменьшить этот риск, ограничив повторные попытки локально с помощью алгоритма маркерной корзины. Это позволяет при наличии маркеров выполнять повторные попытки всех вызовов, а при их исчерпании – повторные попытки с фиксированной скоростью. В AWS было добавлено это поведение для AWS SDK в 2016 году. Это значит, что у пользователей с SDK есть встроенное регулирование.
• Принятие решения о выполнении повторной попытки. Мы считаем, что опасно выполнять повторную попытку в случае интерфейсов API с побочными эффектами, если они не способны обеспечить идемпотентность. Она гарантирует, что побочные эффекты будут возникать всего единожды независимо от количества повторных попыток. API только для чтения обычно являются идемпотентными, а API создания ресурсов – нет. Некоторые API, такие как Amazon Elastic Compute Cloud (Amazon EC2) RunInstances API, предоставляют явные механизмы на основе маркеров, чтобы обеспечить идемпотентность и безопасность повторных попыток. Чтобы побочные эффекты не дублировались, разработка API должна быть продуманной, а реализация клиентов – правильной.
• Определение того, при каких сбоях необходимы повторные попытки. HTTP обеспечивает четкое разделение между ошибками клиента и сервера. То есть при ошибках клиента нецелесообразны повторные попытки для одного запроса, так как они не будут удачными, а при ошибках сервера последующие повторные попытки могут быть удачными. К сожалению, потенциальная непротиворечивость в системах вносит в этот принцип множество оговорок. Ошибка клиента в одно мгновение может смениться успешным выполнением сразу после изменения состояния.
Повторные попытки – это мощное средство обеспечения высокой доступности при возникновении временных и случайных ошибок. Чтобы найти правильный компромисс для каждого сервиса, необходимо правильное суждение. Наш опыт показывает, что отличной отправной точкой является понимание того, что повторные попытки «эгоистичны». Повторные попытки – это способ подтверждения клиентами важности запроса и требование подключения большего количества ресурсов для его обработки. Если клиент слишком «эгоистичен», могут возникнуть серьезные проблемы.
Джиттер
Когда сбои вызваны перегрузкой или соперничеством, довольно часто алгоритм отсрочки оказывается не таким уж и полезным. Это связано с корреляцией. Если все неудачные вызовы отменяются одновременно, при повторных попытках они вызывают соперничество или перегрузку. Нашим решением является джиттер. Джиттер добавляет некоторую случайность в алгоритм отсрочки, чтобы распределить повторы операций во времени. Дополнительную информацию о степени и наилучших способах добавления джиттера см. в статье Экспоненциальный алгоритм отсрочки и джиттер.
Джиттер можно использовать не только для повторных попыток. Благодаря практическому опыту мы знаем, что трафик наших сервисов, включая операции плоскости управления и плоскости данных, имеет пиковую тенденцию. Эти пики трафика могут быть непродолжительными и зачастую могут скрываться агрегированными метриками. При разработке систем мы рекомендуем добавление джиттера в определенной степени ко всем таймерам, периодическим заданиям и другим отложенным работам. Это помогает распределить рабочие пики и упростить масштабирование нисходящих сервисов для рабочей нагрузки.
При добавлении джиттера к запланированным задачам мы выбираем джиттер для каждого хоста не случайным образом. Вместо этого мы используем согласованный метод, который каждый раз создает одинаковое количество на одном и том же хосте. Таким образом, при перегрузке сервиса или в состоянии гонки происходит добавление по шаблону. Люди склонны к выявлению шаблонов и с большой вероятностью определят первопричину. При перегрузке ресурса использование случайного метода обеспечивает добавление случайным образом. Это значительно усложняет поиск и устранение неисправностей.
В системах, над которыми я работал, таких как Amazon Elastic Block Store (Amazon EBS) и AWS Lambda, мы заметили, что клиенты зачастую отправляют запросы через регулярные промежутки времени, например, один раз в минуту. Если у клиента множество серверов с одинаковым алгоритмом поведения, они могут одновременно инициировать запросы. Это могут быть первые несколько секунд в минуте или первые несколько секунд после полуночи для ежедневных заданий. Сосредоточив внимание на нагрузке в секунду и работая с клиентами для устранения периодических рабочих нагрузок, мы выполнили тот же объем работы при меньшем использовании ресурсов сервера.
У нас меньше возможностей контроля над пиками клиентского трафика. Однако даже для задач, инициируемых пользователем, рекомендуется добавлять джиттер там, где он не повлияет на качество обслуживания.
Выводы
В распределенных системах временные сбои или задержки в случаях удаленного взаимодействия неизбежны. Тайм-ауты предотвращают необоснованно длительное зависание, повторные попытки могут скрывать сбои, а отсрочки и джиттер могут улучшить использование и уменьшить перегрузку в системах.
Мы в Amazon узнали, как важна осторожность при повторных попытках. Повторные попытки могут усилить нагрузку на зависимую систему. Если время ожидания для вызовов в системе начинает истекать, а система перегружена, повторные попытки могут усилить перегрузку. Мы стараемся избегать этого, обеспечив выполнение повторных попыток только при работоспособной зависимости. Мы прекращаем повторные попытки, если они не помогают улучшить доступность.
Об авторе
Марк Брукер – главный инженер в Amazon Web Services. Работает в AWS с 2008 года над множеством сервисов, включая EC2, EBS и IoT. В настоящее время Брукер сосредоточил усилия на сервисе AWS Lambda, работая в том числе над вопросами масштабирования и виртуализации. А еще он всегда внимательно изучает данные по исправлению ошибок (COE) и результаты анализа причин неудачи. Марк Брукер – обладатель докторской степени в области электроинженерии.