프로덕션 준비 기능
프로덕션 준비(Production-ready) 기능은 스프링 부트 어플리케이션에서 제공하는 여러가지 정보를 모니터링하기 쉽게 해주는 기능을 말한다.
서비스를 운영할 때 장애는 언제든 발생할 수 있으니 모니터링을 통해 잘 대응하는 것이 중요하다.
어플리케이션을 개발할 때 기능개발 뿐 아니라 서비스에 문제가 없는지 모니터링하고 지표를 심어서 감시하여 오류나 문제들을 빠르게 발견하고 쉽게 대응할 수 있어야 한다.
운영 환경에서 서비스 할 때는 이런 비기능적 요구사항인 프로덕션 준비 기능을 갖추고 나가야한다.
스프링 부트에서는 어플리케이션의 여러 지표들을 모니터링하거나 매트릭(metric)과 같은 기능을 HTTP와 JMX 엔드포인트를 통해 제공할 수 있는 엑추에이터 라이브러리가 있다.
또한 엑추에이터는 마이크로미터, 프로메테우스, 그라파나, 핀 포인트 등 여러 모니터링 시스템과 쉽게 연동할 수 있도록하는 기능도 제공한다.
엑추에이터 설정
엑추에이터를 사용하기 위해서는 의존성을 빌드에 추가해야 한다.
// Monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core:1.11.2'
implementation 'io.micrometer:micrometer-registry-prometheus'
Plain Text
복사
엑추에이터의 모든 엔트포인트의 경로에는 /actuator의 기본 경로가 앞에 붙는다. 기본 경로는 아래와 같이 application.properties나 application.yml에서 바꿀 수 있다.
management:
endpoints:
web:
base-path: /management
YAML
복사
엑추에이터의 엔드포인트를 사용하기 위해서는 엔드 포인트를 활성화하고 엔드포인트를 노출해야 볼 수 있다.
기본적으로 많은 기능들이 활성화 되어있고, shutdown과 같이 중요하거나 민감한 엔드포인트들은 활성화 되어있지 않아 아래처럼 따로 활성화 해주어야 한다.
management:
endpoint:
shutdown:
enabled: true
YAML
복사
활성화된 엔드 포인트를 노출해야 도메인 뒤에 /actuator를 붙여 데이터를 확인할 수 있다. 엔드 포인트를 노출하도록 지정하는 방법은 아래와 같다. 활성화는 했지만 노출하지 않도록 할 수 있고, *을 통해 모든 엔드 포인트를 노출시킬 수도 있다.
management:
endpoints:
web:
exposure:
include: health,info,beans,conditions
exclude: threaddump, heapdump
YAML
복사
엔드 포인트의 종류는 아주 많지만 자주 쓰이는 것들만 정리하면 다음과 같다.
•
beans : 스프링 컨테이너에 등록된 스프링 빈을 보여준다.
•
conditions : condition을 통해서 빈을 등록할 때 평가 조건과 일치하거나 일치하지 않는 이유를 표시한다.
•
configprops : @configurationProperties 를 보여준다.
•
env: Environment 정보를 보여준다.
•
health : 애플리케이션 헬스 정보를 보여준다.
•
httpexchanges : HTTP 호출 응답 정보를 보여준다. HttpExchangeRepository 를 구현한 빈을 별도로 등록해야 한다.
•
info : 애플리케이션 정보를 보여준다.
•
loggers : 애플리케이션 로거 설정을 보여주고 변경도 할 수 있다.
•
metrics : 애플리케이션의 각종 지표(메트릭 정보)를 보여준다.
•
mappings : @RequestMapping 정보를 보여준다.
•
threaddump : 쓰레드 덤프를 실행해서 보여준다.
•
shutdown : 애플리케이션을 종료한다. 이 기능은 기본으로 비활성화 되어 있고, get 명령으로는 실행되지 않는다.
Health
어플리케이션의 건강 상태를 알아볼 수 있는 엔드포인트로, 설정에 따라 db, mongo, redis, diskspace, ping 등 수많은 정보들을 제공한다. 추가적으로 원하는 상태를 보여주도록 추가할 수 있다.
{
"status": "up"
}
JSON
복사
기본적으로 위와 같이 아주 간단한 정보만 보여주고, 조금 더 상세하게 보고 싶다면 아래와 같이 application.yml에 설정해주어야 한다.
management:
endpoint:
health:
# show-components: always # 여러 상태 정보들을 간략하게 표시
show-details: always
YAML
복사
{
"status": "UP",
"details": {
"diskSpace": {
"status": "UP",
"details": {
"total": 499963174912,
"free": 394359185408,
"threshold": 10485760
}
},
"mongo": {
"status": "UP",
"details": {
"version": "3.2.2"
}
},
"refreshScope": {
"status": "UP"
},
"discoveryComposite": {
"status": "UP",
"details": {
"discoveryClient": {
"status": "UP",
"details": {
"services": [
]
}
},
"eureka": {
"description": "Eureka discovery client has not yet successfully connected to a Eureka server",
"status": "UP",
"details": {
"applications": {
}
}
}
}
},
"configServer": {
"status": "UNKNOWN",
"details": {
"error": "no property sources located"
}
},
"hystrix": {
"status": "UP"
}
}
}
JSON
복사
DB 연결 상태 같은 경우에는 JDBC의 validationQuery 기능으로 연결 상태를 확인하는 query를 날려보고 연결 상태를 가져온다.
상태는 UP, DOWN, UNKNOWN, OUT_OF_SERVICE 4가지가 있고, 각각의 상태를 가지며 전체 상태는 1개라도 DOWN 되어있다면 DOWN으로 표시된다.
•
UP : 외부 시스템이 작동 중이고 접근 가능
•
DOWN : 외부 시스템이 작동하지 않거나 접근할 수 없음
•
UNKNOWN : 외부 시스템의 상태가 분명하지 않음
•
OUT_OF_SERVICE : 외부 시스템에 접근할 수 있지만, 현재는 사용할 수 없음
Info
실행 중인 스프링 부트 어플리케이션에 관련된 정보들을 볼 수 있는 엔드 포인트이다.
기본적으로 제공하는 기능들은 다음과 같다.
•
java : 자바 런타임 정보
•
os : OS 정보
•
env : Environment에서 info로 시작하는 정보
•
build : 빌드 정보, META-INF/build-info.properties 파일이 필요하다.
•
git : git 정보, git.properties 파일이 필요하다.
env, java, os 기능은 기본적으로 비활성화 되어있어, 활성화 해주어야 한다.
management:
info:
java:
enabled: true
os:
enable: true
env:
enabled: true
YAML
복사
env의 환경변수에 새로운 환경변수를 추가하여, 엔드 포인트에서 확인하려면 다음과 같이 하면 된다.
info:
my-app:
name: my-actuator
version: 1.0
company: actuator-test
YAML
복사
{
"my-app": {
"name": "my-actuator",
"version": "1.0",
"company": "actuator-test"
}
}
JSON
복사
build의 경우 build.gradle에 아래의 설정을 추가해주어야, 빌드 산출물 jar 파일 내에 MEAT-INF/build-info.properties가 포함되어 이 정보를 바탕으로 info 엔드 포인트가 정보를 출력한다.
springBoot {
buildInfo()
}
Plain Text
복사
{
"build": {
"version": "0.0.1-SNAPSHOT",
"artifact": "spring-boot-actuator-level1",
"name": "spring-boot-actuator-level1",
"group": "com.nhnent.forward",
"time": "2018-10-25T05:18:50.466Z"
}
}
JSON
복사
git의 경우에는 plugin에 아래의 설정을 추가해주고 디렉토리에 git이 연동되어 있어야 git.properties가 생성되고 해당 정보를 바탕으로 엔드 포인트 정보를 출력한다.
id “com.gorylenko.gradle-git-properties” version “2.4.1”
Plain Text
복사
{
"git": {
"branch": "commit_id_plugin",
"commit": {
"id": "7adb64f",
"time": "2016-08-17T19:30:34+0200"
}
}
}
JSON
복사
Loggres
각 컴포넌트들 별로 설정된 로그 레벨을 확인할 수 있는 엔드 포인트로, 해당 엑추에이터를 통해 실시간으로 로그 레벨을 변경할 수 있다.
기본 로그 레벨이 INFO로 되어있고 하위 컴포넌트들은 전부 상위 레벨을 따라가기 때문에, 모든 log 기본값이 INFO로 되어있다.
엑추에이터 경로 뒤에 /actuator/loggers/hello.controller처럼 확인하고 싶은 컴포넌트를 붙여 해당 컴포넌트의 로그 레벨만 따로 확인 가능하다.
로그 레벨의 경우 DEBUG와 TRACE를 많이 달면 로그 저장 용량이나 성능면에서 좋지 못하기 때문에 일반적으로 INFO를 많이 사용한다.
이런 로그 레벨을 변경하기 위해서는 일반적으로 로그 설정 변경 후 재시작하여 변경하지만, 해당 엑추에이터 경로에 POST 요청으로 아래와 같은 로그 레벨을 body로 담아 보내면 실시간으로 로그 레벨을 변경할 수 있다.
{
"configuredLevel": TRACE
}
JSON
복사
Httpexchanges
HTTP 요청과 응답의 기록을 확인하고 싶다면 HttpExchangeRepository 인터페이스의 구현체를 빈으로 등록하여 httpexchanges 엔드 포인트로 사용하면 된다.
@Configuration
public class ActuatorConfiguration {
@Bean
public HttpExchangeRepository httpExchangeRepository()
{
InMemoryHttpExchangeRepository repository = new InMemoryHttpExchangeRepository();
repository.setCapacity(1000);
return repository;
}
}
Java
복사
스프링 부트는 기본적으로 InMemoryHttpExchangeRepository를 제공하며, 최대 100개를 저장하고 setCapacity()로 최대 저장 수를 변경할 수 있다.
Metrics
메트릭은 모니터링을 위한 여러 지표들을 확인할 수 있는 엔드 포인트로, 일반적으로 마이크로미터가 제공하는 지표 수집 기능으로 다른 모니터링 툴에 연동하여 사용된다.
{
"names": [
"application.ready.time",
"application.started.time",
"disk.free",
"disk.total",
"hikaricp.connections",
"hikaricp.connections.acquire",
"hikaricp.connections.active",
"hikaricp.connections.idle",
"hikaricp.connections.max",
"hikaricp.connections.usage",
...
]
}
JSON
복사
/actuator/metrics/{name}과 같이 metrics 경로 뒤에 확인하고 싶은 지표의 이름을 넣어 더 자세한 정보들을 확인할 수 있다.
// /actuator/metrics/jvm.memory.used
{
"name": "jvm.memory.used",
"description": "The amount of used memory",
"baseUnit": "bytes",
"measurements": [
{
"statistic": "VALUE",
"value": 124404728
}
],
"availableTags": [
{
"tag": "area",
"values": [
"heap",
"nonheap"
]
},
{
"tag": id,
"values": [
"G1 Survivor Space",
"Compressed Class Space",
"Metaspace",
"CodeCache",
"G1 Old Gen",
"G1 Eden Space"
]
}
]
}
JSON
복사
/actuator/metrics/jvm.memory.used?tag=area:heap과 같이 상세화면에서도 뒤에 파라미터 형태로 태그(tag)를 넣어주면, 해당 태그에 해당하는 지표값들만 따로 확인할 수 있다.
엑추에이터에서 기본적으로 다양한 메트릭을 제공한다.
•
JVM 메트릭
◦
메모리 및 버퍼 풀 세부 정보
◦
가비지 수집 관련 통계
◦
스레드 활용
◦
로드 및 언로드된 클래스 수
◦
JVM 버전 정보
◦
JIT 컴파일 시간
•
시스템 메트릭
◦
CPU 지표
◦
파일 디스트립터 메트릭
◦
가동 시간 메트릭
◦
사용 가능한 디스크 공간
•
애플리케이션 시작 메트릭
◦
애플리케이션을 시작하는데 걸린 시간(application.started.time) - ApplicationStartedEvent로 측정(스프링 컨테이너가 온전히 실행된 이후 측정 → 측정 후 커맨드 라인 러너 호출)
◦
애플리케이션이 요청을 처리할 준비까지 걸린 시간(application.ready.time) - ApplicationReadyEvent로 측정(커맨드 라인 러너가 실행된 이후 측정)
•
스프링 MVC 메트릭
◦
스프링 MVC 컨트롤러가 처리하는 모든 요청을 다룸
◦
uri, method, status, exception, outcome 등 여러 TAG를 사용해 더 자세히 분류하여 확인할 수 있다.
•
데이터 소스 메트릭
◦
최대 커넥션, 최소 커넥션, 활성 커넥션, 대기 커넥션 수 등을 확인할 수 있다.
◦
히카리 커넥션 풀을 사용하면 hikaricp를 통해 더 자세한 커넥션 풀에 관련된 메트릭을 확인할 수 있다.
•
로그 메트릭
◦
logback 로그에 대한 메트릭을 확인할 수 있다.
◦
trace, debug, info, warn, error 각각 로그 레벨에 따른 로그 수를 확인할 수 있다.
•
톰캣 메트릭
◦
톰캣의 최대 스레드, 사용 스레드 수를 포함한 다양한 메트릭을 확인할 수 있다.
◦
톰캣 메트릭을 모두 사용하려면 아래와 같이 옵션을 켜야한다.(옵션 안켜면 tomcat.session만 노출)
server:
tomcat:
mbeanregistry:
enabled: true
YAML
복사
•
사용자가 직접 정의한 메트릭
◦
주문 수, 취소 수 등 사용자가 직접 메트릭을 정의하여 사용할 수 있다.
이와 같은 많은 지표들을 저장하여 보관해서 과거의 데이터들을 확인하려면 데이터베이스가 필요하고, 이런 매트릭들을 그래프를 통해 한눈에 쉽게 확인할 수 있는 대시보드도 필요하다.
사용자 메트릭 등록하여 사용하기
여러 자주 사용되는 기술 메트릭은 이미 등록 되어있으니, 그러한 메트릭들을 잘 사용해서 대시보드를 구성하고 모니터링을 하면 된다.
이미 등록된 메트릭 외에 각각의 비즈니스에 특화된 메트릭을 등록하여 모니터링하면, 시스템의 비즈니스 문제를 빠르게 파악하고 문제를 해결하는데 도움을 준다.
Counter를 통해 사용자 메트릭 등록하기
MeterRegistry는 마이크로미터 기능을 제공하는 핵심 컴포넌트로, 스프링을 통해서 주입받아 사용하고 카운터, 게이지 등을 등록한다.
Counter는 선형적으로 증가하는 단일 누적 측정항목으로 등록하여, 단일 값으로 보통 하나씩 증가하고 누적이므로 전체 값을 포함(total)한다.
Counter로 설정된 메트릭은 값이 증가하거나, 0으로 초기화하는 것만 가능하다.
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public void order() {
log.info("주문");
stock.decrementAndGet();
// micrometer의 Counter 사용해야함
Counter.builder("my.order") // 메트릭 이름
.tag("class", this.getClass().getName()) // 태그(레이블) 추가
.tag("method", "order") // 태그(레이블) 추가
.description("order") // 설명
.register(registry).increment(); // 등록 및 카운터 증가
}
public void cancel() {
log.info("주문 취소");
stock.incrementAndGet();
Counter.builder("my.order") // 메트릭 이름
.tag("class", this.getClass().getName()) // 태그(레이블) 추가
.tag("method", "cancel") // 태그(레이블) 추가
.description("order") // 설명
.register(registry).increment(); // 등록 및 카운터 증가
}
Java
복사
위와 같이 마이크로미터의 Counter를 통해 메트릭의 이름과 태그들을 설정하여 등록할 수 있다.
{
"name": "my.order",
"description": "order",
"measurements": [
{
"statistic": "COUNT",
"value": 2
}
],
"availableTags": [
{
"tag": "method",
"values": [
"cancle",
"order"
]
},
{
"tag": "class",
"values": [
"hello.controller.v1.orderServiceV1"
]
}
]
}
JSON
복사
#HELP my_order_total order
#HELP my_order_total counter
my_order_total{class="hello.order.v1.OrderServiceV1",method="order",} 1.0
my_order_total{class="hello.order.v1.OrderServiceV1",method="cancel",} 1.0
YAML
복사
등록하고 나서 엑추에이터에서 확인해보면 위와 같이, 등록한대로 잘 출력되는 것을 확인할 수 있다.
아래와 같은 promQuery를 입력하여 그라파나에 새 패널로 출력하면 된다.
increase(my_order_total{method=”order”}[1m])
increase(my_order_total{method=”cancel”}[1m])
GraphQL
복사
위 예시의 문제는 비즈니스 로직에 메트릭을 관리하는 로직이 침투했다는 것이다. 스프링 AOP를 통해 두 로직을 분리할 수 있고, 마이크로미터에 이런 상황에 필요한 AOP 구성요소를 미리 다 만들어두었다.
private AtomicInteger stock = new AtomicInteger(100);
@Counted("my.order")
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Counted("my.order")
public void cancel() {
log.info("주문 취소");
stock.incrementAndGet();
}
Java
복사
@Counted 애노테이션을 적용하면 메트릭 이름을 지정 받고, class 태그에 해당 클래스명과 method 태그에 해당 메서드의 이름이 자동으로 분류된다.
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
Java
복사
다만 @Counted 애노테이션을 사용하려면 위와 같이 빈으로 등록 해주어야 제대로 동작하게 된다.
Timer를 통해 메트릭 등록하기
Timer는 카운터와 유사하지만, 시간도 함께 측정할 수 있다.
•
seconds_count : 누적 실행 수(카운터)
•
seconds_sum : 실행 시간의 합(sum)
•
seconds_max : 최대 실행 시간 / 가장 오래걸린 실행 시간(게이지) - 최근 1~3분마다 새로 갱신
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public void order() {
Timer timer = Timer.builder("my.order") // 메트릭 이름
.tag("class", this.getClass().getName()) // 태그(레이블) 추가
.tag("method", "order") // 태그(레이블) 추가
.description("order") // 설명
.register(registry); // 등록
timer.record(() -> {
log.info("주문");
stock.decrementAndGet();
});
}
public void cancel() {
Timer timer = Timer.builder("my.order") // 메트릭 이름
.tag("class", this.getClass().getName()) // 태그(레이블) 추가
.tag("method", "cancel") // 태그(레이블) 추가
.description("order") // 설명
.register(registry); // 등록
timer.record(() -> {
log.info("주문 취소");
stock.incrementAndGet();
});
}
Java
복사
이 역시 아래와 같은 promQuery를 입력하여 그라파나에 새 패널로 출력하여 모니터링 할 수 있다.
increase(my_order_seconds_count{method=”order”}[1m])
increase(my_order_seconds_count{method=”cancel”}[1m])
my_order_seconds_max
increase(my_order_seconds_sum[1m]) / increase(my_order_seconds_count[1m])
GraphQL
복사
Timer 역시 AOP가 미리 준비되어 있어, AOP를 사용하면 아래와 같이 비즈니스 로직과 메트릭 로직을 분리하고 간결하게 작성할 수 있다.
private AtomicInteger stock = new AtomicInteger(100);
@Timed("my.order")
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Timed("my.order")
public void cancel() {
log.info("주문 취소");
stock.incrementAndGet();
}
Java
복사
@Timed는 타입에도 타이머가 적용될 수 있다. 이 역시 마찬가지로 아래와 같이 빈으로 등록해주어야 정상 동작한다.
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
Java
복사
액추에이터 보안
엑추에이터를 통해 애플리케이션의 많은 중요한 정보들을 확인할 수 있기 때문에, 액추에이터에 아무나 접근할 수 없도록 해야한다.
아래와 같이 액추에이터에 접근할 수 있는 별도의 포트를 설정할 수 있고, 해당 포트를 내부망에서만 접속할 수 있도록 설정하자.
management:
server:
port: 8080
YAML
복사
포트 변경이 어렵다면, 서블릿 필터, 스프링 인터셉터, 스프링 시큐리티 등으로 인증과 인가된 사용자만 접근할 수 있도록 제어가 필요하다.