스프링부트

spring boot application startup tracking

알쓸개잡 2023. 9. 17.

spring boot 애플리케이션을 시작하는 동안 SpringApplication과 ApplicationContext는 애플리케이션 라이프사이클, 빈 라이프사이클, 애플리케이션 이벤트 처리와 관련된 많은 작업을 수행한다. Spring은 ApplicationStartup을 통해서 StartupStep 객체를 사용하여 애플리케이션 시작 과정을 추적할 수 있다. 추적 데이터는 프로파일링 목적으로 수집하거나 애플리케이션 시작 프로세스를 이해하는데 도움이 될 수 있다. spring boot 애플리케이션 시작 과정에 대한 추적 데이터를 얻는 방법에 대해서 기록한다.

 

Spring Boot 2.4부터 ApplicationStartup 메트릭을 생성할 수 있게 되었다. 해당 메트릭에는 시작 과정에서 생성되는 각각의 이벤트와 라이프사이클이 처리되는 소요 시간 정보도 포함되어 있어 어느 부분에서 지연이 발생되는지도 확인할 수 있다.

 

Startup 추적 데이터 형식

Startup 추적 데이터는 애플리케이션이 실행되는 전체 과정을 여러 스텝으로 나누어 표시한다. 각 스텝은 아이디, 이름, 상위 아이디, 시작시간, 종료시간, 소유시간 정보를 포함한다.

추적 데이터 형식은 다음과 같다.

{
    "springBootVersion": "3.1.3",
    "timeline": {
        "startTime": "2023-09-16T12:27:09.491465Z",
        "events": [
            {
                "endTime": "2023-09-16T12:27:09.507801Z",
                "duration": "PT0.01286S",
                "startTime": "2023-09-16T12:27:09.494941Z",
                "startupStep": {
                    "name": "spring.boot.application.starting",
                    "id": 0,
                    "tags": [
                        {
                            "key": "mainApplicationClass",
                            "value": "com.example.defaultport.DefaultPortApplication"
                        }
                    ]
                }
            },
            ...
            {
                "endTime": "2023-09-16T12:27:10.268548Z",
                "duration": "PT0.000164S",
                "startTime": "2023-09-16T12:27:10.268384Z",
                "startupStep": {
                    "name": "spring.boot.application.ready",
                    "id": 311,
                    "tags": []
                }
            }
        ]
    }
}

 

Startup 추적 데이터 수집 하기

Startup 추적 데이터 수집을 활성화하기 위해서는 Spring Boot 버전 2.4 이상부터 가능하다.

 

pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.3</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
...
</dependencies>

 

SpringApplication 인스턴스에 setApplicationStartup 설정을 한다.

public static void main(String[] args) {
    SpringApplication application = new SpringApplication(DefaultPortApplication.class);
    application.setApplicationStartup(new BufferingApplicationStartup(10000));
    application.run(args);
}

setApplicationStartup 메서드는 ApplicationStartup 타입의 인스턴스를 인자로 전달받는다.

BufferingApplicationStartup(int capacity) 생성자는 capacity를 전달받는데 이는 Step의 개수만큼만 버퍼에 담아서 처리한다는 것이다. (추적 데이터의 byte 길이가 아니라는 것에 유의) Step의 개수가 capacity를 초과하면 추적되지 않는다.

위 코드는 최대 10000개 까지의 Step을 추적하도록 설정하였다.

 

애플리케이션 Startup 추적 데이터는 actuator의 /startup 엔드포인트에서 모니터링할 수 있다. 이를 위해서는 actuator의 /startup 엔드포인트를 활성화해야 한다.

management:
  endpoints:
    web:
      exposure:
        include: 'startup'

준비는 끝났다. 애플리케이션을 실행 후 Postman에서 POST /actuator/startup 엔드포인트를 호출해 보았다.

/actuator/startup 엔드포인트 호출

결과는 아래와 같은 형식으로 출력된다.

{
    "springBootVersion": "3.1.3",
    "timeline": {
        "startTime": "2023-09-16T12:27:09.491465Z",
        "events": [
            {
                "endTime": "2023-09-16T12:27:09.507801Z",
                "duration": "PT0.01286S",
                "startTime": "2023-09-16T12:27:09.494941Z",
                "startupStep": {
                    "name": "spring.boot.application.starting",
                    "id": 0,
                    "tags": [
                        {
                            "key": "mainApplicationClass",
                            "value": "com.example.defaultport.DefaultPortApplication"
                        }
                    ]
                }
            },
            {
                "endTime": "2023-09-16T12:27:09.612995Z",
                "duration": "PT0.081873S",
                "startTime": "2023-09-16T12:27:09.531122Z",
                "startupStep": {
                    "name": "spring.boot.application.environment-prepared",
                    "id": 1,
                    "tags": []
                }
            },
            {
                "endTime": "2023-09-16T12:27:09.641478Z",
                "duration": "PT0.000117S",
                "startTime": "2023-09-16T12:27:09.641361Z",
                "startupStep": {
                    "name": "spring.boot.application.context-prepared",
                    "id": 2,
                    "tags": []
                }
            },
            {
                "endTime": "2023-09-16T12:27:09.665428Z",
                "duration": "PT0.000868S",
                "startTime": "2023-09-16T12:27:09.664560Z",
                "startupStep": {
                    "name": "spring.boot.application.context-loaded",
                    "id": 3,
                    "tags": []
                }
            },
            {
                "endTime": "2023-09-16T12:27:09.678599Z",
                "duration": "PT0.000524S",
                "startTime": "2023-09-16T12:27:09.678075Z",
                "startupStep": {
                    "name": "spring.beans.instantiate",
                    "id": 7,
                    "tags": [
                        {
                            "key": "beanName",
                            "value": "org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory"
                        }
                    ],
                    "parentId": 6
                }
            },
            {
                "endTime": "2023-09-16T12:27:09.681786Z",
                "duration": "PT0.008426S",
                "startTime": "2023-09-16T12:27:09.673360Z",
                "startupStep": {
                    "name": "spring.beans.instantiate",
                    "id": 6,
                    "tags": [
                        {
                            "key": "beanName",
                            "value": "org.springframework.context.annotation.internalConfigurationAnnotationProcessor"
                        },
                        {
                            "key": "beanType",
                            "value": "interface org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor"
                        }
                    ],
                    "parentId": 5
                }
            },
            ...
            {
                "endTime": "2023-09-16T12:27:09.936435Z",
                "duration": "PT0.26572S",
                "startTime": "2023-09-16T12:27:09.670715Z",
                "startupStep": {
                    "name": "spring.context.beans.post-process",
                    "id": 5,
                    "tags": [],
                    "parentId": 4
                }
            },
            ...
            {
                "endTime": "2023-09-16T12:27:10.100159Z",
                "duration": "PT0.162986S",
                "startTime": "2023-09-16T12:27:09.937173Z",
                "startupStep": {
                    "name": "spring.boot.webserver.create",
                    "id": 36,
                    "tags": [
                        {
                            "key": "factory",
                            "value": "class org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory"
                        }
                    ],
                    "parentId": 4
                }
            },
            ...
            {
                "endTime": "2023-09-16T12:27:10.244112Z",
                "duration": "PT0.003741S",
                "startTime": "2023-09-16T12:27:10.240371Z",
                "startupStep": {
                    "name": "spring.beans.smart-initialize",
                    "id": 305,
                    "tags": [
                        {
                            "key": "beanName",
                            "value": "org.springframework.context.event.internalEventListenerProcessor"
                        }
                    ],
                    "parentId": 4
                }
            },
            {
                "endTime": "2023-09-16T12:27:10.244221Z",
                "duration": "PT0.000087S",
                "startTime": "2023-09-16T12:27:10.244134Z",
                "startupStep": {
                    "name": "spring.beans.smart-initialize",
                    "id": 306,
                    "tags": [
                        {
                            "key": "beanName",
                            "value": "healthEndpointGroupMembershipValidator"
                        }
                    ],
                    "parentId": 4
                }
            },
            ...
            {
                "endTime": "2023-09-16T12:27:10.265704Z",
                "duration": "PT0.600247S",
                "startTime": "2023-09-16T12:27:09.665457Z",
                "startupStep": {
                    "name": "spring.context.refresh",
                    "id": 4,
                    "tags": []
                }
            },
            {
                "endTime": "2023-09-16T12:27:10.268021Z",
                "duration": "PT0.0014S",
                "startTime": "2023-09-16T12:27:10.266621Z",
                "startupStep": {
                    "name": "spring.boot.application.started",
                    "id": 310,
                    "tags": []
                }
            },
            {
                "endTime": "2023-09-16T12:27:10.268548Z",
                "duration": "PT0.000164S",
                "startTime": "2023-09-16T12:27:10.268384Z",
                "startupStep": {
                    "name": "spring.boot.application.ready",
                    "id": 311,
                    "tags": []
                }
            }
        ]
    }
}

애플리케이션이 기동하면서 수행되는 애플리케이션 라이프사이클, 빈 라이프 사이클, 이벤트에 대해서 어떤 클래스에서 수행되고 소요시간을 얼마나 걸리는지 확인해 볼 수 있다. /actuator/startup 엔드포인트를 호출하면 버퍼에 기록된 Startup 추적데이터를 응답하고 버퍼를 비우기 때문에 그다음 /actuator/startup을 호출 시에는 빈 데이터를 응답한다.

빈 데이터 응답

Java Flight Recorder(JFR) 를 이용한 Startup 추적 데이터 수집

JFR(Java Flight Recorder)은 JVM의 일부 모니터링 도구이다. Java는 다양한 메트릭 및 진단 데이터를 수집하고 이를 분석할 수 있는 도구를 제공한다. JFR을 통해서 Startup 추적 데이터를 확인하려면 FlightRecordingApplicationStartup을 사용해야 한다.

public static void main(String[] args) {
    SpringApplication application = new SpringApplication(DefaultPortApplication.class);
    application.setApplicationStartup(new FlightRecorderApplicationStartup());
    application.run(args);
}

JFR을 사용하여 Startup 추적 데이터를 캡처하기 위해서 애플리케이션 실행 시 몇 가지 실행 옵션을 전달해야 한다.

java -XX:+FlightRecorder -XX:StartFlightRecording=duration=5s,filename=startup.jfr \
-jar target/default-port-0.0.1-SNAPSHOT.jar

위 명령을 실행하면 위 명령을 실행한 경로에 startup.jfr 파일이 생성된다. intellij를 통해서 jfr 파일을 확인할 수도 있고 JMC(Java Mission Control) 도구를 설치하여 jfr 파일을 확인할 수 있다.

JMC 도구는 링크로 이동하여 다운로드 받을 수 있다.

 

Intellij 에서 jfr 파일 분석
JMC를 통한 jfr 파일 분석

 

Spring Boot 애플리케이션 실행 흐름에 대한 추적 데이터를 생성하는 방법에 대해서 알아보았다. 

Spring Boot 애플리케이션의 시작 루틴에 대해서 프로파일링을 통해서 Spring Boot의 시작흐름을 파악하고 시작 과정에서 지연되는 지점을 파악하는데 도움이 될 것 같다.

댓글

💲 추천 글