스레드 풀 사이즈 산정 원리

1,743 단어·4 분·원문(.md)

스레드가 많을수록 처리량이 높아질까? 현실은 그렇지않다. 스레드가 너무 많으면 컨텍스트 스위칭 오버헤드와 메모리 고갈로 인해 오히령 성능이 급감할 수도 있다.

스레드 풀 사이즈를 산정하기 위해서 반드시 이해해야하는 핵심 원리들을 단계벼로 알아보겠다.

작업의 성격 분석 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에서 적정 스레드 수를 구하는 유명한 공식을 세 웠다.

이 공식은 언어와 무관하게 시스템 설계의 근본 기준이긴한데.

Nthreads=Ncpu×Ucpu×(1+WC)N_{threads} = N_{cpu} \times U_{cpu} \times \left(1 + \frac{W}{C}\right)
  • NthreadsN_{threads}: 최적의 스레드 개수
  • NcpuN_{cpu}: 사용 가능한 CPU 코어 수
  • UcpuU_{cpu}: 목표로 하는 CPU 활용률 (0Ucpu10 \le U_{cpu} \le 1)
  • WW: 대기 시간 (Wait time - I/O 작업 등으로 대기하는 시간)
  • CC: 연산 시간 (Compute time - 실제 CPU를 사용하는 시간)

보기만 하면 연산시간이 높거나 대기시간이 적을수록 적정 스레드 수가 적어지는걸 알 수 있다.

cpu bound 작업일 경우 대기시간 w가 0에 수렴하므로 WC0\frac{W}{C} \approx 0이 된다.

따라서 최적의 스레드 수는 Ncpu+1N_{cpu} + 1 수준이 되는거고 +1을 하는 이유는 페이지 폴트나 일시적인 중단 시 예비용 스레드가 cpu를 점유하게 하기 위해서다.

io bound 작업일 경우 대기시간W이 연산시간 C보다 훨씬 길다. 만약 db를 기다리는 시간이 실제 연산 시간의 10배라면 (WC=10\frac{W}{C} = 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%를 치고 응답시간이 폭증하면 -> 스레드가 너무 많아 컨텍스트 스위칭 병목이거나 로직 자체의 개선이 필요하다.
SRE/question/q_25.md