Наверх ▲

Архитектура SEDA (Staged Event Driven Architecture) – ключ к построению надёжных и высоконагруженных сервисов

Алексей Рагозин Алексей Рагозин Специализируется на разработке высоконагруженных распределённых систем на платформе Java. Более чем за 10 лет работы в индустрии он получил опыт разработки информационных систем в таких отраслях, как финансы, телеком, E-commerce и здравоохранение.
View more presentations from rit2010.

Алексей Рагозин: Я работаю в Grid Dynamics и хотел бы продемонстрировать вам еще один паттерн для реализации асинхронного программирования на серверной стороне.

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

Итак, архитектура SEDA.

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

Stage Event Driven Architecture. Этот термин впервые появился в 2000-м году, хотя идеи возникли и стали применяться на практике намного раньше. Это является некоторым компромиссом между подходом «1 поток или процесс - на запрос» и асинхронным программированием, когда у нас есть один event-despatch wop и огромный switch, если такой-то callback, вызываем эту процедуру и т.д.

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

Терминология SEDA. Мы имеем следующие понятия.

Ступень (stage) — это некий черный ящик, внутрь которого попадают произвольные сообщения ("ивенты") и где происходит процесс обработки, после чего наружу тоже выходят "ивенты". Между ступенями они передаются посредством очередей. Там они могут подвергаться буферизации, а очередь позволяет нам обеспечивать асинхронность работы системы.

Event — это произвольное сообщение. Обычно контекст запроса, обрабатываемый в SEDA-системе, инкапсулируется в эти "ивенты", а те в свою очередь передаются между ступенями.

Схема обработки выглядит приблизительно следующим образом.

На диаграмме зеленые квадраты — это ступени. У них своя логика. Стрелочки — это очереди. Соответственно, по ним "ивенты" перемещаюстя с одной ступени на другую.

Запрос поступает во входную очередь, инициируя обработку ступени. Далее происходит генерация новых "ивентов", которые тоже обрабатываются. И так до тех пор, пока обработка не завершится в финальном блоке. Что касается езультата, то он направляется в выходную ступень.

Описанную последовательность действий (с момента поступления запроса во входную очередь до момента поступления результата в выходную очередь) можно назвать SEDA-транзакцией, т.е., обработкой запроса.

На первый взгляд SEDA-транзакция кажется довольно сложной, поскольку "ивенты" обрабатываются на каждой ступени. При это процесс должен протекать асинхронно. Ответ на вопрос о простоте подобного подхода далеко не очевиден.

Он проще, как правило, потому, что все ступени можно отнести к четырем основным типам. Благодаря комбинации этих типов, можно реализовать до 80% требуемого функционала.

Первая ступень — это процессор. На вход приходит один "ивент", на выходе — тоже один ивент. У нас всегда соотношение один к одному. Внутри может происходить любая обработка. Могут использоваться какие-то дополнительные ресурсы, например базы данных. Главное, чтобы один вошел — один вышел.

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

Второй типовой блок — это маршрутизатор (англ. router). У него есть две выходные очереди. Он тоже работает по принципу «один вошел — один вышел». Но он на основании своей логики способен выбирать, на какой из выходов отправить результирующий запрос.

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

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

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

Достоинства SEDA можно разделить на три категории. Это плюсы с функциональной и нефункциональной точки зрения. Особняком стоит распределенная обработка запросов.

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

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

О нефункциональной части хочется рассказать подробнее.

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

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

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

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

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

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

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

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

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

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

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

У нас есть два основных инструмента для использования кластера.

  1. Вертикальный подход, когда мы можем разносить различные функциональнее ступени на различные сервера. То есть в выполнении одной SEDA-транзакции будут физически участвовать несколько серверов. Каждый из них будет выполнять свою часть работы.
  2. Горизонтальное масштабирование. Мы можем выполнять операцию одной и той же функциональной ступени на нескольких серверах. В SEDA у нас весь контекст транзакций инкапсулирован в сообщение. У нас практически не возникает проблем с тем, чтобы иметь некое распределенное состояние. Оно инкапсулировано в сообщение. Соответственно сообщение приходит на сервер и может там выполниться. Если сервер «упал», то (при наличии надежной очереди) это сообщение будет восстановлено и отправлено на другой сервер. Он его обработает, и транзакция все равно будет доведена до конца.

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

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

Наконец-то я перейду к примерам, чтобы не быть совсем отделенным от практики.

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

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

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

Если сохранение изменений все же происходит, то мы должны произвести сохранение и в базе данных. Это делается асинхронно, т.е. независимо от отправки подтверждения. "Eventual atomicity" гарантирует нам, что изменения попадут в базу данных.

Использование SEDA позволило решить следующие проблемы:

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

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

Второй пример, который я хочу привести, наверное, будет для многих неожиданным. Он относится к сети Интернет. 

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

  • База с исходной информацией о продуктах.
  • Кластер application-серверов, который на схеме расположен в овале.
  • В этом кластере размещен Data Grid, который хранит данные.
  • "Люсиновский" индекс (англ. - lucene index), который позволяет выполнять полнотекстовый поиск по атрибутам.
  • Дополнительные структуры данных в памяти.
  • Кэш, который хранит "фэситы" продуктов. Что такое "фэсит", долго объяснять. Но, я думаю, можно зайти на ЯндексМаркет и найти подходящие примеры.
  • Веб-кэш, реализованный на уровне Content Delivery Network. Он "кэширует" страницы, которые предоставляет Application Cluster.

