스냅샷
고친 사람 github-actions[bot]
스냅샷은 어느 한 시점의 데이터 상태를 고정해 남겨 두는 일입니다. 원본이 그 뒤에 바뀌어도 남겨 둔 상태는 따라 바뀌지 않습니다. 그래서 나중에 그 시점의 모습을 다시 읽거나 그 시점으로 되돌아갈 수 있습니다.
쉽고 빠른 이해
스냅샷은 어느 시점의 데이터 상태를 고정해 두는 일입니다. 큰 변경을 하기 전에 디스크의 지금 상태를 하나 남겨 두는 것이 그 예입니다.
이게 없으면 계속 바뀌는 데이터의 온전한 사본을 얻기 어렵습니다. 처음부터 끝까지 복사하는 동안에도 원본이 바뀌기 때문입니다. 그러면 사본의 앞부분과 뒷부분이 서로 다른 때의 값이 됩니다.
이렇게 돕니다:
- 쓰기를 멈추고 지금 시점에 표시를 남깁니다
- 데이터는 그때 복사하지 않고 원본과 나눠 씁니다
- 원본에서 바뀌는 조각만 옛 값과 새 값으로 갈라집니다
대가는 공간과 쓰기 비용입니다. 원본이 많이 바뀔수록 갈라진 조각이 쌓입니다. 같은 디스크 안에서 데이터를 나눠 쓰는 스냅샷은 그 디스크가 망가지면 원본과 함께 망가집니다.
상세
단체 사진을 한 장 찍었다고 해 봅시다. 사진을 찍고 나면 사람들이 흩어지든 옷을 갈아입든 사진 속 모습은 변하지 않습니다. 나중에 사진을 꺼내 보면 그날 그 순간이 그대로 있습니다.
스냅샷은 범위를 정한 데이터의 한 시점 상태를, 그 뒤의 변경과 무관하게 다시 읽을 수 있게 남긴 것입니다. 스냅샷을 만드는 일을 흔히 「뜬다」고 합니다. 이름도 순간을 찍어 남기는 사진에서 왔습니다.
대표적인 쓰임은 이렇습니다. 데이터베이스가 든 디스크를 크게 손보기 전에 스냅샷을 하나 떠 둡니다. 작업이 잘못되면 그 스냅샷의 시점으로 되돌아갑니다.
스냅샷이 고정하는 범위
고정하는 단위는 범위를 정한 상태 전체입니다. 디스크 볼륨 하나, 파일 시스템 하나, 데이터베이스 하나가 그런 단위입니다. 범위는 뜰 때 고르는 것이고, 그 범위 안의 것들은 함께 고정됩니다.
범위 안의 것들이 서로 맞물려 있기 때문입니다. 주문 표와 재고 표가 그런 예입니다. 주문 표만 어제 상태로 되돌리면 재고와 아귀가 안 맞습니다. 둘을 한 시점으로 같이 고정해야 되돌린 상태가 쓸모 있습니다.
복사를 미루는 방법
한 시점을 남기는 가장 단순한 방법은 그 순간 데이터를 전부 복사하는 것입니다. 이 방법은 시간과 공간이 데이터 크기만큼 듭니다.
그래서 많은 스냅샷은 복사를 미룹니다. 뜨는 순간에는 지금 데이터를 가리키는 표시만 남깁니다. 원본과 스냅샷은 같은 데이터를 나눠 쓰는 상태로 시작합니다. 갓 뜬 스냅샷이 공간을 거의 안 쓰는 이유입니다.
원본의 어느 조각이 바뀔 때에야 그 조각이 옛 값과 새 값으로 갈라집니다. 이렇게 바뀌는 순간까지 복사를 미루는 기법을 흔히 copy-on-write(쓰기 시 복사)라고 부릅니다. 갈라지는 방식은 하나가 아닙니다. 복사를 미루지 않고 미리 사본을 만들어 두는 방식도 있습니다. 「갈래」 절이 셋으로 가릅니다.
스냅샷이 아무것도 따로 남기지 않는 경우도 있습니다. 데이터베이스는 질의문 하나에 「이 시점까지 확정된 변경만 보인다」는 규칙을 걸어 한 시점을 보여 줍니다. 이때 스냅샷은 사본이 아니라 무엇이 보일지를 정하는 규칙입니다.
읽기 전용과 쓰기 가능
스냅샷을 고쳐 쓸 수 있느냐는 구현마다 다릅니다. ZFS(OpenZFS 프로젝트의 파일 시스템) 스냅샷은 읽기만 됩니다. 리눅스 커널에서 블록 장치를 다루는 층인 device-mapper 의 스냅샷은 고쳐 쓸 수 있습니다. 파일 시스템 Btrfs 의 스냅샷도 기본이 고쳐 쓸 수 있는 쪽입니다. 고쳐 쓸 수 있는 스냅샷은 같은 데이터에서 갈라져 나온 여러 판을 따로 두는 데 씁니다.
고쳐 쓰는 쪽에서도 스냅샷에 쓴 내용은 원본에 닿지 않습니다. 그래서 「읽기 전용」은 정의에 들지 않습니다. 정의가 지키는 것은 하나입니다. 원본이 바뀌어도 스냅샷이 따라 바뀌지 않는다는 점입니다.
한 시점으로 자르는 순서
스냅샷을 뜨는 동안에도 쓰기는 들어옵니다. 어느 쓰기까지 담고 어느 쓰기부터 뺄지가 한 줄로 갈려야 합니다. 그 시점 전에 끝난 쓰기는 전부 담겨야 합니다. 그 뒤의 쓰기는 하나도 안 담겨야 합니다. 이렇게 한 시점으로 아귀가 맞는 상태를 일관성 있는 상태라고 합니다.
저장 장치만 시점을 잘라서는 모자랄 때가 있습니다. 응용 프로그램이나 운영체제가 메모리의 캐시에 들고 있던 값은 아직 디스크에 안 내려갔습니다. 이런 값은 스냅샷에 안 담깁니다.
그래서 뜨기 직전에 순서를 밟습니다. 스냅샷을 조정하는 쪽이 응용 프로그램에 열린 작업을 마치고 캐시를 내려보내라고 알립니다. 그다음 파일 시스템에 새 쓰기를 멈추고 남은 데이터를 디스크에 쓰게 합니다. 이것을 파일 시스템 동결이라고 합니다. 그 사이 쓰려는 프로세스는 동결이 풀릴 때까지 기다립니다.
그림은 조정하는 쪽이 캐시를 내려보내게 하는 때부터 쓰기를 다시 받기까지를 순서대로 놓은 것입니다.
sequenceDiagram
participant 조정 as 조정하는 쪽
participant 앱 as 응용 프로그램
participant 파일 as 파일 시스템
participant 장치 as 저장 장치
조정->>앱: 열린 작업을 마치고 캐시를 내려보내라
앱->>파일: 남은 값을 내려보낸다
조정->>파일: 새 쓰기를 멈추고 남은 데이터를 디스크에 써라
조정->>장치: 스냅샷을 만들어라
장치->>장치: 이 시점에 스냅샷 표시를 남긴다
조정->>파일: 쓰기를 다시 받아라
쓰기가 멈춰 있는 구간은 파일 시스템을 얼린 때부터 쓰기를 다시 받을 때까지입니다. 데이터를 통째로 복사하지 않으므로 표시를 남기는 단계 자체는 짧습니다. 그 앞의 내려보내기는 내려보낼 양에 따라 길어질 수 있습니다. 동결이 정해진 시간을 넘기면 스냅샷 만들기를 중단하는 구현도 있습니다.
조정을 누가 맡는지는 구현마다 다릅니다. 스냅샷 요청을 받은 장치 계층이 그 장치의 파일 시스템을 알아서 얼리는 구현도 있습니다.
스냅샷을 뜨는 때
흔한 때는 둘입니다. 하나는 큰 변경이나 위험한 작업 전에 되돌아갈 시점을 만들 때입니다. 다른 하나는 서비스를 멈추지 않고 한 시점의 사본을 얻을 때입니다. 백업을 뜨는 동안 밑에서 데이터가 안 바뀌게 고정해 두는 것이 그 쓰임입니다.
그 밖의 쓰임도 있습니다. 쌓인 기록을 버리고 그 시점의 상태 하나로 갈음할 때도 뜹니다. 같은 데이터를 한 벌만 두는 중복 제거에 쓰는 구현도 있고, 깨끗하지 않게 멈춘 뒤 빠르게 다시 서려고 내부에서 쓰는 구현도 있습니다.
반대로 디스크 고장에 대비한 보관은 같은 디스크 안의 스냅샷만으로 안 됩니다. 이 선은 「경계」 절이 긋습니다.
배경
계속 바뀌는 데이터를 백업해야 합니다. 파일을 처음부터 끝까지 복사하는 동안에도 쓰기는 계속 들어옵니다. 그러면 사본 속 파일들이 서로 다른 시점의 것이 됩니다. 먼저 복사한 주문 표와 나중에 복사한 재고 표가 서로 다른 때의 값이 되는 식입니다.
복사하는 동안 서비스를 멈추면 시점은 맞습니다. 대신 데이터가 클수록 멈추는 시간이 길어집니다. 사본도 원본만큼 공간을 차지합니다. 그래서 필요한 것은 셋이었습니다. 서비스를 멈추지 않을 것, 거의 즉시 끝날 것, 한 시점으로 맞물릴 것입니다.
이 셋을 채우는 일에 붙은 이름이 스냅샷입니다.
예시
스냅샷은 저장 장치 · 버전 관리 · 데이터베이스 · 분산 합의에 나타납니다. 무엇을 한 시점으로 고정하는지가 저마다 다릅니다.
ZFS 파일 시스템
ZFS 는 파일 시스템이나 볼륨 하나를 고정합니다. 파일이나 디렉터리 하나는 단위가 아닙니다. 아래 명령은 pool/home/bob 파일 시스템의 지금 상태를 yesterday 라는 이름으로 남깁니다.
zfs snapshot pool/home/bob@yesterday
남긴 스냅샷은 그 파일 시스템 맨 위의 .zfs/snapshot 디렉터리에서 필요할 때 마운트되어 열립니다. ZFS 스냅샷은 읽기 전용입니다. 아주 빨리 만들어집니다. 처음에는 공간을 더 쓰지 않습니다. 원본 데이터가 바뀔수록 원본과 나눠 쓰지 못하는 데이터가 늘어 공간을 더 차지합니다.
한 시점의 기준도 정해 둡니다. 그 시점 전에 성공적으로 끝난 시스템 콜의 변경을 전부 담습니다.
이름은 마음대로 붙일 수 있습니다. 날짜를 이름으로 삼아 일주일치를 돌려 쓰는 방법이 있습니다. 가장 오래된 @7daysago 를 지우고, @6daysago 를 @7daysago 로 바꾸는 식으로 남은 이름을 하루씩 밀어 올린 뒤, @today 를 새로 뜹니다.
Git 커밋
Git 은 자기 데이터를 개념상 작은 파일 시스템의 스냅샷이 이어진 것처럼 다룹니다. 커밋할 때마다 모든 파일이 그 순간 어떤 모습인지를 사진 찍듯 남깁니다. 그리고 그 스냅샷을 가리키는 참조를 저장합니다.
바뀌지 않은 파일은 다시 저장하지 않습니다. 앞서 저장한 같은 파일로 가는 링크만 둡니다. 파일마다 바뀐 내용을 쌓아 가는 델타 기반 버전 관리와 갈리는 대목입니다. 디스크 블록을 떠 두는 것이 아니라 데이터를 다루는 방식이 스냅샷을 닮았다는 뜻입니다.
PostgreSQL 조회
PostgreSQL 은 데이터를 따로 복사해 두지 않고 한 시점을 보여 줍니다. 방식의 이름은 MVCC(Multiversion Concurrency Control, 다중 버전 동시성 제어)입니다. 질의문 하나하나가 얼마 전 어느 때의 데이터베이스 모습을 봅니다. 여기서 스냅샷은 사본이 아니라 무엇이 보일지를 정하는 것입니다.
트랜잭션은 여러 읽기와 쓰기를 한 덩어리로 묶는 단위입니다. 스냅샷이 질의문 하나에 걸리는지 트랜잭션 전체에 걸리는지는 격리 수준이 정합니다. 격리 수준은 동시에 도는 트랜잭션끼리 서로를 얼마나 가리나를 정하는 설정입니다. 격리 수준이 높은 쪽에서는 트랜잭션이 사는 동안 같은 스냅샷을 씁니다. 그 사이 다른 트랜잭션이 같은 행을 고쳐도 그 트랜잭션에는 안 보입니다.
두 세션이 똑같은 시점을 봐야 할 때는 pg_export_snapshot() 을 부릅니다. 이 함수는 지금 트랜잭션의 스냅샷을 저장하고 그 스냅샷을 가리키는 문자열을 돌려줍니다. 다른 트랜잭션은 그 문자열을 SET TRANSACTION 명령에 넘겨 같은 스냅샷을 가져옵니다. 가져올 수 있는 것은 내보낸 트랜잭션이 끝나기 전까지입니다.
Raft 로그 압축
Raft 는 여러 서버가 같은 로그를 같은 순서로 실행하게 맞추는 합의 알고리즘입니다. 로그는 그대로 두면 끝없이 자랍니다. 길어질수록 공간을 차지합니다. 재시작 때 다시 실행하는 시간도 늘어납니다.
그래서 각 서버가 지금 시스템 상태 전체를 안정된 저장소의 스냅샷에 씁니다. 그 시점까지의 로그는 버립니다. 여기서 한 시점으로 고정하는 것은 디스크 블록이 아니라 서버가 들고 있는 상태입니다.
너무 뒤처진 서버에는 로그 대신 스냅샷을 보냅니다. 로그를 나눠 주는 서버인 리더가 InstallSnapshot 이라는 RPC(Remote Procedure Call, 원격 프로시저 호출)로 보냅니다. 정상 운영에서는 드문 일입니다. 유난히 처리가 더딘 서버나 새로 합류한 서버에서 생깁니다.
스냅샷을 쓰는 동안에도 새 요청을 받으려고 운영체제의 copy-on-write 를 쓰기도 합니다. 프로세스를 복제하는 리눅스의 fork 가 그 수단입니다. fork 는 메모리를 바로 복사하지 않고 두 프로세스가 나눠 쓰게 둡니다. 한쪽이 고치는 쪽만 갈라지므로 복제한 쪽에는 복제한 때의 메모리가 남습니다. 그 메모리가 상태 전체의 스냅샷이 됩니다.
갈래
축은 하나입니다. 원본이 바뀔 때 새 값과 옛 값 중 무엇을 어디에 쓰느냐입니다. 방식은 셋입니다. 무엇이 비싸지는지가 저마다 다릅니다. 저장 장치는 데이터를 일정한 크기의 조각으로 나눠 담습니다. 이 절은 그 조각 하나를 블록이라고 부릅니다.
옛 값을 옮겨 두는 방식
원본 블록을 덮어쓰기 전에 그 블록의 옛 값을 읽어 따로 마련한 영역에 옮겨 둡니다. 그다음 원본 블록에 새 값을 씁니다. 스냅샷을 읽을 때는 옮겨 둔 옛 값과 안 바뀐 원본 블록을 합쳐 보여 줍니다. LVM(Logical Volume Manager, 논리 볼륨 관리자)이 lvcreate -L 100M --snapshot 처럼 크기를 주고 만드는 스냅샷이 이 방식입니다.
flowchart TD
W["쓰기 요청 · 블록 3을 3′로 바꾼다"] --> R["원본에서 블록 3의 옛 값을 읽는다"]
R --> C["옛 값 3을 따로 마련한 영역에 옮겨 둔다"]
C --> O["원본 블록 3 위에 3′를 덮어쓴다"]
O --> S["스냅샷 읽기 · 옮겨 둔 블록 3과 원본의 안 바뀐 블록들을 합친다"]
비싸지는 것은 쓰기입니다. 쓰기 한 번마다 읽기 한 번과 쓰기 한 번이 더 붙습니다. 바뀌는 데이터가 많으면 비싸질 수 있습니다.
옛 값을 담는 영역은 원본보다 작은 경우가 많습니다. 그 영역이 차면 옛 값을 지킨다는 보장도 끝납니다. device-mapper 스냅샷은 그때 쓸모없어져 꺼지고 오류를 냅니다.
새 값을 딴 곳에 쓰는 방식
원본 블록을 아예 덮어쓰지 않습니다. 새 값은 빈 블록에 쓰고 원본 쪽 가리킴만 새 블록으로 옮깁니다. 스냅샷은 옛 블록을 계속 가리킵니다. 블록을 가리키는 표는 뿌리 하나에서 갈라지는 나무 꼴로 쌓입니다. 뜨는 순간에는 그 뿌리 하나만 복제하면 복제한 뿌리가 그 아래 블록 전부를 원본과 함께 가리키므로 거의 즉시 끝납니다.
flowchart TD
A["지금의 원본 · 뿌리 가리킴"] --> U["안 바뀐 블록들"]
A --> N["블록 3′ · 새 값"]
P["스냅샷 · 복제한 뿌리 가리킴"] --> U
P --> D["블록 3 · 옛 값"]
ZFS 와 Btrfs 의 스냅샷이 이 방식입니다. LVM 의 씬 스냅샷도 같습니다.
씬 풀은 여러 볼륨이 필요한 만큼 블록을 꺼내 쓰는 공용 공간입니다. 씬 볼륨은 그 풀에서 쓴 만큼만 블록을 받는 볼륨입니다. 씬 볼륨을 원본으로 두고 lvcreate --snapshot 에서 크기 옵션을 빼면 씬 스냅샷이 됩니다. 같은 명령에 크기를 주면 앞의 첫째 방식이 됩니다.
비싸지는 것은 공간입니다. 원본이 바뀔수록 원본은 안 쓰고 스냅샷만 붙든 블록이 늘어납니다. 씬 풀에서는 블록을 여럿이 나눠 쓰므로 씬 볼륨 하나를 지워도 공간이 바로 안 돌아올 수 있습니다. 바뀐 값을 원본 볼륨이 아니라 다른 볼륨에 적어 두는 Windows 의 VSS(Volume Shadow Copy Service, 볼륨 섀도 복사 서비스)에서는 읽기 요청이 많을 때 비싸질 수 있습니다. 지금 값을 읽으려면 다른 볼륨에 적힌 바뀐 블록과 원본의 안 바뀐 블록을 합쳐야 하기 때문입니다.
전부 복사하는 방식
원본과 사본을 미러 볼륨으로 묶어 같은 내용을 함께 쓰게 둡니다. 스냅샷 시점에 그 미러를 끊습니다. 끊은 뒤로 둘은 서로 독립입니다. 사본은 끊은 때의 데이터를 읽기 전용으로 간직합니다.
미러를 끊어 사본을 얻는 이 방법을 분할 미러(split mirror)라고 합니다. 그렇게 얻은 사본은 전체 복사본 또는 클론이라고 부릅니다.
flowchart TD
subgraph B["미러를 끊기 전"]
W1["쓰기 요청"] --> V1["원본 볼륨"]
W1 --> V2["사본 볼륨 · 같은 내용"]
end
subgraph A["미러를 끊은 뒤"]
W2["쓰기 요청"] --> V3["원본 볼륨"]
V4["사본 볼륨 · 끊은 때의 데이터"]
end
B --> A
비싸지는 것은 공간입니다. 원본과 같은 내용을 담는 볼륨을 한 벌 더 둡니다. 대신 뜨는 순간에 복사할 것이 없습니다. 미리 같은 내용을 함께 써 두었기 때문입니다. 끊은 뒤에는 원본에 쓰기가 들어와도 스냅샷 쪽에서 할 일이 없습니다.
copy-on-write 라는 이름의 갈림
같은 이름이 서로 다른 방식을 가리킵니다. 위에 이름이 나온 구현들만 모아 보면 이렇습니다.
| 구현 | 쓰는 방식 | 그 구현이 부르는 이름 |
|---|---|---|
| device-mapper 스냅샷 | 옛 값을 옮겨 두는 방식 | copy-on-write |
| ZFS · Btrfs 스냅샷 | 새 값을 딴 곳에 쓰는 방식 | copy-on-write |
| VSS | 새 값을 딴 곳에 쓰는 방식 | redirect-on-write(쓰기 시 재배치) |
그래서 copy-on-write 라는 말만 보고 어느 방식인지 가를 수 없습니다. 이 절은 이름 대신 하는 일로 불렀습니다.
경계
스냅샷은 백업일까요? 그 자체로는 아닙니다. 스냅샷은 시점을 고정할 뿐입니다. 백업이 되느냐는 고정한 상태를 원본과 다른 장치로 옮겨 두느냐가 정합니다.
같은 장치에서 블록을 나눠 쓰는 스냅샷은 백업이 아닙니다. Btrfs 스냅샷은 원본과 같은 데이터 블록을 나눠 쓰는 상태로 시작합니다. 그 블록이 디스크 불량 섹터나 실수로 망가지면 스냅샷과 원본이 함께 망가집니다. 이런 스냅샷이 백업에 하는 일은 따로 있습니다. 전체 백업을 뜨는 동안 밑에서 아무것도 안 바뀌게 파일 시스템 상태를 고정해 주는 일입니다.
다른 곳에 옮겨 둔 스냅샷은 백업입니다. Amazon EBS(Elastic Block Store) 스냅샷은 볼륨과 따로 Amazon S3(Simple Storage Service)에 저장됩니다. 이 스냅샷을 증분 백업이라고 부릅니다. 가장 최근 스냅샷 뒤에 바뀐 블록만 저장한다는 뜻입니다.
이 스냅샷에서도 시점을 고정하는 일과 옮기는 일은 갈려 있습니다. 시점은 만드는 즉시 정해집니다. 데이터를 S3 로 옮기는 일은 뒤에 끝납니다. 바뀐 블록이 많으면 몇 시간이 걸릴 수 있습니다. 그동안에도 볼륨은 계속 쓸 수 있습니다.
관련 항목
스냅샷을 떠 두는 저장 계층
파일 시스템 · 볼륨 · 블록 장치 · 디스크 · 가상 머신 · ZFS · Btrfs · LVM · device-mapper · Amazon EBS · LVM 스냅샷 · ZFS 스냅샷
스냅샷이 원본과 갈라질 때 쓰는 기법
쓰기 시 복사 · 쓰기 시 재배치 · 분할 미러 · 씬 풀 · 참조 카운트 · 하드링크 · 블록 · 중복 제거
스냅샷을 한 시점으로 자를 때 거치는 단계
파일 시스템 동결 · fsfreeze · VSS · 일관성 · 캐시 · 플러시
스냅샷에서 이어지는 작업
롤백 · 클론 · 복원 · 증분 전송
같은 목적으로 스냅샷 대신 쓰는 수단
백업 · 전체 백업 · 증분 백업 · 덤프 · 복제 · 시점 복구 · 아카이빙
버전 관리에서 스냅샷을 담는 단위
커밋 · 트리 객체 · 브랜치 · 태그 · Git · 머클 트리 · 델타 기반 버전 관리
데이터베이스가 읽기 시점을 고정하는 장치
MVCC · 스냅샷 격리 · 직렬가능 스냅샷 격리 · 격리 수준 · 트랜잭션 · 리비전 · 낡은 데이터 · PostgreSQL
복제 로그를 줄이려고 스냅샷을 쓰는 장치
Raft · 로그 압축 · InstallSnapshot · 체크포인트 · 선행 기록 로그 · 컴팩션 · 변경 데이터 캡처
메모리 상태를 파일로 떠 두는 장치
Redis · RDB · AOF · fork · 지속성
쌓인 스냅샷을 줄일 때 거는 규칙
보존 기간 · TTL · 축출 · 콜드 스토리지 · 계층형 저장소 · 롤링 스냅샷
스냅샷과 이름만 겹치는 이웃
SNAPSHOT 버전 · Maven · 릴리스 버전 · 개발 브랜치
다른 이름: snapshot · 스냅숏 · point-in-time copy