Требования к приложениям кардинально изменились в последние годы. Только несколько лет назад большие приложения требовали десятки серверов для развертывания, обладали временем отклика порядка секунд, требовали часов оффлайн-обслуживания и обрабатывали гигабайты данных. Сегодня приложения развертываются на разнообразных платформах от мобильных устройств до облачных кластеров с тысячами многоядерных процессоров. При этом пользователи ожидают время отклика порядка миллисекунд и 100% доступности, а объемы данных приближаются к петабайтам.
Первоначально первопроходцами в этой области были такие интернет-компании как Google и Twitter, но сейчас подобные требования к приложениям начинают появляться и в других отраслях. Финансовая сфера и телекоммуникации были первыми, кто принял новые практики для удовлетворения новых потребностей. За ними последовали другие отрасли.
Новые потребности требуют новых технологий. Предшествующие решения фокусировались на управляемых серверах и контейнерах. Масштабируемость достигалась посредством покупки более мощных сервером и одновременной многопоточной обработки (multi-threading). Процедура добавления дополнительных серверов выполнялась с помощью сложных, неэффективных и дорогих проприетарных решений.
Сейчас появился новый тип архитектуры, позволяющий разработчикам проектировать и разрабатывать приложения, удовлетворяющие сегодняшним потребностям. Мы называем их reactive-приложения. Этот тип архитектуры позволяет разработчикам создавать событийно-ориентированные, масштабируемые, отказоустойчивые и интерактивные системы, обеспечивая пользователям высокоинтерактивное взаимодействие с системой с ощущением отклика в реальном времени. Такие системы построены на масштабируемом и отказоустойчивом стеке приложений и готовы к развертыванию на многопроцессорных и облачных платформах. Reactive-манифест описывает отличительные черты, необходимые для соответствия термину "reactive".
Словарь Мериэм-Вебстер определяет "reactive" как "готовый к отклику на воздействие", т.е. его компоненты "активны" и готовы к возникновению событий. Это определение описывает сущность reactive-приложений, выделяя такие системы, которые:
- реагируют на события: событийно-ориентированная природа обеспечивает остальные качества
- реагируют на нагрузку: фокусируются на масштабируемости нежели на производительности для отдельного пользователя
- реагируют на отказ: отказоустойчивы, спроектированы с возможностью восстановления после отказа на любом уровне
- реагируют на действия пользователя: комбинируют предыдущие особенности, обеспечивая интерактивное взаимодействия пользователя с системой
Каждая из этих характеристик является важнейшей для reactive-приложений. Несмотря на то, что между ними есть зависимости, эти черты не являются подобием уровней (tiers) в стандартном понимании архитектуры многоуровневых приложений. Вместо этого эти качества описывают свойство архитектуры, которые применимы для всего технологического стека.
Далее мы более детально рассмотрим каждое из этих четырех качества и как они взаимосвязаны друг с другом.
Приложение, построенное на принципах асинхронной коммуникации, само по себе ведет к архитектуре с низкой связанностью (loosely coupled design), причем намного лучшей, чем у приложения, построенного только на синхронном вызове методов. Отправитель и получатель могут быть реализованы безотносительно деталей того, как передаются события, позволяя программному интерфейсу сфокусироваться непосредственно на данных, участвующих в коммуникации. Это ведет к реализации, более простой для расширения, развития и поддержки, что обеспечивает лучшую гибкость и снижает стоимость поддержки.
Так как получатель в асинхронном взаимодействии может оставаться в спящем состоянии до тех пор, пока не произойдет событие или будет получено сообщение, то событийно-ориентированный подход делает возможным эффективное использование ресурсов, позволяя огромному количеству получателей совместно использовать один физический поток выполнения. По этой причине неблокирующие приложения под высокой нагрузкой показывают более низкое время отклика и большую пропускную способность, чем традиционные приложения, основанные на блокирующей синхронизации и примитивах взаимодействия. Это ведет к более низким операционным издержкам, улучшенной утилизации ресурсов и, кроме того, к большему удовлетворению конечных пользователей.
В событийно-ориентированном приложении компоненты взаимодействуют друг с другом посредством создания и обработки событий - дискретных порций информации, описывающих некоторые факты. Эти события отправляются и получаются в асинхронной и неблокирующей манере. Событийно-ориентированные системы более полагаются на "проталкивание" (push), чем на "вытягивание" (pull or poll) данных, т.е. они отдают данные их потребителям в том момент, когда эти данные готовы, вместо того чтобы тратить впустую ресурсы, заставляя потребителей непрерывно делать запросы или ожидать данные.
- Асинхронная отправка событий, также известная как "передача сообщений" (message-passing), означает то, что приложения способно к одновременной обработке множества запросов по своей природе и может использовать многоядерное железо без каких-либо доработок. Любое ядро в процессоре способно обрабатывать любое событие, что означает исключительное увеличение возможностей по параллелизации.
- "Неблокирующий" означает, что приложение очень эффективно в терминах использовании аппаратных ресурсов, так как неактивные компоненты приостанавливаются и их ресурсы освобождаются для использования другими компонентами.
Традиционные серверные архитектуры основаны на совместно используемом изменяемом состоянии и блокирующих операциях в одном потоке. И то и другое ведет к трудностям, когда такую систему необходимо масштабировать для удовлетворения изменившихся требований. Совместное использование изменяемого состояния требует синхронизации, следствием которой является недетерминизм и сложность, связанная с побочными эффектами, что делает программный код трудным для понимания и поддержки. Перевод потока в спящее состояние посредством блокировки расходует ограниченные ресурсы и подразумевает высокую цену для его возобновления.
Разделение генерации события и его обработки позволяет среде выполнения заботиться о деталях синхронизации и о маршрутизации событии по потокам выполнения, при этом образуется программная абстракция уже на уровне бизнес-процессов. Вы размышляете о том, каким образом через вашу систему распространяются события и каким образом взаимодействуют компоненты, вместо того чтобы возиться с низкоуровневыми примитивами вроде потоков выполнения и блокировок.
Событийно-ориентированные системы делают возможным низкую связанность между компонентами и подсистемами. Этот уровень косвенности, как мы рассмотрим в дальнейшем, является необходимым условием для масштабируемости и отказоустойчивости. Удаляя сложные и сильные зависимости между компонентами, событийно-ориентированные приложения могут быть расширяемы с минимальными изменениями существующего кода.
Когда приложения находятся в жестких рамках требований высокой производительности и масштабируемости, то трудно предположить где могут возникнуть узкие места. По этой причине важно, чтобы всё решение было полностью асинхронным и неблокирующим. В типичном случае это значит, что вся архитектура должна быть событийно-ориентированной - это проявляется во всем, начиная от пользовательского запроса на клиенте (в браузере, REST-клиенте и т.д.), а также в обработке и диспетчеризации запроса в веб-слое, в сервисных компонентах в слое бизнес-логики, в отношении кэширования и, наконец, в работе с базой данных. Если один из этих слоёв не участвует - выполняются блокирующие вызовы к базе данных, применяется общее изменяемое состояние, выполняются обращения к затратным синхронным операциям, - то тогда тормозит весь конвейер, что означает для пользователей увеличение времени отклика и ограничения по масштабируемости.
Приложение должно быть реактивным на всех уровнях.
Потребность в исключении самого слабого звена в цепи хорошо иллюстрируется законом Амдаля (Amdahl's Law), который, согласно Википедии, говорит о следующем:
Ускорение выполнения программы с использованием множества процессоров для параллельного вычисления ограничено той частью программы, которая выполняется последовательно. Например, если 95% процентов программы может быть распараллелено, то теоретический максимум ускорения с использованием параллельных вычислений, как показано на диаграмме, составляет 20 раз в независимости от того, как много процессоров используется.
Слову "scalable" в словаре Мэриэм-Вебстер дано следующее определение "способность к простому расширению или модернизации по мере необходимости". Масштабируемое приложение обладает способностью к расширению в зависимости от его использования. Это может быть достигнуто посредством придания приложению свойства эластичности, т.е. способности к горизонтальному масштабированию ("scaling out and in" - добавлению и удалению узлов) по запросу. Кроме того, такая архитектура делает простым вертикальное масштабирование ("scaling up and down" - развертывание узлов с большим или меньшим количеством процессоров) без перепроектирования или переписывания приложения. Эластичность делает возможным минимизацию операционных издержек для функционирования приложений в облачной инфраструктуре, позволяя вам получать выгоду от использования модели оплаты только за используемые ресурсы.
Масштабируемость также помогает в управлении рисками: если вы предоставите недостаточный объем серверных мощностей для текущей нагрузки, то это приведет к разочарованию пользователей и потере клиентов, если же вы используете слишком много серверов (а ведь еще нужен дополнительный персонал для их обслуживания), то это ведет к простаивающим без какой-либо пользы ресурсам и излишним расходам. Масштабируемое решение также снижает риск получить приложение, которое не в состоянии использовать новое железо: мы увидим процессоры с сотнями, если не тысячами, аппаратных потоков выполнения в течение следующей декады, и для использования их потенциала требуется приложение масштабируемое на очень детальном уровне.
Событийно-ориентированные системы построенные на передаче сообщений обеспечивают основу для масштабируемости. Такие системы подразумевают низкую связанность между компонентами и подсистемами и это делает возможным горизонтальное масштабирование системы на множество узлов, сохраняя ту же самую программную модель и ее семантику. Добавление большего количество экземпляров компоненты увеличивает пропускную способность системы в обработке событий. В смысле реализации нет никакой разницы между вертикальным масштабированием путем использования множества процессоров и горизонтальным масштабированием с помощью использования большего количества узлов в дата-центре или кластере. Топология приложения становится решением при развертывании, которое реализуется через конфигурацию и/или с помощью адаптивных алгоритмов времени выполнения, реагирующих на использование приложения. Это то, что мы называем «независимость от месторасположения» - [location transparency] (http://en.wikipedia.org/wiki/Location_transparency).
Важно понимать, что целью является не реализация прозрачных распределенных вычислений, распределенных объектов или взаимодействия в стиле вызова удаленных процедур (RPC) - были попытки сделать это раньше и они провалились. Вместо этого мы должны использовать сетевую модель в качестве программной модели с асинхронной передачей сообщений. Истинная масштабируемость естественным образом подразумевает распределенные вычисления, подразумевающие межузловое взаимодействие, которое означает передачу данных по сети, что, как нам известно, по своей природе ненадежно. Поэтому так важно в программной модели сделать явными все ограничения, недостатки и сценарии обработки ошибок, присущие сетевому программированию, вместо того чтобы скрывать их с помощью "текущих" (leaky) абстракций в попытках "упростить" вещи. Как следствие, не менее важно предоставить программные инструменты, инкапсулирующие общие строительные блоки для решения типичных проблем, возникающих в распределенном окружении - такие механизмы как достижение консенсуса или абстракции в передаче сообщений, предоставляющие лучшие степени надежности.
Неработоспобность приложения является одной из тех проблем, которые могут причинить наибольший ущерб бизнесу. Обычно это означает, что все операции просто останавливаются, образуя дыру в потоке доходов. В долгосрочной перспективе это также ведет к неудовлетворенным клиентам и дурной репутации, что ударит по бизнесу более серьезно. Это удивительно, но отказоустойчивость приложения — это такое требование, которое обычно игнорируется или видоизменяется с помощью специфических трюков. Это означает, что оно реализуется на неверном уровне детализации с использованием слишком грубых инструментов. Обычной техникой является кластеризация сервера для восстановления во время выполнения в случае отказа. К несчастью, обеспечение отказоустойчивости сервера чрезвычайно затратное дело и к тому же опасное - потенциально это может привести к каскадному отказу и неработоспособности всего кластера. Причина этого в том, что это неправильный уровень детализации для управления отказами, вместо этого требуется реализация на подходящем уровне детализации - обеспечение устойчивости на уровне компонент.
Словарь Мэриэм-Вебстер определяет устойчивость (resilient) как:
- способность субстанции или объекта восстанавливать свою форму
- возможность быстрого восстановления после трудностей
Для reactive-приложений отказоустойчивость — это не решение принятое на поздних этапах, а часть архитектуры с самого начала. Представление сбоя в программной модели в качестве полноправного объекта придает осмысленности в обработке и управлении сбоями, что позволяет создавать высокоустойчивые к сбоям приложения с возможностью самовосстанавливаться во время выполнения. Традиционный способ обработки ошибок не может достичь этого, т.к. он прибегает к защитной стратегии в малом и к агрессивной в большом масштабе - вы либо обрабатываете исключение в тот момент, когда оно возникло, и в том же месте, либо инициируете отказ всего экземпляра приложения целиком.
Для управления отказом нам требуется изолировать его, чтобы он не распространился на остальные функционирующие компоненты, и наблюдать его из безопасной точки вовне контекста произошедшего сбоя. Один из шаблонов, который приходит на ум, - модель отсеков - bulkhead pattern, проиллюстрированная на картинке. В этой модели система построена из безопасных отделений таким образом, что в случае отказа одного элемента остальные не затронуты. Это предотвращает классическую проблему [каскадного отказа] (http://en.wikipedia.org/wiki/Cascading_failure) и позволяет решать проблемы изолированно.
Событийно-управляемая модель, делая возможным масштабирование, также обладает необходимыми примитивами для реализации такой модели управления отказами. Низкая связанность в событийно-ориентированной модели предоставляет полностью изолированные компоненты, внутри которых сбои могут быть отслежены вместе с их контекстом, инкапсулированы в сообщения, и переданы другим компонентам, которые отслеживают ошибки и могут принять решения каким образом реагировать.
Такой подход создает систему, в которой бизнес-логика остаётся прозрачной, отделенной от логики обработки непредвиденных ситуаций. Сбой моделируется явно для того, чтобы быть классифицированным, наблюдаемым, управляемым и конфигурируемым в декларативном стиле. Такая система самовосстанавливается автоматически. Это работает наилучшим образом, если части структурированы иерархически, подобно большой корпорации, где каждая проблема передаётся наверх до того уровня, который обладает полномочиями для ее решения.
Красота этой модели в том, что она полностью событийно-ориентированна, основана на реагирующих компонентах и асинхронных событиях и поэтому независима от месторасположения. На практике это означает, что система работает в распределенном окружении с той же семантикой, что и в локальном.
Интерактивные приложения взаимодействуют в реальном времени, увлекательны и позволяют работать совместно. Бизнес создает открытый и продолжающийся диалог со своими клиентами с помощью интерактивного взаимодействия. Это делает такие компании более эффективными, создает чувство соединенности и возможности решения проблем и выполнения задач. Один из таких примеров - Google Документы, которые позволяют пользователям редактировать документы совместно, в реальном времени. Пользователи могут видеть изменения и комментарии непосредственно в тот момент, как они создаются.
Пользователям обладают исключительными возможностями, когда они могут взаимодействовать с данными, преобразуемыми в осмысленную информацию в реальном времени. Интерактивные приложения делают совместную работу с информаций присущей любому интерфейсу, таким образом люди взаимодействуют эффективнее и чаще. Это усиливается по мере изменения детализации обратной связи - от традиционного обновления всей страницы к обновлению отдельных элементов, например в одностраничном веб-клиенте электронной почты. Моментальные социальные взаимодействия через огромные расстояния радикально изменяют то, каким образом люди вовлечены во взаимодействие с информацией и друг с другом. Например, GitHub посредством интерактивного браузерного приложения революционным образом воздействует на совместную работу разработчиков через "социальное программирование". И конечно же Twitter также глубоко изменил способ распространения новостей.
Построенные на событийно-ориентированном подходе, reactive-приложения достаточно хорошо оснащены, чтобы быть интерактивными. Когда приложение становится популярным, то для достижения этого качества необходима масштабируемость, а отказоустойчивость приложения дает возможность пользователям непрерывно наслаждаться его функциями.
В reactive-приложениях используются наблюдаемые модели, потоки событий и клиенты, обладающие состоянием.
Наблюдаемые модели позволяют другим компонентам получать события при изменении состояния. Это обеспечивает соединение в реальном времени между системой и пользователем. Например, когда несколько пользователей одновременно работают над общим набором данных, то изменения непосредственно синхронизируются между ними в обоих направлениях.
Потоки событий формируют базовую абстракцию для построения такой связи. Сохранение их реагирующими означает избегание блокирования, вместо этого разрешая асинхронные и неблокирующие трансформации. Например, поток данных в реальном времени может быть пропущен через некоторую проекцию в реальном времени, что порождает новый поток анализированных данных.
Большинство reactive-приложений обладают богатым веб- и мобильными клиентами, что делает их увлекательными для пользователей. Такие приложения реализуют логику и сохраняют состояние на стороне клиента, где наблюдаемые модели предоставляют механизм для обновления пользовательского интерфейса с изменением данных в реальном времени. Такие технологии как веб-сокеты и инициируемые сервером сообщения позволяют пользовательским интерфейсам напрямую подключаться к потокам событий. Таким образом событийно-ориентированные системы распространяются повсюду от серверной части до клиентской. Это позволяет реагирующим приложениям отправлять события в браузер или мобильные приложения масштабируемым и эластичным способом, используя асинхронную и неблокирующую передачу данных.
С учетом всего этого, становится понятным, каким образом четыре качества - событийно-ориентированная архитектура, масштабируемость, устойчивость и интерактивность - создают связанное единое целое.
Reactive-приложения представляют сбалансированный подход к решению современных проблем в разработке ПО. Построенные на событийно-ориентированной архитектуре, основанной на передаче сообщений, они представляют инструменты для обеспечения масштабируемости и устойчивости. Помимо этого, они обеспечивают интерактивность взаимодействия с пользователем в реальном времени. Мы ожидаем, что быстро растущее число систем последует такой архитектуре в ближайшие годы.