개발 이야기/인프라

Spring, MySQL 모니터링 & 성능 테스트 가이드 with Rest API

제이온 (Jayon) 2024. 5. 19.

안녕하세요? 제이온입니다.

 

오늘은 Rest API 서버에 k6를 이용하여 부하 테스트를 진행하면서 Spring과 MySQL의 각종 지표를 모니터링하는 과정을 소개하려고 합니다.

 

시각화

다양한 부하 상황에 따라 우리의 애플리케이션이 어떠한 상태를 보이고 있는지 모니터링할 수 있어야 하며, 시각화를 통해 더 효과적으로 모니터링이 가능합니다.

 

Prometheus와 Grafana를 이용하여 스프링 애플리케이션의 메트릭을 시각화하는 방법을 작성해 보겠습니다.

 

 

 

Spring Boot Actuator

Spring Boot Actuator를 활성화하면, 애플리케이션을 모니터링하고 관리할 수 있는 엔드 포인트에 접속이 가능해 집니다.

 

# build.gradle
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")

 

그리고 Actuator를 통해 모니터링되는 엔드 포인트는 Prometheus이 사용할 엔드 포인트만 열도록 하겠습니다. 이외 다양한 엔드 포인트가 있지만 보안상 필요한 것만 여는 것이 좋습니다.

 

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: prometheus

 

Prometheus

Prometheus는 메트릭을 수집하고 저장하며, 이를 통해 모니터링하거나 경고할 수 있게 도와줍니다.

 

메트릭은 쉽게 말해서 숫자 데이터로, 요청 시간, 데이터 베이스 활성 연결 수, CPU 사용량 등이 있습니다. 그리고 Prometheus는 이 메트릭을 HTTP를 통한 pull model 방식으로 주기적으로 수집하여 시계열 데이터로 저장합니다. 따라서, 숫자로 구성된 시계열 데이터를 수집하고 모니터링 하는 데 적합한 툴이라고 할 수 있습니다.

 

먼저 Prometheus 관련 설정 파일을 작성합니다. 주의할 점은 Prometheus를 도커 컨테이너로 띄우고 있고, 스프링 애플리케이션은 호스트 혹은 별도 도커 컨테이너이므로 localhost가 아니라 “host.docker.internal”로 해야 합니다.

 

host.docker.internal의 정확한 역할

  • 컨테이너 내부에서 Host에 접근하는 명령어입니다.
  • 만일 “host.docker.internal:8080”을 입력한다면,
  • 프로메테우스 컨테이너에서 Host에 있는 8080 포트로 접근할 수 있습니다.
  • 이때 로컬에서 스프링 도커 컨테이너를 띄웠고, 이 컨테이너에 대해 8080:8080 포트 포워딩을 하였다면, 프로메테우스 컨테이너에서 “host.docker.internal:8080”를 통해 스프링 컨테이너에 접근할 수 있다는 뜻입니다.

 

# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

  - job_name: "java_application"
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ["host.docker.internal:8080"]

 

그리고 docker-compose.yml을 작성합니다.

 

version: '3.7'
services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    restart: always

 

참고로 docker-compose로 실행하는 컨테이너들은 모두 같은 도커 네트워크에 속하게 됩니다.

이후 docker-compose up --build -d 를 수행하여 localhost:9090에 접속해 보면 Prometheus 대시보드가 표시됩니다.

 

 

Grafana

Grafana는 오픈 소스 데이터 시각화 및 메트릭 분석 도구입니다. Prometheus도 기본적인 시각화를 제공하지만, Grafana는 시각화에 특화된 툴이라서 Prometheus의 시계열 데이터를 더욱 효과적으로 보여 줍니다. 위에서 작성한 docker-compose.yml에 grafana 관련 설정도 추가하여 파일을 완성합니다.

 

위에서 작성한 docker-compose.yml에 grafana 관련 설정도 추가하여 파일을 완성합니다.

 

version: '3.7'
services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    restart: always
  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - "3000:3000"
    restart: always
    depends_on:
      - prometheus
    privileged: true

 

이후 docker-compose up --build -d 를 수행하여 localhost:3000에 접속하면 Grafana 로그인 창이 표시됩니다.

 

 

초기 ID/Password는 모두 admin, admin입니다. 로그인이 완료되면 아래 Grafana 대시 보드에서 데이터 소스를 생성하는 메뉴로 들어갑니다.

 

 

이후 Prometheus를 선택하여 다음과 같이 연결 정보를 적어 줍니다.

 

 

Grafana와 Prometheus 모두 도커 컨테이너 위에서 실행되고 있으므로 localhost가 아닌 컨테이너 이름인 prometheus로 적어주어야 합니다. 다음으로 시각화 할 대시 보드를 추가해야 합니다. 우리가 직접 커스터마이징할 수 있긴 하지만 귀찮으므로(?) 잘 만들어진 대시 보드를 훔쳐 옵니다. 아래 링크 중 원하는 대시 보드를 복사합니다.

 

 

그리고 Dashboards / Import dashboard 접속하여 Import 합니다.

 

 

Import가 완료되면 다음과 같이 시각화 화면을 볼 수 있습니다.

 

 

부하 테스트란?

부하 테스트란 임계값 한계에 도달할 때까지 시스템의 부하를 지속적으로 꾸준히 증가시켜 시스템의 성능을 테스트하는 것입니다. 부하 테스트는 유저의 수와 초당 API 요청 등을 증가시키며 시스템의 내구성을 테스트하고 결과를 모니터링하며 애플리케이션의 한계를 찾아내는 것을 목표로 합니다.

 

이때, 가상의 부하를 일으키기 위해 k6, ngrinder, pinpoint 등의 부하 테스트 툴을 사용합니다.

 

K6

  • 오픈소스 성능테스트 솔루션입니다.
  • 사용하기가 쉽고, Grafana 등과 연동하여 UI를 구성할 수 있습니다.
  • CLI 툴을 사용하여 성능 테스트를 수행합니다.
  • 로컬 혹은 원격지의 스크립트를 로드하여 테스트 할 수 있습니다.
  • Check/Thresholds 를 제공하여 성능 목표를 다양하게 구성할 수 있습니다.
  • 자바 스크립트 ES6 문법을 지원합니다.

 

K6 세팅하기

k6는 brew를 통해 설치할 수도 있고, docker로 이미지 내려받아 실행할 수도 있는데 후자로 진행하겠습니다.

먼저, 다음과 같이 docker-compose.yml을 작성합니다.

 

version: '3.7'

services:
  k6:
    image: grafana/k6:latest
    ports:
      - "6565:6565"
    volumes:
      - ./script.js:/script.js
    command: run /script.js
    network_mode: host

 

이때 network_mode는 host로 설정하지 않을 경우, k6 컨테이너와 스프링 컨테이너는 서로 독립된 도커 네트워크에서 동작하므로 localhost:8080으로 접근할 수 없게 됩니다. 따라서 k6 컨테이너는 host의 네트워크를 함께 하도록 하여 스프링 컨테이너에 접근하도록 구성하였습니다.

 

물론 script.js에서 “host.docker.internal”으로 설정해도 됩니다.

이후 실제로 부하 테스트를 진행할 스크립트를 작성합니다.

 

// script.js
import http from 'k6/http';
import { check } from 'k6';
import { randomIntBetween, randomString } from "https://jslib.k6.io/k6-utils/1.1.0/index.js";

export let options = {
    vus: 300, // 가상 사용자 수
    duration: '10m', // 테스트 지속 시간
};

export default function () {
    // 랜덤 데이터 생성
    const id = randomIntBetween(1, 1000000);
    const name = randomString(10);
    const age = randomIntBetween(1, 99);
    const address = `Random Street ${randomIntBetween(1, 1000)}`;

    // JSON 바디 구성
    const body = JSON.stringify({
        id: id,
        name: name,
        age: age,
        address: address
    });

    // HTTP POST 요청 설정
    const params = {
        headers: {
            'Content-Type': 'application/json',
        },
    };

    // POST 요청 보내기
    let response = http.post('http://localhost:8080/api/people', body, params);

    // 응답 확인
    check(response, {
        'is status 200': (r) => r.status === 200,
    });
}

 

위 코드를 실행하면 가상의 사용자 수 300명이 정의된 function을 10분 동안 수행하게 됩니다.

