UGC-movies - сервис генерации контента для Онлайн кинотеатра "Movies".
Сервис предоставляет возможность обрабатывать огромное количество данных, генерируемых пользователями онлайн-кинотеатра. Например, данные о поведении пользователя во время просмотра фильма. Также это могут быть лайки, добавление в избранное, оценка фильма и т.д. На основании этих данных можно строить рекомендательную систему.
Благодаря Kafka, сервис способен обрабатывать огромное количество запросов в секунду. Данные хранятся в ClickHouse - OLAP хранилище, это позволяет мгновенно выполнять сложные аналитические запросы. Данные о лайках и избранном хранятся в MongoDB, что позволяет быстро выполнять запросы к данным и масштабировать хранилище горизонтально без предела.
- 🌟 Особенности
- ✅ Функционал сервиса
- 📦 Компоненты системы
- 🛁 Чистая архитектура
- 🚀 Быстрый старт
- 👉 Режим разработки
- 👉 Особенности разработки
🌟 Особенности 🔝
- Работает с Python 3.11;
- Полностью асинхронный;
- ClickHouse кластер - OLAP хранилище. Легко настраиваемый конфиг кластера;
- Kafka кластер - обработка потоковых данных:
- aiokafka - асинхронная работа с kafka;
- schema_registry - централизованное хранилище и валидация схем kafka;
- kafka-ui - веб-интерфейс для кластера kafka;
- Непрерывный ETL процесс из Kafka в ClickHouse
- MongoDB кластер - отказоустойчивый, масштабируемый кластер для хранение данных о лайках и избранном;
- FastAPI - быстрый и современный фреймворк для создания API;
- Nginx - веб-сервер для обработки запросов к API;
- Python инитеры - python-код для автоматической инициализации всех кластеров:
- Clickhouse initer;
- Kafka initer;
- Mongo initer;
- Полная интеграция с Docker:
- Docker-compose для локальной разработки;
- Тесты в Docker;
- Тонкие образы, благодаря multi-stage сборке;
- Makefile - удобный интерфейс для запуска команд проекта;
- Pre-commit hooks - автоматическая проверка кода перед коммитом для соблюдения стандартов разработки;
- Conventional commits - стандарт для написания коммитов;
- Применение SOLID принципов.
✅ Функционал сервиса 🔝
Сервис предоставляет следующий функционал:
- Фиксация события, произошедшего во время просмотра фильма (начало просмотра, пауза, перемотка вперед, назад, остановка);
- Фиксация текущей позиции воспроизведения фильма;
- Добавление времени просмотра фильма для построения рекомендательной системы;
- Добавление фильма в избранное. Получение списка избранных фильмов;
- Оценки фильмов.
Подробную документацию API можно посмотреть по адресу: http://127.0.0.1:8000/api/v1/openapi/
📦 Компоненты системы 🔝
🍔 Кластер MongoDB 🔝
Кластер MongoDB реализован по P-S-S архитектуре (Primary with Two Secondary Members).
- Config Server (1 сервер): mongo-configsvr01
- 2 шарда (на каждом реплика-сет из 3х реплик):
- mongo-shard01-a, mongo-shard01-b, mongo-shard01-c
- mongo-shard02-a, mongo-shard02-b, mongo-shard02-c
- Router (1 сервер): mongo-router01
🍔 Кластер Clickhouse 🔝
Кластер Clickhouse реализован с Clickhouse keeper - встроенный в Clickhouse механизм, который предоставляет систему координации репликации данных и выполнения распределенных DDL- запросов. Аналог ZooKeeper.
- 2 шарда по 2 реплики на каждом. Одна из реплик содержит keeper.
- Шард 1:
- clickhouse-shard1: c keeper;
- clickhouse-shard1-replica1: без keeper;
- Шард 3:
- clickhouse-shard2: c keeper;
- clickhouse-shard2-replica1: без keeper;
- clickhouse-keeper-quorum: нода без базы данных, только с keeper для достижения кворума при голосовании.
🍔 Кластер Kafka 🔝
Кластер Kafka состоит из 1 сервера для kafka broker
, 1 сервера для schema-registry
для хранения и
валидации схем топиков и сервера для UI интерфейса.
🛁 Чистая архитектура 🔝
Сервис построен на основе принципов чистой архитектуры. Бизнес-логика оперирует абстракциями и отделена от конкретных реализации таких как базы данных.
Например, рассмотрим, процесс добавления данных о времени, потраченном пользователем на просмотр фильма. Диаграмма последовательности процесса представлена ниже:
Разберем участников процесса:
Controller
- ендпоинт, обрабатывающий запрос от клиента;MovieViewingService
- выполняет бизнес логику;MovieViewingGateway
- шлюз данных. Это интерфейс, предоставляющий методы работы с базой данных. Весь код на языке запросов для конкретной базы данных должен находиться здесь;KafkaMovieViewingGateway
- конкретная реализация шлюзаMovieViewingGateway
для Kafka;DatabaseClient
- интерфейс, предоставляющий соединение с базой данных. Здесь присутствует методexecute
, который отправляет в базу данных запрос подготовленный в шлюзеMovieViewingGateway
;KafkaEventProducerClient
- конкретная реализация интерфейсаDatabaseClient
для Kafka.
Диаграмма классов выглядит следующим образом:
Классы в пакете Internal
реализуют бизнес логику приложения. Пакет API
- это контроллер. Пакет
Adapters
- это адаптеры для работы с конкретными системами.
Класс MovieViewingService
использует интерфейс MovieViewingGateway
для работы с базой данных.
Класс KafkaMovieViewingGateway
реализует этот интерфейс и выполняет операции в фактической
базе данных KafkaEventProducerClient
.
Если упростить диаграмму классов, то получим следующую картину:
Компонент BusinessRules
- это высокоуровневые политики приложения, здесь хранится вся бизнес-логика
приложения. Компонент Database
- это низкоуровневые политики, здесь хранится все, что связано с
конкретными системами, например, с конкретной базой данных.
Двойными линиями обозначены архитектурные границы. Направление стрелки указывает на то, что компонент
Database
знает о существовании компонента BusinessRules
. Компонент BusinessRules
не знает о
существовании компонента Database
. Это говорит о том, что интерфейс MovieViewingGateway
находятся
в компоненте BusinessRules
, а класс KafkaMovieViewingGateway
- в компоненте Database
.
Проведя границу между двумя компонентами и направив стрелку в сторону BusinessRules
, мы видим,
что компонент BusinessRules
мог бы использовать базу данных любого типа.
Компонент Database
можно заменить самыми разными реализациями - для BusinessRules
это совершенно неважно.
А это означает, что выбор базы данных можно отложить и сосредоточиться на реализации и тестировании
бизнес-правил.
Как видно из схемы, все стрелки (все зависимости) направлены внутрь - это доказывает соблюдение принципов чистой архитектуры:
Главным правилом, приводящим эту архитектуру в действие, является правило зависимостей (Dependency Rule): Зависимости в исходном коде должны быть направлены внутрь, в сторону высокоуровневых политик.
Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)
🚀 Быстрый старт 🔝
Все команды, приведенные в данном руководстве, выполняются из корневой директории проекта, если иное не указано в описании конкретной команды.
⚙️ Настройка переменных окружения 🔝
Создай файлы .env
и .env.local
в корне проекта, выполнив команду:
make env
Можно просто скопировать файлы env.template
и env.local.template
.
🏁 Запуск проекта 🔝
🚨 Убедись, что у тебя свободны все следующие порты:
- Все порты в
.env.local
; - Все порты раздела
Expose ports for clickhouse's nodes
в.env
; - Порт
KAFKA_BROKER_EXPOSE_PORT
в.env
.
🚨 Укажи свою платформу для docker образов в .env
. Например, для Mac на M1 вот так:
DOCKER_IMG_PLATFORM=linux/arm64
Запустить проект в докере:
make run
Приложение будет доступно по адресу http://localhost:8000.
Документация: http://localhost:8000/api/v1/openapi.
Запустить проект локально (должны быть запущены все докер-контейнеры кроме app
):
python src/movies_ugc/main.py
Запустить тесты в докере:
make test
Запустить тесты локально (должны быть запущены все докер-контейнеры):
make test-local
👉 Режим разработки 🔝
📚 Pre requirements 🔝
Для успешного развертывания среды разработки понадобится:
- Python ^3.11;
- Менеджер пакетов Poetry;
- Docker (version ^23.0.5). Если у тебя его еще нет, следуй инструкциям по установке;
- Docker compose (version ^2.17.3). Обратись к официальной документации для установки;
- Pre-commit.
Также будет полезным:
- Hadolint - линтер докер файлов.
🛠️ Создание среды разработки 🔝
Перед выполнением команд из этого раздела, убедись, что у тебя установлены все компоненты Pre requirements, в противном случае, смотри инструкции по установке в подразделах ниже.
Для создания среды разработки, выполни следующие команды одну за другой, из корневой директории проекта:
make env
poetry shell
make init
Те же команды без Makefile
:
cp .env.template .env
cp .env.local.template .env.local
poetry shell
poetry install
pre-commit install
pre-commit install --hook-type commit-msg
🌍 Переменные окружения 🔝
В проекте используются 2 файла с переменными окружения: .env
и .env.local
, для каждого из которых
есть соответствующие шаблоны: env.template
и env.local.template
.
Создай файлы .env
и .env.local
в корне проекта с помощью следующей команды:
make env
Файл .env.local
необходим для локальной разработки. Значения переменных в этом файле переопределяют значения
переменных из файла .env
при запуске приложений локально. Это полезно при разработке, когда
нужно запустить приложение на localhost
, а не в контейнере и указать специфический порт для него.
Рассмотрим пример с переменными APP_HOST
и APP_PORT
. В файле .env
указаны следующие значения:
APP_HOST=0.0.0.0
APP_PORT=8000
При запуске проекта в докере, приложение будет доступно на хосте app
(поскольку имя сервиса в
docker-compose.yml
указано как app
) по порту 8000
.
В файле .env.local
указаны следующие значения:
APP_HOST=localhost
APP_PORT=8000
При запуске проекта локально командой python src/movies_ugc/main.py
приложение будет доступно на хосте
localhost
по порту 8000
.
Отладчик в твоем любимом IDE также будет отлично работать.
Также ты можешь указать специфические порты для баз данных, если стандартные порты у тебя уже заняты.
📖 Установить Poetry 🔝
Подробнее про установку Poetry здесь.
Linux, macOS, Windows (WSL)
curl -sSL https://install.python-poetry.org | python3 -
Важно: после установки, необходимо добавить путь к Poetry в свой PATH
. Как правило, это делается
автоматически. Подробнее смотри в разделе Add Poetry to your PATH.
Проверить, что Poetry установлен корректно:
poetry --version
# Poetry (version 1.4.0)
🐳 Подробнее про Docker 🔝
Запуск проекта выполняется с помощью docker-compose. Проект содержит следующие файлы docker-compose:
- docker-compose.yml - главный файл;
- docker-compose.dev.yml - содержит только изменения относительно главного файла, необходимые для режима разработки;
- docker-compose.test.yml - содержит только изменения для запуска тестов;
Файл docker-compose.yml 🔝
Файл docker-compose.yml
- это главный compose-файл. Любая команда docker-compose
должна использовать
этот файл в качестве первого аргумента.
Файл содержит все сервисы проекта (кроме тестовых) и основные метаданные для каждого сервиса,
такие как build
, env_file
, depends_on
и т.п.
Файл docker-compose.yml
не должен содержать разделов с монтированием томов (volume
)
и указанием портов (ports
), особенно здесь не должно быть портов, смотрящих наружу.
Профили 🔝
Все сервисы в файле docker-compose.yml
сгруппированы по профилям. Профили необходимы для возможности
запуска только определенной группы сервисов.
Главный профиль - default
, все сервисы должны иметь этот профиль.
Существуют следующие профили:
default
olap
oltp
nosql
api
Например, к профилю nosql
относятся только сервисы кластера mongo
и больше ничего. Запустить исключительно
кластер mongo
можно следующей командой:
make run-nosql
Файл docker-compose.dev.yml 🔝
Файл docker-compose.dev.yml
используется для запуска проекта в режиме разработки. Здесь добавляются
изменения относительно docker-compose.yml
. Например, здесь можно примонтировать тома для папок приложения
и указать порты, смотрящие наружу, чтобы облегчить отладку.
Файл docker-compose.test.yml 🔝
Файл docker-compose.test.yml
содержит сервисы только для тестов. Этот файл используется для запуска
тестов в докере.
🔄 Режимы запуска 🔝
Основной тип запуска проекта - docker-compose. Запуск проекта может быть выполнен в 2-х режимах:
development
и production
. Режим запуска управляется настройкой ENVIRONMENT
в файле .env:
ENVIRONMENT=development
Если настройка ENVIRONMENT
имеет значение development
, то при запуске используется файл
docker-compose.yml. Если настройка ENVIRONMENT
имеет значение production
, то при
запуске используется дополнительный файл docker-compose.dev.yml.
При запуске в режиме development
папка приложения src монтируется как том, а каждый сервис
имеет expose
порты:
services:
app:
volumes:
- ./src:/app/src
ports:
- "8000:8000"
kafka-broker:
volumes:
- kafka_broker_data:/bitnami/kafka
ports:
- "${KAFKA_BROKER_EXPOSE_PORT}:${KAFKA_BROKER_EXTERNAL_PORT}"
📄 Один Dockerfile для двух режимов 🔝
Стоит обратить внимание на Dockerfile для базового Python образа python-src
.
Помимо multistage
сборки, данный файл использует слои development
и production
в соответствии с
настройкой ENVIRONMENT
:
# ./docker/python/Dockerfile
...
FROM final as development
FROM final as production
COPY ./$HOST_SRC_DIR ./$HOST_SRC_DIR
COPY ./init_db ./init_db
FROM ${env}
Данная конфигурация позволяет использовать разные итоговые образы в зависимости от режима запуска. При запуске
в режиме development
папка приложения src монтируется как том в файле docker-compose.dev.yml,
поэтому слой development
в Dockerfile
пустой:
# ./docker/python/Dockerfile
...
FROM final as development
...
При запуске в режиме production
папка приложения src копируется с помощью инструкции COPY
:
# ./docker/python/Dockerfile
ENV HOST_SRC_DIR=src
...
FROM final as production
COPY ./$HOST_SRC_DIR ./$HOST_SRC_DIR
В конце файла, выбирается образ из переменной env
:
# ./docker/python/Dockerfile
ARG env=production
...
FROM ${env}
Переменная env
в свою очередь передается как аргумент сборки:
# ./docker-compose.yml
services:
python-src:
build:
context: .
dockerfile: ./docker/python/Dockerfile
args:
env: ${ENVIRONMENT}
Итоговый образ будет использовать самый последний слой.
🔑 Essential services 🔝
Некоторые сервисы проекта используют одни и те же образы. Например, сервисы clickhouse-initer
,
kafka-initer
, mongo-initer
используют образ python-src
. Образ python-src
билдится сервисом
python-src
. Такие сервисы называются Essential services, а образы, которые они собирают
Essential images - обязательные образы, необходимые другим сервисам.
Перед запуском проекта необходимо сначала собрать все образы для обязательных сервисов и только потом
запускать проект через docker-compose
.
Собрать Essential images:
make prebuild
Нативная команда:
docker-compose build python-src clickhouse-default-node
🏁 Запуск проекта в докере 🔝
Файл docker-compose.yml использует профили, что позволяет запускать отдельные наборы
сервисов. Основной профиль - это default
, он указан для всех сервисов.
Запустить все сервисы проекта:
make run
Команда make run
автоматически определяет режим запуска из настройки ENVIRONMENT
.
Посмотреть все запущенные сервисы:
docker-compose ps
Нативная команда для запуска в режиме development
:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml --profile default up -d --build
Нативная команда для запуска в режиме production
:
docker-compose --profile default up -d --build
Запустить только кластер Kafka и API-приложение (--profile oltp
):
make run-oltp
Запустить только кластер Clickhouse (--profile olap
):
make run-olap
Запустить только кластер MongoDB (--profile nosql
):
make run-nosql
Запустить кластер Kafka, кластер MongoDB и API-приложение (--profile api
):
make run-api
🔧 Полезные команды 🔝
Запустить несколько определенных сервисов:
make run s="kafka-initer mongo-initer"
Зайти внутрь контейнера:
make bash
Далее ввести имя сервиса.
Посмотреть логи сервиса:
make logs
Далее ввести имя сервиса.
Остановить все сервисы и удалить контейнеры:
make down
Посмотреть текущий конфиг docker-compose:
make config
Удалить все неиспользуемые образы, контейнеры и тома:
make remove
💻 Локальный запуск 🔝
Запустить приложение локально полезно, чтобы иметь возможность пользоваться отладчиком. Для локального
запуска необходимо чтобы были запущены сервисы профайла api
.
Запусти сервисы профайла api
:
make run-api
Данная команда запустит кластеры Mongo и Kafka, а так же само приложение - сервис app
- его нужно остановить.
Останови сервис app
:
make stop
Введи имя сервиса app
:
❯ make stop
Containers name (press Enter to stop all containers): app
Теперь можно запускать приложение, используя файл main.py
:
python src/movies_ugc/main.py
Приложение будет доступно по адресу http://localhost:8000.
Файл main.py
можно запускать отладчиком PyCharm.
👉 Особенности разработки 🔝
При разработке необходимо придерживаться установленных правил оформления кода. В этом разделе ты найдешь описание настроек редактора кода, линтеры и форматеры, используемые в проекте, а также другие особенности, которые необходимо учитывать при разработке.
🔗 Управление зависимостями 🔝
В качестве пакетного менеджера используется Poetry.
Для управления зависимостями используются группы (см. файл pyproject.toml
).
Все основные зависимости располагаются в группе tool.poetry.dependencies
:
[tool.poetry.dependencies]
python = "^3.11"
Добавление основной зависимости:
poetry add pendulum
Остальные зависимости делятся на группы. Например, группа lint
- зависимостей для линтинга:
[tool.poetry.group.lint.dependencies]
flake8 = "^6.0.0"
flake8-quotes = "^3.3.2"
pep8-naming = "^0.13.3"
Добавление зависимости в конкретную группу (используй флаг --group
и название группы):
poetry add pytest --group=test
📝 Conventional Commits 🔝
Твои комментарии к коммитам должны соответствовать Conventional Commits.
Pre-commit хук conventional-pre-commit
выполнит проверку комментария перед коммитом.
Если твой комментарий не соответствует конвенции, то в терминале ты увидишь подобное сообщение:
commitizen check.........................................................Failed
- hook id: conventional-pre-commit
- exit code: 1
Для более удобного написания комментариев к коммитам, ты можешь воспользоваться плагином Conventional Commit для PyCharm:
🖥️ Настройки IDE 🔝
Проект содержит файл .editorconfig
- ознакомься с ним, чтобы узнать какие настройки
должны быть в твоем редакторе.
Основное:
- максимальная длина строки: 110;
- отступы: пробелы;
- количество отступов: 4.
✨ Форматер и линтер 🔝
В качестве форматера мы используем black.
Конфиг black см. в файле pyproject.toml
в секции [tool.black]
.
Линтер - flake8, конфиг находится в файле setup.cfg
.
Если ты используешь PyCharm, то можешь настроить форматирование файла с помощью black через External Tools:
Также можно повесить на это действие hot key: