Migrating a ZFS pool from RAIDZ1 to RAIDZ2

Michael Lynch

RAIDZ1에서 RAIDZ2로 ZFS 풀 마이그레이션하기

최근 집에서 쓰고 있는 TrueNAS 서버를 업그레이드하면서 4개 디스크로 구성된 RAIDZ1 ZFS 풀에 있던 18TB 데이터를 새로운 RAIDZ2 풀로 마이그레이션했습니다.

핵심은 8TB 디스크 세 개만 추가로 구입해 외부 스토리지에 데이터를 옮기지 않고 작업을 완료했다는 점입니다.

외부 스토리지로 데이터를 옮기지 않고 RAIDZ1에서 RAIDZ2로 업그레이드하는 것이 까다로운 이유는 다음과 같습니다.

  1. RAIDZ1 풀을 그 자리에서 RAIDZ2 풀로 변환할 수 없습니다.
  2. ZFS 풀을 더 적은 수의 디스크로 축소할 수 없습니다.
  3. 새로 구입한 8TB 디스크 세 개로 구성한 RAIDZ2 풀에는 18TB 데이터를 담을 수 없습니다.

어떻게 했는지

0단계: 초기 상태

시작 시점에는 8TB 디스크 4개로 구성된 RAIDZ1 ZFS 풀을 사용 중이었고, 전체 23TB 용량 중 18TB를 쓰고 있었습니다. 이번 마이그레이션을 위해 리퍼브 8TB 디스크 세 개를 구입했습니다.

1단계: 디스크 하나를 빌려 RAIDZ2 풀 만들기

먼저 기존 RAIDZ1 풀에서 디스크 하나를 빼내 풀이 디그레이드(degraded) 상태가 되도록 했습니다.

그런 다음 다음 구성으로 8TB 디스크 5개짜리 RAIDZ2 풀을 새로 만들었습니다.

  1. 새로 구입한 디스크 세 개
  2. RAIDZ1 풀에서 빼낸 디스크 하나
  3. 가짜 디스크 역할을 할 8TB 스파스 파일 하나

스파스 파일은 /tmp 디렉터리에 만들었습니다. 해당 파일시스템에는 실제로 8TB를 저장할 공간이 없지만 괜찮습니다. 이 스파스 파일은 임시방편으로, 5개 디스크 풀을 만들 수 있게 해주는 꼼수이며 실제로 데이터를 쓰지는 않을 것이기 때문입니다.

2단계: 가짜 디스크 오프라인 처리하기

다음으로 RAIDZ2 풀에서 가짜 디스크(8TB 스파스 파일)를 오프라인 상태로 전환했습니다.

가짜 디스크를 오프라인 처리한 뒤에도 풀은 여전히 정상(healthy) 상태를 유지합니다. RAIDZ2는 디스크 두 개가 빠져도 동작할 수 있기 때문입니다.

3단계: RAIDZ1 풀의 스냅샷을 RAIDZ2 풀로 마이그레이션하기

RAIDZ1 풀의 모든 데이터셋을 스냅샷으로 만든 뒤 zfs send를 이용해 새 RAIDZ2 풀로 마이그레이션했습니다.

4단계: 기존 풀 삭제하기

데이터가 RAIDZ2 풀로 성공적으로 마이그레이션된 것을 확인한 뒤 기존 RAIDZ1 풀을 삭제했고, 그 결과 디스크 세 개가 남게 되었습니다.

5단계: 가짜 디스크를 기존 디스크로 교체하기

기존 RAIDZ1 풀에서 나온 디스크 하나를 이용해 RAIDZ2 풀의 가짜 디스크를 교체해, 더 이상 빠진 디스크가 없는 상태로 만들었습니다.

6단계: 기존 디스크로 새 풀 확장하기

마지막으로 ZFS 확장 기능을 이용해 기존 RAIDZ1 풀에서 남은 디스크 두 개를 새 RAIDZ2 풀에 추가했습니다. 그 결과 정상 상태의 8TB 디스크 7개짜리 RAIDZ2 풀에서 총 33TB의 사용 가능한 용량을 확보하게 되었습니다.

업데이트: 더 안전한 전략

업데이트(2025-07-25): 디스크 고장 위험을 더 잘 완화할 수 있는 대안 전략을 제안해 주신 독자분들께 감사드립니다.

독자분들이 제안한 이 전략의 변형은 제가 원래 썼던 방식보다 더 마음에 듭니다.

  1. 새로 구입한 디스크 세 개와 가짜 디스크(스파스 파일) 두 개를 이용해 8TB 디스크 5개짜리 RAIDZ2 풀을 만듭니다.
  2. RAIDZ2 풀에서 가짜 디스크들을 오프라인 처리합니다.
  3. 기존 RAIDZ1 풀의 데이터를 새 RAIDZ2 풀로 마이그레이션합니다.
  4. RAIDZ1 풀에서 디스크 하나를 오프라인 처리한 뒤, 이를 이용해 RAIDZ2 풀의 가짜 디스크 하나를 교체합니다.
  5. RAIDZ1 풀을 삭제합니다.
  6. 삭제된 RAIDZ1 풀에서 남은 디스크 세 개를 RAIDZ2 풀로 마이그레이션합니다.

이 전략이 좋은 이유는 마이그레이션 과정 중 어떤 디스크 하나가 고장 나더라도 견딜 수 있기 때문입니다. 원래 방식에서는 RAIDZ2 풀 쪽 디스크가 고장 나는 것은 견딜 수 있었지만, 마이그레이션 도중 RAIDZ1 풀의 디스크 중 하나라도 고장 나면 풀 전체가 실패했습니다.

이 새로운 전략의 유일한 단점은 제가 원래 전략을 검증했던 것처럼 직접 테스트해보지 않았다는 점입니다.

