,

Ceph의 CRUSH 맵을 손보다가 이상한 문제를 만났습니다. OSD는 정상으로 돌아가는데, Proxmox 관리 화면에서 일부 OSD의 상세 정보를 열면 ec-pve4라는 노드를 찾지 못한다는 오류가 났습니다. 이름을 못 찾는 것 같아서 /etc/hosts에도 별칭을 넣었습니다. 그래도 그대로였습니다.

원인은 제가 역할을 나누려고 만든 CRUSH의 가상 host 이름을 Proxmox가 실제 클러스터 노드 이름으로 사용한다는 데 있었습니다. 2026년 8월에 이 문제를 추적하면서 device class 전환안까지 시험했습니다. 최종적으로는 기존 구성을 유지했지만, 그 과정에서 CRUSH의 배치 정책과 관리 도구의 가정이 어디서 만나는지 꽤 많이 배웠습니다.

🧭 EC와 mirror OSD를 나누고 싶었던 이유

제 홈랩은 Proxmox 노드 다섯 대로 구성되어 있습니다. 각 미니 PC 안의 NVMe 외에, OCuLink로 밖에 연결한 NVMe도 사용합니다. 외부 SSD 네 개는 상위 PCIe 3.0 x4 연결 하나를 공유합니다. SSD 개수가 네 배라고 호스트까지 가는 대역폭도 네 배가 되는 구조는 아닙니다.

그래서 VM 디스크 등에 쓰는 복제 풀과 큰 파일을 보관하는 EC 풀의 OSD 집합을 나누고 싶었습니다. 여기서 mirror는 제가 복제 풀 계열을 부르는 이름이고, RAID1 장치를 뜻하는 것은 아닙니다. 과거 EC 풀 운영 중 겪은 문제 때문에, 내부 NVMe도 파티션을 나눠 OSD를 따로 두었습니다.

저장장치 / OSDEC 계열 후보mirror 계열 후보
내부 NVMe의 EC용 파티션 OSD포함제외
내부 NVMe의 mirror용 파티션 OSD제외포함
외부 OCuLink NVMe OSD포함제외
SATA / USB SATA OSD해당 매체를 허용한 풀만 포함제외

목표는 OSD 하나가 EC와 mirror 양쪽의 후보가 되지 않게 하는 것이었습니다. 같은 계열의 여러 풀이 OSD를 공유하는 것까지 금지한다는 뜻은 아닙니다. 또 같은 SSD를 파티션으로 나눠도 물리 디스크나 컨트롤러 장애는 함께 겪습니다. 여기서 얻으려던 것은 OSD와 배치 정책의 분리입니다.

🗺️ 역할을 host 이름으로 나누니 생긴 문제

당시에는 역할별 가지 아래에 pve1과 ec-pve1처럼 별도의 host 버킷을 두었습니다. 같은 실제 서버의 OSD라도 역할에 따라 다른 CRUSH host 밑으로 들어간 셈입니다. 아래는 관계만 남긴 개념도입니다.

CRUSH의 역할별 가지
├─ mirror
│  └─ host pve4       → mirror용 OSD
└─ erasurecode
   └─ host ec-pve4    → EC용 OSD

실제 Proxmox 서버는 두 경우 모두 pve4

Ceph의 배치 규칙에서는 두 집합을 구분할 수 있었습니다. 그런데 Proxmox의 OSD 관리 API는 OSD의 직접 부모가 host이면 그 버킷 이름을 OSD의 host 필드로 넣었습니다. 조사 당시 설치된 PVE/API2/Ceph/OSD.pm에서 확인한 동작입니다. Proxmox의 OSD API 소스에서도 이 관계를 확인할 수 있습니다.

OSD의 CRUSH 부모 = ec-pve4
→ API 응답의 host = ec-pve4
→ 관리 화면이 /nodes/ec-pve4/... 경로로 요청
→ 실제 Proxmox 클러스터에는 pve4만 등록되어 있음

