[BASIC #9] 장애 처리와 모니터링

2025. 9. 20. 21:51·MSA/MSA 기본

0. 들어가며

마이크로서비스 아키텍처에서는 수많은 서비스가 네트워크를 통해 통신한다. 이는 독립적인 배포와 확장이라는 장점을 제공하지만, 동시에 새로운 유형의 장애 가능성을 만들어낸다. 네트워크 지연, 서비스 과부하, 일시적 장애 등이 연쇄적으로 전파되어 전체 시스템을 마비시킬 수 있다.

 

이러한 분산 환경에서 시스템의 안정성과 회복 탄력성(Resilience)을 확보하는 것은 필수적이다. 또한 장애 발생 시 빠르게 원인을 파악하고 대응하기 위한 모니터링과 분산 추적(Distributed Tracing) 체계도 함께 갖춰야 한다.

 

이번 글에서는 서킷 브레이커(Circuit Breaker) 패턴과 Resilience4J를 활용한 장애 격리 기법, 그리고 Spring Cloud Sleuth와 Zipkin을 이용한 분산 추적 시스템 구축 방법을 살펴본다. 또한 Micrometer, Prometheus, Grafana를 활용한 모니터링 체계도 함께 다룰 예정이다.


1. CircuitBreaker와 Resilience4J

1.1. 서킷 브레이커 패턴이란?

서킷 브레이커는 전기 회로의 차단기에서 이름을 따온 장애 격리 패턴이다. 특정 서비스에 장애가 발생하거나 응답 시간이 지연될 때, 해당 서비스로의 요청을 차단하여 장애가 전체 시스템으로 전파되는 것을 방지한다.

서킷 브레이커의 세 가지 상태:

  1. CLOSED (닫힘): 정상 상태. 모든 요청이 외부 서비스로 전달된다. 실패 횟수가 임계치를 초과하면 OPEN 상태로 전환된다.
  2. OPEN (열림): 장애 상태. 모든 요청이 즉시 실패 처리되며, 외부 서비스를 호출하지 않는다. 설정된 시간이 지나면 HALF_OPEN 상태로 전환된다.
  3. HALF_OPEN (반만 열림): 복구 확인 상태. 제한된 수의 요청을 외부 서비스로 보내 복구 여부를 확인한다. 성공하면 CLOSED로, 실패하면 다시 OPEN으로 전환된다.

1.2. Resilience4J란?

Resilience4J는 Netflix Hystrix가 더 이상 유지보수되지 않게 되면서 등장한 경량 장애 내성 라이브러리다. 서킷 브레이커 외에도 다양한 장애 처리 기능을 제공한다.

Resilience4J의 주요 모듈:

  • CircuitBreaker: 서킷 브레이커 패턴 구현
  • RateLimiter: 요청 속도 제한
  • Retry: 실패한 요청 자동 재시도
  • Bulkhead: 동시 요청 수 제한 (격리)
  • TimeLimiter: 응답 시간 제한
  • Cache: 결과 캐싱

1.3. 의존성 추가

build.gradle (각 마이크로서비스)

dependencies {
    // Resilience4J
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
    implementation 'org.springframework.boot:spring-boot-starter-aop'

    // Actuator (모니터링)
    implementation 'org.springframework.boot:spring-boot-starter-actuator'

    // 기존 의존성...
}

1.4. Resilience4J 설정

application.yml (user-service, order-service 등)

resilience4j:
  circuitbreaker:
    instances:
      user-service:
        register-health-indicator: true
        sliding-window-type: COUNT_BASED
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3
        automatic-transition-from-open-to-half-open-enabled: true
        minimum-number-of-calls: 5
        event-consumer-buffer-size: 10
        record-exceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - org.springframework.web.client.HttpServerErrorException
        ignore-exceptions:
          - com.example.orderservice.exception.BusinessException

  retry:
    instances:
      user-service-retry:
        max-attempts: 3
        wait-duration: 1s
        retry-exceptions:
          - java.io.IOException
          - org.springframework.web.client.HttpServerErrorException

  bulkhead:
    instances:
      user-service-bulkhead:
        max-concurrent-calls: 10
        max-wait-duration: 10ms

  timelimiter:
    instances:
      user-service-timelimiter:
        timeout-duration: 3s

2. Users Microservice에 CircuitBreaker 적용

2.1. 서킷 브레이커가 필요한 상황

User Service를 호출하는 Order Service를 예로 들어보자. 만약 User Service에 장애가 발생하면, Order Service는 응답을 기다리며 스레드가 점유되고, 결국 Order Service의 리소스도 고갈되어 장애가 발생할 수 있다. 이러한 연쇄 장애를 방지하기 위해 서킷 브레이커를 적용한다.

2.2. Feign Client에 CircuitBreaker 적용

application.yml (order-service)

feign:
  circuitbreaker:
    enabled: true
  client:
    config:
      default:
        connectTimeout: 3000
        readTimeout: 3000
        loggerLevel: full

2.3. Fallback 클래스 구현

UserServiceFallback.java

package com.example.orderservice.client;

import com.example.orderservice.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Slf4j
@Component
public class UserServiceFallback implements UserServiceFeignClient {

    @Override
    public UserDto getUserById(String userId) {
        log.warn("Fallback called for getUserById. userId: {}", userId);

        // 기본값 또는 캐시된 데이터 반환
        UserDto fallbackUser = new UserDto();
        fallbackUser.setUserId(userId);
        fallbackUser.setName("Unknown User (Fallback)");
        fallbackUser.setEmail("unknown@fallback.com");
        fallbackUser.setOrders(Collections.emptyList());

        return fallbackUser;
    }

    @Override
    public Boolean checkUserExists(String userId) {
        log.warn("Fallback called for checkUserExists. userId: {}", userId);

        // 안전하게 false 반환
        return false;
    }

    @Override
    public List<UserDto> getUsers() {
        log.warn("Fallback called for getUsers");
        return Collections.emptyList();
    }
}

2.4. Feign Client에 Fallback 지정

UserServiceFeignClient.java

package com.example.orderservice.client;

import com.example.orderservice.dto.UserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceFeignClient {

    @GetMapping("/users/{userId}")
    UserDto getUserById(@PathVariable("userId") String userId);

    @GetMapping("/users/{userId}/exists")
    Boolean checkUserExists(@PathVariable("userId") String userId);

    @GetMapping("/users")
    List<UserDto> getUsers();
}

2.5. RestTemplate에 CircuitBreaker 적용

RestTemplateConfig.java

package com.example.orderservice.config;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public CircuitBreaker userServiceCircuitBreaker() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(10)
                .failureRateThreshold(50.0f)
                .waitDurationInOpenState(Duration.ofSeconds(10))
                .permittedNumberOfCallsInHalfOpenState(3)
                .automaticTransitionFromOpenToHalfOpenEnabled(true)
                .build();

        return CircuitBreaker.of("user-service", config);
    }

    @Bean
    public TimeLimiter userServiceTimeLimiter() {
        TimeLimiterConfig config = TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofSeconds(3))
                .build();

        return TimeLimiter.of("user-service", config);
    }
}

