■ 적정 쓰레드수 산정하기
각 API 처리 전용 쓰레드풀을 만들게 되었다.
각각의 API가 가지는 쓰레드풀에 적당한 코어 쓰레드를 할당해보자.
먼저 파악해야 할 것은
- 이 서버에 할당할 리소스가 어느정도인지 알아야 한다.
- 그리고 Brian Goetz의 쓰레드 산출 공식을 적용하려면 수행 대상 로직의 대기 시간과 서비스 시간을 미리 알아봐야 한다.
이를 통해 전체 API에 할당할 총 쓰레드 수를 알아보려 한다.
□ 서버에 할당된 리소스
- CPU: 4코어
- Memory: 8GB
□ 쓰레드 산출 공식 (Java Concurrency in Practice - Brian Goetz)
적정 쓰레드 수 = 코어 수 x (1 + 대기시간/서비스시간)
∨ 대기 시간: CPU가 Wait 상태인 시간 (I/O 작업 등)
∨ 서비스 시간: 실제로 CPU가 동작 중인 시간 (대기 시간 제외)
대기 시간과 서비스 시간을 실측이 필요한 부분이다.
이어서 진행해보자.
.
.
본격적으로 쓰레드 수를 산출해보자.
API로는 start, talk, stop가 있는데, 먼저 통틀어 어느정도의 쓰레드를 할당해야 적절한지 검토해보자.
그리고나서 각 API에 나눠주자.
시작!
□ 쓰레드 수 산출 및 검토
- 코어 할당
- 주요 API들을 처리하는 것이 핵심인 서버다.
- 주요 API 처리에 리소스 절반인 2코어를 할당하자.
- 대기시간 가정:
- 메인이 되는 API인 talk의 network I/O 작업에 총 1~2초 정도 소요한다.
- 보수적으로 대기시간(I/O 작업)의 비중을 적게 설정하여, 대기시간을 1초로 가정하자.
- start, stop의 경우 network I/O (DB, Redis) 0.3초를 가정하자.
- 이때 talk와 start/stop의 비중을 대략 7:3으로 임의 가정하자. (talk가 더 많음을 적절히 반영)
- 비율에 따라 가정한 대기시간은 2*0.7 + 0.3*0.3 = 1.49
- 서비스시간 가정:
- 위 I/O 작업 제외하고 talk에 대해 측정한 결과 50번 평균 약 0.001X 소요하였다.
- 따라서 서비스시간은 0.002로 가정해보자.
- 쓰레드 수 산출 (Brian Goetz의 공식)
- 적정 쓰레드 수 = 코어 수 x (1 + 대기시간/서비스시간)
- 적정 쓰레드 수 = 2코어 * (1 + 1.49 / 0.002) = 1490개
- ** 위험) 리소스도 고려해야 한다.
- ** 쓰레드 1개당 1~2MB 메모리를 사용: 1490개면 1GB~3GB 사용
- ** 너무 많을 경우 컨텍스트 스위칭으로 CPU에도 영향이 감
- 최종 결정: 400개
- 1490개의 30%(임으로 낮춤) 수준인 447개 → 설정 편의성을 위해 400개로 결정
- 400개면 600MB~800MB 메모리 사용 예상
- 적절성 검토 (리소스 모니터링)
- 쓰레드 400개 중에서 300개를 talk에 할당한다고 가정하고 CPU, Mem 사용량을 모니터링 해보자
- 결론: 초반에 몇초간 400% CPU 사용률을 보이는데 쓰레드 생성시 오버헤드 때문일 것임. 10초쯤 그러다가 80% 아래로 정리되는 모습을 보임.. 영향이 적지 않을까..? 트래픽이 갑작스럽게 몰릴 때가 아니면 급증하지 않을 거고, 트래픽이 몰리더라도 초반에 급작스런 쓰레드 생성으로 CPU가 바쁘다가 곧 안정될 것 같다는 생각이 든다.
서버 부팅 초기 | 트래픽 준 이후 | 처리 완료 이후 | |
Memory | 9.70% (795.3MB) | 1.25GB (15.7%) 유지 | 감소 없음 |
CPU | 2~15% 유지 | - 380~430% (초반 일시적) - 이후 30~80%로 감소하다가 - 한창 처리 중에 20~40% 유지 |
감소하여 1~15% 유지 |
결론적으로 이 API 쓰레드풀에는 총 400개의 쓰레드를 할당하기로 했다.
그 중 비중이 가장 큰 talk에 300개를,
나머지 start와 stop에는 각 50개를 할당하려 한다.
- 번외: 참고사항 (Http 관련)
- talk의 경우 300개의 쓰레드가 풀가동되며 외부 host로 http 요청을 보낸다.
- 그러나 기존 http 연결 모듈의 방식은 http 모듈을 맺을 때마다 새로운 커넥션을 얻는 것이었다.
- 응답을 얻은 이후 disconnect를 명시적으로 해주고 있었지만 keep alive 같은 개념으로 바로 커넥션을 정리하지는 않는 것 같다. 그러지 않고서야 요청이 대거 쌓인 후 ConnectionException이 날 만한 이유는.. 모르겠다..! 그래서 추측하건대 커넥션이 정리되지 않는 것이 원인일 것이다.
- http 요청을 보내는 방식을 변경했다. 기존에는 HttpUrlConnection 객체를 직접 만들어서 요청 보냈는데, 그러지 않고 PoolingHttpClientConnectionManager, CloseableHttpClient을 뒀고, 5초마다 IdleConnectionMonitor에서 closeIdleConnections랑 closeExpiredConnections을 해줘서 커넥션 풀을 최적으로 관리하고자 했다. 쓰레드를 적극 돌리다보니 재사용하는 것보다 새 커넥션을 위해 빨리빨리 정리해주는게 맞는 선택일 것 같다.
- (아직 확인이 부족한 사항: 언제 재사용하고 언제 새로 맺는가)
■ 처리량 검토 (리틀의 법칙)
리틀의 법칙 (L = λ * W)과 테스트를 통해,
지연이 없는 경우와 지연이 있는 경우의 처리량을 따져보자.
◇ 처리량 검토1: 지연이 없는 경우 쓰레드의 처리량
- 동시 처리 요청 개수 = 시스템 처리량 * 요청당 처리시간
- talk api 처리량 = 300개 쓰레드 / 2초 = 150개(sec)
- 결론: 1초에 150개까지 지연 없이 바로바로 처리할 수 있다.
◇ 처리량 검토2: 지연이 있는 경우 처리량 (API 진입~쓰레드 끝)
- talk에 쓰레드 300개를 할당했다
- 한번에 600건씩 트래픽이 오는 상황을 가정하여, 50초동안 3만개 요청을 Jmeter로 보냈다.
- 쓰레드에서는 network I/O로 총 2초의 작업이 소요된다.
- API 진입점부터 쓰레드 처리 완료까지의 시간을 측정해보았다.
[테스트 복붙]
{"count":30000,"average":147368.10273333333,"max":274627,"min":15104,"throughput":92.87925696594428}
{"count":30000,"average":143916.25026666667,"max":271532,"min":10399,"throughput":93.16770186335404}
{"count":30000,"average":124756.73733333334,"max":251776,"min":2022,"throughput":99.66777408637874}
{"count":30000,"average":124818.52143333333,"max":251867,"min":2024,"throughput":99.66777408637874}
{"count":30000,"average":147328.19893333333,"max":267897,"min":13607,"throughput":90.9090909090909}
[측정 값 분석]
- min time: 쓰레드풀 급증이 없다면 2.023초(예상과 일치) / 최대치로 급증한다면 10~15초
- max time: 250~270초 (4분10초~4분30초)
- avg: 140초 (2분 20초)
- throughput: 90~99
*서버 켜자마자 구동하면 min 시간이 예상보다 엄청 긴 이유가 쓰레드풀 갑작스레 늘리느라 그런 것으로 유추됨
* min이 2초쯤으로 찍힌 건 활성 쓰레드가 이미 돌고 있어서 생성 오버헤드가 들지 않아서 그럴 것임. 한번 테스트 돌리고 활성 쓰레드가 사라지기 전에 30000건 요청 또 보내보면 min이 예상대로 나온다.
*서비스 도중에는 어느 정도 트래픽이 있어서 활성 쓰레드가 좀 있기 때문에 이 테스트 결과보다는 좋은 상황을 보일 것임
◇ (최종) 처리량 검토 결과
- 지연 없을 때는 throughput 150
- 아주 지연될 때는 throughput 90~100
바로바로 응답을 줘야하는 것이 아닌 데이터를 저장하는 것이 목적이기에
타겟 API 전체에 배정할 쓰레드풀의 코어를 400 잡은 것,
그리고 HTTP 커넥션 풀 호스트당 1000 잡은 것은 괜찮은 튜닝이라는 결론이다. (이 포스트에는 없는 내용임)
'미분류글' 카테고리의 다른 글
Springboot 3 Migration (from 2.6) (2) | 2025.01.06 |
---|---|
[Redis] StreamListener(onMessage)와 Consumer 객체간 관계 (0) | 2024.09.19 |
Redis Stream의 구조적 특징(Message Queue) (2) | 2024.08.26 |
[IntelliJ] 폐쇄망 개발환경 세팅 (0) | 2024.08.21 |
[Web][Docker] 동일 ip, 서로 다른 도메인 80포트에 대한 포트포워딩 설정 (1) | 2024.07.16 |