Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

인터파크 0.001% Spike 테스트 보고서 #135

Closed
junha-ahn opened this issue Nov 6, 2023 · 2 comments · Fixed by #100 or #121
Closed

인터파크 0.001% Spike 테스트 보고서 #135

junha-ahn opened this issue Nov 6, 2023 · 2 comments · Fixed by #100 or #121
Assignees
Labels
documentation Improvements or additions to documentation major Major topic performanceTest

Comments

@junha-ahn
Copy link
Member

junha-ahn commented Nov 6, 2023

Description

인터파크 기준 0.001% Spike 테스트 보고서

0.0001%는 데이터 부족으로 Dashboard Data Empty 문제 발생

  0.001% 0.01% 인터파크 기준
User per minute 2,00 2,000 20만
Request per minute 3,000 30,000 300만 (370만)

목표에 대한 더 자세한 내용은 f-lab-clone/ticketing-infra#62 참고

테스트 요약

  1. 천만건 데이터 SELECT COUNT(*)NoOffset 구현으로 개선
  2. 하나의 자원(=Event)에 대한 Lock 경쟁 발생: 대기열 시스템도입 으로 해결

Enviroment

NodeGroup  NodeType Running Pods
Ingress [t3.small] Nginx Ingress 팟 1대
Backend [t3.small, t3.small, t2.small, t2.small] 백엔드 1대 (기타 모니터링 등)

성능: https://aws.amazon.com/ko/ec2/instance-types/

테이블명  데이터 수
User 100만
Event 1,000만

Thresholds

thresholds: {
  http_req_failed: ['rate<0.01'], // http errors should be less than 1%
  http_req_duration: ['p(95)<300'], // 95% of requests should be below 300ms
}

100ms는 connection time으로 가정 (자세한 내용)

시나리오 (각 유저당 총 15 Request 발생)

  1. 이벤트 페이지 조회 (GET /events?sort=id,asc&size=20&page=0)
    • 총 13번 조회한다.
    • 매 반복마다 page += randomInt(1, 10)
  2. 이벤트 상세 조회 (GET /event/{ID})
    • 이때 사용하는 EventID는 미리 정해진 ID이다. (대다수의 유저가 예약에 성공할 수 있는 max_attendees 를 가진 이벤트를 사용)
  3. 예약 (POST /reservations)
@junha-ahn junha-ahn added documentation Improvements or additions to documentation performanceTest labels Nov 6, 2023
@junha-ahn
Copy link
Member Author

junha-ahn commented Nov 6, 2023

Report

  • Time Range: from=1697001119010&to=1697001329462

K6 Report

image

Spike Test 시나리오 중 GET events 부분에서 너무 많은 시간이 소요되어, 그 다음으로 넘어가지 못했다.

Resource Report

image

Nginx Report

Spring Report

image

MySQL Report

image

병목 지점 분석

원인

CPU와 메모리 이상보다는 Slow Query의 분석이 필요해보인다.

SQL 분석

Get Events는 event 테이블에 대해 두가지 쿼리를 발생시킨다.

  • event 테이블은 천만건의 데이터를 저장중이다.

테이블

analyze table event; -- 통계 최신화

select table_schema, table_name, table_rows,
    round(data_length/(1024*1024),2) as 'data_size(mb)',
    round(index_length/(1024*1024),2) as 'index_size(mb)'
from   information_schema.tables
where TABLE_SCHEMA="ticketingdb" and TABLE_NAME = "event"
group by table_name
order by data_length desc
limit 100;
TABLE_SCHEMA TABLE_NAME TABLE_ROWS data_size(mb) index_size(mb)
ticketingdb event 9680553 815.00 0.00

쿼리

1. 리스트 조회 쿼리

