Spring 액츄에이터

Kevin·2024년 11월 16일
1

Spring

목록 보기
25/27
post-thumbnail

서론

회사에서 기존 Node.js로 운영 되고 있는 백엔드 API 서버를 Spring Boot로 마이그레이션 하자는 이야기가 나왔다.

그러면서 자연스럽게 어떠한 스택을 사용할지에 대해서 자유롭게 대화를 나누는 계기가 되었다.

나는 최근 주변으로부터 많은 이야기를 들었었던 프로메테우스와 그라파나에 대해서 추천을 하였었다.

이 때 다른 이들에게 새로운 기술들을 추천을 하는 나였지만 이 때 해당 기술이 우리 서비스에 정말 필요로 한 것인지, 오버 엔지니어링은 아닌지에 대해서 명확히 판단을 하지 못했다.

단순히 남들이 많이 쓴다더라, 요즘 이런 기술이 유행이다더라 등에 불과한 주장이었던 것이다.

그리하여 나는 우리 서비스에 해당 기술이 정말 필요한지 알기 위해서 직접 사용해보기로 하였다.

모든 기술에는 사용 근거가 있어야 한다.


프로덕션 준비 기능이란?

해당 기술을 알기 위해서는 먼저 프로덕션 준비 기능에 대해서 알아야 한다.

프로덕션 준비 기능은 운영 환경에서 서비스할 때 서비스에 문제가 없는지 모니터링하고 지표들을 심어서 관리하는 활동을 의미한다.

어플케이션이 살아있는가?

로그 정보는 정상 설정 되었는가?

커넥션 풀은 얼마나 사용되고 있는가?

등과 같은 정보를 통해 현재 서비스가 정상적으로 운영 되고 있는지를 모니터링 해야 한다.

액츄에이터란?

액츄에이터는 Spring에서 제공하는 프로덕션 준비 기능을 제공하는 라이브러리이다.

액츄에이터는 아래와 같이 gradle에 선언하면 된다.

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

위 세팅을 통해서 서버 시작시 http:localhost:8080/actuator 을 통해 동작을 확인 할 수 있다.

액츄에이터가 제공하는 모든 기능을 웹에서 노출 시키기 위해서는 아래 YML 설정이 필요하다.

management:
  endpoints:
    web:
      exposure:
        include: "*"

위 설정 후 http:localhost:8080/actuator 에 접근시 아래와 같은 JSON이 화면에 출력 된다.

{
  "_links": {
    "self": {
      "href": "http://localhost:8080/actuator",
      "templated": false
    },
    "beans": {
      "href": "http://localhost:8080/actuator/beans",
      "templated": false
    },
    "caches-cache": {
      "href": "http://localhost:8080/actuator/caches/{cache}",
      "templated": true
    },
    "caches": {
      "href": "http://localhost:8080/actuator/caches",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8080/actuator/health/{*path}",
      "templated": true
    },
    "health": {
      "href": "http://localhost:8080/actuator/health",
      "templated": false
    },
    "info": {
      "href": "http://localhost:8080/actuator/info",
      "templated": false
    },
    "conditions": {
      "href": "http://localhost:8080/actuator/conditions",
      "templated": false
    },
    "shutdown": {
      "href": "http://localhost:8080/actuator/shutdown",
      "templated": false
    },
    "configprops-prefix": {
      "href": "http://localhost:8080/actuator/configprops/{prefix}",
      "templated": true
    },
    "configprops": {
      "href": "http://localhost:8080/actuator/configprops",
      "templated": false
    },
    "env": {
      "href": "http://localhost:8080/actuator/env",
      "templated": false
    },
    "env-toMatch": {
      "href": "http://localhost:8080/actuator/env/{toMatch}",
      "templated": true
    },
    "loggers": {
      "href": "http://localhost:8080/actuator/loggers",
      "templated": false
    },
    "loggers-name": {
      "href": "http://localhost:8080/actuator/loggers/{name}",
      "templated": true
    },
    "heapdump": {
      "href": "http://localhost:8080/actuator/heapdump",
      "templated": false
    },
    "threaddump": {
      "href": "http://localhost:8080/actuator/threaddump",
      "templated": false
    },
    "metrics": {
      "href": "http://localhost:8080/actuator/metrics",
      "templated": false
    },
    "metrics-requiredMetricName": {
      "href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
      "templated": true
    },
    "sbom": {
      "href": "http://localhost:8080/actuator/sbom",
      "templated": false
    },
    "sbom-id": {
      "href": "http://localhost:8080/actuator/sbom/{id}",
      "templated": true
    },
    "scheduledtasks": {
      "href": "http://localhost:8080/actuator/scheduledtasks",
      "templated": false
    },
    "mappings": {
      "href": "http://localhost:8080/actuator/mappings",
      "templated": false
    }
  }
}

