Я уверен, что с вами такого никогда не случалось, но попробуйте представить. Ваш дежурный телефон только что получил сообщение: Сайт недоступен! Вы открываете панель инструментов Nagios, и вас встречает красное море. Все серверы Apache не отвечают. Соединения с базой данных исчерпаны. Балансировщики нагрузки кричат, что у них нет работоспособных серверных частей. Все остальное выглядит как мертвая тишина. Все начинают строить догадки, что происходит.

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

Руководитель группы: Кто-то что-то развернул?

Разработчик: Последний раз мы развертывали сайт четыре часа назад. Вероятно, это база данных, я видел предупреждение о том, что пул соединений исчерпан, позвоните администратору базы данных!

DBA: База данных в порядке. Количество обычных открытых подключений от серверов приложений всего в 10 раз больше, но это ничего не делает. Я сделал одну ошибку два года назад, и теперь вы всегда вините базу данных. Может это сеть?

Сетевой администратор: Если бы это была сеть, ничего бы не сработало. Но трафика почти нет. Это должно быть что-то в коде. А может балансировщики нагрузки сошли с ума, тут написано, что они оффлайн?

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

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

… 10 минут подобных разговоров пролетают быстро…

SRE: Это, наверное, ничего, но вы знаете тот виджет, который у нас есть на странице продукта? Тот, который показывает, сколько людей в данный момент просматривают эту страницу? У нас есть микросервис для этого, верно? Экземпляр Redis, который содержит для него данные, выглядит так, будто его нагрузка равна 120. Наверное, это нехорошо, верно? Но, конечно же, это не может быть причиной того, что весь сайт не работает, верно? Я имею в виду, что виджет красив, но не критичен для работы сайта.

Dev: Конечно, если звонок не работает, мы просто не отображаем его, и все работает нормально. Мы планировали это. Этого не может быть.

SRE: Видите, в том-то и дело. Сервис работает, но ответ занимает 10 секунд. Но у вас, ребята, очень короткие тайм-ауты, когда вы вызываете службу на странице продукта, верно?

Дев: …

СРЭ: Верно?!

Как мы это пропустили?

Может быть, вы не сталкивались с чем-то подобным, но я сталкивался. И больше раз, чем я хотел бы признать. Несмотря на то, что тайм-ауты являются известным методом обеспечения устойчивости с незапамятных времен, они (или, точнее, их отсутствие) являются наиболее распространенным источником простоев в любой достаточно сложной системе, которую я видел. Этому есть несколько причин.

Безумные значения по умолчанию. Большинство сетевых вызовов и примитивов имеют встроенные тайм-ауты. Но эти тайм-ауты имеют значения по умолчанию, установленные в прошлом тысячелетии. Возьмем curl, самую популярную библиотеку, используемую для HTTP-вызовов. Время ожидания соединения составляет 300 секунд (5 минут). И общее время ожидания запроса равно 0, что означает, что время ожидания никогда не истекает. Вы должны сознательно установить его на что-то вменяемое самостоятельно. Такая же ситуация с большинством тайм-аутов в стеке TCP/IP и во многих библиотеках. На практике значения по умолчанию обычно настолько высоки, что кажется, что тайм-аута вообще не было.

Несовершенные ментальные модели. Когда мы рассуждаем о сложной системе, мы используем ментальную модель. В идеальном мире у всех была бы одна и та же ментальная модель, которая точно воспроизводила бы реальную вещь. О, и у нас также была бы красивая современная схема Visio такой системы, напечатанная на каждой стене. Увы, мы живем не в идеальном мире. Документация обычно разбросана по вики и файлам README в репозитории, если она вообще существует. Все работают с несовершенной ментальной моделью, и от стажа работы зависит, насколько модель удалена от реальной вещи. Большинство людей не будут знать о каждом движущемся элементе системы, не говоря уже о том, как они взаимодействуют. В такой ситуации они цепляются за то, что знают, и ищут там проблемы. При полном крахе проблемы проявляются повсюду, и трудно отличить причину от следствия.