EXPLAIN SELECT * FROM event ORDER BY id asc LIMIT 0, 10;
EXPLAIN ANALYZE SELECT * FROM event ORDER BY id asc LIMIT 0, 10;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE event null index null PRIMARY 4 null 10 100 null
EXPLAIN ANALYZE
-> Limit: 10 row(s) (cost=0.046 rows=10) (actual time=0.0575..0.0698 rows=10 loops=1)
-> Index scan on event using PRIMARY (cost=0.046 rows=10) (actual time=0.0555..0.062 rows=10 loops=1)

리스트 조회 쿼리는 현재 ORDER BY ID를 사용하고 있기 때문에 슬로우 쿼리가 발생하지 않는다

또한 시나리오상 LIMIT을 통해 인덱스 초반 부분만 탐색하기 때문에 지연이 발생하지 않는다. (500만번째를 검색하는 등)

2. totalElements 조회 쿼리

EXPLAIN SELECT COUNT(1) FROM event;
EXPLAIN ANALYZE  SELECT COUNT(1) FROM event;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE event null index null PRIMARY 4 null 9680553 100 Using index
EXPLAIN ANALYZE
-> Count rows in event (actual time=14779..14779 rows=1 loops=1)

슬로우 쿼리 조회

SELECT start_time, user_host, query_time, lock_time, rows_sent, rows_examined, db, CONVERT(sql_text USING utf8 ) sql_text FROM mysql.slow_log;

https://dreamcoding.tistory.com/62#article-4--mysql

image

현재 1천만개의 인덱스를 모두 집계하고 있기 때문에 상당히 많은 시간이 소요된다.

개선 방법

참고

1. 무한 스크롤

특히 SNS에서 자주 사용한다.

예상 문제점: offset 10000, limit 20 이라 하면 최종적으로 10,020개 Row를 읽어야 한다

  • No Offset으로 개선: WHERE id < {last_access_id} LIMIT {size}
  • 다만 무한스크롤 구현의 경우는 특이 케이스(검색엔진)이 아니라면, 많은수의 Offset을 가질 일이 적다고 예상된다. 예를들어 페이지네이션의 경우는 마지막페이지 버튼을 클릭 하는 등의 행위가 있을 수 있지만, '무한스크롤'에서는 그런일이 발생하지 않는다.
  • "도대체 어떤 할 일 없는 놈이 우리 앱에서 50,000 페이지나 스킵하겠어?"

2. 유사 페이지네이션 구현

N개의 페이지네이션 바를 고정 노출한다 (즉 totalCount 를 조회하지 않는다)

  • 유저는 실제 해당 페이지로 이동시 데이터 존재 여부를 알 수 있다.
image

https://m.blog.naver.com/birdparang/221574304831

3. totalCount 캐싱

가장 대표적으로 COUNT 쿼리 캐싱을 사용한다.

또는 첫 페이지 요청시에만 totalCount를 계산하고, 두번째 페이지부터는 프론트에서 저장한 totalCount 를 계속 사용한다.

4. 유사 totalCount

방법

  • DB Trigger를 이용해 Insert / delete때 totalCount + 1 하기
  • 애플리케이션에서 매 작업마다 counter table에 update
  • 배치 작업을 통해 기록하고 해당 값을 제공
  • 통계테이블의 추측값을 사용해서 반환

위 모든 작업이, where 조건이 추가된다면 큰 의미가 없어진다 (예를들어 특정 검색결과에 대한 전체 개수)

5. Count 쿼리의 응답 속도를 증가시킨다

COUNT 쿼리를 Paralle 로 실행한다. (MYSQL 8.0)

  • 한 쿼리를 여러개의 스레드로 분할 실행 가능하기에 성능이 개선된다.
  • 다만 아직 조건문이 포함된 쿼리의 병렬실행은 불가능하다고 한다.
  • 이에 대한 더 자세한 검색이 필요하다 (= 최신 버전 정보)

조회 전용 Replica DB를 증설한다.

의사 결정

설계상 (프론트엔드가 존재한다면) 무한스크롤을 가정했기 때문에, COUNT 쿼리를 제거함으로 Slow Query를 제거한다

만약 비지니스상 totalCount를 반환해야하는 순간이 온다면 위 방법 중 하나를 고려해볼만 하다

  • 만약 "매 요청마다", "정확히" 라는 조건이 추가된다면 점점 더 많은 어려움이 생길 것이다.
  • 해당 상황을 가정하고, 성능 개선을 시도해볼까 했지만, 다른 문제점을 찾고, 성능 개선하기 위해 totalCount 강제 상황은 가정하지 않기로 결정했다. (우선순위)

개선 이후 동일 테스트 Report

#124 을 통해 totalCountQuery를 제거했다.

  • Time Range: from=1697462864082&to=1697463044132
image
  • 테스트에 사용한 이벤트의 maxAttendees = 191이므로, Success Reservation 테스트는 191개 정상 성공한것을 확인 가능하다.
  • 다만 Already reserved 테스트의 응답이 3개 유실된것으로 추측된다.
image
  • totalCount 쿼리의 제거GET events/GET events/{id}latency는 매우 개선되었다
  • 다만POST /reservation의 응답속도가 지연됨을 확인 가능하다.
image

POST /reservation은 동시성 문제를 해결하기 위해 #54 를 통해 비관적 Lock을 사용한다. 따라서 200 VU 모두가 하나의 자원(=Event)에 대한 Lock을 Wait 하고 있는 상태가 발생하기 때문에 지연이 발생함으로 추측 가능하다.

Lock Time은 f-lab-clone/ticketing-infra#78 를 통해 대시보드에 포함

@junha-ahn
Copy link
Member Author

왜 대기열 시스템이 필요할까?

동시성 문제(=갱신누락)에 대한 어떤 해결 방법이 있을까?

  • Auto-commit : 자원이 몇개 더 생성될 수 있다.
  • Raw Lock
  • Queue or Pub/Sub (Redis)
  • 이벤트소싱

갱신누락 문제를 방지하면서, 하나의 자원(=Event)에 대한 각 유저의 Wait time을 어떻게 개선할 수 있을까?

  • 현재 Raw Lock 사용에서 (크게) 개선할 수 없다.
  • 낙관적 락이나 Auto Commit을 사용하면 Latency 개선 가능하지만 1분 Spike Test 케이스때 갱신 누락이 발생함으로 현재 상황에 맞는 해결법이 아니다. (좋아요 Count 같은 경우는 갱신누락이 발생해도 문제없겠지만, 티켓은 한개 더 발생하는 경우가 생겨서는 안된다)

물런 엄청나게 멋진 DB를 쓰면 어느 순간까지는 대응이 가능할것이다.

그렇다면 각 유저의 Wait Time을 개선할 수 없는데, 왜 대기열을 구현해야할까?

  • "장애가 일어나지 않는 수준의 트래픽에 대해" 대기열을 사용한다면 POST /reservation 요청자체의 latency는 엄청난 개선이 발생한다
  • 하지만 사용자가 대기열에 진입하는 순간부터를 생각해본다면 엄청난 성능 증가라고 말하기는 힘들 것이다 (평소 온라인 대기열을 생각해보자)

지연이 아니라, 정상 응답 조차 감당할 수 없을 정도로 트래픽이 늘어나면 어떠한 문제점이 발생할까?

  • tomcat이 처리할 수 있는 요청량을 벗어나 request timeout
  • hikari pool connection 고갈
  • 즉 한마디로 이쯤되면 지연이 문제가 아니라, 장애가 발생한다.
  • 이때 기준의 성능을 놓고 비교한다면 엄청난 개선이 발생한다 (동일한 트래픽에 대해 대기열은 장애가 발생하지 않을 가능성이 높으니)
image

2000 VU로 설정하여 요청이 증가하자, http_req_failed이 크게 증가함을 알 수 있다.

