쿠폰 발급의 경우 두가지 요건을 충족해야 한다.
이 문제를 해결하기 위해 성능 테스트를 진행했다.
CPU
: i5-10400 (6Core, 12Threads)RAM
: 32GBDISK
: 1.5TB (500GB M.2, 1TB M.2)Master
: 3개 (docker swarm master)Worker
: 2개 (docker swarm worker)CPU
: 2RAM
: 4GBDISK
: 50GBNETWORK
: Docker Network다음은 서버 인스턴스 현황이다.
MySQL
: 1개 (8.0.23
, master)Redis
: 1개 (7.2.5
, master)JVM
: 2개 (jshop-0.0.7-beta
)nGrinder/controller
: 1개 (3.5.9-p1
)nGrinder/agent
: 3개 (3.5.9-p1
)user
: 100만
명coupon
: 수량 100,000
개
결과를 보면 한번에 모든 vuser 가 동시 요청하자 일부 유저는 쿠폰을 발급받지만 대부분의 유저는 발급받지 못하고 오류를 뿜는다.
내가 원하는 사양은 300명
의 유저가 동시에 쿠폰 발급 요청을 하더라도, 정상적으로 수행할 수 있기를 바란다.
서버측 모니터링 결과를 보면, 요청에 대한 예외는 없었다. 하지만 요청 자체를 받지 못한것으로 추정되는 상황은 있다.
빨간색 부분은 인스턴스로부터 응답을 받지 못하는 상황으로, 인스턴스에 문제가 생긴 상황이다. 이때는 데이터 수집이 되질 않는다.
즉 서버가 처리할 수 있는 한도 이상으로 요청이 들어와 스프링 부트 서버가 뻗어버린 것으로 예상된다.
서버가 처리할 수 있는 한계를 파악해보기로 했다.
vuser 를 점점 늘려가며 부하 테스트를 진행했다.
vuser
를 30명씩 늘릴때, 60 까진 가
서버에서 동시에 처리할 수 있는 요청의 수를 넘어선 요청이 들어오면 큐에 들어가게 된다.
또한 이 큐의 용량도 넘어서게 되면 예외가 발생하게 된다.
스프링 부트의 기본 설정은 다음과 같다.
server:
tomcat:
threads:
max: 200 // 동시 처리 스레드 수
max-queue-capacity: 2147483647 // 스레드풀의 내부 작업 큐
min-spare: 10 // 대기시 최소 스레드 수
accept-count: 100 // 요청 대기열의 크기
https://docs.spring.io/spring-boot/appendix/application-properties/index.html
현재는 많은 요청이 들어와, 요청 대기열이 터져 이후 들어오는 모든 요청이 처리되지 못하고 예외를 반환한것 같다.
요청 대기열의 크기를 1000
까지 늘려서 확인해보자.
server:
tomcat:
accept-count: 1000 // 요청 대기열의 크기
그래도 여전히 오류가 발생하는것을 확인할 수 있다.
Rate Limiting 을 사용해 트래픽을 제어하는 방법이다.
현재 리버스 프록시로 Nginx를 로드밸런서로 사용하고 있기에, 여기에 Rate Limiting 을 적용하려 한다.
worker_processes 1;
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=myzone:10m rate=50r/s;
upstream backend_servers {
server 192.168.123.103:8000;
server 192.168.123.201:8000;
}
server {
listen 8000;
server_name localhost;
location / {
limit_req zone=myzone burst=1000;
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
limit_req_zone $binary_remote_addr zone=myzone:10m rate=100r/s;
myzone
이며 메모리는 10MB
를 사용한다.limit_req zone=myzone burst=10000;
myzone
설정을 기반으로 Rate Limiting
을 적용한다. 1,000개
를 대기열에 넣는다.Rate Limiting
으로 초당 50개의 요청만 리버스 프록시로 포워딩 하고, 나머지 요청에 대해서는 대기를 시킨다.
Spring Application이 처리할 수 있는 양만 보내기 때문에 예외는 발생하지 않는다.
또한 모든 요청들이 Nginx 큐에서 대기하기 떄문에 일정한 대기시간을 갖게 된다.
원하는 사양이였던 동시 300명의 유저가 요청을 할때 어떻게 동작하는지 알아보기 위해 테스트를 진행해봤다.
정확하게 10000 개의 쿠폰이 발급된것을 확인할 수 있다.
Spring Application으로 초당 50건 이상으로 무리한 요청이 가지 않은 덕분에 모든 요청을 처리할 수 있었다.
대신 Nginx에서 대기하느라 요청 응답시간이 약 6초정도 생기게 되었다.
대신에 많은 사용자가 한번에 요청을 하더라도 처리할 수 있게 되었다.
제한된 성능 (2vCPU, 2GB Ram) 의 테스트 환경에서 쿠폰 발급을 테스트 해봤다.
쿠폰 발급의 경우 동시성 문제를 해결해야 하다보니 분산 처리를 하지 못하고 하나의 자원에서 처리를 진행해야 한다.
나의 경우 Redis 분산락을 사용해 MySQL의 접근을 제어해 동시성 문제를 해결했다.
하지만 이러한 방식을 사용하다 보니, 처리속도 이상으로 들어오는 트래픽에 대해 제어를 하지 못하고 오류가 발생했다.
이 문제를 해결하기 위해 tomcat 설정을 건드려 보았지만 큰 효과는 없었다.
대신 외부 리버스 프록시로 동작하는 Nginx에서 Rate Limiting
을 적용하니, Spring 에서도 큰 무리없이 처리할 수 있었다.
다만 이경우 요청이 Nginx 에서 대기해야 하다보니 어느정도 지연이 발생하게 된다.