среда, 2 ноября 2011 г.

[prog.thoughts] Нашлось время вернуться к обсуждению взаимодействия компонентов программных комплексов

В обсуждении заметки “Нашел интересное в очередном потоке сознания Стива Йегга” я выказал намерение написать о том, почему я считаю хорошими принципы взаимодействия компонентов, озвученные в 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, ни их клиенты даже не догадываются о том, что параллельно работают разные версии.

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

Вот как-то так. Наверняка есть и другие “за” (равно как и “против”), но три вышеозвученных момента сразу же приходят на ум.

12 комментариев:

имя комментирует...

гы

что интересно, у тебя нет первого приходящего мне в голову момента:

А. на работу программистов тратится бабло

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

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

например рассмотрим чисто гипотетически ситуацию, когда до реформы "умный" поиск был поверх внутреннего сервиса "найти точно те слова, которые я ввел, без транслитерации и синонимов"

а после реформы поисковый индекс создается так, что слова русские и транслитерированные не отличаются (в смысле, занимают одну позицию индекса -- надеюсь, ясно?)

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

поэтому продажа таких сервисов наружу будет тормозом для реформ

имя комментирует...

теперь можно обсудить именно твои аргументы

там куча мыслей, и сразу сделать я это не смогу -- буду постепенно постить

вот в нулевом приближении ("Хуже" означает "настолько хуже, что практически должно избегаться") с чем можно согласится:

А. там-же-поточное использование компонента (я ясно выражаюсь?) Хуже, чем использование в другом потоке

В. использование компонента в другом потоке Хуже, чем в другом процессе

... но здесь вот уже конец, т.е. с пунктом

С. использование компонента в другом процессе Хуже, чем использование по сети

я уже не согласен

твои аргументы "использование памяти, использование процессора" не катят -- в линуксе можно выделить квоту на то и другое (наверно и в винде можно, но я ее всерьез не рассматриваю)

относительно возможности разнести сервисы на разные машины -- да, согласен (хотя возражения есть), и поэтому часто сетевая прозрачность нужна, но не всегда

т.е. "Единственная разрешенная форма коммуникации – вызовы интерфейсов через сеть" -- это оверкилл

имя комментирует...

* нужна, но не всегда обязательна

имя комментирует...

рассмотрим например HSM -- для него вполне логично догадаться, что лучше к нему обращаться по сети; для этого есть по крайней мере 3 способа:

1. беглый взгляд на первый абзац из http://en.wikipedia.org/wiki/Hardware_security_module показывает слова "or an external TCP/IP security device"; ход мысли: "раз их делают как сетевые устройства -- может и нам они потребуются как сетевые устройства? а что мы потеряем, если будем юзать их только как сетевые устройства? ничего?"

2. HSM одно, а машинок, где оно нужно, может быть несколько

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

в целом, безос мне напоминает фразу "в армии лето наступает не по календарю, а когда выходит приказ перейти на летнюю форму одежды"

если уж хочешь навести порядок, сделай правило "каждый, кто хочет несетевое ipc, должен представить мне письменное обоснование необходимости этого"

и потом -- асинхронность она не бесплатна -- может вполне потребоваться приоритизация запросов, пришедших из разных источников, а если юзать HSM синхронно -- тогда с ним общаться проще

имя комментирует...

* 3 способа в дополнение к указанным тобой

т.е. тут вопрос стоит так: как программист может НЕ догадаться, что работа по сети может потребоваться

и с другой стороны, это не значит, что реализация сетевой работы обязательна -- если время жмет, можно сознательно это не делать

имя комментирует...

* можно сознательно этого не делать -- но это примерно как брать деньги в кредит -- когда-то придется возвращать

имя комментирует...

абзац:

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

следует читать в такой редакции:

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

имя комментирует...

> Допустим, есть компонент A версии 1.0. Мы хотим ввести в эксплуатацию его следующую версию 1.1, но очень плавно

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

Евгений Охотников комментирует...

@имя:

>что интересно, у тебя нет первого приходящего мне в голову момента

Наверное это потому, что данные принципы я рассматриваю уже безотносительно к Amazon и Google. А в большей степени к своим собственным задачам. И, поскольку, мы делаем продукты, которые сами же эксплуатируем и наружу не выпускаем, то данный момент не актуален. По крайней мере не сильно актуален.

Евгений Охотников комментирует...

@имя:

По остальным комментариям: прошу прощения, но я не понял, в чем их суть.

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

Коммуникация через каналы связи (сокеты, пайпы, мейлслоты) далеко не бесплатна. Но она дает возможность разносить процессы территориально. Это очень важно.

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

Евгений Охотников комментирует...

@имя:

Далее, я бы не стал закладываться на "разумность" проектировщиков ПО. Поскольку если программирование чему-то и учит, так это тому, что все ошибаются. _Очень часто_ ошибаются.

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

Евгений Охотников комментирует...

@имя:

Пример с HSM-ом был приведен только лишь как яркий пример того, что какой-то компонент может быть разработан на очень маленьком спектре языков (практически либо на C, либо на C++) -- все остальное может стать изрядным геморроем.