В чем состоит проблема?

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

Информация из базы данных должна попасть в Data Grid. На основе данных из Data Grid должны быть изменены дополнительные структуры в памяти. На основе этой информации происходит перестройка "люсиновского" индекса. На основе "люсиновского" индекса происходит перестройка "фэситов", которые должны быть отправлены в кэш.

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

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

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

Мы распределили нагрузку по выполнению операций изменения между серверам кластера.

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

Для выполнения "тяжелых" операций мы применили пакетное выполнение операций, например, для обновления "люсиновского" индекса. Задействовать его при каждом незначительном изменении нецелесообразно. Намного эффективнее накопить несколько подобных изменений и один раз пересчитать индекс.

Кроме того, поддержка сложного рабочего процесса позволила реализовать версионность изменений и, соответственно, быстрое переключение между версиями. Сделать это без "eventual atomicity" было бы, наверное, значительно сложнее, поскольку требовалась координация работы значительного числа систем для фокусировки на серьезном количестве маленьких операций.

Периодически перед системами ставятся задачи, к которым предъявляются диаметрально противоположные требования. В биллинге необходимо малое время доступа. Для изменения контента нам не принципиально, занимает времени та или иная операция: секунду или минуту, — лишь бы не больше часа.

В обоих случаях использование паттерна SEDA добавило некоторые плюсы.

Из чего это можно сделать? Из чего угодно. Это паттерн достаточно высокого уровня. Аналогичные идеи уже используются в таких продуктах, как Enterprise Service Bus, а экторные модели применяются в Erlang/OTP. На этой базе можно построить систему и смоделировать ее как SEDA, т.е. сеть, состоящую из блоков обработки.

Подведём итоги.

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

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

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

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

В рамках неортодоксального применений SEDA в одном из проектов мы также использовали Data Grid и Oracle Coherence.

Coherence является распределенным кэшем. Но используя его возможности хранения данных, мы создали собственные очереди, которые унаследовали надежность самого Coherence. Он же был использован для хранения состояния. В итоге была создана SEDA-платформа, работающая на Oracle Coherence.

Я не буду пропагандировать это как универсальное решение, но в нашем случае в этом были свои плюсы.

Вопросы и ответы

Вопрос:
— У меня два вопроса. Правильно ли я понял, что SEDA является другим названием того, что традиционно называется ESB — Enterprise Service Bus?
Алексей Рагозин:
— Нет.
Вопрос:
— В чем разница?
Алексей Рагозин:
— Enterprise Service Bus — это более специализированная разработка. Во-первых, есть некоторое функциональное разделение. Enterprise Service Bus — это все-таки оркестрация независимых сервисов.
SEDA можно использовать и на этом уровне, и на более низком для того, чтобы оркестрировать очень маленькие подсистемы. Это необязательно XML-формат сообщений. Это немножко более общее понятие.
Вопрос:
— Второе. Насколько я знаю, у Oracle есть собственные механизмы очередей. Почему вы использовали не его, а Coherence?
Алексей Рагозин:
— У нашего проекта был своеобразный контекст. Мы делали "proof of concept" применительно именно к технологии Coherence. Поэтому иное использование этой технологии могло стать нашим преимуществом.
С точки зрения реализации на Coherence очереди обладают некоторыми положительными сторонами. Они кластеризованные. Их пропускная способность масштабируется вместе с кластером Coherence. Поскольку у нас очереди и данные размещены на Coherence, то с добавлением серверов мы получаем увеличение производительности (англ. - capacity) и по тому, и по другому направлению.

Комментарии

Нет ни одного комментария

Только пользователи могут оставлять комментарии

Возможно, вам будет интересно:

Андрей Смирнов

Андрей Смирнов

Андрей Смирнов – руководитель разработки, разработчик, фанат Go, Python, DevOps и больших нагрузок. Руководил разработкой backend-сервисов в стартапе Qik, после его покупки продолжил работать в компаниях Skype и Microsoft.

Андрей Смирнов (Qik) рассказывает, для каких задач хорош Twisted, и делится секретами работы с этим фреймворком.

Юрий Востриков

Юрий Востриков

Один из ведущих разработчиков Mail.Ru.

Юрий Востриков (Mail.Ru) рассказывает о Tarantool/Silverbox - высокопроизводительной базе данных в оперативной памяти.

Борис Вольфсон

Борис Вольфсон

Борис Вольфсон занимается веб-разработкой и разработкой программного обеспечения с 2003 года. Карьеру начал в качестве программиста компании «Систем-Софт» в Оренбурге. С 2008 года – руководитель проектов и руководитель регионального отдела разработки в компании Softline.

Борис Вольфсон рассказывает о применении сбалансированной системы показателей в организациях, использующих Agile-методологию для разработки ПО.