etcd 의 snapshot 과 WAL이 무엇일까요? (etcd backup snapshot이 아닌)
etcd 의 snapshot 그리고 WAL 에 대해서 알아봅니다. etcd 는 raft 합의 알고리즘을 기반으로 구현된 key-values 저장소로. raft 알고리즘에서 snapshot 에 대한 내용이 존재합니다. 그것을 etcd 는 어떻게 구현했는지 알아봅니다. (etcdctl snapshot save 를 통해 생성하는 스냅샷과는 조금은 다른 내용입니다)

etcd 에서는 compaction 을 진행하기 이전에, 인메모리에 보관할 Raft 항목의 수를 --snapshot-count
라는 인자로 지정 할 수 있습니다. 로그 항목의 수가 snapshot-count
에 도달하면, 서버는 스냅샷 데이터를 디스크에 저장한 다음, 오래된 항목을 잘라내는 작업을 합니다. 또한 느린 팔로워(랙)가 압축된 이전의 로그를 요청하면 리더는 팔로워가 상태를 덮어쓰도록 스냅샷을 보내게 됩니다.
이 스냅샷은 인메모리에 저장할 로그 항목의 수를 지정하는것으로, 이 값이 크면 클수록 더 많은 로그 항목을 인메모리에 저장하기때문에, 메모리 사용량이 반복적으로 증가하게됩니다.
따라서 snapshot-count
는 메모리 사용량의 증가와, 느린 팔로워의 가용성 사이에서 절충안을 찾아 설정해야 합니다.v3.2
이후부터는 기본 값이 100,000 으로 변경되었습니다. (인메모리에 100,000 개의 로그 항목을 저장하도록)
여기서 DefaultSnapshotCatchupEntries
라는 값이 5000 으로 하드코딩 되어있습니다.
const (
DefaultSnapshotCount = 10000
// DefaultSnapshotCatchUpEntries is the number of entries for a slow follower
// to catch-up after compacting the raft storage entries.
// We expect the follower has a millisecond level latency with the leader.
// The max throughput is around 10K. Keep a 5K entries is enough for helping
// follower to catch up.
DefaultSnapshotCatchUpEntries uint64 = 5000
...
)
이 값은, Raft 스토리지의 항목을 압축 한 이후에, 느린 팔로워가 따라잡을 수 있는 항목의 개수를 의미하는데. 스냅샷을 찍은 이후에 인메모리에 우선 5000개의 항목만을 메모리에 들고있고, 만약 느린 팔로워가 5천개 이상 Lag을 갖고있다면 리더가 스냅샷을 보내서 따라오도록 하는 설정이라고 볼 수 있습니다.
// etcd/server/storage/storage.go
// SaveSnap saves the snapshot file to disk and writes the WAL snapshot entry.
func (st *storage) SaveSnap(snap raftpb.Snapshot) error {
st.mux.RLock()
defer st.mux.RUnlock()
walsnap := walpb.Snapshot{
Index: snap.Metadata.Index,
Term: snap.Metadata.Term,
ConfState: &snap.Metadata.ConfState,
}
// save the snapshot file before writing the snapshot to the wal.
// This makes it possible for the snapshot file to become orphaned, but prevents
// a WAL snapshot entry from having no corresponding snapshot file.
err := st.s.SaveSnap(snap)
if err != nil {
return err
}
// gofail: var raftBeforeWALSaveSnaphot struct{}
return st.w.SaveSnapshot(walsnap)
}
// etcd/server/storage/wal/wal.go
func (w *WAL) SaveSnapshot(e walpb.Snapshot) error {
if err := walpb.ValidateSnapshotForWrite(&e); err != nil {
return err
}
b := pbutil.MustMarshal(&e)
w.mu.Lock()
defer w.mu.Unlock()
rec := &walpb.Record{Type: SnapshotType, Data: b}
if err := w.encoder.encode(rec); err != nil {
return err
}
// update enti only when snapshot is ahead of last index
if w.enti < e.Index {
w.enti = e.Index
}
return w.sync()
}
이 시점에서 etcd 는 두가지의 동작을 하게 됩니다. (위 코드를 보면 알겠지만)
- 현재의 메타 데이터를
.snap
파일로 저장 - 1번 데이터를 포함한 현재 상태(state)를
wal
파일에 저장
따라서 .snap
파일은 데이터베이스가 얼마나 크건, 얼마나 많이 만들었건에 관련 없이 일정한 크기를 갖게 됩니다.
그리고 메모리에서 제거한 데이터(스냅샷)는 wal
파일에 저장합니다.
따라서 느린 팔로워가 compact-index
- snapshot-index
사이에 last-applied-index
를 갖고있는 경우에는 스냅샷을 받아서 리더를 따라가는게 아니라, 리더와 AppendEntries RPC 를 주고 받으면서 따라가고, 그렇지 않은 경우에는 스냅샷을 받아서 처리하게 됩니다. 그리고 snapshot-index
- compact-index
의 값의 차이는 5000으로 고정되어있다고 생각하면 됩니다.
WAL 에 저장되는 데이터들 (Write Ahead Log)
앞서서, Raft Snapshot 도 궁극적으로 WAL 에 저장된다고 했습니다. 여기서는 WAL 에 어떤 데이터들이 어떤 형식으로 저장되는지 설명합니다.
물리적 요소
WAL 로그 파일은 프레임
의 연속적인 내용을 저장하는데, 각 프레임은 아래를 포함하고있습니다.
- LittleEndian : 인코딩된 uint64 형태의 마샬링된(직렬화와 비슷한 개념이라고 생각하면 됩니다)
walpb.Record
의 길이 - Padding : 전체 프레임이 정렬된 크기가 되도록 하는 0 바이트의 패딩
- 마샬링된
walpb.Record
의 데이터
이 파일은 64*10^6 바이트(64MB) 마다 잘라집니다.
논리적인 요소
WAL 로그 파일은 아래와 같은 논리적 계층의 내용을 포함합니다.
Raftpb.Entry
: Raft 의 리더에 의해 복제된 제안(로그 항목 추가하라는 제안). 일부 제안은 커밋된것으로 간주됩니다.Raftpb.HardState(term,commit,vote)
: 커밋되어(과반수 이상에 복제되어서 커밋된) 변경/재정의 되지 않도록 보장되고, 백엔드에 적용 될 수 있는 로그 항목의 index 에 대한 정보, term 정보, 투표 정보walpb.Snapshot(term, index)
: Raft 상태에 대한 주기적인 스냅샷 (DB 내용은 하나도 없고, 스냅샷 로그 인덱스와 Raft 임기 정보만 있습니다.) etcd v3 store 의 경우 데이터는 bbolt 파일에 유지되고, Snapshot 에 임기/인덱스 정보가 반영되면 암시적인 스냅샷이 됩니다. (데이터에 대한 스냅샷 파일을 따로 만드는게 아님)crc32 체크섬
etcdserverpb.Metadata(node_id, cluster_id)
: 로그가 나타내는 클러스터 및 복제를 표현
각각의 WAL-log 파일은 아래 순서대로 빌드됩니다.
- CRC-32 프레임
- 메타데이터 프레임(클러스터, 복제본 IDs)
- 최초의 WAL 파일인 경우
- 빈 Snapshot 프레임 (인덱스, 임기 모두 0 인)
- 최초가 아닌 WAL 파일
- Hardstate 프레임 (임기, 커밋, 투표 정보)
- 항목, hard-state, snapshot record 의 조합
WAL 로그의 인덱스는 아래와 같이 생성, 유지됩니다.
- snapshot 의 인덱스로부터 시작
- 같은 임기(term)에 있는 경우 해당 스냅샷 이후로 순차적으로 증가
- 임기가 변경되면 인덱스가 감소 할 수 있지만, 최신 Hardstate.commit 보다 높은 새로운 값으로 변경 가능
- 새 스냅샷은 반드시 index >= Hardstate.commit 에서 발생해야 하며, 인덱스에 대한 새로운 시퀀스를 생성

