본문 바로가기
Spring

스프링 부트가 제공하는 프로덕션 준비 기능

by hseong 2023. 5. 29.

0.

 

스프링 부트 - 핵심 원리와 활용 - 인프런 | 강의

실무에 필요한 스프링 부트는 이 강의 하나로 모두 정리해드립니다., - 강의 소개 | 인프런

www.inflearn.com

본 게시글은 해당 강의를 수강하고 정리한 내용입니다.

 

1. 액추에이터(actuator)란?

스프링 부트는 애플리케이션을 프로덕션 환경으로 전환할 때 애플리케이션을 모니터링하고 관리하는데 도움이 되는 다양한 추가 기능을 포함하고 있다. 이러한 프로덕션 준비 기능은 스프링 부트 액추에이터 모듈을 통해 제공한다.

스프링 부트 액추에이터는 지표(metric), 추적(trace), 감사(auditing), 모니터링과 같이 운영환경에서 시비스할 때 필요한 프로덕션 준비 기능을 편리하게 사용할 수 있는 다양한 편의 기능들을 제공한다.

이를 이용하기 위하여 다음 종속성을 build.gradle에 추가한다.

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

 

2. 엑추에이터 엔드포인트

설정

액추에이터가 제공하는 기능을 이용하기 위해서는 /actuator 로 시작하는 경로를 통해서 접근하면 된다.

각각의 엔드포인트를 사용하기 위해서는 1. 엔드포인트 활성화, 2. 엔드포인트 노출 2가지 과정이 필요하다. 엔드포인트는 대부분 기본적으로 활성화 되어 있기 때문에 HTTP에 어떤 엔드포인트를 노출할지 성택하면 된다.

엔드포인트 활성화

management:
  endpoint:
    shutdown:
      enabled: true
  • 특정 엔드포인트를 활성화 하려면 management.endpoint.{엔드포인트명}.enabled=true 를 적용하면 된다.
  • shutdown를 제외한 나머지 엔드포인트는 기본적으로 활성화되어 있다. shutdown의 경우 이름 그대로 서버를 내리는 기능을 가진 엔드포인트이기 때문에 주의하여야 한다.

엔드포인트 노출

management:
  endpoints:
    web:
      exposure:
        include: "*"
          exclude: "env,beans"
  • include를 통해 노출하고자 하는 엔드포인트를 지정하고, exclude를 통해 제외할 수 있다.
  • 각각의 엔드포인트는 다음 링크에서 확인할 수 있다.
  • Production-ready Features (spring.io)

 

health

/actuator/health

헬스 정보는 애플리케이션이 사용하는 데이터베이스가 응답하는지, 디스크 사용량에는 문제가 없는지와 같은 다양한 정보들을 포함하여 만들어진다.

management:
  endpoint:
    health:
      show-components:always
#      show-details: always
  • 헬스 정보를 자세히 보고 싶다면 show-details를 always로 설정하면 된다.
  • 간략한 정보를 보고 싶다면 show-components를 always로 설정하면 된다.
  • 만일 헬스 컴포넌트 중 하나라도 DOWN 된다면 전체 상태는 DOWN이 된다.
//show-components: always
{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP"
    },
    "diskSpace": {
      "status": "UP"
    },
    "ping": {
      "status": "UP"
    }
  }
}

 

logger

/actuator/loggers

로깅과 관련된 정보를 확인하고 실시간으로 변경할 수 있다.