2.6. RestTemplate을 사용한 서킷 브레이커 적용

UserServiceClientWithResilience4j.java

package com.example.orderservice.client;

import com.example.orderservice.dto.UserDto;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

@Slf4j
@Component
@RequiredArgsConstructor
public class UserServiceClientWithResilience4j {

    private final RestTemplate restTemplate;

    private static final String USER_SERVICE_URL = "<http://user-service>";

    @CircuitBreaker(name = "user-service", fallbackMethod = "getUserByIdFallback")
    @Retry(name = "user-service-retry")
    @TimeLimiter(name = "user-service-timelimiter")
    public CompletionStage<UserDto> getUserById(String userId) {
        return CompletableFuture.supplyAsync(() -> {
            String url = USER_SERVICE_URL + "/users/" + userId;
            return restTemplate.getForObject(url, UserDto.class);
        });
    }

    public CompletionStage<UserDto> getUserByIdFallback(String userId, Throwable t) {
        log.warn("Fallback called for getUserById. userId: {}, error: {}", userId, t.getMessage());

        UserDto fallbackUser = new UserDto();
        fallbackUser.setUserId(userId);
        fallbackUser.setName("Unknown User (Fallback)");
        fallbackUser.setEmail("unknown@fallback.com");

        return CompletableFuture.completedFuture(fallbackUser);
    }

    @CircuitBreaker(name = "user-service", fallbackMethod = "checkUserExistsFallback")
    public Boolean checkUserExists(String userId) {
        String url = USER_SERVICE_URL + "/users/" + userId + "/exists";
        return restTemplate.getForObject(url, Boolean.class);
    }

    public Boolean checkUserExistsFallback(String userId, Throwable t) {
        log.warn("Fallback called for checkUserExists. userId: {}, error: {}", userId, t.getMessage());
        return false;  // 안전하게 false 반환
    }
}

2.7. 서킷 브레이커 이벤트 모니터링

CircuitBreakerEventListener.java

package com.example.orderservice.listener;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.core.EventPublisher;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class CircuitBreakerEventListener {

    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @PostConstruct
    public void registerEventListeners() {
        circuitBreakerRegistry.getAllCircuitBreakers().forEach(this::registerEventListeners);
    }

    private void registerEventListeners(CircuitBreaker circuitBreaker) {
        EventPublisher eventPublisher = circuitBreaker.getEventPublisher();

        eventPublisher.onSuccess(event ->
            log.debug("CircuitBreaker '{}' success: {}", circuitBreaker.getName(), event));

        eventPublisher.onError(event ->
            log.warn("CircuitBreaker '{}' error: {}", circuitBreaker.getName(), event));

        eventPublisher.onStateTransition(event ->
            log.info("CircuitBreaker '{}' state transition: {} -> {}",
                    circuitBreaker.getName(), event.getOldState(), event.getNewState()));

        eventPublisher.onReset(event ->
            log.info("CircuitBreaker '{}' reset", circuitBreaker.getName()));

        eventPublisher.onCallNotPermitted(event ->
            log.warn("CircuitBreaker '{}' call not permitted: {}", circuitBreaker.getName(), event));
    }
}

3. 분산 추적 개요

3.1. 분산 추적이 필요한 이유

마이크로서비스 아키텍처에서는 하나의 사용자 요청이 여러 서비스를 거쳐 처리된다. 예를 들어, 주문 요청은 API Gateway → Order Service → User Service → Catalog Service → Payment Service를 순차적으로 호출할 수 있다. 이때 장애가 발생하거나 지연이 생기면, 어떤 서비스에서 문제가 발생했는지 파악하기 매우 어렵다.

분산 추적의 핵심 개념:

  • Trace ID: 하나의 요청에 할당되는 고유 ID. 모든 서비스 로그에 포함되어 요청의 전체 흐름을 추적할 수 있게 한다.
  • Span ID: 개별 서비스 호출 단위에 할당되는 ID. 각 서비스에서의 처리 시간과 상세 정보를 포함한다.
  • Parent Span ID: 상위 Span의 ID로, 호출 관계를 트리 형태로 구성할 수 있게 한다.

3.2. Spring Cloud Sleuth란?

Spring Cloud Sleuth는 Spring Boot 애플리케이션에 분산 추적 기능을 자동으로 추가해주는 라이브러리다. HTTP 요청, 메시지 채널 등에 자동으로 Trace ID와 Span ID를 추가하고, 이를 로그에 포함시킨다.

3.3. Zipkin이란?

Zipkin은 분산 추적 데이터를 수집, 저장, 조회, 시각화하는 오픈소스 시스템이다. Sleuth에서 생성된 추적 데이터를 Zipkin 서버로 전송하면, 웹 UI를 통해 요청의 전체 흐름을 그래프로 확인할 수 있다.


4. 분산 추적 구현

4.1. 의존성 추가

build.gradle (모든 마이크로서비스)