Сложное тестирование. Наиболее часто используемые методы тестирования не охватывают случаи, когда вызов, который обычно занимает 10 мс, неожиданно занимает 10 с. Это почти невозможно покрыть модульным тестом, и даже интеграционные тесты обычно имеют дело с вызовом, который либо проходит нормально, либо полностью завершается с ошибкой. Мне еще предстоит увидеть инструмент статического анализа, который бы сообщал вам «Вы используете вызов curl, но не установили тайм-аут для соединения» (я был бы более чем счастлив, если бы кто-то мог меня поправить). Требуется ручной процесс, чтобы проверить все места на наличие внешних вызовов и убедиться, что установлены разумные тайм-ауты.

Будь агрессивным

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

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

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

or

«У нас есть один клиент, у которого 1000 товаров в его списке желаний, и если мы уменьшим время ожидания вызова службы списка желаний до 100 мс, список желаний перестанет работать для него».

Вы поняли идею. Все они вроде как действительны, но просто показывают плохой дизайн или проблемы с емкостью.

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

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

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

Бюджет производительности

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

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

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

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

Предполагая, что вы используете кэширование для какого-либо вызова службы, вы можете сделать еще одну вещь. Типичный сценарий таков: вы получаете какое-то значение от службы, вычисление которого обходится дорого, и сохраняете его некоторое время в каком-то кеше. Это может быть memcached, Redis, не имеет значения. Вы устанавливаете время, на которое вы хотите кэшировать его, в качестве тайм-аута или истечения срока действия ключа. Таким образом, вы сначала просматриваете кеш, если он есть, вы используете значение, если нет, вы вызываете службу, чтобы получить свежее значение. Но вы можете быть умнее этого. Вы можете сделать время истечения срока действия частью данных, которые вы храните в кеше, и установить фактический срок действия ключа на более длительный период, скажем, в 3 раза больше желаемого периода кеша. Это позволяет вам использовать эти немного устаревшие данные, если вызов службы завершается сбоем или истекает время ожидания. Это может не соответствовать вашему стандарту свежести, но в зависимости от вашего варианта использования все же лучше, чем полный провал. Это особенно полезно в тех случаях, когда вам абсолютно необходимы данные, а вызов службы стоит дорого, поэтому у вас нет времени в вашем бюджете на повторную попытку, если время ожидания истекло.

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

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

Примите хаос

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

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

Это по-прежнему не мешает людям забывать вставлять тайм-ауты или позволять им устанавливать их слишком долго. Однако мы можем использовать технику из области инженерии хаоса.

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

В частности, мы будем случайным образом вводить задержки в наш ответ. На самом деле это очень просто и тривиально реализовать. Как специалисту по сопровождению службы, все, что вам нужно сделать, это поместить sleep() вызов в вашу службу и вызывать его случайным образом с заданной вероятностью. Допустим, обычно ваш сервис отвечает за 100 мс. Теперь в 0,01% (1 из 10000) вызовов вы делаете паузу на 5 секунд и, таким образом, продолжительность вызова составляет 5100 мс. Я предлагаю вам сделать это, а затем подождать. Кроме того, сейчас самое время создать панель мониторинга, отображающую количество таких событий. Или, может быть, красивая гистограмма продолжительности звонков, хм?

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

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

Я предлагаю настроить вероятность отказа для каждой среды. Начните с введения его в тест. Увеличьте частоту сбоев до 10% или более и увеличьте задержку, потому что обычно среды тестирования не получают такого большого трафика, и люди привыкли к тому, что в тестах все работает не так гладко. Вы должны действительно с треском провалиться, чтобы кто-то это заметил. Как только вы поверите, что раскрыли все, пришло время активизировать свою игру и ввести недостатки в производство. Очевидно, вы хотите, чтобы это происходило гораздо реже и в идеале только в рабочее время, когда все свежие и готовы ответить. О, и скажите людям, что вы делаете это, пожалуйста.

Закрытие

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

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

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