Алгоритмы описывают реальную жизнь

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

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

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

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

Двойственная природа очередей

Очереди – мощный инструмент для создания надежных асинхронных систем. Благодаря очередям одна система может получать сообщение от другой и хранить до полной его обработки даже в случае длительного отключения электропитания, сбоев серверов или проблем с зависимыми системами. Сообщения, находящиеся в очередях, не удаляются в случае сбоя, а отправляются повторно до тех пор, пока не будут успешно обработаны. В конечном счете очередь повышает надежность и доступность системы, но при этом периодически увеличиваются задержки из-за повторных попыток.
 
Amazon создает множество асинхронных систем с использованием очередей. В некоторых таких системах рабочие процессы могут выполняться долго, требовать определенных действий в реальном мире. Примером может служить выполнение заказов, размещенных на сайте amazon.com. В других системах координируются этапы, для чего тоже может понадобиться значительное время. Например, Amazon RDS запрашивает инстансы EC2, ждет их запуска и затем настраивает базы данных. В некоторых системах используются преимущества пакетной обработки. Например, системы, используемые для получения метрик и журналов CloudWatch, извлекают пакет данных, которые затем объединяют и разделяют на фрагменты.
 
Если преимущества очередей для асинхронной обработки сообщений понятны, сопутствующие риски не столь заметны. С годами мы обнаружили, что очередь, предназначенная для повышения доступности, может создать проблемы. Фактически она может значительно увеличить время восстановления после отключения электропитания.
 
Если в системе, основанной на очередях, остановится обработка, но сообщения будут продолжать поступать, накопится длинная очередь необработанных сообщений, и для них понадобится дополнительное время. Работа может завершиться настолько поздно, что результаты уже не будут важны. По сути, это снизит доступность, а именно ради высокой доступности и создавались очереди.
 
Иными словами, система, основанная на очередях, работает в двух режимах, у нее двухрежимное поведение. Когда накопления в очереди нет, система работает в быстром режиме (с малыми задержками). Если же из-за сбоя или неожиданной схемы загрузки скорость поступления превысит скорость обработки, система быстро перейдет в неблагоприятный режим работы. В таком режиме сквозная задержка все увеличивается. Может понадобиться много времени на устранение накопления в очереди, чтобы система вернулась в быстрый режим.

Системы, основанные на очередях

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

С помощью AWS Lambda можно выгрузить код функции и затем вызвать функции одним из двух способов:

• синхронно – функция возвращает выходные данные в HTTP-ответе;
• асинхронно – HTTP-ответ возвращается немедленно, а функция выполняется (в том числе повторно) без вашего вмешательства.

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

С помощью AWS IoT Core можно подключить устройства и приложения, а также подписать их на темы сообщений PubSub. Когда устройство или приложение публикует сообщение, приложения с соответствующими подписками получают собственную копию сообщения. В основном такая передача сообщений в PubSub выполняется асинхронно, поскольку ограниченное устройство IoT не может тратить свои ограниченные ресурсы, ожидая, пока все подписанные устройства, приложения и системы получат копию. Это очень важно, так как определенное устройство может работать в автономном режиме, когда другое устройство, на которое оно подписано, опубликует сообщение. Когда устройство, которое работало в автономном режиме, снова подключится к сети, оно должно будет сначала вернуться в обычный режим работы, а затем получить сообщения (сведения о настройке системы с помощью кода для управления доставкой сообщений после повторного подключения см. в разделе Постоянные сеансы MQTT руководства разработчика AWS IoT). Есть несколько вариантов сохранения и необходимой для этого асинхронной обработки без вашего вмешательства.

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

Сбои асинхронных систем

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

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

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

Способы измерения доступности и задержек

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

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

Мы также измеряем доступность с учетом очереди сообщений, которые не могут быть обработаны (DLQ). Если сообщение не доставляется после определенного числа повторных попыток, оно удаляется или помещается в DLQ. DLQ – это просто отдельная очередь, используемая для хранения сообщений, которые невозможно обработать для дальнейшего расследования и вмешательства. Коэффициент сообщений, которые были удалены или помещены в DLQ, – хороший показатель доступности, но проблема будет обнаружена слишком поздно. Хотя оповещать об объемах DLQ целесообразно, сведения о DLQ поступят слишком поздно, чтобы с их помощью можно было выявлять проблемы.

Как насчет задержки? Задержка на стороне производителя отражает задержку сервиса очередей. Поэтому мы больше сосредоточены на измерении возраста сообщений, находящихся в очереди. Так быстро выявляются случаи, когда работа систем медленна, часто приводит к ошибкам и повторным попыткам. Такие системы, как SQS, отмечают время поступления сообщения в очередь. Благодаря временной отметке можно регистрировать и генерировать метрики отставания систем при каждом удалении сообщения из очереди.

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

В сервисе AWS IoT используется следующая метрика: AgeOfFirstAttempt. Из измеренного значения вычитается время добавления сообщения в очередь, но только если это была первая попытка AWS IoT доставить сообщение устройству. То есть при резервном копировании данных устройств мы получаем точную метрику, которая не искажается, когда устройство повторно отправляет сообщения или ставит их в очередь. Чтобы повысить точность, мы добавили вторую метрику – AgeOfFirstSubscriberFirstAttempt. В таких системах PubSub, как AWS IoT, можно подписать практически неограниченное количество устройств или приложений на определенную тему, поэтому задержка при отправке сообщения на миллион устройств будет больше, чем при отправке на одно. Для получения стабильной метрики мы запускаем метрику таймера при первой попытке опубликовать сообщение для первого подписчика на ту или иную тему. Затем с помощью других метрик мы замеряем, как справляется система с публикацией остальных сообщений.

Метрика AgeOfFirstAttempt заблаговременно предупреждает о проблемах масштаба всей системы по большей части благодаря тому, что отфильтровываются помехи от устройств, читающих сообщения медленнее. Стоит упомянуть, что в таких системах, как AWS IoT, используются многие другие метрики. Когда доступны все метрики, связанные с задержкой, в Amazon обычно рассматривают отдельно задержку при первых попытках и задержку при повторных попытках.

Измерение задержки и доступности в случае асинхронных систем – достаточно сложный процесс. Сложна и отладка, поскольку запросы перемещаются между серверами и могут задерживаться за пределами каждой системы. Для упрощения распределенного отслеживания мы передаем идентификатор запроса в сообщениях, поставленных в очередь, чтобы можно было видеть всю картину. Для этой же цели мы используем такие системы, как X-Ray.

Большие очереди в мультитенантных асинхронных системах

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

Сервисы AWS не предоставляют свои внутренние очереди непосредственно вызывающим сторонам для добавления сообщений. Вместо этого используются упрощенные API для проверки подлинности вызывающих сторон и добавления сведений о них в каждое сообщение перед добавлением в очередь. Описанная ранее архитектура Lambda работает аналогичным образом: при асинхронном вызове функции Lambda помещает сообщение в собственную очередь Lambda и сразу же его возвращает, но не предоставляет внутренние очереди Lambda напрямую.

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

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

Давайте рассмотрим такой пример. Что произойдет, если в асинхронной системе будет недостаточно встроенных средств защиты от соседних помех? Если один клиент системы резко увеличит свой нерегулируемый трафик и создаст большую очередь, влияющую на всю систему, может пройти порядка 30 минут, прежде чем оператор сможет выявить и устранить возникшую проблему. За эти 30 минут на стороне производителя в системе может образоваться большая очередь из всех сообщений. Но если объем сообщений, находящихся в очереди, в 10 раз больше, чем можно обработать с помощью ресурсов на стороне потребителя, то система сможет обработать очередь и восстановиться только через 300 минут. Даже короткие пиковые нагрузки могут привести к многочасовому восстановлению и многочасовым простоям.

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

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

Стратегия Amazon заключается в создании отказоустойчивых мультитенантных асинхронных систем

В системах Amazon используется несколько схем, благодаря которым мультитенантные асинхронные системы устойчивы к изменениям рабочих нагрузок. Технологий много, но и систем, используемых в Amazon, немало. У каждой из них есть свои требования к работоспособности и надежности. В следующем разделе я опишу некоторые схемы, используемые в нашей компании, а также расскажу, что применяют клиенты AWS в собственных системах.

Разделение рабочих нагрузок на отдельные очереди

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

Сегментирование в произвольном порядке

AWS Lambda – это пример системы, в которой опрос каждой отдельной очереди для каждого клиента Lambda привел бы к чрезмерным расходам. Однако наличие одной очереди может привести к определенным проблемам, освещенным в настоящей статье. Итак, вместо использования одной очереди AWS Lambda выделяет фиксированное количество очередей и хэширует данные каждого клиента, предоставляя ему лишь несколько очередей. Перед добавлением сообщения в очередь система проверяет, какая из целевых очередей содержит меньше всего сообщений, и помещает сообщение в нее. Когда рабочая нагрузка одного клиента увеличивается, это приводит к увеличению тех очередей, которые были ему выделены, но другие рабочие нагрузки будут автоматически поступать в другие очереди. Для волшебной изоляции ресурсов не нужно много очередей. Это лишь одно из многих средств защиты, встроенных в Lambda, но такая технология применяется и в других сервисах Amazon.

Отведение чрезмерного трафика в отдельную очередь

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

Отведение старого трафика в отдельную очередь

Старый трафик можно отводить в отдельную очередь так же, как и чрезмерный. При выведении сообщения из очереди можно проверить время его поступления. Можно не просто зарегистрировать возраст, а еще и использовать эту информацию, чтобы определить, нужно ли перемещать сообщение в большую очередь, которая будет обработана только после того, как мы разберемся с «живой» очередью. Если возникнет пиковая нагрузка в месте поступления большого количества данных и мы это увидим, можно будет отвести волну трафика в другую очередь. Понадобится лишь время на его удаление из одной очереди и добавление в другую. Таким образом ресурсы потребителя освободятся, и новые сообщения обработаются быстрее, чем в случае последовательного продвижения по очереди с накоплением. Это один из способов приблизительного упорядочения по принципу LIFO.

Удаление старых сообщений (время существования сообщения)

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

Ограничение потоков (и других ресурсов) для каждой рабочей нагрузки

Мы проектируем асинхронные системы (как и синхронные) так, чтобы одна рабочая нагрузка не использовала больше потоков, чем для нее выделено. Мы еще не рассмотрели одну особенность AWS IoT – обработчик правил. Клиент может настроить AWS IoT таким образом, чтобы сообщения перенаправлялись с его устройств на принадлежащий ему кластер Amazon Elasticsearch, Kinesis Stream и т. д. Если задержка таких ресурсов, принадлежащих клиенту, станет низкой, но скорость поступления сообщений останется постоянной, объем параллельного выполнения в системе возрастет. А поскольку объем параллельного выполнения, который может обработать система, ограничено в любой момент времени, обработчик правил не допустит, чтобы одна рабочая нагрузка потребляла больше ресурсов, связанных с параллельным выполнением, чем ей выделено.

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

Обработчик правил предотвращает подобную ситуацию с помощью нескольких технологий. Он использует ввод/вывод без блокирования, чтобы пул потоков не исчерпался, хотя объем работ для определенного сервера ограничен и другими параметрами (например, памятью и файловыми дескрипторами, когда клиент перебирает подключения, а время ожидания зависимости истекает). Вторая защита при параллельном выполнении, которую можно использовать, – это семафор, измеряющий и ограничивающий объем такого выполнения для одной рабочей нагрузки в любой момент времени. В обработчике правил также обеспечивается равнодоступность путем ограничения на основе скорости. Но поскольку рабочие нагрузки обычно меняются с течением времени, обработчик правил автоматически масштабирует ограничения соответствующим образом. Работа обработчика правил зависит от очередей, поэтому он служит буфером между устройствами IoT и автоматическим масштабированием ресурсов, применяя защитные ограничения без вашего вмешательства.

В разных сервисах Amazon применяются отдельные пулы потоков для каждой рабочей нагрузки, чтобы одна рабочая нагрузка не могла потреблять все доступные потоки. Кроме того, для каждой рабочей нагрузки применяется AtomicInteger, чтобы ограничить допустимое параллельное выполнение, а также регулирование нагрузки с учетом скорости, чтобы изолировать ресурсы с учетом скорости.

Замедление поступления запросов

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

Если предоставлять одну очередь SQS для нескольких рабочих нагрузок, применять такой подход будет сложнее. Хотя существует API SQS, который возвращает количество сообщений, находящихся в очереди, нет API, который возвращает количество сообщений в очереди с определенным атрибутом. Мы по-прежнему могли бы измерять глубину очереди и замедлять поступление запросов соответствующим образом, но этот же подход применялся бы и к рабочим нагрузкам других клиентов в той же очереди, а это несправедливо. В других системах, например Amazon MQ, применяется более детальное отображение большой очереди.

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

Использование очередей с задержкой для откладывания работ

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

Сокращение числа промежуточных сообщений

