Spring Batch + Prometheus + Pushgateway + Grafana + Docker로 애플리케이션 모니터링

Spring batch에 Pushgateway, Prometheus, Grafana 연동

Spring Batch + Prometheus + Pushgateway + Grafana + Docker

개요

프로메테우스(Promethues)는 기본적으로 매트릭 지표를 제공하는 서버에게 주기적으로 요청(pull)하여 매트릭을 수집하도록 되어 있다.

그런데, Spring Batch 같이 CLI 형태로 주기적으로 실행만 되게 해놓은 경우가 있다. 이런 경우에는 IP가 따로 없어서 기적으로 Polling 방식이 아닌 프로메테우스에 매트릭을 역으로 Push 할 수 있도록 해야 한다. 매트릭을 푸시할 수 있도록 지원하는게 바로 Pushgateway이다.

Pushgateway

프로메테우스에서 제공하는 Pushgateway는 매트릭을 Push 할 수 있도록 지원하며 Push 된 매트릭을 프로메테우스에서 Pulling 하여 가져갈 수 있도록 중개자 역할을 한다. 이런 구조로 Pushgateway에 Push된 매트릭을 프로메테우스에서 가져갈 수 있다.

Pushgateway

Spring Batch 프로젝트 생성

신규 프로젝트 생성 명령

아래와 같이 curl 명령어를 사용하여 Spring Boot 신규 프로젝트를 생성한다.

curl https://start.spring.io/starter.tgz \
-d bootVersion=2.7.6 \
-d dependencies=batch,h2 \
-d baseDir=spring-batch-prometheus \
-d groupId=com.devkuma \
-d artifactId=spring-batch-prometheus \
-d packageName=com.devkuma.batch.prometheus \
-d applicationName=BatchPrometheusApplication \
-d javaVersion=11 \
-d packaging=jar \
-d type=gradle-project | tar -xzvf -

위 명령어를 실행하게 되면 Spring Batch, H2 Database를 추가하였다.

Spring Batch 설정

/src/main/java/com/devkuma/batch/prometheus/BatchPrometheusApplication.java

package com.devkuma.batch.prometheus;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
@EnableBatchProcessing
public class BatchPrometheusApplication {

	public static void main(String[] args) {
		SpringApplication.run(BatchPrometheusApplication.class, args);
	}
}

@EnableBatchProcessing 어노테이션을 추가하여 배치 기능을 활성화 하고, @EnableScheduling 어노테이션도 추가하여 스케줄러 기능도 활성화 하였다.

prometheus 관련 라이브러리 추가

/build.gradle

// ... 생략 ...

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-batch'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.batch:spring-batch-test'

    // 프로메테우스 관련 라이브러리 추가
	implementation 'io.micrometer:micrometer-registry-prometheus'
	implementation 'io.prometheus:simpleclient_pushgateway'
}

// ... 생략 ...

파일 내용 중에 의존성 라이브러리를 확인해 보면 Spring 배치 관련 라이브러리와 Prometheus 및 Pushgateway 라이브러리가 추가되어 있는 것을 볼 수 있다.

Tasklet 처리 방식인 Job 구성 구현

간단한 Tasklet 처리 방식인 Joba 구성 파일을 추가한다.

/src/main/java/com/devkuma/batch/prometheus/batch/TaskletStepJobConfiguration.java

package com.devkuma.batch.prometheus.batch;

import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TaskletStepJobConfiguration {

    private static final Logger LOGGER = LoggerFactory.getLogger(TaskletStepJobConfiguration.class);

    private Random random;

    public TaskletStepJobConfiguration() {
        this.random = new Random();
    }

    @Bean
    public Job taskletStepJob(JobBuilderFactory jobBuilderFactory, Step taskletStep1, Step taskletStep2) {
        return jobBuilderFactory.get("taskletStepJob")
                                .start(taskletStep1)
                                .next(taskletStep2)
                                .build();
    }

    @Bean
    public Step taskletStep1(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory.get("taskletStep1")
                                 .tasklet((contribution, chunkContext) -> {
                                     LOGGER.info("taskletStep1");
                                     // simulate processing time
                                     Thread.sleep(random.nextInt(3000));
                                     return RepeatStatus.FINISHED;
                                 })
                                 .build();
    }

    @Bean
    public Step taskletStep2(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory.get("taskletStep2")
                                 .tasklet((contribution, chunkContext) -> {
                                     LOGGER.info("taskletStep2");
                                     // simulate step failure
                                     int nextInt = random.nextInt(3000);
                                     Thread.sleep(nextInt);
                                     if (nextInt % 5 == 0) {
                                         throw new Exception("Boom!");
                                     }
                                     return RepeatStatus.FINISHED;
                                 })
                                 .build();
    }

}

