tracio.ai의 30ms 미만 파이프라인을 구축한 방법
신호 수집부터 방문자 ID 산출까지 30ms 이내: Go, ClickHouse, Redis와 분산 처리를 활용한 아키텍처를 소개합니다.
tracio.ai의 기기 식별 엔진을 구축하기 시작했을 때, 우리에게는 타협 불가능한 요구사항이 하나 있었습니다. 암호화된 신호를 수신해 방문자 ID를 반환하기까지 전체 파이프라인이 95번째 백분위수 기준 30밀리초 이내에 완료되어야 한다는 것이었습니다. 이 글에서는 그 목표를 달성하기 위해 구축한 아키텍처를 상세히 설명합니다.
파이프라인 개요
식별 파이프라인은 다섯 단계로 구성됩니다. 신호 복호화, 신호 정규화, 해시 계산, 신원 확인, 응답 직렬화입니다. 각 단계는 독립적으로 최적화되며, 병렬 실행이 가능한 단계는 병렬로 실행됩니다. 전체 예산은 30ms이며 대략 다음과 같이 배분됩니다. 복호화 2ms, 정규화 3ms, 해싱 2ms, 신원 확인 20ms, 직렬화 1ms. 남는 2ms는 버퍼입니다.
신호 복호화는 클라이언트 측에서 암호화된 전송을 역으로 되돌립니다. 우리는 하드웨어 가속을 지원하는 Go의 암호화 패키지를 사용하며, 이는 일반적인 4KB 페이로드의 복호화를 1ms 이내에 완료합니다. 정규화는 신호 JSON을 파싱하고 타입을 검증하며 플랫폼별 변환을 적용합니다. 예를 들어 버전별 노이즈를 제거하도록 사용자 에이전트 문자열을 정규화합니다.
분산 신원 확인
신원 확인, 즉 이 기기가 이전에 관측된 적이 있는지를 판별하는 단계는 지연 시간에 가장 민감한 단계입니다. 우리는 기기 프로필을 Redis에 저장하며, 분산 키 라우팅 계층을 통해 클러스터 전반에 샤딩합니다. 라우팅은 하드웨어 계층 지문(fingerprint)을 기반으로 키를 분배하므로, 동일한 기기에 대한 조회는 항상 같은 Redis 노드로 향합니다.
우리의 샤딩 구현은 가상 노드(물리 노드당 150개)를 사용해 균등한 분배를 보장합니다. 노드가 추가되거나 제거될 때 재매핑이 필요한 키는 전체의 1/N뿐이며, 여기서 N은 노드 수입니다. 우리는 이 라우팅 계층을 Go로 구현했으며, 조회 시간은 O(log n)이고 할당(allocation)은 전혀 발생하지 않습니다.
신원 저장소로서의 Redis
우리가 다른 대안(Memcached, ScyllaDB, DynamoDB)이 아닌 Redis를 선택한 이유는 일관되게 밀리초 미만의 응답 시간을 제공하고 복잡한 데이터 구조를 지원하기 때문입니다. 각 기기 프로필은 Redis 해시로 저장되며, 각 신호 계층의 해시, 방문자 ID, 마지막 관측 타임스탬프, 신뢰도 메타데이터에 해당하는 필드를 갖습니다.
신원 확인 쿼리는 단일 HGETALL 호출과 그 뒤에 이어지는, 수신된 신호 해시를 저장된 해시와 비교하는 작업으로 이루어집니다. 하드웨어 계층이 일치하면 기존 방문자 ID를 높은 신뢰도로 반환합니다. 소프트웨어 계층만 일치하면 신호 수준 데이터의 유사도 비교를 수행해 브라우저가 업데이트된 동일 기기인지 판별합니다. 아무것도 일치하지 않으면 새 방문자 ID를 생성합니다.
이벤트 저장을 위한 ClickHouse
모든 식별 이벤트는 ClickHouse에 비동기로 기록됩니다. 우리는 삽입을 배치로 묶는 버퍼드 라이터를 사용합니다. 100ms 동안, 또는 이벤트가 1,000건 누적될 때까지 중 먼저 도달하는 시점까지 이벤트를 모읍니다. 이 배치 처리는 매우 중요한데, ClickHouse는 개별 행 삽입보다 대량 삽입(한 번에 수천 행)에서 최고의 성능을 내기 때문입니다.
우리의 ClickHouse 스키마는 가장 흔한 두 가지 쿼리 패턴에 맞춰 최적화되어 있습니다. 특정 방문자 ID에 대한 모든 이벤트를 조회하는 것과, 시간 구간에 걸쳐 이벤트를 집계하는 것입니다. 우리는 (visitor_id, timestamp)를 기본 키로 하는 MergeTree 엔진을 사용하는데, 이는 빠른 포인트 조회와 효율적인 범위 스캔을 제공합니다. 구체화된 뷰(materialized view)가 사전 집계된 일별·시간별 지표를 유지합니다.
대규모 환경에서 30ms 미만 달성하기
지연 시간 목표를 맞추는 데 결정적이었던 아키텍처 결정이 세 가지 있습니다. 첫째, 파이프라인은 완전한 스트리밍 방식으로, HTTP 요청 본문 전체가 수신되기 전부터 신호 처리를 시작합니다. 둘째, Redis 조회는 지속 연결을 유지하는 커넥션 풀링을 사용해 TCP 핸드셰이크 오버헤드를 제거합니다. 셋째, ClickHouse 쓰기는 완전 비동기로 응답 경로를 절대 막지 않습니다.
초당 5만 건의 부하 테스트에서 우리의 p50 지연 시간은 12ms, p95는 24ms, p99는 38ms입니다. Redis 클러스터 리밸런싱 중에는 p99가 이따금 30ms 목표를 초과하지만, p95는 일관되게 30ms 아래를 유지합니다. 더 엄격한 지연 시간 요구사항을 가진 고객에게는 멀티테넌트 경합을 제거하는 전용 Redis 클러스터를 제공합니다.