킴의 레포지토리
스프링 부트의 WAS 설정 알아보기 본문
WAS를 구현해보면서 스프링부트의 내장 WAS인 톰캣은 어떻게 동시 요청을 처리하는지 알아보았다.
1. 스레드 풀
1-1. 스레드 풀은 왜 사용하는가?
WAS는 동시에 들어오는 요청을 처리하기 위해서 멀티 스레드로 동작한다. 동시에 들어오는 요청만큼 쓰레드를 계속 생성한다면 다음과 같은 문제가 발생한다.
1. 스레드를 생성하기 위해서는 시스템 콜을 통해 OS 커널 모드로 진입해 OS 스레드를 생성하는데 이 동안 요청은 대기하게 된다.
2. 스레드가 많으면 하나의 CPU에서 여러 스레드를 동시에 작업하기 위해서 스레드간의 컨텍스트 스위칭이 많이 발생해서 CPU 오버헤드가 커진다.
3. 스레드는 고유한 실행 컨텍스트를 가지며, 호출 스택을 저장하기 위해서 스택 메모리를 할당받는데 스레드가 너무 많아지면 StackOverFlow 에러가 발생할 수 있다.
WAS는 스레드 풀을 사용해서 1) 동시에 사용할 수 있는 스레드의 개수를 제한하고 2) 스레드를 재사용해서 자원을 효율적으로 사용하고 응답시간을 줄인다.
1-2. 스레드가 요청을 할당받는 과정
tomcat 8부터는 기본적으로 HTTP 요청을 처리하기 위해 Non-Blocking I/O를 방식의 http-nio-connector를 사용하여 클라이언트와 연결한다. 블로킹 방식의 bio-connector는 클라이언트와 네트워크 연결을 맺고, 클라이언트로부터 요청을 받아들이는 과정에서 쓰레드가 블록되어 다른 연결을 받지 못한다. 따라서, 동시에 여러 연결을 처리하기 위해서는 각 연결마다 스레드가 할당되어야 하므로 많은 연결을 처리할 때 성능이 저하될 수 있다. 반면에, 논블로킹 방식의 nio-connector는 요청을 비동기적으로 처리하기 때문에, 한 스레드가 여려 연결을 동시에 처리할 수 있고, 스레드의 개수보다 많은 양의 연결을 유지할 수 있게 된다.
nio-connector를 사용하는 톰캣에서 생성한 스레드는 다음과 같이 nio-xxx-exec-xx와 같은 형식의 이름을 가진다.
nio-connector는 요청을 받아들여서 큐에 쌓고, 쓰레드들이 큐에서 요청을 하나씩 꺼내어 처리하게 된다. 이때 요청을 처리하는 쓰레드를 worker라고 하고, worker들을 쓰레드 풀로 관리한다. 스레드가 요청을 처리하는 과정을 간략이 표현하면 다음과 같다.
1. 요청을 처리하는데 사용할 worker들의 쓰레드 풀을 미리 생성해 둔다.
2. acceptor가 소켓 연결을 통해 클라이언트 요청을 받아들인 후 처리할 준비가 완료되면 큐에 추가한다.
3. worker들을 큐에서 요청을 하나씩 꺼내어 처리한다. 요청 처리가 끝나면 다시 스레드 풀에 반납되고 다음 요청을 처리하는데 재사용된다.
1-3. 스프링부트의 스레드 풀 설정 튜닝
스프링부트 프로젝트에서는 application.yaml의 프로퍼티를 사용해 간단하게 쓰레드 풀 관련 설정을 튜닝할 수 잇다. 스프링부트의 auto configuration에 의해서 application.yaml의 프로퍼티 값을 읽어 ServerProperties 객체를 생성하고 해당 설정이 TomcatWebServerFactoryCustomizer(서블릿 컨테이너로 tomcat을 사용하는 경우) 에 의해서 반영되어 내장 톰캣을 구성하기 때문이다.
다음은 application.yaml에서 내장 톰캣의 커넥션 풀과 관련된 설정을 추가한 것이다. 해당 값들은 ServerProperties에 대응된다. 만약 설정을 추가하지 않는다면 ServerProperties의 기본 값들이 사용된다.
server:
tomcat:
threads:
max: 200
min-spare: 10
max-connections: 8192
accept-count: 100
connection-timeout: 20000
- server.tomcat.threads.max: 서버가 생성할 수 있는 최대 스레드(worker)수.
- server.tomcat.thread.min-spare: 서버에서 유지할 최소 스레드(worker) 수.
- server.tomcat.max-connections: 네트워크 연결의 최대 수. acceptor가 수용하는 최대 연결 수를 의미하며 max-connections보다 더 많은 연결이 시도되면 connection-refused 에러가 발생한다. 서버가 과도하게 많은 연결을 처리하려고 할때 네트워크 리소스 및 메모리(요청을 쌓아두는 큐는 힙 메모리를 사용)가 부족해질 수 있어 네트워크 레이어에서 최대 연결 수를 제한한다.
- server.tomcat.accept-count: 최대 연결 수용. WAS가 max-connections를 맺은 후에 더 많은 요청이 들어오면 운영체제가 요청을 수용해둔다. OS가 제공하는 큐의 최대 길이를 의미한다.
- server.tomcat.connection-timeout: 클라이언트가 서버와 연결이 수락되기를 기다리는 시간이다. 시간 내에 연결에 실패하면 timeout 예외가 발생한다.
위와 같은 설정에서 WAS가 클라이언트와의 연결 및 스레드 풀을 관리하는 과정은 다음과 같다. WAS가 구동될때 10개의 스레드를 생성해서 스레드 풀에 추가한다. 모든 스레드가 요청을 처리하는데 사용되어 더이상 사용할 수 있는 스레드가 없다면 스레드를 동적으로 생성해서 추가한다. 이때 스레드의 개수가 200개가 될때까지 스레드를 추가할 수 있다. 만약, 스레드가 10개 보다 많이 생성된 상태이고 더 이상의 클라이언트 요청이 없어서 사용되지 않는다면 스레드는 제거된다. 이때, 처리할 요청이 없더라도 10개의 스레드는 유지한다.
연결은 최대 8192개를 맺을 수 있고, 8192개의 연결을 맺은 이후의 연결 요청들은 톰캣이 받지 않고 거절한다. 거절된 연결 요청들은 중 100개는 운영체제의 큐에 쌓이고 이후에 톰캣에 의해서 연결될 수 있다. 클라이언트가 연결을 요청하고 20동안 연결이 수락되지 않으면 timeout이 발생한다.
2. 커넥션 풀과 스레드 풀 최적화
커넥션 풀의 크기가 지나치게 큰 경우 실제로 사용되지 않는 연결에 의해 힙 메모리가 낭비되고 네트워크 자원이 불필요하게 소모될 수 있다. 반면에 커넥션 풀의 크기가 지나치게 작은 경우 동시에 많은 스레드가 DB 서버와 연결하려고 할때 연결이 거절되거나 대기시간이 동시 처리 성능이 나빠진다.
따라서, 커넥션 풀의 크기는 운영 환경의 특성을 고려하여 최적화되어야한다. 절대적으로 적절한 값은 없으며 연결 사용 추이, DB 서버 CPU 이용률 등을 모니터링해 적절한 사이즈를 찾아야 한다. 커넥션 풀의 크기를 결정할 때 참고할 수 있는 사항은 다음과 같다.
1. local(개발자용 pc)에서는 동시 요청이 잘 발생하지 않기 때문에 minimum-idle을 최소로 유지해 서버 기동 시간을 줄이고 리소스를 효율적으로 사용할 수 잇다.
2. produdction(상용 서버)에서는 minimum-idle과 maximum-pool-size를 동일하게 유지하는 것이 좋다. 요청을 처리하는 중에 커넥션을 생성하게 되면 응답시간이 늘어나서 사용자 경험을 저해한다.
3. DB 서버의 CPU이용률이 낮은 경우 더 많은 커넥션 풀 사이즈를 키워 더 많은 요청이 동시에 처리되도록 할 수 있다.
4. 커넥션 풀 사이즈에 비해 사용되는 커넥션 수가 작은 경우 커넥션 풀의 사이즈를 줄일 수 있다. connection-timeout이 자주 발생하면 커넥션 풀 사이즈를 키워야한다.
스레드 풀의 크기가 지나치게 큰 경우 스택 메모리가 부족해질 수 있고 스레드간 컨텍스트 스위칭이 자주 발생해 CPU 부하가 발생하고 처리 시간이 길어진다. 스레드 풀의 크기가 지나치게 작은 경우 동시에 처리할 수 있는 요청의 수가 작아 대기시간으로 인해 응답시간이 길어진다.
따라서, 스레드 풀의 크기는 운영 환경의 특성을 고려하여 최적화되어야한다. 절대적으로 적절한 값은 없으며 동시 요청 수 및 WAS의 CPU 이용률, 스택 메모리 사용률, 커넥션 풀 사이즈 등을 고려하여 스레드 풀의 크기를 조절할 수 있다.
1. WAS의 CPU 이용률이 낮다면 스레드 풀의 사이즈를 늘려서 동시에 여러 요청을 처리하도록 할 수 있다. 이미 WAS의 CPU 이용률이 높다면 스레드를 추가로 생성하는 것이 오히려 컨텍스트 스위칭 비용만 키워 성능이 나빠질 수 있다.
2. 스택 메모리의 총 크기 및 스레드 별 할당되는 스택 메모리의 크기를 고려하여 최대 스레드 수를 제한한다.
3. 스레드의 개수는 DB 연결 개수보다 절대로 적어서는 안된다. 스레드 풀은 입구 역할, 커넥션 풀은 출구 역할이다. 스레드 개수가 DB 연결 개수보다 작다면 사용되지 않은 채 낭비되는 DB 연결이 생긴다. 보통 스레드의 개수는 DB 연결보다 10개 정도 더 많이 설정한다. 모든 스레드가 DB에 접속하는 것은 아니기 때문이다.
4. 스레드의 최소 개수를 CPU의 코어 수보다 많게 설정해서 DB I/O가 발생하는 동안 다른 스레드를 처리할 수 있도록 한다.
3. 세션 타임아웃 설정
3-1. 세션이란
세션은 Stateless한 Http를 보완해서 클라이언트의 상태를 유지하기 위해 사용된다. 예를 들어, 사용자 인증 상태를 유지하거나, 장바구니 정보 등을 사용자의 상태를 유지하는데 사용된다. 세션의 특징은 다음과 같다.
1. 상태 유지: 웹 어플리케이션에서 사용자의 상태를 유지하고 추적하기 위한 매커니즘이다.
2. 유일성: 각 세션은 고유한 식별자를 가진다(JSESSIONID). 클라이언트가 최초로 서버에 요청을 보낼때, 서버는 세션을 생성하고 클라이언트에게 세션 식별자를 전달한다.
3. 제한된 수명: 세션은 일정 시간 동안만 유지된다. 클라이언트가 서버와의 상호작용을 일정 시간동안 (세션 타임아웃) 하지 않으면 세션은 만료된다.
4. 서버 측 관리: 세션 데이터는 WAS에서 생성되고 저장되며, 클라이언트는 쿠키에 세션 식별자를 담아 전달하여 세션 접근 권한을 부여받는다.
3-2. 스프링부트의 세션 타임아웃 관리
스프링부트는 기본적으로 서블릿 컨테이너를 내장하고 있으며, 서블릿 기반의 HTTP 세션을 지원한다. 스프링부트는 기본적으로는 세션을 생성하지 않고, 필요한 경우에 HttpSession 객체를 사용해서 세션을 생성할 수 있다. Spring Security를 사용하는 경우 기본적인 세션 생성 정책(SessionCreationPolicy.ALWAYSS) 은 모든 요청에 대해 세션을 생성하고 세션에 Security Context를 저장해 인증 상태를 유지한다.
생성된 세션은 서블릿 컨테이너에 의해 관리된다. 이때 application.yaml에서 내장 서블릿 컨테이너의 세션 만료 시간을 설정할 수 있다. 아래의 설정에서는 30분 동안 사용자의 세션을 유지하고 30분 후에 세션을 만료시킨다.
server:
servlet:
session:
timeout: 30m # 세션의 유효 시간 설정(기본값은 30분)
세션 타임아웃 시간이 너무 짧으면 사용자가 세션을 유지하지 못하고 자주 로그인해야하는 불편함이 발생할 수 있고, 너무 길면 메모리를 많이 사용하여 WAS 성능을 저하시킬 수 있다.
4. Keep-Alive 타임아웃 설정
5-1. Keep-Alive 란
클라이언트와 WAS는 HTTP 통신을 하는데 Keep-Alive 설정을 통해 연결을 유지할 수 있다. HTTP 통신은 기본적으로 statless하기 때문에 매 요청마다 클라이언트와 서버는 3-way-handshake를 통해 연결을 맺고 끊어야한다. 이러한 과정은 응답시간을 지연시키고 네트워크 자원을 낭비할 수 있다. 이때, Keep-Alive 설정을 통해 연결을 유지한 채로 여러 HTTP 요청과 응답을 처리할 수 있다.
Keep-Alive timeout 시간 동안 클라이언트로부터 요청이 없으면 연결을 닫아서 다른 클라이언트와 연결이 맺어질 수 있도록 한다. 기본적으로 톰캣의 Keep-Alive imeout은 20초이다.
4-2. 스프링부트의 Keep-Alive 설정
스프링부트 어플리케이션에서 클라이언트와의 연결은 내장 서블릿 컨테이너에 의해 관리된다. 이때 application.yaml에 내장 서블릿 컨테이너의 keep-alive-timeout 시간을 설정할 수 있다. 아래의 설정에서 톰캣은 1분동안 연결된 클라이언트로부터 요청이 없으면 연결을 닫는다.
server:
tomcat:
connection-timeout: 60000
[참고] 웹 서버 설정시 주의사항
웹 서버를 따로 둬야하는 이유
WAS의 성능을 높이기 위해서 WAS 앞 단에 apache나 nginx와 같은 웹 서버를 두는 것이 권장된다. 웹 어플리케이션 서버(WAS)도 정적 리소스를 반환할 수 있기 때문에 웹 서버 역할을 할 수 있다. 하지만, 상용 서버에서 WAS를 웹서버로 사용하는 것은 권장되지 않는다. 웹 서버를 따로 두는 것이 다음과 같이 WAS의 성능을 높이고 부가적인 기능을 제공할 수 있기 때문이다.
- 정적인 부분을 웹 서버에서 처리하여 WAS의 스레드를 사용하지 않아서 WAS의 처리 성능을 높인다.
- reverse proxy server로서의 부가적인 기능을 제공한다.
- 로드밸런싱을 통해 여러 대의 WAS에 요청을 고르게 분산하여 서버 부하를 균등하게 유지한다.
- 익명성 제공: 클라이언트로 부터 서버를 숨긴다. 실제 요청을 처리하는 WAS의 IP를 숨긴다.
웹 서버와 WAS 설정의 일관성 주의
이때 웹서버와 WAS의 설정이 다르지 않도록 주의해야한다. 요청은 웹 서버를 거쳐서 WAS에 전달되기 때문에 두 서버의 설정이 다르면 리소스를 낭비할 수 있다. 예를 들어, 웹서버의 Keep-Alive 설정이 꺼져있는데 WAS의 Keep-Alive Timeout이 길면 네트워크 연결 자원을 낭비하게 된다.
정리
✅ 쓰레드 풀은 동시에 처리할 수 있는 작업의 수를 제한하고, 쓰레드를 미리 생성하여 재사용함으로써 OS 커널 접근으로 인한 쓰레드 생성 비용과 응답 시간을 줄이는 데 사용됩니다.
✅ 세션 타임아웃, Keep-Alive 설정, 웹 서버 설정이 WAS의 성능에 영향을 미친다.
'study > spring' 카테고리의 다른 글
인증(1) - 스프링 시큐리티의 OAuth2.0 로그인 과정과 네이버, 카카오 로그인 구현 (0) | 2024.05.14 |
---|---|
어디서 JWT를 체크해야할까? Filter와 Interceptor의 장단점 분석 (3) | 2024.04.10 |