RAIDZ1에서 RAIDZ2로 ZFS 풀 마이그레이션하기
원문은 Michael Lynch님이 에 게재했습니다. 이 블로그 구독하기
최근 집에서 쓰는 TrueNAS 서버를 업그레이드하면서 4개 디스크로 구성된 RAIDZ1 ZFS 풀에 있던 18TB 데이터를 새로운 RAIDZ2 풀로 옮겼다.
재미있는 점은 8TB 디스크 3개만 추가로 사용했고, 데이터를 외부 저장소로 옮기는 과정이 전혀 없었다는 것이다.
데이터를 외부 저장소로 옮기지 않고 RAIDZ1에서 RAIDZ2로 업그레이드하는 게 까다로운 이유는 다음과 같다:
- RAIDZ1 풀을 그 자리에서 RAIDZ2 풀로 변환할 수 없다.
- ZFS 풀을 더 적은 수의 디스크로 축소할 수 없다.
- 18TB 데이터를 8TB 디스크 3개로 구성된 RAIDZ2 풀(새로 산 디스크 3개)에는 담을 수 없다.
내가 한 방법
0단계: 초기 상태
처음 상태에서는 8TB 디스크 4개로 구성된 RAIDZ1 ZFS 풀이 있었고, 전체 용량 23TB 중 18TB를 사용 중이었다. 이번 마이그레이션을 위해 리퍼브 8TB 디스크 3개를 구매했다.
1단계: 디스크 하나를 빌려 RAIDZ2 풀 만들기
먼저 기존 RAIDZ1 풀에서 디스크 하나를 빼서 풀을 디그레이드(degraded) 상태로 만든다.
이어서 다음과 같이 8TB 디스크 5개로 구성된 새로운 RAIDZ2 풀을 만든다:
- 새로 구매한 디스크 3개
- RAIDZ1 풀에서 빼낸 디스크 1개
- 가짜 디스크 역할을 할 8TB 스파스 파일 1개
스파스 파일은 /tmp 디렉터리에 있는데, 그곳 파일시스템이 실제로 8TB 데이터를 저장할 수는 없다. 하지만 괜찮다. 이 스파스 파일은 임시방편이기 때문이다. 5개 디스크짜리 풀을 만들기 위한 꼼수일 뿐, 실제로 데이터를 쓰지는 않을 것이다.
2단계: 가짜 디스크 오프라인 처리
다음으로 RAIDZ2 풀에서 가짜 디스크(8TB 스파스 파일)를 오프라인 처리한다.
가짜 디스크를 오프라인 처리한 뒤에도 풀은 여전히 정상 상태다. RAIDZ2는 디스크 2개가 빠져도 동작할 수 있기 때문이다.
3단계: RAIDZ1 풀의 스냅샷을 RAIDZ2 풀로 마이그레이션
RAIDZ1 풀에 있는 모든 데이터셋을 스냅샷으로 만들고, zfs send를 이용해 그 스냅샷을 새로운 RAIDZ2 풀로 마이그레이션한다.
4단계: 기존 풀 삭제
데이터가 RAIDZ2 풀로 성공적으로 마이그레이션된 것을 확인한 뒤 기존 RAIDZ1 풀을 삭제한다. 그러면 디스크 3개가 남는다.
5단계: 가짜 디스크를 기존 디스크로 교체
기존 RAIDZ1 풀에서 나온 디스크 하나를 이용해 RAIDZ2 풀의 가짜 디스크를 교체한다. 이제 빠진 디스크가 없는 상태가 된다.
6단계: 기존 디스크로 새 풀 확장
마지막으로 ZFS 확장 기능을 이용해 기존 RAIDZ1 풀에서 남은 디스크 2개를 새로운 RAIDZ2 풀에 추가한다. 이렇게 하면 정상 상태의 7x8TB RAIDZ2 풀에서 총 33TB의 사용 가능한 용량을 확보하게 된다.
업데이트: 더 안전한 전략
독자들이 제안한 이 전략의 변형이 내가 원래 썼던 방식보다 더 마음에 든다:
- 새 디스크 3개와 가짜 디스크(스파스 파일) 2개를 이용해 5x8TB RAIDZ2 풀을 만든다.
- RAIDZ2 풀에서 가짜 디스크들을 오프라인 처리한다.
- 기존 RAIDZ1 풀에서 새로운 RAIDZ2 풀로 데이터를 마이그레이션한다.
- RAIDZ1 풀에서 디스크 하나를 오프라인 처리해 RAIDZ2 풀의 가짜 디스크 하나를 교체한다.
- RAIDZ1 풀을 삭제한다.
- 삭제된 RAIDZ1 풀에서 남은 디스크 3개를 RAIDZ2 풀로 옮긴다.
이 전략이 좋은 이유는 마이그레이션 과정 중 어떤 디스크 하나가 고장 나더라도 견딜 수 있기 때문이다. 원래 방식에서는 RAIDZ2 풀의 디스크 중 하나가 고장 나도 마이그레이션을 버틸 수 있었지만, 마이그레이션 중에 RAIDZ1 풀의 디스크 중 하나라도 죽으면 풀이 망가졌다.
이 새 전략의 유일한 단점은 내 원래 전략처럼 직접 테스트해보지 않았다는 점이다.
왜 RAIDZ1에서 RAIDZ2로 갈아탔을까?
나는 2022년에 홈 TrueNAS 서버를 4x8TB RAIDZ1 풀로 구축했다. RAIDZ1은 디스크 하나 고장은 견딜 수 있고, 4개 중 2개가 동시에 고장 날 가능성은 낮다고 생각했기 때문에 RAIDZ1에 만족했다.
내가 간과한 점은 풀에 디스크를 추가할 때마다 데이터 손실 가능성이 커진다는 것이었다. 디스크 4개 중 2개가 고장 나는 건 걱정하지 않았지만, 디스크가 6개나 8개가 되면 동시에 고장 날 가능성이 더 현실적으로 느껴지기 시작한다.
RAIDZ1 풀에 디스크를 하나 추가할 때마다 RAIDZ2로의 마이그레이션은 더 어려워진다. 새 풀은 더 커져야 하고, 새 풀에 쓸 수 있는 물리적 디스크 슬롯은 더 줄어들기 때문이다.
RAIDZ2는 디스크 2개가 고장 나도 데이터 손실 없이 견딜 수 있다. 현재 서버 케이스의 최대 용량인 10개 디스크까지 확장하더라도 디스크 3개가 동시에 고장 날 가능성은 충분히 낮다고 느껴져 걱정되지 않는다.
왜 RAIDZ1에서 RAIDZ2로 전환하기 어려울까?
ZFS는 RAIDZ1에서 RAIDZ2로 전환하는 것을 지원하지 않는다. RAIDZ 모드는 풀 생성 시점에 정해지며 절대 바꿀 수 없다.
가장 단순한 방법은 디스크 5개를 추가로 사서 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개로 마이그레이션 전략을 테스트하는 모습
드라이브 크기가 서로 달라 완벽한 실험은 아니었지만 과정은 성공했고, 덕분에 프로덕션 서버에서도 마이그레이션 계획을 시도할 자신감을 얻었다.
2단계: 데이터 백업
위에서 데이터를 외부 저장소로 옮긴 적이 없다고 했지만, 엄밀히 말하면 사실이 아니다. 백업용으로는 외부 저장소를 사용했지만, 결국 필요하지는 않았다.
나는 이미 restic으로 매일 밤 백업을 실행하고 있지만, 이는 ZFS 수준이 아니라 파일시스템 수준의 백업이다. 즉, 마이그레이션 중에 ZFS 풀을 망가뜨렸다면 ZFS를 인식하는 백업에서 한 번에 복원하는 대신 ZFS 데이터셋을 하나씩 다시 만들고 백업에서 파일을 복원해야 한다는 뜻이다.
매일 밤 백업을 하고 있지만, 백업하지 않는 데이터가 한 가지 있다. 바로 미디어다. 나는 데이터를 모으는 걸 좋아해서 DVD와 블루레이의 원본 이미지를 모두 보관한다. 1998년 영화 메리에겐 뭔가 특별한 것이 있다(There’s Something About Mary) DVD에 담긴 감독 코멘터리를 언젠가 보고 싶어질지 모르지 않은가?

