-
Notifications
You must be signed in to change notification settings - Fork 2
성능 테스트 ‐ 랭킹 JWT 인덱스 적용 (최종)
min9805 edited this page Aug 27, 2024
·
19 revisions
- 테스트 API 종류
- 상위 20명 랭킹 조회
- 특정 인원 게임 점수 갱신
- 랭킹 조회
- EventUsers 테이블에 GameScore 필드로
Index
가 생성되어있다. SELECT (...) FROM EventUsers ORDER BY game_score DESC LIMIT 20
- 해당 쿼리는 20 개의 row 만 검사하기에 매우 빠른 실행 속도를 가진다.
- EventUsers 테이블에 GameScore 필드로
- 게임 점수 갱신
- EventUser 테이블의 특정 row 의 GameScore 를 갱신한다.
- GameScore 로 index 가 생성되어있기 때문에 update 시 index 재정렬이 필요해 EventUser 테이블이 클 수록 많은 시간이 소요된다.
- 랭킹 조회
- 서버 실행 시 GameScore 상위 20명의 정보를 가져와 Redis
SortedSet
에 삽입한다. - SortedSet 의 score 는 GameScore 를 사용한다.
- 랭킹 조회 시 상위 20명의 데이터를 가져온다.
- 서버 실행 시 GameScore 상위 20명의 정보를 가져와 Redis
- 게임 점수 갱신
- 점수 갱신 시 데이터베이스에 접근하지 않고 해당 유저의 정보와 점수를
SortedSet
에 삽입한다. - N 명 점수 갱신 시 Redis 에는
(20 + N) 개
이하의 데이터가 삽입된다. (중복 시 갱신 처리로 데이터의 양이 늘어나지 않음) - 이후 Batch 작업으로 Redis 와 DB 를 동기화시킨다.
- 점수 갱신 시 데이터베이스에 접근하지 않고 해당 유저의 정보와 점수를
- index 를 사용한 데이터베이스 직접 조회와 Redis 조회의 성능 차이는 무의미하다.
- 로직 별 응답속도 차이를 확인하기 위해 점수 갱신의 API 요청 비율을 높인다.
- 점수에 대해 index 가 생성되어있기 때문에 데이터베이스 갱신 시 Redis 의 SortedSet 에 add 하는 것보다 더 많은 시간이 걸릴 것이다.
Type | Name | # Requests | # Fails | Median (ms) | 95%ile (ms) | 99%ile (ms) | Average (ms) | Min (ms) | Max (ms) | Average size (bytes) | Current RPS | Current Failures/s |
---|---|---|---|---|---|---|---|---|---|---|---|---|
GET | /api/v1/lottery/drawing/rank?subEventId=4 | 53914 | 0 | 1700 | 3900 | 6100 | 1856.07 | 3 | 58826 | 1067.55 | 336.7 | 0 |
GET | /api/v1/lottery/drawing/score?subEventId=4 | 32561 | 0 | 1900 | 4000 | 6400 | 1974.69 | 5 | 58825 | 142.19 | 197 | 0 |
Aggregated | 86475 | 0 | 1800 | 4000 | 6200 | 1900.73 | 3 | 58826 | 719.12 | 533.7 | 0 |
Type | Name | # Requests | # Fails | Median (ms) | 95%ile (ms) | 99%ile (ms) | Average (ms) | Min (ms) | Max (ms) | Average size (bytes) | Current RPS | Current Failures/s |
---|---|---|---|---|---|---|---|---|---|---|---|---|
GET | /api/v1/lottery/drawing/rank?subEventId=4 | 44550 | 0 | 1500 | 3900 | 5800 | 1642.36 | 4 | 40140 | 1084.09 | 301.6 | 0 |
GET | /api/v1/lottery/drawing/score?subEventId=4 | 26955 | 1 | 1700 | 4200 | 6000 | 1782.8 | 7 | 40138 | 142.13 | 186 | 0 |
Aggregated | 71505 | 1 | 1600 | 4000 | 5900 | 1695.3 | 4 | 40140 | 729 | 487.6 | 0 |
- 데이터베이스에 10만명의 유저가 있는 상황에서 (Locust 기준) 2000 명의 User, 600 이상의 Rps 이후 응답 시간이 급격하게 증가하는 현상
- 해당 시점 이전까지는 DB, Redis 40ms 이하의 응답 시간을 보인다.
- 해당 시점 이후의 응답 시간의 증가는 랭킹 관련 로직 (DB, Redis 사용 유무) 과 관련없이 t3.smaill 인스턴스의 Spring 서버가 요청을 수용할 수 있는 한계라 판단.
- 즉, 응답 시간의 증가는 로직의 병목 구간이 아닌 인스턴스와 서버의 한계이기에 해당 테스트로 로직에 따른 성능을 평가할 수 없다.
- 실제 로그를 통해 메서드 실행 시간을 판별했을 때에도 요청의 수와 무관하게 항상 낮은 실행 시간을 직접 확인할 수 있었다. 즉, 메서드의 병목이 아니다.
- 따라서 DB, Redis 의 사용 유무에 따른 로직 성능을 평가하기 위해서는 각 요청의 응답 시간을 증가시키기 위해 EventUser 를 100만명으로 증가시켰다.
Method | DB Average Time (ms) | Redis Average Time (ms) |
---|---|---|
랭킹 갱신 | 22.26 ms | 11.75 ms |
랭킹 조회 | 8.12 ms | 15.18 ms |
- 100만명의 유저를 생성할 경우 Redis 에 업데이트 및 조회 및 DB 조회에서는 성능 상 차이가 없을 것으로 예상한다.
- 다만, 데이터베이스 직접 업데이트 시 인덱스 재정렬을 위한 시간이 유저가 많아질 수록 더 많이 소요될 것이라 예상하며 이는 Redis 와의 속도 차이로 연결 될 것으로 예상한다.
- 여전히 조회에 있어서 DB 조회와 Redis 조회에서는 무의미한 성능 차이를 가질 것이다.
Type | Name | # Requests | # Fails | Median (ms) | 95%ile (ms) | 99%ile (ms) | Average (ms) | Min (ms) | Max (ms) | Average size (bytes) | Current RPS | Current Failures/s |
---|---|---|---|---|---|---|---|---|---|---|---|---|
GET | /api/v1/lottery/drawing/rank?subEventId=4 | 178453 | 0 | 440 | 2400 | 3200 | 739.63 | 4 | 4924 | 1076.79 | 311.6 | 0 |
GET | /api/v1/lottery/drawing/score?subEventId=4 | 107253 | 3 | 470 | 2400 | 3200 | 766.97 | 5 | 4616 | 142.08 | 191.2 | 0 |
Aggregated | 285706 | 3 | 450 | 2400 | 3200 | 749.89 | 4 | 4924 | 725.91 | 502.8 | 0 |
Type | Name | # Requests | # Fails | Median (ms) | 95%ile (ms) | 99%ile (ms) | Average (ms) | Min (ms) | Max (ms) | Average size (bytes) | Current RPS | Current Failures/s |
---|---|---|---|---|---|---|---|---|---|---|---|---|
GET | /api/v1/lottery/drawing/rank?subEventId=4 | 174595 | 0 | 450 | 2400 | 3300 | 760.16 | 3 | 5019 | 1107.72 | 320.5 | 0 |
GET | /api/v1/lottery/drawing/score?subEventId=4 | 104954 | 3 | 480 | 2400 | 3300 | 786.78 | 5 | 4996 | 142.08 | 197.9 | 0 |
Aggregated | 279549 | 3 | 460 | 2400 | 3300 | 770.15 | 3 | 5019 | 745.18 | 518.4 | 0 |
- 100만명의 유저를 넣었지만, 실제 데이터베이스에서 업데이트 쿼리의 속도를
profiling
해봤을 때에도 성능적으로 우수함을 볼 수 있다. - 실제 Locust 로 테스트 해봤을 때에도 2000명 이하 User, 600 이하의 Rps 의 경우 DB와 Redis 의 성능에서는 유의미한 차이를 찾아볼 수 없었다.
SET profiling = 1;
UPDATE event_users SET game_score = 95 WHERE id = 387280;
SHOW PROFILE FOR QUERY 2;
단계 | 시간 (초) |
---|---|
starting |
0.000080 |
Executing hook on transaction |
0.000006 |
starting |
0.000009 |
checking permissions |
0.000142 |
Opening tables |
0.000053 |
init |
0.000007 |
System lock |
0.000048 |
updating |
0.000060 |
end |
0.000004 |
query end |
0.000005 |
waiting for handler commit |
0.000012 |
closing tables |
0.000009 |
freeing items |
0.000045 |
cleaning up |
0.000014 |
MySQL 내에서도 캐시가 존재하기에 조회의 성능을 높이는 요소 중 하나이다.
Query | Average Execution Time (seconds) |
---|---|
Mysql 캐시 미사용 | 0.003841 |
Mysql 캐시 사용 | 0.001912 |
- 결론적으로 100만명의 유저가 존재하는 경우에서도, t3.small 인스턴스 위 Spring 서버 자체의 한계만 존재할 뿐 로직 자체의 병목 현상은 발생하지 않는다.
- 또한 기획적 의도에서 참여자 수가 175,500 명이기 때문에 해당 환경에서는 외부 리소스(비용)인 Redis 의 도입보다 데이터베이스의 index 를 사용하는 것이 옳다.
- 다만 상황에 따라 랭킹 로직의 부하를 데이터베이스에서 Redis 로 옮기는 것도 충분히 고려할 수 있다.