{
  "levels": [
    "OFF",
    "ERROR",
    "WARN",
    "INFO",
    "DEBUG",
    "TRACE"
  ],
  "loggers": {
    "ROOT": {
      "configuredLevel": "INFO",
      "effectiveLevel": "INFO"
    },
    "SQL dialect": {
      "effectiveLevel": "INFO"
    },
    "_org": {
      "effectiveLevel": "INFO"
    },
    "_org.springframework": {
      "effectiveLevel": "INFO"
    },
    "_org.springframework.web": {
      "effectiveLevel": "INFO"
    },
		...
}
  • /actuator/loggers/{로거이름} 으로 특정 로거 이름 기준으로 조회할 수도 있다.
  • 해당 엔드포인트를 이용하여 애플리케이션을 다시 시작하지 않고 실시간으로 로그 레벨을 변경할 수 있다.

  • POST 메서드를 사용하여 메시지 바디에 변경하고자 하는 레벨을 지정하여 요청하면 로그 레벨의 실시간 변경이 가능하다.

 

이 외에도 애플리케이션의 정보를 제공하는 info, git 정보를 제공하는 git 등 다양한 정보를 제공하는 엔드포인트가 존재한다.

개발자는 액추에이터를 통해 애플리케이션의 유용한 정보들을 쉽게 획득할 수 있다. 그런만큼 액추에이터 URL 경로를 통해 외부인이 접근할 수 없도록 조치가 필요하다.

  • 엑추에이터 포트 설정
    • management.server.port=9292
  • 엑추에이터 URL 경로에 인증 설정
  • 엔드포인트 경로 변경
management:
  endpoints:
    web:
      base-path: "/manage"

 

3. 메트릭

마이크로미터

마이크로미터는 애플리케이션의 메트릭(측정 지표)을 마이크로미터가 정한 표준 방법으로 모아서 제공해준다. 사용하는 모니터링 툴에 따라 구현체만 변경해준다면 애플리케이션의 코드 변경 없이 자유롭게 모니터링 툴의 변경이 가능하다.

스프링 부트 엑추에이터는 마이크로미터가 제공하는 지표 수집을 @AutoConfiguration 을 통해 자동으로 등록해준다. /actuator/metrics 엔드포인트를 통하여 기본으로 제공되는 메트릭들을 확인할 수 있다.

{
  "names": [
    "application.ready.time",
    "application.started.time",
    "disk.free",
    "disk.total",
    "executor.active",
    "executor.completed",
    "executor.pool.core",
    "executor.pool.max",
    "executor.pool.size",
    "executor.queue.remaining",
    "executor.queued",
    "hikaricp.connections",
    "hikaricp.connections.acquire",
    "hikaricp.connections.active",
    "hikaricp.connections.creation",
    "hikaricp.connections.idle",
    "hikaricp.connections.max",
    "hikaricp.connections.min",
    "hikaricp.connections.pending",
    "hikaricp.connections.timeout",
    "hikaricp.connections.usage",
    "http.server.requests",
    "http.server.requests.active",
    "jdbc.connections.active",
		...
    ]
}

각각의 메트릭은 다음과 같은 패턴을 사용하여 자세히 확인할 수 있다.

/actuator/metrics/{name}

// /actuaotr/metrics/hikaricp.connections
{
  "name": "hikaricp.connections",
  "description": "Total connections",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 10.0
    }
  ],
  "availableTags": [
    {
      "tag": "pool",
      "values": [
        "HikariPool-1"
      ]
    }
  ]
}

 

프로메테우스

애플리케이션에서 발생한 메트릭의 이력을 확인하기 위해서는 메트릭을 보관하는 DB가 필요하다. 이를 위해 메트릭을 지속적으로 수집하고 DB에 저장하는 역할을 하는 것이 프로메테우스이다.

프로메테우스를 이용하기 위해 공식사이트에서 프로메테우스를 다운로드 해준다.

 

Download | Prometheus

An open-source monitoring system with a dimensional data model, flexible query language, efficient time series database and modern alerting approach.

prometheus.io

프로메테우스 연동하기

프로메테우스를 이용하기 위하여 다음 종속성을 build.gradle에 추가한다.

implementation 'io.micrometer:micrometer-registry-prometheus'
  • 마이크로미터 프로메테우스 구현 라이브러리를 추가한다.
  • 마이크로미터 표준 방식으로 측정된 메트릭들은 프로메테우스 구현체에 의해 프로메테우스 포맷으로 변환된다.
  • 액추에이터에 프로메테우스 메트릭 수집 엔드포인트가 자동으로 추가된다.
    • /actuator/prometheus

프로메테우스 수집 설정

프로메테우스가 애플리케이션의 /actuator/prometheus 를 호출해서 메트릭을 주기적으로 수집하도록 설정해야 한다. 압축을 해제한 폴더 내부의 설정 파일을 수정해준다.

prometheus.yml

# my global config
global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ["localhost:9090"]
  #추가
  - job_name: "spring-actuator"
    metrics_path: "/actuator/prometheus"
    scrape_interval: 1s
    static_configs:
      - targets: ["localhost:8080"]
  • job_name
    • 수집하는 이름
  • metrics_path
    • 수집할 경로
  • scrape_interval
    • 수집할 주기
    • 기본값은 1m 이다.
    • 애플리케이션의 성능에 대한 영향을 고려하여 적절할 주기를 선택해야 한다.
  • targets
    • 수집할 서버의 IP, PORT