대기열 시스템은 트래픽의 안정적 순차 처리에 중점을 둔다.

  • 먼저 한번에 모든 요청을 DB까지 직접 전달해 처리할 필요가 없었다. 가장 중요한건 선입선출로 Ticket 구매를 보장하는 것이다.
  • API서버, DB 등에서 처리 가능한 트래픽만 FIFO 방식으로 전달함에 따라 서버, DB 등 성능을 안정적으로 유지 가능하며, 장애 전파(Circuit Breaker)를 막는다.
  • Non-blocking API와 대기열 시스템을 통해 "대용량 트래픽"을 보다 안정적으로 처리한다.
  • 자원의 Scale Out 시간보다 짧은 순간의 증가되는 트래픽을 제어 가능하다
  • 또한 프론트 대기열 화면 구현을 통해 대기시간을 고지할 수 있다. (Lock은 사용자가 인터렉션했을때 반응이 존재하지 않지만, 대기열은 반응이 존재)
  • 성능 저하 포인트를 막아 성능을 개선한다.

그렇다면 Peak 트래픽에 대기열 큐가 터지는건 어떻게 방어할 수 있을까? 오토스케일로는 감당 불가능할것으로 추측된다. 충분한 성능 준비, Pre-warming, 재해복구 등의 키워드가 필요할 것 같다.

왜 API 서버로 Non-blocking을 논할까?

사례

Gmarket: "높은 퍼포먼스를 가지고 있는 Redis를 충분히 활용하기 위해 API 서비스는 Non-Blocking 처리가 편리한 Node.js으로 작성"

배민 : "WebFlux + Netty 논블록킹 서버"

서블릿 컨테이너는 thread-per-request 모델을 따른다

즉 한 쓰레드당 한개의 요청 밖에 처리하지못한다. 그렇게 되면 I/O 작업이 일어날때 쓰레드는 실질적으로 하고있는것이 없지만 다른요청을 처리하지못한다. 그렇게 되면 자원이 부족한 서버(thread를 많이 만들 수 없는)에서 처리할 수 있는 동시 처리량은 매우 제한적일 것이다. 쓰레드를 많이 만든다하더라도 컨텍스트 스위칭 문제가 생겨서 성능에 많은 부담을 주게된다.

특히 이벤트루프기반 비동기 + 논블록킹 Node.js를 사용하면 트래픽을 효율적으로 처리 가능할 것이다

  • Node.js API는 Redis에 빠르게 전달 후 특정 스레드를 할당해 응답을 기다리지 않는다. (= 응답을 기다리는데 특정 스레드나 자원을 할당하지 않는다) 따라서 높은 TPS를 유지할 수 있다.

참고

대기열 적용 이후 테스트

image

변경 시나리오

  • 이벤트 페이지 조회 (GET /events?sort=id,asc&size=20&page=0) 총 13번 조회한다.
  • 이벤트 상세 조회 (GET /event/{ID})
  • 대기열 티켓 생성 (POST /ticket)
  • 대기열 티켓 조회 (GET /ticket/{eventId}/{userId})
    • 대기열에 입장할때까지 계속 반복 조회
  • 예약 (POST /reservations)

대기열 조건

JOB_INTEVAL_SEC: 10
JOB_MOVE_PER_INTEVAL: 20
JOB_TICKET_EXPIRED_SEC: 180

K6 Report

image
  • 모든 테스트 조건을 통과함을 알 수 있다.
  • 물런 유저가 이벤트 페이지에 입장해서 예매에 성공할때까지의 Latency를 의미하는게 아니라, 대기열 조회, 예매 등의 각각의 요청의 Latency만 크게 개선되는 한계점이 있다. (물런 전체적인 Wait Time도 개선된다)

Spring

image
  • POST /reservations AVG 기준 439ms => 74ms

MySQL

image - 기존 테스트에 비해 개선되었다 (MAX: 4.31 => 2.53s, AVG: 616 => 507ms)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation major Major topic performanceTest
Projects
Status: Done
Status: Done
1 participant