[Spring] Apache AB를 활용한 서버 성능테스트와 톰캣 쓰레드(Thread) 확장으로 성능 최적화
웹 서버들은 어떻게 동시에 여러개의 요청을 처리할까?
일반적으로 멀티 쓰레드를 이용한 방법을 채택한다.
그런데 동시에 10만개의 요청이 들어왔을때, 쓰레드가 10만개가 생성될까?
멀티 쓰레드는 다음과 같은 단점이 있다.
- 쓰레드를 생성하는 비용이 크다
- CPU level에서 context switching 이 더 자주 일어나게 되면서, 성능 저하가 발생한다.
- limit 을 걸어놓지 않으면 쓰레드가 무한정 생성되고, 물리적 성능 한계를 넘어서면 서버가 다운될 수 있다.
그래서 Tomcat에서는 Tread Pool 이라는 것을 관리한다.
Thread Pool 의 동작 과정은 다음과 같다.
- 일정 개수의 Thread를 미리 열어놓는다.
- 새로운 요청이 들어오면, 비어있는 Thread를 할당한다.
- 서블릿 객체를 통해 HttpRequest와 HttpResponse를 생성한다.
- HttpRequst에 담긴 내용을 바탕으로, 작성된 로직을 수행한다
- 로직 수행 후, HttpResponse에 담고, 요청에 대한 응답을 보낸다.
- Thread를 반납하고, 해당 Thread는 다음 요청을 기다린다.
그렇다면, 할당되어 있는 Thread를 모두 사용하면 어떤 일이 생길까?
이는 개발자가 다음과 같은 전략(strategy)을 선택할 수 있다.
- 요청을 queue에 담아놓고 대기
- 요청 거절
- Tread pool 초기화
1. 실제 서버 성능 테스트하기 - 환경 구축
@Controller
public class ThreadTest {
//아무 로직 없는 테스트
@GetMapping("/nothing")
public ResponseEntity<Void> nothingController() {
return ResponseEntity.status(HttpStatus.OK).build();
}
//3초짜리 테스트
@GetMapping("/threeSeconds")
public ResponseEntity<Void> threeSecondsController() throws InterruptedException {
Thread.sleep(3000);
return ResponseEntity.status(HttpStatus.OK).build();
}
}
spring에서 간단한 api를 작성하자.
/nothing : 아무 로직 없이 200 응답을 주는 API
/threeSeconds : 요청 하나 처리에 3초가 걸리는 API
아파치에서 제공하는 AB 툴을 사용해서 테스트 해보자.
.\ab.exe -n <테스트요청개수> -c <동시요청개수> http://localhost:8080/test
<option>
-n 10000 : 테스트를 위해 10000개의 요청을 보낸다.
-c 300 : 동시에 300개의 요청을 던진다. (기본 값은 하나만 던짐)
3. 실제 서버 성능 테스트하기 - 요청 처리에 3초가 걸리는 API
@GetMapping("/threeSeconds")
public ResponseEntity<Void> threeSecondsController() throws InterruptedException {
Thread.sleep(3000);
return ResponseEntity.status(HttpStatus.OK).build();
}
요청 하나 처리에 3초가 걸리는 API를 테스트 해보자.
CASE 1
먼저 쓰레드를 하나만 열도록 설정해서, 정말 한 번에 하나의 작업만 처리하는지 확인해보자.
- Thread Pool 설정 1개 (싱글스레드)
- 총 요청 10개
- 동시 요청 10개
Result : 30.2 sec
실제로 작업 하나를 3초간 처리했기에, 총 30초가 걸린 모습이다.
이제, 톰캣 기본 설정인 200개를 세팅해서 어떤식으로 데이터가 나오는지 살펴보자
CASE 2
- Thread Pool 설정 200개 (default)
- 총 요청 1000개
- 동시 요청 1000개
Result : 6 sec
와우! 아주 정확하고 이상적인 결과가 나왔다.
200개씩 3초마다 처리 가능한 곳에, 1000개를 동시에 두 번 밀어넣었으니, 15초가 나왔다.
물론 로컬이 아니라, 실제 환경이라면 더 오래 걸릴 것이다.
그렇다면, 여기에서도 Thread Pool을 1000으로 늘린다면, 성능이 좋아질 것이다.
테스트해보자.
CASE 3
- Thread Pool 설정 200개 (default)
- 총 요청 1000개
- 동시 요청 1000개
Result : 3.6 sec
Thread 개수를 200개에서 1000개로 늘렸을 뿐인데, 15초가 걸리던 요청이 3초대로 내려왔다.
3. 실제 서버 성능 테스트하기 - 아무 로직 없는 API
@GetMapping("/nothing")
public ResponseEntity<Void> nothingController() {
return ResponseEntity.status(HttpStatus.OK).build();
}
위와 같이 아무 로직이 없는 정말 단순한 API를 구성해봤다.
쓰레드가 많으면 많을수록 성능이 좋을지에 대해서 한 번 살펴보자.
CASE 4
- Thread Pool 설정 200개 (default)
- 총 요청 15000개
- 동시 요청 15000개
Result : 평균 3.3 sec
1.5만개의 요청을 모두 동시에 보냈는데, 3.3초밖에 걸리지 않았다.
그렇다면 톰캣 설정을 바꿔서, 쓰레드 풀의 최대치를 높이면, 더 단축되지 않을까?
CASE 5
- Thread Pool 설정 500개
- 총 요청 15000개
- 동시 요청 15000개
Applicaiton.yml
server:
tomcat:
threads:
max: 500 //기본값 200
Result : 평균 3.2 sec
500개로 늘려서 테스트를 진행했다.
생각보다 성능이 많이 개선되지는 않았지만, 확실히 시간이 단축되었음은 확인했다.
아마 너무 간단하고 빠르게 끝나는 로직이라서 효과는 미미한 것 같다.
그렇다면 쓰레드 개수를 좀 더 늘려서, 1000개로 테스트를 해보자.
CASE 6
- Thread Pool 설정 1000개
- 총 요청 15000개
- 동시 요청 15000개
Result : 평균 3.5 sec
엇. 이상하다. 쓰레드 개수를 분명 몇배로 늘렸는데, 성능이 개선되기는 커녕 악화되버렸다.
앞에서 말했듯이, 쓰레드가 너무 많으면 cpu context switching이 자주 일어나서, 성능이 오히려 악화될 수 있다.
결론
실제로 Tomcat에서 Thread Pool 의 size를 조절해보면서 성능 테스트를 어떻게 하는지 살펴봤다.
물론 PC 사양에 따라 천차 만별이겠지만, 다음과 같은 결과를 얻을 수 있었다.
- 쓰레드를 너무 적게 열면, 동시 처리 가능한 요청이 적어져서, 성능이 악화된다.
- 쓰레드를 너무 지나치게 많이 열면, context switching 때문에 성능이 오히려 악화된다.
- 서버 성능과, 해당 API 서버가 처리하는 로직의 복잡도에 따라서 적절한 Thread Pool 설정이 필요하다.