상황
800억 건 규모의 PC 기능 통계를 추가하면서, 적재한 데이터를 대시보드에서 조회할 수 있도록 집계를 확장했습니다. 이 글에서는 공통 집계의 재사용과 집계 방식별 비용, 조회 병목 개선을 정리합니다. Splunk 데이터의 적재 구조와 건수 확인 부하는 앞선 글에 모았습니다.
4코어, 메모리 16GB인 EC2 한 대에서 알매니저 이벤트 적재, PC 기능 통계의 1분 주기 적재와 1년치 백필, 광고와 삭제 코호트 집계가 실행됐습니다. 오전 CPU 사용량은 평균 3.4코어였고, PC 기능 개요의 차트 요청은 평균 30초가 걸렸습니다.
공통 집계를 재사용해 조회 확장하기
집계를 작게 만들수록 재사용할 정보도 줄어듭니다. 그래서 공통 일 집계에는 분석 기준과 Aggregate State를 남기고, 자주 쓰는 조회 결과는 더 작게 집계한다는 기준을 세웠습니다. 기기 수처럼 중복을 제거해야 하는 값은 확정된 숫자를 더하는 대신 State를 병합할 수 있어야 합니다. (Aggregate State 병합)
공통 일 집계를 입력으로 새 조회용 Refreshable MV를 만들면, Log Ingester를 멈추고 기존 데이터를 다시 채우는 대신 수집을 계속하면서 조회를 확장할 수 있다고 봤습니다. 다만 수집을 계속할 수 있다는 이점과 이후 반복 재계산 비용은 함께 고려해야 했습니다.
반복 집계 비용을 줄이기 위한 Incremental MV 전환
처음에는 기존 데이터와 계속 추가되는 데이터를 함께 재계산할 수 있어, 그래프용 제품별 일 집계에 Refreshable MV를 적용했습니다. 일별 그래프에서 날짜와 제품별로 다시 묶는 계산을 미리 저장하려는 선택이었습니다.
MV 갱신과 Rill 캐시 주기는 각각 15분으로 설정했습니다. 재계산이 갱신 주기 안에 끝나고 캐시가 15분 안에 새 결과로 전환될 때, 최대 약 30분에 재계산 시간 T를 더한 반영 지연이 생기는 조건이었습니다. (Rill 캐시 동작)
하지만 제품별 일 집계는 하루치만 바뀌어도 15분마다 1년치를 재계산해, 갱신에 주기의 평균 58%를 썼습니다.
| 구분 | Incremental MV | Refreshable MV |
|---|---|---|
| 실행 시점 | 데이터 적재 시 | 설정한 갱신 주기 |
| 계산 범위 | 새로 들어온 데이터만 처리 | 쿼리가 지정한 범위 전체를 재계산 |
| 결과 반영 | 새 계산 결과를 추가하고 병합 | 기존 결과를 새 결과로 전체 교체 |
| 최신성 | 적재와 함께 반영 | 다음 갱신이 끝난 뒤 반영 |
| 연산 비용 | 적재 시 연산 추가 | 갱신 시 CPU와 메모리 부하 집중 |
먼저 갱신 주기를 15분에서 1시간으로 늘렸습니다.
- CPU: 갱신 횟수가 줄어 시간당 사용량이 감소합니다.
- 최신성: 다음 MV 갱신까지의 대기가 최대 15분에서 1시간으로 늘어납니다.
- 메모리: 한 번에 계산하는 범위가 같아 피크 메모리를 줄이는 대책은 아닙니다.
주기를 늘려도 계산 범위는 그대로여서, 제품별 일 집계를 Incremental MV로 전환했습니다.
| 제품별 일 집계 방식 | 시간당 CPU초(평균) | 반영 주기 |
|---|---|---|
| 15분 주기 Refreshable MV | 2,000 | 15분 |
| 1시간 주기 Refreshable MV, 중간 단계 | 550 | 1시간 |
| Incremental MV | 0.3 | 1분 |
공통 일 집계에 저장된 uniq 상태는 새 입력의 상태와 병합할 수 있어, 기기 수를 구할 때 과거 데이터를 다시 읽을 필요가 없었습니다. 원본과 공통 일 집계는 유지하고 제품별 집계의 계산 방식만 바꿨습니다. 적재 시 연산을 추가하는 대신 조회와 경쟁하던 반복 재계산과 갱신 대기를 없앴습니다.
코호트 OOM을 막기 위한 데이터 결합 방식 변경
광고와 삭제 코호트는 같은 달의 사용 기기를 광고 노출 수준별로 묶어 다음 달 삭제율을 비교합니다. 제품과 기기를 기준으로 세 기록을 조합합니다.
- 기능 사용 기록: 사용 기기, 기능 사용량과 광고 노출량
- 실행 기록: 라이선스 구분
- 삭제 기록: 다음 달 삭제 여부
9월 광고 노출과 10월 삭제 여부를 연결하듯, 관찰 기간이 끝날 때까지 재계산이 필요합니다.
전날에는 이 조회를 위해 두 월 집계를 입력으로 받는 Refreshable MV를 추가했습니다.
- 제품 이벤트 월 집계: 실행, 설치, 삭제 기록
- 광고 노출 월 집계: 기능별 광고 노출 기록
기존 수집과 월 집계를 유지하고 조합한 결과를 Rill에 연결했습니다.
라이선스, 기능 사용량과 활성 일수로 나눠 광고 노출과 다음 달 삭제율을 비교했습니다. 아래는 가상 제품 A의 비교 예시입니다.
JOIN의 오른쪽 입력인 수백만 기기의 해시 테이블에 메모리 13.77GiB를 할당하려다 OOM이 발생했습니다.
JOIN을 UNION ALL과 GROUP BY로 바꿔, 디스크 I/O를 더 쓰는 대신 중간 집계의 메모리를 제한했습니다. (외부 집계 동작)
- 스레드: 쿼리당 2개
- 메모리: 쿼리당 최대 8GB
- 외부 집계: 2GB를 넘으면 중간 집계 결과를 디스크에 기록
백필 완료 후 갱신에는 50분이 걸렸습니다. 다른 작업과의 경합을 줄이도록 갱신 주기도 6시간에서 하루 한 번 새벽 4시로 늘렸습니다.
첫 조회를 빠르게 하기 위한 대시보드용 집계 추가
제품별 합계만 필요한 차트도 버전과 기능 코드까지 나눈 집계를 매번 다시 합치고 있었습니다. 앞선 실험에서는 Rill 캐시와 singleflight로 중복 조회의 CPU 부하를 줄였습니다. 다만 캐시에 없는 조회 조건이면 집계를 다시 실행해야 했습니다.
| 방식 | 줄이는 비용 | 첫 조회에 미치는 영향 | 추가 비용과 조건 |
|---|---|---|---|
| Rill singleflight | 진행 중인 같은 요청의 중복 실행 | 한 번은 계산하고 나머지는 결과를 기다림. 동시 경합은 감소 | 같은 캐시 키의 동시 요청에 적용 |
| Rill 결과 캐시 | 완료된 같은 쿼리의 재실행 | 캐시 미스면 계산, 히트면 결과 재사용 | 캐시 보관과 무효화 관리, 갱신 확인 주기에 따른 반영 지연 |
| 대시보드용 집계 | 조회 한 번에 읽고 합치는 데이터량 | 캐시가 없어도 미리 만든 집계를 조회 | 별도 저장 공간과 적재 또는 주기적 갱신 연산 |
캐시와 singleflight를 유지하고 대시보드용 집계를 추가했습니다. (Rill 캐시 설정)
- PC: 제품별, 버전별, 기능 코드별 월 집계
- 알매니저: 앱별, OS별, 버전별 집계
PC 쿼리는 유휴 상태의 서버에서 ClickHouse 쿼리 캐시를 끄고 비교했습니다.
| 조회 | 기존 월 집계 | 새 집계 적용 시 |
|---|---|---|
| 제품과 버전 피벗 | 12.9초 | 2.0초 |
| 기능별 기기 수 순위 | 24초 | 15초, 채택하지 않음 |
제품별 기기 수와 전체 합계 조회는 평균 600ms 걸렸습니다.
알매니저의 기존 가운데 차트는 평균 11초 걸렸습니다. 적용 후에는 Rill 캐시를 비운 콜드 로드와 캐시를 재사용한 웜 로드를 비교했습니다.
| 대시보드 | 콜드 로드 | 웜 로드 |
|---|---|---|
| 알매니저 개요 | 가장 느린 요청 1.2초 | 1초 |
| PC 기능 개요 | 전체 로딩 30초, 가장 느린 요청 17초 | 1.3초 |
콜드 로드에서 PC의 기능별 기기 수 순위와 상세 표는 평균 16초 걸렸습니다.
두 조회는 새 집계를 적용해도 효과가 작았고, 큰 기기 수 집계 상태를 동시에 병합하다 계정 메모리 한도 6GB를 넘어 조회 OOM 2건이 발생했습니다. 두 조회는 기존 월 집계로 롤백하고, 기능 코드별 집계는 기기 수 상태를 병합하지 않는 차트에만 적용했습니다.
- 저장 공간: 새 집계 1.7GiB, 전체 디스크의 0.4%
- 적재 CPU: 알매니저 기존 대비 평균 8% 추가
기능 코드별 집계가 추가 용량의 70%를 차지하면서 조회 효과는 작았습니다. 요약에 없는 필터는 기존 테이블로 폴백합니다.
PC 집계 4개의 12개월 지표는 기준 집계와 차이가 없었습니다. 개요의 사용 횟수 823.8억과 기기 수 1,032만도 전후가 같았습니다.
적재 중단을 줄이기 위한 대시보드용 집계의 갱신 방식 검토
Incremental MV 생성 시 초기 백필과 신규 적재가 겹치면 중복 집계될 수 있습니다. 알매니저는 집계 정의를 바꿀 때마다 수집 서버 2대의 Vector를 중단해야 해 대시보드 반영이 늦어졌습니다.
기본 집계는 Incremental MV로 유지하고, 입력이 작은 대시보드용 집계만 Refreshable MV로 바꾸는 안을 검토했습니다. 전환 기준은 입력 건수와 집계 상태의 크기, CPU와 메모리 비용으로 정합니다.
알매니저 집계 쿼리 하나는 기본 월 집계 40만 행을 읽어 2코어 조건에서 5초 걸렸습니다. 월 단위 개요에는 1시간 주기의 갱신도 충분하다고 봤습니다.
- 감수할 비용: 갱신 주기와 실행 시간에 따른 반영 지연, 주기적 재계산
- 얻는 효과: 적재 중단 없이 집계 정의 변경
Refreshable MV로 전환하기 전에는 다음을 검증합니다.
- 메모리: 중간 집계 크기를 측정하고 스레드, 메모리, 외부 집계 기준을 설정합니다.
- 작업 중첩: 정기 갱신뿐 아니라 MV 생성 직후의 갱신도 다른 작업과 겹치지 않는지 확인합니다.
- 반영 지연: 갱신 주기와 실행 시간, Rill 캐시 지연을 함께 봅니다.
알매니저는 현재 모두 Incremental MV를 사용하며, Refreshable MV 전환은 검토 중입니다.