들어가면서
서비스 API 서버를 구축하고 운영하면서 Tomcat 스레드 풀과 관련된 설정값들을 어떻게 튜닝할 수 있을지 생각해볼 기회가 생겼습니다. 막연하게 최적의 설정값을 찾으면 성능이 좋아지겠다고 생각은 했지만 왜 튜닝을 해야하는지, 어떻게 진행해야 하는지 감이 잘 잡히지 않았었습니다. 그래서 나름대로 백엔드 서버에서 Tomcat 스레드 풀 관련 설정값들을 왜 튜닝해야 하는지, 어떻게 튜닝할 수 있는지를 고민해보고 정리해봤습니다.
스레드 풀의 크기를 적절하게 조절해야 하는 이유
스레드를 생성하는 것은 비용이 많이 드는 작업입니다. 새로운 스레드를 위한 메모리 공간을 할당하는데 이 메모리 공간은 JVM이 할당받은 메모리 공간을 점유하게 됩니다. 이 때 기본적으로 1MB의 메모리 영역만큼을 하나의 스레드가 사용할 수 있도록 예약하며, 아무것도 수행하지 않는 스레드도 16kb 만큼의 메모리 영역을 점유합니다. 컴퓨팅 리소스 중 메모리 영역은 굉장히 비싼 자원인데 무분별한 스레드 생성으로 인해 메모리 영역이 낭비된다면, 효율적인 프로그램이라고 말할 수 없을 것입니다. 또한 관리해야 하는 스레드의 개수가 늘어나면 늘어날수록 각기 다른 스레드 접근 시 발생하는 잦은 컨텍스트 스위치 비용 또한 무시할 수 없습니다.
이러한 문제들을 방지하기 위해 스레드 풀에서 미리 정해둔 개수만큼만 스레드를 생성해두고 재사용할 수 있습니다. 하지만 스레드 풀에 스레드를 미리 생성해서 사용한다고 하더라도 얼마만큼의 스레드를 생성해서 사용하게 할 것인지는 또 다른 문제입니다. 앞서 얘기한대로 스레드 풀에서 관리하는 스레드 개수가 너무 많다면 여러가지 비용이 많이 발생할 것이고, 너무 적다면 우리가 가진 CPU를 최대한 사용하기 힘들 것입니다. 따라서 우리는 스레드 풀에서 관리할 스레드 수를 적절하게 설정할 수 있어야 합니다.
스레드 풀 적정 크기에 영향을 주는 요인들
스레드 풀 적정 크기에 영향을 주는 주된 요인 중 하나는 프로세스 작업의 종류입니다. 프로세스는 크게 CPU Bound 프로세스와 I/O Bound 프로세스로 분류할 수 있는데 각 개념의 뜻은 다음과 같습니다.
- CPU Bound 프로세스 : 프로세스가 CPU에서 연속적으로 실행되는 시간이 긴 프로세스
- 예시 : 동영상 편집 프로그램, 머신러닝 프로그램 등...
- I/O Bound 프로세스 : 프로세스가 I/O 작업을 요청하고 결과를 기다리는 시간이 긴 프로세스
- 예시 : 일반적인 백엔드 API 서버
프로세스를 위와 같이 분류할 수 있기는 하지만 하나의 프로세스가 온전하게 CPU Boundary 프로세스이거나 I/O Bound 프로세스인 것은 아닙니다. 일반적으로 프로세스는 CPU 작업과 I/O 작업을 번갈아가며 수행합니다. 여기에서 상대적으로 CPU Bound 작업이 많으면 CPU Bound 프로세스, I/O Bound 작업이 많으면 I/O Bound 프로세스라고 분류할 수 있습니다.
우리는 백엔드 개발자기 때문에 우리의 주 관심사는 일반적인 백엔드 API 서버 작업에 해당하는 I/O Bound 프로세스일 것입니다. 보통의 API 서버는 HTTP Request를 받으면 DB서버나 캐시 서버에 데이터를 요청한 뒤, 수신한 데이터를 가공하여 다시 HTTP Response로 돌려주는 작업을 수행합니다. 이 때 DB 서버나 캐시 서버에 데이터를 요청하는 부분이 바로 I/O Bound 작업에 해당합니다. 이 부분은 요청과 응답이 네트워크를 통해 이뤄질 수 밖에 없기 때문에 CPU에서 명령이 여러 개 수행되는 것보다 훨씬 느리게 처리되는 것이 일반적입니다. 물론 API가 어떤 작업을 실제로 수행하는지에 따라 이러한 내용은 바뀔 수 있습니다.
만약 CPU Bound 프로세스를 주로 처리한다고 하면 무엇을 기준으로 스레드 풀 크기를 결정해야 할까요? CPU Bound 프로세스에 여러 스레드가 할당되면 상대적으로 CPU를 오래 점유해야하는 작업이 많을 것이므로 각 스레드 간 CPU 경쟁이 심해질 것입니다. 그러면 CPU에서는 여러 스레드에게 번갈아가며 작업을 수행할 수 있도록 하기 위해 계속해서 점유 스레드를 변경해주는데 이 때 잦은 컨텍스트 스위칭 비용이 발생할 수 밖에 없습니다. 오히려 CPU 개수에 맞춰서 할당한 스레드가 task를 처리하도록 하는 것이 불필요한 리소스 낭비를 막을 수 있습니다.
"Java Concurrency in Practice"의 저자인 Brian Goetz는 CPU Bound 프로그램에서 적절한 스레드 수는 CPU 개수 +1 이라고 발표하기도 했습니다.
위과 같이 현재 개인 컴퓨터의 가용 CPU 개수가 16개일 때 CPU Bound 프로세스만을 처리하는 경우, Goetz가 발표한 공식에 따라 17개의 스레드 풀 크기를 유지하는 것이 최적화된 튜닝이라고 말할 수 있을 것입니다.
반대로 I/O Bound 프로세스를 처리하는 경우에는 위의 경우와 다르게 딱 정해진 규칙이 없어서 여러 상황에 맞춰 적절한 스레드 수를 찾아야 합니다. 만약 동작 스레드가 DB 혹은 외부 서비스 로직에 의존하는 작업을 수행한다면, 스레드 풀 크기는 해당 호출 작업에서 처리할 수 있는 작업량의 크기에 따라 제한될 가능성이 높습니다. 즉 아무리 스레드 개수를 높여도 DB, 외부 서비스 호출 로직의 처리량으로 인해 유의미한 성능개선이 이뤄지지 않을 수 있다는 것입니다. 이러한 또 다른 요인들로는 메모리, 파일 처리, 소켓 처리 등이 고려될 수 있습니다. 따라서 하드웨어 스펙, 프로그램의 특성 등 여러가지 상황을 고려해서 결정해야 하는데 다음과 같은 사항들이 그 예시가 될 수 있습니다.
- API 서버의 하드웨어 스펙은 어느정도인지?
- API 어플리케이션의 I/O 작업 처리로 인한 대기 시간이 얼마정도인지
- 서비스를 운영하면서 예상되는 트래픽의 패턴은 어떠한지
그래서 스레드 풀 튜닝, 어떻게 해야 하나요?
위와 같이 I/O Bound 프로세스를 처리하는 경우 튜닝을 할 때 고려해야 할 요소가 굉장히 많습니다. 그래서 이러한 문제를 해결하고자 적정 스레드 개수를 구하는 공식이나 성능 측정과 관련된 법칙 등을 사용하기도 하는데요, 이는 이상적인 해결책인지라 실제로 운영중인 서비스 서버에 적용하기에는 무리가 있다고 합니다.
따라서 튜닝 대상 설정값을 하나씩 바꿔보며 성능 테스트를 해보는 방식으로 휴리스틱하게 튜닝하는 것이 효율적이라는 결론을 내릴 수 있습니다. 튜닝 과정에서 서비스 사용자 시나리오, 서버 스펙, 수행되는 서비스 로직들을 함께 고려하며 실제로 측정한 성능이 왜 이렇게 변화했는지 규정하고, 목표로 하는 성능까지의 개선을 위해 최적의 설정값을 찾아가는 것이 중요합니다. 시간이 된다면, 이러한 점들을 고려하며 실제로 휴리스틱하게 스레드 풀 튜닝을 진행하는 내용을 정리해서 다음에 포스팅해보도록 하겠습니다.
Reference
- https://www.youtube.com/watch?v=qnVKEwjG_gM
- https://engineering.zalando.com/posts/2019/04/how-to-set-an-ideal-thread-pool-size.html
- https://bcho.tistory.com/788
- https://velog.io/@jaeyunn_15/JavaKotlin-%EC%9E%90%EB%B0%94-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%92%80-%EC%83%9D%EC%84%B1-%EB%B9%84%EC%9A%A9
'우아한테크코스 > 학습 정리' 카테고리의 다른 글
[Grafana Loki] 다중 서버 환경에서 그라파나 로키를 이용해 로깅하기 (0) | 2023.10.01 |
---|---|
[DB] 인덱스 기본 개념과 클러스터링, 논클러스터링 인덱스 (2) | 2023.09.17 |
[Logging] Slack 알림 메세지 Logger 구현하기 (1) | 2023.08.28 |
[Logging] Logback을 이용한 기본적인 로깅하기 (1) | 2023.08.06 |
[Spring Data JPA] Save() 메서드 persist VS merge (0) | 2023.07.30 |