dependencies {
    // Spring Cloud Sleuth
    implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'

    // Zipkin 연동
    implementation 'org.springframework.cloud:spring-cloud-sleuth-zipkin'

    // 기존 의존성...
}

참고: Spring Boot 3.x에서는 Micrometer Tracing으로 통합되었다. 최신 버전을 사용한다면 다음 의존성을 추가한다.

dependencies {
    // Micrometer Tracing
    implementation 'io.micrometer:micrometer-tracing-bridge-brave'
    implementation 'io.zipkin.reporter2:zipkin-reporter-brave'

    // 기존 의존성...
}

4.2. Zipkin 서버 설치

Docker로 Zipkin 설치

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

docker-compose.yml에 추가

version: '3'
services:
  zipkin:
    image: openzipkin/zipkin:latest
    container_name: zipkin
    ports:
      - "9411:9411"
    environment:
      - STORAGE_TYPE=mem

4.3. 애플리케이션 설정

application.yml (모든 마이크로서비스)

spring:
  application:
    name: user-service  # 각 서비스별로 다르게 설정

  sleuth:
    sampler:
      probability: 1.0  # 100% 샘플링 (개발 환경)
      # probability: 0.1  # 10% 샘플링 (프로덕션 환경)
    trace-id128: true
    log:
      slf4j:
        enabled: true

  zipkin:
    base-url: <http://localhost:9411>
    sender:
      type: web
    enabled: true

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

4.4. 로그 패턴 설정

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProperty scope="context" name="springAppName" source="spring.application.name"/>

    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %clr([${springAppName},%X{traceId:-},%X{spanId:-}]){yellow} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

4.5. Feign Client에 분산 추적 적용

Feign Client는 자동으로 Sleuth와 통합되므로 별도 설정 없이 Trace ID가 전파된다.

4.6. RestTemplate에 분산 추적 적용

RestTemplate도 Sleuth와 자동으로 통합된다. @LoadBalanced 어노테이션만 있으면 된다.

4.7. Kafka 메시지에 분산 추적 적용

KafkaConfig.java

package com.example.orderservice.config;

import brave.kafka.clients.KafkaTracing;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

@Configuration
@RequiredArgsConstructor
public class KafkaConfig {

    private final KafkaTracing kafkaTracing;

    @Bean
    public KafkaTemplate<String, Object> kafkaTemplate(
            ProducerFactory<String, Object> producerFactory) {
        KafkaTemplate<String, Object> template = new KafkaTemplate<>(producerFactory);
        return kafkaTracing.kafkaTemplate(template);
    }

    @Bean
    public ConsumerFactory<String, Object> consumerFactory(
            ConsumerFactory<String, Object> consumerFactory) {
        return kafkaTracing.consumerFactory(consumerFactory);
    }
}

5. 분산 추적 테스트

5.1. Zipkin UI 확인

Zipkin 서버가 실행 중이면 브라우저에서 http://localhost:9411에 접속한다.

5.2. 요청 생성 및 추적 확인

  1. API Gateway를 통해 주문 생성 요청을 보낸다.
    POST <http://localhost:8000/orders/test-user>
    Content-Type: application/json
    
    {
        "productId": "CATALOG-001",
        "quantity": 2,
        "unitPrice": 1500
    }
    
  2. Zipkin UI에서 "Run Query" 버튼을 클릭하면 최근 요청 목록이 표시된다.
  3. 특정 Trace를 클릭하면 서비스 간 호출 흐름과 각 구간의 소요 시간을 그래프로 확인할 수 있다.

5.3. 로그에서 Trace ID 확인

각 서비스의 로그에서 Trace ID를 확인할 수 있다.

2024-01-15 10:30:15.123 INFO [order-service,abc123def456,xyz789] 12345 --- [nio-8080-exec-1] c.e.o.service.OrderService : Order created: order-123
2024-01-15 10:30:15.234 INFO [user-service,abc123def456,pqr567] 23456 --- [nio-8081-exec-2] c.e.u.service.UserService : User found: test-user
2024-01-15 10:30:15.345 INFO [catalog-service,abc123def456,lmn890] 34567 --- [nio-8082-exec-3] c.e.c.service.CatalogService : Stock deducted: CATALOG-001

동일한 Trace ID(abc123def456)가 모든 서비스 로그에 포함되어 있어, 하나의 요청 흐름을 추적할 수 있다.


6. Micrometer 개요

6.1. Micrometer란?

Micrometer는 JVM 기반 애플리케이션의 메트릭을 수집하기 위한 벤더 중립적인 인터페이스다. Prometheus, Graphite, Datadog 등 다양한 모니터링 시스템에 메트릭을 전송할 수 있다.

6.2. 주요 메트릭 유형

  • Counter: 단조롭게 증가하는 값 (요청 수, 에러 수)
  • Gauge: 임의로 변하는 값 (현재 스레드 수, 메모리 사용량)
  • Timer: 지연 시간과 호출 빈도 측정
  • DistributionSummary: 분포 측정 (응답 크기)

7. Micrometer 구현

7.1. 의존성 추가

build.gradle (모든 마이크로서비스)

dependencies {
    // Micrometer
    implementation 'io.micrometer:micrometer-core'
    implementation 'io.micrometer:micrometer-registry-prometheus'

    // Actuator
    implementation 'org.springframework.boot:spring-boot-starter-actuator'

    // 기존 의존성...
}

7.2. 애플리케이션 설정

application.yml (모든 마이크로서비스)

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /actuator
  metrics:
    tags:
      application: ${spring.application.name}
    export:
      prometheus:
        enabled: true
        step: 1m
  endpoint:
    prometheus:
      enabled: true
    metrics:
      enabled: true

7.3. 커스텀 메트릭 생성

OrderMetricsService.java

package com.example.orderservice.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

@Slf4j
@Component
public class OrderMetricsService {

    private final Counter orderCreatedCounter;
    private final Counter orderFailedCounter;
    private final Timer orderProcessingTimer;
    private final AtomicLong activeOrdersGauge;

    public OrderMetricsService(MeterRegistry registry) {
        // 주문 생성 카운터
        this.orderCreatedCounter = Counter.builder("order.created.total")
                .description("Total number of orders created")
                .tag("status", "success")
                .register(registry);

        // 주문 실패 카운터
        this.orderFailedCounter = Counter.builder("order.created.total")
                .description("Total number of failed orders")
                .tag("status", "failed")
                .register(registry);

        // 주문 처리 시간 타이머
        this.orderProcessingTimer = Timer.builder("order.processing.time")
                .description("Order processing time")
                .publishPercentiles(0.5, 0.95, 0.99)
                .publishPercentileHistogram()
                .sla(Duration.ofMillis(100), Duration.ofMillis(500), Duration.ofSeconds(1))
                .register(registry);

        // 활성 주문 게이지
        this.activeOrdersGauge = registry.gauge("order.active", new AtomicLong(0));
    }

    public void recordOrderCreated() {
        orderCreatedCounter.increment();
        activeOrdersGauge.incrementAndGet();
    }

    public void recordOrderFailed() {
        orderFailedCounter.increment();
    }

    public void recordOrderCompleted() {
        activeOrdersGauge.decrementAndGet();
    }

    public <T> T recordOrderProcessingTime(Supplier<T> supplier) {
        return orderProcessingTimer.record(supplier);
    }

    public void recordOrderProcessingTime(Runnable runnable) {
        orderProcessingTimer.record(runnable);
    }
}

7.4. 서비스에 메트릭 적용

OrderService.java (메트릭 추가)