prometheus.exe를 실행하면 기본 포트인 9090으로 접속할 수 있다. 그리 Status -> Targets에서 다음과 같이 스프링 애플리케이션이 등록된 것을 확인할 수 있다.

 

그라파나

프로메테우스가 메트릭을 보관하는 DB라면, 그라파나는 DB에 있는 데이터를 불러와 그래프로 보여주는 툴이다.

그라파나를 이용하기 위해서 공식사이트에서 그라파나를 다운로드해준다.

 

Download Grafana | Grafana Labs

Overview of how to download and install different versions of Grafana on different operating systems.

grafana.com

그라파나 연동

  • 좌측 상단 메뉴 버튼 → Administration → Data sources를 선택한다.
  • Add new data source를 선택
  • Prometheus를 선택

  • HTTP URL에 프로메테우스 URL을 입력
  • 하단의 Save & test를 선택

  • Data source is working이 표시되면 프로메테우스와의 연동이 완료된다.

 

그라파나 공유 대시보드 사용

그라파나는 다른 사람이 미리 만들어둔 대시보드 템플릿을 사용할 수 있다. 이를 통해 편리한 모니터링 환경의 구성이 가능하다.

Spring Boot 2.1 System Monitor | Grafana Labs

대시보드 템플릿을 사용하기 위해 Copy Id to clipboard 를 선택하여 템플릿의 ID를 복사한다.

  • Dashboards → New → Import

  • Import via grafana.com에 ID를 입력한 뒤 Load를 선택
  • Prometheus 데이터소스를 선택하고 Import 버튼 선택

 

4. 커스텀 메트릭 만들기

MeterRegistry

마이크로미터 기능을 제공하는 핵심 컴포넌트이다. 스프링을 통해서 주입 받아 사용하고 이를 통해 카운터, 게이지 등을 등록한다.

카운터(Counter)

  • 단조롭게 증가하는 단일 누적 측정항목
  • 값을 증가하거나 0으로 초기화 하는 것만 가능
@Override
public void order() {
    log.info("주문");
    stock.decrementAndGet();

    Counter.builder("my.order")  //카운터를 생성한다. builder()의 인수로 메트릭 이름을 지정한다.
            .tag("class", this.getClass().getName())
            .tag("method", "order")  //프로메테우스에서 필터할 수 있는 레이블을 지정한다.
            .description("order")
            .register(registry)  //만든 카운터를 MeterRegistry 에 등록한다.
            .increment();  //카운터의 값을 하나 증가한다.
}
  • Counter.builder(name)
    • 카운터를 생성한다.
    • name에는 메트릭 이름을 지정한다.
  • tag(key, value)
    • 프로메테우스에서 필터할 수 있는 레이블을 지정한다.
  • register(meterRegistry)
    • 생성한 카운터를 MeterRegistry에 등록한다.
  • increment()
    • 카운터의 값을 하나 증가한다.

@Counted

마이크로미터는 비즈니스 로직과 메트릭을 관리하는 로직을 분리하기 위한 AOP 구성요소를 이미 만들어두었다.

@Counted("my.order")
@Override
public void order() {
log.info("주문");
    stock.decrementAndGet();
}
  • @Counted
    • 측정을 원하는 메서드에 메트릭 이름과 함께 지정한다.
    • 이렇게 사용하면 tag에 method를 기준으로 분류해서 적용한다.
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
    return new CountedAspect(registry);
}
  • CountedAspect를 등록하면 @Counted를 인지해서 Counter를 사용하는 AOP를 적용한다.
  • CountedAspect를 빈으로 등록하지 않으면 @Counted 관련 AOP가 동작하지 않는다.
{
  "name": "my.order",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 10.0
    }
  ],
  "availableTags": [
    {
      "tag": "result",
      "values": [
        "success"
      ]
    },
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    },
    {
      "tag": "method",
      "values": [
        "cancel",
        "order"
      ]
    },
    {
      "tag": "class",
      "values": [
        "hello.order.v2.OrderServiceV2"
      ]
    }
  ]
}

