본문 바로가기
Spring/스프링 기본 원리

[Spring DB] HikariCP는 왜 다른 CP보다 훨씬 빠를까?

by Taler 2022. 11. 24.

2022.09.06 - [Spring/DB Migrator] - [DB Migrator] Spring Boot에 2개 이상의 DataSource 붙이기

 

[DB Migrator] Spring Boot에 2개 이상의 DataSource 붙이기

2022.08.30 - [Spring] - [DB Migrator] Spring Boot와 함께하는 DB Migrator 프로젝트 - 1 [DB Migrator] Spring Boot와 함께하는 DB Migrator 프로젝트 - 1 이전에 인턴십을 진행했던 회사에서는 Spring에서 NestJS로의 리팩토링

taler.tistory.com

과거 포스팅인 위의 멀티 데이터소스 글을 다시 돌아보며 보강작업을 하던 중 그래서 왜 HikariCP가더 빠른지에 대해서 궁금해졌다. 뭔가 다른 CP에서는 사용하지 않던 새로운 인사이트가 있었던 것인지, 혹은 저번에 HikariCP에서 봤던 것처럼 double-checked locking을 사용해 CQRS를 달성했기 때문인지 궁금해졌다.

 

그래서 찾아보던 중 해당 글을 발견하게 되어 접근성 패치를 진행해보려고 한다.

 

 

왜 HikariCP일까?


HikariCP는 pooling mecahnism에 늦게 진입했지만, C3P0Apache Commons DBCPBoneCPTomcatVibur 등등이 제공하던 것보다 훨씬 더 나은 성능을 보여준다. 아래의 벤치마크 그래프가 성능 차이를 보여준다.

 

늘상 보던 HikariCP의 성능표

 

다수의 블로그들이 이미 HikariCP의 성능 비교에 관해서 다뤘기 때문에 이번 글에서는 내부의 복잡한 사정을 한 번 다뤄보고자 한다. 즉, 왜 HikariCP가 다른 CP에 비해서 더 빠른지, 어떤 점이 특별한지 등을 다룰 생각이다.

 

Hikari CP에 대한 이해를 향상시켜준 다른 소스들에 대한 링크는 마지막에 공유한다.

 

 

Hood Feature 아래에는...


이번 섹션에서는 HikariCP의 특별한 점을 나열한다.

 

1. Byte Code 단순화

HikariCp 라이브러리는 CPU 캐시가 더 많은 프로그램 코드를 로드할 수 있도록 컴파일된 Byte Code가 최소가 될 때까지 코드를 최적화했다. 해당 최적화는 JavaAssist를 통해 달성했으며, JavaAssist는 JDK 프록시 위에서 동적 프록시를 생성하는데 사용된다. JDK 프록시보다 적은 바이트를 생성하기 때문에, 불필요한 바이트코드들을 무효화시켰고 이로 인해 속도가 빨라질 수 있었다.

 

2. Proxy와 Interceptor 최적화

Hikari 라이브러리는 많은 코드를 줄였다. 예를 들어 HikariCP의 Statement Proxy에는 단 100줄의 코드만이 남아있다.

 

3. ArrayList 대신 FastList 선택

get()호출이 remove()호출 중에도 실행되는 경우가 많다. 때문에 매번 범위 확인을 하는것을 피하고자 햇고, HikariCP는 ArrayList 대신 FastList를 구현해 전체 배열 스캔을 방지했다. ArrayList의 remove 메서드는 처음부터 배열을 순회하고, FastList는 배열의 끝에서부터 순회하기 때문에 일반적으로 제거된 요소가 끝부분에 있을 때 더 나은 성능을 보이는 것이다. 아래 코드들은 FastList에서 발췌했다. (참고로 FastList는 ArrayList를 상속한다.)

 

@Override   
public T get(int index)   
{
	return elementData[index];   
	// no range check of ArrayList
}

@Override
public boolean remove(Object element) {
	for (int index = size - 1; index >= 0; index--) {
 		if (element == elementData[index]) {
			final int numMoved = size - index - 1;
            
			if (numMoved > 0) {
				System.arraycopy(elementData, index + 1, elementData, index, numMoved);
			}
			elementData[--size] = null;
            
			return true;
		}
	}
    
	return false;
}

 

4. ConcurrentBag의 사용

Read/Write에 대한 동시 작업의 효율성을 향상시키기 위해 ConcurrentBag이라 불리는 커스텀 Collection을 사용한다. CustomBag 구현은 lock 없는 설계, ThreadLocal 캐싱, Queue-stealing 및 direct hand-off의 최적화를 제공한다. 그 결과 높은 수준의 동시성, 매우 낮은 latency, 그리고 false-sharing 발생 최소화를 달성할 수 있었다.

 

5.  CPU time splice 사용 최적화

method call과 관련된 다른 최적화들이 구현되어 있다.

 

6. HikariCP Artifact 사이즈 최소화

HikariCP jar의 사이즈는 단지 135KB밖에 안된다. 코드의 양이 적기 때문에 실행의 효율성은 증대됐으며, "적은 양의 코드에는 버그 발생 확률도 적다."라는 소프트웨어 업계의 격언에 따라 HikariCP에는 거의 버그가 존재하지 않는다.

 

 

맺으며


가장 중요한 것은 ConcurrentBag의 구현이었던 것같다. ConcurrentBag에 대한 lockless, thread safe 구현에 대한 소스코드는 여기에서 확인할 수 있다. 또한, javadoc의 HikariCP ConcurrentBag 문서를 확인해보면 다음과 같은 문구를 확인할 수 있다.

 

ConcurrentBag은 Connection Pool을 위해 LinkedBlockingQueue나 LinkedTransfersQueue보다 우수한 성능을 달성하는데 특화된 Concurrent Bag이다. 가능한 한 Lock을 피하기 위해 ThreadLocal storage를 사용하지만, ThreadLocal list에 사용가능한 item이 없는 경우, 공통 collection에서 스캔을 수행한다.

ThreadLocal list에서 사용 중이지 않은(Not-in-use) item들은 빌린 thread의 데이터가 없는 경우 훔쳐질 수 있다. Thread 간 신호를 관리하기 위해 특화된 AbstractQueuedLongSynchronizer를 사용하는 "lock-less(락 없는)" 구현이다. Bag에서 빌린 item은 그 어떤 collection에서도 제거되지 않으므로, 그것에 대한 참조가 중단되더라고 GC가 발생해 삭제되진 않는다. 따라서, 빌린 객체에 대해 requite하는 것에 주의를 기울여야 한다. 그렇지 않으면 memory leak(메모리 누수)가 발생할 수 있다.

오직 "remove" 메서드만이 bag으로부터 객체를 완전히 삭제할 수 있다.

 

HikariCP 뿐만 아니라 Concurrent Bag에 대해서도 더 분석해볼 필요가 있을 것 같다...

 

 

 

더 읽을 거리들 (저자가 소개한)


추가로

댓글