Некоторое время назад мы вместе с небольшой командой программистов начали разработку достаточно интересного с технической точки зрения аналитического проекта. Основной его целью была обработка данных, получаемых с различных веб-страниц. Нужно было обрабатывать эти данные, приводя в удобный вид и после этого анализировать собранную статистику.
До тех пор, пока у нас не было большого количества всевозможных данных, мы не имели каких-то нестандартных проблем и все решения были достаточно прямолинейными. Но проект разрастался, и размер собираемой информации, хотя сначала и не очень быстро, но все же увеличивался. Росла и кодовая база. И через некоторое время мы осознали весьма печальный факт — из-за всяких костылей и быстро-фиксов мы нарушили почти все возможные принципы проектирования. И если сначала организация кода была не столь важна, то со временем стало понятно, что без хорошего рефакторинга далеко мы не уедем.
После обсуждений и размышлений было решено, что для наших целей архитектура “парсилки” интернетов в первую очередь должна быть сервис-ориентированной(SOA). Далее мы отталкивались от этого подхода и выделили три осноных части будущей системы, отвечающих за следующие задачи:
- Получение содержимого страниц, данных из различных сервисов через API, данных из структурированных файлов
- Структуризация полученной информации
- Анализ статистики и создание рекомендаций
В результате такого разделения должны были получиться три независимых сервиса: Fetch Service, Parse Service и Analyze Service
* Здесь и далее буду использовать некоторые англоязычные названия для большего удобства восприятия и краткости
Далее встал вопрос о том, как эти сервисы будут общаться друг с другом. Определяя общий механизм было решено использовать концепцию конвейерной обработки(pipeline). Достаточно понятный и простой подход, когда нужно последовательно обработать какую-либо информацию, передавая её от одного узла к другому. В качестве коммуникационной шины был выбран механизм очередей на базе RabbitMQ.
Итак, с основной архитектурной моделью определились. Она получилась достаточно простой, понятной и вполне расширяемой. Далее опишу то, из чего состоит каждый сервис и что позволяет их масштабировать.
Компоненты сервисов и технологии
Давайте немного поговорим о технологиях, которые используются внутри каждого отдельного сервиса. В данной статье я в основном буду описывать то как работает Fetch Service. Однако остальные сервисы имеют аналогичную архитектуру. Ниже опишу общие моменты, а точнее основные компоненты. Всего их четыре.
Первый — это модуль обработки данных(proccessing module), который содержит всю основну логику работы с данными. Представляет из себя набор воркеров, которые выполняют задачи. И клиентов, которые эти задачи создают. Здесь используется Gearman в качестве сервера задач и соответственно его API. Сами воркеры и клиенты являются отдельными процессами, которые контролируются с помощью Supervisord.
Следующий компонент — хранилище результатов. Которое является базой данных в MongoDB. В основном данные извлекаются из веб-страниц, либо через различные API, возвращающие JSON. И MongoDB достаточно удобна для хранения такого вида информации. Кроме того, структура результатов может меняться, могут появляться новые метрики и тд. И в данном случае мы можем легко вносить изменения в структуру документов.
И наконец, третий компонент системы — очереди. Есть два типа очередей. Первые занимаются тем, что служат для передачи запросов к сервисам от других сервисов или же от внешних клиентов(не путать с Gearman-клиентами). Такие очереди именуются как Request Queues. В случае с упомянутым ранее сервисом получения контента (Fetch Service) в очередь такого типа поступает JSON строка. Она содержит URL нужной страницы или параметры для запроса к стороннему API.
Второй тип очередей — это очереди уведомлений(Notifications Queues). В очередь такого типа сервисы помещают информацию о запросах, которые были обработаны и результат может быть получен из хранилища. Таким образом реализуется асинхронность выполнения запросов на получение, обработку и анализ данных.
В качестве брокера сообщений был выбран RabbitMQ. Это хорошее решение, работает отлично, хоть и с некоторыми заморочками. Однако, для такой системы является уж слишком навороченным, поэтому возможно будет лучше заменить его на что-то более простое.
Коммуникация
Итак, коммуникация обеспечивается за счет очередей и это очевидный и удобный способ связать между собой различные сервисы. Далее я опишу процесс коммуникации более детально.
Существует два типа коммуникации. Внутри системы, между сервисами. И между конечным клиентом и всей системой в целом.
Например, для Parse Service требуются новые данные. Он посылает запрос в очередь для Fetch Service и дальше продолжает заниматься своими делами — запросы выполняются асинхронно.
После того, как Fetch Service получит запрос из очереди, он выполнит необходимые действия по извлtчению данных из нужного источника(web-страница, файл, API) и поместит их в хранилище(MongoDB). И далее отправит уведомление о завершении операции, которое в свою очередь получит Parse Service чтобы обработать свежие данные.
Fetch Service
И напоследок расскажу немного подробнее про сервис, отвечающий за получение исходных данных из внешних источников.
Эта базовая часть системы является первой стадией в конвейере обработки данных. На него ложится решение следующих задач:
- Получение данных из внешнего источника
- Обработка исключительных ситуаций и ошибок на этой стадии(Например обработка ответов HTTP)
- Предоставление основной информации о полученных данных (заголовки, статистика изменений файлов и тп.)
Само по себе извлечение исходных данных является важной частью большинства систем, где производится структуризация данных. И сервис-ориентированный подход в данном случае весьма удобен.
Мы просто говорим: “Дай мне вот эти данные” и получаем то, что хотим. Кроме того, такой подход позволяет создавать различные воркеры для получения информации из специфических источников не заставляя клиентов думать о том, откуда конкретно будет поступать материал для обработки. Можно использовать различные API, с разными форматами и протоколами. Вся логика получения целевых данных изолирована на этом уровне.
В свою очередь поверх данного сервиса могут быть построены другие, реализующие более конкретную логику вроде обхода сайтов, парсинг, агрегацию и тд. Но каждый раз нет необходимости заботиться о сетевых взаимодействиях и обработке множества ситуаций.
На этом я пожалуй закончу. Разумеется есть еще множество аспектов разработки подобных систем. Но главное, что стоит помнить — всегда в первую очередь нужно подумать об архитектуре и использовать принцип единственной обязанности. Изолировать компоненты системы и связать их простым и понятным способом. И вы получите результат, который легко масштабировать, легко контролировать и очень просто использовать в дальнейшем.
Автор: zarincheg