디스크 서버 용량의 대부분은 내가 리핑한 DVD와 블루레이가 차지한다.
원본 디스크 이미지와 인코딩된 비디오 파일을 합치면 TrueNAS 서버에 영화와 TV 프로그램이 약 15TB 있다. Wasabi나 Backblaze B2 같은 저렴한 클라우드 스토리지에 백업해도 한 달에 약 90달러가 들어서, 그런 파일들은 클라우드에 백업하지 않는다.
미디어 데이터를 잃더라도 원본 디스크에서 다시 리핑하면 된다고 스스로를 달래곤 했다. 하지만 이 작업을 시작하면서 500장이 넘는 디스크를 하나하나 다시 리핑하고 재인코딩해야 할 가능성을 생각해 보니, 혹시 마이그레이션 중에 문제가 생길 경우를 대비해 예외적으로 미디어 파일을 단기간 백업하기로 결정했다.
15TB를 3일 동안 어디에 백업할 수 있을까?
나는 15TB만 백업하면 됐다. 일 단위나 시간 단위로 과금하는 업체를 찾을 수 있다면 비용이 꽤 저렴할 것 같았다.
ZFS 네이티브 백업을 제공하는 클라우드 업체는 두 곳을 알고 있다:
- rsync.net: 최소 과금 기간이 한 달이다(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 Box: 0.09달러/TB/일
- 안타깝게도 이 옵션은 마이그레이션을 마친 뒤에야 알게 됐다.
나는 Backblaze B2를 선택했는데, 15TB 데이터가 Backblaze 쪽에서는 어떻게 24.6TB가 되었다.
모든 데이터를 Backblaze에 업로드하는 데 거의 일주일이 걸려서 예상보다 오래 데이터를 보관해야 했지만, 월말 청구서는 평소보다 몇 달러 더 나온 정도라 비용이 어떻게 계산된 건지 잘 모르겠다.
경고: 클라우드 스토리지 업체의 최소 보관 정책을 확인하세요
처음에는 실패한 마이그레이션 시도 때 Wasabi에 백업했다. 괜찮다고 생각했다. 며칠 백업한 비용으로 Wasabi에 30달러 정도만 내면 될 줄 알았다. 그런데 Wasabi는 일 단위 과금을 지원하긴 하지만, 백업한 모든 데이터에 대해 최소 90일치 요금을 내야 한다는 걸 알게 됐다. 그래서 30달러 청구서를 예상했는데 300달러 청구서를 받게 생긴 것이다.
Wasabi 지원팀에 선처를 부탁하는 메일을 보냈더니, 이상한 조언이 돌아왔다. 계정을 삭제하라는 것이었다.
전체 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 대시보드를 보는 것이 더 편하다:
위 스크린샷을 기준으로 내 디스크 ID는 다음과 같다:
- 기존 풀의 디스크:
sda,sdc,sde,sdf - 새 디스크:
sdb,sdg,sdh
4단계: 가장 약한 디스크 찾기
마이그레이션에서 가장 위험한 구간은 기존 RAIDZ1 풀에서 디스크를 빌린 시점부터 데이터를 새 RAIDZ2 풀로 마이그레이션할 때까지다. RAIDZ1 풀에서 디스크 하나를 빼면 풀이 디그레이드 상태가 된다. 데이터 마이그레이션 중에 기존 풀의 다른 디스크 중 하나라도 죽으면 데이터를 잃게 된다.
이 위험한 구간 때문에 기존 풀에서 가장 약한 디스크를 빼서 새 풀을 만드는 데 쓰고 싶었다. RAIDZ2 풀은 약한 디스크가 고장 나도 견딜 수 있지만, RAIDZ1 풀은 그렇지 못하기 때문이다.
가장 약한 디스크를 찾기 위해 내 디스크들의 SMART 진단 데이터를 조회하는 간단한 bash 스니펫을 실행했다:
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: 032sdc: 077sde: 067sdf: 067
- 새 디스크
sdb: 100sdg: 100sdh: 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 errorssda2(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 errors7단계: 약한 디스크 완전히 지우기
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단계: 스파스 파일로 가짜 디스크 만들기
4x8TB RAIDZ2 풀을 만들 수도 있지만, 그러면 사용 가능한 용량이 16TB밖에 되지 않아 18TB 데이터를 담기에 부족하다.
대신 5x8TB RAIDZ2 풀을 만들되, 다섯 번째 디스크는 실제로 존재하지 않게 한다. ZFS에서는 스파스 파일을 디스크처럼 사용해 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 풀 생성
마침내 5x8TB 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 풀.
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 export와 zpool 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을 새로운 5x8TB RAIDZ2 풀로 인식한다:
13단계: 새 RAIDZ2 풀 스크럽
꼭 필요한지는 모르겠지만, 기존 풀을 삭제하기 전에 데이터 무결성에 대한 확신을 더 얻기 위해 새 풀에서 스크럽을 실행했다. 스크럽은 오류 없이 완료됐다:
14단계: 기존 풀 삭제
이제 가장 무서운 단계다. 기존 풀을 삭제하는 것이다. zpool destroy로 날려 버린다:
zpool destroy "${OLDPOOL}-old"15단계: 가짜 디스크를 실제 디스크로 교체
기존 RAIDZ1 풀을 삭제했으므로 이제 새로운 RAIDZ2 풀로 옮길 디스크가 3개 남았다.
첫 번째 디스크 마이그레이션은 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 errorsTrueNAS UI에서도 디스크를 교체 중임을 표시한다:
15단계: 남은 두 디스크 흡수
마지막으로 삭제된 기존 RAIDZ1 풀에서 남은 디스크 2개로 새로운 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 localzpool 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 gozpool status에 확장이 100%에 도달했다고 표시되면 남은 디스크로 같은 과정을 반복해 최종 상태에 도달한다.
완성된 7x8TB RAIDZ2 풀
모든 디스크 마이그레이션이 끝나자, 이제 33TB(30TiB)의 사용 가능한 용량을 가진 건강하고 정상적인 7x8TB RAIDZ2 풀이 생겼다:
선택 사항: 데이터 강제 리스트라이핑
독자 @intelfx님이 RAIDZ 확장 기능의 함정 하나를 알려주셨다. RAIDZ 확장 이전에 디스크에 존재하던 데이터는 3x 데이터 + 2x 패리티에서 5x 데이터 + 2x 패리티로 자동으로 리스트라이핑되지 않으므로, 7개 디스크 어레이의 개선된 저장 효율을 활용할 수 없다. 결국 RAIDZ2 풀의 전체 용량을 다 쓸 수 없게 된다.
이는 마이그레이션 이전 데이터셋을 강제로 다시 쓰는 방식으로 우회할 수 있다(예: 각 데이터셋을 백업으로 zfs send한 뒤 zfs receive로 복원). OpenZFS 기여자 Rob Norris님이 같은 일을 더 우아하게 해주는 zfs rewrite 명령이 OpenZFS 2.4.x에 들어올 예정이라고 지적한다.
선택 사항: TrueNAS 기능 설정 맞추기
TrueNAS를 유지보수하는 iXsystems 팀원 한 분이 reddit에 댓글을 달아 TrueNAS는 사용자가 커맨드라인에서 ZFS 풀을 생성하는 것을 기술적으로 지원하지 않는다고 말했다. 그는 TrueNAS에서 테스트 풀을 하나 만든 뒤 그 기능 플래그를 덤프해, 내가 커맨드라인에서 만든 새로운 RAIDZ2 풀에 적용하라고 제안했다.
CLI에서 만든 풀에 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님께도 감사드립니다.
글을 무작위로 읽기






댓글
로그인하고 댓글 남기기