IP 주소를 찾는 것과 클러스터 노드로 등록되어 있는 것은 별개였습니다. getent hosts ec-pve4가 올바른 IP를 반환해도, 그것이 Proxmox의 노드 ID가 되지는 않습니다. 그래서 hosts 별칭을 추가해도 이 문제는 해결되지 않았습니다. 가상의 CRUSH 이름을 맞추려고 Corosync의 노드 구성을 바꿀 이유도 없었습니다.

🔍 UI를 살리려면 실제 host 아래에 OSD를 두기

검토한 전환안은 host를 실제 노드명으로 통일하고, 역할을 device class로 옮기는 것이었습니다.

host pve4
├─ 내부 NVMe의 mirror용 OSD    class mirror
├─ 내부 NVMe의 EC용 OSD        class ec
└─ 외부 NVMe의 EC용 OSD        class ec

mirror 풀 규칙 → mirror class만 선택
EC 풀 규칙     → ec class만 선택

device class를 지정하면 Ceph는 그 class에 속한 장치만 포함하는 shadow hierarchy를 이용해 배치를 계산합니다. 역할을 class로 표현하면 실제 host 계층을 유지하면서 OSD 후보를 나눌 수 있습니다. 이 동작은 Ceph Squid의 device class 문서에 설명되어 있습니다.

다만 제 구성에서는 class에 이미 nvmex4, nvmex1, sata처럼 물리 연결 정보를 넣어두고 있었습니다. 특히 SATA 포함 여부는 실제 풀 선택에도 영향을 줬습니다. OSD 하나에 두 개의 class를 태그처럼 붙일 수는 없으니, ec/mirror만으로 바꾸려면 매체 제한도 다시 설계해야 했습니다.

역할과 매체를 조합한 class를 만들거나, 물리 연결 정보는 별도 인벤토리로 옮기는 방법을 검토했습니다. 중요한 것은 class 이름을 예쁘게 바꾸는 게 아니라, 각 풀에서 허용하는 OSD 집합이 원래 의도와 같은지 확인하는 일이었습니다.

🧪 운영 맵을 바꾸기 전에 복사본으로 비교했다

구조를 새로 만들면 데이터 배치가 달라질 수 있습니다. 그래서 운영 클러스터에 바로 적용하지 않고, CRUSH 맵 복사본에 전환안을 만든 뒤 기존 맵과 비교했습니다. Ceph에는 이런 병렬 계층을 class 기반으로 전환하는 crushtool --reclassify 기능과 비교 기능이 있습니다. 공식 전환 설명은 기존 bucket ID를 shadow bucket에 보존하는 방식을 다룹니다.

이 실험에서도 ID를 맞추는 것만으로 끝나지 않았습니다. 중간 후보에서는 EC 규칙의 비교 입력 1,000,001개 중 15개가 달랐습니다. 텍스트로 옮긴 weight의 정밀도를 원본 값에 맞춘 뒤, 최종 후보에서 차이가 0개가 됐습니다. 눈으로는 거의 같은 weight여도 배치 결과는 다를 수 있었습니다.

# original.bin과 candidate.bin은 당시 따로 준비한 CRUSH 맵 복사본
# 규칙 번호와 shard/replica 수는 당시 환경의 값
crushtool -i candidate.bin --compare original.bin \
  --min-x 0 --max-x 1000000 --rule 1 --num-rep 5

crushtool -i candidate.bin --compare original.bin \
  --min-x 0 --max-x 1000000 --rule 10 --num-rep 3
당시 시험 조건비교 입력 수최종 불일치
EC 규칙 1, 5 shard1,000,0010
복제 규칙 10, 3 replica1,000,0010