이 때 엑츄에이터가 제공하는 기능 하나 하나를 엔드포인트라 칭한다.


엔드포인트란?

이 때 엑츄에이터가 제공하는 기능 하나 하나를 엔드포인트라 칭한다.

우리는 YML 설정을 통해 액츄에이터가 제공하는 모든 엔드포인트를 웹에 출력 하도록 설정 하였다.

만약 일부만 웹에 출력 하고 있다면 아래와 같이 작성하면 된다.

management:
  endpoints:
    web:
      exposure:
        include: "health,info"
  • 이 때는 health와 info 엔드포인트만 화면에 출력 되게 된다.

이 때 일부 엔드포인트에 대해서 간략하게 이야기 하고 넘어가자.


metrics 엔드포인트

엔드포인트 중 metrics 이라는 엔드포인트가 있는데, 이 엔드포인트는 애플리케이션의 메트릭 정보를 보여준다.

애플리케이션의 메트릭에는 System CPU 사용률등의 유용한 지표가 있으며, 이 정보를 통해 그라파나의 대시보드를 출력 하게 된다.

스프링 부트 액츄에이터는 마이크로미터가 제공하는 지표 수집을 @AutoConfiguration 을 통해 자동으로 등록해준다.

마이크로미터는 후술 하겠다.

metrics 엔드포인트로 접근시 아래와 같은 JSON을 확인 할 수 있다.

{
  "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.active",
    "jdbc.connections.active",
    "jdbc.connections.idle",
    "jdbc.connections.max",
    "jdbc.connections.min",
    "jvm.buffer.count",
    "jvm.buffer.memory.used",
    "jvm.buffer.total.capacity",
    "jvm.classes.loaded",
    "jvm.classes.unloaded",
    "jvm.compilation.time",
    "jvm.gc.live.data.size",
    "jvm.gc.max.data.size",
    "jvm.gc.memory.allocated",
    "jvm.gc.memory.promoted",
    "jvm.gc.overhead",
    "jvm.info",
    "jvm.memory.committed",
    "jvm.memory.max",
    "jvm.memory.usage.after.gc",
    "jvm.memory.used",
    "jvm.threads.daemon",
    "jvm.threads.live",
    "jvm.threads.peak",
    "jvm.threads.started",
    "jvm.threads.states",
    "logback.events",
    "process.cpu.time",
    "process.cpu.usage",
    "process.files.max",
    "process.files.open",
    "process.start.time",
    "process.uptime",
    "spring.security.authorizations",
    "spring.security.authorizations.active",
    "spring.security.filterchains",
    "spring.security.filterchains.JwtAuthenticationFilter.before",
    "spring.security.filterchains.access.exceptions.before",
    "spring.security.filterchains.active",
    "spring.security.filterchains.authentication.anonymous.before",
    "spring.security.filterchains.authentication.form.before",
    "spring.security.filterchains.authorization.before",
    "spring.security.filterchains.context.async.before",
    "spring.security.filterchains.context.holder.before",
    "spring.security.filterchains.context.servlet.before",
    "spring.security.filterchains.cors.before",
    "spring.security.filterchains.header.before",
    "spring.security.filterchains.logout.before",
    "spring.security.filterchains.requestcache.before",
    "spring.security.filterchains.session.management.before",
    "spring.security.filterchains.session.urlencoding.before",
    "spring.security.http.secured.requests.active",
    "system.cpu.count",
    "system.cpu.usage",
    "system.load.average.1m",
    "tomcat.sessions.active.current",
    "tomcat.sessions.active.max",
    "tomcat.sessions.alive.max",
    "tomcat.sessions.created",
    "tomcat.sessions.expired",
    "tomcat.sessions.rejected"
  ]
}

각 메트릭들은 아래 요청 방식을 통해서 더 자세히 확인할 수 있다.

도메인/actuator/metrics/{메트릭 이름}

