Общая часть
Привет, читатель, хочу поделиться своей историей о разработке проекта для сбора фриланс заказов, на данный момент с русских фриланс бирж, реализованных на языке джава, то есть агрегатора. Разработка проекта https://github.com/gdevby/alert-job была начата 15.10.2022, запущенный проект доступен https://aj.gdev.by. Данная статья будет интересна следующим группам людей:
Кто хочет попробовать оптимизировать получения уведомлений о новых заказах с фриланс бирж, то есть уменьшить время затраты на эту задачу
Кто хочет сравнить свой тестовый проект в области java spring для микросервисной архитектуры.
Кто хочет попробовать разработать приложения с микросервисной архитектурой с помощью java spring.
Кто хочет запустить такие проекты в докере на отдельном виртуальном сервере. В статье сначала описываются общие вопросы, после описываются детали, которые могут вызвать интерес для джава разработчиков.
Идея проекта появилась из задачи поиска работы на фриланс бирже. Поиск аналогичных проектов на тот момент ни к чему не привел, хотя после реализации мы смогли найти несколько. Так как ежедневная проверка заказов на нескольких биржах и выборка по критериям спустя два месяца утомила, нужна была какая-то автоматизация, чтобы можно было очень гибко настроить и потом уже просматривать заказы, которые с большой долей вероятности подходили бы. В среднем уходило около 20-45 минут каждый день. Сначала я сделал базовое ядро для себя, это было просто консольное приложение, оно настраивалось через файлы настроек, потом захотел поделиться с другими, то есть необходимо полноценное веб приложение, которое будет доступно для каждого с помощью веб браузера.
Каким функционалом должна данная система обладать:
Фильтровать заказы по технологиям
Фильтровать заказы по названию
Фильтровать заказы по описанию
Фильтровать заказы по цене
Должны быть исключающие слова, чтобы отбросить заказы, которые вам не подойдут, например заказ, название которое содержит «для учёбы»
Настройка уведомлений, чтобы не беспокоить в ночное время и выходные
Теперь поговорим об уже готовой реализации и представлении сервиса и рассмотрим, как мы можем создать свой фильтр. Здесь будут пропущены шаги создания аккаунта на данном сервисе, так как это стандартная задача.
В данной системе есть понятие модуль, это вид деятельности, по которому будут собираться заказы, к примеру «Бэк или Мобильная разработка». Он служит, чтобы объединять информацию с нескольких бирж для «Бэк или Мобильная разработка», чтобы указать ключевые слова для данного типа заказов
Дальше указываем источники - это откуда будут браться заказы, мы можем указать сразу несколько источников (фриланс бирж) и категории для них
Теперь настраиваем «Фильтр» на то, какие мы будем заказы получить, то есть указываем ключевые слова и слова-минусы, если данное слово содержится в определенном месте (например, только для описания), то этот заказ будет отброшен, так как вы точно знаете, что это не ваш заказ.
Так же хотелось бы знать, какие заказы к вам не приходят, чтобы изменить фильтры, то есть иметь быструю связь на изменения в фильтрах
Настройка уведомлений имеет такую форму. В нем можно задать промежутки и дни, когда можно получать уведомления
На этом общее описание функционала и задач подошло к концу, дальше у нас пойдет описание разработки: что и как делали. Мы так же добавляем новые биржи по запросам, на данный момент 5 бирж, хотя проблемы есть с FREELANCEHUNT.COM — не работает, они
деактивировали мой аккаунт, да и нет моей страны в списке, видно, мое происхождение им не нравится.
Разработка
Так как этот проект создавался на свободных началах, как открытый проект, то было решено взять технологии, с которыми мы не работали, чтобы жизнь простой не казалась. В результате было решено написать его в реактивном стиле или хотя бы максимально приблизиться к этому. В первую очередь мы решили попробовать spring cloud, spring webflux, так же решили обкатать протокол SSE https://ru.wikipedia.org/wiki/Server-sent_events, хотя по нему было не так много документации, но он хорошо подходил для связи между двумя сервисами для получения новых заказов. На фронт решили попробовать React JS. В целом микросервисная архитектура для такого типа проекта как сбор заказов не нужна, достаточно было бы реализовать монолитное веб приложение. Но так как у нас много времени и очень сильное желание пробовать новые вещи, мы решились на это, понимая, что некоторые проблемы могут нас преследовать. Это потребует временных затрат в два-три раза больше, чем если бы мы использовали текущие знания и наработки.
В нашей команде было три человека, это 2 бэк разработчика и один фронт разработчик, в теории я ожидал что мы уложимся за 60 дней в худшем случае, так как это проект на свободных началах, то есть без оплаты. Мы могли делать этот проект не полный день, а когда есть свободное время от других задач. В результате было несколько критичных проблем, иногда некоторые сервисы переставали работать, поэтому приложение полноценно заработало только через 150 дней, хотя прототип был готов уже после 50 дней работы.
Модули, из которых состоит приложение, микросервисную архитектуру мы разбили так:
alert-job-config-repo — репозиторий с конфигурацией, храним здесь все конфиги для дев и для прода
alert-job-config — отдает конфигурацию для бизнес логики по http, когда сервис запускатеся он обращается сюда и получает конфигурацию, с которым запустится
alert-job-eureka — для регистрации и связи сервисов
alert-job-gateway — предоставляет единую точку входа запросов
keycloak — отвечает за авторизацию и аутентификацию open authenticaion 2.0 протокол(oauth 2.0)
logstash — отвечает за сбор логов
front — мы использовали react js
notification-alert-job — отвечает за отправку сообщений о заказах
prometheus и grafana - за отображение логов, метрики, и оповещений об ошибках уровня ERROR
Docker — технология для запуска контейнеров
nginx-proxy — вспомогательный контейнер для конфигурации доменных имен и ssl сертификатов.
Parser-alert-job — отвечает за парсинг заказов
core-alert-job — основной бэк для взаимодействия с сайтом
Интересные выжимки из сервисов при решении проблем
Для сбора логов и анализа, мы сперва смотрели на одно из популярных решений - ELK stack, но в ходе изучения столкнулись с тем, что необходима защита сервисов на уровне Kibana, для этого требуется платная лицензия. Выход нашли из ситуации — перенесли эти требования на grafana, там можно получать логи и посылать уведомления.
При работе с sse и webflux через некоторое время у нас перестали поступать заказы с нашего сервиса, это проблема повторилась один раз. Что-то похожее у нас было, когда на уровне подписки возникала ошибка и тогда подписка не восстанавливалась, тогда мы обернули проблемный код try catch, чтобы не убивало подписку webflux, после перезапуска у нас все заработало, пока наблюдаем
@GetMapping(value = "/stream-sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<List<OrderDTO>>> streamFlruEvents() {
log.trace("subscribed on orders");
Flux<ServerSentEvent<List<OrderDTO>>> flruFlux = Flux
.interval(Duration.ofSeconds(parserInterval)).map(sequence -> ServerSentEvent.<List<OrderDTO>>builder()
.id(String.valueOf(sequence)).event("periodic-flru-parse-event").data(fl.flruParser()).build())
.doOnNext(s -> {
int size = s.data().size();
context.getBean(COUNTER_FLRU, Counter.class).increment(size);
});
...
return Flux.merge(flruFlux, hubrFlux, freelanceRuFlux, weblancerFlux, freelancehuntOrderParcerFlux);
}
И сама подписка на заказы
public void sseConnection() {
ParameterizedTypeReference<ServerSentEvent<List<OrderDTO>>> type = new ParameterizedTypeReference<ServerSentEvent<List<OrderDTO>>>() {
};
Flux<ServerSentEvent<List<OrderDTO>>> sseConection = webClient.get().uri("http://parser:8017/api/stream-sse")
.accept(MediaType.TEXT_EVENT_STREAM).retrieve().bodyToFlux(type)
.doOnSubscribe(s -> log.info("trying subscribe"))
.retryWhen(Retry.backoff(Integer.MAX_VALUE, Duration.ofSeconds(30)));
sseConection.subscribe(event -> {
try {
log.trace("got elements by subscription {} size {}", event.event(), event.data().size());
Set<AppUser> users = userRepository.findAllUsersEagerOrderModules();
forEachOrders(users, event.data());
} catch (Throwable ex) {
log.error("problem with subscribe", ex);
}
}, error -> log.warn("failed to get orders from parser {}", error));
}
При запуске в докере требовалось чтобы порт был прописан, хотя мы считали, что эврика это разрешит. В результате обращались мы к серверу с помощью http://host:port
Elasticsearch не стартовал корректно для logstash, при такой конфигурации. Для корректировки поведения необходимо было изменить права доступа для этой директории, в которую смонтирован elasticsearch chmod 777 public
elasticsearch:
restart: always
image: elasticsearch:8.9.2
container_name: elasticsearch
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms256m -Xmx1024m
- xpack.security.enabled=false
- TZ=Europe/Moscow
volumes:
- $elasticsearch_directory:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9200/?pretty"]
interval: 10s
timeout: 10s
retries: 3
start_period: 60s
Проблема keycloak, так как были разные докер контейнеры keycloak, попробовал использовать quay.io/keycloak/keycloak, но когда я создавал конфигурацию, я не мог сохранить это в файл конфигах, чтобы потом любой пользователь смог построить образ с нужными настройками, готовый для развертывания в среде разработки, здесь описан мой пример решения проблемы https://keycloak.discourse.group/t/keycloak-17-docker-container-how-to-export-import-realm-import-must-be-done-on-container-startup/13619/23
Что касается безопасности, было реализовано только на уровне gateway, это единая точка входа. Нижележащим сервисам передаются уже данные пользователя в аргументах. Хотели реализовать просто, не дублировать код для каждого сервиса. Как оказалось, не так уж и много будем дублировать, можно создать общий модуль и основную логику реализовать там. И для каждого сервиса реализовать проверку прав для доступа, то есть проверка jwt и извлечения данных из токена.
Интересный докер образ мы нашли при настройке обратного прокси и https сертификата. Это nginx-proxy и nginx-proxy-acme. С помощью данных образов можно получать сертификаты, генерировать прокси маршрутизацию nginx для твоих контейнеров, которые находится в одной сети.
Все динамические параметры для докера мы описывали .env, оказался очень удобный способ для задания параметров для docker compose
Вывод
Я рад, что взялся за такого рода проект, так как принесло практический и теоретический опыт. Так же смогли решить качественно эту задачу. Времени на написание было затрачено много и администрирование, поддержка будет отнимать какое-то время, но это ожидаемые затраты.
Книги, которые помогли реализовать приложение:
Использование docker - Эдриен Моуэт
ELK - Шукла,Кумар
Микросервисы spring в действии - Карнем, Санчес
Spring security in action - Laurentiu Spilca
Практика реактивного программирования в spring — Докука, Лозинский