Tinybird가 사용되지 않지만 삭제되지도 않은 S3 객체를 정리해 클라우드 스토리지 비용을 약 45% 절감하고, 그 과정에서 드러난 운영 안전성과 복구 절차의 교훈을 공유합니다.
Tinybird는 ClickHouse, Inc.와 제휴, 연관 또는 후원을 받지 않습니다. ClickHouse®는 ClickHouse, Inc.의 등록 상표입니다.
아무도 읽지 않는 수 페타바이트 규모의 S3 객체 비용을 지불하고 있었습니다. 지난달 저희는 이를 정리했고 객체 스토리지 비용은 약 45% 감소했습니다. 그 과정에서 실제 데이터를 거의 잃을 뻔하기도 했습니다. 무슨 일이 있었는지 말씀드리겠습니다.
Tinybird에서는 객체 스토리지 기반 의 대규모 ClickHouse® 클러스터를 운영합니다. 대규모 분산 스토리지 시스템을 운영하는 많은 팀처럼, 저희도 복제, 일관성, 장애 복구에 대해 많은 시간을 들여 고민해 왔습니다.
배경에서 계속 커지고 있던 문제 중 하나는 클라우드 스토리지 쓰레기였습니다. 더 이상 사용되지 않지만 삭제되지도 않은 객체들이었습니다. 그저 그대로 남아 비용만 계속 발생시키고 있었습니다.
지난 한 달 동안 저희는 이 쓰레기가 어디서 발생하는지 조사하고, 정리 도구를 개선했으며, 매월 수만 달러 규모의 스토리지 비용을 회수했습니다. 하지만 솔직히 말하면 가장 큰 성과는 비용 절감이 아니었습니다. 그 과정에서 운영 안전성과 복구 절차를 크게 강화할 수 있었다는 점이 더 중요했습니다.
zero-copy replication을 사용할 때 ClickHouse는 데이터를 원격에 저장하고 여러 복제본이 동일한 파일을 참조할 수 있도록 합니다.
이를 안전하게 조정하기 위해 ClickHouse는 ZooKeeper에 각 데이터 파트를 어떤 복제본이 사용 중인지 설명하는 메타데이터를 유지합니다. 실제로 이러한 참조는 분산 참조 카운터처럼 동작합니다. 복제본은 새 파트가 연결되거나 복제될 때 참조를 생성하고, 파트가 사라질 때 참조를 제거하며, 모든 참조가 사라진 뒤에야 객체를 삭제할 수 있습니다.
각 복제 테이블에 대해 ClickHouse는 활성 복제본도 별도로 추적합니다. 정상적인 상황에서는 활성 복제본 목록과 데이터 파트를 참조하는 복제본 목록이 동일합니다.
하지만 저희는 그렇지 않은 상황들을 발견했습니다.
복제본이 제거될 때(이제는 저희의 셀프서비스 클러스터 관리 도구 덕분에 흔한 작업입니다) 복제 메타데이터는 올바르게 사라지지만, 특정 조건에서는 일부 zero-copy 참조가 남아 있게 됩니다. 복제본은 더 이상 존재하지 않는데도 객체는 여전히 참조 중인 것처럼 보입니다. 이 오래된 참조 때문에 객체의 참조 수가 영원히 0에 도달하지 못합니다.
이 시점에서 그 객체는 사실상 고아가 됩니다. 어떤 살아 있는 복제본도 그것을 필요로 하지 않지만, 스토리지 정리 작업은 절대 그것을 삭제하지 못합니다.
시간이 지나면서 이러한 불일치가 누적되어 삭제되지 않은 스토리지가 매우 큰 규모로 쌓였습니다.
작은 규모에서는 고아 객체가 대체로 무해하지만, 큰 규모에서는 천천히 누적되며 매우 비싸질 때까지 탐지하기가 어렵습니다.
쓰레기 식별 도구를 개선하고 이를 여러 클러스터에 실행한 뒤, 저희는 서로 다른 환경 전반에 걸쳐 삭제 가능한 스토리지 객체가 수 페타바이트에 이른다는 사실을 발견했습니다. 전체적으로 이 정리 작업은 클라우드 스토리지 비용의 약 45% 에 해당했습니다.
변경 전후의 클라우드 스토리지 모습입니다.
비용 절감은 이야기의 일부일 뿐이었습니다. 훨씬 더 어려운 과제는 어떤 객체가 실제로 삭제해도 안전한지 판단하는 일이었습니다.
저희는 이미 고아가 된 원격 객체를 식별하도록 설계된 내부 가비지 컬렉터를 가지고 있었습니다. 이 과정은 세 단계로 동작했습니다.
하지만 도구를 다시 점검한 뒤 몇 가지 빈틈을 발견했습니다.
이 문제들을 수정한 뒤 저희는 클러스터별로 컬렉터를 실행하기 시작했고, 마침내 스토리지 쓰레기 누적 상태를 훨씬 더 정확하게 파악할 수 있게 되었습니다.
정리 프로세스를 넓게 배포한 직후, 저희는 가장 우려하던 실패를 실제로 겪었습니다. 정상 데이터를 (일시적으로) 삭제한 것입니다.
저희는 이 작업에 들어갈 때 백업 복구 역량에 자신이 있었습니다. 핵심 목표는 뭔가 잘못되더라도 데이터 유실이 발생하지 않도록 보장하는 것이었습니다. 기술적으로는 맞았습니다. 실제로 모든 것을 복구해 냈습니다. 하지만 저희가 과소평가했던 것은 그 복구가 실제로 얼마나 복잡한가 하는 점이었습니다.
삭제 로직 자체가 잘못된 것은 아니었지만, 컬렉터는 분석을 시작하기 전에 활성 원격 객체의 완전한 스냅샷을 구축하는 데 의존합니다. 일부 실행에서는 클러스터에서 메타데이터를 수집하는 단계가 조용히 타임아웃되었습니다. 그 결과 살아 있는 데이터셋에 대한 불완전한 스냅샷이 만들어졌습니다.
이후 단계는 그대로 계속 진행되었고, 그 결과 일부 객체는 컬렉터가 스냅샷 생성 중 그것들을 보지 못했다는 이유만으로 사용되지 않는 것으로 잘못 분류되었습니다.
저희는 ClickHouse가 그 객체들을 읽으려 시도했고 객체 스토리지가 "file not found" 오류를 반환하기 시작한 뒤에야 일부 정상 데이터가 삭제 대상 쓰레기로 표시되었다는 사실을 알게 되었습니다.
복구는 무엇을 삭제할지 결정하는 일만큼이나 어려운 작업이었습니다.
핵심적인 어려움은 원격 객체가 원래 파일 경로와 아무 관련이 없는 불투명한 식별자로 표현된다는 점이었습니다. S3 blob, ClickHouse 파트, 백업, mutation, 중복 제거된 파일 사이의 관계를 재구성하려면 여러 시스템의 메타데이터를 결합해야 했습니다.
예를 들어 디스크에서 ClickHouse는 테이블 UUID를 사용해 파트를 저장합니다.
disks/<disk_name>/store/<table_uuid[0:2]>/<table_uuid>/<part_name>/
그리고 data/ 아래에서는 동일한 파트가 데이터베이스와 테이블로 표현됩니다.
data/<database_name>/<table_name>/<part_name>/
백업은 두 번째 구조를 따르고, 실행 중 스토리지 경로는 보통 첫 번째 구조를 사용합니다.
복구 중에는 객체 스토리지에서 누락된 것으로 보고된 blob을 백업 경로에 직접 매핑할 수 없습니다. 먼저 테이블 UUID를 사용하는 런타임 레이아웃에서 데이터베이스와 테이블 이름을 사용하는 백업 레이아웃으로 변환해야 합니다. 그다음 백업 체인 내부에서 해당 파트와 파일을 찾아야 합니다.
여기에 몇 가지 추가적인 복잡성이 더해져 복구는 더욱 난해해졌습니다.
어떤 경우에는 누락된 객체 하나를 복원한 뒤에야 같은 파트에서 추가로 누락된 객체들이 드러나기도 했습니다.
안전하게 복구하기 위해 저희는 다음을 수행해야 했습니다.
조사 과정은 수백만 개 객체와 서로 독립적인 많은 메타데이터 소스에 걸친 관계를 재구성하는 작업을 포함했습니다. 도구의 도움(LLM 포함)은 사고 대응 중 복구 과정의 일부를 상당히 가속했습니다.
결국 저희는 영향을 받은 모든 데이터를 되찾았고 복구 절차도 검증했습니다. 이 모든 과정은 저희의 절차를 훨씬 더 견고하게 만들었습니다.
이 프로젝트에서 가장 큰 도전은 쓰레기를 식별하는 일이 아니었습니다. 감히 삭제하기 전에 데이터가 정말 사용되지 않는다는 사실을 증명하는 일이었습니다.
복제 테이블과 zero-copy를 사용하는 ClickHouse 클러스터에서 삭제 워크플로는 그것이 의존하는 메타데이터 스냅샷만큼만 신뢰할 수 있습니다. 저희의 경우에는 불완전한 수집 스냅샷 때문에 삭제 로직의 나머지 부분은 올바르더라도 정상 객체가 쓰레기로 분류되었습니다.
이 경험은 시스템의 여러 부분을 개선하도록 만들었습니다.
또한 저희는 가비지 컬렉터 자체를 어떻게 운영할지 다시 고민하고 있습니다. 현재는 스토리지 절감 효과와 운영 리스크 사이의 균형을 맞추기 위한 적절한 실행 주기를 여전히 찾고 있습니다.
한편 새로운 고아 객체가 생성되는 것을 방지하기 위한 수정 사항은 이미 클러스터 전반에 배포되고 있습니다.