http://localhost:8080/actuator/metrics/application.ready.time

{
  "name": "application.ready.time",
  "description": "Time taken for the application to be ready to service requests",
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 4.043
    }
  ],
  "availableTags": [
    {
      "tag": "main.application.class",
      "values": [
        "com.HelloWorld.Daily.DailyApplication"
      ]
    }
  ]
}

메트릭은 availableTags 를 통해서 더 자세한 정보를 제공한다.

availableTags 는 TAG와 VALUES로 이루어져 있다.

이 때 쿼리 파라미터로 TAG를 기반으로 해당 TAG 정보를 필터링해서 확인할 수 있다.

이 때 요청 방식은 아래와 같다.

도메인/actuator/metrics/{메트릭 이름}?tag={태그 KEY}:{values의 value}

http://localhost:8080/actuator/metrics/application.ready.time?tag=main.application.class:com.HelloWorld.Daily.DailyApplication

{
  "name": "application.ready.time",
  "description": "Time taken for the application to be ready to service requests",
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 4.043
    }
  ],
  "availableTags": []
}

위에서 보듯 엑츄에이터가 기본적으로 제공하는 메트릭은 매우 많다.

JVM 메트릭, 시스템 메트릭, 애플리케이션 시작 메트릭, 스프링 MVC 메트릭, 톰캣 메트릭, 데이터 소스 메트릭, 로그 메트릭 등등이 있다.

이렇게 엑츄에이터가 기본적으로 제공 해주는 메트릭 뿐만 아니라 사용자가 직접 메트릭을 정의할 수도 있다. 예를 들어서 핵심 비즈니스의 회원가입 수, 주문수, 취소수 등을 메트릭으로 만들 수 있다.

엑츄에이터는 현재 시점의 메트릭만 보여준다. 과거 시점의 메트릭 또한 저장을 하기 위해서 별도 메트릭 DB가 필요하다. 또한 메트릭을 그래프 형식으로 보여주는 것이 필요하다.

이게 프로메테우스그라파나이다.


Health 엔드포인트

Health 정보를 통해 애플리케이션에 문제가 발생했을 때 문제를 빠르게 인식할 수 있다.

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

헬스 메트릭을 더 자세히 보기 위해서는 아래와 같이 YML 설정을 해준다.

management:
  endpoint:
    shutdown:
      enabled: true
    health:
      show-details: always

해당 설정 이후 health 엔드포인트에서는 아래와 같은 JSON을 출력한다.

{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "H2",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 494384795648,
        "free": 100696059904,
        "threshold": 10485760,
        "path": "/Users/iseung-geon/Downloads/Daily/.",
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    }
  }
}

이 때 db, diskspace, ping 등의 health 상태를 출력 해준다.

UP이면 정상이라는 뜻이며, 위 정보 중 하나라도 다운이면, 해당 애플리케이션 health는 DOWN으로 출력된다.


Info 엔드포인트

info 엔드포인트는 애플리케이션의 기본 정보를 노출한다.

이 때 애플리케이션의 기본 정보라면 자바 런타임 정보, OS 정보, 환경 설정 정보, 빌드 정보, GIT 정보 등이 있다.

이는 각 java, os, env, build, git 설정을 나타낸다.

이 중 env, java, os는 기본적으로 비활성화 되어있다.

env, java, os를 활성화 시키기 위해서는 아래와 같이 설정 하면 된다.

management:
  info:
    java:
      enabled: true
    os:
      enabled: true
    env:
      enabled: true

이 때 순서상으로 반드시 management 다음으로 info가 먼저 들어가야 한다.

Build 정보를 노출하기 위해서는 빌드 시점에 ‘META-INF/build-info.properties’ 파일을 만들어야 한다.

gradle을 사용하면 다음 내용을 추가하면 된다.

springBoot {
    buildInfo()
}

→ 이러면 자동으로 빌드 시점에 META-INF/build-info.properties 파일을 만들어준다.

{
  "build": {
    "artifact": "Daily",
    "name": "Daily",
    "time": "2024-11-09T07:05:55.649Z",
    "version": "0.0.1-SNAPSHOT",
    "group": "com.HelloWorld"
  },
}
  • 애플리케이션 기본 정보, 버젼, 빌드 시간을 확인할 수 있다.

빌드 시점에 git.properties 파일을 자동으로 만들기 위해서는 plugin을 추가 해주어야 한다.

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.0'
	id 'io.spring.dependency-management' version '1.1.5'
	id "com.gorylenko.gradle-git-properties" version "2.4.1" //git info
}

참고로 해당 프로젝트가 git으로 관리 되지 않으면 오류가 발생한다.

{
  "git": {
    "branch": "homework",
    "commit": {
      "id": "672aaa6",
      "time": "2024-07-19T01:40:14Z"
    }
}

httpexchanges 엔드포인트

httpexchanges 엔드포인트를 사용하면 HTTP 요청과 응답의 과거 기록을 확인 할 수 있다.

httpexchangesRepository 인터페이스의 구현체를 빈으로 등록하면 해당 엔드포인트를 사용할 수 있다.

Springboot는 기본적으로 InMemoryHttpExchangeRepository 구현체를 제공한다.

아래와 같이 InMemoryHttpExchangeRepository 객체를 Bean으로 등록 해주면 된다.

@Configuration
public class ActuatorConfig {

    @Bean
    public InMemoryHttpExchangeRepository httpExchangeRepository() {
        return new InMemoryHttpExchangeRepository();
    }

}

그 후 엔드포인트에 접근시 아래와 같은 JSON을 출력한다.

{
  "exchanges": [
    {
      "timestamp": "2024-11-09T07:39:39.183815Z",
      "request": {
        "uri": "http://localhost:8080/actuator",
        "method": "GET",
        "headers": {
          "host": [
            "localhost:8080"
          ],
          "connection": [
            "keep-alive"
          ],
          "sec-ch-ua": [
            "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\""
          ],
          "sec-ch-ua-mobile": [
            "?0"
          ],
          "sec-ch-ua-platform": [
            "\"macOS\""
          ],
          "upgrade-insecure-requests": [
            "1"
          ],
          "user-agent": [
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
          ],
          "accept": [
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
          ],
          "sec-fetch-site": [
            "none"
          ],
          "sec-fetch-mode": [
            "navigate"
          ],
          "sec-fetch-user": [
            "?1"
          ],
          "sec-fetch-dest": [
            "document"
          ],
          "accept-encoding": [
            "gzip, deflate, br, zstd"
          ],
          "accept-language": [
            "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"
          ]
        }
      },
      "response": {
        "status": 200,
        "headers": {
          "Vary": [
            "Origin",
            "Access-Control-Request-Method",
            "Access-Control-Request-Headers"
          ],
          "X-Content-Type-Options": [
            "nosniff"
          ],
          "X-XSS-Protection": [
            "0"
          ],
          "Cache-Control": [
            "no-cache, no-store, max-age=0, must-revalidate"
          ],
          "Pragma": [
            "no-cache"
          ],
          "Expires": [
            "0"
          ],
          "X-Frame-Options": [
            "DENY"
          ],
          "Content-Type": [
            "application/vnd.spring-boot.actuator.v3+json"
          ],
          "Transfer-Encoding": [
            "chunked"
          ],
          "Date": [
            "Sat, 09 Nov 2024 07:39:39 GMT"
          ],
          "Keep-Alive": [
            "timeout=60"
          ],
          "Connection": [
            "keep-alive"
          ]
        }
      },
      "timeTaken": "PT0.040068S"
    }
  ]
}

httpexchanges 엔드포인트는 최대 100개의 HTTP 요청을 제공하며, 최대값을 변경할 수 있다.

이 기능은 매우 단순하고 기능에 제한이 많기 때문에 개발 단계에서만 사용하고, 실제 운영 서비스에서는 모니터 링 툴이나 핀포인트, Zipkin 같은 다른 기술을 사용하는 것이 좋다.

더 다양한 엔드포인트들은 공식 문서를 참고하자.

https://docs.spring.io/spring-boot/reference/actuator/endpoints.html#actuator.endpoints


엑츄에이터의 보안

액츄에이터가 제공하는 기능들은 우리 애플리케이션의 내부 정보를 너무 많이 노출한다.

그래서 외부 인터넷 망이 공개된 곳에 액츄에이터의 엔드포인트를 공개하는 것은 보안상 좋은 방안이 아니다.

액츄에이터의 엔드포인트들은 외부 인터넷에서 접근이 불가능하게 막고, 내부에서만 접근 가능한 내부망을 사용하는 것이 안전하다.

액츄에이터를 다른 포트에서 실행

예를 들어서 외부 인터넷 망을 통해서 8080 포트에만 접근할 수 있고, 다른 포트는 내부망에서만 접근할 수 있다면 액츄에이터에 다른 포트를 설정하면 된다.

액츄에이터의 기능을 애플리케이션 서버와는 다른 포트에서 실행하려면 다음과 같이 설정하면 된다. 이 경우 기존 8080 포트에서는 액츄에이터를 접근할 수 없다.

management:
  server:
    port: 8090 # 특정 포트로 접근하게 변경

포트를 분리하는 것이 어렵고 어쩔 수 없이 외부 인터넷 망을 통해서 접근해야 한다면 /actuator 경로에 서블릿 필터, 또는 스프링 시큐티리를 통해서 인증된 사용자만 접근 가능하도록 추가 개발이 필요하다.


마이크로미터

위에서는 Spring이 제공하는 엑츄에이터과 엑츄에이터의 대표적인 엔드포인트들에 대해서 다루어보았다.

서비스를 운영할 때는 애플리케이션의 CPU, 메모리, 커넥션 사용, 고객 요청수 같은 엔드포인트들의 지표들을 확인하는 것이 필요하다.

그래야 어디에 어떤 문제가 발생했는지 사전에 대응도 할 수 있고, 실제 문제가 발생해도 원인을 빠르게 파악 해서 대처할 수 있다. 예를 들어서 메모리 사용량이 가득 찼다면 메모리 문제와 관련있는 곳을 빠르게 찾아서 대응할 수 있을 것이다.

세상에는 수 많은 모니터링 툴이 있고, 시스템의 다양한 정보를 이 모니터링 툴에 전달해서 사용하게 된다.

모니터링 툴이 동작하려면, 엔드포인트를 통해 얻은 지표들을 모니터링 툴에게 보내주어야 한다.

그런데 만약 모니터링 툴이 여러개이고, 해당 모니터링의 API 양식이 다 다르면 어떻게 될까?

그렇다면 애플리케이션이 해당 모니터링의 API 양식에 의존적일 수 밖에 없을 것이다.

그렇기에 이러한 문제를 아래와 같이 해결한다.

Spring을 사용 해보았다면 어디서 많이 본 방식 아닌가?

그렇다 JDBC가 사용하는 방식인 추상화 방식을 사용하였다.

각 DB Driver들이 JDBC Driver Manager로 추상화 되는 것처럼 각 모니터링 구현체들은 Micrometer를 구현하게 된다.

우리는 추상화 된 구현체를 가져다 사용하면 된다.

보통은 스프링이 이런 추상화를 직접 만들어서 제공하지만, 마이크로미터라는 이미 잘 만들어진 추상화가 있기 때문에 스프링은 이것을 활용한다.

스프링 부트 액츄에이터는 마이크로미터를 기본으로 내장해서 사용한다.

개발자는 마이크로미터가 정한 표준 방법으로 메트릭(측정 지표)를 전달하면 된다.

그리고 사용하는 모니터링 툴에 맞는 구현체를 선택하면 된다.

이후에 모니터링 툴이 변경되어도 해당 구현체만 변경하면 된다. 이를 통해서 애플리케이션 코드는 모니터링 툴이 변경되어도 그대로 유지할 수 있다.

애플리케이션에서 발생한 메트릭을 그 순간만 확인하는 것이 아니라 과거 이력까지 함께 확인하려면 메트릭을 보관하는 DB가 필요하다.

이렇게 하려면 어디선가 메트릭을 지속해서 수집하고 DB에 저장해야 한다.

프로메테우스가 바로 이런 역할을 담당한다.

프로메테우스가 DB라고 하면, 이 DB에 있는 데이터를 불러서 사용자가 보기 편하게 보여주는 대시보드가 필요하다.

그라파나는 매우 유연하고, 데이터를 그래프로 보여주는 툴이다. 수 많은 그래프를 제공하고, 프로메테우스를 포함한 다양한 데이터소스를 지원한다.

위를 정리하면 아래와 같다.

액츄에티어와 마이크로미터로 생성된 메트릭들을 프로메테우스가 수집한다.

프로메테우스는 이 메트릭들을 내부 DB에 저장하고, 사용자는 그라파나 대시보드 툴을 사용해 그래프로 조회한다.

profile
Hello, World! \n

0개의 댓글