@Service
@Slf4j
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final OrderMetricsService orderMetricsService;
    private final KafkaTemplate<String, Object> kafkaTemplate;

    @Transactional
    public ResponseOrder createOrder(String userId, RequestOrder requestOrder) {
        return orderMetricsService.recordOrderProcessingTime(() -> {
            try {
                // 주문 생성 로직
                String orderId = UUID.randomUUID().toString();

                OrderEntity orderEntity = new OrderEntity();
                orderEntity.setProductId(requestOrder.getProductId());
                orderEntity.setQuantity(requestOrder.getQuantity());
                orderEntity.setUnitPrice(requestOrder.getUnitPrice());
                orderEntity.setTotalPrice(requestOrder.getQuantity() * requestOrder.getUnitPrice());
                orderEntity.setUserId(userId);
                orderEntity.setOrderId(orderId);
                orderEntity.setStatus("PENDING");

                orderRepository.save(orderEntity);

                // 메트릭 기록
                orderMetricsService.recordOrderCreated();

                // Kafka 이벤트 발행
                OrderEvent event = new OrderEvent(orderId, userId, requestOrder.getProductId(),
                        requestOrder.getQuantity(), LocalDateTime.now());
                kafkaTemplate.send("order-created-topic", orderId, event);

                return mapToResponseOrder(orderEntity);

            } catch (Exception e) {
                orderMetricsService.recordOrderFailed();
                throw e;
            }
        });
    }

    @Transactional
    public void completeOrder(String orderId) {
        OrderEntity order = orderRepository.findByOrderId(orderId)
                .orElseThrow(() -> new RuntimeException("Order not found"));

        order.setStatus("COMPLETED");
        orderRepository.save(order);

        // 주문 완료 메트릭 기록
        orderMetricsService.recordOrderCompleted();
    }
}

7.5. Prometheus 엔드포인트 확인

각 마이크로서비스의 http://localhost:{포트}/actuator/prometheus 엔드포인트에 접속하면 Prometheus 형식의 메트릭을 확인할 수 있다.

# HELP order_created_total Total number of orders created
# TYPE order_created_total counter
order_created_total{application="order-service",status="success",} 125.0
order_created_total{application="order-service",status="failed",} 3.0

# HELP order_processing_time Order processing time
# TYPE order_processing_time histogram
order_processing_time_count{application="order-service",} 128.0
order_processing_time_sum{application="order-service",} 45.678
order_processing_time_bucket{application="order-service",le="0.1",} 98.0
order_processing_time_bucket{application="order-service",le="0.5",} 25.0
order_processing_time_bucket{application="order-service",le="1.0",} 5.0
order_processing_time_bucket{application="order-service",le="+Inf",} 128.0

8. Prometheus와 Grafana 개요

8.1. Prometheus란?

Prometheus는 SoundCloud에서 개발한 오픈소스 모니터링 시스템이다. 풀(Pull) 방식으로 메트릭을 수집하고, 시계열 데이터베이스에 저장한다. 강력한 쿼리 언어(PromQL)를 제공하여 다양한 분석이 가능하다.

Prometheus 주요 특징:

  • 다차원 데이터 모델 (키-값 쌍으로 레이블링)
  • 유연한 쿼리 언어 (PromQL)
  • 풀 방식의 데이터 수집
  • 서비스 디스커버리와의 통합
  • 알림(Alerting) 기능

8.2. Grafana란?

Grafana는 메트릭 데이터를 시각화하는 오픈소스 대시보드 도구다. Prometheus, InfluxDB, Elasticsearch 등 다양한 데이터 소스를 지원하며, 풍부한 시각화 옵션을 제공한다.


9. Prometheus와 Grafana 설치

9.1. Docker로 Prometheus 설치

prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'user-service'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['host.docker.internal:60000', 'host.docker.internal:60001']
        labels:
          application: 'user-service'

  - job_name: 'order-service'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['host.docker.internal:61000', 'host.docker.internal:61001']
        labels:
          application: 'order-service'

  - job_name: 'catalog-service'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['host.docker.internal:62000']
        labels:
          application: 'catalog-service'

  - job_name: 'apigateway-service'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['host.docker.internal:8000']
        labels:
          application: 'apigateway-service'

docker-compose.yml에 Prometheus 추가

prometheus:
  image: prom/prometheus:latest
  container_name: prometheus
  volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
    - prometheus-data:/prometheus
  ports:
    - "9090:9090"
  command:
    - '--config.file=/etc/prometheus/prometheus.yml'
    - '--storage.tsdb.path=/prometheus'
    - '--web.console.libraries=/usr/share/prometheus/console_libraries'
    - '--web.console.templates=/usr/share/prometheus/consoles'
    - '--web.enable-lifecycle'

