Блог Amazon Web Services
Надёжность, шаблон постоянной работы и чашка хорошего кофе
Оригинал статьи: Reliability, constant work, and a good cup of coffee, By Colm MacCárthaigh
Кофе и масштабируемость
Одна из моих любимых картин – «Ночные ястребы» Эдварда Хоппера. Несколько лет назад мне повезло увидеть её в Чикагском институте искусств. Поздняя ночь. Витрина открывает вид на трех посетителей. На одном углу барной стойки мужчина, сидящий спиной, на другом – пара: мужчина и женщина. За стойкой, ближе к одинокому мужчине, нагнулся бармен в белом фартуке; похоже он моет чашки. На заднем плане, справа от бармена, видны две огромных, на несколько десятков литров, кофеварки, рассчитанные на приготовление кофе сотнями чашек.
Такие кофеварки часто используют на больших мероприятиях: конференциях, свадьбах, съемочных площадках, и даже в офисах Amazon. Вы когда-нибудь задумывались зачем нужны такие большие ёмкости? В них всегда есть готовый кофе, потому что они постоянно выполняют один и тот же объём работы.
Опытный бариста готовит кофе по одной чашке. И каждая может стать шедевром. Но стоит ему получить больше заказов – например, приготовить 100 чашек – и сразу возникнет проблема. В час пик образуется длинная очередь. Многолитровые кофеварки, если не вдаваться в детали, работают иначе: их производительность не зависит от того, сколько человек и когда придут за кофе. В них всегда достаточное количество готового напитка. Три запоздавших посетителя или толпа в час пик –кофе хватит всем. Если описывать такие кофеварки языком компьютерных наук, можно сказать, что время их работы не зависит от объема входных данных. Они выполняют одинаковый объем работы независимо от того, сколько людей пришли за кофе. В нотации «О» большое это выглядит как O(1), а не O(N).
Прежде чем продолжить, позвольте ответить на пару возражений, которые, возможно, у вас возникли. Если вы проектируете системы – а раз вы читаете эту статью, вы, скорее всего, это так –вы, наверное, уже подумали: «Ну, на самом деле…». Во-первых, если опустошить кофеварку полностью, её придется заново заполнять, и людям придется подождать подольше. Именно поэтому выше я оговорился: «если не вдаваться в детали». Если вы когда-либо бывали на ежегодной конференции AWS re:Invent в Лас-Вегасе, вы, возможно, видели сотни кофеварок в столовой конференц-центра Sands Expo. Это наглядный пример масштаба, необходимого для удовлетворения потребностей десятков тысяч участников в кофеине.
Во-вторых, многие кофеварки оснащены нагревательными элементами и термостатами. Это значит, что по мере уменьшения объёма кофе внутри они потребляют меньше энергии. Когда нужно подогревать меньше кофе, они выполняют немного меньше работы. Таким образом, в утренний час пик, кофеварки на самом деле чуть более производительны. Способность сохранять или даже увеличивать производительность в момент возрастания нагрузки называеться антихрупкость.
Кофеваркам, с учётом описанных замечаний, не приходится выполнять больше работы только потому, что больше людей решили выпить кофе. Они служат отличным примером. Недороги, просты в использовании и обслуживании, и невероятно надёжны. Плюс, они не дают миру заснуть. Браво, скромным кофеваркам!
Компьютер – идеальный работник
В отличие от приготовленого вручную кофе, одной из замечательных особенностей компьютеров является постоянство: не нужно выбирать между качеством и количеством. Достаточно однажды написать программу, и компьютер будет повторять действия снова и снова. Каждый раз с одинаковым результатом. Конечно, чтобы программировать нужны знания, однако качество результата будет зависеть только от того, насколько качественно составлена программа. Если грамотно обучить его всем нюансам приготовления идеальной чашки кофе, компьютер приготовит миллионы идеальных чашек.
Безусловно, выполнение действия миллион раз потребует больше времени, чем сделать то же самое тысячи или сотни раз. Попросите компьютер сложить два плюс два миллион раз. Каждый раз результат будет четыре, просто это займет больше времени, чем если проделать это единожды. Когда мы эксплуатируем высоконадежные системы, вариативность является самой большой проблемой. Истинность этого утверждения наглядно подтверждается в ситуацииях, когда приходится сталкиваться с ростом нагрузки или изменением состояния системы, например из-за модификации настроек или необходимости реагировать на аварии вроде потери электропитания или сбоя сети. Избыточная нагрузка на систему, совпадающая с большим количеством изменений – худший момент, когда система может начать терять свою производительность. Снижение производительности означает, что очереди вырастают, как в кафе, где кофе готовит бариста. Однако, в отличие от очереди в кафе, очереди в системе могут привести к цепной реакции: система отвечает медленнее, пользователи повторяют запросы, система отвечает еще медленнее. Система губит саму себя.
Марк Брукер и Дэвид Яначек написали статью в Amazon Builder’s Library о том как правильно обрабатывать таймауты с использованием повторных попыток и задержек, чтобы избежать цепной реакции. Однако, даже если вы сделали все правильно, увеличение времени отклика останется проблемой. Замедление реакции во время обработки аварий и сбоев приводит к простоям.
Вот почему многие из наших самых надежных систем основываются на чрезвычайно простых, даже примитивных, но очень надежных шаблонах постоянной работы. Так же, как и кофеварки, эти подходы обладают тремя ключевыми свойствами. Во-первых, системы не масштабируются динамически при изменении нагрузки. Во-вторых, у них нет разных режимов работы – они выполняют одни и те же операции в любых условиях. В-третьих, единственная вариативность заключается в том, что они могут ограничивать объем выполняемой работы в момент наибольшей нагрузки, так как именно тогда важнее сохранить работоспособность системы вообще. Это и есть проявление антихрупкости.
Каждый раз, когда я упоминаю антихрупкость, мне напоминают, что ещё одним примером антихрупкого шаблона является кэш. Кэш сокращает время отклика, и, как правило, под нагрузкой отклик может оказаться даже быстрее. Однако, кэш работает в разных режимах. Когда он пуст, время отклика может значительно возрасти, что делает систему нестабильной. Хуже того, когда кэш перестает быть эффективным из-за слишком большой нагрузки, он может каскадом вызвать сбой источника кэшируемых данных, который может не справится с поступающими к нему запросами. Поначалу кэши кажутся антихрупкими, но на самом деле они увеличивают хрупкость при перегрузках. Поскольку сейчас мы не обсуждаем кэши, не буду углубляться в тему. Если хотите узнать больше об использование кэшей, прочитайте Мэтта Бринкли и Джеса Хабра, о, как построить действительно антихрупкий кэш.
Эта статья, впрочем, не столько о масштабировании раздачи кофе, сколько о том, как мы применяем шаблон постоянной работы в Amazon. Я приведу два примера, которые упрощены и несколько абстрагированы от реальных реализаций, в основном для того, чтобы не углубляться в некоторые механизмы и запатентованные технологии, на которых основаны другие функций. Рассматривайте эти примеры, как выжимку ключевых свойств шаблона постоянной работы.
Проверка работоспособности в Amazon Route 53
Трудно представить функцию более важную, чем проверка работоспособности. Если инстанс, сервер или зона доступности теряет питание или подключение к сети, проверка работоспособности выявляет это и обеспечивает перенаправление трафика в другое место. Проверка работоспособности интегрирована в Amazon Route 53, балансировщики Elastic Load Balancing и другие сервисы. В этой статье мы рассмотрим, как работает проверка работоспособности в Route 53, которая является одной из самых критичных. Не существует других способов восстановить работоспособность системы, кроме как с помощью DNS перенаправить трафик на работающие ресурсы.
С точки зрения пользователя, проверка работоспособности в Route 53 работает так: DNS-имя связывается с двумя и более записями (например, IP-адресами, принадлежащими сервису). Записи могут иметь разные «веса», или это могут быть две разные записи для основной и резервной конфигурации. При этом одна из записей будет иметь приоритет, пока связанный с ней сервер или сервис продолжают работать. Работоспособность проверяется с помощью теста, который настраивается для каждого варианта записи. В конфигурации теста, обычно, указывают IP-адрес сервера, чаще всего совпадающий с адресом в записи, порт, протокол, тайм-аут и так далее. Если вы используете сервисы Elastic Load Balancing, Amazon Relational Database Service или многие другие сервисы, которые используют Route 53 для обеспечения высокой доступности, эти параметры настраиваются автоматически.
Сервера, выполняющие проверки работоспособности в Route 53, распределены по различным регионам AWS. Это резервирование с избытком. Каждые несколько секунд десятки серверов отправляют запросы по заданным адресам и проверяют ответы. Затем эти ответы передаются меньшему набору серверов-агрегаторов. Они реализуют логику определяющую чувствительность проверок. Если один из десяти тестов во время последней проверки работоспособности оказался неудачным, еще не значит, что сервис перестал работать. Возможно, произошла случайная ошибка. Агрегаторы опираются на правила. Например, мы можем считать ресурс неработоспособным только в случае, если как минимум три отдельных теста оказались неудачными. Пользователи могут настроить эти параметры самостоятельно, агрегаторы будут следовать правилам, заданным для каждого ресурса.
Все что мы описали до сих пор – примеры применения шаблона постоянной работы. Независимо работоспособен ли ресурс, тестирующие сервера и сервера-агрегаторы выполняют одни и те же действия. Конечно, пользователи могут добавить проверки для новых серверов и адресов, и это немного увеличит объем работы, но об этом можно не беспокоиться.
Одна из причин, почему это не имеет большого значения, заключается в использовании сотовой архитектуры для серверов, выполняющих тесты, и серверов-агрегаторов. Мы измерили сколько тестов работоспособности может выполнять каждая сота, и всегда контролируем нагрузку на каждую из них. Если нагрузка приближается к пределу, мы добавляем соту серверов-тестировщиков или серверов-агрегаторов, в зависимости от потребностей.
Другая причина, и, вероятно, самое интересное в этой статье. Даже если настроено лишь несколько проверок работоспособности, сервера-тестировщики отправляют серверам-агрегаторам набор данных максимального размера. Например, если на сервере настроено только 10 проверок, он все равно будет каждый раз отправлять набор из 10 000 результатов, если это максимальное количество проверок, которые он может выполнять. Из них 9 990 будет заполнено пустыми записями. Это гарантирует, что нагрузка на сеть и объем работы, выполняемый агрегаторами, не будет увеличиваться с добавлением новых проверок. Таким образом устраняется гигантский источник вариативности.
Что еще более важно, даже если очень большое количество ресурсов одновременно перестанут выдерживать проверку работоспособности, скажем, в результате потери электропитания в зоне доступности, это никак не скажется ни на работе серверов-тестировщиков, ни на работе серверов-агрегаторов. Они продолжат делать то, что уже делают. На самом деле, если рассматривать общий объем работы выполняемый системой, он может, даже, оказаться меньше потому, что некоторые из серверов-тестировщиков сами могут оказаться в аварийной зоне доступности.
Двинемся дальше. Route 53 может проверять работоспособность ресурсов и агрегировать результаты используя шаблон постоянной работы. Однако само по себе это не приносит пользы. Нам нужно что-то делать с результатами этих проверок. И вот тут начинается самое интересное. Логично использовать результаты проверок для изменения записей DNS. Мы могли бы сравнить текущий результат проверки с предыдущим. Если статус изменился на «неработоспособный», использовать API, чтобы удалить все связанные записи из DNS. Если работоспособность восстановиться, добавить обратно. Или, чтобы избежать добавления и удаления записей, мы могли бы добавить флаг «активен», который будет устанавливаться или сниматься по необходимости.
Может показаться разумным рассматривать Route 53 как некую базу данных, но это не так. Во-первых, одна проверка работоспособности может быть связана с несколькими записями в DNS. Один и тот же IP-адрес может быть привязан к различным именам DNS. Неудачная проверка может потребовать изменения как одной, так и сотен записей. Дальше — больше, в маловероятном случае, если зона доступности потеряет питание, десятки тысяч проверок работоспособности могут оказаться неудачными одновременно. Потребуются миллионы изменений в DNS. Это займет значительное время, и это не лучшее действие в момент пропадания электропитания.
Архитектура Route 53 устроена иначе. Каждые несколько секунд сервера-агрегаторы отправляют таблицу фиксированного размера с результатами проверок работоспособности на сервера DNS. Сервер DNS получает таблицу и сохраняет её в памяти практически без изменений. Это типичный образец использования шаблона постоянной работы. Каждые несколько секунд получаем таблицу и просто сохраняем её в памяти. Почему Route 53 отправляет данные на сервера DNS, вместо того чтобы делать наоборот? Серверов DNS больше, чем серверов-агрегаторов. Если вам интересно, как была придумана такая архитектура прочитайте статью Джо Магеррамова о том, как возложить управление на сервис меньшего масштаба.
Когда, сервер DNS в Route 53 получает запрос, он делает выборку всех возможных вариантов ответа. Затем, прямо в процессе обработки запроса, он сверяет эти ответы со статусом работоспособности из таблицы в памяти. Если статус потенциального ответа «работоспособен», он считается подходящим. Однако даже если первый же подходящий вариант ответа «работоспособен», сервер проверят и остальные варианты. Такой подход гарантирует, что даже если состояние работоспособности измениться, DNS-сервер продолжит выполнять тот же объем работы, как и раньше. Время, затрачиваемое на выборку и проверку ответов не увеличиться.
На это можно посмотреть так: сервера DNS просто не заботятся о том сколько проверок работоспособности окажутся удачными, неудачными или изменят состояние, код все время будет выполнять одни и те же действия. Не существует различных режимов работы. Нам не нужно делать большого количества изменений, не нужно переключаться в режим чего-то вроде «когда зона доступности неработоспособна». Разница только в ответах, которые выберет Route 53. Обращение происходит к тому же объему памяти и за то же время. Это делает процесс чрезвычайно надежным.
Обработка изменения конфигурации
Другим приложением, которое требует исключительной надёжности, является изменение конфигурации базовых компонентов AWS, например балансировщика Network Load Balancer. Когда пользователь изменяет конфигурацию балансировщика, например, добавляет новый инстанс или контейнер в качестве цели, это, обычно, критично и срочно. Возможно, приложение столкнулось со всплеском трафика и необходимо быстро увеличить ёмкость. Под капотом балансировщики сетевой нагрузки используют AWS Hyperplane, внутренний сервис, вживлённый в сеть Amazon Elastic Compute Cloud (EC2). AWS Hyperplane мог бы обрабатывать изменения настроек с помощью некоего процесса. Например, каждый раз, когда пользователь вносит изменения, создается событие, которое запускает отправку изменений на все устройства AWS Hyperplane, где они должны быть применены. Устройства применяют изменения.
Проблема такого подхода в том, что при большом количестве изменений, происходящих одновременно, реакция системы, скорее всего, замедлится. Больше изменений означает больше нагрузки. Когда система не отвечает вовремя, пользователи, естественно, повторяют попытку, что еще больше замедляет работу системы. Это не то, чего мы хотим.
Решение удивительно простое. Вместо того, чтобы генерировать события, AWS Hyperplane собирает все изменения в конфигурационный файл, хранимый в Amazon S3. Это происходит в момент, когда пользователь вносит изменения. Далее, вместо того чтобы получать изменения от стороннего процесса, устройства AWS Hyperplane скачивают этот файл с Amazon S3 каждые несколько секунд. После чего они обрабатывают и применяют конфигурацию из файла. Цикл повторяется, даже если изменений не произошло. Даже если она полностью идентична той, что была загружена в предыдущий раз, устройства всё равно загружают и применяют вновь загруженную копию. Фактически, система всегда получает и применяет изменения для максимально возможного количества устройств. Независимо от того, изменилась ли конфигурация одного балансировщика или сотни, она ведёт себя одинаково.
Вы, вероятно, уже догадались, что файл конфигурации также имеет максимальный размер с самого начала. Даже когда мы запускаем новый регион и задействованы лишь несколько балансировщиков, файл конфигурации будет таким же большим, каким он вообще может быть. В нём есть пустые «слоты» конфигурации, ожидающие заполнения значениями пользователя. Однако с точки зрения работы AWS Hyperplane эти слоты всегда заполнены.
Поскольку AWS Hyperplane является высоконадежной системой, в её дизайн заложена антихрупкость. Если устройства AWS Hyperplane выходят из строя, общий объем работы, выполняемой системой в целом, сокращается, а не увеличивается. Вместо того чтобы повторять попытки доставить изменения на неработающее устройство, количество обращений к Amazon S3 сокращается.
Помимо своей просты и надёжности, этот подход весьма экономичен. Хранение файла в Amazon S3 и его цикличная загрузка, даже сотнями устройств, обходиться гораздо дешевле, чем затраты на разработку и альтернативные издержки на создание более сложного решения.
Шаблон постоянной работы и самовосстановление
Есть еще одно интересное свойство архитектур на основе шаблона постоянной работы, о котором я еще не упоминал. Такие архитектуры, как правило, восстанавливают работоспособность самостоятельно естественным путем и способны справляться с различными проблемами без стороннего вмешательства. Например, предположим, что файл конфигурации был каким-то образом поврежден в процессе изменения: ошибочно обнулен из-за проблемы с сетью. На следующей итерации эта проблема будет устранена. Или, скажем, DNS-сервер полностью пропустил обновление. Он получит следующее обновление, не накапливая очередь изменений, которые необходимо применить. Архитектуры на основе шаблона постоянной работы всегда начинают работу с «чистого листа», они всегда работают в режиме «исправить все».
В отличие от них, системы, основанные на процессах, обычно запускаются в следствие какого-то события, то есть всегда должно присутствовать что-то, например, изменение конфигурации или состояния системы, что приводит к запуску процесса. Сначала изменения нужно обнаружить, затем запустить выполнить соответствующие действия, и всё это должно происходить в строгой последовательности. Требуется сложная логика для обработки случаев, когда какие-то действия завершились неуспешно, или требуют исправления из-за ошибок в конфигурации. Система может начать отставать, накапливая очередь изменений. Другими словами, архитектуры основанные на процессах не умеют самостоятельно исправлять восстанавливаться после ошибок, вы должны реализовывать восстановление самостоятельно.
Архитектура и управляемость
Ранее я упоминал о нотации «О» большое и о том, что системы построенные на основе шаблона постоянной работы относятся к классу О(1). Важно помнить, что O(1) не означает, что система или алгоритм выполняет только одну операцию. Это означает, что количество операций остаётся независимо от объема входных данных. Обозначение O(C) [от Сonstant — постоянный] выглядело бы лучше. И система изменения конфигурации Network Load Balancer, и система проверки работоспособности в Route 53 на самом деле выполняют тысячи операций для каждого действия или на каждой итерации цикла, который они повторяют. Но количество этих операций не меняется при изменении результата проверки работоспособности или изменении конфигураций балансировщика пользователем. В этом и заключается суть. Они похожи на кофеварки, которые наполнены кофе на сотни чашек, независимо от количества гостей.
В реальном применение шаблона постоянной работы приводит к потерям в виде отходов. Если из большой кофеварки выпьют лишь несколько чашек, остальное уйдет в канализацию. Вы зря потратите электричество на нагрев, усилия на подготовку и доставку воды, а заплатите за лишний кофе. Эти затраты будут незначительными и приемлемыми для кафе или при обслуживании мероприятия. Более того, затраты на приготовление кофе индивидуально могут оказаться даже выше, потому что теряется эффект масштаба.
Для большинства систем, которые реализуют изменение конфигурации или рассылают трафик проверки работоспособности, проблема отходов не возникает. Разница в расходе электроэнергии на одну проверку работоспособности и 10 000 проверок незаметна. Шаблон постоянной работы может даже оказаться более экономически эффективным, так как не требует реализации повторов и оркестрации.
В то же время есть системы, для которых такой шаблон не применим. Если ваш веб-сайт требует 100 серверов в момент пика нагрузки, можно держать эти сервера. Это, безусловно, уменьшит вариативность, и будет соответствовать шаблону постоянной работы, но будет расточительно. Для таких систем эластичное масштабирование подойдет лучше, потому что экономия велика. Использовать, например, вдвое меньше серверов в обычное время будет нормальным. Постоянное масштабирование может выявить какие-то проблемы, однако экономия принесет больше пользы как пользователям, так и планете в целом.
Достоинства простого дизайна
В этой статье я говорю про упрощение. Кофейники собраны из небольшого количества деталей.Это один из способов сделать систему проще, но я хочу сказать о другом. Ошибочно опираться на количество компонент. Цирковой уницикл имеет одно колесо, но ездить на нем гораздо сложнее. Это не то чего мы хотим добиться. Хорошая архитектура должна быть способна выдерживать перегрузки и справляться с авариями. Как правило, «естественный отбор» постепенно отсеивает архитектуры, состоящие из излишне большого или наоборот недостаточного количества компонент, да и просто непрактичные.
Когда я говорю о простой архитектуре, я имею в виду архитектуру, которую легко понять, реализовать и эксплуатировать. Если архитектура понятна команде, которая не участвовала в её разработке, это признак качественной архитектуры. В AWS шаблон постоянной работы использован многократно. Вы будете удивлены, узнак как много систем можно упростить применяя подход «примени всю конфигурацию заново» в цикле.