В последнее время, с усложнением веб-сервисов всё более актуальной и даже необходимой становится микросервисная архитектура. Она отличается высокой отказоустойчивостью, масштабированием и сложностью проектирования и поддержки.
Рассмотрим небольшой пример, демонстрирующий современные технологии интеграции между сервисами (REST, gRPC, RabbitMQ), а также современные стэк, окружающий эти сервисы кэшированием (Redis), мониторингом (Prometheus Stack) и логированием (ELK).
А ниже подробное описание.
Сервис, который принимает HTTP-запросы от клиентов и маршрутизирует их к соответствующим внутренним сервисам. Написан на Java и Spring Boot.
Состоит из:
- /config - конфигурации Redis и RabbitMQ;
- /redis - вспомогательные файлы для Redis;
- /rest - контроллеры и dto для общения с клиентом, тут происходит перенаправление на внутренние сервисы;
Слой /service не нужен, так как никакой бизнес-логики сервис не реализует.
Вся конфигурация сервиса лежит в /gateway/src/main/resources/application.yaml.
Все точки взаимодействия прописаны, как переменные окружения. Это обеспечивает гибкость при запуске.
Чтобы контейнеризировать сервис нужен Dockerfile:
FROM openjdk:17-jdk-alpine
WORKDIR /app
COPY build/libs/gateway-0.0.1-SNAPSHOT.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]Собираем сервис, собираем образ, тэгируем и пушим на наш докерхаб:
./gradlew clean build -x test
docker build -t gateway .
docker tag gateway yuridiachenko/gateway:latest
docker push yuridiachenko/gateway:latestНе забудьте выполнить перед этим docker login!
Поднимаем в docker-compose, прокидывая стандартные порты и переменные окружения, а также прокинем из контейнера файл с логам (пригодится позже).
gateway:
image: yuridiachenko/gateway:latest
pull_policy: always
container_name: gateway
ports:
- "8080:8080"
volumes:
- ./logstash/logs/application.log:/app/logs/application.log
environment:
- RABBITMQ_HOST=rabbitmq
- RABBITMQ_PORT=5672
- REDIS_HOST=redis
- REDIS_PORT=6379
- GRPC_SERVER_HOST=domain
- GRPC_SERVER_PORT=50051
depends_on:
- domain
restart: alwaysCервис, предоставляющий API для выполнения операций создания, чтения, обновления и удаления данных (CRUD). Взаимодействовать с шлюзом через gRPC-протокол и шину данных. Написан на Java и Spring Boot.
Состоит из:
- /config - конфигурации gRPC и RabbitMQ;
- /domain - доменный слой;
- /grpc - файлы для работы с gRPC;
- /redis - файлы для работы с RabbitMQ;
- /repository - репозиторий для CRUD операций работы с MongoDB;
Слой /service не нужен, так как никакой бизнес-логики сервис не реализует, только CRUD.
Вся конфигурация сервиса лежит в /domain/src/main/resources/application.yaml.
Все точки взаимодействия прописаны, как переменные окружения. Это обеспечивает гибкость при запуске.
Чтобы контейнеризировать сервис нужен Dockerfile:
FROM openjdk:17-jdk-alpine
WORKDIR /app
COPY build/libs/domain-0.0.1-SNAPSHOT.jar /app/app.jar
EXPOSE 8082
EXPOSE 50051
ENTRYPOINT ["java", "-jar", "/app/app.jar"]Собираем сервис, собираем образ, тэгируем и пушим на наш докерхаб аналогично со шлюзом.
Поднимаем в docker-compose, прокидывая стандартные порты и переменные окружения.
domain:
image: yuridiachenko/domain:latest
pull_policy: always
container_name: domain
ports:
- "8082:8082"
- "50051:50051"
environment:
- RABBITMQ_HOST=rabbitmq
- RABBITMQ_PORT=5672
- MONGODB_HOST=mongo
- MONGODB_PORT=27017
- GRPC_SERVER_PORT=50051
depends_on:
- mongo
- rabbitmq
- redisВ качестве базы данных используется MongoDB, а также инструмент к нему MongoExpress. (Можно заменить на PostgreSQL и PgAdmin)
Поднимаем в docker-compose, прокидывая стандартные порты, следя за тем, чтобы экспресс поднялся только после монго через depends_on:
mongo:
image: mongo:7
container_name: mongo
ports:
- "27017:27017"
mongo-express:
image: mongo-express:latest
container_name: mongo-express
depends_on:
- mongo
ports:
- "8081:8081"Реализовано через Redis и Java RedisClient и происходит в шлюзе. Кэшируются GET-запросы.
Поднимаем в docker-compose, прокидывая стандартные порты:
redis:
image: "redis:6.2-alpine"
container_name: redis
ports:
- "6379:6379"В качестве шины данных используется RabbitMQ, а также инструмент к нему RabbitManagement. Помогает реализовывать асинхронные операции создания, обновления, удаления.
Поднимаем в docker-compose, прокидывая стандартные порты:
rabbitmq:
image: rabbitmq:management
container_name: rabbitmq
ports:
- "5672:5672"
- "15672:15672"
restart: alwaysРеализован через Promethues и Grafana, а также с помощью специальной библиотеки Micrometer,
dependencies {
//metrics
implementation 'io.micrometer:micrometer-registry-prometheus:1.12.3'
// ...
}которая собирает метрики с шлюза и предоставляет их на эндпоинт /actuator/prometheus.
Prometheus собирает их раз в 15 секунд с этого энпоинта и сохраняет в своей СУБД временных рядов. Конфигурация находится тут, и выглядит это вот так:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'gateway'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['host.docker.internal:8080']Grafana использует Prometheus как источник данных, для этого нужно сконфигурировать его таковым как тут, и выглядит это вот так:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: trueТеперь у нас есть хранилище метрик, к которому мы имеем доступ через Grafana Datasources. Настроим дашборды. Это можно сделать и руками через UI (как и datasource), но это долго и пришлось бы делать каждый раз заново. Поэтому мы просто сконфигурируем их, указав к ним путь, тип и другие параметры как тут и выглядит это вот так:
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
options:
path: /etc/grafana/provisioning/dashboardsДалее лениво заходим на Grafana Labs и выбираем понравившиеся нам дашборды. У меня это Spring Boot HTTP и Spring Boot 2.1 Statistics. Сохраняем понравившиеся дашборды в JSON рядом с конфигурацией - и всё.
Единственный момент - стоит проследить, чтобы название датасорса в дашборде соответствовало сконфигурированному ранее.
Поднимаем в docker-compose, прокидывая стандартные порты, следя за тем, чтобы наша система мониторинга поднялась только после шлюза, также не забываем прокинуть в контейнер все файлы конфигурации, которые написали, через volumes:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
depends_on:
- gateway
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on:
- prometheusРеализовано через Elastic, Logstash и Kibana или коротко ELK, а также библиотеки для логирования Logback.
Заморачиваться ни о чём не нужно, Logback входит в Spring Boot стандарт и
обычно вместе со своим другом log4j скрывается за аннотацией @Slf4j, которую мы используем в шлюзе.
Нам нужно только добавить аппендер рядом с конфигурацией проекта, так чтобы он выводил все логи помимо консоли ещё и в файл, как тут, и выглядит это вот так:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>Логи теперь есть в /gateway/logs/application.log. При создании шлюза мы уже прокинули
этот файл наружу контейнера в /logstash/logs/application.log, для того,
чтобы сейчас положить его в контейнер Logstash.
Сконфигурируем Logstash так, чтобы он читал логи из указанного нами файла и отправлял их в ElasticSearch, как тут, и выглядит это вот так:
input {
file {
path => "/app/logs/application.log"
start_position => "beginning"
sincedb_path => "/dev/null"
}
}
# ...
output {
elasticsearch {
hosts => ["host.docker.internal:9200"]
index => "gateway-logs-%{+YYYY.MM.dd}"
}
stdout { codec => rubydebug }
}
Поднимаем в docker-compose ElasticSearch и Logstash, прокидывая стандартные порты, также не забываем прокинуть в контейнер все файлы конфигурации, которые написали:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.10.1
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"
logstash:
image: docker.elastic.co/logstash/logstash:7.10.1
container_name: logstash
volumes:
- ./logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
- ./logstash/logs/application.log:/app/logs/application.log
depends_on:
- elasticsearch
ports:
- "5044:5044"
- "5000:5000"
- "9600:9600"Осталось отобразить эти логи в Kibana. Чтобы это сделать поднимем её в docker-compose.
kibana:
image: docker.elastic.co/kibana/kibana:7.10.1
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
ports:
- "5601:5601"
depends_on:
- elasticsearchЛоги нужно индексировать, это можно сделать через UI, но я не хочу :) ,
поэтому создадим индекс gateway-logs-* с меткой по времени,
обратившись к API Kibana через вот такой bash скрипт:
curl -X POST "http://localhost:5601/api/saved_objects/index-pattern/gateway-logs-*" \
-H "kbn-xsrf: true" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"title": "gateway-logs-*",
"timeFieldName": "@timestamp"
}
}'Теперь логи можно смотреть и делать по ним запросы в Kibana Discovery.
Все указанные выше docker-compose компоненты объединены в docker-compose.yaml
и поднять их все можно одной командой:
docker compose up -dНе забудем про последний шаг настройки логирования:
cd kibana
cd init
./init.shИ всё - система готова к работе!
- http://localhost:8080/book/all - покидать запросы на шлюз через Postman, бразуер или как тут, через особый файл;
- http://localhost:8081/ - посмотреть работу MongoDB (логин: admin, пароль: pass);
- http://localhost:15672/#/ - посмотреть работу RabbitMQ (логин: guest, пароль: guest);
- http://localhost:8080/actuator/metrics - посмотреть, какие метрики отдаёт шлюз;
- http://localhost:8080/actuator/prometheus - посмотреть сами метрики;
- http://localhost:9090/query - поделать запросы к Prometheus;
- http://localhost:3000/dashboards - посмотреть дашборды в Grafana (логин: admin, пароль: admin);
- http://localhost:5601/app/discover - - посмотреть логи в Kibana;