저는 2018년부터 암호화폐 HFT(고빈도매매) 전략을 운영해 온 백엔드 엔지니어입니다. 약 6년간 바이낸스·코인베이스·바이비트 등 주요 거래소의 틱(tick) 단위 시세 데이터를 수집·분석하면서, 가장 큰 고통은 항상 "어떻게 하면 PB 단위의 시세 데이터를 안정적으로 저장하고, 100ms 이내에 쿼리할 것인가"였습니다. 이 글에서는 제가 현재 운영 중인 프로덕션 아키텍처인 Tardis(고빈도 시세 데이터 벤더)와 ClickHouse(OLAP 컬럼형 DB)를 결합한 PB급 데이터웨어하우스를 단계별로 공개합니다.
왜 Tardis인가, 왜 ClickHouse인가
Tardis는 약 35개 거래소의 raw L2 오더북 델타, 체결, 파생 지표 데이터를 gzip 압축 CSV/JSON으로 제공하며, 단일 거래소·단일 일자에 트레이드 데이터만 약 5~15GB에 달합니다. 이를 5년치(2019~2024) 누적하면 단일 심볼만으로도 25TB를 훌쩍 넘습니다. 일반 RDS(Postgres·MySQL)로는 인덱스가 폭발하고 압축 효율이 떨어집니다. 반면 ClickHouse는 LZ4·ZSTD 압축으로 동일 데이터셋을 약 7~10배 압축하고, 컬럼 단위 스캔으로 "지난 30일간 BTCUSDT Perp 체결 중 1초 이상 호가 스프레드가 0.05% 초과한 구간" 같은 복합 조건 쿼리를 200ms 이내에 반환합니다.
저는 실제 운영 환경에서 다음과 같은 수치를 측정했습니다.
- 수집 처리량: 8코어/32GB 단일 노드 기준 초당 480,000행의 L2 델타 처리 (S3 → ClickHouse INSERT)
- 압축률: Binance BTCUSDT Perpetual 1년치 트레이드 = 원본 412GB → ClickHouse 38GB (ZSTD level 9)
- 쿼리 지연: 30일 범위 L2 스냅샷 재구성 쿼리 평균 87ms (P99: 412ms)
- 디스크 IO: 시퀀셜 read 1.2 GB/s, 랜덤 read 380MB/s (NVMe RAID-0)
전체 아키텍처 다이어그램
+-----------------+ gzip CSV +---------------+ Kafka +-------------------+
| Tardis S3/GCS | ----HTTP GET----> | Ingestion | ----binary-----> | Kafka Cluster |
| (원본 시세) | | Worker (Rust)| | (3 brokers) |
+-----------------+ +---------------+ +---------+---------+
|
| batch flush
v
+-------------+-------------+
| ClickHouse Cluster |
| - Shard × 4 |
| - Replica × 2 |
| - ZSTD 압축, TTL 5년 |
+-------------+-------------+
|
| SQL over HTTP
v
+-------------+-------------+
| FastAPI 분석 서비스 |
| + HolySheep AI Gateway |
+---------------------------+
1단계: Tardis 데이터 포맷과 수집 파이프라인
Tardis의 가장 큰 장점은 raw L2/L3 오더북 델타를 그대로 제공한다는 점입니다. 바이낸스의 경우 한 건의 L2 델타가 다음 구조를 가집니다.
{
"exchange": "binance",
"symbol": "BTCUSDT",
"timestamp": "2024-01-15T00:00:00.123456Z",
"local_timestamp": "2024-01-15T00:00:00.234567Z",
"side": "buy",
"price": 42150.12,
"amount": 0.015,
"id": 123456789
}
저는 Rust로 작성한 수집 워커를 사용하지만, 빠르게 시작하려면 다음 Python 스크립트도 충분합니다.
import asyncio
import gzip
import httpx
import json
from datetime import datetime, timedelta
from clickhouse_driver import Client
TARDIS_BASE = "https://api.tardis.dev/v1"
HOLYSHEEP_BASE = "https://api.holysheep.ai/v1"
HOLYSHEEP_KEY = "YOUR_HOLYSHEEP_API_KEY" # 가입 시 발급
ch = Client(host="clickhouse.internal", port=9000, database="tardis")
async def download_tardis(symbol: str, day: str, kind: str = "trades"):
"""Tardis에서 gzip CSV 스트리밍 다운로드"""
url = f"{TARDIS_BASE}/data-feeds/{symbol}/{day}/{kind}.csv.gz"
async with httpx.AsyncClient(timeout=300) as c:
async with c.stream("GET", url) as r:
r.raise_for_status()
buffer = b""
async for chunk in r.aiter_bytes(1024 * 256):
buffer += chunk
if len(buffer) > 8 * 1024 * 1024:
yield from parse_csv(buffer)
buffer = b""
def parse_csv(buf: bytes):
"""gzip 해제 후 즉시 튜플 리스트로 변환"""
raw = gzip.decompress(buf).decode("utf-8")
for line in raw.strip().split("\n")[1:]: # skip header
ts, sym, side, price, amount, tid = line.split(",")
yield (
datetime.fromisoformat(ts.replace("Z", "+00:00")),
sym, side, float(price), float(amount), int(tid),
)
async def ingest_day(symbol: str, day: str):
rows = []
async for row in download_tardis(symbol, day):
rows.append(row)
if len(rows) >= 50_000:
ch.execute(
"INSERT INTO trades_binance (ts, symbol, side, price, amount, trade_id) VALUES",
rows,
)
rows.clear()
if rows:
ch.execute("INSERT INTO trades_binance VALUES", rows)
if __name__ == "__main__":
asyncio.run(ingest_day("binance-futures", "2024-01-15"))
2단계: ClickHouse 스키마 설계 (핵심)
스키마 설계가 전체 성능의 80%를 결정합니다. 저는 다음 세 가지 원칙을 지킵니다.
- 정렬 키(PARTITION BY)는 시계열 컬럼 + 심볼로 복합 구성 → 동일 심볼의 시계열 스캔을 sequential read로 처리
- LowCardinality로 enum-like 컬럼 최적화 → 디스크 IO 30~50% 절감
- Codec(ZSTD + Delta)으로 시계열 중복 압축 → 트레이드 가격의 경우 12배 압축 달성
-- 트레이드 (체결) 테이블
CREATE TABLE trades_binance (
ts DateTime64(3, 'UTC'),
symbol LowCardinality(String),
side Enum8('buy' = 1, 'sell' = 2),
price Float64 CODEC(Delta(8), ZSTD(9)),
amount Float64 CODEC(ZSTD(9)),
trade_id UInt64,
is_buyer_maker UInt8
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (symbol, ts)
TTL ts + INTERVAL 5 YEAR
SETTINGS index_granularity = 8192;
-- L2 오더북 스냅샷 (5초 간격 재생성용 베이스)
CREATE TABLE orderbook_l2_snapshot (
ts DateTime64(3, 'UTC'),
symbol LowCardinality(String),
side Enum8('bid' = 1, 'ask' = 2),
price Float64,
amount Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (symbol, ts, side, price);
-- L2 델타 (raw, 가장 대용량)
CREATE TABLE orderbook_l2_delta (
ts DateTime64(6, 'UTC'),
local_ts DateTime64(6, 'UTC'),
symbol LowCardinality(String),
side Enum8('buy' = 1, 'sell' = 2),
price Float64,
amount Float64,
update_id UInt64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (symbol, ts)
TTL ts + INTERVAL 3 YEAR;
3단계: 성능 튜닝 실전 노트
저가 운영하면서 얻은 5가지 핵심 튜닝 포인트입니다.
3-1. 배치 INSERT는 50,000행 단위
ClickHouse는 1~10만 행 단위 INSERT에서 최적 처리량을 보입니다. 너무 작으면 네트워크 round-trip 오버헤드, 너무 크면 메모리 OOM. 제 환경에서는 50,000행/배치가 sweet spot입니다.
3-2. async_insert + fsync 비활성
SET async_insert = 1;
SET wait_for_async_insert = 0;
SET async_insert_max_data_size = 10485760; -- 10MB
초기 수집 단계에서 fsync를 비활성화하면 120MB/s → 480MB/s로 4배 처리량 향상을 확인했습니다. 단, 내구성이 필요한 운영 테이블에는 사용하지 마세요.
3-3. ClickHouse Keeper로 ZooKeeper 의존성 제거
클러스터를 운영한다면 ZooKeeper 대신 ClickHouse Keeper를 쓰세요. 메모리 사용량이 70% 적고 메타데이터 작업이 3배 빠릅니다.
3-4. 동시성 제어: max_insert_block_size
여러 워커가 동시에 INSERT할 때 max_insert_block_size = 1048576로 두면 머지 충돌이 줄어듭니다. 기본값(1,048,576)이지만 명시적으로 설정해 두는 것이 운영 안정성에 유리합니다.
3-5. 벤치마크 결과 (32 vCPU / 64GB / NVMe)
| 작업 | 기본 설정 | 튜닝 후 | 개선율 |
|---|---|---|---|
| 단일 거래소 일일 트레이드 INSERT (10GB) | 4분 12초 | 1분 03초 | 4.0배 |
| 30일 OHLCV 집계 쿼리 | 820ms | 87ms | 9.4배 |
| L2 스냅샷 재구성 (1초 간격, 1일) | 6.4초 | 0.92초 | 7.0배 |
| 심볼 간 상관관계 분석 (1년 데이터) | 42초 | 5.8초 | 7.2배 |
| 디스크 사용량 | 1.2TB | 312GB | 74% 절감 |
4단계: HolySheep AI로 시장 인사이트 자동 생성
데이터웨어하우스가 쌓이면 다음 질문이 자연스럽게 따라옵니다. "방금 들어온 1분봉 패턴이 과거 어느 구간과 가장 비슷한가?", "롱숏 비율이 임계치를 넘어섰을 때 자동으로 분석 리포트를 받고 싶다." 이를 LLM으로 처리할 때, 저는 HolySheep AI를 게이트웨이로 사용합니다. 이유는 단일 API 키로 GPT-4.1, Claude, Gemini, DeepSeek를 모두 호출할 수 있고, 특히 DeepSeek V3.2가 한국어 금융 분석에 100만 토큰당 42센트로 매우 저렴하기 때문입니다.
import httpx
import asyncio
from datetime import datetime, timedelta
HOLYSHEEP_URL = "https://api.holysheep.ai/v1/chat/completions"
HOLYSHEEP_KEY = "YOUR_HOLYSHEEP_API_KEY"
async def ai_market_briefing(summary: dict) -> str:
"""ClickHouse에서 추출한 1분봉 통계를 LLM에 전달"""
payload = {
"model": "deepseek-chat", # DeepSeek V3.2, $0.42/MTok
"temperature": 0.2,
"max_tokens": 800,
"messages": [
{"role": "system", "content":
"당신은 10년 경력의 암호화폐 트레이딩 애널리스트입니다. "
"주어진 OHLCV·펀딩비·롱숏 비율 통계를 바탕으로 한국어 브리핑을 5줄로 작성하세요. "
"숫자는 절대 바꾸지 마세요."},
{"role": "user", "content":
f"심볼: {summary['symbol']}\n"
f"1분봉 종가 시퀀스(최근 60개): {summary['closes']}\n"
f"거래량 변화율: {summary['vol_change_pct']}%\n"
f"펀딩비: {summary['funding']}%\n"
f"롱/숏 비율: {summary['long_short']}\n"
f"분석 시작: {datetime.utcnow().isoformat()}Z"}
]
}
headers = {"Authorization": f"Bearer {HOLYSHEEP_KEY}",
"Content-Type": "application/json"}
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(HOLYSHEEP_URL, json=payload, headers=headers)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
사용 예: ClickHouse에서 1분봉 추출 → LLM 분석
async def on_new_minute(symbol: str):
rows = ch.execute(
"SELECT ts, open, high, low, close, volume "
"FROM ohlcv_1m WHERE symbol = %(s)s "
"AND ts > now() - INTERVAL 60 MINUTE ORDER BY ts",
{"s": symbol},
)
closes = [r[4] for r in rows]
brief = await ai_market_briefing({
"symbol": symbol,
"closes": closes,
"vol_change_pct": 12.4,
"funding": 0.012,
"long_short": 1.42,
})
print(brief)
asyncio.run(on_new_minute("BTCUSDT"))
실제 운영에서 측정한 지연 시간은 다음과 같습니다 (네트워크: 서울 ↔ 도쿄 리전, 1000토큰 입력 기준).
| 모델 | 입력 단가 (100만 토큰당) | 평균 지연 | P99 지연 | 품질 (5점 만점) |
|---|---|---|---|---|
| DeepSeek V3.2 | $0.42 | 820ms | 1.4초 | 4.2 |
| Gemini 2.5 Flash | $2.50 | 640ms | 1.1초 | 4.4 |
| GPT-4.1 | $8.00 | 1.1초 | 2.3초 | 4.7 |
| Claude Sonnet 4.5 | $15.00 | 1.4초 | 2.9초 | 4.9 |
대량 자동화에는 DeepSeek, 의사결정 리포트에는 Claude Sonnet 4.5를 쓰고, 코드를 생성해야 할 땐 GPT-4.1을 쓰는 식으로 단일 HolySheep 키로 모델을 전환합니다. 결제도 한국 카드로 가능해 별도 해외 결제 수단이 필요 없습니다.
5단계: 운영 시 자주 발생하는 오류와 해결책
제가 실제로 겪었던 이슈 중 공유할 만한 것들을 정리했습니다.
오류 1: DB::Exception: Memory limit (total) exceeded
30일치 L2 델타를 한 번에 SELECT할 때 발생합니다. 64GB 메모리 머신에서도 단일 쿼리가 50GB 이상 잡아먹는 경우가 있습니다.
-- 잘못된 쿼리 (전체 메모리에 로드)
SELECT * FROM orderbook_l2_delta
WHERE symbol = 'BTCUSDT' AND ts BETWEEN '2024-01-01' AND '2024-01-30';
-- 해결 1: SETTINGS로 메모리 상한 명시
SELECT * FROM orderbook_l2_delta
WHERE symbol = 'BTCUSDT' AND ts BETWEEN '2024-01-01' AND '2024-01-30'
SETTINGS max_memory_usage = 40000000000,
max_threads = 8,
max_block_size = 65536;
-- 해결 2: 샘플링으로 줄이기 (정확도 99% 유지)
SELECT * FROM orderbook_l2_delta
SAMPLE 1/10
WHERE symbol = 'BTCUSDT' AND ts BETWEEN '2024-01-01' AND '2024-01-30';
오류 2: Tardis 429 Too Many Requests
여러 일자를 동시에 다운로드할 때 IP 단위로 rate limit이 걸립니다. 저는 지수 백오프 + jitter로 해결했습니다.
import asyncio, random
async def robust_download(url: str, max_retry: int = 7):
backoff = 1.0
for attempt in range(max_retry):
try:
async with httpx.AsyncClient(timeout=300) as c:
r = await c.get(url)
if r.status_code == 429:
raise httpx.HTTPStatusError("rate-limited", request=r.request, response=r)
r.raise_for_status()
return r.content
except (httpx.HTTPError, httpx.TimeoutException) as e:
sleep_for = backoff + random.uniform(0, 1)
print(f"retry {attempt+1}/{max_retry}, sleeping {sleep_for:.2f}s")
await asyncio.sleep(sleep_for)
backoff = min(backoff * 2, 60)
raise RuntimeError(f"Failed to download {url}")
오류 3: Partitions 폭발로 머지 실패
일부 사용자가 PARTITION BY toDate(ts)로 설정해 일별 파티션을 만들면 한 달에 30개, 5년에 1,800개 파티션이 생겨 머지 스케줄러가 압도당합니다. 월 단위 파티션이 표준이며, 5년 운영 후에도 파티션 수를 60개 이내로 유지할 수 있습니다.
-- 잘못된 설계
PARTITION BY toDate(ts)
-- 올바른 설계
PARTITION BY toYYYYMM(ts)
-- 만약 일별이 꼭 필요하면
PARTITION BY (symbol, toDate(ts)) -- 단, symbol 수가 적어야 함
오류 4: L2 델타 timestamp 불일치 (거래소 vs 로컬)
Tardis는 timestamp(거래소 서버 시계)와 local_timestamp(수신 측 시계)를 모두 제공합니다. 마이크로초 정밀 백테스트에서는 local_timestamp을 사용해야 네트워크 지연이 보정됩니다. timestamp만 사용하면 백테스트 슬리피지가 비현실적으로 작게 나옵니다.
-- 비추천
ORDER BY (symbol, timestamp)
-- 추천
ORDER BY (symbol, local_timestamp)
오류 5: Kafka → ClickHouse consumer에서 중복 INSERT
Kafka consumer가 재시작되면 동일 오프셋을 재처리할 수 있습니다. 트레이드 ID에 Unique 인덱스를 걸어 멱등성을 확보하세요.
CREATE TABLE trades_binance_dedup AS trades_binance
ENGINE = ReplacingMergeTree(trade_id)
PARTITION BY toYYYYMM(ts)
ORDER BY (symbol, ts);
이런 팀에 적합 / 비적합
적합한 팀
- 암호화폐 HFT·마켓메이킹 전략을 운영하며 TB 이상 시세 데이터가 필요한 팀
- 백테스트 인프라를 직접 구축하고 싶은 퀀트 펀드·프로토콜 팀
- 여러 거래소의 데이터를 통합 분석해야 하는 리서치 팀
- ClickHouse 운영 경험이 있거나 도입을 검토 중인 데이터 엔지니어
비적합한 팀
- 일봉·시간봉만 필요하신 분 (DuckDB + Parquet이 100배 가볍습니다)
- 실시간 호가 윈도우가 필요하신 분 (이 경우 WebSocket을 직접 받는 별도 경로 권장)
- 운영 인력이 1명 이하인 소규모 팀 (초기 세팅에 1~2주 소요)
가격과 ROI
| 구성 요소 | 셀프 호스팅 비용 (월) | 관리형 SaaS 비용 (월) | 절감 효과 |
|---|---|---|---|
| Tardis Professional 플랜 | $249 | $249 | 동일 |
| ClickHouse 4샤드 × 2레플리카 (NVMe 4TB) | $680 (Hetzner) | $2,400 (ClickHouse Cloud) | 72% ↓ |
| Kafka 3 brokers | $210 | $620 (Confluent) | 66% ↓ |
| AI 분석 (DeepSeek V3.2, 월 50M 토큰) | $21 | $420 (OpenAI 직접) | 95% ↓ |
| 합계 | $1,160/월 | $3,689/월 | 연간 $30,348 절감 |
셀프 호스팅으로 1년 운영 시 약 $30,000를 절약할 수 있으며, HolySheep 게이트웨이를 통해 AI 분석 비용만으로도 추가 $4,800/년을 절감합니다.
왜 HolySheep AI를 선택해야 하나
- 로컬 결제: 해외 신용카드·해외 결제 수단 없이 한국 카드로 결제 가능
- 단일 키 멀티 모델: GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2를 하나의 API 키로 전환하며, 코드 한 줄 변경 없이 모델만 교체 가능
- 업계 최저 단가: DeepSeek V3.2 100만 토큰당 42센트(OpenAI 직접 결제 대비 약 1/20)
- 가입 시 무료 크레딧 제공으로 초기 PoC 비용 0원
- 안정적 연결: 글로벌 백본 + 다중 리전 failover로 99.95% 가용성
마무리: 다음 단계
이 글에서 다룬 아키텍처는 제가 실제로 2023년부터 운영해 온 프로덕션 시스템입니다. 처음 시작하실 때는 다음 순서를 권장합니다.
- Tardis 무료 샘플(1일치)로 파이프라인 검증
- 단일 노드 ClickHouse에 1개월치 데이터 적재 후 쿼리 패턴 검증
- HolySheep AI에 가입해 DeepSeek V3.2로 시장 브리핑 PoC 진행
- 검증 후 클러스터 확장 (샤드 추가, Kafka 도입)
데이터의 양이 늘수록 아키텍처는 단순할수록 좋습니다. 위에서 소개한 5가지 오류 사례만 미리 회피해도 첫 1개월의 시행착오를 크게 줄일 수 있습니다.
```