스레드 풀
스레드가 일을 시작하고, 멈추는 과정에 발생하는 일
스레드가 일을 하려면 CPU Time Slice 를 할당 받아야 한다. 할당된 시간이 끝나면 스레드는 정지상태로 들어가는데, 이 때 프로세스당 하나씩 있는 PCB에 프로그램 카운터, 스케줄링 정보 등 작업중이던 일에 대한 문맥이 저장된다. CPU 스케줄링에 따라 CPU Time Slice 를 해당 스레드가 다시 할당 받게 되면 이전에 하던 작업의 문맥을 다시 복원하고 작업을 재개한다.
이 과정을 컨텍스트 스위칭이라고 한다.
스레드가 많을 수록 좋은 것인가
스레드가 많을 수록 개별 스레드가 부여받는 한번의 작업당 주어지는 CPU Time Slice 는 짧다. 왜냐하면 자원 할당을 요구하는(자원 경합에 참여하는) 스레드의 수가 많기 때문에 CPU 스케줄링에 따라서 개별 스레드에 줄 수 있는 CPU Time Slice 가 짧을 수 밖에 없기 때문이다.
그래서 스레드가 많을 수록 개별 스레드에게 주어지는 CPU Time Slice 는 짧아지고 그만큼 컨텍스트 스위칭이 더 많이 발생한다. 문제는 컨텍스트 스위칭 비용이 서버의 전체적인 레이턴시를 늘리게 될 수 있다는 것이다.
스레드가 적을 수록 좋은 것인가
CPU 자원이 풍부한데 자원을 할당받아 일을 처리하는 스레드의 수가 적으면 모든 스레드가 정지 상태에 들어 갔을 때 CPU 가 놀게 된다.
예를 들어 스레드가 총 50개가 있는데 50건의 요청이 동시에 왔고, 데이터베이스 IO 나 네트워크 요청 등으로 모든 스레드가 정지된 상태에 들어갔는데 신규 요청이 들어오면 이를 처리할 스레드가 없다. CPU 의 자원은 아직 여유가 많아서 CPU 시간을 할당해 줄 수 있는 능력이 있는데도 스레드가 없어서 신규 요청을 처리할 수 없는 것이다. 스레드 수가 더 많이 있었다면 기존 50개의 스레드가 정지해있어도 다른 스레드가 이를 처리할 수 있었을 것이다.
적절한 스레드 수를 산정하는 방법
나는 적어도 실무에서 블로킹으로 된 프로젝트에서 따로 톰캣 스레드 수를 커스텀 한적은 없다.
왜 스레드 '풀'로 스레드들을 미리 만들어서 관리하는가
스레드 하나당 약 1MB 를 차지하는데 요청이 들어오면 스레드를 만들어서 이를 처리하고 응답을 한 뒤에는 스레드를 없애는것이 메모리를 더 효과적으로 관리하는 것이 아닌가? 라는 생각을 할 수 있다. 막연하게 스레드 풀을 당연하게 생각할 것이 아니라 왜 '풀'에 다수의 스레드 들을 미리 만들어두고 이를 관리하는 것인지에 대한 의문을 가져보자.
스레드 생성, 소멸에 비용이 든다
결론부터 말하자면 스레드는 생성과 소멸에 비용이 많이 든다. 그래서 미리 만들어 두는게 좋고, 다써도 다음을 위해 없애지 않는 것이 좋다. 스레드는 자체적인 메모리 공간이 있기 때문에 스레드 생성시 메모리 할당이 발생한다. 그리고 CPU Time Slice 를 부여 받기 위해서 CPU 스케줄링 큐에 해당 스레드가 등록이 되어야 한다. 소멸 할때에도 마찬가지로 메모리를 반환하기 위한 절차가 필요하며 모든 이 과정들이 비용이다.
미리 만들어두지 않으면 생성 비용만큼 레이턴시가 증가한다
스레드가 없다면 요청을 처리하기 위해 스레드를 만들어야하는데 매번 이렇게 만드는 과정에 드는 시간만큼 응답 레이턴시가 늘어나게 되는 것은 당연한 일이다.
톰캣의 스레드풀의 스레드와 데이터베이스 커넥션풀의 스레드는 다른 것인가
기본적으로 다수의 스레드를 미리 만들어서 풀에 넣어 보관하다가 이를 쓰고 반납하는 매커니즘의 존재 이유는 위에서 설명한 것과 동일하지만 톰캣의 스레드풀에 있는 스레드와 데이터베이스 커넥션풀의 스레드는 다르다.
이는 스레드풀의 존재 목적과 깊은 연관이 있는데 데이터베이스 커넥션풀의 경우 목적 자체가 데이터베이스에 연결을 하는 과정에 드는 비용이 너무 높아서 미리 커넥션을 맺어두는게 목적이다. 따라서 데이터베이스 커넥션 풀에 들어가있는 스레드는 데이터 베이스와 '이미 연결된 상태' 이다.
'연결된 상태' 란 데이터베이스와 커넥션을 미리 가지고 있다는 것이다. 데이터베이스에 인증 절차를 거쳐서 커넥션을 가져오고 이 커넥션을 이용해서 쿼리를 수행하는 등 데이터베이스와 상호작용 할 수 있는데 이걸 얻는 과정에 비용이 너무 많이 드니 미리 만들어 두는 것이다.
그래서 데이터베이스 커넥션풀에 담긴 각각의 스레드는 각각의 커넥션에 대한 주소값을 지니고 있다. 각각의 커넥션은 힙 메모리에 적재된 상태이다.
톰캣의 스레드풀과 데이터베이스 커넥션풀의 사용 관점에서 정리한 흐름
사용자 상세 조회 요청이 들어왔다고 가정하자. 이 때에 스레드 풀의 사용 관점에서 전체 흐름을
클라이언트 요청: 클라이언트가 HTTP 요청을 보낸다.
톰캣 스레드 풀 (T1): 톰캣의 스레드 풀에서 빈 스레드(유휴 스레드) T1이 할당 된다.
웹 애플리케이션 처리: T1은 해당 요청을 웹 애플리케이션 내의 컨트롤러 등으로 전달하고 애플리케이션 로직이 실행된다.
데이터베이스 작업 요청: 애플리케이션 로직 실행 중, 데이터베이스 작업이 필요한 부분이 있을 경우, 데이터베이스 커넥션을 획득하기 위해 데이터베이스 커넥션 풀로부터 빈 커넥션을 가져온다.
데이터베이스 작업 수행 (T1): T1은 가져온 커넥션을 사용하여 데이터베이스 작업(예: 조회)을 수행하고 그 결과를 가져온다. 이 때 T1은 대기 상태가 된다.(블로킹인 경우)
데이터베이스 작업 완료: T1은 데이터베이스 작업을 마치고 커넥션을 다시 데이터베이스 커넥션 풀로 반환한다.
웹 애플리케이션 처리 계속: T1은 데이터베이스 작업 결과를 활용하여 웹 애플리케이션의 나머지 로직을 처리한다.
응답 생성: T1은 HTTP 응답을 생성하고 클라이언트에게 응답을 전송한다.
스레드 반환: T1은 작업을 마치고 톰캣의 스레드 풀로 반환된다.
Last updated