기존 구조
로그 적재 서버마다 로그 적재 서비스가 로그 파일을 씁니다. 집계 서버의 스크립트 세 개가 이 로그를 하둡 클러스터에 적재하고 집계합니다.
- 적재 스크립트: 로그 적재 서버의 로그 파일을 가져와 하둡 클러스터에 적재한다.
- 집계 스크립트: 하둡 클러스터의 로그를 집계해 BI 서버의 bi DB에 쓴다.
- 용량정리 스크립트: 하둡 클러스터의 용량을 정리한다.
하둡 클러스터는 하둡 메인과 하둡 서버 두 대로 구성됩니다. bi는 bi DB에 저장된 집계 결과를 조회합니다.
바꾸려는 이유
하둡은 통계를 집계하기 전에 로그를 잠시 두는 저장소인데, 운영하면서 아래 문제를 확인했습니다.
- 용량 관리
- 상황: 처리한 파일이 쌓여 집계가 실패했다. 직접 지워 DFS 사용률을 73.04%에서 62.57%로 낮췄다.
- 원인: 처리한 파일을 정기적으로 지우는 정책이 없었다.
- 해결: 보관 정책에 맞게 오래된 파일을 지우는 스크립트를 등록해, 1년 넘게 문제가 없다.
- 서비스 관리
- 상황: DataNode 3대 중 2대가 2021년 12월부터 Dead 상태라 1대로 동작하고, 누락 블록이 약 47만 개다.
- 원인: 저장 용량이 약 2.2TB일 만큼 파일이 많이 쌓여, DataNode가 블록 정보를 메모리에 올리다 OOM으로 죽은 것으로 추정한다. DataNode는 블록이 많으면 메모리가 부족해질 수 있다(HDFS-6088, Cloudera HDFS heap sizing).
- 해결: 보관 기간을 줄여 파일 수를 줄이려 했지만, 사업팀과 합의하지 못했다.
- 크론잡 관리
- 상황: 크론 작업 21개로 수집, 집계, 정리를 돌리고, 일 단위 통계는 다음 날 새벽에 만들어진다.
- 원인: 로그를 매시 가져와 하둡에 쌓은 뒤, 기능마다 정해진 시각에 배치로 집계한다.
- 해결: 크론 작업이 OOM으로 실패하면 직접 다시 실행했다. 두 달에 한 번꼴로 이틀에서 일주일 동안 이 작업만 했고, 폐쇄망이라 그동안 다른 작업은 할 수 없었다.
- 스크립트 관리
- 상황: 스크립트 21개를 관리하고, 집계를 추가할 때마다 집계 스크립트와 크론 작업이 늘어난다.
- 원인: 기능마다 별도 스크립트가 하둡의 로그를 읽어 집계 결과를 만든다.
- 해결: 집계를 추가할 때마다 스크립트와 크론 작업을 직접 만들어 등록한다.
하둡을 유지하려고 파일을 직접 지우고, OOM으로 실패한 크론 작업을 직접 다시 실행하고, 집계마다 스크립트를 추가했습니다. 이런 수작업이 계속 운영 부담으로 이어졌습니다.
그래서 Vector로 ClickHouse에 바로 적재하고, 보관 기간은 ClickHouse의 TTL로 관리하는 구조로 바꾸려고 합니다.
바꿀 구조
로그 적재 서버마다 Vector를 두고, Vector가 로그 파일을 읽어 ClickHouse에 적재합니다. bi는 ClickHouse를 직접 조회합니다.
서버별로 달라지는 점
로그 적재 서버
| AS-IS | TO-BE |
|---|---|
- 적재 방식: 집계 서버의 적재 스크립트가 로그 파일을 가져가던 방식에서, 서버마다 둔 Vector가 파일을 읽어 ClickHouse로 보내는 방식으로 바뀐다.
- 부하: Vector가 쓰는 CPU와 메모리만큼 로그 적재 서버의 부하가 늘어난다.
하둡 클러스터와 ClickHouse
| AS-IS | TO-BE |
|---|---|
- 저장소: 하둡 메인과 하둡 서버 두 대 대신 ClickHouse에 저장한다.
- 부하: 기존에는 집계를 하둡에서 실행해 BI 조회와 부하가 나뉘었지만, 바꾸면 적재와 BI 조회가 같은 ClickHouse에서 실행된다.
BI 서버
| AS-IS | TO-BE |
|---|---|
- 조회 방식: bi DB에 미리 집계한 결과를 읽던 방식에서, ClickHouse의 원본을 조회할 때 집계하는 방식으로 바뀐다.
- 반영 시점: 어제까지의 데이터만 보던 것에서, Vector가 보낸 직후의 로그까지 볼 수 있다.
- 지표 추가: 집계 스크립트와 bi DB 테이블을 고치던 것에서, 쿼리만 추가하면 된다.
참고한 사례: Uber의 로그 분석 플랫폼
곧바로 바꾸면 좋지만, 게으른 엔지니어가 되자는 목표에 맞게 바꿀 구조가 최종적으로 어떤 모습이 될지 먼저 그려서 데이터 증가에 대한 확장성을 고려해야 했습니다. Uber의 아키텍처를 기준으로 어떻게 확장할지 정리했습니다.
Uber의 Fast and reliable schema-agnostic log analytics platform(2021년 2월)은 ELK로 운영하던 로그 플랫폼을 ClickHouse로 옮긴 사례입니다. 기존 저장소는 하둡이 아니라 Elasticsearch입니다. 이 구조에서는 ClickHouse 노드를 추가해 클러스터를 선형으로 확장합니다.
Uber 구조를 기준으로 확장하기
지금은 Uber처럼 역할마다 서버를 두지 않습니다. 한 서버에 여러 역할이 함께 있고, 역할은 시스템 내부 디렉토리와 코드로 구분합니다. 데이터가 늘면 이 구분을 따라 역할을 서버로 나눠 다음과 같이 확장합니다.
Uber가 ClickHouse로 옮긴 뒤 확인한 결과
- 집계 조회: 쿼리의 80% 이상이 집계 쿼리였다. Elasticsearch에서는 최근 6시간 로그(약 1.3TB)를 조회하는 대시보드가 자주 타임아웃됐다. ClickHouse로 옮긴 뒤에는 조회 기간이 하루 미만이면 대부분 몇 초 안에 끝났다.
- 비용: ClickHouse 노드 한 대가 초당 로그 30만 건을 적재했다. Elasticsearch 노드 한 대의 약 10배다. 하드웨어 비용은 ELK를 쓸 때의 절반 이하로 줄었다.
성능 테스트
바꿀 구조가 지금 로그 양을 처리할 수 있는지 로컬에서 확인했습니다. 적재는 튜닝하지 않고 결과만 참고했고, 조회는 기준을 넘겨 튜닝했습니다.
테스트 조건
- 부하: 2026-09-30 실데이터에서 가장 붐빈 20분을 하루 2천만 건 규모로 환산해 10초 단위로 재현했다. 피크 20분의 초당 평균은 2,169건이다.
- 수집 경로: nginx가 받은 요청 본문을 로그 파일(
events.log)에 쓰고, Vector가 이 파일을 읽어 ClickHouse에 적재했다. 바꿀 구조처럼 Vector가 로그 파일을 읽는 경로다. - 기존 데이터: 30일치 약 6.1억 건을 미리 적재한 상태에서 진행했다.
- 환경: 로컬 PC에 ClickHouse 1대를 두고 메모리 상한을 4GB(운영 권장 사양 16GB의 1/4)로 잡았다.
적재 성능
적재는 기준 7개를 모두 통과해 튜닝하지 않았습니다.
- 30초 구간 최대 처리량
- nginx 수신: 2,571건/초
- Vector 읽음: 2,567건/초
- ClickHouse 저장: 2,635건/초
- 유실: 0건
- 지연: 전송에서 저장까지 p95 2.8초다. Vector가 최대 2초씩 모아서 저장하는 대기 시간이 포함된다.
- 자원: ClickHouse 메모리 최대 2.1GB(상한 4GB), CPU 최대 1.86코어(14코어의 13%)다.
조회 성능
문제: 30일 고유 기기 수 조회가 기준 2초를 넘었다
대시보드 조회 3종 중 30일 고유 기기 수 조회만 기준 p95 2초를 넘었습니다.
- 30일 고유 기기 수: 피크 p95 2.49초, 피크 중 2초 초과 40/66회
- 30일 시간대별 실행 수: 피크 p95 1.13초, 피크 중 2초 초과 1/67회
- 30일 버전별 집계: 피크 p95 0.06초, 피크 중 2초 초과 0/66회
부하가 없을 때도 평균 1.95초가 걸렸습니다. 적재 부하보다 조회할 때마다 원본 6.09억 건(13.75GB)을 읽는 것이 원인이었습니다.
적용: 일별 고유 기기 수를 미리 집계했다
| AS-IS | TO-BE |
|---|---|
- 요약 테이블
daily_app_stats: 날짜와 앱마다 1행을 둔다. 고유 기기는uniqState집계 상태로, 이벤트 수는 합계로 저장한다. - Materialized View
daily_app_stats_mv: 원본events에 INSERT가 들어올 때마다 그 묶음을 날짜와 앱별로 요약해 요약 테이블에 넣는다. - 과거 30일치: 하루 단위로 백필했다. 6.09억 건이 780행(38MB)이 됐고 11초 걸렸다.
- 대시보드 조회: 원본을
uniq(uuid)로 읽던 쿼리를 요약 테이블을uniqMerge(devices)로 읽게 바꿨다. 나머지 조회 2종과 부하 조건은 바꾸지 않았다.
원리: 숫자 대신 집계 상태를 저장한다
- 숫자로 더할 때: 묶음별 고유 기기 수를 더하면 겹친 기기가 두 번 세어진다(3대 + 2대 = 5대, 실제 4대).
- 저장: 요약 테이블에 숫자 대신 집계 상태(
uniqState)를 저장한다. - 병합: AggregatingMergeTree가 같은 날짜와 앱의 상태를 합쳐 한 행으로 만든다.
- 조회:
uniqMerge가 30일치 상태를 합쳐 겹친 기기를 한 번만 센다. 원본 6.09억 건 대신 요약 780행을 읽는다. - 정확도:
uniq는 근사 집계다. 원본을uniq로 계산한 값과 차이가 0이었다.
주의점: Materialized View는 원본 INSERT와 함께 실행된다
- 권한: 수집 계정에 권한이 없으면 INSERT는 실패하지만 원본에는 기록돼, 재시도마다 중복이 쌓인다. 권한을 먼저 부여한다.
- 백필 범위: Materialized View를 먼저 만들고, 그 이전 데이터만 백필한다.
- 요약 기준 변경: 새 기준(예: OS별)은 요약 테이블을 새로 만들어 원본에서 백필한다.
결과
| 항목 | AS-IS | TO-BE |
|---|---|---|
| 30일 고유 기기 수 단독 조회 (부하 없음) | 2,144ms, 13.75GB 읽음 | 152ms, 70KB 읽음 |
| 30일 고유 기기 수 p95 (피크) | 2.49초 | 0.17초 |
| 대시보드 조회 3종 p95 (피크) | 2.33초 | 0.97초 |
| 2초 초과 조회 (피크) | 41/199회 | 0/199회 |
| ClickHouse CPU 피크 평균 / 최대 | 1.27 / 1.86코어 | 0.70 / 1.24코어 |
| 합격 기준 | 7/8 | 8/8 |
30일 고유 기기 수 조회가 읽는 데이터는 13.75GB에서 70KB로 줄었고, 피크 중에도 0.17초 안에 끝났습니다. 대시보드 조회 3종 모두 기준 2초 안에 들어와 합격 기준 8개를 모두 통과했습니다. 같은 부하에서 ClickHouse CPU 피크 평균도 1.27코어에서 0.70코어로 줄었습니다.