글 작성자: Key Ryung

개요

Index를 적용하여 쿼리 최적화를 진행한 뒤에 N + 1 문제도 해결했습니다. 하지만 N + 1 문제를 개선한 뒤에 분리하여 테스트를 기록하지는 않았습니다. 왜냐하면 가장 오래 걸렸던 테스트 TOP 3안에 뽑히는 Database Connection PoolSize를 조정해보며 실험을 하게 되면 자연스럽게 기본 Pool 사이즈에서 N + 1 문제의 개선 결과도 볼 수 있을 것이기 때문입니다.

 

이 부분에서 가장 고생했던 부분은 Pool Size를 최적화 실험에서 더 효율적으로 테스트 하는 방법이었습니다. 예를 들면 '어느 정도의 테스트 시간을 설정하면 Connection Pool Size에 대한 변화를 잘 볼 수 있을까?', 'Connection Pool Size의 간격을 어느 정도로 진행해야 최적의 크기를 찾을 수 있을까?' 그리고 '효율적인 테스트를 위해서 어떻게 환경을 구축할 수 있을까?'였습니다.

 

 왜 Database Connection Pool Size 실험이 오래 걸렸을지 짐작 가시나요?

 

맞습니다. Database Connection Pool Size를 최적화하기 위한 실험의 최적화가 필요했습니다. 이 과정은 다른 포스팅을 통해 `최적화 실험의 최적화`에 대한 부분을 올리겠습니다.

 

PoolSize와 성능의 연관관계

 테스트의 목적은 #2에서 찾은 2개의 실마리를 활용하여 성능을 높일 수 있는 방안을 찾는 것입니다. 찾은 실마리는 다음과 같았습니다.

  • CPU Usage가 100%에 가까우면 문제가 발생할까?
  • 왜 Connection을 갖고 오는 부분에서 병목이 생기는 것일까?


이번 테스트에서는 특히 Connection을 갖고 오는 부분에서 병목이 왜 생기는지에 대한 더 많은 단서들을 찾기 위해 많은 노력을 기울였습니다. Pinpoint의 Stack Trace에서 HikariCP에서 getConnection()이라는 메서드에서 지연시간이 발생하므로 Pool Size가 성능에 영향을 미쳤을 것이라고 예상했습니다.

Pinpoint StackTrace

 

최적화된 Pool Size를 구하는 공식

 HikariCP 공식 Github Wiki에서는 Connection Pool Size에 대해서 친절하고 자세하게 설명되어 있으며 다시 한번 Computer Science까지 되짚어 주니 꼭 참고 하시면 좋을 것 같습니다.

 

https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing

 

GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

光 HikariCP・A solid, high-performance, JDBC connection pool at last. - GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

github.com


현재 진행하고 있는 Test와 연관지어 요약하면 많은 Connection을 Pool에 갖고 있다고 하더라도 더 잦은 Context Switching으로 비효율이 발생되고 따라서 Performance의 저하가 발생되기 때문에 많은 Connection 수를 설정하는 것보다 적절한 Connection 수를 설정하는 것을 권장합니다.

 

세팅되어 있는 Infra 구조는 다음과 같았습니다. 특히 주목해야 되는 부분은 Database가 HDD가 아니 SSD를 사용했다는 것입니다. HikariCP 글에서는 PostgreSQL에서 제공하는 공식을 보여주는 데 HDD 기준입니다. 하지만 결국 SSD도 Connection 수를 더 늘리는 것이 아닌 적정한 Connection 수를 설정하기를 권고합니다.

Pool Size가 충분히 크지 않아 지연이 발생된다고 가정했으나 공식문서를 통해 적정 수치보다 높아 문제가 발생할 수도 있다는 가정도 추가했습니다.

 

Pool Size 변경 Test - Resource

하나하나의 실험보다 전체적인 비교 그래프를 통해 결과를 보여드리겠습니다. 하지만 결과를 보여드리기에 앞서 다른 여러 지표들도 보여드리겠습니다. 먼저 Docker를 사용하여 Application Server를 Run 하고 있는 환경이고 JVM 관련된 설정은 Default 설정입니다.

 

  • JVM Default MaxHeapSize(전체 메모리의 1/4) = 2GB
  • JVM Default GC - G1 GC

모든 실험에서 MaxHeapSize에 못 미치게 사용하며 Minor GC와 Major GC가 상황에 맞춰 동작하는 것을 볼 수 있었습니다.

CPU의 경우 80% 이상의 CPU 점유율을 유지하는 것으로 보아 CPU 사용도 잘 하는 것으로 보이나 100% 가까이 사용하는 시점이 있다는 부분에서 문제를 일으킬 소지가 예상되었습니다.