이 결과는 당시 후보 맵과 해당 규칙·입력 범위에서 배치가 같았다는 뜻입니다. 모든 풀, 모든 장애 상태, 나중에 추가할 OSD까지 검증했다는 뜻은 아닙니다. 실제 적용을 한다면 최신 맵을 다시 받아 사용 중인 모든 규칙과 풀 조건을 확인해야 합니다. 이번 작업에서는 이 후보 맵을 운영 클러스터에 적용하지 않았습니다.

🚧 OCuLink의 I/O를 하나씩만 처리하게 해주지는 않는다

처음 목표를 이야기하면서 혼동했던 부분도 있습니다. host를 failure domain으로 선택하면 하나의 PG에 필요한 복제본이나 EC shard를 서로 다른 host 버킷으로 나눌 수 있습니다. 하지만 여러 PG와 여러 클라이언트가 동시에 작업하는 상황에서, 같은 OCuLink에 연결된 SSD들이 반드시 하나씩만 동작하게 되는 것은 아닙니다.

외부 SSD를 mirror 풀의 후보에서 제외하면 그 풀의 데이터가 외부 OSD에 배치되는 것을 막을 수 있습니다. 그렇다고 EC 작업끼리의 동시 I/O까지 없어지지는 않습니다. CRUSH가 정하는 것은 데이터의 배치이고, 링크 전체의 I/O 실행 순서는 아닙니다.

가상 host를 유지할 때는 반대쪽 함정도 있습니다. pve4와 ec-pve4는 물리적으로 같은 서버여도 CRUSH에서는 서로 다른 host 버킷입니다. 두 가지를 모두 포함하는 규칙에서 host 분산만 믿으면 같은 실제 서버에 데이터 조각이 겹칠 수 있습니다. 따라서 역할별 후보 집합과 각 풀이 쓰는 규칙을 함께 확인해야 합니다.

🛠️ 최종 선택은 기존 구성을 유지하는 것

전환 가능성은 확인했지만, 그날은 기존 구성을 유지하기로 했습니다. 물리 매체 구분과 역할 분리를 모두 다시 정리하면서 관리 화면까지 맞추는 대신, 해당 OSD의 관리 작업은 실제 호스트를 확인해서 CLI로 처리하기로 했습니다. UI 오류를 고친 전환 후기라고 쓸 수 없는 이유입니다. ㅎㅎ

상태를 확인할 때는 아래처럼 배치 정보와 실제 실행 호스트를 구분해서 봅니다. ceph osd find의 CRUSH host도 가상 이름일 수 있으므로, 그 값만으로 실제 서버를 단정하지 않는 것이 포인트입니다.

# 45는 예시 OSD 번호
ceph osd tree
ceph osd find 45
ceph osd metadata 45
ceph osd pool ls detail
ceph osd crush rule dump

# 실제 실행 호스트를 확인한 뒤, 그 호스트에서 상태 조회
systemctl status [email protected]

OSD 재시작 때 위치를 어떻게 등록하는지도 함께 확인해야 합니다. 당시에는 OSD별 crush_location 설정이 있었기 때문에 가상 host 구조가 유지되고 있었습니다. 향후 전환한다면 맵뿐 아니라 이 시작 시점 설정까지 같은 설계로 맞춰야 합니다.

📝 정리

이번에 확실해진 것은 세 가지입니다. CRUSH host와 Proxmox 노드 이름은 관리 화면에서 연결되어 있었고, hosts 별칭은 그 노드 등록을 대신하지 못했습니다. 역할을 device class로 옮기는 전환안은 오프라인에서 시험할 수 있었지만, 물리 매체 정책까지 함께 검토해야 했습니다. 그리고 매핑 비교가 통과했다는 사실과 운영에 적용했다는 사실은 구분해야 했습니다.

나중에 이 구성을 다시 정리한다면 실제 노드 계층, OSD 역할, 매체 정보를 먼저 따로 적어볼 생각입니다. 관리 화면에서 보이는 이름 하나를 고치려다가, 제가 스토리지에 기대하고 있던 조건부터 다시 정리하게 된 작업이었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다