왜 RAIDZ1에서 RAIDZ2로 바꿨나?

2022년에 집에서 쓸 TrueNAS 서버를 8TB 디스크 4개로 구성된 RAIDZ1 풀로 구축했습니다. RAIDZ1은 디스크 하나 고장은 견딜 수 있고, 4개 중 2개가 동시에 고장 날 가능성은 낮다고 생각했기 때문에 안심하고 쓸 수 있었습니다.

제가 간과했던 점은 풀에 디스크를 추가할 때마다 데이터 손실 확률이 높아진다는 사실이었습니다. 디스크 4개 중 2개가 고장 나는 상황은 걱정하지 않았지만, 디스크가 6개나 8개가 되면 동시 고장이 훨씬 현실적으로 느껴지기 시작했습니다.

RAIDZ1 풀에 디스크를 추가할 때마다 RAIDZ2로의 마이그레이션은 더 어려워집니다. 새 풀이 더 커져야 하고, 새 풀에 쓸 수 있는 물리적 슬롯은 더 줄어들기 때문입니다.

RAIDZ2는 데이터 손실 없이 디스크 두 개 고장을 견딜 수 있습니다. 현재 서버 케이스의 최대 용량인 10개까지 확장하더라도, 디스크 세 개가 동시에 고장 날 가능성은 충분히 낮아 걱정하지 않습니다.

왜 RAIDZ1에서 RAIDZ2로 전환하기가 어려운가?

ZFS는 RAIDZ1에서 RAIDZ2로의 전환을 지원하지 않습니다. RAIDZ 모드는 풀 생성 시점에 정해지며 이후에는 바꿀 수 없습니다.

단순한 방법은 디스크 다섯 개를 추가로 구입해 RAIDZ2 풀을 만든 뒤 데이터를 모두 옮기는 것입니다. 이렇게 하면 기존 디스크 4개에 새 디스크 5개를 더해 총 9개가 되는데, 저는 6~7개만 원했습니다.

온라인에서 RAIDZ1 풀을 RAIDZ2로 변환하는 방법을 검색하면, 대부분 단순한 방법을 쓰거나 여분의 ZFS 서버로 데이터를 모두 옮긴 뒤 RAIDZ1 풀을 삭제하고 그 자리에 RAIDZ2 풀을 만들어 데이터를 다시 옮기라고 합니다. 하지만 저처럼 요트 어딘가에 18TB짜리 여분 ZFS 서버를 굴리고 있지 않은 사람이라면 어떨까요?

RAIDZ1에서 RAIDZ2로 더 효율적으로 전환하는 방법에 대한 안내가 별로 없는 이유는, 제 해결책이 RAIDZ 확장 기능에 의존하는데 이 기능이 ZFS에서 불과 6개월 전에 제공되기 시작했기 때문입니다.

RAIDZ1에서 RAIDZ2로 마이그레이션: 실제 과정

이론상으로는 RAIDZ1에서 RAIDZ2로의 마이그레이션 계획이 간단했지만, 실제로 수행하면서 몇 가지 문제가 발생했습니다.

이 과정을 재현하려는 분들을 위해 실행한 정확한 명령어와 출력 결과를 상세히 기록해 공유합니다.

1단계: USB 메모리로 연습 마이그레이션하기

제 마이그레이션 계획에는 다소 위험한 작업이 포함되어 있습니다. 모든 데이터가 들어 있는 메인 TrueNAS 서버에서 시도하기 전에 먼저 테스트 서버에서 연습했습니다.

오래된 미니 PC에 TrueNAS Core 25.04를 설치한 뒤 USB 메모리 7개를 연결했습니다.

여분의 TrueNAS 서버와 USB 메모리 7개로 마이그레이션 전략을 테스트하는 모습

여분의 TrueNAS 서버와 USB 메모리 7개로 마이그레이션 전략을 테스트하는 모습

드라이브 크기가 제각각이라 완벽한 실험은 아니었지만 과정 자체는 동작했고, 덕분에 프로덕션 서버에서도 마이그레이션 계획을 시도할 자신감을 얻었습니다.

2단계: 데이터 백업하기

위에서 외부 스토리지에 데이터를 옮기지 않았다고 했지만, 엄밀히 말하면 완전히 사실은 아닙니다. 백업 용도로 외부 스토리지를 사용하긴 했지만, 결국 필요하지는 않았습니다.

이미 restic으로 매일 밤 백업을 하고 있지만, ZFS 레벨이 아닌 파일시스템 레벨 백업입니다. 즉, 마이그레이션 중 ZFS 풀을 손상시켰다면 ZFS를 인식하는 백업에서 한 번에 복원하는 것이 아니라, 각 ZFS 데이터셋을 다시 만들고 백업에서 파일을 복원해야 합니다.

매일 밤 백업을 하고 있지만 백업하지 않는 데이터가 하나 있습니다. 바로 미디어 파일입니다. 저는 데이터를 쌓아두는 걸 좋아해서 DVD와 블루레이 원본 이미지를 모두 보관합니다. 혹시라도 1998년 영화 메리에겐 뭔가 특별한 것이 있다(There’s Something About Mary) DVD에 수록된 감독 코멘터리를 보고 싶어질지 모르니까요.

리핑한 DVD와 블루레이 컬렉션

디스크 서버 용량의 대부분은 제가 리핑한 DVD와 블루레이입니다.

원본 디스크 이미지와 인코딩된 비디오 파일을 합치면 TrueNAS 서버에 영화와 TV 프로그램이 약 15TB 있습니다. Wasabi나 Backblaze B2 같은 저렴한 클라우드 스토리지에서도 한 달에 약 90달러가 들기 때문에 이 파일들은 클라우드에 백업하지 않습니다.