이후 docker-compose up --build -d 를 수행하면 자동으로 위 script.js 스크립트를 실행합니다.

 

 

부하 테스트할 때 주의할 점

한 컴퓨터에서 스프링 컨테이너도 띄워 놓고, 부하 테스트 컨테이너도 띄워 놓고, 그 밖에 다양한 컨테이너를 띄워 놓는 환경일 경우, 매번 부하 테스트 결과가 다르게 나올 수 있습니다. 다양한 컨테이너에 주어지는 CPU, 메모리가 매번 달라질 것이기 때문입니다. (그때그때 CPU 더 많이 필요한 곳에 몰리고, 다른 프로세스가 느려짐.)

 

따라서 우리는 스프링 컨테이너와 DB 컨테이너에 대해 cpu, memory 자원을 제한하겠습니다.

 

version: '3.7'
services:
  spring:
    container_name: spring
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1024M
        reservations:
          cpus: '1.0'
          memory: 1024M
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    depends_on:
      - mysql
  mysql:
    image: mysql:8.0
    container_name: mysql
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1024M
        reservations:
          cpus: '1.0'
          memory: 1024M
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: 'example'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: 'password'
      MYSQL_ROOT_PASSWORD: 'root'
      TZ: Asia/Seoul
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./db/mysql/data:/var/lib/mysql
      - ./db/mysql/init:/docker-entrypoint-initdb.d

 

터미널에서 docker stats spring을 수행하면 다음과 같이 자원이 제한된 것을 볼 수 있습니다.

 

 

또한, 그라파나에서도 마찬가지로 CPU 사용량을 확인할 수 있습니다. 이때 Spring을 실행 중인 프로세스인 Process CPU Usage를 봐야 합니다.

 

 

다음과 같이 초당 요청 수와 요청의 응답 시간을 확인할 수도 있습니다. 그리고 이 응답 시간을 줄이는 것이 부하 테스트의 최대 목표입니다!

 

 

(추가) MySQL 서버 모니터링

Spring 서버 외에 MySQL 서버도 모니터링하고 싶다면 mysqld-exporter를 사용하면 됩니다.

Spring, MySQL로 이루어진 docker-compose.yml을 다음과 같이 수정합니다.

 

version: '3.7'
services:
  spring:
    container_name: spring
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1024M
        reservations:
          cpus: '1.0'
          memory: 1024M
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    depends_on:
      - mysql
  mysql:
    image: mysql:8.0
    container_name: mysql
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1024M
        reservations:
          cpus: '1.0'
          memory: 1024M
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: 'example'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: 'password'
      MYSQL_ROOT_PASSWORD: 'root'
      TZ: Asia/Seoul
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./db/mysql/data:/var/lib/mysql
      - ./db/mysql/init:/docker-entrypoint-initdb.d
  mysql-exporter:
    container_name: mysql-exporter
    image: prom/mysqld-exporter
    command:
      - "--mysqld.username=user:password"
      - "--mysqld.address=mysql:3306"
    ports:
      - "9104:9104"
    depends_on:
      - mysql

 

그리고 prometheus.yml도 다음과 같이 수정합니다.

 

global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

  - job_name: "mysqld-exporter"
    static_configs:
      - targets: ["host.docker.internal:9104"]

  - job_name: "java_application"
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ["host.docker.internal:8080"]

 

이후 docker-compose up --build -d 수행한 다음 localhost:9090 들어가보면 다음과 같이 잘 mysql 지표를 얻어온 것을 확인할 수 있습니다.

 

 

그라파나에서 MySQL 전용 대시보드도 제공하고 있습니다.

 

 

위에서 했던 것과 동일한 방식으로 대시보드를 추가하면 다음과 같이 MySQL 각종 지표를 모니터링할 수 있습니다!

 

 

특히 슬로우 쿼리를 개선할 때 도움이 많이 됩니다. 다만, mysqld-exporter의 경우 서버 cpu 사용량 정보는 주지 않고 전반적인 mysql의 각종 지표를 주는 것이라 cpu 사용량을 체크하고 싶다면 docker stats 명령어를 사용하시길 바랍니다.

댓글

추천 글