etcd 스토리지 백엔드(bbolt)의 내부 구조와 동작 방식
etcd v3는 물리적인 디스크 스토리지 엔진으로 bbolt(구 BoltDB)를 채택했습니다. bbolt는 Go 언어로 순수하게 작성된 키-값(Key-Value) 임베디드 데이터베이스입니다. 복잡한 SQL 쿼리 기능이나 서버-클라이언트 통신 구조를 배제하고, 오직 로컬 파일 시스템의 단일 파일(.db)에 데이터를 안전하고 빠르게 읽고 쓰는 데 극도로 최적화된 아키텍처를 가지고 있습니다. 쿠버네티스의 모든 선언적 상태 데이터가 최종적으로 기록되는 이 로우 레벨(Low-level) 스토리지의 내부 구조를 해부해 봅니다.

1. B+Tree 기반의 디스크 데이터 구조
bbolt는 내부적으로 B+Tree(B-Tree의 변형) 자료구조를 사용하여 데이터를 항상 키(Key) 기준으로 정렬된 상태로 디스크에 저장합니다. 트리 구조는 최상위의 루트(Root) 노드에서 시작하여 중간 경로를 안내하는 브랜치(Branch) 노드를 거쳐, 실제 데이터(Key와 Value의 쌍)가 저장되는 최하단의 리프(Leaf) 노드로 구성됩니다.
일반적인 B-Tree와 달리 B+Tree 구조를 채택한 이유는 디스크 I/O를 최소화하고 검색 성능을 극대화하기 위해서입니다. 브랜치 노드에는 실제 Value가 들어가지 않고 오직 탐색을 위한 Key 정보만 담기므로, 하나의 노드(디스크 페이지) 안에 훨씬 더 많은 분기 포인터를 저장할 수 있습니다. 결과적으로 트리의 깊이(Depth)가 매우 얕게 유지되어 데이터 접근 속도가 극적으로 향상됩니다. 또한 모든 리프 노드들이 링크드 리스트(Linked List) 형태로 연결되어 있어, 특정 범위의 데이터를 순차적으로 긁어오는 범위 검색(Range Scan) 작업에 탁월한 성능을 발휘합니다.
2. Memory Mapping (mmap)과 Zero-Copy 아키텍처
bbolt 아키텍처의 가장 눈에 띄는 특징 중 하나는 운영체제의 메모리 맵 파일(Memory-Mapped File, mmap) 기술을 전적으로 활용한다는 점입니다.
bbolt는 프로세스가 구동될 때 데이터베이스 파일 전체를 가상 메모리 공간에 매핑합니다. 일반적인 데이터베이스가 디스크에서 시스템 메모리로 데이터를 읽어오고, 이를 다시 애플리케이션의 버퍼로 복사(Copy)하는 무거운 I/O 과정을 거치는 반면, bbolt는 다릅니다. 커널의 페이지 캐시(Page Cache)에 적재된 데이터를 포인터 연산만으로 애플리케이션 계층에서 직접 읽어옵니다.
이러한 Zero-Copy 읽기 메커니즘 덕분에 Kube-apiserver가 etcd를 향해 쏟아내는 수많은 상태 조회(GET, LIST) 요청들은 사실상 디스크 I/O를 전혀 발생시키지 않으며, 순수한 메모리 접근 속도와 동일한 수준으로 초고속 처리가 가능합니다.
3. Copy-on-Write (CoW) 기반의 트랜잭션과 무결성 보장
분산 시스템의 저장소는 전원 차단이나 프로세스 크래시 등 어떠한 악조건 속에서도 데이터 파일의 손상(Corruption)이 발생해서는 안 됩니다. bbolt는 동시성 제어와 장애 복구를 위해 Copy-on-Write (CoW) 기법을 핵심 트랜잭션 모델로 사용합니다.
쓰기 트랜잭션이 발생하여 특정 리프 노드의 데이터를 수정하거나 추가해야 할 때, bbolt는 절대 기존의 디스크 블록(페이지)을 덮어쓰지 않습니다. 대신 해당 노드의 복사본을 메모리의 새로운 빈 페이지에 생성하여 데이터를 수정합니다. 자식 노드의 물리적 위치가 변경되었으므로, 이를 가리키는 부모 브랜치 노드 역시 새로운 복사본을 만들어 업데이트하고, 이 과정이 최상위 루트 노드까지 연쇄적으로 올라갑니다.
트랜잭션이 최종 커밋(Commit)될 때, 이 새롭게 구성된 트리의 변경 사항들을 디스크에 한 번에 동기화(fsync)합니다. 이 과정 도중 시스템이 다운되더라도 기존 트리 구조와 데이터는 단 1바이트도 훼손되지 않은 채 디스크에 온전히 보존되어 있으므로, 재시작 시 완벽한 데이터 무결성을 보장할 수 있습니다. 또한 쓰기 작업이 기존 데이터를 건드리지 않기 때문에 읽기 트랜잭션은 락(Lock) 없이 완전히 독립적으로 병렬 수행될 수 있습니다.
4. etcd MVCC 모델과 bbolt의 결합
etcd는 데이터를 덮어쓰지 않고 변경 이력을 모두 남기는 다중 버전 동시성 제어(MVCC) 로직을 사용합니다. 그렇다면 쿠버네티스의 논리적인 리소스 키(예: /registry/pods/default/nginx)가 물리적인 bbolt 스토리지에는 어떻게 맵핑되어 저장될까요?
핵심은 bbolt 트리의 Key로 쿠버네티스의 리소스 이름이 아닌 '리비전(Revision) 번호'를 사용한다는 것입니다. 사용자가 파드를 생성하거나 수정하면 etcd는 전역적인 리비전 번호(예: 1004)를 발급합니다. 그리고 bbolt에 데이터를 기록할 때, bbolt의 Key를 이 리비전 번호(1004)로 지정하고, Value 영역에 실제 쿠버네티스의 리소스 이름(/registry/pods/...)과 상태 데이터를 직렬화하여 통째로 저장합니다.
즉, bbolt 내부의 디스크 데이터는 철저하게 시간순(리비전이 증가하는 순서)으로 정렬된 B+Tree를 형성합니다. 이 아키텍처적 결단 덕분에 쿠버네티스의 핵심인 Watch 메커니즘이 눈부시게 빠른 속도를 냅니다. Kube-apiserver가 "리비전 1000번 이후의 모든 변화를 알려줘"라고 요청하면, etcd는 bbolt 트리에서 1000번 Key를 O(1)에 가깝게 찾아낸 뒤, 그 뒤에 연결된 리프 노드들을 순차적으로 읽어(Sequential Read) 압도적인 속도로 이벤트를 스트리밍할 수 있습니다.
5. 내부 페이지 관리와 Freelist 아키텍처
디스크 파일은 운영체제의 특성에 맞추어 보통 4KB 단위의 고정된 페이지(Page)들로 나뉘어 관리됩니다. bbolt의 단일 데이터베이스 파일 내부에는 트리 구조를 메타데이터로 관리하는 메타(Meta) 페이지, 탐색을 위한 브랜치(Branch) 페이지, 실제 데이터를 담는 리프(Leaf) 페이지, 그리고 프리리스트(Freelist) 페이지가 존재합니다.
앞서 설명한 CoW 트랜잭션 방식으로 인해, 데이터가 한 번 수정되거나 삭제되면 기존 데이터를 담고 있던 구버전 페이지들은 트리에서 연결이 끊어져 더 이상 참조되지 않는 '고아 페이지'가 됩니다. bbolt는 스토리지 낭비를 막기 위해 이런 버려진 페이지들의 번호를 추적하여 Freelist라는 특별한 자료구조에 모아둡니다. 새로운 쓰기 트랜잭션이 발생하여 공간을 요구할 때, 파일의 크기를 무조건 끝에 덧붙여 늘리는 대신 이 Freelist를 먼저 뒤져 재사용 가능한 빈 페이지를 할당받아 덮어쓰게 됩니다.
쿠버네티스 환경처럼 파드의 생성과 삭제, 컨트롤러의 리더 갱신 하트비트가 쉴 새 없이 발생하는 클러스터에서는 수많은 페이지가 버려지고 재사용되는 과정이 반복됩니다. 이 과정에서 필연적으로 연속된 빈 공간을 찾기 어려워지는 내부 파편화(Fragmentation)가 발생하며, 디스크 I/O 성능이 서서히 저하됩니다. 정기적으로 빈 페이지들을 물리적으로 잘라내고 데이터베이스 파일을 다시 촘촘하게 재조립하는 조각 모음 작업이 필수적인 이유가 바로 이 Freelist 기반의 페이지 재사용 메커니즘에 기인합니다.
'1. K8s Core & Architecture > 1.1. 컨트롤 플레인 (Control Plane) 심층 분석' 카테고리의 다른 글
| API 서버의 페이징(Paging) 및 청킹(Chunking)을 통한 대규모 데이터 조회 (0) | 2026.03.19 |
|---|---|
| 클러스터 상태(State)와 명세(Spec)의 차이: 선언적 API의 본질 (0) | 2026.03.19 |
| 쿠버네티스 API 서버 우회 접속(Proxy) 메커니즘 이해하기 (0) | 2026.03.18 |
| 가비지 컬렉션(Garbage Collection) 컨트롤러의 동작 원리 (0) | 2026.03.18 |
| Kube-apiserver의 캐싱 아키텍처와 성능 향상 원리 (0) | 2026.03.18 |