9.2. Docker로 Grafana 설치

docker-compose.yml에 Grafana 추가

grafana:
  image: grafana/grafana:latest
  container_name: grafana
  ports:
    - "3000:3000"
  volumes:
    - grafana-data:/var/lib/grafana
  environment:
    - GF_SECURITY_ADMIN_USER=admin
    - GF_SECURITY_ADMIN_PASSWORD=admin
    - GF_INSTALL_PLUGINS=grafana-piechart-panel

10. Prometheus와 Grafana 연동

10.1. Grafana에 Prometheus 데이터 소스 추가

  1. 브라우저에서 http://localhost:3000 접속 (아이디: admin, 비밀번호: admin)
  2. 왼쪽 메뉴에서 "Configuration" → "Data Sources" 클릭
  3. "Add data source" 버튼 클릭
  4. "Prometheus" 선택
  5. URL에 http://prometheus:9090 입력 (Docker 환경) 또는 http://localhost:9090 (로컬 환경)
  6. "Save & Test" 버튼 클릭하여 연결 확인

10.2. 대시보드 구성

주요 대시보드 패널 예시:

  1. 시스템 상태 패널
    • 각 서비스의 인스턴스 수
    • 서비스별 헬스 체크 상태
  2. 요청 처리량 패널
    • 서비스별 초당 요청 수
    • 엔드포인트별 요청 수
  3. 응답 시간 패널
    • 평균 응답 시간
    • 95백분위수 응답 시간
    • 99백분위수 응답 시간
  4. 에러율 패널
    • 서비스별 에러율
    • HTTP 4xx/5xx 에러 수
  5. 리소스 사용량 패널
    • CPU 사용률
    • 메모리 사용량
    • GC 활동

10.3. PromQL 쿼리 예시

초당 주문 생성 수

rate(order_created_total{application="order-service"}[1m])

평균 응답 시간 (최근 5분)

rate(order_processing_time_sum[5m]) / rate(order_processing_time_count[5m])

95백분위수 응답 시간

histogram_quantile(0.95, sum(rate(order_processing_time_bucket[5m])) by (le))

에러율

sum(rate(order_created_total{status="failed"}[5m])) / sum(rate(order_created_total[5m])) * 100

서비스별 활성 주문 수

order_active

10.4. 알림 설정 (Prometheus)

prometheus.yml에 알림 규칙 추가

rule_files:
  - "alert.rules.yml"

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

alert.rules.yml