itemReader, itemWriter 처리 방식인 Job 구성 구현

간단한 itemReader, itemWriter 처리 방식을 Job 구성 파일을 생성한다.

/src/main/java/com/devkuma/batch/prometheus/batch/ItemStepJobConfiguration.java

package com.devkuma.batch.prometheus.batch;

import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ItemStepJobConfiguration {

    private static final Logger LOGGER = LoggerFactory.getLogger(ItemStepJobConfiguration.class);

    private Random random;

    public ItemStepJobConfiguration() {
        this.random = new Random();
    }

    @Bean
    public Job itemStepJob(JobBuilderFactory jobBuilderFactory, Step itemStep) {
        return jobBuilderFactory.get("itemStepJob")
                                .start(itemStep)
                                .build();
    }

    @Bean
    public Step itemStep(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory.get("itemStep").<Integer, Integer>chunk(3)
                                 .reader(itemReader())
                                 .writer(itemWriter())
                                 .build();
    }

    @Bean
    @StepScope
    public ListItemReader<Integer> itemReader() {
        List<Integer> items = new LinkedList<>();
        // read a random number of items in each run
        for (int i = 0; i < random.nextInt(100); i++) {
            items.add(i);
        }
        return new ListItemReader<>(items);
    }

    @Bean
    public ItemWriter<Integer> itemWriter() {
        return items -> {
            for (Integer item : items) {
                int nextInt = random.nextInt(1000);
                Thread.sleep(nextInt);
                // simulate write failure
                if (nextInt % 57 == 0) {
                    throw new Exception("Boom!");
                }
                LOGGER.info("item = " + item);
            }
        };
    }
}

Job 스케줄러 생성

여기서는 구지 스케줄러는 따로 필요는 없지만, 그래프를 좀 더 잘 보기 위해 주기적으로 데이터를 보내기 위해서 Job 스케줄러를 만든다.

/src/main/java/com/devkuma/batch/prometheus/batch/JobScheduler.java

package com.devkuma.batch.prometheus.batch;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class JobScheduler {

	private final Job taskletStepJob;

	private final Job itemStepJob;

	private final JobLauncher jobLauncher;

	@Autowired
	public JobScheduler(Job taskletStepJob, Job itemStepJob, JobLauncher jobLauncher) {
		this.taskletStepJob = taskletStepJob;
		this.itemStepJob = itemStepJob;
		this.jobLauncher = jobLauncher;
	}

	@Scheduled(cron = "*/10 * * * * *")
	public void launchJob1() throws Exception {
		JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis())
																.toJobParameters();

		jobLauncher.run(taskletStepJob, jobParameters);
	}

	@Scheduled(cron = "*/15 * * * * *")
	public void launchJob2() throws Exception {
		JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis())
																.toJobParameters();

		jobLauncher.run(itemStepJob, jobParameters);
	}

}

이어서, thread.pool.size도 설정 파일에 넣는다.
/src/main/resources/application.properties

thread.pool.size=3

앞에서 넣은 thread.pool.size를 넣어 ThreadPoolTaskScheduler을 설정 파일을 생성한다.
/src/main/java/com/devkuma/batch/prometheus/batch/SchedulerConfiguration.java

package com.devkuma.batch.prometheus.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class SchedulerConfiguration {

    @Bean(destroyMethod = "shutdown")
    public ThreadPoolTaskScheduler taskScheduler(@Value("${thread.pool.size}") int threadPoolSize) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(threadPoolSize);
        return threadPoolTaskScheduler;
    }
}

프로메테우스 설정

이제는 프로메테우스 설정을 한다.
/src/main/resources/application.properties

prometheus.push.rate=5000
prometheus.job.name=springbatch
prometheus.grouping.key=appname
prometheus.pushgateway.url=localhost:9091

프로메테우스를 push 주기를 넣고, Job명과 그룹키를 넣는다. 그리고, Pushgateway URL를 설정한다.

설정한 프로메테우스 설정값을 설정 객체에 반영한다.
/src/main/java/com/devkuma/batch/prometheus/config/PrometheusConfiguration.java

package com.devkuma.batch.prometheus.config;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;

import io.micrometer.core.instrument.Metrics;
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.PushGateway;

@Configuration
public class PrometheusConfiguration {

    private static final Logger LOGGER = LoggerFactory.getLogger(PrometheusConfiguration.class);

    @Value("${prometheus.job.name}")
    private String prometheusJobName;

    @Value("${prometheus.grouping.key}")
    private String prometheusGroupingKey;

    @Value("${prometheus.pushgateway.url}")
    private String prometheusPushGatewayUrl;

    private Map<String, String> groupingKey = new HashMap<>();

    private PushGateway pushGateway;

    private CollectorRegistry collectorRegistry;

    @PostConstruct
    public void init() {
        pushGateway = new PushGateway(prometheusPushGatewayUrl);
        groupingKey.put(prometheusGroupingKey, prometheusJobName);
        PrometheusMeterRegistry prometheusMeterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
        collectorRegistry = prometheusMeterRegistry.getPrometheusRegistry();
        Metrics.globalRegistry.add(prometheusMeterRegistry);
    }

    @Scheduled(fixedRateString = "${prometheus.push.rate}")
    public void pushMetrics() {
        try {
            pushGateway.pushAdd(collectorRegistry, prometheusJobName, groupingKey);
            LOGGER.info("Push Metrics");
        }
        catch (Throwable ex) {
            LOGGER.error("Unable to push metrics to Prometheus Push Gateway", ex);
        }
    }

}

Prometheus + Pushgateway + Grafana 서버를 Docker로 기동

이제는 도커로 Prometheus + Pushgateway + Grafana 서버를 띄우기 위해 docker-compose.xml를 만들고, Prometheus 설정을 해보도록 하겠다.

docker-compose.xml 설정 파일

/src/prometheus/docker-compose.yml

version: '3.7'
services:

  prometheus:
    image: prom/prometheus
    container_name: 'prometheus'
    ports:
      - '9090:9090'
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml

  pushgateway:
    image: prom/pushgateway
    container_name: 'pushgateway'
    ports:
      - '9091:9091'

  grafana:
    image: grafana/grafana
    container_name: 'grafana'
    ports:
      - '3000:3000'

prometheus.yml 설정 파일

/src/docker/prometheus/prometheus.yml

global:
  scrape_interval:     5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: 'springbatch'
    honor_labels: true
    static_configs:
      - targets: ['host.docker.internal:9091'] # pushgateway

테스트 환경이 macOS여서 pushgateway의 URL을 host.docker.internal:9010으로 기입했는데, linux 등을 다를 수 있다. 테스트 환경에 맞게 URL를 기입하도록 한다.

docker 기동

% cd src/prometheus
% docker-compose up -d

접속 실행

  • Pushgateway

    • http://localhost:9091
  • Prometheus

    • http://localhost:9090
  • Grafana

    • http://localhost:3000
      • 기본 계정 ID/PW: admin/admin

Pushgateway 수집 정보 확인

Pushgateway(http://localhost:9091)에 접속해 보면 spring-batch에서 수집된 정보를 확인할 수 있다.
Pushgateway 수집 정보

Prometheus 수집 정보 확인

Prometheus(http://localhost:9090)에 접속해 보면 Pushgateway을 통해서 전달 받은 spring-batch에서 수집된 정보를 확인할 수 있다.
Prometheus 수집 정보

Grafana 설정

그럼, spring-batch에서 수집된 정보를 Grafana을 통해서 좀 더 시각적으로 표시해 보겠다.

Grafana 데이터 소스 설정

먼저 데이터 소스를 추가한다.
Grafana 설정

데이터 소스로 “Promethues"을 선택한다.
Grafana 설정

Promethues의 Data Source 소스를 추가 화면이 나오면, “Name”, “URL"를 입력한다.
(여기서는 구현 환경이 macOS이어서 URL에는 “http://host.docker.internal:9090"을 입력하였으나, Linux 환경 등에서는 다를 수 있으니 주의바란다.)
Grafana 설정

Grafana 설정

Grafana 대시보드 설정 및 확인

이어서 대시보드를 설정을 “Import"으로 추가한다.
Grafana 대시보드 설정

준비된 “Import” Json 파일(spring-batch-dashboard.json)을 선택한다.
Grafana 대시보드 설정

내용을 확인하고, “Import"을 클릭한다.
Grafana 대시보드 설정

이제 대시 보드 화면으로 이동하면 sprinb-batch 메트릭 정보가 그래프로 보이는 것을 확인할 수 있다.
Grafana 대시보드 확인

참고


위에 예제 코드는 GitHub에서 확인해 볼 수 있다.




최종 수정 : 2024-01-18