Приложение, состоит из двух инстансов.
- нативное приложение для обработки пользовательских запросов
- Java приложение для парсинга контента из источников Ни одно приложение не хранит состояние. Масштабируются горизонтально.
Обоснование такого решения привел в руководстве по эксплуатации
- Архитектурное решение
- Производительность
- Параллельную неблокирующую работу отдельных компонентов приложения
- Потребление ресурсов
- Уникальность контента в приложении
- Гибкая настройка источников мемов
- REST-парадигмa
- Контейниризация Docker
- Конфигурироваться через environment-переменные
- Дополнительные языки
- Сортировка ленты от более интересного контента к менее интересному
- Противодействие rate-limiting
- healthcheck и readiness check
- endpoint с Prometheus-метриками
- OpenAPI-документация
- Руководстве по эксплуатации
- Клаассные фишки
Оба приложения разворачиваются в Docker.
Предлагаю воспользоваться Docker-compose для разворачивания. Для этого подготовил файлы с конфигурацией окружения :
core.env // для приложения Core
parser.env // для приложения Parser
в них прописываются все доступы к окружению (Infinispan, MongoDb, Minio, RedisBloom). Для запуска достаточно запустить скрипт :
$ up.sh
Разделил приложение на две части.
- сore - обработка пользовательских запросов
- parser - получение контента от источников
Для дистрибуции Core приложения выбрал нативную сборку. Для Parser Jvm приложение.
Решение принял на основании тестов benchmark. И ограничений накладываемых на приложения запускаемые в Docker и нативные приложения.
Для кэширования данных выбрал Infinispan. Предлагает бинарный протокол и лучшее время работы в кластере.
Для персистентного хранилища выбрал MongoDB.
Для хранения медиа фалйов выбрал Minio.
Для проверки дубликатов выбрал Redis c фльтром Bloom.
Обоснование такого решения привел в руководстве по эксплуатации
Приложение обрабатывающее запросы пользователей может запускаться в двух вариантах, как нативное и как JVM приложение. Тестировались приложения в двух этих режимах. В режиме JVM приложение обеспечивает лучшую пропускную способность и время отклика, чем приложение в нативном режиме. Однако использует на 277% больше RSS памяти. Теоретически возможно улучшить показатели пропускной способности нативного приложения в три раза, запустив несколько экземляров. Тестирование приложений продолжалось 3 часа, без перезапуска, нативное приложение обработало 33 млн запросов. Тесты проводились без прогревания.
Для тестирования использовался WRK:
$ core/sh/benchmark/wrk.sh
Для статистических данных AB:
$ core/sh/benchmark/ab.sh
Для определения памяти PS:
$ ps -o rss -p <PID>
####Startup Time Самый важный показатель для приложений развернутых в Docker. Оценивал время от запуска до первого запроса. Предлагаю оценивать этот показатель по следующей методике.
В отдельном терминале запускаем скрипт :
$ core/sh/performance/startuptime.sh
В другом терминале запускаем приложение :
$ date +"%T.%3N" && target/core-1.0-SNAPSHOT-runner
В логах будет строчка :
В консоле будет вывод значения времени запуска приложения в милисекундах:
first timestamp : 17:13:26.536 final timestamp : 17:13:26.624
startup time : 188 milliseconds
####Memory usage
Для измерения объема памяти измерения проводились не для Heap JVM, а для RSS памяти, включающей в себя Heap, метаданные, стек тредсов, компиляцию, утилизацию. В случае работы с Docker более показательно.
Поскольку кубер будет убивать процесс на основе именно RSS памяти.
Среднее значение памяти в результате тестов нативного приложения было : 214 Мб
Для тестирования использовался скрипт:
$ core/sh/performance/rss.sh
Проиложение Parser написано с использованием фреймворка Vertx. Обеспечивающего паралельную работу потоков не в ForkJoinPool, а в собственной реализации потоков. Обеспечивающих работу воркеров в параллельных неблокирующих потоках. Даже при обращении в жесткому диску.
Для управления производительностью можно задать максимально количество потоков через конфиг приложения. Если этот параметр не определен, приложение будет использовать N+1 потоков, где N - число доступных процессоров.
На сайте, который использовал для парсинга, почти все мемы сопровождаются текстовым описанием. Поэтому разделил фильтрацию на дву части:
- фильтрация по тексту, на основе фильтра Блума
- фильтрация по хешу от содежимого файла, на основе murmur_32 Фильтр Блума реализован на базе реализации RedisBloom filter, хеширование файлов на основе Guava Hashing.
Каждый источник мемов добавляется через конфигурацию приложения. Для примера приведу текущую конфигурацию
application.url = https://de.orschlurch.net // url сайта
application.seedSelector = a[class=page-link] // селектор для сидов
application.contentPageSelector = h4[class=card-title] > a // селектор адресов страниц
application.contentPageUrlPattern = https://de.orschlurch.net%s // паттерн адресов страниц
application.imageSelector = .row-pix img // селектор списка изображений
application.videoSelector = div [class=container] > video[controls] > source // селектор видео файлов
application.descriptionSelector = div [class=container] > p // селектор описаний
application.titleSelector = div [class=site-title] > h1 // селектор названий
application.upsSelector = span.lnd // селектор лайков
application.textLanguages = de // список языков
В соответсвии с заданием:
endpoint GET /feed[?page=*] // возвращает постраничный список контента
endpoint GET /feed/* // возвращает отдельный контент
Список языков контент на которых может быть распарсен устанавливается в конфигурации приложения или через env переменные:
application.textLanguages = de,en // список языков
Сортировка контента определяется в файле конфигурации. Для того, что бы изменить сортировку по времени на сортировку по лайкам достаточно заменить значение параметра "rest.feed.sort.field=timestamp" :
rest.feed.sort.field=timestamp // заменить на ups
Использовал список простых HTTP проксей для парсинга. Он задается в конфигурации или через env переменные:
application.proxies = 43.246.140.4:43091, 124.122.156.168:8118
В дальнейшем можно и более сложные прокси использовать, просто на этапе разработки у меня не оказалось платной подписки. Не смог протетстировать сок5 например.
endpoint /health/live - приложение запущено и работает
endpoint /health/ready - приложение готово обслуживать запросы
endpoint /health - общий чек для двух состояний
Для полчения метрик :
endpont /metrics/
endpont /metrics/application
Для запуска Prometheus использовал :
$ core/sh/prometheus.sh
Для конфигурации Prometheus использовал темплейт :
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first.rules"
# - "second.rules"
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['localhost:9090']
- job_name: core
static_configs:
- targets: ['localhost:8080']
Реализованана через нотификации в коде приложения.
endpoint /swagger-ui - Swagger UI
endpoint /swagger - экспорт для клиентских приложений
Добавил CircuitBreaker при обращении к кэшу. Ожидает прекращения доступа к InfiniSpan. И Retry, повторные попытки получения, если первые запросы завершились неудачно. Например при расхождении синхронизации Mongo и InfiniSpan.
Добавил логирование ошибок в Sentry. Каждый exception который будет выкидываться в приложении, будет логирвоаться сортироваться по типу в Sentry.