-
Notifications
You must be signed in to change notification settings - Fork 0
배포 환경에서의 선착순 이벤트 성능 모니터링
SEUNGUN CHAE edited this page Aug 28, 2024
·
14 revisions
팀 어썸오렌지는 다양한 구현방안 분석 결과를 바탕으로 "Redis"와 "LuaScript"를 결합하여 선착순 이벤트 참여 기능을 구현하는 방안을 채택했습니다.
이는 다양한 방안에 대한 실험 결과 동시성 문제를 방지하면서도 가장 응답 시간 및 처리량이 우수했기 때문입니다.
또한 17시마다 선착순 이벤트가 시작된다는 요구사항에 따라, 16시가 되면 오토 스케일링을 통해 3개의 인스턴스를 추가로 생성하여, 순간적으로 폭등할 선착순 이벤트 관련 트래픽을 로드 밸런싱을 통해 여러 인스턴스로 분산하는 환경을 구축했습니다.
로드 밸런싱 알고리즘은 "라운드 로빈"을 채택했습니다.
이는 오토 스케일링을 통해 태어나는 추가 인스턴스가 동일한 성능(t3.small)과 설정(공통된 시작 템플릿)을 갖고 있기에 균등한 트래픽 분배가 적합하기 때문입니다.
이제 팀 어썸오렌지가 배포 환경의 Springboot 서버의 선착순 이벤트 기능을 테스트한 결과에 대해 설명드리겠습니다.
- 배포 환경(EC2 t3.small)은 로컬(M1 Pro)보다 훨씬 서버 스펙이 낮습니다. (RAM 2GB, Disk 8GB)
- 또한, Apache JMeter로 Spike Test를 실행하던 도중 소프티어 부트캠프 교육장 WiFi와 자택 WiFi, 그리고 5G 등 Spike Test를 실행하는 네트워크 환경에 따라 처리량과 응답 시간의 편차가 굉장히 크다는 것이 확인되었습니다.
- 위와 같이 네트워크 환경으로 인한 편차를 최소화하기 위해 테스트 실행 전용 EC2 인스턴스를 생성하여 해당 인스턴스에서 테스트를 실행하는 계획을 수립했습니다.
- 그러나, 테스트용 EC2 역시 t3.small이었기에 RAM의 크기가 2GB로 작았고, Jmeter를 실행하려면 1GB 이상의 Heap Memory가 요구되는 상황이었습니다.
- 실제로 Jmeter를 통해 EC2 안에서 Spike Test 실행 시 CPU 사용이 90%를 초과했다는 경고 메시지와 함께 인스턴스와의 연결이 끊어지고 있었습니다.
- 따라서 로컬과 달리 테스트용 EC2에서는 Jmeter 대신 더 가벼운 javascript 기반의 k6을 활용하여, 메모리와 CPU를 절약하며 Spike Test를 실행하고 기록했습니다.
- 팀 어썸오렌지의 선착순 이벤트는 일정 시각이 되면 활성화되는 4가지 카드 중 정답 카드를 뒤집는 형태이며, 정답 카드를 먼저 뒤집은 순서대로 당첨 여부를 판정하고 있습니다.
- 하나의 카드를 뒤집는 데 걸리는 시간은 약 0.5초입니다.
- 즉 클라이언트는 "특정 시각에", "최대 4장의 카드를", "0.5초 간격으로 선택하는" 요청을 전송하는 것입니다.
- 즉 서버 입장에서 선착순 이벤트의 동시 사용자 수가 1000명이라면 2초 전후로 최대 4000번의 요청을 소화하는 것입니다.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
import { Rate } from 'k6/metrics';
// Custom metric to track failures
export const failureRate = new Rate('failed_requests');
// Load JWT tokens from CSV file
const jwtTokens = new SharedArray('JWT Tokens', function () {
return open('./user_tokens.csv')
.split('\n')
.slice(1) // Skip the header row
.map(token => token.trim())
.filter(token => token.length > 0);
});
export let options = {
vus: 1000, // 동시 사용자 수
duration: '10s', // 테스트 수행 시간
};
function getRandomUniqueNumber(usedNumbers) {
const availableNumbers = [1, 2, 3, 4].filter(num => !usedNumbers.includes(num));
const randomIndex = Math.floor(Math.random() * availableNumbers.length);
return availableNumbers[randomIndex];
}
export default function () {
const url = '${SERVER_URL}';
const token = jwtTokens[__VU % jwtTokens.length]; // Assign token based on virtual user ID
const headers = {
'Authorization': `${token}`,
'Content-Type': 'application/json'
};
let usedNumbers = [];
// 4번의 요청을 보내며, 각 요청에서 중복되지 않는 숫자를 선택합니다
for (let i = 0; i < 4; i++) {
const selectedNumber = getRandomUniqueNumber(usedNumbers);
usedNumbers.push(selectedNumber); // 사용된 숫자를 기록합니다
const payload = JSON.stringify({
answer: selectedNumber.toString()
});
let res = http.post(url, payload, { headers: headers });
// Validate response
check(res, {
'status is 200': (r) => r.status === 200
}) || failureRate.add(1);
sleep(0.5); // 0.5초 간격으로 카드 뒤집기
}
}
- k6을 활용한 Spike Test 역시 해당 요구사항을 반영하여 작성했으며, 테스트 결과와 실제 서비스가 이질감이 없도록 유저에게 발급된 JWT 토큰을 헤더에 실어 요청을 보내도록 했습니다.
- 이제 작성한 k6.js와 미리 발급해둔 테스트용 JWT 토큰을 EC2 인스턴스에 옮기고 k6을 설치해준 후 "k6 run k6.js"로 테스트를 실행할 수 있습니다.
- 동시 사용자 수를 조절하며 HTTP 요청 관련 데이터를 모니터링하였습니다.
- k6의 실행 결과로부터 요청 처리 관련 지표(metric)를 선정했습니다.
- http_req_blocked: 요청을 시작하기 전에 블락된(TCP 연결 슬롯을 기다리는) 시간
- http_req_connecting: 서버에 TCP 연결을 설정하는 데 걸린 시간
- http_req_duration: 요청에 걸린 총 시간 (보통 http_req_sending+http_req_waiting+http_req_receiving)
- http_req_receiving: 서버로부터 응답 데이터를 받는 데 걸린 시간
- http_req_sending: 서버에 데이터를 전송하는 데 걸린 시간
- http_req_waiting: 서버의 응답을 기다리는 데 걸린 시간
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 5.74ms | 1.36µs | 3.06µs | 175.15ms | 6.88µs | 49.34ms |
http_req_connecting | 1.11ms | 0s | 0s | 26.61ms | 0s | 14.65ms |
http_req_duration | 25.77ms | 2.28ms | 11.06ms | 366.16ms | 39.21ms | 103.97ms |
http_req_receiving | 189.87µs | 13.21µs | 56.23µs | 27.97ms | 314.02µs | 542.48µs |
http_req_sending | 560.47µs | 6.5µs | 20.79µs | 34.12ms | 1.13ms | 2.95ms |
http_req_waiting | 25.02ms | 2.19ms | 10.68ms | 360.41ms | 36.81ms | 100.43ms |
iteration_duration | 2.13s | 2.01s | 2.06s | 2.61s | 2.36s | 2.43s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 12.45ms | 1.24µs | 3.15µs | 440.81ms | 11.04µs | 79ms |
http_req_connecting | 9.95ms | 0s | 0s | 303.04ms | 0s | 66.06ms |
http_req_duration | 144.1ms | 2.55ms | 121.32ms | 674.35ms | 283.89ms | 352.33ms |
http_req_receiving | 425.58µs | 13.18µs | 52.54µs | 107.85ms | 378.1µs | 828.43µs |
http_req_sending | 1.35ms | 6.71µs | 21.32µs | 91.48ms | 2.61ms | 6.14ms |
http_req_waiting | 142.33ms | 2.45ms | 120.03ms | 671.71ms | 280.94ms | 347.63ms |
iteration_duration | 2.63s | 2.19s | 2.59s | 3.68s | 2.99s | 3.08s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 26.97ms | 1.36µs | 3.12µs | 654.93ms | 10.03µs | 322.55ms |
http_req_connecting | 13.52ms | 0s | 0s | 437.76ms | 0s | 125.15ms |
http_req_duration | 327.11ms | 2.83ms | 282.1ms | 1.3s | 560.3ms | 657.73ms |
http_req_receiving | 495.8µs | 14.1µs | 49.71µs | 163.97ms | 355.66µs | 845.63µs |
http_req_sending | 6.24ms | 7.79µs | 21.62µs | 229.67ms | 13.89ms | 40.83ms |
http_req_waiting | 320.36ms | 2.67ms | 273.04ms | 1.27s | 552.93ms | 652.76ms |
iteration_duration | 3.44s | 2.67s | 3.38s | 4.77s | 3.89s | 4.02s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 28.11ms | 1.29µs | 3.11µs | 786.17ms | 10.7µs | 220.24ms |
http_req_connecting | 4.13ms | 0s | 0s | 86.75ms | 0s | 58.72ms |
http_req_duration | 484.02ms | 3.84ms | 455.58ms | 1.62s | 697.1ms | 806.79ms |
http_req_receiving | 926.56µs | 13.06µs | 51.03µs | 150.67ms | 398.07µs | 1.17ms |
http_req_sending | 2.75ms | 6.87µs | 22.3µs | 203.26ms | 5.1ms | 15.87ms |
http_req_waiting | 480.34ms | 3.72ms | 451.4ms | 1.62s | 693.97ms | 795.87ms |
iteration_duration | 4.06s | 3.21s | 3.96s | 5.28s | 4.7s | 4.82s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 8.84ms | 1.44µs | 3.15µs | 587.08ms | 9.86µs | 1.15ms |
http_req_connecting | 1.95ms | 0s | 0s | 122.94ms | 0s | 1.07ms |
http_req_duration | 795.81ms | 20.11ms | 790.66ms | 1.84s | 1.01s | 1.11s |
http_req_receiving | 620.6µs | 12.14µs | 53.86µs | 137.65ms | 379.77µs | 865.75µs |
http_req_sending | 2.7ms | 6.96µs | 21.03µs | 233.07ms | 1.74ms | 9.31ms |
http_req_waiting | 792.48ms | 20.06ms | 789.3ms | 1.84s | 1s | 1.1s |
iteration_duration | 5.23s | 4.01s | 5.21s | 6.39s | 5.51s | 5.76s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 44.59ms | 1.42µs | 3.11µs | 1.49s | 9.66µs | 591.25ms |
http_req_connecting | 40.31ms | 0s | 0s | 1.21s | 0s | 586.03ms |
http_req_duration | 777.91ms | 2.87ms | 793.78ms | 1.85s | 1.03s | 1.2s |
http_req_receiving | 483.6µs | 12.85µs | 47.01µs | 798.83ms | 356.12µs | 764.15µs |
http_req_sending | 14.66ms | 6.72µs | 20.53µs | 743.73ms | 19.36ms | 95.83ms |
http_req_waiting | 762.76ms | 2.57ms | 777.02ms | 1.68s | 999.82ms | 1.18s |
iteration_duration | 5.36s | 3.8s | 5.16s | 1m0s | 6.38s | 6.99s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 6.31ms | 1.35µs | 3.38µs | 192.79ms | 9µs | 53.91ms |
http_req_connecting | 1.36ms | 0s | 0s | 34.63ms | 0s | 16.99ms |
http_req_duration | 83.46ms | 2.36ms | 7.88ms | 2.54s | 82.73ms | 565.11ms |
http_req_receiving | 421.55µs | 13.61µs | 178.45µs | 50.79ms | 623.17µs | 1.1ms |
http_req_sending | 315.14µs | 7.46µs | 21.53µs | 11.27ms | 340.89µs | 2.75ms |
http_req_waiting | 82.72ms | 2.23ms | 7.45ms | 2.53s | 81.32ms | 562.15ms |
iteration_duration | 2.36s | 2.01s | 2.04s | 4.7s | 3.39s | 3.8s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 11.32ms | 1.33µs | 2.88µs | 354.9ms | 9.89µs | 82.44ms |
http_req_connecting | 2.1ms | 0s | 0s | 46.96ms | 0s | 29.87ms |
http_req_duration | 50.22ms | 2.07ms | 26.49ms | 328.82ms | 137.01ms | 176.03ms |
http_req_receiving | 417.15µs | 12.75µs | 44.5µs | 112.63ms | 374.07µs | 858.47µs |
http_req_sending | 2.79ms | 6.51µs | 24.29µs | 113.82ms | 5.39ms | 13.22ms |
http_req_waiting | 47.01ms | 2.02ms | 24.99ms | 325.37ms | 125.59ms | 161.7ms |
iteration_duration | 2.26s | 2.01s | 2.18s | 2.91s | 2.58s | 2.65s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 1.15ms | 1.34µs | 2.85µs | 152.29ms | 9.32µs | 910.2µs |
http_req_connecting | 762.26µs | 0s | 0s | 151.3ms | 0s | 832.89µs |
http_req_duration | 64.17ms | 2.35ms | 39.16ms | 483.29ms | 157.19ms | 211.33ms |
http_req_receiving | 534.22µs | 12.57µs | 37.01µs | 387.82ms | 340.28µs | 733.31µs |
http_req_sending | 5.03ms | 6.72µs | 24.46µs | 347.71ms | 8.41ms | 21.71ms |
http_req_waiting | 58.6ms | 2.29ms | 36.96ms | 387.45ms | 145.45ms | 185.42ms |
iteration_duration | 2.3s | 2.01s | 2.28s | 2.91s | 2.55s | 2.61s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 31.04ms | 1.24µs | 2.86µs | 945.27ms | 10.23µs | 279.43ms |
http_req_connecting | 5.07ms | 0s | 0s | 120.13ms | 0s | 73.34ms |
http_req_duration | 133.57ms | 2.49ms | 88.17ms | 636.6ms | 349.09ms | 425.82ms |
http_req_receiving | 677.8µs | 11.98µs | 30.87µs | 398.54ms | 330.59µs | 665.53µs |
http_req_sending | 15.56ms | 6.77µs | 38.48µs | 494.62ms | 42.41ms | 84.69ms |
http_req_waiting | 117.32ms | 2.32ms | 79.85ms | 623.1ms | 294.11ms | 381.75ms |
iteration_duration | 2.76s | 2.02s | 2.62s | 1m0s | 3.47s | 3.66s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 29.22ms | 1.44µs | 2.88µs | 903ms | 9.96µs | 302.14ms |
http_req_connecting | 5.79ms | 0s | 0s | 132.34ms | 0s | 76.99ms |
http_req_duration | 159.83ms | 2.55ms | 124.79ms | 859.31ms | 338.49ms | 456.49ms |
http_req_receiving | 1.02ms | 12.5µs | 26.79µs | 612.09ms | 251.07µs | 808.24µs |
http_req_sending | 23.13ms | 6.89µs | 163.83µs | 794.46ms | 55.52ms | 88.78ms |
http_req_waiting | 135.67ms | 2.44ms | 112.13ms | 677.87ms | 279.72ms | 375.95ms |
iteration_duration | 2.9s | 2.01s | 2.82s | 4.39s | 3.47s | 3.65s |
Metric | avg | min | med | max | p(90) | p(95) |
---|---|---|---|---|---|---|
http_req_blocked | 44.38ms | 1.18µs | 2.71µs | 1.18s | 9.79µs | 326.11ms |
http_req_connecting | 5.63ms | 0s | 0s | 116.04ms | 0s | 80.67ms |
http_req_duration | 164.56ms | 2.39ms | 99.94ms | 1.13s | 445.08ms | 650.01ms |
http_req_receiving | 913.42µs | 12.05µs | 26.85µs | 949.44ms | 280.59µs | 871.96µs |
http_req_sending | 25.13ms | 6.76µs | 66.09µs | 969ms | 29.36ms | 144.38ms |
http_req_waiting | 138.51ms | 2.28ms | 91.93ms | 1.09s | 361.92ms | 549.32ms |
iteration_duration | 3.07s | 2.02s | 2.95s | 5.01s | 3.92s | 4.11s |
- 주요 지표를 통해 두 환경의 성능을 비교하였습니다.
- 단일 인스턴스는 요청 수에 따라 지연 시간이 늘어나다가 특정 시점부터 60% 이상 폭등하는 경향을 보입니다.
- 반면 분산 환경은 요청 수가 늘어나도 지연 시간이 뚜렷하게 증가하지는 않습니다.
- 단일 인스턴스는 전송 시간이 불규칙한 측면을 보입니다.
- 반면 분산 환경은 뚜렷한 전송 시간 증가가 나타났습니다. 이는 로드밸런싱 과정에서 추가적인 패킷 이동으로 인해 발생하는 추가적인 오버헤드를 원인으로 추론하였습니다.
- 단일 인스턴스는 요청 대기 시간이 뚜렷하게 증가함을 확인할 수 있습니다.
- 반면 분산 환경은 요청 수가 늘어나도 거의 일정한 요청 대기 시간을 제공합니다.
- 요청이 많아질수록 처리량이 서서히 벌어져, 동시 사용자수 3000명 기준 2배 가량의 차이가 나타났습니다.
- 참고로 TPS는 (전체 HTTP 요청 수 / 테스트의 총 시간)으로 계산하였습니다.
- 예약 기반의 오토 스케일링을 통해 3개의 인스턴스를 추가로 활성화하고, 로드 밸런서를 통해 부하를 분산시키는 환경을 조성했습니다.
- 그 결과 단일 인스턴스 대비 최고 부하 기준 2배 가량의 TPS 향상을 이뤄냈고, 평균 요청 지연/대기 시간을 거의 일정하게 유지할 수 있었습니다.
- 그러나 4개의 인스턴스를 운영함에도 기대와 달리 TPS가 4배만큼 증가하지는 않는다는 점에서, 로드 밸런서를 통한 부하 분산 전략이 모든 문제를 해결하는 Silver-Bullet이 아니다라는 결론을 내릴 수 있었습니다.
- 또한 각 노드의 CPU Usage를 조회한 결과 예상과 달리 부하를 정확하게 1/n 비율로 분산하지는 않는 것으로 확인되었습니다. 추가적인 조사가 필요합니다.
- 평이한 성능의 서버를 Scale-Out함으로써 아주 짧은 시간 동안의 강한 부하로 인해 CPU가 100%를 초과하여 발생할 수 있는 서버 내 위험을 방지하여 QoS를 향상시킬 수 있었습니다.