하드웨어 고급 #4 ZFS 심화 — RAID와 파일시스템이 하나가 될 때
중급 6편에서 RAID 운영의 어두운 면을 다뤘습니다. 리빌드가 가장 위험한 시간이라는 것, 대용량 디스크에서 URE가 RAID5를 위협한다는 것, 스크럽 없이는 불량 섹터가 최악의 순간까지 숨어 있다는 것까지였습니다. 그 글에서 잠깐 이름만 나온 ZFS는 이 문제들의 상당수를 운영 기법이 아니라 구조로 풀었습니다. 이번 글에서는 ZFS가 무엇을 어떻게 다르게 설계했는지, 그리고 그 대가로 무엇을 요구하는지를 정리하겠습니다.
전통 스택의 문제 — 층이 나뉘면 정보도 나뉜다 #
전통적인 스토리지 스택은 층으로 쌓입니다. 맨 아래에 RAID 카드나 mdadm이 디스크들을 하나의 블록 장치로 묶고, 그 위에 LVM 같은 볼륨 관리자가 공간을 나누고, 맨 위에 ext4나 XFS 같은 파일시스템이 파일을 얹습니다. LVM 자체는 RHEL 운영의 영역이라 여기서는 이름만 두고 넘어가겠습니다. 핵심은 각 층이 서로의 사정을 모른다는 점입니다.
- RAID 층은 어느 블록에 데이터가 있는지 모릅니다. 그래서 리빌드는 빈 블록까지 포함해 디스크 전체를 복사합니다. 10%만 쓴 어레이도 리빌드 시간은 100% 쓴 어레이와 같습니다.
- 쓰기 구멍(write hole)이 생깁니다. RAID5의 한 스트라이프를 갱신하려면 데이터 블록과 패리티 블록을 둘 다 써야 하는데, 둘 사이에 전원이 나가면 패리티가 데이터와 어긋난 채 남습니다. RAID 층은 파일시스템의 트랜잭션을 모르니 어느 쓰기가 한 묶음인지 알 길이 없고, 어긋난 패리티는 다음 리빌드 때 잘못된 데이터를 만들어 냅니다.
- 조용한 손상을 잡지 못합니다. 디스크가 오류 신고 없이 잘못된 비트를 돌려주면(비트 로트), RAID 층은 그것을 그대로 위로 올립니다. 패리티라는 정답지를 들고 있으면서도 평소 읽기에서는 대조하지 않기 때문입니다.
각 층은 자기 일을 성실히 하지만, 층 사이의 정보 단절이 구조적인 허점을 낳습니다.
ZFS의 답 — 한 층으로 합치고, 제자리에 덮어쓰지 않는다 #
ZFS는 RAID, 볼륨 관리, 파일시스템을 한 소프트웨어로 합쳤습니다. 디스크들을 vdev(가상 장치)로 묶고, vdev들을 풀(pool)로 합치고, 그 위에 파일시스템을 만드는데, 이 전부가 한 층이라 파일시스템이 어느 블록이 살아 있는지, 지금 쓰는 블록들이 한 트랜잭션인지를 RAID 수준까지 알고 있습니다. 명령으로 보면 전통 스택의 여러 단계가 두 줄로 줄어듭니다.
# 디스크 6장을 패리티 2장의 RAIDZ2 풀로 묶기
zpool create tank raidz2 sda sdb sdc sdd sde sdf
# 풀 위에 파일시스템 만들기 (포맷·마운트 설정이 따로 없습니다)
zfs create tank/data파티셔닝, RAID 구성, 포맷, fstab 등록이 각각의 도구로 나뉘어 있던 일이 풀 생성과 파일시스템 생성으로 끝납니다. 만든 파일시스템은 즉시 마운트되어 사용할 수 있습니다.
여기에 CoW(Copy-on-Write)가 더해집니다. ZFS는 데이터를 제자리에 덮어쓰지 않습니다. 수정할 내용을 빈 블록에 새로 쓰고, 쓰기가 끝난 뒤에 포인터를 새 블록으로 바꿉니다. 포인터 전환은 원자적이라 전원이 어느 순간에 나가도 디스크에는 항상 일관된 상태(전환 전 아니면 전환 후)만 남습니다. 갱신 도중이라는 어중간한 상태가 디스크에 존재하지 않으니 쓰기 구멍이 원천적으로 사라지고, 부팅 후 fsck로 파일시스템을 점검할 필요도 없습니다.
체크섬과 자가 치유 — 읽을 때마다 검증한다 #
ZFS는 모든 블록의 체크섬을 그 블록 자신이 아니라 부모 블록의 포인터 옆에 저장합니다. 블록을 읽을 때마다 체크섬을 대조하므로, 디스크가 오류 신고 없이 잘못된 데이터를 돌려줘도 그 자리에서 들통납니다. 체크섬을 데이터와 같은 곳에 두지 않는 덕에, 블록 전체가 엉뚱한 내용으로 바뀌는 손상도 잡아냅니다.
미러나 RAIDZ처럼 중복이 있는 구성이라면 탐지에서 끝나지 않습니다. 체크섬이 어긋난 사본을 발견하면 다른 사본에서 올바른 데이터를 읽어 응답하고, 망가진 쪽을 그 데이터로 다시 써서 고칩니다. 이것이 자가 치유(self-healing)입니다. 전통 RAID에서 “어느 쪽이 정답인지 모르는” 상황이, ZFS에서는 체크섬이라는 심판이 있어 항상 판정이 납니다. 치유 이력은 zpool status의 CKSUM 열에 남습니다.
NAME STATE READ WRITE CKSUM
tank ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
sda ONLINE 0 0 0
sdb ONLINE 0 0 3CKSUM 카운트가 0이 아닌 디스크는 오류 신고 없이 잘못된 데이터를 돌려준 적이 있다는 뜻입니다. 숫자가 계속 늘면 케이블, 컨트롤러, 디스크 순으로 의심하고 교체를 검토합니다. 전통 스택에서는 존재 자체를 모르고 지나갔을 손상이 여기서는 수치로 보입니다.
resilver — 데이터만 복사하는 리빌드 #
ZFS에서 죽은 디스크를 교체하면 리빌드 대신 resilver가 돕니다. 이름만 다른 게 아닙니다. 파일시스템이 어느 블록이 살아 있는지 알기 때문에, resilver는 실제 데이터가 있는 블록만 복사합니다. 30% 찬 풀이라면 복사량도 30%입니다. 중급 6편에서 본 “리빌드가 길수록 두 번째 고장과 URE의 창이 넓어진다"는 문제에서, 창의 길이 자체를 줄이는 답입니다.
복사하는 블록마다 체크섬 검증도 함께 합니다. resilver 중에 읽기 오류를 만나도 어레이 전체가 무너지는 게 아니라, 해당 블록이 속한 파일이 무엇인지까지 특정해서 알려줍니다. 잃는 범위가 “어딘가의 섹터"에서 “이 파일"로 좁아집니다.
RAIDZ — 패리티 세 장까지 #
ZFS의 패리티 구성은 RAIDZ1/2/3으로, 각각 패리티 1·2·3장에 해당합니다. mdadm의 RAID5/6과 비교하면 이렇습니다.
- RAIDZ1 ≈ RAID5, RAIDZ2 ≈ RAID6에 대응하지만, CoW 덕에 쓰기 구멍이 없고 resilver가 빠르다는 차이가 있습니다.
- RAIDZ3은 패리티가 세 장입니다. mdadm에는 대응물이 없습니다. 디스크가 수십 TB로 커진 시대에, resilver 며칠 동안 두 장이 더 죽어도 버티는 보수적 선택지입니다.
- 약점도 있습니다. RAIDZ vdev 하나의 랜덤 IOPS는 대략 디스크 한 장 수준에 머뭅니다. 용량 효율이 필요하면 RAIDZ, 랜덤 I/O 성능이 필요하면 미러 vdev 여러 개라는 구분이 ZFS 설계의 기본 공식입니다.
표로 정리하면 이렇습니다.
| 구성 | 패리티 | mdadm 대응 | 쓰기 구멍 | 복구 방식 |
|---|---|---|---|---|
| RAIDZ1 | 1장 | RAID5 | 없음 | resilver (데이터만) |
| RAIDZ2 | 2장 | RAID6 | 없음 | resilver (데이터만) |
| RAIDZ3 | 3장 | 대응 없음 | 없음 | resilver (데이터만) |
| mdadm RAID5/6 | 1〜2장 | - | 있음 | 리빌드 (전체 복사) |
ARC와 메모리 — ZFS가 RAM을 많이 쓰는 이유 #
ZFS는 운영체제의 페이지 캐시 대신 자체 캐시인 ARC(Adaptive Replacement Cache)를 씁니다. 최근에 쓴 블록과 자주 쓰는 블록을 함께 추적하는 알고리즘이라 적중률이 좋은 대신, 기본값으로 시스템 RAM의 절반까지 가져갑니다. “ZFS는 메모리를 먹는다"는 평판의 정체가 이것인데, 정확히는 남는 메모리를 캐시로 쓰는 것이고 다른 프로세스가 요구하면 돌려줍니다. 다만 반납이 즉각적이지는 않아서, DB처럼 메모리를 직접 관리하는 애플리케이션과 같은 머신에 둘 때는 ARC 상한을 조정하는 게 정석입니다.
RAM이 모자라면 SSD를 2차 캐시인 L2ARC로 붙일 수 있습니다. 주의할 점은 L2ARC의 블록을 가리키는 인덱스가 RAM에 살기 때문에, RAM이 부족한 머신에 L2ARC를 크게 붙이면 오히려 ARC를 갉아먹는다는 것입니다. RAM부터 늘리는 게 먼저입니다. 참고로 “1TB당 1GB” 같은 무거운 요구는 중복 제거(dedup)를 켤 때의 이야기이고, 일반 용도라면 8GB 이상에서 무난하게 돕니다.
스냅샷과 send/recv — CoW가 주는 보너스 #
CoW 구조에서 스냅샷은 거의 공짜입니다. 어차피 옛 블록을 덮어쓰지 않으니, 스냅샷은 “이 시점의 포인터를 지우지 말 것"이라는 표시 하나입니다. 생성은 즉시 끝나고, 용량도 이후 변경된 분량만큼만 차지합니다. 그래서 ZFS 운영에서는 시간 단위 스냅샷을 수십 개 유지하는 게 일상적인 풍경입니다.
스냅샷은 zfs send로 직렬화해 다른 풀이나 다른 머신으로 보낼 수 있고, 두 스냅샷의 차이만 보내는 증분 전송도 됩니다.
# 스냅샷 생성 (즉시 끝납니다)
zfs snapshot tank/data@2026-06-15
# 백업 서버로 전송
zfs send tank/data@2026-06-15 | ssh backup zfs recv pool/data
# 다음 날부터는 차이만 증분 전송
zfs send -i tank/data@2026-06-15 tank/data@2026-06-16 | ssh backup zfs recv pool/data파일 단위로 비교하는 rsync와 달리 변경 블록을 이미 알고 있으니 증분 백업이 빠릅니다. 단, 같은 풀 안의 스냅샷은 실수 복구용이지 백업이 아닙니다. 풀이 통째로 죽으면 스냅샷도 함께 사라지므로, 중급 6편의 결론은 여기서도 유효합니다. send/recv로 다른 머신에 보낸 사본부터가 백업입니다.
압축 — lz4는 켜는 게 기본 #
ZFS는 블록 단위 투명 압축을 지원합니다. lz4는 압축·해제가 워낙 빨라서 CPU 비용보다 줄어드는 디스크 I/O의 이득이 큰 경우가 대부분이고, 압축이 안 되는 데이터는 일찍 포기하는 로직도 있어 손해 보는 상황이 드뭅니다. 그래서 “compression=lz4는 일단 켜라"가 ZFS 커뮤니티의 오랜 기본값이고, 최신 OpenZFS는 아예 기본으로 켭니다. 더 높은 압축률이 필요하면 zstd를 레벨로 조절해 선택할 수 있습니다.
운영 주의 — 스크럽과 풀 확장 #
- 스크럽은 여전히 필요합니다. 체크섬 검증은 읽을 때만 일어나므로, 안 읽는 데이터의 손상은 스크럽이 돌 때까지 잠복합니다.
zpool scrub을 월 1회 수준으로 스케줄에 넣고, 결과가 사람에게 닿는 알림 경로까지 확인합니다. - 풀 용량은 여유를 둡니다. CoW는 항상 빈 블록을 찾아 쓰는 구조라, 풀이 가득 차면 단편화로 성능이 급격히 나빠집니다. 80〜90% 선을 운영 상한으로 잡는 게 통례입니다.
- vdev 확장에는 제약이 있습니다. 풀에 vdev를 추가해 키우는 건 쉽지만, 기존 RAIDZ vdev에 디스크 한 장을 끼워 넣는 건 오랫동안 불가능했습니다. OpenZFS 2.3부터 raidz expansion으로 가능해졌지만, 기존 데이터는 옛 패리티 비율을 유지한 채 남아 공간 효율이 계산보다 낮을 수 있습니다. 디스크 몇 장으로 시작해 한 장씩 키워 갈 계획이라면 처음부터 vdev 구성을 신중히 정하는 게 여전히 정답입니다.
정리 #
이번 글에서 잡은 그림입니다.
- 전통 스택은 RAID·볼륨·파일시스템 층의 정보 단절 때문에 쓰기 구멍, 전체 복사 리빌드, 조용한 손상이라는 빈틈을 안고 있습니다.
- ZFS는 세 층을 합치고 CoW로 덮어쓰기를 없애 쓰기 구멍을 구조적으로 제거했습니다.
- 모든 읽기가 체크섬 검증을 거치고, 중복이 있으면 자가 치유까지 합니다. resilver는 데이터만 복사해 위험한 창을 줄입니다.
- RAIDZ는 패리티 세 장까지 지원하지만 랜덤 IOPS는 미러가 우세합니다. ARC 때문에 메모리는 넉넉히 잡습니다.
- 스냅샷은 거의 공짜지만 같은 풀 안에서는 백업이 아닙니다. send/recv로 다른 머신에 보낸 사본이 백업입니다.
다음 — 데이터센터 전력 #
여기까지가 서버 안의 이야기였습니다. CPU에서 시작해 메모리, 디스크, 그리고 그것들을 묶는 파일시스템까지 내려왔으니, 다음 글인 “하드웨어 고급 #5 데이터센터 전력"부터는 서버 밖으로 나갑니다. 서버 수백 대가 모인 건물에서 전기가 어떻게 들어와 분배되는지, UPS와 발전기가 어느 틈을 메우는지, 전력 효율 지표인 PUE가 무엇을 말하는지를 다루겠습니다.