DBCP Pool Size 그래프를 보면 Connection의 반납과 획득이 이루어지며 Test 시작 시 모든 Connection을 사용하고 Test 종료 시 모든 Connection이 반납되는 것을 볼 수 있었습니다.

Pool Size 변경 Test - TPS

드디어 공개하는 Pool Size에 따른 비교 그래프입니다. 초기 시작은 Pool Size를 2개씩 증가시키며 실험을 할 계획이었습니다만 너무 많은 시간이 소모되어 최적화 과정에서 5개의 Pool Size를 선정했습니다.

그래프를 보시면 공식문서에 확인했던 내용과 마찬가지로 Pool Size가 적정수치가 아니면 TPS 성능이 저하되는 것을 확인할 수 있었습니다. 가장 흥미로웠던 점은 Connection 수가 특정 수치를 넘어가면서 TPS 성능이 저하된다는 것입니다. 성능 수치와 더불어 Server의 자원 사용량도 비교해보았습니다.

 

CPU, Memory 사용량 비교 그래프

 

Database 모니터링 툴을 적용하지 않은 상태여서 Docker Stat을 통해 Docker의 Resource 사용량을 보여주는 명령어를 통해 추가적인 엑셀 작업을 통해 산출했습니다.

Resource 사용량을 비교해본 결과도 흥미로웠습니다. 메모리 사용량은 전체적인 실험에서 Server, Database 모두 비슷한 값을 보였지만 최적의 Pool Size라고 예상되는 10개의 Connection을 갖는 설정에서 Server CPU Usage는 가장 낮고, Database CPU Usage는 가장 높았습니다.

Assumption

이번 실험을 통해서 2개의 확정적인 가정을 얻을 수 있었습니다.

 

  • Application Server CPU Resource 부족
  • Database Server Resource 부족

 


해당 가정이 참일 경우를 바탕으로 논리를 각각 전개 해보았습니다.


1. Application Server CPU Resource 부족

가정

Application Server의 CPU는 2vCPU로 Spring과 HikariCP의 ConnectionPool을 유지하기 위한 Spec으로 부족합니다.

PoolSize = 4 일 때 Thread Count

PoolSize와 관계없이 Pinpoint의 Total Thread의 Max가 230으로 동일

PoolSize = 4 일 때 System CPU Usage

테스트 시 System CPU Usage가 거의 100% 정도로 유지

nGrinder에서 VUser를 500으로 세팅했으므로 Spring에서는 기본적으로 한 순간에 Request를 처리하는 데 500개의 Thread가 기본적으로 필요합니다.


💡 Tomcat은 ThreadPool을 통해 200개의 Thread Limit를 갖도록 기본 설정이 되어 있습니다.

 

결론

따라서 2vCPU만으로 230개의 Thread를 처리하는 과정에서 병목이 발생하여 문제가 발생한다고 생각할 수 있습니다.

 

2. Database Server Resource 부족

가정

Connection을 Database에서 충분히 가질 수 없기 때문에 getConnection()에서의 지연시간이 생깁니다.

 

MySQL Connection의 고갈 문제를 생각하여 MySQL의 Max Used Connections를 조회해보면 23개로 Default Maximum Connection인 151개를 다 사용하지 못하고 있습니다.

 

Prometheus를 통해 MySQL Monitoring

 

쿼리 실행시간이 느려서 앞서 말한 문제들이 발생할 가능성도 있다고 가정하더라고 테스트에 실행되는 모든 쿼리를 로깅해서 실행시간을 계산해보면 최대 실행 시간이 10ms 정도로 측정됩니다.

 

결론

따라서 Database Server의 Resource 부족으로 인해 병목이 생기는 것이 아닙니다.

 


 

Conclusion

현재까지 실험한 결과를 바탕으로 나온 가정들을 보았을 때는 현재 Application Server의 Spec 부족으로 문제가 발생한다는 것을 알 수 있었습니다. 하지만 아직도 의문점은 남아 있었습니다.

'Application Server의 Spec이 부족해서 문제가 발생한 것이 아니라면?'

그래서 추가적인 성능 개선 포인트와 가정과 결론에 대한 재검증을 계획했습니다.

추가적인 성능 개선 포인트는 Connection의 획득/반환 시기를 조절하여 Connection을 점유하는 시간을 줄이면 성능이 개선해볼 계획입니다. 결론의 재검증은 Application Server의 Memory는 동일한 것을 사용하고 vCPU만 Scale-Up 하여 비교하여 수행할 생각입니다.

 고민을 위한 여지

 

  • 현재 서버 구성에서 Connection은 언제 획득되고 언제 반환될까?
  • 어떻게 Connection을 점유하는 시간을 줄일 수 있을까?
해당 프로젝트는 네이버 클라우드를 활용하여 진행했습니다.