В некоторых сервисах очередей, например SQS, ограничивается число промежуточных сообщений, которые можно доставлять потребителю очереди. Это число сообщений, обрабатываемых парком потребителя одновременно. Оно отличается от числа сообщений, которые могут находиться в очереди (для них нет определенного лимита). Число может быть увеличено, если система выводит сообщения из очереди, но затем не может их удалить. Например, мы видели ошибки, при которых код не перехватывал исключение во время обработки сообщения и «забывал» удалить сообщение. В таких случаях сообщение остается в статусе промежуточного с точки зрения SQS в течение периода VisibilityTimeout для этого сообщения. Когда мы разрабатываем обработку ошибок и стратегию поведения при перегрузках, мы учитываем эти ограничения и стремимся перемещать избыточные сообщения в другую очередь вместо того, чтобы оставлять их видимыми.

В очередях FIFO SQS применяется аналогичное, но более тонкое ограничение. С помощью FIFO SQS системы получают сообщения по порядку для отдельной группы сообщений, при этом сообщения других групп могут обрабатываться в любом порядке. Итак, если мы работаем с небольшим накоплением в одной группе сообщений, то продолжаем обрабатывать сообщения в других группах. Однако FIFO SQS опрашивает только самые последние необработанные 20 тысяч сообщений. Поэтому если в подгруппе групп сообщений окажется больше 20 тысяч необработанных сообщений, другие группы новых сообщений не получат достаточно ресурсов.

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

Сообщения, которые не могут быть обработаны, иногда приводят к перегрузке системы. Если система помещает в очередь сообщение, которое не может быть обработано (например, потому что оно запускает периферийный контроль ввода), SQS поможет переместить эти сообщения автоматически в отдельную очередь с помощью компонента очереди необработанных сообщений (DLQ). Мы оповещаем о том, содержит ли эта очередь сообщения, так как их наличие говорит об ошибке, которую нужно исправить. Преимущество DLQ заключается в возможности повторно обработать сообщения после исправления ошибки.

Обеспечение дополнительного буфера в потоках опросов на каждую рабочую нагрузку

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

Периодический контроль сообщений, которые долго обрабатываются

Когда система обрабатывает сообщение SQS, SQS предоставляет ей определенное время, в течение которого нужно завершить обработку и доставить сообщение другому потребителю для повторной попытки. В противном случае система аварийно завершает работу. Если код продолжает выполняться и «забывает» о конечном сроке, то же самое сообщение может отправляться несколько раз параллельно. Хотя первый процессор по-прежнему работает над сообщением после тайм-аута, второй процессор принимает то же самое сообщение и точно так же продолжает над ним работать после тайм-аута. Затем подключается третий процессор и т. д. Это может привести к каскадному снижению мощностей, поэтому мы внедрили определенную логику: останавливается обработка сообщений по истечении тайм-аута либо продолжается периодический контроль, а в SQS поступает информация о том, что обработка продолжается. Такая концепция похожа на аренду при выборе лидера.

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

Планирование отладки ошибок на разных хостах

Понять, как происходят сбои в распределенной системе, довольно сложно. В статье, посвященной инструментарию, описаны некоторые наши подходы для оснащения асинхронных систем инструментами, которые позволяют как периодически записывать глубину очередей, так и распространять «идентификаторы отслеживания» и выполнить интеграцию с X-Ray. Если же в наших системах применяется сложный асинхронных рабочий процесс, выходящий за рамки обычной очереди SQS, мы часто используем другой сервис, например Step Functions, отображающий процесс и упрощающий отладку в распределенной системе.

Выводы

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

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

Дополнительные сведения

Теория очередей.
Закон Литтла.
Закон Амдала.
• Little, A Proof for the Queuing Formula: L = λW, Case Western, 1961
• McKenney, Stochastic Fairness Queuing, IBM, 1990
• Nichols and Jacobson, Controlling Queue Delay, PARC, 2011

Об авторе

Дэвид Янацек работает старшим главным инженером в AWS Lambda. Дэвид разрабатывает программное обеспечение в Amazon с 2006 года, раньше работал над Amazon DynamoDB и AWS IoT, а также внутренними платформами веб-сервисов и системами автоматизации операций парка. Одно из любимых занятий Дэвида – анализ журналов и тщательная проверка операционных показателей. Таким образом он ищет способы сделать работу систем беспроблемной.

Алгоритм выбора лидера в распределенных системах Инструментирование распределенных систем для операционного контроля