미디어 데이터를 잃더라도 원본 디스크에서 다시 리핑하면 된다고 스스로를 달랬지만, 이번 작업을 시작하면서 디스크 500장 이상을 하나하나 다시 리핑하고 인코딩해야 할 가능성을 떠올리니, 만약을 대비해 예외적으로 미디어 파일을 단기간 백업하기로 했습니다.

15TB를 3일 동안 어디에 백업할 수 있을까?

15TB만 백업하면 됐습니다. 일 단위나 시간 단위로 과금하는 업체를 찾으면 비용이 꽤 저렴할 것 같았습니다.

ZFS 네이티브 백업을 제공하는 클라우드 업체는 두 곳을 알고 있습니다.

  • rsync.net: 최소 과금 기간이 1개월(15TB에 150달러)입니다.
  • zfs.rent: 제가 직접 하드디스크를 보내야 합니다.

그래서 두 옵션 모두 며칠 동안만 백업하기에는 적합하지 않았습니다.

ZFS를 S3 호환 스토리지에 백업하는 도구도 있지만 너무 복잡해 보였고, 15TB 용량의 여분 서버 없이는 백업이 제대로 됐는지 검증할 수 없어 시도하지 않았습니다.

그래서 평소 쓰던 restic 백업 스크립트를 이용해 일 단위 이하로 세분화해 과금하는 클라우드 스토리지 업체에 미디어 파일을 백업하기로 했습니다. 제가 찾은 옵션은 다음과 같습니다.

  • Amazon S3(Standard): $0.76/TB/일
  • Google Cloud Storage(Standard): $0.66/TB/일
  • Cloudflare R2: $0.50/TB/일
  • Backblaze B2: $0.20/TB/일
  • Hetzner Storage Boxes: $0.09/TB/일
    • 안타깝게도 이 옵션은 마이그레이션을 마친 뒤에야 알게 되었습니다.

Backblaze B2를 선택했는데, 어쩐 일인지 제 15TB 데이터가 Backblaze 쪽에서는 24.6TB로 잡혔습니다.

Backblaze에 표시된 스토리지 사용량

모든 데이터를 Backblaze에 업로드하는 데 거의 일주일이 걸려 예상보다 오래 데이터를 보관해야 했지만, 월말 청구서는 평소보다 몇 달러 더 나온 정도라 비용이 어떻게 계산된 건지 잘 모르겠습니다.

경고: 클라우드 스토리지 업체의 최소 보관 기간 정책을 확인하세요

처음에는 실패한 마이그레이션 시도에서 Wasabi에 백업했습니다. 그래도 괜찮다고 생각했습니다. 며칠 백업했으니 30달러 정도 내면 되겠지 싶었죠. Wasabi도 일 단위 과금을 지원하긴 하지만, 백업한 모든 데이터에 대해 최소 90일 치 요금을 내야 한다는 걸 알게 되었습니다. 그래서 30달러 청구서를 예상했는데 300달러 청구서를 받게 생겼습니다.

자비를 구하고자 Wasabi 지원팀에 메일을 보냈더니 이상한 조언이 돌아왔습니다. 계정을 삭제하라는 것이었습니다.

Wasabi 계정 자체를 삭제하면 최소 보관 요금 의무에서 벗어날 수 있다는 것입니다. 꼼수나 트릭이 아니라 Wasabi 지원팀의 공식 안내였습니다.

그런데 제가 따로 항의하지도 않았는데 다른 담당자가 티켓에 합류해 계정을 삭제할 필요가 없다고 하더군요. 한 번에 한해 예외적으로 초과 요금을 크레딧으로 돌려주겠다고 했습니다(고맙습니다, Wasabi!).

3단계: TrueNAS 업데이트하기

이번 마이그레이션에는 ZFS의 RAIDZ 확장 기능이 필요하며, 이 기능은 OpenZFS 2.3.0과 TrueNAS 24.10(Electric Eel) 릴리스에 포함되어 있습니다.

TrueNAS 셸에서 ZFS 버전을 확인했습니다.

root@truenas:~# zfs --version
zfs-2.3.0-1
zfs-kmod-2.3.0-1

주의: 업데이트할 때 릴리스 트레인을 확인하세요.

TrueNAS에서 최신 버전이라고 알려줘서 실수로 업데이트 단계를 건너뛰었습니다. 알고 보니 제가 선택한 릴리스 트레인 기준으로는 최신이었던 것이고, 기본값이 여전히 24.x에 머물러 있었습니다. 따라서 ZFS 2.3.0 이상이 보이지 않는다면, 더 높은 메이저 버전을 선택하도록 TrueNAS를 업데이트해야 할 수도 있습니다.

3단계: 디스크 식별하기

이번 마이그레이션을 시작하려면 ZFS 명령줄 유틸리티에서 참조할 수 있도록 모든 디스크를 식별해야 합니다.

디스크를 확인하는 가장 간단한 방법은 fdisk 유틸리티를 쓰는 것입니다.

fdisk --list

저는 TrueNAS의 Storage > Disks 대시보드를 확인하는 편이 더 쉽습니다.

TrueNAS 디스크 대시보드

위 스크린샷을 기준으로 디스크 ID는 다음과 같습니다.

  • 기존 풀에 있는 디스크: sda, sdc, sde, sdf
  • 새 디스크: sdb, sdg, sdh

4단계: 가장 상태가 안 좋은 디스크 찾기

마이그레이션에서 가장 위험한 구간은 기존 RAIDZ1 풀에서 디스크를 빌려오는 시점부터 데이터를 새 RAIDZ2 풀로 마이그레이션할 때까지입니다. 기존 RAIDZ1 풀에서 디스크를 빼면 풀이 디그레이드 상태가 됩니다. 데이터 마이그레이션 중에 기존 풀의 다른 디스크 중 하나라도 고장 나면 데이터를 잃게 됩니다.

