Integration Patterns: Shared DB, state machine и очередь сообщений

14 января 2014 г.

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

Вводная: Что такое Shared DB?

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

Недавно я делал выступление на тему шаблонов интеграции, там были рассмотрены плюсы и минусы такого стиля интеграции.

Краулинг данных и общая БД

Web crawling довольно популярная задача, которая часто встречалась мне в проектах. Мы рассмотрим простой пример краулинга без обработки ошибок и других ответвлений:

  1. Сбор данных: сервис заходит на веб-сайт, использует поиск, постраничную навиграцию и другие возможности, чтобы собрать URL'ы для скачивания. Например, если мы краулим каталог продуктов, то нам будут нужны ссылки на детали каждого продукта.
  2. Скачивание HTML: сервис берет подготовленные на предыдущем шаге URL'ы, по каждому из них скачивает HTML и сохраняет результат
  3. Создание сущностей: скачанный HTML парсится, создаются сущности нашей системы и сохраняются
  4. Анализ данных: новые сущности были созданы, но еще не обработаны. Осталось провести по ним аналитику и использовать данные в нашей системе (вывести их в UI, пересчитать статистику, сделать рассылку и т.п.)

Под каждое состояние в графе мы создадим свой сервис (обычно многопоточное приложение), потом запустим параллельно много сервисов для достижения нужной скорости скачивания данных и их обработки. Например, сбор URL'ов могут осуществлять сразу десятки "пауков" в несколько потоков, выдавая список URL'ов единым списком для следущего состояния.

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

Сервисы используют результаты работы друг друга. Как они узнают для каких URL уже скачан HTML? Какой HTML обработан? Используется два основных подхода:

  1. Данные от всех сервисов сохраняются в одну таблицу. Каждая запись помечаются флагом состояния. Каждый сервис в стороку БД, где хранятся данных краулинга, выставляет свою цифру. Например, скачали HTML - выставили состояние 2. Следующий сервис ищет сроки в состоянии 2, обрабатывает и ставит цифру 3 и т.д.
  2. Сохранение результатов каждого шага в отдельной таблице. Например, сервис по парсингу HTML выбирает данные из таблицы UrlTable, обрабатывает и помещает в таблицу RawHtmlTable для следующего сервиса. Надо не забывать, что запущено много однотипных сервисов и они будут брать данные из одного места. Поэтому если сервис по скачиванию HTML набрал URL'ов для обработки, то он удаляет их из таблицы UrlTable или помечает обработанными.

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

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

Краулинг данных и очередь сообщений

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

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

Что делать, если новые URL поступают слишком быстро и сервисы не успевают скачивать HTML? Ускорим их или просто подключим еще пару десятков сервисов, скачиющих HTML, к шине.

Обратите внимание, что БД может оставаться для фиксированая промежуточных результатов и статистики обработки. Конечный результат также записывается в БД.

Особенности подходов и типовые решения проблем

Общая БД

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

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

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

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

Очередь сообщений

Самый главный плюс - это простое регулирование нагрузки и маршрутизации сообщений между сервисами.

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

Что будет если один из узлов сломается? Например, в нашей схеме сервер с сервисами для состояния 2 отключится. Сервисы 1 и 3 продолжат работать. Сервис 1 будет отправлять в очередь новые URL'ы, они будут копиться в очереди до восстановления сервисов 2. А сервисы 3 будут выбирать из очереди то, что в ней уже было, после этого остановятся в ожидании новых данных.

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

Скорость обмена сообщениями зависит от объема сообщений. Некоторые облачные решения, например, IronMQ, даже имеют ограничения на размер сообщения. Что делать, если надо пересылать "толстые" сообщения? Помогает простое решение - их можно сжимать в Zip или 7z на одном конце и разжимать при приеме. В этом случае мы немного повышаем нагрузку на сервисы, но ускоряем передачу соощений по шине.

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

Можем ли мы ответить на вопрос: Когда будет обработан конкретный URL? Если у нас миллионы сообщений в очереди, то можно логировать перемещение конкретных данных по ней, но бывает проблема, что нельзя дать конкретный ответ.

Что выбрать?

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

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

  1. Александр Багдад14 января 2014 г. в 12:53

    Возможно стоило как-то подробнее описать, что Вы имеете в виду под общей шиной? Сейчас можно сделать вывод, что это такая БД, но только шина=)

    ОтветитьУдалить
  2. Да, спасибо, я думал, что это очевидно. Например, RabbitMQ или IronMQ.

    ОтветитьУдалить
  3. Александр Багдад14 января 2014 г. в 13:00

    Интересует не просто название, было бы не плохо, если бы Вы описали то, как используя любую из существующих очередей можно распараллелить данный пример. Например сервисы сбора данных работают с одной(несколькими очередями), а те кто скачивают HTML конкуррентно забирают URL, удаляя их из очереди, ну и т.д.

    ОтветитьУдалить
  4. Хм, это основы работы с очередями, я думаю сейчас описывать не буду. Может отдельно опишу, как эти инструменты работают. Для примера можно заглянуть сюда http://www.rabbitmq.com/getstarted.html

    ОтветитьУдалить
  5. Gregor Hohpe не зря так ратует за Messaging, Вас ведь он тоже убедил использовать именно этот интеграционный стиль? Еще было бы чрезвычайно интересно узнать, какие Messaging Patterns вы используете в реальных проектах.

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

    На счет конкретных паттернов, если взять тот же RabbitMQ, то большая часть из них уже реализована в этом инструменте http://www.rabbitmq.com/how.html#messaging-patterns

    С одной стороны, это как начинать программировать на C# и не написать ни одной программы на ASM, можно не понять сути и не "почувствовать железо". С другой стороны теории по этим шаблонам достаточно, чтобы осознанно использовать очередь сообщений, но не хватит, если самому захочется написать брокер сообщений. Второе пока для меня не приоритетно.

    ОтветитьУдалить
  7. У вас опечатка в заголовке (Intergration).

    ОтветитьУдалить
  8. Александр Понеметски24 мая 2014 г. в 18:26

    Спасибо за статью. Обращаюсь к вам за советом по архитектуре. Имеется система краулинга состоящая из таких частей:

    1) 2-5 пауков (консольные приложения которые скачивают страницы, парсят их, создают объекты, сериализуют полученные массивы объектов в xml и сохраняют их на диск)
    2) программа сохранения в базу (следит за папкой в которую пауки кладут xml, при появлении там файлов начинает процесс вставки в базу) специфика заключается в том что перед вставкой каждого объекта нужно понять если он уже в базе или нет, то есть нужно сделать select запрос а потом insert (или update если что то изменилось) что бьет по производительности
    3) сайт который отображает объекты собранные пауками и осуществляет поиск по ним. огромное количество результатов поиска с пагинацией которая вызывает проблемы с производительностью

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



    объекты которые собираются пауками хранятся в двух таблицах:

    1) Сущности (поля которые используются для списков, для поиска - частично денормализована) ~5млн записей

    2) Атрибуты (Ид сущности, ключ, значение, дата изменения, версия) ~130млн записей


    тормозит сайт и программа рассылки (3 и 4 компоненты) особенно когда все вместе начинают обращаться к бд


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

    на одном запущен сайт, а на втором остальные компоненты



    настроили две репликации A->B и B->A так как сайт тоже пишет в базу и эти данные нужны рассылальщику (в основном данные конечно идут в одну сторону)



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


    заранее спасибо

    ОтветитьУдалить
  9. Александр, у вас типовая ситуация. Буквально недавно написал статью на эту тему http://blog.byndyu.ru/2014/05/blog-post.html с готовой архитектурой. Если останутся вопросы, то пишите комментарием или мне на почту.

    ОтветитьУдалить
  10. Ну и что же такого особенного в очередях сообщений, чего нельзя сделать через БД? Всё тоже самое, только неконтролируемо. Сообщения хранятся на диске и БД хранится на диске. Может быть затык на уровне БД, но точно такой же затык может произойти на уровне очередей. Так что плюсы не очевидны, очередная модная абстракция.

    >Поэтому проблемы с упорядочиванием изменений нет
    А вот это вообще неправда. Если сервис 1 и 2 разгребают очередь, сервис 1 берёт сообщение 1, сервис 2 берёт сообщение 2. Нет никакой гарантии, что сервис 1 обработает сообщение быстрее чем сервис 2. В таком случае порядок обработки сообщений будет нарушен.

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...