How shredding JSON is giving Logfire 1000x query speedups
JSON 분해 저장으로 Logfire 쿼리 속도를 최대 1000배 높인 방법
Logfire가 OpenTelemetry 속성 JSON을 자주 쓰이거나 큰 키별 컬럼으로 나누는 동적 분해 저장(Dynamic Shredding)을 도입합니다. 불필요한 JSON 다운로드와 파싱을 줄여 일부 쿼리를 30초 이상에서 1초 미만으로 단축하며, 2026년 2월부터 고객에게 적용할 계획입니다.
- 주제
AI 요약
Logfire는 OpenTelemetry 데이터를 Parquet 파일로 오브젝트 스토리지에 저장합니다. 타임스탬프나 span 이름은 고정 스키마 컬럼에 들어가지만, 사용자 ID나 HTTP 상태 코드 같은 속성은 span마다 달라 JSON 한 컬럼에 모아 저장했습니다. 초기에는 Jiter로 JSON을 빠르게 파싱했지만, 파싱 속도만 높여서는 데이터 다운로드와 처리 자체를 피할 수 없었습니다. 오브젝트 스토리지에서는 큰 JSON을 읽는 데 드는 I/O와 다운로드 시작 지연이 쿼리 시간을 좌우했습니다.
정적 분해 저장의 한계
기존 방식은 http.status_code 같은 자주 쓰는 속성을 미리 지정해 별도 타입 컬럼으로 뽑는 정적 분해 저장(Static Shredding)이었습니다. 컬럼 통계로 관련 없는 파일이나 row group을 건너뛰고 필요한 컬럼만 읽을 수 있지만, 지정하지 않은 user_id나 my_llm_response를 조회하면 JSON 컬럼 전체를 읽고 파싱해야 합니다. 큰 값 하나가 JSON 컬럼을 부풀리면 그 값을 조회하지 않는 쿼리도 영향을 받습니다. 정적으로 만든 내부 컬럼이 사용자 쿼리 결과에 노출되고, 값이 전부 비어 있는 컬럼에도 처리 오버헤드가 생깁니다.
파일별로 속성을 나누는 동적 방식
새 방식은 수집과 압축 과정에서 실제 데이터를 살펴보고, 자주 나타나거나 전체 데이터 크기가 큰 속성을 별도 컬럼으로 추출합니다. 추출한 속성은 강한 타입을 가진 Parquet 컬럼으로 저장하고, 나머지는 JSON 컬럼에 남깁니다. 데이터가 파일마다 다르므로 컬럼도 파일별로 달라집니다. 사용자는 기존처럼 attributes->>'user_id' 같은 표현을 쓰며, 내부에서는 해당 속성 컬럼으로 쿼리를 바꿉니다. 결과적으로 관련 컬럼만 읽고 zone map 통계로 파일과 row group을 건너뛰며, JSON 파싱 없이 값을 처리합니다. 압축 효율도 좋아집니다.
Logfire 운영 데이터의 99%는 attributes에 최상위 키가 128개 미만이었고, 중앙값은 약 32개였습니다. 이를 바탕으로 파일마다 전체 크기가 큰 상위 128개 키까지 추출하도록 기본값을 정했습니다. 무작위 UUID를 키로 만들어 컬럼이 늘어나는 상황도 제한합니다. 추출하지 않은 드문 키는 JSON에 남으므로 조회는 가능하지만 기존처럼 파싱 비용이 듭니다. 속성 값의 타입은 정수에서 실수, 이후 JSON으로 넓히며, 불리언과 문자열도 서로 다른 타입이 섞이면 JSON으로 저장합니다. 문자열 값만 있다면 JSON 파싱 없이 문자열 컬럼으로 읽습니다.
DataFusion 변경과 적용 계획
Logfire는 DataFusion 실행 엔진을 손봐 JSON 접근 표현식을 Parquet 읽기 단계까지 밀어 넣는 표현식·조건 푸시다운을 구현했습니다. 이 작업은 DataFusion 52.1.0에 반영됐습니다. GROUP BY 표현식까지 스캔 단계로 전달하는 작업은 복잡한 표현식 재작성과 최적화가 필요해 2026년 2월 반영을 예상합니다. 적용 과정에서는 먼저 쿼리 경로만 동적 분해 방식으로 바꾸고, 쓰기 경로의 정적 분해는 유지해 위험을 낮춥니다. 쿼리 경로를 검증한 뒤 파일 기록에도 동적 분해를 적용합니다. 기능은 2026년 2월부터 고객에게 배포할 계획입니다.
현재는 zone map을 활용하며, 앞으로 텍스트 검색용 역색인과 고카디널리티 컬럼용 Bloom filter를 검토합니다. 장기적으로는 남은 속성을 Parquet Variant 형식에 저장하는 방안을 추진합니다. 쿼리 사용 패턴을 수집해 어떤 속성을 분해하고 색인할지 정하는 방법도 고려하고 있습니다.
Lobsters 반응
- @0x2ba22e11 — 간단히 말하면, ‘분해 저장’은 모든 JSON 객체를 파싱하지 않고 빠르게 조회하도록 필드를 객체에서 복사해 각각의 컬럼에 넣는 방식입니다. 처음에는 미리 지정한 필드 목록에 적용하는 ‘정적 분해’를 썼고, 새 방식은 Parquet 파일을 만들 때 관측된 패턴을 바탕으로 복사할 필드를 고릅니다.
- @ryan-duve — ‘Shredding’은 업계에서 통용되는 용어인가요? 제가 일하는 곳에서는 몇 년째 같은 일을 하면서 ‘Exploding’이라고 부릅니다. 같은 개념입니다. 중첩 JSON 객체는 이중 밑줄 경로 구분자를 써서 펼치고, 배열은 부모 행과 외래 키로 연결하는 새 테이블을 만듭니다. 32개나 128개가 큰 키 수인지도 잘 모르겠습니다. 저희는 동적으로 수만 개 컬럼을 펼칩니다. 예외는 실제 JSON 경로에 UUID 같은 값이 들어 행 수만큼 카디널리티가 높아지는 필드뿐입니다. 5년 동안 펼쳐 왔는데, 이제 이런 폭증을 감지하고 값을 모으는 방식으로 바꿔야 할지 고민할 정도로 문제가 생기기 시작했습니다. 이 문제를 고려해 본 분이 있다면 찾아보신 자료나 경험을 알려주세요.
- @peter-leonov — 공유해 주셔서 감사합니다! ClickHouse의 JSON 지원처럼 형식 안에서 확장하는 방식과, 여기처럼 형식 바깥에서 처리하는 방식을 비교해 봤나요?
원문: Pydantic / 번역·요약: Trawling