이 위험한 구간 때문에 기존 풀에서 가장 상태가 안 좋은 디스크를 새 풀을 만드는 데 빌려오고 싶었습니다. RAIDZ2 풀은 그 디스크가 고장 나도 견딜 수 있지만 RAIDZ1 풀은 그렇지 못하기 때문입니다.

가장 상태가 안 좋은 디스크를 찾기 위해 다음 간단한 bash 스니펫으로 디스크들의 SMART 진단 데이터를 조회했습니다.

for drive in /dev/sd?; do
  [ -e "$drive" ] && echo -e "\n=== $drive ===" && smartctl -A $drive | \
    grep -E '(Power_On_Hours|Wear_Leveling|Media_Wearout|Reallocated_Sector)'
done
=== /dev/sda ===
  5 Reallocated_Sector_Ct   0x0033   100   100   050    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0032   032   032   000    Old_age   Always       -       27228

=== /dev/sdb ===
  5 Reallocated_Sector_Ct   0x0033   100   100   005    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0012   100   100   000    Old_age   Always       -       423

=== /dev/sdc ===
  5 Reallocated_Sector_Ct   0x0033   100   100   010    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0032   077   077   000    Old_age   Always       -       20998

=== /dev/sdd ===
  9 Power_On_Hours          0x0032   100   100   000    Old_age   Always       -       29606

=== /dev/sde ===
  5 Reallocated_Sector_Ct   0x0033   100   100   010    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0032   067   067   000    Old_age   Always       -       29599

=== /dev/sdf ===
  5 Reallocated_Sector_Ct   0x0033   100   100   010    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0032   067   067   000    Old_age   Always       -       29603

=== /dev/sdg ===
  5 Reallocated_Sector_Ct   0x0033   100   100   005    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0012   100   100   000    Old_age   Always       -       141

=== /dev/sdh ===
  5 Reallocated_Sector_Ct   0x0033   100   100   005    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0012   100   100   000    Old_age   Always       -       147

결과를 보면 모두 꽤 건강한 상태였습니다. 차이가 나는 지표는 Power-on hours(전원 투입 시간)뿐이었습니다.

  • 기존 디스크
    • sda: 032
    • sdc: 077
    • sde: 067
    • sdf: 067
  • 새 디스크
    • sdb: 100
    • sdg: 100
    • sdh: 100

새 디스크는 모두 100%로, 리퍼브한 지 얼마 되지 않았으니 당연합니다. sda가 32%로 가장 낮아 가장 상태가 안 좋은 디스크로 간주하고 먼저 새 풀로 옮기기로 했습니다.

5단계: 안정적인 디스크 식별자 만들기

/dev/sdX 경로는 재부팅하면 유지되지 않습니다. /dev/sda에 있던 디스크가 다음 부팅 때 /dev/sdf로 나타날 수도 있습니다. ZFS가 더 안정적인 식별자로 디스크를 해석하는지 확실하지 않아 혹시 모를 위험을 감수하고 싶지 않았습니다.

/dev/sdX 경로를 안정적인 식별자로 변환하는 bash 함수를 만들었습니다.