정리
WAL 파일은 Write Ahead Log 의 약자로, B-Tree 에서의 보통 쓰기 전 임시로 저장하여 쓰기 실패시 복구하기 위한 용도로 사용되는데, etcd 는 한 단계 더 확장해서, Raft 로 부터 제안된 로그 항목 임시 저장 + Snapshot 정보 저장, 기타 메타데이터 저장(임기, 커밋, 인덱스) 용도로 사용되는 파일입니다.
etcd 의 --snapshot-count
도달시 생기는 000000-000000.snap
형태의 스냅 파일은 일종의 메타데이터를 저장하고 있는 파일이고, 실제로는 WAL에도 저장되며, 이 시점에서의 DB 에 대한 스냅샷 파일을 명시적으로 만드는것이 아닌(만들어서 Disk 에 별도로 저장하는게 아니라) 암시적으로 스냅샷 시점의 임기, 인덱스 값을 기반으로 스냅샷을 논리적으로 갖고있는 것이라고 볼 수 있습니다.
실제로 스냅샷이 필요한 시점 (느린 팔로워의 동기화를 위해서)에서는 암시적인 스냅샷을 실제 물리적인 파일 형태로 만들어서 (*.snap.db
) 팔로워에게 파일을 전달하고 동기화 된 이후에는 삭제하게 됩니다.