Roman Kryvolapov Engineering Blog

Java Spring – Микроуслуги

Как могат да взаимодействат микроуслугите

В архитектурата с микроуслуги микроуслугите могат да взаимодействат помежду си по няколко начина, например чрез синхронни и асинхронни извиквания. Нека разгледаме основните подходи за взаимодействие между микроуслугите:

Синхронни извиквания (HTTP/REST):
Това е най-разпространеният начин за взаимодействие между микроуслугите. Една микроуслуга изпраща HTTP заявка към друга микроуслуга. В Spring Boot това може да се реализира с помощта на RestTemplate или WebClient.

Пример за използване на RestTemplate:

Kotlin
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
@Service
class UserService {
private val restTemplate = RestTemplate()
fun getUserDetails(userId: Long): User {
val url = "http://order-service/orders/user/$userId"
val response = restTemplate.getForObject(url, User::class.java)
return response ?: throw RuntimeException("User not found")
}
}

Пример за използване на WebClient:

Kotlin
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
@Service
class UserService {
private val webClient = WebClient.create("http://order-service")
fun getUserDetails(userId: Long): User {
return webClient.get()
.uri("/orders/user/$userId")
.retrieve()
.bodyToMono(User::class.java)
.block() ?: throw RuntimeException("User not found")
}
}

Асинхронни извиквания (съобщения):
Микроуслугите могат да взаимодействат асинхронно, като обменят съобщения чрез брокери на съобщения като RabbitMQ, Apache Kafka и други.

Пример за използване на RabbitMQ:

Kotlin
import org.springframework.amqp.core.Queue
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class RabbitMQConfig {
@Bean
fun queue(): Queue {
return Queue("example.queue", false)
}
}
Kotlin
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.stereotype.Component
@Component
class MessageSender(private val rabbitTemplate: RabbitTemplate) {
fun sendMessage(message: String) {
rabbitTemplate.convertAndSend("example.queue", message)
}
}
Kotlin
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component
@Component
class MessageReceiver {
@RabbitListener(queues = ["example.queue"])
fun receiveMessage(message: String) {
println("Received message: $message")
}
}

Взаимодействие чрез база данни:
Микроуслугите могат да взаимодействат чрез обща база данни, но това не се препоръчва, тъй като нарушава принципа за изолация на данните в архитектурата с микроуслуги. По-добре е базата данни да се използва само за съхранение на данни, а взаимодействието да се осъществява чрез API.

Използване на услуги за откриване и маршрутизация:
Eureka (откриване на услуги)

Пример за конфигурация на Eureka:

Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
@Service
class UserService {
@Autowired
private lateinit var discoveryClient: DiscoveryClient
private val restTemplate = RestTemplate()
fun getUserDetails(userId: Long): User {
val instances = discoveryClient.getInstances("order-service")
val orderServiceUri = instances[0].uri
val url = "$orderServiceUri/orders/user/$userId"
val response = restTemplate.getForObject(url, User::class.java)
return response ?: throw RuntimeException("User not found")
}
}

Spring Cloud Gateway (маршрутизация):
използва се за маршрутизиране на заявките към съответните микроуслуги.
Пример за конфигурация на Gateway е даден по-долу в тази статия.

Използване на gRPC:
gRPC е съвременен RPC (Remote Procedure Call) фреймуърк, който използва Protocol Buffers и поддържа асинхронни извиквания.

Пример за използване на gRPC (Протокол (user.proto)):

Protobuf
syntax = "proto3";
option java_package = "com.example.demo";
option java_multiple_files = true;
service UserService {
rpc GetUserDetails (UserRequest) returns (UserResponse);
}
message UserRequest {
int64 userId = 1;
}
message UserResponse {
int64 id = 1;
string username = 2;
string password = 3;
string role = 4;
}
Kotlin
import io.grpc.stub.StreamObserver
import net.devh.boot.grpc.server.service.GrpcService
@GrpcService
class UserServiceImpl : UserServiceGrpc.UserServiceImplBase() {
override fun getUserDetails(request: UserRequest, responseObserver: StreamObserver<UserResponse>) {
val user = UserResponse.newBuilder()
.setId(request.userId)
.setUsername("testuser")
.setPassword("password")
.setRole("ROLE_USER")
.build()
responseObserver.onNext(user)
responseObserver.onCompleted()
}
}
Kotlin
import net.devh.boot.grpc.client.inject.GrpcClient
import org.springframework.stereotype.Service
@Service
class UserServiceClient {
@GrpcClient("user-service")
private lateinit var userServiceStub: UserServiceGrpc.UserServiceBlockingStub
fun getUserDetails(userId: Long): UserResponse {
val request = UserRequest.newBuilder().setUserId(userId).build()
return userServiceStub.getUserDetails(request)
}
}

Какво са Dockerfile и Docker Compose

Dockerfile:
използва се за създаване на индивидуални Docker образи. Това е основният начин да се опише как се изгражда образът на контейнера с нужните зависимости и конфигурации.

Dockerfile
# Използване на базов образ
FROM openjdk:11-jre-slim
# Задаване на работна директория
WORKDIR /app
# Копиране на jar файла в контейнера
COPY target/myapp.jar /app/myapp.jar
# Указване на командата за стартиране на приложението
CMD ["java", "-jar", "myapp.jar"]

Docker Compose:
използва се за управление на многоконтейнерни приложения. Това е инструмент за оркестрация, който позволява да се стартират и управляват няколко контейнера като едно цяло, като се определят взаимодействието и зависимостите между тях.

YAML
version: '3.8'
services:
web:
build: .
ports:
- "8080:8080"
depends_on:
- db
db:
image: postgres:13
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydatabase
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:

И двата инструмента често се използват заедно:
първо се създава Dockerfile за всяка услуга, след това Docker Compose файл за управление на всички тези услуги в едно приложение.

Как се използва Docker в Spring

Docker е платформа за разработване, доставка и стартиране на приложения в контейнери. Контейнерите позволяват приложението да се опакова заедно със зависимостите му и да се осигурят неговата изолация и преносимост.

Основни концепции на Docker:

Контейнери:
Изолирани среди, в които се стартират приложенията.

Образи:
Шаблони, използвани за създаване на контейнери.

Dockerfile:
Скрипт, който описва как да се създаде Docker образ.

Docker Hub:
Облачен регистър, в който могат да се съхраняват и споделят Docker образи.

Основни команди на Docker:

docker build:
Създаване на Docker образ от Dockerfile.

docker run:
Стартиране на контейнер от Docker образ.

docker pull:
Изтегляне на Docker образ от регистъра.

docker push:
Качване на Docker образ в регистъра.

docker ps:
Списък на стартираните контейнери.

docker stop/start:
Спиране/стартиране на контейнер.

Dockerfile
# Етап 1: Билд на приложението
FROM openjdk:21-jdk-slim AS build
WORKDIR /app
# Копираме файловете за билд на Gradle и изходния код
COPY build.gradle settings.gradle gradlew gradlew.bat ./
COPY gradle gradle
COPY src src
# Задаваме разрешения за изпълнение на файла gradlew
RUN chmod +x ./gradlew
# Изтегляме зависимостите и билдваме проекта
RUN ./gradlew bootJar
# Етап 2: Създаване на минимален образ за стартиране на приложението
FROM openjdk:21-jdk-slim
WORKDIR /app
# Копираме билднатия jar файл от етапа на билдване
COPY --from=build /app/build/libs/*.jar /app/app.jar
# Указваме командата за стартиране на Spring Boot приложението
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Билд на проекта:
Билднете проекта с Maven или Gradle, за да получите изпълним jar файл в папката target.

Shell
./gradlew build

Билд на Docker образа:
Използвайте командата docker build, за да създадете Docker образ от Dockerfile.

Shell
docker build -t your-dockerhub-username/demo .

Стартиране на контейнера:
Стартирайте контейнера с командата docker run.

Shell
docker run -p 8080:8080 your-dockerhub-username/demo

Сега приложението ви е достъпно на адрес http://localhost:8080.

Качване на образа в Docker Hub:
Влезте в Docker Hub и качете вашия образ.

Shell
docker login
docker push your-dockerhub-username/demo

Полезни команди на Docker:

docker images:
Списък на всички образи.

docker ps -a:
Списък на всички контейнери.

docker stop:
Спиране на контейнер.

docker start:
Стартиране на спрян контейнер.

docker rm:
Премахване на контейнер.

docker rmi :
Премахване на образ.

Как се използва Docker Compose в Spring

Docker Compose е инструмент за дефиниране и управление на многоконтейнерни Docker приложения. С Docker Compose можете да опишете услугите, мрежите и томовете в един YAML файл, а след това лесно да ги внедрите с една команда.

Пример за използване на Docker Compose:
Да кажем, че имаме Spring Boot приложение, което използва база данни PostgreSQL. Искаме да внедрим тези две услуги заедно с помощта на Docker Compose.

Properties
spring.datasource.url=jdbc:postgresql://db:5432/mydatabase
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.hibernate.ddl-auto=update

Създаване на Dockerfile:

Dockerfile
# Use the official image as a parent image
FROM openjdk:11-jre-slim
# Set the working directory in the container
WORKDIR /app
# Copy the jar file to the container
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
# Run the jar file
ENTRYPOINT ["java", "-jar", "app.jar"]

Създайте файл docker-compose.yml в главната директория на проекта:

YAML
version: '3.8' # Указва версията на формата на файла на Docker Compose
services: # Определя списъка с услуги, които ще бъдат внедрени
app: # Дефиниция на услугата за Spring Boot приложението
image: your-dockerhub-username/demo # Име на Docker образа
build: # Дефиниция на процеса на билд на Docker образа
context: . # Контекст на билда, текущата директория
dockerfile: Dockerfile # Име на Dockerfile, който ще се използва за билда
ports:
- "8080:8080" # Препращане на порт 8080 на хоста към порт 8080 на контейнера
environment: # Променливи на средата за настройка на приложението
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydatabase # URL за връзка с базата данни
SPRING_DATASOURCE_USERNAME: postgres # Потребителско име за базата данни
SPRING_DATASOURCE_PASSWORD: postgres # Парола за базата данни
depends_on: # Определя зависимостите от други услуги
- db # Зависи от услугата db
db: # Дефиниция на услугата за базата данни PostgreSQL
image: postgres:13 # Име на Docker образа на PostgreSQL версия 13
environment: # Променливи на средата за настройка на PostgreSQL
POSTGRES_DB: mydatabase # Име на базата данни, която ще бъде създадена
POSTGRES_USER: postgres # Потребителско име в базата данни
POSTGRES_PASSWORD: postgres # Парола на потребителя на базата данни
ports:
- "5432:5432" # Препращане на порт 5432 на хоста към порт 5432 на контейнера
volumes:
- postgres_data:/var/lib/postgresql/data # Създаване на том за съхранение на данните на PostgreSQL
volumes: # Определя списъка с томове, които ще се използват от услугите
postgres_data: # Том за съхранение на данните на PostgreSQL, за да се запазват между рестартиранията на контейнера

Пояснение за всяка секция:

version:
Указва версията на формата на файла на Docker Compose. В случая се използва версия 3.8.

services:
Определя всички услуги, които ще бъдат внедрени.

app:
Услуга за Spring Boot приложението.

image:
Име на Docker образа, който ще се използва за стартиране на контейнера.

build:
Определя параметрите за билд на Docker образа.

context:
Директорията, в която се намира Dockerfile.

dockerfile:
Име на файла Dockerfile.

ports:
Препращане на порт 8080 на хоста към порт 8080 на контейнера.

environment:
Променливи на средата за настройка на връзката с базата данни.

depends_on:
Определя, че тази услуга зависи от услугата db.

db:
Услуга за базата данни PostgreSQL.

image:
Име на Docker образа на PostgreSQL версия 13.

environment:
Променливи на средата за настройка на базата данни.

ports:
Препращане на порт 5432 на хоста към порт 5432 на контейнера.

volumes:
Създаване на том за съхранение на данните на PostgreSQL.

volumes:
Определя тома, който ще се използва за съхранение на данните на PostgreSQL.

Как работи:

Билд и стартиране:
При изпълнение на командата docker-compose up — build, Docker Compose първо билдва Docker образа за услугата app с помощта на указания Dockerfile. След това се стартират и двете услуги (app и db), като услугата app зависи от услугата db и затова db се стартира първа.

Променливи на средата:
Променливите на средата се използват за настройка на параметрите за връзка с базата данни в Spring Boot приложението.

Препращане на портове:
Портовете се препращат от контейнерите към хост машината, за да може да се осъществява достъп до приложението и до базата данни.

Том за данни:
Създава се томът postgres_data за съхранение на данните на базата данни PostgreSQL, за да се запазват данните между рестартиранията на контейнера.

Този файл docker-compose.yml позволява лесно внедряване на многоконтейнерно приложение с Docker Compose, като осигурява съгласуваност и удобство при управлението на контейнерите.

Билд и стартиране:
Стартирайте docker-compose, за да билднете и стартирате всички услуги.

Shell
docker-compose up --build

Тази команда:
Ще билдне Docker образа за вашето Spring Boot приложение.
Ще стартира контейнерите за приложението и за базата данни PostgreSQL.
Ще настрои връзката между контейнерите.

Как се използва Kubernetes в Spring

Kubernetes е система за оркестрация на контейнери с отворен код, която позволява да се автоматизират внедряването, мащабирането и управлението на контейнеризирани приложения. Тя осигурява платформа за стартиране на контейнери, за управлението и мащабирането им.

Ето стъпково ръководство за използване на Kubernetes за внедряване на Spring Boot приложение.

Предварителни изисквания:

Docker:
Уверете се, че Docker е инсталиран и настроен.

Kubernetes:
Инсталирайте и настройте Kubernetes (например с помощта на Minikube за локална разработка).

kubectl:
Инсталирайте помощната програма за команден ред kubectl за взаимодействие с клъстера Kubernetes.

Създаване на Docker образ:

Dockerfile
# Use the official image as a parent image
FROM openjdk:11-jre-slim
# Set the working directory in the container
WORKDIR /app
# Copy the jar file to the container
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
# Run the jar file
ENTRYPOINT ["java", "-jar", "app.jar"]

Билднете Docker образа и го качете в Docker Hub (или в друг регистър на Docker образи).

Shell
# Билд на Docker образа
docker build -t your-dockerhub-username/demo .
# Влизане в Docker Hub
docker login
# Качване на Docker образа в Docker Hub
docker push your-dockerhub-username/demo

Създаване на манифести за Kubernetes:
Създайте манифести за Kubernetes за внедряване (Deployment), услуга (Service) и конфигурация на Ingress.

deployment.yaml:

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-deployment
labels:
app: demo
spec:
replicas: 3
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
containers:
- name: demo
image: your-dockerhub-username/demo:latest
ports:
- containerPort: 8080

service.yaml:

YAML
apiVersion: v1
kind: Service
metadata:
name: demo-service
spec:
selector:
app: demo
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer

ingress.yaml:

YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-service
port:
number: 80

Прилагане на манифестите за Kubernetes:
Използвайте командата kubectl, за да приложите манифестите във вашия клъстер Kubernetes.

Shell
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml

Настройка на DNS:
За локална разработка с Minikube добавете следния запис във файла /etc/hosts:

<MINIKUBE_IP> demo.local

Вземете IP адреса на Minikube с командата:

Shell
minikube ip

Проверка на внедряването:
Проверете състоянието на вашето внедряване, услуга и ingress с командите:

Shell
kubectl get deployments
kubectl get services
kubectl get ingress

Отворете браузъра и отидете на адрес http://demo.local, за да видите внедреното Spring Boot приложение.

Как се използва Apache Kafka в Spring

Apache Kafka е разпределена стрийминг платформа, която се използва за изграждане на системи за обработка на данни в реално време. Kafka първоначално е разработена в LinkedIn и през 2011 година е публикувана като проект с отворен код. Използва се за публикуване, съхранение и обработка на потоци от записи в реално време.

Основни компоненти на Kafka:

Producer:
Изпраща записи (съобщения) в топици на Kafka.

Consumer:
Чете записи (съобщения) от топици на Kafka.

Broker:
Сървър на Kafka, който приема данни от продюсерите, съхранява ги и ги предава на консюмерите. Клъстерът на Kafka се състои от един или повече брокери.

Topic:
Логическа категория или канал, в който продюсерите изпращат данни и от който консюмерите ги четат. Топикът се разделя на партиции, за да се осигурят паралелизъм и мащабируемост.

Partition:
Подразделение на топика. Всяка партиция е подреден и неизменяем лог, в който продюсерите добавят съобщения.

Zookeeper:
Система за координация, използвана от Kafka за управление на метаданните на клъстера и за проследяване на състоянието на брокерите и топиците.

Как работи Kafka:

Публикуване на съобщения:
Продюсерите публикуват съобщения в определени топици. Съобщенията се запазват в партициите на топика под формата на логове.

Съхранение на съобщения:
Съобщенията се съхраняват в партициите на топиците на диск и може да се настрои да се пазят само определено време или до достигане на определен обем данни.

Четене на съобщения:
Консюмерите се абонират за топици и четат съобщения от партициите. Kafka позволява на консюмерите да контролират от кое място започват да четат съобщенията, което осигурява голяма гъвкавост и възможност за повторно четене на данните.

Мащабируемост:
Партициите позволяват натоварването да се разпределя между няколко брокера, което осигурява хоризонтално мащабиране.

Отказоустойчивост:
Репликацията на партициите между няколко брокера осигурява висока наличност и устойчивост на откази.

Настройка на Kafka:
Инсталирайте и настройте Apache Kafka и Zookeeper с помощта на Docker Compose.
Създайте файл docker-compose.yml

YAML
version: '3.8'
services:
zookeeper:
image: bitnami/zookeeper:latest
container_name: zookeeper
ports:
- "2181:2181"
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
image: bitnami/kafka:latest
container_name: kafka
ports:
- "9092:9092"
environment:
- KAFKA_BROKER_ID=1
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
- ALLOW_PLAINTEXT_LISTENER=yes

отидете в командния ред в директорията на файла и стартирайте:

Shell
docker-compose up -d

Проверете състоянието

Shell
docker-compose ps

Създаване на продюсър на събития:
Създайте Spring Boot приложение със зависимостите на Kafka.

Kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.kafka:spring-kafka")
}
Properties
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=my-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

или чрез конфигурационен клас

Kotlin
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.common.serialization.StringSerializer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.core.DefaultKafkaProducerFactory
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.core.ProducerFactory
import org.springframework.kafka.support.serializer.JsonSerializer
@Configuration
class KafkaProducerConfig {
@Bean
fun producerFactory(): ProducerFactory<String, String> {
val configProps = HashMap<String, Any>()
configProps[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = "localhost:9092"
configProps[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java
configProps[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = JsonSerializer::class.java
return DefaultKafkaProducerFactory(configProps)
}
@Bean
fun kafkaTemplate(): KafkaTemplate<String, String> {
return KafkaTemplate(producerFactory())
}
}
Kotlin
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.annotation.EnableKafka
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
import org.springframework.kafka.core.ConsumerFactory
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
import org.springframework.kafka.support.serializer.JsonDeserializer
@EnableKafka
@Configuration
class KafkaConsumerConfig {
@Bean
fun consumerFactory(): ConsumerFactory<String, String> {
val configProps = HashMap<String, Any>()
configProps[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = "localhost:9092"
configProps[ConsumerConfig.GROUP_ID_CONFIG] = "group_id"
configProps[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java
configProps[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java
configProps[ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS] = JsonDeserializer::class.java.name
return DefaultKafkaConsumerFactory(configProps, StringDeserializer(), JsonDeserializer())
}
@Bean
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {
val factory = ConcurrentKafkaListenerContainerFactory<String, String>()
factory.consumerFactory = consumerFactory()
return factory
}
}

Producer и Consumer:

Kotlin
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.stereotype.Service
@Service
class EventProducer(private val kafkaTemplate: KafkaTemplate<String, String>) {
fun sendMessage(topic: String, message: String) {
kafkaTemplate.send(topic, message)
}
}
Kotlin
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Service
@Service
class EventConsumer {
@KafkaListener(topics = ["topic_name"], groupId = "group_id")
fun consume(message: String) {
println("Consumed message: $message")
}
}

Как се използва RabbitMQ в Spring

RabbitMQ е софтуер за опашки от съобщения (message broker), който позволява на приложенията да обменят съобщения и да изпълняват задачи асинхронно. Поддържа няколко протокола за обмен на съобщения и се използва широко за изграждане на разпределени и мащабируеми системи.

Основни концепции на RabbitMQ:

Протокол AMQP:
RabbitMQ поддържа протокола AMQP (Advanced Message Queuing Protocol), който определя правилата за обмен на съобщения между клиентите и брокерите.

Опашки (Queues):
Опашката представлява буфер за съхранение на съобщения. Съобщенията се изпращат в опашката, където чакат да бъдат обработени от получателя.

Обменници (Exchanges):
Обменникът получава съобщения от продюсера и ги маршрутизира към една или повече опашки в зависимост от зададените правила.

Връзки (Bindings):
Връзката свързва опашка с обменник и определя правилата за маршрутизиране на съобщенията от обменника към опашката.

Съобщения (Messages):
Съобщенията представляват данни, които се предават между приложенията чрез опашки. Всяко съобщение се състои от хедър и тяло (полезен товар).

Продюсери (Producers):
Продюсерите изпращат съобщения към обменниците.

Консюмери (Consumers):
Консюмерите получават съобщения от опашките и ги обработват.

Пример за работа на RabbitMQ:
Продюсерът (Producer) изпраща съобщение към обменника (Exchange).
Обменникът маршрутизира съобщението към съответната опашка (Queue) въз основа на зададените правила.
Консюмерът (Consumer) получава съобщението от опашката и го обработва.

За да използвате RabbitMQ в Kotlin със Spring Boot, можете да се възползвате от библиотеката Spring AMQP (Spring for RabbitMQ)

Groovy
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation 'org.jetbrains.kotlin:kotlin-reflect'
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}
Kotlin
import org.springframework.amqp.core.Queue
import org.springframework.amqp.rabbit.connection.ConnectionFactory
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class RabbitConfig {
@Bean
fun queue(): Queue {
return Queue("myQueue", false)
}
@Bean
fun rabbitTemplate(connectionFactory: ConnectionFactory): RabbitTemplate {
return RabbitTemplate(connectionFactory)
}
}
Kotlin
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.stereotype.Service
@Service
class MessageSender(private val rabbitTemplate: RabbitTemplate) {
fun sendMessage(message: String) {
rabbitTemplate.convertAndSend("myQueue", message)
}
}
Kotlin
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Service
@Service
class MessageListener {
@RabbitListener(queues = ["myQueue"])
fun receiveMessage(message: String) {
println("Received message: $message")
}
}
Kotlin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class MessageController(private val messageSender: MessageSender) {
// http://localhost:8080/send?message=Hello
@GetMapping("/send")
fun send(@RequestParam message: String): String {
messageSender.sendMessage(message)
return "Message sent: $message"
}
}

Каква е разликата между Kafka и RabbitMQ

Kafka и RabbitMQ са две популярни системи за обмен на съобщения, които се използват за предаване на съобщения между компонентите на разпределени системи. Те обаче имат различни архитектурни подходи, целеви сценарии за използване и особености.

Apache Kafka:

Архитектура:

Журнал за публикуване и абониране (log-based pub/sub):
Kafka съхранява съобщенията във вид на логове, което позволява на абонатите да четат съобщения от определено отместване.

Брокер на съобщения:
Kafka се състои от клъстери от брокери, които съхраняват данните разпределено.

Съхранение на съобщения:
Съобщенията се съхраняват на диск и всяко съобщение има уникално отместване. Това позволява съобщенията да се четат повторно и журналът да се превърта.
Поддръжка на дългосрочно съхранение на данни.

Производителност:
Висока производителност и пропускателна способност, подходяща за обработка на големи обеми данни в реално време.

Поддръжка на мащабиране:
Лесно се мащабира хоризонтално чрез добавяне на нови брокери и партиции (partitions).

Целеви сценарии за използване:
Потокова обработка на данни (stream processing).
Реализация на системи, управлявани от събития.
Анализ на данни в реално време.

Екосистема:
Включва компоненти като Kafka Streams за обработка на потоци и Kafka Connect за интеграция с различни източници на данни.

RabbitMQ:

Архитектура:

Опашки от съобщения:
RabbitMQ използва концепцията за опашки от съобщения и маршрутизиране (routing).

Брокер на съобщения:
Съобщенията се предават чрез обменници (exchanges) и опашки (queues).

Съхранение на съобщения:
Съобщенията могат да се съхраняват в оперативната памет или на диск.
Поддържа надеждна доставка на съобщения чрез потвърждения (acknowledgments) и повторен опит (retry).

Производителност:
Добра производителност за широк набор от сценарии, но може да е по-малко ефективен при обработка на големи обеми данни в сравнение с Kafka.

Поддръжка на мащабиране:
Поддръжка на клъстеризация и федерация за хоризонтално мащабиране, но може да е по-сложен за настройка и управление в сравнение с Kafka.

Целеви сценарии за използване:
Класическа опашка от задачи (task queue) за асинхронна обработка.
Реализация на система за обмен на съобщения със сложна маршрутизация.
Интеграция и обмен на данни между разнородни системи.

Екосистема:
Поддръжка на различни протоколи (AMQP, MQTT, STOMP).
Голям брой плъгини за разширяване на функционалността.

Kafka и RabbitMQ решават различни задачи в областта на предаването на съобщения и имат свои предимства и ограничения. Kafka е подходяща за обработка на големи обеми данни и за потокова обработка, докато RabbitMQ се справя по-добре със задачите за асинхронна обработка и със сложната маршрутизация на съобщения. Изборът между тях зависи от конкретните изисквания и сценарии за използване във вашия проект.

Какво е Eureka Service

Eureka Service е част от набора инструменти за разработване на микроуслуги на Netflix, който се нарича Netflix OSS. Използва се за регистриране и откриване на услуги в разпределени системи. Eureka Service е централна част на услугата за откриване (service discovery), която позволява на различните микроуслуги да се намират и да взаимодействат помежду си.

Основни компоненти на Eureka:

Eureka Server:
Централен сървър, който действа като регистър за всички микроуслуги. Микроуслугите се регистрират в Eureka Server и съобщават състоянието си (healthy, down, etc.).

Eureka Client:
Клиентите на Eureka, които се регистрират в Eureka Server и могат да го използват за откриване на други услуги.

eureka-service-project
├── eureka-server
│ ├── build.gradle
│ ├── src
│ │ ├── main
│ │ │ ├── java (или kotlin)
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── eurekaserver
│ │ │ │ └── EurekaServerApplication.kt
│ │ │ ├── resources
│ │ │ │ └── application.properties
│ │ └── test
│ │ ├── java (или kotlin)
│ │ │ └── com
│ │ │ └── example
│ │ │ └── eurekaserver
│ │ │ └── EurekaServerApplicationTests.kt
├── eureka-client
│ ├── build.gradle
│ ├── src
│ │ ├── main
│ │ │ ├── java (или kotlin)
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── eurekaclient
│ │ │ │ ├── EurekaClientApplication.kt
│ │ │ │ └── ServiceInstanceRestController.kt
│ │ │ ├── resources
│ │ │ │ └── application.properties
│ │ └── test
│ │ ├── java (или kotlin)
│ │ │ └── com
│ │ │ └── example
│ │ │ └── eurekaclient
│ │ │ └── EurekaClientApplicationTests.kt
└── settings.gradle

Как работи Eureka Service:

Регистриране на услуги:
Всяка микроуслуга, която е клиент на Eureka, се регистрира в Eureka Server при стартиране, като предоставя информация за себе си, например адрес, порт и идентификатор на услугата.

Обновяване на състоянието:
Клиентите редовно изпращат „пингове“ (heartbeats) към Eureka Server, за да потвърдят, че все още са активни.

Откриване на услуги:
Микроуслугите могат да използват Eureka Client, за да получат списък с всички налични услуги, регистрирани в Eureka Server, и да взаимодействат с тях.

Основни анотации и използването им в Eureka:

@EnableEurekaServer:
Включва Eureka Server.

@EnableEurekaClient:
Включва Eureka Client за регистриране и откриване на услуги.

@EnableDiscoveryClient:
Включва общия механизъм за откриване на услуги (може да се използва с различни системи за откриване, включително Eureka).

@LoadBalanced:
Маркира RestTemplate или WebClient за използване на балансиране на натоварването от страна на клиента.

Пример за използване на Eureka Service:
В settings.gradle трябва да се добавят всички модули с includeBuild

Пример за Eureka Server:

Groovy
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Properties
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.server.enable-self-preservation=false

server.port=8761:
Задава порта, на който ще бъде стартиран Eureka Server. По подразбиране Spring Boot стартира приложението на порт 8080, а Eureka Server по принцип се стартира на 8761.

eureka.client.register-with-eureka=false:
Указва, че Eureka Server не трябва да се регистрира сам в себе си като клиент. Това е параметър от конфигурацията на клиента на Eureka, който указва, че този сървър на Eureka няма да се регистрира в друг сървър на Eureka.

eureka.client.fetch-registry=false:
Указва, че Eureka Server не трябва да извлича (fetch) регистъра на другите услуги. Това също е параметър от конфигурацията на клиента на Eureka, който изключва опитите за получаване на списъка с всички регистрирани услуги.

eureka.server.enable-self-preservation=false:
Изключва режима на самосъхранение (self-preservation mode) на Eureka Server. Този режим е предназначен за защита от загуба на регистрации на услуги, когато сървърът не получава пингове от клиентите продължително време. Изключването на този режим позволява услугите да се премахват от регистъра веднага, ако не изпращат пингове, което може да е полезно за тестване, но не се препоръчва за продукционна среда.

@EnableEurekaServer:
Тази анотация се използва за включване на сървъра на Eureka. Указва се на основния клас на приложението, за да се отбележи като Eureka Server.

Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer
@SpringBootApplication
@EnableEurekaServer
class EurekaServerApplication
fun main(args: Array<String>) {
runApplication<EurekaServerApplication>(*args)
}

Пример за Eureka Client:

Groovy
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Properties
server.port=0
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

server.port=0:
Задава порта, на който ще бъде стартиран Eureka Client. В случая клиентското приложение ще работи на порт, избран автоматично.

**eureka.client.service-url.defaultZone=http://localhost:8761/eureka/:**\ Указва URL адреса за регистриране на клиента в Eureka Server. Параметърът defaultZone определя адреса на сървъра на Eureka, с който клиентът ще взаимодейства за регистриране и откриване на услуги. В случая клиентът ще се регистрира в Eureka Server, който работи на http://localhost:8761/eureka/.

Допълнителни параметри, които могат да се използват в application.properties:

За Eureka Server:

eureka.instance.hostname:
Задава името на хоста за Eureka Server.

eureka.instance.prefer-ip-address:
Указва дали да се използва IP адрес вместо име на хост за регистриране.

За Eureka Client:

eureka.instance.hostname:
Задава името на хоста за Eureka Client.

eureka.instance.prefer-ip-address:
Указва дали да се използва IP адрес вместо име на хост за регистриране на клиента.

eureka.client.initial-instance-info-replication-interval-seconds:
Задава интервала в секунди между повторните изпращания на метаданните на инстанцията към Eureka Server.

eureka.client.registry-fetch-interval-seconds:
Задава интервала в секунди между опитите на клиента да получи обновен регистър на услугите от Eureka Server.

eureka.client.instance-info-replication-interval-seconds:
Задава интервала в секунди между изпращанията на информация за инстанцията на услугата към Eureka Server.

@EnableEurekaClient:
Тази анотация се използва за включване на клиента на Eureka. Указва се на основния клас на приложението, за да се отбележи като клиент на Eureka. Клиентът се регистрира автоматично в Eureka Server и може да го използва за откриване на други услуги.

@EnableDiscoveryClient:
Тази анотация е по-обща анотация за включване на откриването на услуги. Тя позволява на приложението да използва различни механизми за откриване на услуги, включително Eureka. Използва се по подобен начин на анотацията @EnableEurekaClient.

Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.netflix.eureka.EnableEurekaClient
@SpringBootApplication
@EnableEurekaClient
class EurekaClientApplication
fun main(args: Array<String>) {
runApplication<EurekaClientApplication>(*args)
}
Kotlin
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class ServiceInstanceRestController(private val discoveryClient: DiscoveryClient) {
@GetMapping("/service-instances")
fun serviceInstances(): List<String> {
return discoveryClient.services
}
}

@LoadBalanced:
Тази анотация се използва за маркиране на RestTemplate или WebClient, така че да използва балансиране на натоварването от страна на клиента, базирано на Ribbon (ако се използва). Това позволява на RestTemplate или WebClient да правят HTTP заявки към именувани услуги, регистрирани в Eureka, с балансиране на натоварването между инстанциите на услугата.

Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.context.annotation.Bean
import org.springframework.web.client.RestTemplate
@SpringBootApplication
class LoadBalancedApplication {
@Bean
@LoadBalanced
fun restTemplate(): RestTemplate {
return RestTemplate()
}
}
fun main(args: Array<String>) {
runApplication<LoadBalancedApplication>(*args)
}

Как се реализира Spring Cloud Gateway

Spring Cloud Gateway е съвременен API шлюз, изграден върху Spring Framework 5, Spring Boot 2 и проекта Spring WebFlux. Предоставя мощни възможности за маршрутизиране и управление на API трафика в архитектура с микроуслуги. Spring Cloud Gateway е проектиран така, че да се интегрира лесно с другите компоненти на Spring Cloud и да работи в асинхронна и реактивна среда.

Основни възможности на Spring Cloud Gateway:

Маршрутизиране на заявки:
Spring Cloud Gateway може да маршрутизира заявките към различни микроуслуги въз основа на различни критерии, като път, хедъри, параметри на заявката и много други.

Филтри:
Позволява прилагане на филтри към заявките и отговорите. Филтрите могат да променят заявките, да добавят или променят хедъри, да извършват автентикация и оторизация, логване и много други.

Балансиране на натоварването:
Вградената поддръжка на балансиране на натоварването позволява заявките да се разпределят между няколко инстанции на микроуслугите.

Интеграция с Eureka:
Spring Cloud Gateway лесно се интегрира с Eureka и други системи за откриване на услуги за динамично маршрутизиране на заявките към регистрираните услуги.

Поддръжка на асинхронна обработка:
Изграден е върху реактивния стек Spring WebFlux, което позволява да се обработва голям брой паралелни заявки с висока производителност.

Сигурност:
Поддръжка на различни механизми за автентикация и оторизация, включително OAuth2.

Пример за използване на Spring Cloud Gateway:
Създайте нов Spring Boot проект със зависимости за Spring Cloud Gateway и Spring Boot Actuator.

eureka-service-project
├── eureka-server
│ ├── build.gradle
│ ├── src
│ │ ├── main
│ │ │ └── ...
│ │ └── ...
├── eureka-client
│ ├── build.gradle
│ ├── src
│ │ ├── main
│ │ │ └── ...
│ │ └── ...
├── api-gateway
│ ├── build.gradle
│ ├── src
│ │ ├── main
│ │ │ └── ...
│ │ └── ...
└── settings.gradle

Настройка на settings.gradle

Groovy
rootProject.name = 'eureka-service-project'
include 'eureka-server', 'eureka-client', 'api-gateway'

api-gateway/build.gradle

Groovy
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2021.0.1"
}
}

api-gateway/src/main/resources/application.properties

Properties
server.port=8080
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

api-gateway/src/main/kotlin/com/example/apigateway/ApiGatewayApplication.kt

Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
class ApiGatewayApplication
fun main(args: Array<String>) {
runApplication<ApiGatewayApplication>(*args)
}

Пояснения към настройките:

server.port=8080:
API Gateway ще се стартира на порт 8080.

spring.cloud.gateway.discovery.locator.enabled=true:
Включва автоматичното откриване на маршрути за микроуслугите, регистрирани в Eureka.

spring.cloud.gateway.discovery.locator.lower-case-service-id=true:
Преобразува идентификаторите на услугите в малки букви, за да се опрости маршрутизирането.

**eureka.client.service-url.defaultZone=http://localhost:8761/eureka/:**\ URL адресът на Eureka Server за регистриране и откриване на услуги.

Маршрутизиране на заявки:
Spring Cloud Gateway автоматично създава маршрути за всички микроуслуги, регистрирани в Eureka. Например, ако имате микроуслуга с име eureka-client, заявките към API Gateway на /eureka-client/** ще бъдат пренасочвани към съответната микроуслуга.
Маршрутизирането може да се настройва в конфигурационния файл или програмно.

src/main/resources/application.properties

Properties
server.port=8080
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Дефиниране на маршрутите за eureka-client
spring.cloud.gateway.routes[0].id=eureka-client
spring.cloud.gateway.routes[0].uri=lb://eureka-client
spring.cloud.gateway.routes[0].predicates[0]=Path=/eureka-client/**
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/eureka-client/(?<remaining>.*), /${remaining}

Пояснения към конфигурацията:

server.port=8080:
Указва, че Spring Cloud Gateway ще бъде стартиран на порт 8080.

spring.cloud.gateway.discovery.locator.enabled=true:
Включва автоматичното откриване на маршрути за услугите, регистрирани в Eureka.

spring.cloud.gateway.discovery.locator.lower-case-service-id=true:
Преобразува идентификаторите на услугите в малки букви, за да се опрости маршрутизирането.

**eureka.client.service-url.defaultZone=http://localhost:8761/eureka/:**\ Указва URL адреса на Eureka Server за регистриране и откриване на услуги.

routes:
Настройка на маршрутите. В случая заявките, започващи с /eureka-client/**, ще бъдат пренасочени към услугата eureka-client.

Как се реализира пренасочване със Spring Cloud Gateway

Да предположим, че имаме две микроуслуги, регистрирани в Eureka: order-service и inventory-service.
Искаме да настроим маршрутизацията в Spring Cloud Gateway така, че заявките, започващи с /orders/, да се пренасочват към order-service, а заявките, започващи с /inventory/, да се пренасочват към inventory-service.

Properties
server.port=8080
# Включваме автоматичното откриване на маршрути за услугите, регистрирани в Eureka
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# Указваме URL адреса на Eureka Server за регистриране и откриване на услуги
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Дефиниране на маршрутите за order-service
spring.cloud.gateway.routes[0].id=order-service
spring.cloud.gateway.routes[0].uri=lb://order-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/orders/**
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/orders/(?<remaining>.*), /${remaining}
# Дефиниране на маршрутите за inventory-service
spring.cloud.gateway.routes[1].id=inventory-service
spring.cloud.gateway.routes[1].uri=lb://inventory-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/inventory/**
spring.cloud.gateway.routes[1].filters[0]=RewritePath=/inventory/(?<remaining>.*), /${remaining}

Пояснение на параметрите:

server.port:
Указва, че Spring Cloud Gateway ще бъде стартиран на порт 8080.

spring.cloud.gateway.discovery.locator.enabled:
Включва автоматичното откриване на маршрути за услугите, регистрирани в Eureka.

spring.cloud.gateway.discovery.locator.lower-case-service-id:
Преобразува идентификаторите на услугите в малки букви, за да се опрости маршрутизирането.

eureka.client.service-url.defaultZone:
Указва URL адреса на Eureka Server за регистриране и откриване на услуги.

spring.cloud.gateway.routes:
routes: Секция за дефиниране на маршрути в Spring Cloud Gateway.

Маршрут за order-service:

id:
Уникален идентификатор на маршрута. В случая това е order-service.

uri:
URI за пренасочване на заявките. Префиксът lb:// указва използването на вградено балансиране на натоварването (load balancer), а order-service е името на услугата, регистрирана в Eureka.

predicates:
Условия, които трябва да бъдат изпълнени, за да бъде заявката пренасочена по този маршрут.

Path=/orders/:
Условие за пътя. Всички заявки, започващи с /orders/**, ще съответстват на този маршрут.

filters:
Списък с филтри, които ще се прилагат към заявките и отговорите.

RewritePath=/orders/(?<remaining>.*), /${remaining}:
Филтър, който пренаписва пътя на заявката. В случая частта от пътя /orders/ ще бъде премахната от заявката, преди тя да бъде изпратена към order-service. Остатъкът от пътя ще бъде запазен и предаден нататък.

Маршрут за inventory-service:

id:
Уникален идентификатор на маршрута. В случая това е inventory-service.

uri:
URI за пренасочване на заявките. Префиксът lb:// указва използването на вградено балансиране на натоварването (load balancer), а inventory-service е името на услугата, регистрирана в Eureka.

predicates:
Условия, които трябва да бъдат изпълнени, за да бъде заявката пренасочена по този маршрут.

Path=/inventory/:
Условие за пътя. Всички заявки, започващи с /inventory/**, ще съответстват на този маршрут.

filters:
Списък с филтри, които ще се прилагат към заявките и отговорите.

RewritePath=/inventory/(?<remaining>.*), /${remaining}:
Филтър, който пренаписва пътя на заявката. В случая частта от пътя /inventory/ ще бъде премахната от заявката, преди тя да бъде изпратена към inventory-service. Остатъкът от пътя ще бъде запазен и предаден нататък.

Какво е Service Discovery

Service Discovery (откриване на услуги) е механизъм, който позволява на микроуслугите да се намират и да взаимодействат помежду си в разпределена система, без да е необходимо изрично да се указват мрежови адреси. В съвременните архитектури с микроуслуги, където броят на услугите може да е много голям, ръчното управление на адресите им става сложно и неудобно. Service Discovery автоматизира процеса на намиране на услуги и улеснява мащабирането и управлението на системата.

Основни компоненти на Service Discovery:

Регистър на услугите (Service Registry):
Регистърът на услугите е централно хранилище, в което всички налични услуги се регистрират със своите метаданни, като IP адреси, портове и състояния. Примери за регистри на услугите са Consul, Eureka, Zookeeper и Etcd.

Клиенти на услугите (Service Clients):
Клиентите на услугите са компоненти, които взаимодействат с регистъра на услугите, за да регистрират, обновяват и търсят услуги.

Механизми за проверка на състоянието (Health Checks):
Механизмите за проверка на състоянието се използват за наблюдение на достъпността и работоспособността на услугите. Услугите редовно изпращат информация за състоянието си към регистъра на услугите, за да поддържат данните актуални.

Видове Service Discovery:

Откриване от страна на клиента (Client-Side Discovery):
При този подход клиентските приложения сами изпращат заявки към регистъра на услугите, за да получат адресите на наличните услуги. Откриването от страна на клиента обикновено се реализира с помощта на библиотеки, интегрирани в клиентските приложения.
Пример:
Netflix Eureka
Consul

Откриване от страна на сървъра (Server-Side Discovery):
При този подход клиентските приложения изпращат заявките си към сървърен прокси или шлюз (например API Gateway), който взаимодейства с регистъра на услугите и пренасочва заявките към нужните услуги.
Пример:
AWS Elastic Load Balancer (ELB)
Kubernetes Service

Какви инструменти има за наблюдение на микроуслугите

Prometheus:
Система за мониторинг и алертиране с мощен език за заявки.

Grafana:
Платформа за визуализация на метрики и създаване на дашборди.

Elasticsearch, Logstash, Kibana (ELK Stack):
Решение за централизирано логване и анализ на данни.

Jaeger, Zipkin:
Инструменти за разпределено проследяване.

Как се използва Elasticsearch в Spring

Elasticsearch е разпределена система за търсене и анализ с отворен код, разработена за съхранение, търсене и анализ на големи обеми данни в реално време. Elasticsearch често се използва за пълнотекстово търсене, логове и анализ.

Kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
}
Properties
spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.username=elastic
spring.elasticsearch.password=your_password
Kotlin
import org.springframework.data.annotation.Id
import org.springframework.data.elasticsearch.annotations.Document
import org.springframework.data.elasticsearch.annotations.Field
@Document(indexName = "products")
data class Product(
@Id
val id: String? = null,
val name: String,
val description: String,
val price: Double,
@Field(type = Date)
val date: String;
)
Kotlin
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository
import org.springframework.stereotype.Repository
@Repository
interface ProductRepository : ElasticsearchRepository<Product, String> {
fun findByName(name: String): List<Product>
}
Kotlin
import org.springframework.stereotype.Service
@Service
class ProductService(private val productRepository: ProductRepository) {
fun findAllProducts(): List<Product> {
return productRepository.findAll().toList()
}
fun saveProduct(product: Product): Product {
return productRepository.save(product)
}
fun findProductByName(name: String): List<Product> {
return productRepository.findByName(name)
}
}
Kotlin
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/products")
class ProductController(private val productService: ProductService) {
@GetMapping
fun getAllProducts(): List<Product> {
return productService.findAllProducts()
}
@PostMapping
fun createProduct(@RequestBody product: Product): Product {
return productService.saveProduct(product)
}
@GetMapping("/search")
fun searchProductsByName(@RequestParam name: String): List<Product> {
return productService.findProductByName(name)
}
}

Какво са Trace ID и Span ID

Trace ID и Span ID са ключови компоненти на разпределеното проследяване, които се използват за мониторинг и дебъгване на разпределени системи. Те помагат да се проследяват и свързват заявките, преминаващи през множество услуги, което позволява да се разбере и диагностицира поведението на системата.

Trace ID:
е уникален идентификатор, който се присвоява на всяка заявка, преминаваща през системата. Този идентификатор остава непроменен през целия жизнен цикъл на заявката и се използва за свързване на всички операции, свързани с нея.

Цел:
Trace ID позволява всички спанове (части на заявката) да се обединят в една обща следа, за да може да се види пълният път на заявката през всички микроуслуги и компоненти на системата.

Span ID:
е уникален идентификатор за конкретна операция или част от заявката. Всеки спан представлява отделен етап или сегмент от заявката, например извикване на метод или изпълнение на заявка към база данни.

Цел:
Span ID позволява да се идентифицират и проследяват отделните операции в рамките на една заявка, като предоставя подробна информация за всяка стъпка.

Пример за използване на Trace ID и Span ID:
Нека разгледаме как Trace ID и Span ID могат да се използват в Spring Boot приложение с помощта на библиотеката Spring Cloud Sleuth, която се интегрира със Zipkin за разпределено проследяване.

Kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-sleuth")
implementation("org.springframework.cloud:spring-cloud-starter-zipkin")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2020.0.4")
}
}
Properties
spring.zipkin.base-url=http://localhost:9411
spring.sleuth.sampler.probability=1.0
Kotlin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.client.RestTemplate
@RestController
class TraceController(
@Autowired val restTemplate: RestTemplate
) {
@GetMapping("/trace")
fun trace(): String {
val response = restTemplate.getForObject(
"http://localhost:8081/external",
String::class.java
)
return "Trace ID and Span ID example: $response"
}
@GetMapping("/external")
fun external(): String {
return "External service response"
}
}

Стартирайте Zipkin с помощта на Docker:

Shell
docker run -d -p 9411:9411 openzipkin/zipkin

Стартирайте вашето Spring Boot приложение и то автоматично ще генерира Trace ID и Span ID за всички заявки.

Преглед на проследяването:
Отидете на http://localhost:9411, за да отворите интерфейса на Zipkin.
Изпратете GET заявка към http://localhost:8080/trace.\ Върнете се в интерфейса на Zipkin и намерете следата, свързана с вашата заявка. Ще видите Trace ID и Span ID, които показват пътя на заявката през вашите услуги.

Copyright: Roman Kryvolapov