get_disk_id() {
    local dev=$1
    local target="/dev/$dev"
    for path in /dev/disk/by-id/*; do
        if [ -L "$path" ] && [ "$(readlink -f "$path")" = "$target" ] &&
           [[ "${path: -2:1}" != ":" ]]; then
            echo "$path"
            return 0
        fi
    done
    echo "Disk ID not found for device: $dev" >&2
    return 1
}

제가 만든 get_disk_id bash 함수는 sdX 식별자를 넘기면 해당 디스크의 안정적인 경로를 반환합니다.

$ get_disk_id sda
/dev/disk/by-id/ata-HGST_HUS728T8TALE6L1_VGGGYUEG

과정을 재사용하기 쉽게 디스크 ID를 환경 변수에 저장했습니다.

# Old disks
DISK_1="$(get_disk_id sda)"
DISK_2="$(get_disk_id sdc)"
DISK_3="$(get_disk_id sde)"
DISK_4="$(get_disk_id sdf)"

새 디스크에 대해서도 환경 변수를 만들었습니다.

# New disks
DISK_5="$(get_disk_id sdb)"
DISK_6="$(get_disk_id sdg)"
DISK_7="$(get_disk_id sdh)"

기존 RAIDZ1 풀을 가리키는 환경 변수도 하나 만들었습니다.

OLDPOOL='pool1'

6단계: 상태가 안 좋은 디스크 오프라인 처리하기

본격적으로 손대기 전에 기존 풀이 정상 상태이고 예상한 디스크들이 맞는지 확인했습니다.

$ sudo zpool status ${OLDPOOL}
  pool: pool1
 state: ONLINE
  scan: scrub repaired 68K in 10:40:08 with 0 errors on Wed May 14 06:25:26 2025
config:
        NAME        STATE     READ WRITE CKSUM
        pool1       ONLINE       0     0     0
          raidz1-0  ONLINE       0     0     0
            sde2    ONLINE       0     0     0
            sdf2    ONLINE       0     0     0
            sdc2    ONLINE       0     0     0
            sda2    ONLINE       0     0     0
errors: No known data errors

sda2(DISK_1)가 새 RAIDZ2 풀로 옮기려는 상태가 안 좋은 디스크이므로 다음 명령을 실행했습니다.

DISK_TO_OFFLINE='sda2'
sudo zpool offline "${OLDPOOL}" "${DISK_TO_OFFLINE}"

zpool status를 확인해보니, 예상대로 디스크를 오프라인 처리하자 풀이 디그레이드 상태가 되었습니다.

$ sudo zpool status ${OLDPOOL}
  pool: pool1
 state: DEGRADED
status: One or more devices has been taken offline by the administrator.
        Sufficient replicas exist for the pool to continue functioning in a
        degraded state.
action: Online the device using 'zpool online' or replace the device with
        'zpool replace'.
  scan: scrub repaired 68K in 10:40:08 with 0 errors on Wed May 14 06:25:26 2025
config:
        NAME        STATE     READ WRITE CKSUM
        pool1       DEGRADED     0     0     0
          raidz1-0  DEGRADED     0     0     0
            sde2    ONLINE       0     0     0
            sdf2    ONLINE       0     0     0
            sdc2    ONLINE       0     0     0
            sda2    OFFLINE      0     0     0
errors: No known data errors

7단계: 상태가 안 좋은 디스크 초기화하기

RAIDZ2 풀을 만들 때 약간의 문제가 있습니다. 상태가 안 좋은 디스크(DISK_1)로 새 풀을 만들려고 하면 ZFS에서 이미 다른 풀에 속해 있다고 합니다. 디스크를 오프라인 처리했음에도 ZFS는 여전히 RAIDZ1 풀이 해당 디스크를 소유하고 있다고 주장합니다.

RAIDZ1 풀이 그 디스크에 대한 소유권을 주장하지 못하게 하려면 디스크를 완전히 초기화해야 합니다.

옮길 디스크를 추적하기 위해 환경 변수를 정의했습니다.

MOVED_DISK="${DISK_1}"

그런 다음 디스크의 모든 데이터를 지웠습니다.

주의: wipefs 명령은 확인 절차 없이 디스크 전체를 지우므로 신중하게 실행하세요.
# Be careful! This command completely wipes the disk with no confirmation.
wipefs --all "${MOVED_DISK}"

8단계: 스파스 파일로 가짜 디스크 만들기

8TB 디스크 4개로 RAIDZ2 풀을 만들 수도 있지만, 그러면 사용 가능한 용량이 16TB에 불과해 18TB 데이터를 담기에 부족합니다.

대신 8TB 디스크 5개짜리 RAIDZ2 풀을 만들되, 다섯 번째 디스크는 실제로 존재하지 않게 합니다. ZFS에서는 스파스 파일을 디스크처럼 사용해 풀을 만들 수 있습니다.

스파스 파일로 가짜 디스크를 만들려면 풀에 있는 다른 디스크들의 정확한 크기를 알아야 하며, 이는 fdisk로 확인할 수 있습니다.

$  fdisk --list | grep "^Disk.*bytes"
Disk /dev/sdf: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdd: 111.79 GiB, 120034123776 bytes, 234441648 sectors
Disk /dev/sda: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdc: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sde: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdb: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/mapper/sdd3: 16 GiB, 17179869184 bytes, 33554432 sectors
Disk /dev/zd0: 10 GiB, 10737418240 bytes, 20971520 sectors
Disk /dev/sdg: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdh: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors

이를 통해 실제 디스크 각각이 8001563222016바이트임을 확인했으므로, truncate 명령으로 가짜 드라이브를 만들었습니다.

FAKE_DISK='/tmp/fake-drive.img'
truncate --size 8001563222016 "${FAKE_DISK}"

마지막으로 새 풀의 이름이 필요합니다. ZFS 문화권에서는 풀 이름을 tank라고 짓는 관습이 있는데, 저는 이를 모르고 기존에 pool1이라는 이름을 썼습니다. 이번 기회에 새 풀 이름을 tank로 지어 쿨한 사람들에 끼어보고 싶었습니다.

NEWPOOL='tank'

안타깝게도 풀 이름 tank를 유지하지는 못했습니다. TrueNAS는 풀 이름에 의존하는 부분이 많아, 모든 공유와 크론 잡을 tank를 가리키도록 일일이 수정하는 대신 다시 pool1으로 돌아가기로 했습니다(아래 참조).

9단계: RAIDZ2 풀 생성하기

이제 8TB 디스크 5개짜리 RAIDZ2 풀을 만들 차례입니다.

zpool create \
  -f \
  ${NEWPOOL} \
  raidz2 \
  -m "/mnt/${NEWPOOL}" \
  "${DISK_5}" \
  "${DISK_6}" \
  "${DISK_7}" \
  "${MOVED_DISK}" \
  "${FAKE_DISK}"

풀 생성에 성공했으며 ZFS 유틸리티로 확인할 수 있습니다.

$ zpool status "${NEWPOOL}"
  pool: tank
 state: ONLINE
config:
        NAME                                   STATE     READ WRITE CKSUM
        tank                                   ONLINE       0     0     0
          raidz2-0                             ONLINE       0     0     0
            ata-HGST_HUS728T8TALE6L1_VGGGYUEG  ONLINE       0     0     0
            ata-HGST_HUS728T8TALE6L1_VRGMRVJK  ONLINE       0     0     0
            ata-HGST_HUS728T8TALE6L1_VRGNZU9K  ONLINE       0     0     0
            ata-TOSHIBA_HDWG480_71R0A14YFR0H   ONLINE       0     0     0
            /tmp/fake-drive.img                ONLINE       0     0     0
errors: No known data errors

$ zpool list "${NEWPOOL}"
NAME   SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
tank  36.4T  1.27M  36.4T        -         -     0%     0%  1.00x    ONLINE  -

TrueNAS 웹 UI에서도 새 RAIDZ2 풀이 보이며, 사용 가능한 용량이 21.4TiB(23.5TB)로 표시됩니다.

TrueNAS 웹 UI에 표시된 새로운 RAIDZ2 풀

TrueNAS 웹 UI에 표시된 새로운 RAIDZ2 풀입니다.

10단계: 가짜 디스크 제거하기

가짜 디스크는 /tmp 디렉터리에 있는 파일일 뿐이며, 주장하는 8TB를 실제로 저장할 수 없습니다. 가짜 파일이 /tmp 파일시스템 공간을 다 써버렸을 때 이상 동작이 발생하는 것을 막기 위해 즉시 RAIDZ2 풀에서 가짜 디스크를 제거했습니다.

zpool offline "${NEWPOOL}" "${FAKE_DISK}" && \
  rm "${FAKE_DISK}"

예상대로 ZFS 유틸리티에서는 RAIDZ2 풀이 디그레이드 상태라고 보고하지만, RAIDZ2는 최대 두 개 디스크 고장까지 견딜 수 있으므로 디스크 하나가 오프라인 상태인 것은 문제가 되지 않습니다.

$ zpool status "${NEWPOOL}"
  pool: tank
 state: DEGRADED
status: One or more devices has been taken offline by the administrator.
        Sufficient replicas exist for the pool to continue functioning in a
        degraded state.
action: Online the device using 'zpool online' or replace the device with
        'zpool replace'.
config:
        NAME                                   STATE     READ WRITE CKSUM
        tank                                   DEGRADED     0     0     0
          raidz2-0                             DEGRADED     0     0     0
            ata-HGST_HUS728T8TALE6L1_VGGGYUEG  ONLINE       0     0     0
            ata-HGST_HUS728T8TALE6L1_VRGMRVJK  ONLINE       0     0     0
            ata-HGST_HUS728T8TALE6L1_VRGNZU9K  ONLINE       0     0     0
            ata-TOSHIBA_HDWG480_71R0A14YFR0H   ONLINE       0     0     0
            /tmp/fake-drive.img                OFFLINE      0     0     0
errors: No known data errors

$ zpool list "${NEWPOOL}"
NAME   SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
tank  36.4T  1.41M  36.4T        -         -     0%     0%  1.00x  DEGRADED  -

11단계: 새 풀로 데이터 마이그레이션하기

새 RAIDZ2 풀이 23.5TB 용량으로 온라인 상태가 되었으므로, 기존 RAIDZ1 풀에 있던 18TB 데이터를 옮겼습니다.

모든 데이터를 옮기기 위해 RAIDZ1 풀 전체를 스냅샷으로 만든 뒤, 그 스냅샷을 기존 풀에서 새 풀로 전송했습니다.

SNAPSHOT_1="migrate1"
zfs snapshot -r "${OLDPOOL}@${SNAPSHOT_1}" && \
  zfs send --verbose --raw --replicate "${OLDPOOL}@${SNAPSHOT_1}" \
    | zfs receive -v -F "${NEWPOOL}"

이상하게도 명령은 오류 없이 완료되었지만, RAIDZ2 풀에서 일부 데이터셋이 빠져 있는 것을 확인했습니다.

마이그레이션 문제 해결하기

데이터 마이그레이션을 완료하기 위해 빠진 데이터셋을 하나씩 전송했습니다.

DATASET='photos'
zfs send --verbose --raw --replicate --skip-missing \
    "${OLDPOOL}/${DATASET}@${SNAPSHOT_1}" \
  | zfs receive -v -F -s "${NEWPOOL}/${DATASET}"

풀을 /mnt/tank/photos에 마운트해도 디렉터리가 여전히 비어 있는 것으로 표시되었습니다.

zpool exportzpool import를 시도해봤지만 변화가 없었습니다.

마지막으로 TrueNAS 서버 전체를 재부팅해봤더니 신기하게도 문제가 해결되었고, /mnt/tank/photos에서 파일을 확인할 수 있었습니다.

중단된 전송 재개하기

대용량 전송을 중단해본 뒤에야 ZFS가 기본적으로 중단 재개를 지원하도록 데이터를 전송하지 않는다는 사실을 알게 되었습니다. 재개하려면 “재개 토큰(resume token)”을 받아와 zfs send 명령에 포함하는 복잡한 과정을 거쳐야 합니다.

RESUME_TOKEN="$(zfs get -H -o value receive_resume_token "${NEWPOOL}/${DATASET}")"
zfs send -v -t "${RESUME_TOKEN}" | zfs receive -v -s "${NEWPOOL}/${DATASET}"

출력에서는 재개 토큰을 성공적으로 파싱했다고 나오지만 진행률이 항상 0으로 초기화되어, 이 명령이 실제로 동작한 건지 알 수 없었습니다.

마이그레이션 마무리하기

마이그레이션에 몇 시간이 걸렸기 때문에 새 스냅샷을 만들고 이전 스냅샷 이후 변경된 모든 데이터에 대해 증분 전송을 수행했습니다.

SNAPSHOT_2="migrate2"
zfs snapshot -r "${OLDPOOL}@${SNAPSHOT_2}"
zfs send -v \
  -i "${OLDPOOL}/${DATASET}@${SNAPSHOT_1}" \
     "${OLDPOOL}/${DATASET}@${SNAPSHOT_2}" \
  | zfs receive -v "${NEWPOOL}/${DATASET}"

12단계: 풀 이름 바꾸기

처음에는 새 풀 이름을 tank로 바꿀 생각이었지만, TrueNAS에서는 네트워크 공유와 크론 잡이 풀 이름에 묶여 있다는 걸 깨달았습니다. 메인 풀 이름을 pool1에서 tank로 바꾸면 새로운 이름에 맞춰 여러 네트워크 공유와 크론 잡을 수동으로 다시 만들거나 수정해야 합니다.

더 쉬운 방법은 새 풀이 기존 풀의 이름을 이어받도록 하는 것이어서 그 방식을 택했습니다.

먼저 풀을 익스포트해 오프라인으로 만들려고 했지만 여러 TrueNAS 시스템 서비스가 이를 막았습니다. 그래서 여러 서비스를 강제로 중단했습니다.

sudo systemctl stop k3s
sudo umount -l /var/lib/kubelet
sudo systemctl stop smbd
sudo systemctl stop nmbd
sudo systemctl stop winbind
sudo systemctl stop middlewared
sudo systemctl stop netdata

그 다음에는 풀을 오프라인으로 만들어 이름을 바꿀 수 있었습니다.

# Rename my old pool with the suffix `-old`.
zpool export -f "${OLDPOOL}" && \
  zpool export -f "${NEWPOOL}" && \
  zpool import "${OLDPOOL}" "${OLDPOOL}-old"
# Rename my new pool to my old pool's name.
zpool import ${NEWPOOL} ${OLDPOOL}
zfs set mountpoint="/mnt/${OLDPOOL}" "${OLDPOOL}"

이후 중단했던 모든 서비스를 다시 시작했습니다.

sudo systemctl start middlewared
sudo systemctl start smbd
sudo systemctl start nmbd
sudo systemctl start winbind
sudo systemctl start netdata
sudo systemctl start k3s

하지만 제대로 동작하지 않아 다시 재부팅했습니다.

재부팅 후 TrueNAS는 pool1을 새로운 8TB 디스크 5개짜리 RAIDZ2 풀로 인식했습니다.

가져오기 후 TrueNAS 대시보드

13단계: 새 RAIDZ2 풀 스크럽하기

꼭 필요한지는 모르겠지만, 기존 풀을 삭제하기 전에 데이터 무결성에 대한 확신을 더 얻고자 새 풀에서 스크럽을 실행했습니다. 스크럽은 오류 없이 완료되었습니다.

스크럽 완료 화면

14단계: 기존 풀 삭제하기

이제 가장 무서운 단계인 기존 풀 삭제입니다. zpool destroy로 완전히 삭제했습니다.

zpool destroy "${OLDPOOL}-old"

15단계: 가짜 디스크를 실제 디스크로 교체하기

기존 RAIDZ1 풀을 삭제했으니 이제 새 RAIDZ2 풀로 옮길 디스크가 세 개 남았습니다.

첫 번째 디스크 마이그레이션은 RAIDZ2 풀에서 가짜 디스크(8TB 스파스 파일)를 교체하는 것이므로 특별한 절차가 필요합니다.

zpool replace를 이용해 가짜 디스크를 실제 디스크로 교체했습니다.

FAKE_DISK='/tmp/fake-drive.img'
REPLACEMENT_DISK="$(get_disk_id sde)"
NEWPOOL='pool1'
zpool replace "${NEWPOOL}" "${FAKE_DISK}" "${REPLACEMENT_DISK}"

zpool status를 확인해보니 ZFS가 방금 교체한 디스크로 데이터를 리실버링(재구성)하고 있었습니다.

$ zpool status "${NEWPOOL}"
  pool: pool1
 state: DEGRADED
status: One or more devices is currently being resilvered.  The pool will
        continue to function, possibly in a degraded state.
action: Wait for the resilver to complete.
  scan: resilver in progress since Sat May 31 19:58:51 2025
        2.04T / 28.5T scanned at 56.6G/s, 0B / 28.5T issued
        0B resilvered, 0.00% done, no estimated completion time
config:
        NAME                                   STATE     READ WRITE CKSUM
        pool1                                  DEGRADED     0     0     0
          raidz2-0                             DEGRADED     0     0     0
            ata-HGST_HUS728T8TALE6L1_VGGGYUEG  ONLINE       0     0     0
            ata-HGST_HUS728T8TALE6L1_VRGMRVJK  ONLINE       0     0     0
            ata-HGST_HUS728T8TALE6L1_VRGNZU9K  ONLINE       0     0     0
            ata-TOSHIBA_HDWG480_71R0A14YFR0H   ONLINE       0     0     0
            replacing-4                        DEGRADED     0     0     0
              /tmp/fake-drive.img              OFFLINE      0     0     0
              ata-ST8000VN004-2M2101_WSD5B9XY  ONLINE       0     0     0
errors: No known data errors

TrueNAS UI에서도 디스크를 교체 중임을 보여줍니다.

가짜 디스크 교체 중 화면

15단계: 남은 디스크 두 개 흡수하기

마지막으로 삭제된 기존 RAIDZ1 풀에서 남은 디스크 두 개로 새 RAIDZ2 풀을 확장할 차례입니다.

또 문제가 발생했습니다.

$ NEWDISK=$(get_disk_id sdd)
$ zpool attach "${NEWPOOL}" raidz2-0 "${NEWDISK}"
cannot attach /dev/disk/by-id/ata-ST8000VN004-3CP101_WRQ02GX5 to raidz2-0: raidz_expansion feature must be enabled in order to attach a device to raidz

알고 보니 RAIDZ 확장 기능은 기본적으로 꺼져 있었습니다. 아마도 저처럼 이전 버전의 TrueNAS에서 업그레이드한 경우에만 꺼져 있는 것 같습니다.

$ zpool get feature@raidz_expansion "${NEWPOOL}"
NAME   PROPERTY                 VALUE                    SOURCE
pool1  feature@raidz_expansion  disabled                 local

zpool upgrade로 RAIDZ 확장 기능을 활성화했습니다.

zpool upgrade -o feature@raidz_expansion=enabled "${NEWPOOL}"

그런 다음 RAIDZ 확장 기능이 활성화되었는지 확인했습니다.

$ zpool get feature@raidz_expansion "${NEWPOOL}"
NAME   PROPERTY                 VALUE                    SOURCE
pool1  feature@raidz_expansion  enabled                  local

이제 다시 디스크를 추가할 수 있습니다.

zpool attach "${NEWPOOL}" raidz2-0 "${NEWDISK}"

그리고 zpool status에서 디스크를 추가 중임을 확인할 수 있습니다.

$ zpool status "${NEWPOOL}" | grep --after-context=1 "expand:"
+ grep --after-context=1 expand:
+ zpool status "${NEWPOOL}"
expand: expansion of raidz2-0 in progress since Sun Jun  1 08:08:03 2025
        17.1G / 28.5T copied at 501M/s, 0.06% done, 16:36:03 to go

zpool status에서 확장이 100%에 도달한 것을 확인한 뒤, 남은 디스크에 대해서도 같은 과정을 반복해 최종 상태에 도달했습니다.

완성된 7x8TB RAIDZ2 풀

모든 디스크 마이그레이션을 마치고 나니, 정상적이고 건강한 8TB 디스크 7개짜리 RAIDZ2 풀을 갖추게 되었고 사용 가능한 용량은 33TB(30TiB)입니다.

완성된 풀의 TrueNAS 대시보드

선택 사항: 데이터 리스트라이핑 강제하기

독자 @intelfx님이 RAIDZ 확장 기능의 함정을 알려주셨습니다. RAIDZ 확장 이전에 디스크에 있던 데이터는 3x 데이터 + 2x 패리티에서 5x 데이터 + 2x 패리티로 자동 리스트라이핑되지 않기 때문에, 7개 디스크 어레이의 향상된 저장 효율을 활용할 수 없습니다. 결과적으로 RAIDZ2 풀의 전체 용량을 다 쓸 수 없게 됩니다.

이는 마이그레이션 이전 데이터셋을 강제로 다시 쓰는 방식으로 우회할 수 있습니다(예: 각 데이터셋을 백업으로 zfs send한 뒤 zfs receive로 복원). OpenZFS 기여자 Rob Norris님이 지적한 바에 따르면, OpenZFS 2.4.x에는 같은 작업을 더 우아하게 수행하는 zfs rewrite 명령이 제공될 예정입니다.

선택 사항: TrueNAS 기능 설정 맞추기

TrueNAS를 유지보수하는 iXsystems 팀의 한 멤버가 reddit에 댓글을 남겨 TrueNAS는 기술적으로 사용자가 명령줄에서 ZFS 풀을 만드는 것을 지원하지 않는다고 말했습니다. 그는 TrueNAS에서 테스트 풀을 하나 만든 뒤 해당 풀의 기능 플래그를 덤프해, 명령줄에서 만든 새 RAIDZ2 풀에 적용하라고 제안했습니다.

명령줄에서 만든 풀에 TrueNAS 플래그를 적용하는 것이 어떤 차이를 만드는지는 잘 모르겠지만, 25.04 기준으로 iXsystems가 권장한다고 생각되는 플래그는 다음과 같습니다.

zpool set feature@lz4_compress=enabled "${NEWPOOL}"
zpool set feature@async_destroy=enabled "${NEWPOOL}"
zpool set feature@empty_bpobj=enabled "${NEWPOOL}"
zpool set feature@multi_vdev_crash_dump=enabled "${NEWPOOL}"
zpool set feature@spacemap_histogram=enabled "${NEWPOOL}"
zpool set feature@enabled_txg=enabled "${NEWPOOL}"
zpool set feature@hole_birth=enabled "${NEWPOOL}"
zpool set feature@extensible_dataset=enabled "${NEWPOOL}"
zpool set feature@embedded_data=enabled "${NEWPOOL}"
zpool set feature@bookmarks=enabled "${NEWPOOL}"
zpool set feature@filesystem_limits=enabled "${NEWPOOL}"
zpool set feature@large_blocks=enabled "${NEWPOOL}"
zpool set feature@large_dnode=enabled "${NEWPOOL}"
zpool set feature@sha512=enabled "${NEWPOOL}"
zpool set feature@skein=enabled "${NEWPOOL}"
zpool set feature@edonr=enabled "${NEWPOOL}"
zpool set feature@userobj_accounting=enabled "${NEWPOOL}"
zpool set feature@encryption=enabled "${NEWPOOL}"
zpool set feature@project_quota=enabled "${NEWPOOL}"
zpool set feature@device_removal=enabled "${NEWPOOL}"
zpool set feature@obsolete_counts=enabled "${NEWPOOL}"
zpool set feature@zpool_checkpoint=enabled "${NEWPOOL}"
zpool set feature@spacemap_v2=enabled "${NEWPOOL}"
zpool set feature@allocation_classes=enabled "${NEWPOOL}"
zpool set feature@resilver_defer=enabled "${NEWPOOL}"
zpool set feature@bookmark_v2=enabled "${NEWPOOL}"
zpool set feature@redaction_bookmarks=enabled "${NEWPOOL}"
zpool set feature@redacted_datasets=enabled "${NEWPOOL}"
zpool set feature@bookmark_written=enabled "${NEWPOOL}"
zpool set feature@log_spacemap=enabled "${NEWPOOL}"
zpool set feature@livelist=enabled "${NEWPOOL}"
zpool set feature@device_rebuild=enabled "${NEWPOOL}"
zpool set feature@zstd_compress=enabled "${NEWPOOL}"
zpool set feature@draid=enabled "${NEWPOOL}"
zpool set feature@zilsaxattr=enabled "${NEWPOOL}"
zpool set feature@head_errlog=enabled "${NEWPOOL}"
zpool set feature@blake3=enabled "${NEWPOOL}"
zpool set feature@block_cloning=enabled "${NEWPOOL}"
zpool set feature@vdev_zaps_v2=enabled "${NEWPOOL}"
zpool set feature@redaction_list_spill=enabled "${NEWPOOL}"
zpool set feature@raidz_expansion=enabled "${NEWPOOL}"
zpool set feature@fast_dedup=enabled "${NEWPOOL}"
zpool set feature@longname=enabled "${NEWPOOL}"
zpool set feature@large_microzap=enabled "${NEWPOOL}"

더 큰 RAIDZ2 풀을 만들기 위한 영리한 스파스 파일 꼼수를 알려주신 TrueNAS 포럼의 @NugentS님께 감사드립니다. 일부 세부 사항을 정정해주신 iXsystems의 Chris님께도 감사드립니다.

원문은 Michael Lynch님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.