특정 상황이 발생해 현재 운영 중인 서버가 정상 작동이 불가능한 상태.
Lack of Resource : 메모리 누수 및 OOM(Out of Memory Error)로 인한 APP 비정상 종료 Unhandled Exception : 처리되지 않은 Exception으로 인한 APP Cash Slow Query, I/O : 응답 지연으로 이어지는 최적화 되지 않은 작업 이 외에도 예측불가한 상황과 유저의 행동 또는 환경들로 인해 언제든 발생할 수 있는게 장애이다.
장애가 지속 ➡️ 사용자의 불편 초래 ➡️ 서비스 이탈❗로 이루어 지기 때문에 장애를 모두 막을 순 없지만 일어난 장애에 대해 빠르게 대응해야한다.
일전에 갑작스럽게 트래픽이 몰렸을 때를 대비한 작업들(인덱스, 캐시, 카프카 등.. )이 정말 트래픽을 견딜 수 있는지 미리 확인하기 위해서는 부하테스트를 해봐야한다.
1) 예상 TPS(Transaction Per Second)
2) 평균/중간/최대 응답시간
위 사항들을 점검하고 목표치를 달성하지 못 하거나 기대치에 못 미치는 경우 원인을 분석하고 성능 개선을 진행한다.
우리 서비스에서 제공하는 전체 API를 나열해 보고, 각각의 목표 TPS를 대략적으로 작성한다.
목표 TPS를 활용하여 User, Response Time등을 설정해서 시나리오를 만들어 본다.
특수한 트래픽을 처리하기 위한 기능, 동시성 이슈를 고려한 기능 등 어떤 유형의 부하가 주어졌을 때, 기능이 예측과 같이 동작하는지 혹은 너무 낮은 성능을 보이고 있지는 않은지 등을 점검한다.
부하 테스트로 다음의 지표를 수집한다.
이커머스에서 갑자기 트래픽이 몰리는 사례
특정 시간 동안 할인을 제공하거나, 한정된 수량의 상품을 판매하는 이벤트를 진행한다.
인기가 많은 브랜드나 카테고리에서 새롭게 출시된 상품을 구매하려는 고객이 몰림.
ex) 블랙프라이데이, 크리스마스, 설날, 추석 등 쇼핑 수요가 높은 시기.
수량이 제한된 한정판 상품 특정시간에 판매.
특정 상품이 소셜 미디어, 뉴스 등을 통해 갑작스럽게 유명해짐.
장애로 인해 중단된 서비스를 복구한 직후에 요청이 한꺼번에 몰림.
포인트 조회 : 대량 사용자 요청 시 성능 확인.
➡️ 상품 주문 시 현재 가지고 있는 포인트 조회를 하게된다.
1초에 100명의 사용자가 포인트를 조회할 경우.
상품 주문 : 주문 시 동시성 문제 및 재고에 대한 데이터 일관성 확인
➡️100명의 사용자가 동시에 같은 상품을 주문할 경우에 대한 동시성 제어
같은 상품 조회 : 높은 조회 빈도에 대한 캐싱
➡️ 500명이 1초에 10번씩 조회
http_req_failed: 실패한 요청의 수.http_req_waiting: 요청이 성공하기까지 대기한 시간.http_reqs: 전체 요청 수.iteration_duration : 1번의 사이클당 걸린 시간을 의미. p(90), p(95), p(99): 특정 값이 전체 데이터에서 차지하는 위치.import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 100, // 가상 사용자 수 (100명)
duration: '1s', // 테스트 실행 시간
};
export default function () {
// __VU은 현재 Virtual User ID를 나타냄 (1부터 시작)
let userId = __VU;
let url = `http://localhost:8080/customers/${userId}/balance`;
let params = {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
};
// HTTP GET 요청
http.get(url, params);
// 1초 대기 (필요 시 조정)
sleep(1);
}
customerId가 url로 들어가므로 __VU를 이용해서 customerId를 하나씩 늘려주며 요청을 보냈다.

http_req_failed: 0.00%http_req_waiting: 13.8mshttp_reqs: 590iteration_duration : 1.02sp(90), p(95) : http_req_waiting을 보면 95%의 사용자가 15.75ms 이내로 응답을 받은 것을 알 수 있다. import http from 'k6/http';
import { sleep } from 'k6';
import { check } from 'k6';
export const options = {
vus: 100, // 가상 사용자 수
duration: '1s', // 테스트 실행 시간
};
export default function () {
// 1~10000 사이의 랜덤 customerId 생성
const customerId = Math.floor(Math.random() * 10000) + 1;
const url = 'http://localhost:8080/orders';
const payload = JSON.stringify({
customerId: customerId,
orderProducts: [
{
productId: 3,
amount: 2,
},
],
});
const params = {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
};
// POST 요청 실행
const response = http.post(url, payload, params);
// 응답 상태 체크
check(response, {
'is status 200': (r) => r.status === 200,
});
// 1초 대기
sleep(1);
}

상품 3의 재고를 150개로 설정해 두었고, 부하 테스트로 3번 상품을 2개씩 100번 구매요청을 보내므로 25번의 요청은 실패를 할 것으로 예상했다.
그러나,

서버의 부하, API로직 문제, 서버 설정 오류1) 요청 사용자 수를 점진적으로 늘려보기.
2) 부하를 일으키는 로직 개선하기
3) 캐시를 적용할 수 있는 부분들은 캐시를 적용하고 성능 개선하기.
4) cpu 성능 향상시키기
🏷️Trouble Shooting
read:connection reset by peer:소켓 개수의 제한으로 발생하는 문제.

최소 9ms, 최대 548ms의 편차가 발생했지만 네트워크 지연 초기 요청 설정 때문으로 보인다.


점진적 과부화를 줬더니 다시 된다..?
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{ duration: '10s', target: 500 }, // 10초 동안 500명의 사용자로 증가
{ duration: '1m', target: 500 }, // 1분 동안 500명의 사용자 유지
{ duration: '10s', target: 0 }, // 10초 동안 사용자를 0으로 감소
],
thresholds: {
http_req_failed: ['rate<0.01'], // 1% 미만의 요청만 실패해야 함
http_req_duration: ['p(95)<500'], // 95% 요청이 500ms 미만이어야 함
},
};
export default function () {
let url = 'http://localhost:8080/products/10';
let params = {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
};
for (let i = 0; i < 10; i++) {
http.get(url, params);
sleep(1); // 각 요청 사이에 1초의 대기 시간
}
}
500명의 고객이 1초에 10번씩 조회하도록 하는 스크립트이다.
지난번에 인덱스 설정이 잘못되어 성능을 향상시킬 수 없었는데, 이번 부하테스트를 위해선 필연적으로 인덱스를 적용시켜야만 했다.
인덱스를 요리조리 바꿔보다가

product_id와 reg_date의 순서를 변경하고 amount를 추가한 복합인덱스로 변경하였다.

그 결과 내가 의도한 인덱스를 타게되었고 성능또한 향상되었다.

1번 호출 시 213ms가 걸렸고 이제 부하테스트를 해보겠다.

인덱스와 캐싱의 조합은 효과가 대단했다..!
http_req_failed : 0%
p(95) : 47.01ms
정도로 500명의 요청자가 무리없이 50ms 내로 응답을 받았다.




spring.datasource.hikari.maximum-pool-size=20
히카리의 풀 사이즈를 최대로 잡아보았다.


하지만 10000명에서는 조금 더 많은 실패를 보였지만 p(95)의 응답시간은 많이 줄어들었다.
이 방법 말고도 위에서 언급한 로드밸런싱을 통해 요청을 분산시키는 방법으로도 트래픽을 대비할 수 있을 것이다.