- java 21
- springboot 3.5.7
- H2 database
- prometheus
- grafana
- k6
docker-compose.yml이 있는 곳에서 터미널을 연다.
도커가 없다면 도커를 설치한다.
먼저, 이 프로젝트의 jar파일을 만든다.
./gradlew bootjar그 후, 도커에 prometheus,grafana,spring-boot 서버를 올린다.
docker compose up -d --buildhttp://localhost:3000에 접속하여 grafana를 확인한다.
- 초기 아이디 비밀번호는 admin/admin이며, 수정가능하다.
- 초기값 1GB로 시작.
- k6로 부하테스트 - 200명이 순차적으로 같은 api를 사용하고, 1분간 연속된 요청 보냄
- JVMGC를 2GB로 교체
- 같은 방식으로 부하 테스트 진행
=> 결과적으로 JVM을 수정하였음에도 오히려 멈춤시간이나 사용량이 늘어날뿐 상황이 개선되지 않음
- 추가적으로 다음 옵션을 추가함.
- - XX:InitiatingHeapOccupancyPercent=30 (GC가 힙이 30퍼만 차도 시작되도록 함)
- - Xlog:gc*:file=gc_log (gc log파일을 저장함)
- - XX:+UseShenandoahGC (Shenandoah GC 활성화 옵션)
이유 : CPU를 좀 더 많이 쓰더라도 대용량 트래픽을 지연없이 처리하기 위해서
- XX:InitiatingHeapOccupancyPercent=30으로 GC가 더 빨리 실행되도록하여 멈춤 시간을 줄임
- XX:+UseShenandoahGC로 throughput(CPU 사용량)을 감수하고 지연 시간을 챙기는 GC옵션이라고 조사됨
=> 위 두가지 튜닝 옵션으로 초저지연 환경을 만들어 더 빠르게 안정화된 상태로 돌아오도록 하고싶었음
-
힙 크기만 변경하였을 때는 큰 효과를 보지 못하였음.
-
다른 옵션들을 모두 사용하니 같은 힙 크기 기준 아주 좋은 성능개선을 보임.
-
최대 힙 크기 1g 기준, http_req_duration에서,
-
최대 지연 시간이 6.29s -> 2.79s
-
p(95) (사용자중 5명) 은 1.22s -> 226.31ms
-
vus(최대 가능 동시접속자 수)는 1->12
-
그래프에서도 성능 개선 확인 가능
-
JVM GC 멈춤 시간은 다소 증가함.
-
JVM 사용량도 최대치는 비슷하나, 상대적으로 빠르게 안정화되는 모습을 보임(15초차이)
-
특히, 초기값 0.5g - 최대값 2g일때 가장 좋은 성능을 보임.
- 현재 코드는 AI가 짜준 문제점이 있는 코드임
- 이를 AI를 사용하지 않고 최적화하는 것이 목표
- 기존 코드는 userRepository.findById(i)에서 n+1개의 쿼리를 날림
=> 해결을 위해서 한번의 쿼리로 모든 user를 가져오는 방식을 채택함.
- 문제 : count값이 long인데 List는 int범위의 데이터까지만 가져옴
- 기존 코드는 log를 계속 String 객체로 만들어서 리스트로 저장함
=> 이를 해결하기 위해 List가 아닌 StringBuilder를 사용해서 로그를 합치기로 함
- 코드를 최적화했다고 생각하고 실행을 해보니 오히려 성능이 저하됨.
- JVM 사용량, GC 멈춤 시간 둘다 엄청나게 늘어남.
- 사용량은 1.의 최적조합에서 최대 40,000,000이였던 반면, 2.에서는 150,000,000을 넘는 사용량을 보임.
- 멈춤시간 또한 1.의 최적조합에서 최대 0.015초 미만에 지속시간 약 1분 15초였던 반면, 멈춤시간 약 0.0360초에 지속시간 3분으로 성능 저하가 발생함.
=> n+1문제를 한 번에 모두 가져오는 방식으로 처리하면 오히려 객체가 증가하여 성능저하가 발생. => 또한, GC 최적화 문제도 결국 로그가 쌓여서 메모리를 차지하게되므로, 멈춤을 조장하는 사태가 발생.
두가지 경우 모두 일정크기로 값을 나눠서 받아 처리하는 분할 처리 시스템으로 재구성해야할 것 같다고 생각함.
- 기존에는 count만큼 모든 객체를 가져와서 메모리상 무리가 오는 경우가 존재.
- 따라서, 페이지네이션기법을 통해 user를 나누어 받도록 개선.
- 로그 또한 StringBuilder가 아닌 Logger를 사용하여 분할 처리하도록 개선
- 통계상으로는 JVM 튜닝 최적 옵션일 때와 거의 유사함.
- JVM 사용량과 멈춤 시간 부분에서 유의미한 성능개선이 포착됨.
- 사용량이 기존에는 일자 형태로 지속되다가 테스트시에 요동쳤음.
- 현재는 일정한 주기로 톱니 패턴이 반복되는 형태를 보임.
- 톱니 패턴 진행중 테스트 시, 테스트가 끝난 후엔 바로 원래 주기로 돌아가는 모습을 보임.
- 요청을 주지 않았음에도 톱니패턴이 생겨 문제가 생겼다고 생각하여 조사함.
- 알아보니, Young/Old Gen 영역 관련 내용을 내가 모르고 있어서 착각했던 것.
- 기존의 코드는 Old Gen 영역을 주로 사용하여 GC가 테스트 시에만 가동함.
- 그래서 튜닝 옵션으로 아주 큰 성능 개선을 볼 수 있었던 것.
- 최종 코드는 Young Gen 영역에 객체를 생성하고 주기를 다하면 바로 GC로 삭제함.
- 따라서, 일정한 주기마다 심장박동과 같이 객체를 생성,삭제하며 서버를 유지하는 모습이였던 것.
=> 그러면 여기서 튜닝 옵션을 빼고 돌리면 어떻게 될까?
- 옵션을 모두 제거한 경우
- k6통계가 가장 좋았음.
- 다만, 그래프 상으로 멈춤시간이 0.01초 늘어남.
- 또한, 사용량도 튀는 현상이 발생함.
- Shenandoah GC만 사용한 경우
- 애매한 결과가 나옴.
- 그래프상으로도, k6 통계도 지금까지 통계의 평균치인 것 같음.
- 강점이 크게 부각되지 않는 느낌이 들었음.
- '대용량' 트래픽을 제어한다면 디폴트 옵션이 생각보다 좋음.
- '동시 접속의 대용량' 트래픽을 제어한다면 JVM GC를 튜닝해보는 것이 좋음.
- 코드를 작성할 때, 메모리를 생각하며, 효율적으로 작동하도록 코드를 구성하기.
- 하드웨어가 좋아졌다지만, 최적화는 항상 옳다.
- 아주 짧은 시간동안 진행된 프로젝트여서 아직 모르는 옵션이나 정보가 많을 것 같음. 더 깊게 공부를 해봐야 할 듯하다.