어렸을 때부터 지속되어 온 생활습관들이 세월이 흘러 중년이 되면 몸에서 질병으로 나타난다. 프로젝트의 요구사항을 기반으로 개발자가 탄생시킨 프로그램도 마찬가지로 에러 메시지로 상황을 알려준다. 


지속해서 눈감고 모른 체하게 되면 프로그램이 중단되는 사태가 발생하는데 질병을 무시했다가 큰 병을 얻는 경우와 같은 상황이다. 

사람은 정기적으로 건강검진을 받는 것처럼, 프로그램도 정기적으로 관리를 해야 한다. 


프로그래밍 언어마다 감시하는 방법이 존재하지만, 예시는 Java 프레임워크 Spring Boot 기준으로 하겠다. 


Spring Boot는 Actuator를 사용하면 프로그램의 상황을 감시할 수 있는데, Http Endpoint, JMX, 원격 SSH 3가지 방법으로 확인할 수 있다.


Actuator를 활성화하려면 모듈을 추가해야 한다. 


dependencies {
    compile 'org.springframework.boot:spring-boot-starter-actuator'
    compile 'org.springframework.boot:spring-boot-starter-security'
    compile 'org.springframework.boot:spring-boot-starter-remote-shell'
}


모듈이 정상적으로 컴파일되면 Http Endpoint, 원격 SSH가 활성화된다. port는 설정하지 않으면 기본값 2000이다.

Http Endpoint의 중복을 방지하기 위해 추가 설정이 필요하다. application.properties에 추가한다.


management:
    context-path: /manage
    security:
        enabled: false
    shell:
        ssh:
            enabled: true
            port: 18080
    health:
        db:
            enabled: true
        diskspace:
            enabled: true
 
endpoints:
    beans:
        sensitive: false


Http Endpoint 목록은 /manage/mappings로 요청하면 확인할 수 있다.


{
  "{[/manage/metrics/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)"
  },
  "{[/manage/metrics || /manage/metrics.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/auditevents || /manage/auditevents.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public org.springframework.http.ResponseEntity<?> org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint.findByPrincipalAndAfterAndType(java.lang.String,java.util.Date,java.lang.String)"
  },
  "{[/manage/configprops || /manage/configprops.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/mappings || /manage/mappings.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/application || /application.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public org.springframework.hateoas.ResourceSupport org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint.links()"
  },
  "{[/manage/trace || /manage/trace.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/heapdump || /manage/heapdump.json],methods=[GET],produces=[application/octet-stream]}": {
    "bean": "endpointHandlerMapping",
    "method": "public void org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint.invoke(boolean,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) throws java.io.IOException,javax.servlet.ServletException"
  },
  "{[/manage/autoconfig || /manage/autoconfig.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/beans || /manage/beans.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/env/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)"
  },
  "{[/manage/env || /manage/env.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/health || /manage/health.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(javax.servlet.http.HttpServletRequest,java.security.Principal)"
  },
  "{[/manage/dump || /manage/dump.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/info || /manage/info.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/loggers/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.get(java.lang.String)"
  },
  "{[/manage/loggers/{name:.*}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v1+json || application/json],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.set(java.lang.String,java.util.Map<java.lang.String, java.lang.String>)"
  },
  "{[/manage/loggers || /manage/loggers.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  }
}


대표적으로 많이 쓰이는 것은 2가지이다. 

상태 값 확인은 /manage/health를 사용하면 되고, 추가 설정을 하게 되면 redis, mysql 저장소도 확인할 수 있다.


{
  "status": "UP",
  "diskSpace": {
    "status": "UP",
    "total": 214734073856,
    "free": 195815051264,
    "threshold": 10485760
  }
}


Cpu, Memory, Threads, GC, Datasource를 감시하기 위해서는 /manage/metrics를 사용한다.


{
  "mem": 12496579,
  "mem.free": 8820382,
  "processors": 8,
  "instance.uptime": 81630586,
  "uptime": 81641671,
  "systemload.average": 0.05,
  "heap.committed": 12340992,
  "heap.init": 12582912,
  "heap.used": 3520609,
  "heap": 12340992,
  "nonheap.committed": 159528,
  "nonheap.init": 2496,
  "nonheap.used": 155587,
  "nonheap": 0,
  "threads.peak": 342,
  "threads.daemon": 234,
  "threads.totalStarted": 3086,
  "threads": 287,
  "classes": 14031,
  "classes.loaded": 14031,
  "classes.unloaded": 0,
  "gc.parnew.count": 522,
  "gc.parnew.time": 9718,
  "gc.concurrentmarksweep.count": 2,
  "gc.concurrentmarksweep.time": 605,
  "httpsessions.max": -1,
  "httpsessions.active": 0,
  "datasource.primary.active": 1,
  "datasource.primary.usage": 0.033333335,
  "datasource.slave.active": 0,
  "datasource.slave.usage": 0.0,
}


원격 SSH는 Spring Boot 2.0에서는 제거되었다. 하지만 기능이 매우 괜찮아서 아쉽기도 하다. 

별도로 유저, 비밀번호 설정을 하지 않으면, 최초 시작 시 로그에서 무작위로 생성되는 비밀번호를 확인해야 한다.


Using default security password: 6a37007e-01c9-465c-b9d8-95a7c88afd38


정상적으로 접속되면 help를 입력하여 확인하자.


$ ssh -18080 user@localhost
Password authentication
Password:
 
 :: Spring Boot ::  (v1.5.8.RELEASE) on localhost
> help
Try one of these commands with the -h or --help switch:
 
NAME       DESCRIPTION
autoconfig Display auto configuration report from ApplicationContext
beans      Display beans in ApplicationContext
cron       manages the cron plugin
dashboard  a monitoring dashboard
egrep      search file(s) for lines that match a pattern
endpoint   Invoke actuator endpoints
env        display the term env
filter     a filter for a stream of map
java       various java language commands
jul        java.util.logging commands
jvm        JVM informations
less       opposite of more
mail       interact with emails
man        format and display the on-line manual pages
metrics    Display metrics provided by Spring Boot
shell      shell related command
sleep      sleep for some time
sort       sort a map
system     vm system properties commands
thread     JVM thread commands
help       provides basic help
repl       list the repl or change the current repl
 
>


dashboard를 입력하면 Thread, Memory 현황을 실시간으로 확인할 수 있다.



Java에서 제공하는 jmap, jstat등을 이용한다면 기본적인 감시는 가능하겠지만, Spring Boot Actuator는 기능이 확장되어 좀 더 쉽게 상황을 진단할 수 있다. 


프로젝트 일정에 너무 치이다 보면 실수를 인지하지 못한 상태로 운영단계에 들어가게 되는데, 반드시 검수 일정을 확보하여 Spring Boot Actuator 기능을 기반으로 실수를 사전에 감시하여 제거했으면 한다.


참고 사이트

  • https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready


+ Recent posts