SOA: распределённая архитектура и её обслуживание (продолжение, часть 2)

В первой части “SOA: делаем высоконадёжный отказоустойчивый веб-сервис” мы поговорили о том, что такое SOA (сервисно-ориентированная архитектура построения надёжных приложений) и как она в сочетании с принципом событийно-ориентированного программирования идеально подходит для разработки высоконагруженных и отказоустойчивых вычислительных систем.

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

Мониторинг SOA

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

Прежде чем говорить о мониторинге, поясню простую вводную мысль: мониторить SOA-приложение очень легко, если просто слушать все “переговоры” между сервисами (весь обмен сообщениями по ESB – шине данных) и делать аналитические выводы. Не вручную делать, разумеется. Но давайте по-порядку:

Что нужно мониторить в SOA?

1. Состояние сервисов (демонов). Если вы используете supervisord, то он рисует красивые зелёненькие [Running] в веб-интерфейсе и отдаёт вменяемые данные по API (что, конечно же, более важно для полноценной системы мониторинга). Это необходимый минимум, но рекомендуется научить сами сервисы сообщать о своих проблемах с помощью той или иной системы логирования.

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

3. Корректность вычислений с течением времени. Больная тема для множества разработчиков, которые считают, что если код в тестовой среде (или “на стейдже”) прошёл все контрольные испытания (тесты), то значит он будет исправно работать всегда. Это опаснейшее заблуждение.

Необходим регулярный, циклический контроль “правильности работы” каждого узла, и здесь SOA снова идёт нам навстречу: достаточно настроить периодическую отправку в каждый сервис некоего тестового набора задач, и сверять полученные ответы с эталонными. Разобравшись, легко понять что это привычный принцип unit-тестирования, просто применённый не к “куску кода”, а к сервису (или группе сервисов) в целом.

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

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

Мониторить ошибки легко: так как все они проходят через шину ESB, значит они логируются наравне со всеми другими сообщениями, и попадают в анализатор. Для человеческой визуализации вполне подходит Graphite, который настолько умён, что даже способен группировать ваши эвенты в древовидную структуру и обеспечивать по ней поиск.

5. Дисконнекты. Проблемная тема для многих приложений, потому что до сих пор, в 2014 году, так и не появилось стандартного способа реконнекта – восстановления соединения с удалённым сервером. Всякий веб-разработчик видел “mysql has gone away” и знает что с этим делать, но что вы скажете о возобновлении соединения с redis-ом? А что скажете, если два инстанса RabbitMQ потеряли связность в репликации?

Веб-программисты на PHP, привыкшие к тому что на любой запрос “из веба” приложение полностью инициализируется заново, работает 1-2 секунды и “умирает”, – оказываются не готовы к “отваливанию” различных серверов. С демонами такое попустительство не прокатывает. Вам нужно будет либо следить за соединениями, либо принудительно восстанавливать коннект при каждой итерации обработки.

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

Так как фактически любой бизнес-процесс в SOA имеет явно выраженное начало (“something.do”), обозначенный конец (“something.do.success”), и объединяющий их идентификатор процесса (“correlation_id”), – легко измерить время, проходящее между началом и концом в потоке таких операций.

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

Деплой SOA-приложения

Хозяйке на заметку: если вы до сих пор не используете capistrano – идите и научитесь сейчас же.

Можно выделить ряд основных логических уровней стратегии деплоя в целом:

1. Инфраструктура. Это железные и виртуальные сервера, на которых развёрнуто и настроено программное обеспечение для работы приложения. Как правило, решается с помощью инструментов chef/puppet. На уровне инфраструктуры находятся такие вещи, как сервер очередей, memory storage, базы данных и т.д.

2. Конфигурация. Это непосредственно конфигурационные структуры для вашего приложения: перечень демонов в supervisord, список баз данных и доступов к ним, конфигурационные файлы “точек входа” (nginx, websocket-сервис) и прочее. Понятно, что этими конфигурациями тоже надо управлять, чтобы добавление нового демона или новой базы данных не превращалось в чтение мантры по пыльной корпоративной документации.

3. Миграции структур данных. На взгляд автора этих строк, структура данных и её изменения с ходом развития – дело каждого сервиса, но есть и иные мнения. Преимущество – инкапсуляция, сокрытие реализации в “чёрный ящик”, когда сервис (и его разработчик) сам знает, как он хранит свои данные и как мигрирует их в обновлённую структуру. В этом случае инфраструктура и конфигурация являются для него – статичными исходными данными.

4. Деплой непосредственно обновлений сервисов. Новая версия сервиса раскатывается по системе, супервайзер мягко останавливает нужные инстансы и перезапускает их новую версию.

Если вы вдруг не читали первую часть статьи – напомню, что одним из преимуществ SOA здесь является то, что сервисы рестартуют без “сбрасывания” текущих бизнес-процессов.

Я настоятельно рекомендую использовать capistrano и относиться к стратегиям деплоя как к полноценным проектам: мэйнтейнить их, держать в репозиториях, подключать как зависимости, с умом использовать semantic versioning. В том числе, например, иметь некий общий процесс деплоя, который расширяется (наследуется) в каждом конкретном сервисе по-своему. Например, где-то нужен этап database migrate, а где-то нет. В каких-то сервисах нужно сбрасывать кэши, в других – нет. Используя наследование и относясь к процессу деплоя как к обычному программированию, вы сможете поддерживать его лёгким и расширяемым.

Расширение функциональности

Самая “богатая” тема SOA. В типичном программировании, с которым сталкивается большинство из нас повседневно, под расширением функциональности подразумевается буквальное внесение изменений в нужный кусок кода, и/или добавление нового кода в нужные места – как правило, вызываемого последовательно со старым кодом (до него, после него, или вперемешку).

Обилие подводных камней здесь пояснять, думаю, не нужно: начиная с того, что любой изменённый код – есть потенциальная точка возникновения ошибки. По множеству как реальных, так и нелепых причин, от кривых тестов и опаздывающей документации до проблем с пониманием новой функциональности у удалённых сотрудников и разработчиков-партнёров, которые пользуются вашим продуктом через публичное API и транслируют его функции уже своим клиентам и субподрядчикам.

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

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

SOA предлагает ряд исходно иных путей к решению таких задач. Я намеренно оставлю за рамками поста лишнюю политику, и объясню исключительно техническую сторону, чтобы вы сами могли понять – какой путь в каком случае вам будет ближе. От простого к сложному:

1. Изменение поведения одной точки в одном узле (сервисе). Самый простой случай, когда один из ключевых узлов бизнес-процесса стал вести себя немного иначе. Банально поменяли одну-две строчки кода. На остальное поведение системы не оказывается практически никакого влияния.

2. Внесение нового последовательного узла в существующую цепочку шагов бизнес-процесса. У вас был процесс A-B-C-D, теперь стал A-B-C-X-D. Новый узел “X” разработан, проверен и протестирован отдельно от всей системы – можно уверенно полагаться на его стабильность. Исходный код узлов “C” и “D” никак не изменился – их стабильность не нарушена. С точки зрения потребителя, поведение системы не изменилось – а значит нет и предпосылок к проблемам.

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

3. Внесение нового узла, обрабатываемого параллельно. В тексте это сложно изобразить графически, но я попробую: A-B-[C|X]-D. Это то же самое, что и предыдущий пункт, за исключением того что процесс раздваивается и операции “C” и “X” выполняются параллельно. Замечу, что в SOA это самая настоящая параллельность, а не её эмуляция: процессы могут протекать как в разных инстансах сервисов одной машины, так и на нескольких машинах независимо.

Параллельность выгодно использовать для длительных операций (спасибо, кэп) или операций с непредсказуемым временем выполнения. В вышеприведённом примере операцию “X” можно распараллелить не только с операцией “C”, но и с более длинной цепочкой “B-C-D”, если это требуется. Разумеется, SOA поддерживает любое ветвление, и здесь вас ограничивает только здравый смысл.

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

Идея проистекает из принципа относительности товарища Эйнштейна, как бы пафосно это ни звучало. Суть в том, что невозможно мгновенно и одновременно изменить состояние всей системы. Если какой-то узел вашей системы кардинально поменял поведение и ждёт иных входных данных (ну вот пришлось поступить столь кардинально), то нельзя исключать что к нему “прилетит” запрос по устаревшей схеме. Например, от необновлённого клиента API, из браузера пользователя и так далее.

В классическом подходе к программированию есть два пути решения:

а) отбросить сообщение, и надеяться что этот факт будет залогирован и исправлен какими-то третьми силами;

б) держать в коде два (три, пять, десять) вариантов обработки, передавая входящий запрос нужному из них.

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

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

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

Распространение конфигурации

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

Для начала вспомним, как обычно выглядит изменение конфигурации работающего демона: остановили демон, поменяли файлик, запустили демон, проверили, внесли изменение в репозиторий конфигурации, закоммитили, запушили, передеплоили… Да, я немного преувеличиваю, но суть передана верно. Если хорошенько подумать, то это всё лишние телодвижения и потери (в том числе на переинициализацию), и не существует никаких причин не делать всё то же самое “на ходу”.

Более правильный способ выглядит так: существует единый сервис конфигурации, который хранит в себе информацию о том, как надо конфигурировать все остальные сервисы. Когда какой-нибудь прикладной сервис стартует, он запрашивает по ESB у центрального сервиса свою конфигурацию, получает ответ, и применяет её к себе.

Однако гораздо более интересен другой эффект: сервис конфигурации может принудительно распространять (“пушить”) обновления конфигурации в целевые сервисы. Те получают эти “пуши” как обычные сообщения из ESB, применяют их и продолжают работать дальше. Быстро, элегантно, без остановки. Можно применить всю свою фантазию к тому, как легко таким образом включать диагностические режимы типа “отлогируй следующие 10 сообщений о балансе пользователя pupkin в режиме debug, а дальше вернись к прежней конфигурации”.

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

Законодатели, исполнители и пинги

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

Такая схема удобна не только с точки зрения “разделения труда”, но и повышает защищённость системы в целом: получив доступ к “исполнительному узлу”, злоумышленник не получает автоматически доступа ко всей системе, а может лишь блокировать работу или фальсифицировать отчёты с одного взломанного сервера.

Вся эта схема более подробно будет рассмотрена в следующей части, а сейчас поговорим об инструменте её контроля и обслуживания:

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

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

Для регулярных напоминаний напрашивается решение с обычным linux-кроном, однако автор этих строк считает такой способ неудобным – требуется вести кронтабы на целевых серверах, не забывая обновлять их, а так же ввести альтернативный способ “дёргать” работающие демоны из командной строки, что тоже выглядит излишеством. Гораздо проще обучить “командующий” сервис (или какой-то центральный сервис во всей системе) регулярно рассылать “пинги”, на которые все “подчинённые” будут рапортовать своим “командующим”. Простое, централизованное решение, не требующее возни с кронтабом и лишней обработки сигналов операционной системы.

Конец второй части. Продолжение следует.