Skip to content

Latest commit

 

History

History
116 lines (81 loc) · 13.4 KB

File metadata and controls

116 lines (81 loc) · 13.4 KB

v20.2.0 — Ceph FastEC: 소거 코드 풀에서 작은 쓰기 하나에 스트라이프 전체를 다시 쓰지 않게 만들기

메타데이터

항목 내용
프로젝트 Ceph
관련 릴리스 v20.2.0 Tentacle (2025-11-18)
설계 문서 doc/dev/osd_internals/erasure_coding/enhancements.rstceph/ceph#64809 — Doc: Erasure Coding enhancements for tentacle (2025-08-05 머지, Bill Scales)
핵심 구현 6eac1550b4 — osd: Add EC_OPTIMIZATIONS flag and add ability to switch it on. (ceph/ceph#62451 병합, Alex Ainscow), 파티 델타 구현 src/osd/ECTransaction.cc/ECUtil.cc, 정렬 검증 ceph/ceph#67319 — mon: Deny EC optimizations (fast EC) for non-4k-aligned chunk-sizes
발견 출처 Ceph.io — Fast Erasure Coding for Tentacle Performance Updates, Tentacle v20.2.0 Release Notes의 RADOS 항목
검증 근거 doc/dev/osd_internals/erasure_coding/enhancements.rst 설계 문서 원문, 실제 구현 커밋(pg_pool_t::FLAG_EC_OPTIMIZATIONS), ParityDelta/parity_delta 관련 소스(ECTransaction.cc, ECUtil.cc, ErasureCodeInterface.h, ISA-L/Jerasure/SHEC 플러그인)
소스 라이선스 코드 기본값 LGPL-2.1 또는 LGPL-3.0, doc/*는 CC BY-SA 3.0 (COPYING 기준)

개요

Ceph Tentacle은 소거 코드(Erasure Coding, EC) 풀의 오래된 근본적인 비효율 — "1바이트만 바꿔도 스트라이프 전체를 읽고 다시 인코딩해서 다시 쓴다" — 를 해결하는 새 I/O 경로(FastEC)를 도입했다. 필요한 조각(shard)만 읽고 쓰는 partial read/write, RAID 컨트롤러에서 쓰이던 기법을 빌려온 parity delta write를 추가하고, 이 새 경로를 켜는 풀 단위의 영구 플래그 allow_ec_optimizations를 만들었다. 기본 EC 플러그인도 오랫동안 유지보수가 끊긴 Jerasure에서 ISA-L로 바뀌었는데, 이는 델타 갱신에 필요한 새 인터페이스를 활성 유지보수 라이브러리에서만 안정적으로 구현할 수 있었기 때문이다.

사전 지식

K+M 소거 코드와 스트라이프

Ceph의 소거 코드 풀은 객체를 K개의 데이터 조각(chunk)과 M개의 코딩 패리티 조각으로 나눠 서로 다른 OSD에 분산 저장한다(K+M 구성). 이 K+M개 조각을 합쳐 하나의 "스트라이프"라 부르고, 기본 조각 크기(chunk size)는 4KB다. 객체가 스트라이프 경계에 맞지 않으면 0으로 패딩해 정수 개의 스트라이프로 맞춘다. M개 조각까지 잃어도 나머지로 원본을 복원할 수 있는 게 소거 코드의 핵심이지만, 그 대가로 조각 하나를 고치려 해도 인코딩 관계(패리티) 전체가 다시 맞아야 한다.

기존 읽기/덮어쓰기 방식: 항상 스트라이프 전체 단위로 움직인다

기존 EC 코드는 읽기 요청이 스트라이프의 일부만 필요로 해도 K개 데이터 조각 전체를 읽어 필요 없는 부분을 버린 뒤 클라이언트에 반환했다. 덮어쓰기(overwrite)는 더 비쌌다 — 기존 스트라이프 전체를 읽고, 새 데이터를 병합하고, 새 코딩 패리티를 계산한 뒤, 데이터·패리티 조각 전체를 다시 썼다(이른바 read-modify-write, RMW). K가 클수록(예: 6+2, 10+2처럼 폭이 넓은 코딩일수록) 작은 쓰기 하나가 건드리는 조각 수는 그대로 K+M개였다 — 용량 효율을 위해 폭 넓은 코딩을 쓰고 싶어도 성능이 이를 가로막았다.

왜 이런 비효율이 존재했는가: 메타데이터 일관성이 단순한 모델을 요구했다

이 설계는 우연이 아니었다. Primary OSD 하나가 스트라이프에 대한 모든 쓰기를 순서대로 처리하고, 객체 메타데이터(object_info_t의 버전 번호, PG 로그, PG 통계)를 모든 K+M개 조각에 동일하게 복제해 두는 모델은, 어느 조각을 봐도 그 객체의 최신 버전·이력을 알 수 있다는 강한 보장을 준다. 이 보장 덕분에 peering(장애 후 어느 조각이 최신인지 합의하는 과정)과 backfill(뒤처진 조각을 따라잡히는 과정)이 단순해진다 — 하지만 데이터 자체를 건드리지 않는 조각까지 메타데이터 갱신을 위해 매번 쓰기를 받아야 한다는 비용을 동반했다.

변경 분석

변경 전: 작은 쓰기 하나가 스트라이프 전체를 다시 쓰게 만든다

[변경 전] Full-stripe read-modify-write (K=4, M=2 예시)

RADOS 클라이언트
  │ 1바이트만 덮어쓰기 요청
  ▼
Primary OSD
  │ 1. 스트라이프 전체 읽기 (데이터 4조각)
  ├──────────┬──────────┬──────────┬──────────┐
  ▼          ▼          ▼          ▼          │
OSD1(D1)   OSD2(D2)   OSD3(D3)   OSD4(D4)      │
  │          │          │          │           │
  │ 2. 새 데이터 병합, 3. 새 패리티 P·Q 계산     │
  │ 4. 데이터+패리티 전체 다시 쓰기               │
  ▼          ▼          ▼          ▼           ▼
OSD1 재기록  OSD2 재기록  OSD3 재기록  OSD4 재기록  OSD-P,Q 재기록

문제: 1바이트를 바꾸려 해도 K+M(=6)개 OSD 모두에 읽기+쓰기가 발생한다.
     K가 커질수록(코딩 폭이 넓을수록) 비용이 그대로 늘어난다.

변경 후: 건드린 조각만 읽고 쓰거나, 델타만 전파한다

doc/dev/osd_internals/erasure_coding/enhancements.rst 설계 문서는 두 가지 최적화를 제시하고, 실제 구현(src/osd/ECTransaction.cc, ECUtil.cc)에 반영됐다.

  • Partial write: 덮어써지지 않는 데이터 조각은 아예 읽지도 쓰지도 않는다. 수정되는 데이터 조각과, 그로부터 다시 계산해야 하는 코딩 패리티 조각만 읽고/쓴다.
  • Parity delta write: RAID5/6 컨트롤러가 쓰던 기법을 빌려왔다. 기존 데이터를 읽어 새 데이터와 XOR해 "델타"를 만들고, 각 코딩 패리티를 읽어 그 델타를 적용(XOR 또는 Galois Field 연산)해 다시 쓴다. M=2 구성이면 이 방식은 K 값과 무관하게 딱 3번의 읽기와 3번의 쓰기로 끝난다 — K가 큰 코딩일수록 partial write보다 훨씬 적은 I/O로 끝난다는 뜻이다.

두 기법 중 어느 쪽이 유리한지는 I/O가 스트라이프의 얼마나 큰 비율을 건드리는지에 달려 있다 — 스트라이프 대부분을 덮어쓰는 큰 쓰기라면 partial write가, 작은 부분만 건드리는 쓰기라면 parity delta write가 유리하다. Primary OSD는 이 계산을 매 쓰기 요청마다 수행해 어느 기법을 쓸지 고른다. 두 기법 모두 XOR/Galois Field 연산을 지원하는 라이브러리가 필요한데, 설계 문서는 "Jerasure와 ISA-L 모두 지원을 확인했다"고 명시한다 — Tentacle이 기본 플러그인을 Jerasure에서 ISA-L로 바꾼 이유 중 하나가 여기 있다(Jerasure는 2014년 이후 유지보수가 끊겼다).

이 새 경로는 풀 단위의 새 플래그 allow_ec_optimizations(pg_pool_t::FLAG_EC_OPTIMIZATIONS, 비트 1<<19)로 켠다. 실제 구현 커밋의 주석은 "enable optimizations, once enabled, cannot be disabled"라고 명시한다 — 한번 켜면 되돌릴 수 없는 영구 플래그다. 또한 mon은 조각 크기가 4KB 단위로 정렬되지 않은 풀에서는 이 플래그를 켜는 것 자체를 거부한다(#67319) — partial I/O가 페이지/조각 크기 경계에 맞춰 정렬된 읽기·쓰기를 전제로 하기 때문이다.

[변경 후] Partial write + parity delta write (K=4, M=2, 작은 쓰기 예시)

RADOS 클라이언트
  │ 1바이트만 덮어쓰기 요청 (allow_ec_optimizations=on 풀)
  ▼
Primary OSD — 스트라이프의 몇 %가 바뀌는지에 따라 기법 선택
  │
  ├─ (건드는 비율이 작음) parity delta write 선택
  │     1. 옛 데이터 조각(D1)만 읽기
  │     2. 새 데이터와 XOR → 델타 생성
  │     3. 코딩 패리티(P, Q)만 읽어 델타 적용 후 다시 쓰기
  │
  ▼
OSD1(D1) 읽기+쓰기      OSD2,3,4(D2~D4) 관여 없음      OSD-P,Q 읽기+쓰기

결과: K값(D2~D4)과 무관하게 3개 OSD(D1, P, Q)만 관여한다.
     M+1(=3)개 조각만 최신 메타데이터(object_info_t, PG 로그, PG 통계)를 갖고,
     나머지 K-1개 조각은 이번 쓰기에 참여하지 않아 메타데이터도 갱신되지 않는다.

설계 결정 및 트레이드오프

왜 partial write 하나로 통일하지 않고 parity delta write를 별도로 구현했는가: partial write는 여전히 "건드린 데이터 조각 수 + M개 패리티"만큼 I/O가 발생해 K가 커질수록 비용이 늘어난다. parity delta write는 K와 무관하게 데이터 조각 1개(또는 건드린 만큼)와 M개 패리티만 오가면 되므로, 폭 넓은 코딩(K가 큰 구성)에서 이득이 훨씬 크다. 대신 스트라이프 대부분이 바뀌는 큰 쓰기에서는 델타 계산 자체가 오히려 낭비이므로, 설계 문서는 두 기법을 모두 구현하고 I/O 크기에 따라 유리한 쪽을 계산해서 고르는 쪽을 택했다 — 하나의 "만능" 경로 대신 상황별 최적 경로 두 개를 유지하는 비용을 감수한 것이다.

allow_ec_optimizations를 되돌릴 수 없는 영구 플래그로 만들었는가: 새 I/O 경로는 일부 조각만 갱신되는 메타데이터 모델(PG 로그 항목에 "이번에 실제로 갱신된 조각 집합"을 기록하는 새 포맷 등)을 쓴다. 설계 문서는 다운그레이드를 지원하지 않는 이유로 "새 포맷의 로그 항목을 구버전 OSD가 이해하지 못하고, 진행 중인 I/O를 안전하게 quiesce하기도 쉽지 않으며, 이미 쓰인 xattr 메타데이터도 구버전이 이해할 수 없다"를 든다. 이론적으로는 플래그를 끄기 전에 전체 메타데이터를 재스캔해 되돌리는 것도 가능하지만, "그런 코드를 작성할 계획이 없다"고 명시적으로 선을 그었다 — 안전하게 구현하기 어려운 되돌리기 경로를 만드는 대신, 애초에 막다른 길(풀 삭제)만 남겨두는 쪽을 택한 것이다.

왜 4KB 정렬 검증을 모니터에서 막았는가: partial read/write는 I/O를 조각 크기(또는 향후 페이지 크기) 단위로 정렬해서 처리한다는 전제 위에 서 있다. 이 전제가 깨지는 조합(4KB로 정렬되지 않은 조각 크기의 EC 프로파일)에서 최적화를 켜면 개별 OSD마다 흩어져서 실패하거나 조용히 잘못된 최적화를 적용할 위험이 있다. mon이 클러스터 전체를 대표하는 지점에서 미리 걸러내면, 잘못된 설정이 실제 I/O 경로까지 가기 전에 막힌다.

왜 이번 릴리스가 "Direct I/O"(클라이언트가 Primary OSD를 거치지 않고 조각을 가진 OSD에 바로 요청)까지 가지 않았는가: 설계 문서는 이 최적화를 별도 스토리로 명확히 분리해 두었다 — 순서 보장, 장애 시나리오, 백필과의 상호작용까지 새로 설계해야 하는 훨씬 어려운 작업이라 설명한다. Tentacle 20.2.0은 그중 위험이 낮고 이득이 확실한 I/O 증폭 감소(partial I/O, parity delta write)만 먼저 출시하는 점증적 전달을 택했다 — Primary OSD가 여전히 모든 쓰기의 순서를 정하는 중앙 지점이라는 기존 모델은 이번 변경에서 그대로 유지된다.

더 살펴볼 점

  • 설계 문서가 별도 스토리로 남겨둔 "Direct read/write"(Primary OSD를 거치지 않는 I/O)는 어느 릴리스에 나올까?
  • allow_ec_optimizations가 영구적이라는 제약이, 이미 운영 중인 대규모 EC 풀의 실제 적용 결정에 얼마나 걸림돌이 될까?
  • ISA-L로 기본값이 바뀐 뒤, 여전히 Jerasure 기반 커스텀 프로파일을 쓰던 클러스터들은 어떤 경로로 마이그레이션하게 될까?

참고 자료

참고 외 별도 확인 링크