스레드 풀 크기를 결정할 때는 시스템 처리량(Throughput) 요구사항을 고려해야 합니다. 단순한 접근법으로 TPS(초당 처리 트랜잭션)가 20이고 각 트랜잭션 처리 시간이 4초일 경우, 80개 스레드가 필요하다고 계산할 수 있지만, 이는 CPU 코어 수를 고려하지 않은 비현실적인 방법입니다.
기본 산정 공식
CPU 코어 수(N)를 기준으로 한 실용적인 접근:
- CPU 집중 작업: N + 1
- I/O 집중 작업: 2N + 1
고급 산정 공식
보다 정확한 계산을 위한 공식:
최적 스레드 수 = ((스레드 대기 시간 + 스레드 CPU 시간) / 스레드 CPU 시간) × CPU 코어 수
예시: CPU 시간 0.5초, 대기 시간 1.5초, 코어 8개 → ((0.5+1.5)/0.5)×8=32
이 공식은 다음 원리를 반영합니다:
- 대기 시간 비율이 높을수록 더 많은 스레드 필요
- CPU 시간 비율이 높을수록 적은 스레드 필요
시스템 처리량 최적화
Amdahl의 법칙에 따르면 병렬화 효과는:
가속비 = 최적화 전 소요시간 / 최적화 후 소요시간
시스템 병렬도(F), CPU 수(N)와의 관계:
가속비 ≤ 1 / (F + (1-F)/N)
동적 계산 구현 예제
실제 환경에서 최적값을 계산하는 Java 구현:
public abstract class PoolCalculator {
private final int QUEUE_SAMPLE = 1000;
private final int TIME_TOLERANCE = 20;
private volatile boolean isExpired;
private final long TEST_DURATION = 3000;
public void compute(BigDecimal targetUsage, BigDecimal maxQueueSize) {
// 큐 용량 계산 로직
Runnable job = generateTask();
executeJob(job); // 워밍업
long cpuDuration = getThreadCpuTime();
executeJob(job); // 실제 측정
cpuDuration = getThreadCpuTime() - cpuDuration;
long waitDuration = (TEST_DURATION * 1000000) - cpuDuration;
// 스레드 수 계산 로직
}
// 추상 메서드 선언
protected abstract Runnable generateTask();
protected abstract BlockingQueue createJobQueue();
protected abstract long getThreadCpuTime();
}
구체적 구현 예시:
public class NetworkPoolCalculator extends PoolCalculator {
@Override
protected Runnable generateTask() {
return new NetworkTask();
}
@Override
protected BlockingQueue createJobQueue() {
return new LinkedBlockingQueue(1000);
}
@Override
protected long getThreadCpuTime() {
return ManagementFactory.getThreadMXBean().getCurrentThreadCpuTime();
}
}
class NetworkTask implements Runnable {
public void run() {
// 네트워크 요청 처리 로직
}
}
실행 결과 활용
계산 결과를 바탕으로 스레드 풀 초기화:
ExecutorService executor = new ThreadPoolExecutor(
88, // 계산된 스레드 수
88,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(2500) // 계산된 큐 크기
);