Timer

  • 시간을 측정하는데 사용되는 메트릭 측정 도구이다.
  • 카운터와 유사하며, 실행 시간도 함께 측정할 수 있다.
    • seconds_count: 누적 실행 수(카운터)
    • seconds_sum: 실행 시간의 합(sum)
    • seconds_max: 최대 실행 시간(게이지)
      • 내부에 타임 윈도우라는 개념이 있어서 1 ~ 3분 마다 최대 실행 시간이 다시 계산된다.
@Override
public void order() {
    Timer timer = Timer.builder("my.order")  //타이머를 생성한다. name 에는 메트릭 이름을 지정한다.
            .tag("class", this.getClass().getName())
            .tag("method", "order")  //프로메테우스에서 필터할 수 있는 레이블로 사용된다.
            .description("order")
            .register(registry);  //타이머를 MeterRegistry 에 등록한다.

    timer.record(() -> {  //타이머를 사용한다. 시간을 측정할 내용을 함수로 포함한다.
        log.info("주문");
        stock.decrementAndGet();
        sleep(500);
    });
}
  • Timer.builder(name)
    • 타이머를 생성한다.
    • name에는 메트릭 이름을 지정한다.
  • tag
    • 프로메테우스에서 필터할 수 있는 레이블을 지정한다.
  • register(meterRegistry)
    • 타이머를 MeterRegistry에 등록한다.
  • timer.record()
    • 타이머를 사용한다.
    • 시간을 측정할 내용을 함수로 포함한다.

@Timed

카운터와 동일하게 AOP 구성요소가 만들어져 있다.

@Timed("my.order")
@Slf4j
public class OrderServiceV4 implements OrderService {

    private AtomicInteger stock = new AtomicInteger(100);

    @Override
    public void order() {
				log.info("주문");
        stock.decrementAndGet();
        sleep(500);
    }
		...
}
  • @Timed
    • 타입이나 메서드 중에 적용가능하다.
    • 타입에 적용하면 해당 타입의 모든 public 메서드에 타이머가 적용된다.
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
    return new TimedAspect(registry);
}
  • TimedAspect를 등록해야 @Timed에 AOP가 적용된다.
{
  "name": "my.order",
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 18.0
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 9.315805
    },
    {
      "statistic": "MAX",
      "value": 0.6730103
    }
  ],
  "availableTags": [
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    },
    {
      "tag": "method",
      "values": [
        "cancel",
        "order"
      ]
    },
    {
      "tag": "class",
      "values": [
        "hello.order.v4.OrderServiceV4"
      ]
    }
  ]
}
  • COUNT
    • 누적 실행 수
  • TOTAL_TIME
    • 실행 시간의 합
  • MAX
    • 최대 실행 시

Gauge(게이지)

  • 임의로 오르내릴 수 있는 단일 숫자 값을 나타내는 메트릭
  • 값의 현재 상태를 보는데 사용한다.
  • 값이 증가하거나 감소할 수 있다.
@Configuration
public class StockConfigV1 {

    @Bean
    public MyStockMetric myStockMetric(OrderService orderService, MeterRegistry registry) {
        return new MyStockMetric(orderService, registry);
    }

    @Slf4j
    static class MyStockMetric {
        private OrderService orderService;
        private MeterRegistry registry;

        public MyStockMetric(OrderService orderService, MeterRegistry registry) {
            this.orderService = orderService;
            this.registry = registry;
        }

        @PostConstruct
        public void init() {
            Gauge.builder("my.stock", orderService, service -> {
                log.info("stock gauge call");
                int stock = service.getStock().get();
                return stock;
            }).register(registry);
        }
    }
}
  • Gauge를 만들 때 전달한 함수는 외부에서 메트릭을 확인할 때 마다 호출된다.
  • 해당 함수의 반환 값이 게이지의 값이다.
@Bean
public MeterBinder stockSize(OrderService orderService) {
   return registry -> Gauge.builder("my.stock", orderService, service -> {
       log.info("stock gauge call");
       return service.getStock().get();
   }).register(registry);
}
  • MeterBinder 타입을 바로 반환하면 게이지를 단순하게 등록할 수 있다.
{
  "name": "my.stock",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 97.0
    }
  ],
  "availableTags": [
    
  ]
}

이렇게 만든 커스텀 메트릭들은 프로메테우스 구현체에 의해 프로메테우스 포맷으로 변환되고, 그라파나 대시보드에 등록하여 그래프로 쉽게 확인할 수 있는 지표로 사용할 수 있다.