3 분 소요

SNUXI 서비스에서, 페이지네이션 방식 테스트 및 서버 자원 사용량 모니터링 시 사용한 스크립트와 PromQL 쿼리, 데이터 수치화를 기록한다.

페이지네이션 방식 적합도 테스트 : 오프셋 vs 커서

  • 문제 제시: 실제 서비스 환경에서, 무한 스크롤 기능을 지원할 때 어떤 방식이 A. 조회 시간 B. 메모리 버퍼풀 사용량 C. 사용자 경험 측면의 3가지 기준에서 더욱 적합할까?

  • 해결: 문제 해결을 위해 다음과 같은 실제 서비스 환경을 모사하는 시나리오 작성.

< 실제 환경 >
1) 서울대 학/석/박사과정 학생 수 총합 30,323명
2) 거의 대부분의 트래픽은, 서비스 특성상 등/하교 피크타임에 몰아서 발생할 것 -> 즉 상대적으로 긴 시간동안 부하를 측정하는 것보다, 15분 내외의 시간에 높은 부하를 주는 것이 훨씬 더 정확한 환경
3) 서로 일면식 없는 2~4명이 모여 오픈 채팅방에서 대화를 나눌 때, 일반적으로 채팅 메시지의 건수는 수십 건 내외일 것 ex: ‘안녕하세요’, ‘어디 계신가요?’, ‘그쪽으로 갈게요’ 등

< 테스트 시나리오 >
1) 이용자수는, 실제 전체 인원수의 약 70% 가량인 2만명으로 책정(출/퇴근 시간이 사람마다 조금씩 다르므로)
2) 일반적으로 돈을 가장 아낄 수 있는 최대 인원수로 채팅방을 만든다고 가정하고, 채팅방 개수 = 5000개로 책정
3) 일반적인 상황을 가정하여, 각 방의 채팅 메시지 건수는 사용자당 25건, 총 100건으로 책정
4) 채팅 내역 자체의 데이터 양을 최대한 줄이기 위해, 즉 Index 유무에 따른 변화만 관찰하기 위해 모든 메시지의 내용은 “1”로 통일

  • 사용 스크립트:
import http from 'k6/http';
import exec from 'k6/execution';

const BASE_URL = __ENV.BASE_URL;

export const options = {
  scenarios: {
    cursor_scroll: {
      executor: 'constant-vus',
      vus: 50,
      duration: '12m',
    },
  },
};

export default function () {
  const roomId = (exec.vu.iterationInScenario % 5000) + 1;
  const userId = roomId;

  let cursor = null;

  for (let i = 0; i < 10; i++) {
    let url = `${BASE_URL}/rooms/${roomId}/messages?size=10`;
    if (cursor !== null) {
      url += `&cursor=${cursor}`;
    }

    const res = http.get(url, {
      headers: {
        'X-USER-ID': String(userId),
      },

      tags: { name: 'get_messages' },
    });

    if (res.status !== 200) {
      throw new Error(`get messages failed: room=${roomId}, user=${userId}, status=${res.status}, body=${res.body}`);
    }

    const body = res.json();

    if (!body.items || body.items.length === 0) {
      break;
    }

    if (!body.hasNext || body.nextCursor == null) {
      break;
    }

    cursor = body.nextCursor;
  }
}
  • 측정 지표:
  • 응답 시간(평균, p(90), p(95))
  • (2) mysql_global_status_innodb_buffer_pool_bytes_data / 1000 / 1000 MySQL InnoDB 버퍼풀의 메모리 사용량(MB)
  • (3) rate(mysql_global_status_sort_rows[1m]) Filesort 발생 빈도

1. offset 기반 페이지네이션

  • 현재 인덱스 상황
    idxoffset
    users 테이블에 1개(PK) / pots 테이블에 2개(PK & FK) / participants 테이블에 3개(PK & FK & UK) / chat_message 테이블에 3개(PK & FK 2개)로 구성되어있다.

  • Grafana 그래프:
    grafanagraphoffset
    (최대 응답시간(ms) = http_server_requests_seconds_max * 1000, 노란색 선)

  • Index 타는지 확인
    indexcheckoffset
    기존 Foreign Key를 사용하는 것을 확인할 수 있다.

  • 테스트 결과:
  • k6 콘솔:
    resultoffset

  • 표 형태로 정리:
avg p(90) p(95) MySQL 메모리 버퍼풀 사용량 (max, MB) Filesort 발생 빈도
44.49ms 62.23ms 72.24ms 35.1MB 0

2. cursor 기반 페이지네이션

  • 현재 인덱스 상황
    idxcursor
    users, pots, participants 인덱스는 offset과 동일하지만, chat_message 테이블에는 기존의 FK 대신 (pot_id, id) 복합 인덱스가 들어간다.

  • Grafana 그래프:
    grafanagraphcursor
    (최대 응답시간(ms) = http_server_requests_seconds_max * 1000, 노란색 선)

  • Index 타는지 확인
    indexcheckcursor
    Using index condition, 즉 인덱스를 사용한다는 것을 확인할 수 있다.

  • 테스트 결과:
  • k6 콘솔:
    resultoffset

  • 표 형태로 정리:
avg p(90) p(95) MySQL 메모리 버퍼풀 사용량 (max, MB) Filesort 발생 빈도
39.54ms 58.22ms 70.62ms 39.0MB 0

3. 결과 분석 및 기술 선택

1) 약 4MB의 메모리 버퍼풀 사용 차이는, 유일하게 달라진 인덱스 하나의 종류에 의한 것으로 판단된다. 또한, 응답 속도는 채팅방 메시지 100건 환경에서 전반적으로 cursor 기반 페이지네이션이 유리하나 응답시간 차이가 10ms 내외로 거의 차이가 없었다.

2) Offset 방식은 FK 인덱스를 이용하여, 특정 방에 존재하는 모든 메시지를 읽었으며 / Cursor 방식은 복합 인덱스를 이용하여 조건에 맞는 메시지만 읽었음을 explain을 통해 확인하였다.

3) 즉 trade-off는: 4MB의 메모리 추가 사용량을 지불하여 조금의 응답속도 개선을 얻을 것인지에 대한 것으로 정의할 수 있다.

4) 해당 관점만 보면 offset 기반 페이지네이션을 사용해도 큰 문제는 없어보이나, 처음에 고려했던 또 다른 기준인 사용자 경험 기준으로 본다면 offset 방식은 메시지 누락/중복 노출의 정합성 문제가 존재한다(이는 채팅방을 통해 서로의 정보를 공유하는 사용자들에게 혼란을 줄 수 있다. 서비스의 신뢰와 직결된 문제).

5) 따라서, 4MB의 추가 메모리 버퍼풀 사용량을 감수하고 사용자 경험을 위해 cursor 기반 페이지네이션을 선택했다.

  • 추가로, 도배 등의 악성 사용자가 존재할 경우 메시지 수가 현재 테스트 시나리오인 100건보다 많아질 수 있는데(= 드문 예외 케이스) 이러한 경우는 현재 서비스에 구현된 신고/관리자 차단 기능으로 1차적으로 막고, 그로 인한 채팅방 페이지네이션 API의 성능 저하 또한 cursor 기반 페이지네이션 방식을 선택하여 방지 가능하다.

카테고리:

업데이트: