현재 진행하고 있는 프로젝트의 대한 최적화를 진행하기 위해 다음과 같은 기술 스택을 사용했고
어떻게 진행했는지, 어떤 과정으로 나아갔는지 작성해보도록 하겠다.
- Spring Boot 3.3.2
- Java 21
- Docker
- 각 서비스 환경 구성
- Grafana
- API 스트레스 테스트, Spring Boot , CPU 메모리등 리소스 시각화
- InfluxDB
- API 스트레스 결과용 DB
- K6
- 스트레스 테스트 툴로 설정 : 리소스를 적게 잡아 먹어 같은 JVM을 사용하는 JMeter및 NGrinder보다 가벼운 장점
- Prometheus
- 각 서비스 컴포넌트 데이터 수집
지난 글에서 말했다시피 다음과 같이 코드레벨에서 멀티모듈로 코드를 작성했고 아키텍쳐는 다음과 같고 Docker에 할당된 리소스는 다음과 같이 구성했다.
CPU: 8
Memory : 4GB
Swap : 3GB
docker compose로 각각의 환경들을 따로 구성해줬다. 처음에는 K6와 influxDB와 Grafana로 스트레스 테스트를 진행했다.
그리고 다음과 같은 처참한 결과를 받아보게 된다.
200명의 VUser[동시접속자 수]를 적게 했음에도 불구하고 위의 구성을 통해서 각각의 API의 P95또한 높게 나온것들을 볼 수 있다.
각각의 K6 지표를 간단하게 설명하자면 다음과 같다.
Metric | Type |
vus | 현재 총 활동하는 가상유저 |
vus_max | 가상 유저의 최대 가능한 수 / VU 리소스가 사전 할당되어 로드 수준을 확장할 때 성능에 영향을 미치지 않는다. |
iterations | VU가 JS 스크립트(기본 기능)을 실행한 총 회수이다. |
iteration_duration | setup과 teardown에 소요된 시간을 포함하여 전체 반복을 완료하는 데 걸린 시간이다. 특정 시나리오에 대한 반복 기능의 지속 시간을 계산하려면 이 해결 방법을 시도하라. |
dropped_iterations | VU부족 또는 시간 부족(반복 기반 실행기에서 만료된 maxDuration)으로 인해 시작되지 않은 반복횟수 |
data_received | 수신된 데이터의 양이다. 이 예에서 개별 URL에 대한 데이터를 추적하는 방법을 다룬다. |
data_sent | 전송한 데이터 량 / 개별 URL에 대한 데이터를 추적하여 개별 URL에 대한 데이터를 추적한다. |
checks | 성공적으로 체크한 비율 |
Metric | Type |
http_reqs | 얼마나 많은 요청을 K6가 생성했는지 카운트 |
http_req_blocked | 유용한 TCP 커넥션 슬롯에 대해서 대기한시간(블록시간) 을 측정한다. |
http_req_connecting | 원격 호스트와 TCP 커넥션을 만들기 까지 소요된 시간 |
http_req_tls_handshaking | 원격 호스트와 TLS 세션 핸드쉐이킹에 소요된 시간 |
http_req_sending | 원격 호스트에 데이터를 전송하는데 걸린 시간 |
http_req_waiting | 원격 호스트로 부터 응답을 기다리는 시간 (첫번째 바이트가 온 시간 혹은 TTFB) |
http_req_receiving | 원격 호스트로 부터 데이터를 수신하는데 걸린 시간 |
http_req_duration | 요청에 대한 총 소요시간 이는 http_req_sending + http_req_waiting + http_req_receiving 과 동일한 시간이다. (이 시간은 원격 서버가 요청을 받고 처리하고 응답을 한 시간이며 초기 DNS 룩업과 커넥션 시간은 제외이다) |
http_req_failed | setResponseCallback에 따른 요청 실패율 |
위 테스트 결과는 약 4000번의 API를 사용함에도 불구하고 심하게 높게 나오는 경우가 많다. 따라서 어떤점이 문제인지 확인해야 하는데 위 결과로는 어느 부분이 문제인지 확인하기 어렵다. 따라서 다음의 스트레스 테스트는 어느 부분에 문제가 있는지 확인하는 용도로 사용하고 어떤 부분을 어떻게 고쳐야 할지는 인텔리제이의 프로파일링 기능을 사용했다.
Intellij에서 자체 프로파일링 기능을 제공해준다. 시작시에 다음과 같이 Intellij Profiler 기능을 활성화 할수도 있고
혹은 최신 인텔리제이를 사용하고 있다면 다음과 같이 성능 표시가 오른쪽에 뜨는것을 볼수있고 클릭해 성능을 측정하고 후에 성능평가 결과표를 받을수 있다.
이렇게 프로파일 기능을 사용하고결 과 표시를 하게 되면 이렇게 옆에 결과가 나오게 되고 다양한 그래프를 확인할수있다.
다음과 같은 그래프와 시각화를 통해서 어느 부분에 문제가 있는지 확인이 가능하다. 각 프로파일의 대한 내용은 망나니개발자님의 블로그에 잘 설명되어있으니 확인해보는것도 좋다.
알다시피 대부분의 병목사항은 I/O에서 발생한다. 예를 들어 DB에 접근하는 로직, 외부 네트워크 호출 로직등의 네트워크 I/O혹은 DB I/O에서 발생하고 실제로 프로파일을 진행해도, DB의 접근하는 로직에서 발생하는 경우가 대부분이다.
따라서 다양하게 Query최적화나, DB 다중화를 시도해보는것이 좋다.
필자의 경우 Stress Test와 프로파일링 진행후 최대한 DB Connection을 줄일수 있는 방향으로, 예를 들어서 DB JOIN을 통해서 RTT를 줄이거나 하는 방향으로 최적화를 했고, 이후 EXPLAIN 키워드를 통해서 쿼리실행계획을 확인하고 Index를 추가하는 방식을 적용했다.
2. 살펴봐야 할 것들
위의 내용으로 살펴봐야할 것들에 대해 생각해보자. 일단 첫번째로 머신의 성능지표가 있을것이다. CPU나 Memory등을 살펴봐야한다. CPU가 왜 어디서 어떻게 사용중인지, Thread가 너무 많아 Context Switching 비용이 많이 일어나지는 않는지 등을 살펴 보아야 한다.
두번째는 JVM에 대한 것들도 살펴보아야한다. JVM의 Thread개수, Thread 상태, GC 상태, GC가 일어나는 비율등을 확인할 수 있다. 이는 JVM 튜닝을 위해서 필요한 지표들이며 이를 통해서 성능 개선을 진행 할 수 있다.
그리고 Spring 에서 연결중인 DB의 커넥션도 살펴보는것이 좋다.
현재 DB의 Active Connection, Idle Connection, Pending Thread등을 확인한다면 현재 데이터베이스에 연결이 많이 밀려있는지, 제대로 된 수행을 진행하는지 확인 할 수 있다.
각각의 지표들의 시각화를 통해서 다음과 같은 최적화를 거쳤다.
- Query최적화
- User-Service 컴포넌트 다중화
- Chat-Service 컴포넌트 다중화
- User-DB 다중화 : Primary - Replica
최적화 진행후에 Vuser는 최대 1200으로 설정하고 스트레스 테스트를 진행함에 따라서 이전 4000번의 API를 날렸던것과 달리
150000정도의 요청을 보냈음에도 이전보다 전체적으로 성능이 우수한 것을 볼 수 있다.
일단 P95의 요청속도도 느리긴 하지만 전체적으로 성능을 높일수 있었다. 이전과 같이 Vuser를 작게 한다면 확실히 성능이 높아진 것을 판단할 수 있다.
:VUser: 최대 400
VUser 최대 800
따라서 최종 아키텍쳐는 다음과 같이 정하게 되었고 여기서 더 추가하자면, docker engine에 CPU나 메모리를 더 줄수 있는 쪽으로 개선하거나, DB를 더 다중화하는쪽으로 정할수 있을 것으로 판단한다.
이번에는 큰 틀에서 어떻게 스트레스 테스트를 진행했는지, 어떻게 최적화를 수행했는지 바라보았다면, 다음글에는 이러한 Docker설정들과 각각의 지표들을 어떻게 확인했는지 좀더 자세히 설명해보도록 하겠다.
'Spring' 카테고리의 다른 글
Spring Boot 서비스 환경 스트레스 테스트 2 [Spring/Java] (0) | 2025.01.08 |
---|---|
Spring Boot 서비스 환경 스트레스 테스트 [Spring/Java] (1) | 2025.01.04 |
멀티모듈 프로젝트 Docker 빌드 전략: Path 기반 접근법[Docker&Github Action] (1) | 2024.12.14 |
가상 스레드 vs 반응형 프로그래밍 [Spring/Java] (2) | 2024.11.22 |
[Spring] 응? 이게 왜 롤백이 안되지? - 비동기와 @Transaction (4) | 2024.10.21 |