TimescaleDB로 27.8억 건 시계열 데이터 다루기

실시간 파이프라인이 안정화된 뒤에는, 그 값들이 쌓이는 쪽 — 대용량 시계열 DB — 이 다음 과제였습니다. 이력·알람·제어로그 3종을 TimescaleDB(PostgreSQL 15) 하이퍼테이블로 전환했고, 조회 시점 기준 누적 27.8억 건(제어 로그만 348만 건)을 365일 보존·3개월 압축 정책으로 운영하고 있습니다.

왜 하이퍼테이블인가

일반 PostgreSQL 테이블에 시계열 데이터를 그대로 쌓으면, 테이블이 커질수록 인덱스 스캔 비용과 vacuum 부담이 함께 커집니다. 하이퍼테이블은 시간 구간별로 데이터를 자동 파티셔닝(청크)해주기 때문에, “최근 데이터 조회”처럼 특정 시간 범위를 조회하는 쿼리가 관련 청크만 스캔하도록 만들 수 있습니다. 설비 이력·알람 조회는 대부분 “최근 N일” 범위를 보는 패턴이라, 이 파티셔닝 구조가 실제 조회 패턴과 잘 맞았습니다.

압축 정책: 653GB → 35GB

3개월이 지난 청크는 압축 정책을 적용해 컬럼 단위로 압축하도록 설정했습니다. 그 결과 저장 용량을 653GB에서 35GB로, 94.6%(약 18.7:1) 절감했습니다. 압축 기준을 3개월로 잡은 이유는, 운영자가 실시간에 가깝게 조회하는 데이터는 대부분 최근 몇 주 이내였고, 그 이후 데이터는 조회 빈도가 급격히 낮아지는 대신 “규정상 보존해야 하는” 요구가 더 크다는 점을 확인했기 때문입니다. 압축된 데이터는 조회 속도는 다소 느려지지만 여전히 SQL로 조회 가능하기 때문에, 조회 빈도와 압축 이득 사이에서 3개월을 기준점으로 잡았습니다.

서비스별 DB 분리, 스키마 분리로는 부족했던 이유

여러 서비스(현장)가 하나의 DB 인스턴스를 공유하는 대신, 서비스 단위로 DB 자체를 분리했습니다. 스키마만 나누는 방식도 고려했지만, 다음 두 가지 이유로 DB 분리를 선택했습니다.

  • 장애 격리: 한 서비스의 대량 쓰기나 락 경쟁이 다른 서비스의 커넥션 풀·쿼리 성능에 영향을 주지 않아야 했습니다. 스키마만 분리하면 커넥션 풀과 리소스는 여전히 공유됩니다.
  • 배포 독립성: 서비스마다 마이그레이션 시점과 스키마 변경 주기가 달랐습니다. DB를 분리하면 한 서비스의 마이그레이션이 다른 서비스에 영향을 주지 않습니다.

대신 서비스별로 마이그레이션 체계를 별도로 구축해야 했고, 새로운 SP나 테이블을 추가할 때 여러 환경(운영/개발/현장별)에 동일하게 반영하는 절차를 문서화해 관리했습니다.

Redis Stream 컨슈머 그룹으로 미러 테이블 동기화

원본 이력은 중앙 테이블에 그대로 적재하고, 화면에서 자주 조회하는 “현재값”만 별도 미러 테이블에 유지하는 이원 구조를 뒀습니다. 미러 테이블은 Redis Stream 컨슈머 그룹을 통해 증분 동기화되는데, 여기서 화면 전용으로 로컬에서 수정되는 컬럼(예: 운영자가 붙인 메모)은 동기화 대상에서 제외했습니다. 그렇지 않으면 원본 쪽 갱신이 화면에서 입력한 로컬 데이터를 덮어써 버리는 문제가 생기기 때문입니다. “어떤 컬럼이 원본을 따라가야 하고, 어떤 컬럼이 화면 쪽 소유인가”를 먼저 구분한 것이 이 동기화 구조의 핵심이었습니다.

Updated: