В обсуждении заметки “Нашел интересное в очередном потоке сознания Стива Йегга” я выказал намерение написать о том, почему я считаю хорошими принципы взаимодействия компонентов, озвученные в 2002 году основателем Amazon Джефом Безосом. Тема интересная, но только сейчас нашлось достаточно времени чтобы вернуться к ней.
Итак, вкратце упомянутые принципы в моем русском варианте звучат следующим образом:
1) Все команды начиная с данного момента должны предоставлять данные и функциональность своих компонентов посредством сервисов со специфицированными интерфейсами.
2) Компоненты должны взаимодействовать друг с другом посредством этих интерфейсов.
3) Не должно быть никаких других форм взаимодействия: ни непосредственной линковки, ни прямого чтения хранилищ данных другой команды, ни модели разделяемой памяти, ни каких “черных входов” или чего-то подобного. Единственная разрешенная форма коммуникации – вызовы интерфейсов через сеть.
4) Без разницы какая технология будет использоваться. HTTP, CORBA, Publisher-Subscriber, слабанная на коленке – все равно, это не важно.
5) Все без исключения интерфейсы должны с самого начала проектироваться с прицелом на взаимодействие с внешним миром. Т.е. команды должны планировать и проектировать свои интерфейсы так, чтобы их можно было предоставить разработчикам вне компании. Без исключений.
На мой взгляд, эти принципы могут применяться и для распределенных программных комплексов много меньшего масштаба, нежели платформа Amazon. И в этом случае я бы заменил пункт #5 – поставил бы на его место правило о том, что интерфейсы должны быть асинхронными, ориентированными на обмен сообщениями. И только в крайних случаях они могут быть синхронными, основанными на чем-то вроде удаленных вызовов.
Теперь, после освежения темы в памяти, можно перейти к перечислению достоинств такой модели.
Первое (важное с точки зрения эксплуатации): такой подход позволяет эксплуатировать каждый из компонентов независимо друг от друга. Это затрагивает множество аспектов. Например, масштабирование. Когда конкретный компонент перестает справляться с нагрузкой, можно заниматься его разгоном независимо от других – докупать новые сервера, увеличивать дисковые массивы, производить обновление железа и пр. – вплоть до переезда на совсем другие аппаратные и программные платформы. Скажем, работал компонент на одном сервере с Windows, а перебрался на Linux-овый кластер с балансировщиком нагрузки на входе. При том, что остальные компоненты могут не иметь возможности подобной миграции в принципе (например, привязаны к железякам, драйвера для которых есть только под одну платформу).
Второе (важное с точки зрения разработки): такой подход позволяет разрабатывать компоненты независимо друг от друга. Без оглядки на используемые технологии и их частные особенности. Здесь я противопоставляю взаимодействие с удаленным компонентом взаимодействию через непосредственную линковку кода компонента с вашим кодом. Допустим, в приложении нужно иметь возможность шифрования/подписи данных с использованием HSM. Код по взаимодействию с HSM можно просто прилинковать к своему коду. Но тогда возникают разнообразные проблемы интеграции – на каком языке написан сторонний компонент, совместим ли он с нашей версией компилятора? Какие фреймворки и их конкретные версии используются – совместимы ли они с тем, что используем мы? Какие особенности есть в работе с компонентом – нужно ли его явно инициализировать/деинициализировать, если нужно, то на контексте какой нити это нужно делать? Как компонент может влиять на работу нашего приложения – насколько он дружит с многопоточностью, может ли он непредсказуемо сильно загрузить процессор или вдруг отожрать много памяти? Обо всем этом не нужно думать, если компонент внешний, мы общаемся с ним только через сеть и работает он фиг знает где. Модуль работы с HSM-мом можно сделать на C++, а свой код мы без лишнего геморроя можем написать на Ruby.
Третье (важное как с точки зрения разработки, так и с точки зрения эксплуатации): такой подход позволяет очень гибко и разнообразно менять потоки данных между компонентами. Зачастую совершенно прозрачно для самих компонентов. Особенно в варианте, когда используется асинхронный обмен сообщениями. Простой пример. Допустим, есть компонент A версии 1.0. Мы хотим ввести в эксплуатацию его следующую версию 1.1, но очень плавно – направив сначала на новую версии только 5% от запросов, затем 10%, затем 50% и только убедившись в полной работоспособности, все 100%. Делается это посредством балансировщика, который устанавливается перед двумя версиями компонентов A. Сначала он пропускает 95% запросов на версию 1.0, а 5% – на версию 1.1. Затем пропорции меняются. Когда происходит полный переход на версию 1.1 этот промежуточный компонент может быть вообще устранен. При этом ни сами компоненты A, ни их клиенты даже не догадываются о том, что параллельно работают разные версии.
Такое “прозрачное” вмешательство в потоки данных позволяет делать интересные вещи. Например, фильтровать поток запросов, удаляя из них заведомо неправильные или потенциально опасные. Проводить преобразование запросов “на лету” (скажем, заменять в платежных запросах устаревший код валюты на актуальный). Сглаживать пики нагрузки и делать поток запросов равномерным. Либо же контролировать объем потока, отсекая “лишние” запросы, тем самым предохраняя компоненты от перегрузки. И т.д., и т.п. Причем, повторюсь, делать это в случае асинхронного обмена сообщениями, на мой взгляд, много проще, чем при синхронном взаимодействии.
Вот как-то так. Наверняка есть и другие “за” (равно как и “против”), но три вышеозвученных момента сразу же приходят на ум.