ЛАБ 11

Микросервис Архитектур

3.1 Лабораторийн зорилго

Энэ лабораторид та:

  1. 3 микросервис үүсгэх (Student, Teacher, Gateway)
  2. Service Discovery (Eureka) тохируулах
  3. Сервис хоорондын REST дуудлага хийх
  4. Circuit Breaker (Resilience4j) нэмэх
  5. Docker Compose-оор бүгдийг ажиллуулах

Хэл: Java 17+ / Spring Boot 3.x | Framework: Spring Cloud | Хэрэгсэл: Docker, Docker Compose


3.2 Лаб 1: Eureka Service Registry

Алхам 1: Eureka Server үүсгэх

pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Application class:

@SpringBootApplication
@EnableEurekaServer
public class RegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(RegistryApplication.class, args);
    }
}

application.yml:

server:
  port: 8761

eureka:
  client:
    register-with-eureka: false   # Өөрийгөө бүртгэхгүй
    fetch-registry: false
  server:
    enable-self-preservation: false

Алхам 2: Student Service — Eureka Client

pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml:

server:
  port: 8081

spring:
  application:
    name: student-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

Алхам 3: Teacher Service — Eureka Client

server:
  port: 8082

spring:
  application:
    name: teacher-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

Шалгах:

# Eureka Dashboard
# http://localhost:8761 → student-service, teacher-service харагдана

3.3 Лаб 2: Сервис хоорондын REST дуудлага

Student Service → Teacher Service дуудах

// Student Service — RestClient тохиргоо
@Configuration
public class RestClientConfig {

    @Bean
    @LoadBalanced   // Eureka-аар IP хаяг олно
    public RestClient.Builder restClientBuilder() {
        return RestClient.builder();
    }
}
// Student Service — Teacher Client
@Service
public class TeacherClient {

    private final RestClient restClient;

    public TeacherClient(RestClient.Builder builder) {
        this.restClient = builder
            .baseUrl("http://teacher-service")  // Eureka нэр (IP биш!)
            .build();
    }

    public TeacherResponse getTeacher(Long id) {
        return restClient.get()
            .uri("/api/teachers/{id}", id)
            .retrieve()
            .body(TeacherResponse.class);
    }
}
// Student Service — Controller
@RestController
@RequestMapping("/api/students")
public class StudentController {

    private final StudentService studentService;
    private final TeacherClient teacherClient;

    @GetMapping("/{id}/with-teacher")
    public StudentWithTeacherResponse getStudentWithTeacher(@PathVariable Long id) {
        StudentResponse student = studentService.getById(id);
        TeacherResponse teacher = teacherClient.getTeacher(student.teacherId());
        return new StudentWithTeacherResponse(student, teacher);
    }
}

public record StudentWithTeacherResponse(
    StudentResponse student,
    TeacherResponse teacher
) {}

Тест хийх:

# Student + Teacher мэдээлэл хамт авах
curl http://localhost:8081/api/students/1/with-teacher

# Хариу:
# {
#   "student": {"id":1,"name":"Бат","teacherId":5},
#   "teacher": {"id":5,"name":"Дорж","department":"CS"}
# }

3.4 Лаб 3: Circuit Breaker (Resilience4j)

Алхам 1: Dependency нэмэх

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

Алхам 2: Circuit Breaker нэмэх

@Service
public class TeacherClient {

    private final RestClient restClient;

    @CircuitBreaker(name = "teacherService", fallbackMethod = "getTeacherFallback")
    @Retry(name = "teacherService")
    public TeacherResponse getTeacher(Long id) {
        return restClient.get()
            .uri("/api/teachers/{id}", id)
            .retrieve()
            .body(TeacherResponse.class);
    }

    // Teacher Service унтарсан бол → Fallback хариу
    private TeacherResponse getTeacherFallback(Long id, Throwable ex) {
        return new TeacherResponse(id, "Багшийн мэдээлэл түр авах боломжгүй", "N/A");
    }
}

Алхам 3: Тохиргоо

# application.yml
resilience4j:
  circuitbreaker:
    instances:
      teacherService:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3
  retry:
    instances:
      teacherService:
        max-attempts: 3
        wait-duration: 1s

Тест хийх:

# Teacher Service ажиллаж байхад
curl http://localhost:8081/api/students/1/with-teacher
# → {"student":{...},"teacher":{"id":5,"name":"Дорж"}}

# Teacher Service ЗОГСООХ (docker stop teacher-service)
curl http://localhost:8081/api/students/1/with-teacher
# → {"student":{...},"teacher":{"id":5,"name":"Багшийн мэдээлэл түр авах боломжгүй"}}
# Circuit Breaker → Fallback ажиллалаа!

3.5 Лаб 4: API Gateway (Spring Cloud Gateway)

Алхам 1: Gateway үүсгэх

pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Алхам 2: Gateway тохиргоо

server:
  port: 8080

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: student-service
          uri: lb://student-service
          predicates:
            - Path=/api/students/**

        - id: teacher-service
          uri: lb://teacher-service
          predicates:
            - Path=/api/teachers/**

      default-filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10
            redis-rate-limiter.burstCapacity: 20

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

Тест хийх:

# Gateway-р дамжуулж дуудах (8080 port)
curl http://localhost:8080/api/students
# → Student Service-ийн хариу

curl http://localhost:8080/api/teachers
# → Teacher Service-ийн хариу

# Client зөвхөн 8080 порт мэдэхэд хангалттай!

3.6 Лаб 5: Docker Compose — Бүгдийг нэгтгэх

docker-compose.yml

version: '3.8'

services:
  # Eureka Registry
  registry:
    build: ./registry-service
    ports:
      - "8761:8761"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"]
      interval: 10s
      retries: 5

  # Student Service
  student-service:
    build: ./student-service
    ports:
      - "8081:8081"
    environment:
      - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://registry:8761/eureka/
      - SPRING_DATASOURCE_URL=jdbc:postgresql://student-db:5432/studentdb
      - SPRING_DATASOURCE_USERNAME=${DB_USERNAME}
      - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
    depends_on:
      registry:
        condition: service_healthy
      student-db:
        condition: service_healthy

  # Teacher Service
  teacher-service:
    build: ./teacher-service
    ports:
      - "8082:8082"
    environment:
      - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://registry:8761/eureka/
      - SPRING_DATASOURCE_URL=jdbc:postgresql://teacher-db:5432/teacherdb
      - SPRING_DATASOURCE_USERNAME=${DB_USERNAME}
      - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
    depends_on:
      registry:
        condition: service_healthy
      teacher-db:
        condition: service_healthy

  # API Gateway
  api-gateway:
    build: ./api-gateway
    ports:
      - "8080:8080"
    environment:
      - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://registry:8761/eureka/
    depends_on:
      registry:
        condition: service_healthy
      student-service:
        condition: service_started
      teacher-service:
        condition: service_started

  # Student DB
  student-db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: studentdb
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - student-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"]
      interval: 5s
      retries: 5

  # Teacher DB
  teacher-db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: teacherdb
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - teacher-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"]
      interval: 5s
      retries: 5

volumes:
  student-data:
  teacher-data:

Ажиллуулах:

# .env файл
echo "DB_USERNAME=admin" > .env
echo "DB_PASSWORD=secret123" >> .env

# Бүгдийг эхлүүлэх
docker-compose up -d --build

# Шалгах
curl http://localhost:8080/api/students    # Gateway → Student Service
curl http://localhost:8080/api/teachers    # Gateway → Teacher Service
curl http://localhost:8761                 # Eureka Dashboard

# Зогсоох
docker-compose down -v