Spring Batch + Prometheus + Pushgateway + Grafana + Docker로 애플리케이션 모니터링
개요
프로메테우스(Promethues)는 기본적으로 매트릭 지표를 제공하는 서버에게 주기적으로 요청(pull)하여 매트릭을 수집하도록 되어 있다.
그런데, Spring Batch 같이 CLI 형태로 주기적으로 실행만 되게 해놓은 경우가 있다. 이런 경우에는 IP가 따로 없어서 기적으로 Polling 방식이 아닌 프로메테우스에 매트릭을 역으로 Push 할 수 있도록 해야 한다. 매트릭을 푸시할 수 있도록 지원하는게 바로 Pushgateway이다.
Pushgateway
프로메테우스에서 제공하는 Pushgateway는 매트릭을 Push 할 수 있도록 지원하며 Push 된 매트릭을 프로메테우스에서 Pulling 하여 가져갈 수 있도록 중개자 역할을 한다. 이런 구조로 Pushgateway에 Push된 매트릭을 프로메테우스에서 가져갈 수 있다.
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
- http://localhost:3000
Pushgateway 수집 정보 확인
Pushgateway(http://localhost:9091)에 접속해 보면 spring-batch에서 수집된 정보를 확인할 수 있다.
Prometheus 수집 정보 확인
Prometheus(http://localhost:9090)에 접속해 보면 Pushgateway을 통해서 전달 받은 spring-batch에서 수집된 정보를 확인할 수 있다.
Grafana 설정
그럼, spring-batch에서 수집된 정보를 Grafana을 통해서 좀 더 시각적으로 표시해 보겠다.
Grafana 데이터 소스 설정
먼저 데이터 소스를 추가한다.
데이터 소스로 “Promethues"을 선택한다.
Promethues의 Data Source 소스를 추가 화면이 나오면, “Name”, “URL"를 입력한다.
(여기서는 구현 환경이 macOS이어서 URL에는 “http://host.docker.internal:9090"을 입력하였으나, Linux 환경 등에서는 다를 수 있으니 주의바란다.)
Grafana 대시보드 설정 및 확인
이어서 대시보드를 설정을 “Import"으로 추가한다.
준비된 “Import” Json 파일(spring-batch-dashboard.json)을 선택한다.
내용을 확인하고, “Import"을 클릭한다.
이제 대시 보드 화면으로 이동하면 sprinb-batch 메트릭 정보가 그래프로 보이는 것을 확인할 수 있다.
참고
위에 예제 코드는 GitHub에서 확인해 볼 수 있다.