백엔드 성능 개선 리포트 (WAS Scale-out + DB Replication)
업데이트:
목푯값 설정 계산 예시
- 우선 예상 1일 사용자 수(DAU)를 정해 본다. : 6,000명
- 피크 시간대의 집중률을 예상해 본다. (최대 트래픽 / 평소 트래픽) : 200GB / 22.5GB = 8.889
- 1명당 1일 평균 접속 혹은 요청 수를 예상해 본다. : 50번
- 이를 바탕으로 Throughput을 계산한다.
-
Throughput : 1일 평균 rps ~ 1일 최대 rps
- 1일 사용자 수(DAU) x 1명당 1일 평균 접속 수 = 1일 총접속 수
- 6,000 x 50 = 300,000
- 1일 총접속 수 / 86,400 (초/일) = 1일 평균 rps
- 300,000 / 86,400 = 3.472
- 1일 평균 rps x (최대 트래픽 / 평소 트래픽) = 1일 최대 rps
- 3.472 x 8.889 = 30.863
- 1일 사용자 수(DAU) x 1명당 1일 평균 접속 수 = 1일 총접속 수
-
Latency : 일반적으로 50 ~ 100ms 이하로 잡는 것이 좋다.
- 50ms
- 사용자가 검색하는 데이터의 양, 갱신하는 데이터의 양 등을 파악해 둔다.
-
Throughput : 1일 평균 rps ~ 1일 최대 rps
트래픽(일)이란?
인터넷 사용자에게 사이트의 정보를 노출 시킬 수 있는 하루 동안의 용량을 말한다.
- 하루 동안 10M짜리 동영상이 10번 노출되면 트래픽(일)은 100M(10M*10)
일 트래픽 계산 방법
방문 수 * 평균 페이지뷰 * 1페이지당 평균 바이트
- 방문 수 : 하루 동안 사이트를 방문한 횟수
- 300,000회
- 1,000,000회
- 평균 페이지뷰 : 방문한 사람에게 노출되는 평균 페이지 수
- 50개
- 100개
- 1페이지당 평균 바이트 : 노출되는 페이지의 평균 용량
- 1.5KB
- 2KB
VUser 구하기
- Request Rate: measured by the number of requests per second (RPS)
- VU: the number of virtual users
- R: the number of requests per VU iteration : 12
- T: a value larger than the time needed to complete a VU iteration : 1
T = (R * http_req_duration) (+ 1s) ; 내부망에서 테스트할 경우 예상 latency를 추가한다.
VUser = (목표 rps * T) / R
- 예를 들어, 두 개의 요청 (R=2)이 있고 왕복시간이 0.5s, 지연시간이 1초라고 가정할 때 (T=2) 계산식은 아래와 같다.
ex) VU = (300 * 2) / 2 = 300
T = (12 * 1) = 12
평균 VU = (3.472 * 12) / 12 = 3.472 -> 4
최대 VU = (30 * 12) / 12 = 30.863 -> 31
VUser가 100명, 500명일 때 각각 트래픽 계산
R = 11 (아래의 smoke.js 파일 등에 있는 VU iteration 당 요청 수가 총 11개이다.)
T = (11 * 1) = 11
VUser = (목표 rps * 11) / 11
VUser = 목표 rps = 1일 최대 rps
VUser = 100
100 = 목표 rps
1일 평균 rps * 8.889 = 100
1일 평균 rps = 11.25
1일 총접속 수 / 86,400 (초/일) = 11.25
1일 총접속 수 = 972,000
1일 사용자 수 * 30 = 972,000
1일 사용자 수 = 32,400명
VUser = 500
500 = 목표 rps
1일 평균 rps * 8.889 = 500
1일 평균 rps = 56.249
1일 총접속 수 / 86,400 (초/일) = 56.249
1일 총접속 수 = 4,859,913
1일 사용자 수 * 30 = 4,859,913
1일 사용자 수 = 161,997명
테스트 기록
- Thread Pool 설정
- Nginx 설정
- 비동기 로깅 적용
- Nginx -> ELB 교체
- 스케일 아웃, 로드밸런싱 : WAS를 5개로 늘림.
- DB Replication 적용 (Master 1대 / Slave 4대)
Smoke Test
- 최소한의 부하로 구성된 테스트로, 테스트 시나리오에 오류가 없는지 확인할 수 있다.
- 최소 부하 상태에서 시스템에 오류가 발생하지 않는지 확인할 수 있다.
- VUser를 1 ~ 2로 구성하여 테스트한다.
smoke.js
import http from 'k6/http';
import {check, sleep} from 'k6';
export let options = {
stages: [
{duration: '1m', target: 1}, // ${duration} 동안 현재 사용자를 ${target}명으로 유지한다.
],
thresholds: {
http_req_duration: ['p(99)<50'], // 99% 개의 요청이 50ms 미만으로 완료되어야 한다.
},
};
function getPage(APIS) {
for (const api of APIS) {
const response = http.get(api);
check(response, {
'response code was 200': (response) => response.status == 200,
});
sleep(4);
}
}
export default function () {
// 메인 페이지 접속
// 인기글 조회
const GET_PAGE_APIS = [
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=20&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/12461',
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/18553',
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=20&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/10847'
];
getPage(GET_PAGE_APIS);
};
개선 전 : AWS ELB + WAS 총 1개
계획
- 30초 동안 VUser 0명 → 1명 증가.
- 1분 동안 VUser 1명 유지.
- 30초 동안 VUser 1명 → 0명 감소.
- 99%의 요청이 각각 50ms 미만으로 완료되어야 한다.
결과
- 모든 요청 성공.
-
95%의 요청 응답시간
173ms
.
로드밸런싱 적용 (WAS 총 5개)
-
95%의 요청 응답시간
252ms
로 응답 속도 느려짐.- 추측 : 1개의 WAS일 때는 애플리케이션 단에서 트랜잭션 관리를 어느 정도 해 주는데, 로드밸런싱을 적용하니 별도의 WAS 5개가 하나의 DB를 동시에 찔러서 과부하가 걸리나?
DB Replication 적용 (Master 1대 / Slave 4대)
-
95%의 요청 응답시간
176ms
로 원상복구 됨.
최종 결과
- 성능 동일.
Load Test
테스트 기간
- 일반적으로 Load Test는 보통 30분 ~ 2시간 사이로 권장한다.
- 부하가 주어진 상황에서 DB Failover, 배포 등 여러 상황을 부여하며 서비스의 성능을 확인한다.
- 애플리케이션 배포 및 인프라 변경(scale out, DB failover 등) 시에 성능 변화를 확인한다.
- 외부 요인(결제 등)에 따른 예외 상황을 확인한다.
- 말 그대로 시스템이 얼마만큼의 부하를 견뎌낼 수 있는가에 대한 테스트를 말한다.
- 보통 Ramp up 이라 하여 낮은 수준의 부하부터 높은 수준의 부하까지 예상 트래픽을 꾸준히 증가시키며 진행하는 테스트로, 한계점의 측정이 관건이며, 그 임계치를 높이는 것이 목적이라 할 수 있다.
- 일반적으로 “동시접속자 수” 와 그 정도의 부하에 대한 Response Time으로 테스트를 측정한다.
- 예를 들어 1분동 안 10만 명의 동시접속을 처리할 수 있는 시스템을 만들 수 있는지가 테스트의 주요 관건이 된다.
load.js
import http from 'k6/http';
import {check, sleep} from 'k6';
export let options = {
stages: [
{duration: '1m', target: 100}, // ${duration} 동안 현재 사용자를 ${target}명으로 유지한다.
],
thresholds: {
http_req_duration: ['p(99)<50'], // 99% 의 요청이 50ms 미만으로 완료되어야 한다.
},
};
function getPage(APIS) {
for (const api of APIS) {
const response = http.get(api);
check(response, {
'response code was 200': (response) => response.status == 200,
});
sleep(4);
}
}
export default function () {
// 메인 페이지 접속
// 인기글 조회
const GET_PAGE_APIS = [
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=20&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/12461',
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/18553',
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=20&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/10847'
];
getPage(GET_PAGE_APIS);
};
개선 전 : AWS ELB + WAS 총 1개
계획 (VUser = 100 = 1일 사용자 약 32,000명)
- 30초 동안 VUser 0명 → 100명 증가.
- 1분 동안 VUser 100명 유지.
- 30초 동안 VUser 100명 → 0명 감소.
- 99%의 요청이 각각 50ms 미만으로 완료되어야 한다.
결과
- 모든 요청 성공.
-
95%의 요청 응답시간
1.86s
.
로드밸런싱 적용 (WAS 총 5개)
-
95%의 요청 응답시간
2.61s
로 응답 속도 느려짐.- 추측 : 1개의 WAS일 때는 애플리케이션 단에서 트랜잭션 관리를 어느 정도 해 주는데, 로드밸런싱을 적용하니 별도의 WAS 5개가 하나의 DB를 동시에 찔러서 과부하가 걸리나?
DB Replication 적용 (Master 1대 / Slave 4대)
-
95%의 요청 응답시간
180.73ms
로,Smoke Test
만큼 빨라짐.- 맨 처음보다 속도
약 10배
배 빨라짐.
- 맨 처음보다 속도
최종 결과
- 속도
약 10배
빨라짐.
Stress Test
- 서비스가 극한의 상황에서 어떻게 동작하는지 확인한다.
- 장기간 부하 발생에 대한 한계치를 확인하고 기능이 정상 동작하는지 확인한다.
- 최대 사용자 또는 최대 처리량을 확인한다.
- 스트레스 테스트 이후 시스템이 수동 개입 없이 복구되는지 확인한다.
stress.js
import http from 'k6/http';
import {check, sleep} from 'k6';
export let options = {
stages: [
{duration: '1m', target: 500}, // ${duration} 동안 현재 사용자를 ${target}명으로 늘린다.
],
thresholds: {
http_req_duration: ['p(99)<50'], // 99% 의 요청이 50ms 미만으로 완료되어야 한다.
},
};
function getPage(APIS) {
for (const api of APIS) {
const response = http.get(api);
check(response, {
'response code was 200': (response) => response.status == 200,
});
sleep(4);
}
}
export default function () {
// 메인 페이지 접속
// 인기글 조회
const GET_PAGE_APIS = [
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=20&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/12461',
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/18553',
'{접속 URL}/api/v1/posts/paging?offset=0&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=10&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/paging?offset=20&size=10&sort=LIKE_COUNT_DESC',
'{접속 URL}/api/v1/posts/10847'
];
getPage(GET_PAGE_APIS);
};
개선 전 : AWS ELB + WAS 총 1개
계획 (VUser = 500 = 1일 사용자 약 162,000명)
- 30초 동안 VUser 0명 → 500명 증가.
- 1분 동안 VUser 500명 유지.
- 30초 동안 VUser 500명 → 0명 감소.
- 99% 의 요청이 각각 50ms 미만으로 완료되어야 한다.
결과
- 58%의 요청 처리 실패.
-
95%의 요청 응답시간
17.24s
.
로드밸런싱 적용 (WAS 총 5개)
- 모든 요청 처리 성공.
-
95%의 요청 응답시간
29s
로 응답 속도 느려짐.- 추측 : 1개의 WAS일 때는 애플리케이션 단에서 트랜잭션 관리를 어느 정도 해 주는데, 로드밸런싱을 적용하니 별도의 WAS 5개가 하나의 DB를 동시에 찔러서, 과부하가 걸리나?
DB Replication 적용 (Master 1대 / Slave 4대)
-
95%의 요청 응답시간
4.88s
로 속도 빨라짐.- 맨 처음보다 속도가
약 3.5배
빨라짐.
- 맨 처음보다 속도가
최종 결과
-
58%
의 요청 처리 실패 -> 모든 요청 처리 성공. - 속도
약 3.5배
배 빨라짐.
최종 결과 요약 [WAS Scale-out(WAS 총 5개) + DB Replication(DB 총 4개)]
- Smoke Test
- 성능 동일
- Load Test
- 속도
약 10배
빨라짐.
- 속도
- Stress Test
-
58%
의 요청 처리 실패 -> 모든 요청 처리 성공. - 속도
약 3.5배
빨라짐.
-
참고자료
- 동시 초당 접속자 - RPS(Request Per Second)란?
- load testing 과 stress testing, performance testing 에 대한 비교
- “Too many open files” Error는 어떻게?
- [Logging] Logback을 이용해 logging하기
SQL INSERT 반복문
drop procedure if exists myFunction;
DELIMITER $$
CREATE PROCEDURE myFunction() -- myFunction이라는 이름의 프로시져
BEGIN
DECLARE i INT DEFAULT 1; -- i변수 선언, defalt 값으로 1 설정
DECLARE min_user_id INT DEFAULT 8002;
DECLARE max_user_id INT DEFAULT 16002;
DECLARE min_post_id INT DEFAULT 10001;
DECLARE max_post_id INT DEFAULT 20011;
WHILE (i <= 48000) DO -- for문 작성(i가 48,000 이하인 동안 반복)
insert ignore
into
likes
(likes_id, created_at, last_modified_at, post_id, user_id)
values
(null, now(), now(), FLOOR(RAND() * (max_post_id - min_post_id + 1)) + min_post_id, FLOOR(RAND() * (max_user_id - min_user_id + 1)) + min_user_id);
SET i = i + 1; -- i에 1 더하고 WHILE문 처음으로 이동
END WHILE;
END$$
DELIMITER ; -- 구분 기호를 다시 ;로 바꿔주기
CALL myFunction;
-- FLOOR(RAND() * (max_post_id - min_post_id + 1)) + min_post_id
댓글남기기