groups:
  - name: microservice-alerts
    rules:
      - alert: HighErrorRate
        expr: |
          sum(rate(order_created_total{status="failed"}[5m]))
          /
          sum(rate(order_created_total[5m])) * 100 > 5
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "High error rate detected"
          description: "Error rate is {{ $value }}% for more than 2 minutes"

      - alert: ServiceDown
        expr: up{application="order-service"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Service {{ $labels.application }} is down"
          description: "Instance {{ $labels.instance }} has been down for more than 1 minute"

      - alert: HighResponseTime
        expr: |
          histogram_quantile(0.95, sum(rate(order_processing_time_bucket[5m])) by (le)) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High response time detected"
          description: "95th percentile response time is {{ $value }}s for more than 5 minutes"

11. 종합 모니터링 대시보드

11.1. 비즈니스 메트릭 대시보드

주요 지표:

  • 일별/시간별 주문 수
  • 총 판매 금액
  • 인기 상품 Top 10
  • 사용자별 주문 수
  • 주문 상태 분포 (PENDING, COMPLETED, FAILED)

11.2. 기술 메트릭 대시보드

주요 지표:

  • 서비스별 인스턴스 상태
  • JVM 메모리 사용량
  • 스레드 풀 상태
  • 데이터베이스 연결 풀 상태
  • Kafka 컨슈머 랙

11.3. 종합 상태 대시보드

Grafana 대시보드 JSON 예시 (일부)

{
  "dashboard": {
    "title": "Microservices Overview",
    "panels": [
      {
        "title": "Service Status",
        "type": "stat",
        "targets": [
          {
            "expr": "up{application=\\"order-service\\"}",
            "legendFormat": "Order Service"
          }
        ]
      },
      {
        "title": "Request Rate",
        "type": "graph",
        "targets": [
          {
            "expr": "sum(rate(http_server_requests_seconds_count[1m])) by (service)",
            "legendFormat": "{{ service }}"
          }
        ]
      },
      {
        "title": "Error Rate",
        "type": "graph",
        "targets": [
          {
            "expr": "sum(rate(http_server_requests_seconds_count{status=~\\"5..\\"}[1m])) by (service) / sum(rate(http_server_requests_seconds_count[1m])) by (service) * 100",
            "legendFormat": "{{ service }}"
          }
        ]
      }
    ]
  }
}

12. 결론

12.1. 장애 처리와 모니터링의 중요성

마이크로서비스 아키텍처에서는 장애가 불가피하다. 중요한 것은 장애가 발생하지 않도록 예방하는 것이 아니라, 장애가 발생하더라도 시스템 전체로 전파되지 않도록 격리하고, 빠르게 복구할 수 있는 능력이다.

Resilience4J를 활용한 서킷 브레이커, 재시도, 벌크헤드 패턴은 장애 격리의 핵심 도구다. Spring Cloud Sleuth와 Zipkin을 통한 분산 추적은 장애 발생 시 원인을 빠르게 파악할 수 있게 해준다. Micrometer, Prometheus, Grafana를 활용한 모니터링은 시스템의 건강 상태를 지속적으로 관찰하고 이상 징후를 조기에 발견할 수 있게 한다.

12.2. 실무 적용 시 고려사항

서킷 브레이커 설정:

  • 타임아웃은 서비스의 SLA에 맞게 설정
  • 실패 임계치는 비즈니스 요구사항에 따라 조정
  • 폴백(Fallback) 전략은 사용자 경험을 고려하여 설계

분산 추적:

  • 프로덕션 환경에서는 샘플링 비율 조정 (0.01 ~ 0.1)
  • 민감한 정보는 추적에서 제외
  • 장기 보관 정책 수립

모니터링:

  • 모든 서비스에 공통 메트릭 표준화
  • 비즈니스 메트릭과 기술 메트릭 분리
  • 알림은 신호 대비 노이즈 비율 최적화

12.3. 다음 단계

이번 글까지 마이크로서비스의 핵심 패턴들을 모두 살펴보았다. 서비스 디스커버리, API 게이트웨이, 설정 관리, 서비스 간 통신, 데이터 동기화, 장애 처리, 모니터링까지 마이크로서비스 구축에 필요한 대부분의 주제를 다루었다. 다음 글에서는 이렇게 구축한 마이크로서비스들을 Docker 컨테이너로 패키징하고, 실제 환경에 배포하는 방법을 살펴볼 예정이다. 컨테이너화의 기본 개념부터 Docker 이미지 생성, 그리고 전체 애플리케이션을 Docker Compose로 실행하는 과정을 단계적으로 알아보겠다.

'MSA > MSA 기본' 카테고리의 다른 글

[ADVANED #1] MSA 고급 패턴 정리  (0) 2025.09.21
[BASIC #10] Docker 기반 MSA 배포 전략  (0) 2025.09.21
[BASIC #8] Kafka 기반 데이터 동기화  (0) 2025.09.20
[BASIC #7] Microservice 간 통신 전략  (0) 2025.09.20
[BASIC #6] Configuration Service와 중앙 설정 관리  (0) 2025.09.20
'MSA/MSA 기본' 카테고리의 다른 글
  • [ADVANED #1] MSA 고급 패턴 정리
  • [BASIC #10] Docker 기반 MSA 배포 전략
  • [BASIC #8] Kafka 기반 데이터 동기화
  • [BASIC #7] Microservice 간 통신 전략
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[BASIC #9] 장애 처리와 모니터링
상단으로

티스토리툴바