중복과 분산 스토리지
디스크는 언젠가 고장 난다. RAID와 이레이저 코딩, 여러 서버에 나눠 담는 분산 스토리지로 버티는 방법과 그래도 백업이 따로 필요한 이유다.
목차 (6)
RAID: 디스크 여러 개를 하나처럼
RAID(Redundant Array of Inexpensive Disks)는 1987년 버클리의 Patterson, Gibson, Katz가 낸 보고서 A Case for Redundant Arrays of Inexpensive Disks에서 이름이 붙었다. 비싼 대형 디스크 하나 대신 싼 디스크 여러 개를 묶으면 더 빠르고 싸지만, 디스크가 많을수록 하나가 고장 날 확률도 커진다. 그래서 중복을 넣어 고장을 견디자는 것이 요지였다. 이 논문은 레벨 1~5를 정의했고, 0·6·10은 나중에 굳은 이름이다.
RAID 0은 데이터를 줄무늬(stripe)처럼 나눠 모든 디스크에 동시에 쓴다. 빠르지만 하나만 죽어도 전부 잃는다. RAID 1은 같은 내용을 두 디스크에 쓴다(미러). RAID 5는 줄마다 패리티 한 칸, RAID 6은 두 칸을 둔다. RAID 10은 미러 짝을 만들고 그 짝들에 줄무늬로 나눠 쓴다. 아래 표는 20TB 디스크로 셈한 것이다.
| 레벨 | 디스크 | 쓸 수 있는 용량 | 견디는 고장 | 알맞은 곳 |
|---|---|---|---|---|
| RAID 0 | 6개 | 120TB (6/6) | 0개 | 임시 작업 공간, 다시 만들 수 있는 캐시 |
| RAID 1 | 2개 | 20TB (1/2) | 1개 | OS 부팅 디스크, 작은 서버 |
| RAID 5 | 6개 | 100TB (5/6) | 1개 | 작은 디스크 몇 개, 읽기 위주 |
| RAID 6 | 6개 | 80TB (4/6) | 2개 | 큰 HDD로 만든 NAS·보관용 |
| RAID 10 | 6개 | 60TB (3/6) | 1개 (짝이 다르면 최대 3개) | 데이터베이스, 작은 쓰기가 많은 곳 |
RAID는 운영체제의 소프트웨어(리눅스 mdadm, 윈도 저장소 공간)나 RAID 카드가 맡는다. ZFS는 파일 시스템이 RAID까지 맡는 RAID-Z(패리티 1~3개)를 쓴다(OpenZFS 문서). RAID 5·6에는 쓰기 구멍(write hole)이라는 약점이 있다. 데이터와 패리티를 쓰는 사이에 전원이 끊기면 둘이 어긋난 채 남는다. 배터리 달린 RAID 카드나 쓰기 기록(저널)이 이를 막고, RAID-Z는 줄을 제자리에 덮어쓰지 않는 쓰기 시 복사로 피한다. 메인보드 BIOS의 RAID 설정(칩셋 RAID)은 실제 계산을 드라이버가 CPU로 하는 중간 형태라, 보드나 드라이버가 바뀌면 배열을 다시 읽기 어려울 수 있다. 어느 쪽이든 RAID의 구성 정보는 그 구현에 묶이므로, 옮길 때 같은 구현이 읽을 수 있는지 먼저 확인한다.
패리티: XOR 하나로 잃은 디스크를 되살린다
RAID 5의 패리티는 같은 줄 데이터 칸들의 XOR이다. XOR은 같은 비트끼리 0, 다른 비트끼리 1이고, 어떤 값을 두 번 XOR하면 원래로 돌아온다(a ⊕ b ⊕ b = a). 그래서 패리티까지 넣어 한 줄을 모두 XOR하면 0이 되고, 칸 하나를 잃으면 남은 칸을 모두 XOR한 값이 곧 잃은 칸이다. 어느 디스크가 죽든, 데이터든 패리티든 같은 계산으로 되살린다.
디스크 4개짜리 RAID 5. 줄마다 데이터 3칸과 그 XOR인 패리티(P) 한 칸이 있고, 패리티 자리는 줄마다 돈다.
공짜는 아니다. 작은 쓰기 하나에 RAID 5는 옛 데이터와 옛 패리티를 읽고, 새 데이터와 새 패리티를 쓰는 네 번의 입출력이 든다(새 패리티 = 옛 패리티 ⊕ 옛 데이터 ⊕ 새 데이터). RAID 6은 두 번째 패리티 Q를 XOR이 아니라 리드-솔로몬 방식의 유한체 연산으로 만들어, 아무 디스크 두 개를 잃어도 되살린다. 대신 작은 쓰기 하나가 여섯 번의 입출력이 된다. 작은 쓰기가 많은 데이터베이스에 RAID 10을 권하는 이유다.
리빌드가 위험한 이유와 RAID 6
디스크가 고장 나면 배열은 저하(degraded) 상태로 돌아가고, 새 디스크를 넣으면 리빌드가 시작된다. 리빌드는 남은 디스크를 처음부터 끝까지 모두 읽는다. 디스크가 커질수록 이 시간이 길어지고, 그동안 RAID 5는 중복이 하나도 없다.
흔히 드는 근거가 URE(Unrecoverable Read Error, 복구 불가 읽기 오류)다. 소비자용 HDD 사양서는 보통 "읽은 비트 1014개당 1회 미만"(약 12.5TB당 1회), 기업용은 1015개당 1회 미만이라고 적는다. 20TB 디스크 4개짜리 RAID 5에서 하나를 리빌드하면 60TB를 읽으니, 사양의 상한을 그대로 곱하면 오류가 약 4.8번 난다는 계산이 나오고, 여기서 "RAID 5는 리빌드가 반드시 실패한다"는 주장이 퍼졌다. 이 계산은 과장이다. 사양은 상한이고 실제 오류는 훨씬 드물다. NetApp 저장 시스템의 디스크 153만 개를 32개월 동안 지켜본 Bairavasundaram 등(SIGMETRICS 2007)의 연구에서 그 기간에 잠복 섹터 오류가 한 번이라도 생긴 디스크는 3.45%였다.
그래도 큰 디스크에 RAID 6을 권하는 데는 근거가 있다. 첫째, 고장은 독립적이지 않다. 디스크 약 10만 대의 교체 기록을 분석한 Schroeder와 Gibson(FAST 2007)은 사양서의 MTTF가 뜻하는 연 0.88% 이하보다 실제 연간 교체율이 높고(보통 2~4%, 어떤 시스템은 13%), 교체가 시간적으로 몰린다는 것을 보였다. 같은 날 산 같은 기종은 비슷한 때 늙는다. 리빌드 동안의 많은 읽기도 약한 디스크를 밀어붙인다. 둘째, 그 긴 창 동안 디스크 하나가 더 죽거나 읽히지 않는 섹터 하나를 만나면 RAID 5는 데이터를 잃지만 RAID 6은 버틴다.
그래서 실무 기준은 이렇다. 큰 HDD로 만든 배열은 RAID 6(RAID-Z2) 이상으로 패리티를 둘 둔다. 주기적으로 배열 전체를 읽는 스크러빙(scrubbing)으로 잠복 오류를 리빌드 전에 찾아 고친다. 핫 스페어를 두어 리빌드를 바로 시작한다. 그리고 무엇보다 RAID와 별개로 백업을 둔다(맨 아래 절).
이레이저 코딩: 패리티를 일반화하다
이레이저 코딩(erasure coding)은 데이터를 k조각으로 나누고 패리티 m조각을 더해 k+m조각을 서로 다른 곳에 둔다. 리드-솔로몬(Reed–Solomon) 같은 코드는 그중 아무 k조각만 남아도 원본을 되살린다. RAID 5는 m=1, RAID 6은 m=2인 특수한 경우다. 분산 스토리지에서는 조각을 디스크가 아니라 서버나 랙, 데이터센터에 나눠 둔다.
복제와 비교하면 차이가 크다. 3벌 복제는 원본의 200%를 더 쓰고 2개 고장을 견딘다. HDFS의 기본 정책 RS 6+3은 50%만 더 쓰고 3개 고장을 견딘다(HDFS 이레이저 코딩). Ceph의 이레이저 코드 풀 기본 프로필은 k=2, m=2다(Ceph 이레이저 코드).
대가는 계산과 네트워크다. 조각 하나를 되살리려면 다른 조각 k개를 읽어 와야 하니, 복구할 때 네트워크로 오가는 양이 복제의 k배쯤 된다. 작은 오브젝트는 조각으로 나누면 메타데이터와 왕복이 늘어 손해이고, 줄 일부만 고쳐 쓰는 일도 비싸다. 그래서 Ceph의 이레이저 코드 풀은 기본으로 오브젝트를 통째로 쓰는 일(RGW)에 맞춰져 있고, 블록(RBD)·파일(CephFS)에 쓰려면 덮어쓰기를 따로 켜고 메타데이터는 복제 풀에 둔다. 마이크로소프트는 복구 때 읽을 조각 수를 줄이려고 지역 패리티를 더한 LRC를 Azure에 썼다(Huang 등, USENIX ATC 2012). 보통은 자주 쓰는 데이터는 복제, 크고 차가운 데이터는 이레이저 코딩으로 나눈다.
분산 스토리지: Ceph와 MinIO
RAID는 서버 한 대 안의 디스크를 묶는다. 서버 자체가 죽거나 용량이 서버 한 대를 넘으면 데이터를 여러 서버에 흩어야 하는데, 그러면 "이 오브젝트는 어느 서버에 있나"를 누가 기억할지가 문제다. 위치 표를 한 서버에 두면 그 서버가 병목이자 약점이 된다.
Ceph는 위치를 기억하지 않고 계산한다(Ceph 아키텍처). 바탕은 RADOS라는 오브젝트 저장소이고, 디스크마다 OSD(Object Storage Daemon) 프로세스가 붙는다. 모니터(MON) 몇 대가 클러스터 맵(어떤 OSD가 살아 있는지, 호스트·랙 구조)을 합의로 관리한다. 오브젝트 이름을 해시해 배치 그룹(PG)을 고르고, CRUSH 알고리즘이 클러스터 맵과 규칙으로 그 PG를 둘 OSD 목록을 계산한다(Weil 등, CRUSH 논문 2006). 클라이언트도 같은 계산을 하므로 중앙 조회 없이 OSD에 바로 간다. OSD가 늘거나 줄면 바뀐 맵에 따라 일부 PG만 옮겨진다.
복제 풀은 보통 3벌이고, 2벌 이상 살아 있어야 쓰기를 받는다(Ceph 풀 설정의 size·min_size). 쓰기는 주 OSD가 순서를 정해 복제 OSD에 보내고, 모두 기록한 뒤에 클라이언트에 답한다. 그래서 RADOS는 강한 일관성을 준다. 이 위에 블록(RBD), 파일(CephFS), S3 호환 오브젝트(RGW) 세 인터페이스를 얹어, 블록·파일·오브젝트를 한 클러스터에서 모두 낸다. 복제와 합의의 일반론은 데이터베이스 노트의 분산 글, 해시 자체는 해시 노트에 있다.
MinIO는 S3 호환 오브젝트 저장소로, 드라이브를 같은 크기의 이레이저 세트로 묶는다. 세트 하나는 기본 2~16개 드라이브이고, 오브젝트 이름의 해시로 세트를 고른 뒤 그 안에서 데이터 K조각과 패리티 M조각으로 나눈다. 패리티는 세트 크기의 절반까지 정할 수 있고, 읽기에는 K조각, 쓰기에는 K개(패리티가 정확히 절반이면 K+1개) 드라이브의 응답이 필요하다(MinIO 이레이저 코딩). 다만 오픈소스 커뮤니티판 저장소는 지금 README에 더는 관리하지 않는다고 밝히고 상용판(AIStor)으로 안내한다(minio/minio). 새로 도입한다면 이 점을 따져 봐야 한다.
RAID는 백업이 아니다
RAID와 이레이저 코딩은 하드웨어 고장을 견디게 할 뿐이다. 실수로 지운 파일, 랜섬웨어가 암호화한 파일, 버그가 망가뜨린 데이터는 모든 디스크에 충실하게 복제된다. 장비가 통째로 불타거나 도난당해도 소용없다. 각 수단이 막는 사고가 다르다.
스냅숏은 특정 시각의 상태를 얼린 것이다. 쓰기 시 복사 덕에 빠르고 싸서 실수와 랜섬웨어를 되돌리는 데 좋지만 같은 장비 안에 있다. 복제(replication)는 다른 장비나 지역에 사본을 둔다. 동기 복제는 지금 상태를 바로 따라가므로 장비 장애와 재해에는 강하지만, 지운 것과 암호화된 것도 그대로 따라간다. 백업은 다른 시스템에, 여러 시점의 버전으로, 가능하면 오프라인이거나 지울 수 없게(S3 오브젝트 잠금 같은 불변 저장, 블록·파일·오브젝트 노트) 둔 사본이다.
기준으로는 사본 세 벌, 매체 두 종류, 한 벌은 다른 장소라는 3-2-1 규칙이 널리 쓰인다(보안 실천 노트의 3-2-1 백업 절). 그리고 백업은 되살려 봐야 백업이다. 정기적으로 실제 복원을 연습해, 걸리는 시간과 빠진 데이터가 없는지 확인한다.
- Ceph Architecture — Ceph Documentation
- A Case for Redundant Arrays of Inexpensive Disks (RAID) — UC Berkeley
- OpenZFS Documentation — OpenZFS
- An Analysis of Latent Sector Errors in Disk Drives — SIGMETRICS 2007
- Disk failures in the real world — USENIX FAST 2007
- HDFS Erasure Coding — Apache Hadoop
- Erasure Coding in Windows Azure Storage — USENIX ATC 2012
- Erasure code — Ceph Documentation
- Pools — Ceph Documentation
- CRUSH: Controlled, Scalable, Decentralized Placement of Replicated Data — SC 2006
- Erasure Coding — MinIO AIStor Documentation
- minio/minio — GitHub