스레드 풀 사이즈 산정 원리
스레드가 많을수록 처리량이 높아질까? 현실은 그렇지않다. 스레드가 너무 많으면 컨텍스트 스위칭 오버헤드와 메모리 고갈로 인해 오히령 성능이 급감할 수도 있다.
스레드 풀 사이즈를 산정하기 위해서 반드시 이해해야하는 핵심 원리들을 단계벼로 알아보겠다.
작업의 성격 분석 cpu bound vs io bound #
스레드풀 사이즈를 결정하는 절대적인 기준은 해당 애플리케이션이 주로 어떤 작업을 수행하는가를 파악하는 것이다.
- cpu bond (연산 중심 작업): 복잡한 수학 연산, 암복호화, 이미지 처리등 cpu를 쉬지않고 사용하는 작업
- io bound (입출력 중심의 작업): db쿼리, api호출, 파일io등 네트워크 디스크의 응답을 기다리는(waiting) 시간이 긴 작업이다. 대부분 웹 애플리케이션이 여기에 속한다.
cpu bound 작업은 코어 수보다 스레드가 많아봐야 cpu는 동시에 처리할 수 없으므로 스레드를 늘리는것은 컨텍스트 스위칭 비용만 낭비하는 것이고
io bound 작업은 스레드가 db응답을 기다리며 대기 상태 blocked, waiting에 빠질때 cpu는 놀게된다.
이때 다른 스레드가 cpu를 사용할 수 있도록 스레드 풀 사이즈를 넉넉하게 잡아주어야 cpu 활용률을 극대화할 수 있다.
브라이언 게츠 공식 #
자바 병렬 프로그래밍의 대가인 브라이언 게츠는 저서 java concurrency in practice에서 적정 스레드 수를 구하는 유명한 공식을 세 웠다.
이 공식은 언어와 무관하게 시스템 설계의 근본 기준이긴한데.
- : 최적의 스레드 개수
- : 사용 가능한 CPU 코어 수
- : 목표로 하는 CPU 활용률 ()
- : 대기 시간 (Wait time - I/O 작업 등으로 대기하는 시간)
- : 연산 시간 (Compute time - 실제 CPU를 사용하는 시간)
보기만 하면 연산시간이 높거나 대기시간이 적을수록 적정 스레드 수가 적어지는걸 알 수 있다.
cpu bound 작업일 경우 대기시간 w가 0에 수렴하므로 이 된다.
따라서 최적의 스레드 수는 수준이 되는거고 +1을 하는 이유는 페이지 폴트나 일시적인 중단 시 예비용 스레드가 cpu를 점유하게 하기 위해서다.
io bound 작업일 경우 대기시간W이 연산시간 C보다 훨씬 길다. 만약 db를 기다리는 시간이 실제 연산 시간의 10배라면 ()으로 cpu코어가 4개일때는 스레드는 최소 4 x 1 x (1 + 10) = 44개가 필요하다는 계산이 나온다.
현실적인 제약 조건들 #
이론적인 공식 외에도 실제 환경에서는 다음 요소들이 스레드 풀의 한계치를 결정한다
- 메모리 제한
- 스레드는 생성될때마다 각자의 독립적인 콜스택 메모리를 할당받는다 os및 언어마다 다르지만 보통 스레드당 1mb 내외라서 스레드풀 1000개로 잡으면 스레드 유지에만 1gb 메모리가 날아가는거라 가용 메모리 용량을 고려해야한다.
- db connection pool
- 웹 서버 스레드풀을 아무리 늘려도 디비 커넥션 풀 사이즈가 50개라면 병목이 50개를 넘어가는 요청을 받으면 발생한다. 50개의 스레드만 db와 통신할 수 있기 때문인거고 db 뿐만 아니라도 외부 api 연동한 곳에서 제공해주는 커넥션수에도 제약이 걸릴수있다. 200개 요청이 들어오면 50개만 처리되고 나머지 150개는 대기하고 있기 때문이다. 일반적으로 스레드풀 사이즈는 커넥션 풀 사이즈와 비슷하거나 약간 큰 수준으로 맞춰야 효율이 좋다.
- 임계영역과 락
- 스레드가 아무리 많아도 공유 자원에대한 락 경합이 심하면 결국 하나의 스레드만 순차적으로 실행되는 현상이 발생하여 성능이 저하된다.
도출 프로세스 #
초기값 설정을 먼저하자 애플리케이션의 성격을 분석하여 기준점baseline을 잡는거고 ex. io위주의 api 서버라면 코어수의 10~20배 수준으로 시작
그 이후 프로파일링 툴을 활용해보자 apm툴 datadog, new relic, scouter등 실제 트래픽에서 개별 요청의 cpu 처리시간 C, IO대기시간 W 비율을 측정한다
부하테스트 k6, locust, nGrinder ..etc 등의 도구를 통해 점진적 부하를 늘려가고 모니터링해서 cpu 활용률, 대기시간, 메모리 사용량, 응답시간등의 지표를 확인하자
- cpu가 놀고있는데 tps가 오르지 않는다면 -> 스레드풀(커넥션풀) 사이즈를 늘린다
- cpu가 100%를 치고 응답시간이 폭증하면 -> 스레드가 너무 많아 컨텍스트 스위칭 병목이거나 로직 자체의 개선이 필요하다.