DRBD — 분산 복제 블록 디바이스
DRBD(Distributed Replicated Block Device)를 고가용성 스토리지 복제 관점에서 심층 분석합니다. Primary/Secondary 역할 전환과 디스크 상태 머신, Protocol A/B/C 지연시간·내구성·처리량 특성 비교, Wire Protocol 패킷 구조와 핸드셰이크 과정, Activity Log와 Bitmap 기반 재동기화 알고리즘, Online Verify를 통한 무정지 무결성 검증, Multi-Volume 리소스 구성, WAN 환경 프록시 복제, drbd_device/drbd_peer_device 내부 동작과 복제 스레드 모델, 네트워크 단절 시 quorum 정책과 split-brain 예방 전략, Pacemaker/Corosync OCF RA 연동 자동화, LINSTOR 기반 대규모 볼륨 수명주기 관리, DRBD + NFS/SMB HA NAS 운영 패턴, 성능 튜닝과 모니터링/트러블슈팅 절차까지 실전 운영 기준으로 다룹니다.
핵심 요약
- DRBD — "네트워크 RAID 1"로 불리는 블록 레벨 복제 솔루션입니다. 두 (또는 그 이상) 노드의 블록 디바이스를 실시간으로 동기화합니다.
- Primary/Secondary — Primary 노드만 읽기/쓰기 마운트가 가능합니다. Secondary는 복제본을 유지하며 대기합니다. (dual-primary 모드는 특수한 경우)
- Protocol A/B/C — A(비동기), B(반동기), C(동기). C는 Secondary가 디스크에 쓴 후에야 Primary의 쓰기가 완료됩니다.
- Activity Log — 현재 쓰기 중인 블록 범위를 추적합니다. 크래시 복구 시 전체 디스크 대신 AL 범위만 재동기화합니다.
- Bitmap — 변경된 블록을 비트맵으로 추적합니다. 재연결 시 변경분만 동기화(Resync)하여 효율적입니다.
- Online Verify — 서비스 중단 없이 Primary/Secondary 데이터 일치 여부를 해시 비교로 검증합니다.
- Quorum — 3노드 이상 구성에서 과반수 동의가 있는 노드만 Primary가 됩니다. Split-Brain을 예방합니다.
- LINSTOR — DRBD 위에서 동작하는 소프트웨어 정의 스토리지(SDS) 레이어입니다. Kubernetes CSI와 통합됩니다.
단계별 이해
- DRBD 기본 개념 파악 — DRBD는 블록 디바이스(
/dev/drbd0)를 제공합니다. 이 위에 파일시스템을 올립니다.Primary 노드에 마운트된 파일시스템의 쓰기가 Secondary로 복제됩니다.
- 복제 프로토콜 이해 — Protocol C(동기)가 기본값입니다. 쓰기 완료 = Primary와 Secondary 모두 디스크에 기록된 시점입니다.
Protocol A는 Primary 로컬 I/O 완료 = 쓰기 완료로 처리하여 성능이 높지만 Primary 장애 시 데이터 손실 가능성이 있습니다.
- Wire Protocol 이해 — DRBD 노드 간 TCP 연결 위에 P_DATA, P_WRITE_ACK 등의 패킷이 교환됩니다.
커넥션 핸드셰이크, 데이터 복제, 제어 메시지가 모두 이 와이어 프로토콜 위에서 동작합니다.
- Activity Log와 Bitmap — AL은 현재 활성 쓰기 영역을, Bitmap은 피어와 달라진 블록을 추적합니다.
크래시 후 AL 범위만 재동기화하고, 네트워크 단절 후에는 Bitmap 기반으로 변경분만 복제합니다.
- Pacemaker 통합 — DRBD OCF RA와 Pacemaker가 함께 동작합니다. 노드 장애 시 Pacemaker가 DRBD를 Promote(Secondary→Primary)하고 서비스를 이전합니다.
NFS 서버나 Samba/ksmbd가 DRBD 디바이스 위에서 실행되므로, 페일오버 시 자동으로 스토리지도 이전됩니다.
- LINSTOR 확장 — LINSTOR는 여러 DRBD 볼륨을 중앙에서 관리합니다. Kubernetes에서 CSI를 통해 영구 볼륨을 자동 프로비저닝합니다.
대규모 NAS 환경에서 볼륨 생성/삭제/크기 조정을 자동화합니다.
DRBD 개요
DRBD(Distributed Replicated Block Device)는 Linux 블록 계층에서 동작하는 네트워크 스토리지 복제 솔루션입니다. 개발사 LINBIT이 오픈소스로 공개하였으며, Linux 커널 2.6.33(2010년)에 메인라인으로 포함되었습니다.
| 특성 | 내용 |
|---|---|
| 메인라인 포함 | Linux 2.6.33 (2010년, DRBD 8.3) |
| 소스 위치 | drivers/block/drbd/ |
| 개발사 | LINBIT (오스트리아) |
| 최신 버전 | DRBD 9.x (다중 복제 지원, OOB 메타데이터) |
| 사용자 도구 | drbdadm, drbdsetup, drbdmeta |
| 설정 파일 | /etc/drbd.conf, /etc/drbd.d/*.res |
| 장치 노드 | /dev/drbd<n> |
| 메타데이터 | 인밴드 또는 외부(별도 디바이스/파티션) |
| 복제 방향 | DRBD 8: 1:1 (2노드), DRBD 9: 1:N (최대 32노드) |
| 프로토콜 | 독자적 Wire Protocol (TCP 기반) |
drivers/block/drbd/)에는 DRBD 8.x가 포함되어 있습니다. DRBD 9.x는 out-of-tree 모듈로, LINBIT 저장소에서 별도로 제공됩니다. DRBD 9는 Multi-Volume, 3노드 이상 복제, Quorum 등의 기능을 추가합니다. 실무에서는 대부분 DRBD 9.x를 사용합니다.
DRBD 복제 아키텍처
DRBD는 블록 디바이스 계층에서 동작하는 커널 모듈로, 상위 파일시스템이나 애플리케이션에는 일반 블록 디바이스처럼 보입니다. 내부적으로 로컬 디스크 I/O와 네트워크 복제를 동시에 처리합니다.
역할 모델
| 역할 | I/O 허용 | 마운트 | 전환 명령 |
|---|---|---|---|
| Primary | 읽기/쓰기 | 가능 | drbdadm primary r0 |
| Secondary | 읽기 전용 (복제 수신만) | 불가 | drbdadm secondary r0 |
| Dual-Primary | 양쪽 읽기/쓰기 | GFS2/OCFS2 필요 | allow-two-primaries yes |
연결 상태
| 연결 상태 | 설명 | 데이터 복제 |
|---|---|---|
StandAlone | 피어와 연결 끊김, 재연결 시도 안 함 | 불가 |
Disconnecting | 연결 해제 중 | 불가 |
Unconnected | 연결 대기 중 (재시도 간격) | 불가 |
Connecting | TCP 연결 시도 중 | 불가 |
Connected | 핸드셰이크 완료, 정상 복제 중 | 정상 |
SyncSource | 동기화 소스 (데이터 전송 중) | Resync 중 |
SyncTarget | 동기화 대상 (데이터 수신 중) | Resync 중 |
VerifyS / VerifyT | 온라인 검증 소스/대상 | 검증 중 |
# 상태 확인 명령
$ drbdadm status r0
r0 role:Primary
disk:UpToDate
peer role:Secondary
replication:Established peer-disk:UpToDate
# 상세 통계와 함께 확인
$ drbdsetup status r0 --verbose --statistics
r0 role:Primary suspended:no
volume:0 minor:0 disk:UpToDate quorum:yes
size:104857600 read:1024 written:51200 al-writes:128 bm-writes:0
node2 role:Secondary
volume:0 replication:Established peer-disk:UpToDate
received:0 sent:51200 out-of-sync:0 pending:0 unacked:0
복제 프로토콜 (Protocol A/B/C)
DRBD는 데이터 신뢰성과 성능 간의 트레이드오프를 조정하는 세 가지 복제 프로토콜을 제공합니다.
| 속성 | Protocol A | Protocol B | Protocol C |
|---|---|---|---|
| 완료 조건 | 로컬 I/O + TCP 송신 버퍼 | 로컬 I/O + 피어 TCP 수신 | 로컬 I/O + 피어 디스크 I/O |
| 쓰기 레이턴시 | 로컬 디스크 레이턴시 | max(로컬, RTT) | max(로컬, RTT + 피어 I/O) |
| 처리량 (IOPS) | 로컬과 동일 | 로컬의 70~90% | 로컬의 50~80% |
| 데이터 손실 위험 | Primary 장애 시 | Secondary 전원 장애 시 | 양쪽 동시 장애 시만 |
| RPO | 수 초 | 0 (정상 시) | 0 (보장) |
| 권장 환경 | WAN / DR | 중간 거리 | 프로덕션 HA (기본) |
Wire Protocol 패킷 구조
DRBD 노드 간 통신은 TCP 위에서 독자적인 Wire Protocol을 사용합니다. 각 패킷은 고정 크기 헤더와 가변 크기 페이로드로 구성됩니다.
주요 패킷 타입
| 패킷 명령 | 방향 | 설명 |
|---|---|---|
P_DATA | Primary → Secondary | 쓰기 데이터 전송 (섹터 오프셋 + 데이터) |
P_WRITE_ACK | Secondary → Primary | 데이터 기록 완료 확인 (Protocol C) |
P_RECV_ACK | Secondary → Primary | 데이터 수신 확인 (Protocol B) |
P_BARRIER | Primary → Secondary | 쓰기 에포크 경계 (순서 보장) |
P_BARRIER_ACK | Secondary → Primary | 에포크 완료 확인 |
P_DATA_REQUEST | Secondary → Primary | 읽기 요청 (피어에서 읽기, 드물게 사용) |
P_RS_DATA_REQUEST | SyncTarget → SyncSource | 재동기화 데이터 요청 |
P_RS_DATA_REPLY | SyncSource → SyncTarget | 재동기화 데이터 응답 |
P_CSUM_RS_REQUEST | SyncSource → SyncTarget | 체크섬 기반 재동기화 요청 |
P_OV_REQUEST | VerifyS → VerifyT | 온라인 검증 해시 요청 |
P_OV_REPLY | VerifyT → VerifyS | 온라인 검증 해시 응답 |
P_STATE | 양방향 | 역할/디스크/연결 상태 변경 알림 |
P_UUIDS | 양방향 | Generation Identifier 교환 |
/* include/linux/drbd.h — 패킷 헤더 구조 (DRBD 8) */
struct p_header80 {
u32 magic; /* DRBD_MAGIC = 0x83740267 */
u16 command; /* P_DATA, P_WRITE_ACK 등 */
u16 length; /* 페이로드 길이 */
} __packed;
/* DRBD 9 확장 헤더 */
struct p_header100 {
u32 magic; /* DRBD_MAGIC_100 = 0x8372 */
u16 volume; /* Multi-Volume 지원 */
u16 command;
u32 length;
u32 pad; /* 8바이트 정렬 */
} __packed;
/* P_DATA 페이로드 — 쓰기 데이터 */
struct p_data {
u64 sector; /* 시작 섹터 */
u64 block_id; /* epoch_entry 식별자 */
u32 seq_num; /* 시퀀스 번호 (순서 보장) */
u32 dp_flags; /* DP_HARDBARRIER, DP_FLUSH 등 */
/* 뒤이어 실제 데이터 바이트 */
} __packed;
current-uuid, bitmap-uuid, history-uuid[0], history-uuid[1].
커널 내부 구조
DRBD 커널 모듈(drbd.ko)은 블록 디바이스 드라이버로서 struct request_queue에 자신의 make_request 함수를 등록합니다.
핵심 구조체
/* DRBD 리소스 — 복제 단위의 최상위 컨테이너 */
struct drbd_resource {
char *name; /* 리소스 이름 (예: "r0") */
struct list_head devices; /* 볼륨 목록 */
struct list_head connections; /* 피어 연결 목록 */
enum drbd_role role; /* R_PRIMARY / R_SECONDARY */
struct mutex state_mutex; /* 상태 전환 직렬화 */
struct kref kref; /* 참조 카운트 */
};
/* DRBD 디바이스 — /dev/drbd<n> */
struct drbd_device {
struct drbd_resource *resource; /* 소속 리소스 */
struct gendisk *vdisk; /* 블록 디바이스 구조체 */
struct request_queue *rq; /* 요청 큐 */
struct drbd_backing_dev *ldev; /* 로컬 백엔드 디바이스 */
unsigned long flags; /* 상태 플래그 */
unsigned int vnr; /* 볼륨 번호 */
/* 비트맵: 변경된 블록 추적 */
struct drbd_bitmap *bitmap;
/* Activity Log: 현재 쓰기 활성 구간 */
struct lru_cache *act_log; /* al-extents LRU 캐시 */
struct list_head active_ee; /* 처리 중인 쓰기 에포크 */
struct list_head sync_ee; /* 동기화 중인 에포크 */
struct list_head done_ee; /* 완료된 에포크 */
};
/* DRBD 피어 디바이스 — 연결된 원격 노드 하나 */
struct drbd_peer_device {
struct drbd_device *device;
struct drbd_connection *connection;
enum drbd_repl_state repl_state; /* L_ESTABLISHED, L_SYNC_SOURCE 등 */
enum drbd_disk_state disk_state; /* D_UP_TO_DATE, D_INCONSISTENT 등 */
};
/* DRBD 연결 — 피어 노드 하나와의 TCP 연결 */
struct drbd_connection {
struct drbd_resource *resource;
struct socket *data_socket; /* 데이터 소켓 */
struct socket *meta_socket; /* 메타 소켓 (제어 메시지) */
struct task_struct *receiver; /* 수신 스레드 */
struct task_struct *sender; /* 송신 스레드 */
struct task_struct *worker; /* 워커 스레드 */
enum drbd_conn_state cstate; /* 연결 상태 */
struct list_head peer_devices; /* 이 연결의 피어 디바이스 목록 */
};
bio 처리 흐름
/* DRBD make_request — 쓰기 요청 처리 */
static void drbd_make_request(struct request_queue *q, struct bio *bio)
{
struct drbd_device *device = q->queuedata;
if (bio_data_dir(bio) == WRITE) {
/* 1. Activity Log에 쓰기 구간 등록 */
drbd_al_begin_io(device, &peer_req->i);
/* 2. 비트맵에 변경 구간 표시 (피어와 다름 기록) */
drbd_bm_set_bits(device, sector, size);
/* 3. 로컬 디스크에 bio 제출 */
drbd_submit_peer_request(device, peer_req, bio->bi_opf);
/* 4. 복제: 피어에게 P_DATA 패킷 전송 */
drbd_send_dblock(peer_device, peer_req);
/* Protocol C: P_WRITE_ACK 대기 (drbd_recv_ack) */
/* Protocol A: TCP 송신 버퍼에 복사되면 완료 */
/* Protocol B: 피어 TCP 수신 확인(P_RECV_ACK)까지 대기 */
} else {
/* 읽기: 로컬 디스크에서만 처리 */
drbd_submit_bio(device, bio);
}
}
스레드 모델
| 스레드 | 생성 위치 | 역할 |
|---|---|---|
drbd_receiver | drbd_receiver.c | 피어로부터 데이터 수신, P_DATA 처리, ACK 전송 |
drbd_sender | drbd_sender.c | P_DATA 전송, 재연결 처리, Resync 스케줄링 |
drbd_worker | drbd_worker.c | 워크 큐 처리, Resync I/O, 비트맵 갱신 |
drbd_asender | drbd_main.c | ACK 전송 전용 (Protocol C 성능 최적화) |
/* drbd_receiver.c — 수신 스레드 메인 루프 */
static int drbd_receiver(struct drbd_thread *thi)
{
struct drbd_connection *connection = thi->connection;
while (get_t_state(thi) == RUNNING) {
/* 패킷 헤더 읽기 */
drbd_recv_header(connection, &pi);
/* 명령별 핸들러 디스패치 */
switch (pi.cmd) {
case P_DATA:
receive_Data(connection, &pi);
break;
case P_RS_DATA_REPLY:
receive_RSDataReply(connection, &pi);
break;
case P_BARRIER:
receive_Barrier(connection, &pi);
break;
/* ... 기타 패킷 핸들러 ... */
}
}
return 0;
}
디스크 상태 머신
| 상태 | 코드 | 설명 | 전환 조건 |
|---|---|---|---|
| Diskless | D_DISKLESS | 디스크 없음 (detached) | attach 전 또는 detach 후 |
| Attaching | D_ATTACHING | 디스크 연결 중 | drbdadm attach 실행 |
| Failed | D_FAILED | 디스크 I/O 오류 | 로컬 디스크 오류 발생 |
| Negotiating | D_NEGOTIATING | 피어와 협상 중 | connect 시 상태 교환 |
| Inconsistent | D_INCONSISTENT | 데이터 불일치 | Resync 시작 전/중 |
| Outdated | D_OUTDATED | 최신이 아님 | 피어가 더 최신 GI 보유 |
| Consistent | D_CONSISTENT | 일관성 있음 | 피어 연결 없이 정상 종료 |
| UpToDate | D_UP_TO_DATE | 최신 상태 | Resync 완료 후 정상 동작 |
Activity Log (AL)
Activity Log는 DRBD에서 가장 중요한 메타데이터 구조 중 하나입니다. 현재 활성 쓰기가 진행 중인 디스크 영역을 추적하여, 크래시 후 복구 시간을 극적으로 단축합니다.
AL 설정
# /etc/drbd.d/r0.res — Activity Log 설정
resource r0 {
disk {
# AL extent 수 (기본 1237, 최대 65534)
# 하나의 extent = 4MB 범위
al-extents 6433; # 6433 x 4MB = 약 25GB 활성 범위
# AL 업데이트 방식
al-updates yes; # 기본값: 매 쓰기마다 AL 갱신
# al-updates no; # 성능 최대화, 크래시 시 전체 Resync 필요
}
}
# AL 통계 확인
$ drbdsetup status r0 --verbose --statistics
volume:0 ... al-writes:12847 bm-writes:0
# al-writes: AL 트랜잭션 수 (높으면 랜덤 I/O 패턴)
# AL extent 크기 계산
# al-extents가 클수록 → 크래시 복구 범위 넓어짐 (느려짐)
# al-extents가 작으면 → LRU 교체 빈번 (AL I/O 오버헤드)
# 최적값: 워킹셋 크기 / 4MB = 필요한 al-extents
al-updates no로 설정하면 AL 쓰기 오버헤드가 사라져 성능이 향상되지만, Primary 크래시 시 전체 디스크를 Resync해야 합니다. 배터리 백업(BBU) RAID 컨트롤러 등 안전한 캐시가 있는 환경에서만 사용하세요.
AL 내부 구현
/* drbd_actlog.c — Activity Log 핵심 함수 */
/* 쓰기 전: AL에 extent 등록 */
void drbd_al_begin_io(struct drbd_device *device,
struct drbd_interval *i)
{
unsigned int first = i->sector >> (AL_EXTENT_SHIFT - 9);
unsigned int last = i->sector + (i->size >> 9) - 1;
last >>= (AL_EXTENT_SHIFT - 9);
wait_event(device->al_wait,
_al_get(device, first, last));
/* AL 트랜잭션을 메타 디바이스에 기록 */
drbd_al_begin_io_commit(device);
}
/* 쓰기 완료 후: AL에서 extent 해제 */
void drbd_al_complete_io(struct drbd_device *device,
struct drbd_interval *i)
{
lc_put(device->act_log, &al_ext->lce);
wake_up(&device->al_wait);
}
Bitmap과 재동기화 알고리즘
DRBD의 비트맵(Bitmap)은 피어와 달라진 블록을 추적하는 핵심 메타데이터입니다. 네트워크 단절 후 재연결 시 비트맵 기반으로 변경분만 효율적으로 재동기화합니다.
Bitmap 및 Resync 설정
# /etc/drbd.d/r0.res — Resync 설정
resource r0 {
net {
# 체크섬 기반 Resync (변경되지 않은 블록 전송 생략)
csums-alg sha256; # 또는 crc32c (빠름), sha1
# 데이터 무결성 검증 (선택사항)
data-integrity-alg sha256; # 전송 데이터 해시 검증
}
disk {
# Resync 속도 제한
resync-rate 200M; # 초당 200MB (기본: 250K)
# 가변 Resync 속도 (DRBD 8.4+)
c-plan-ahead 20; # 20 0.1초 단위 = 2초 미리 보기
c-fill-target 50; # 50 섹터 목표 충진량
c-min-rate 10M; # 최소 10MB/s 보장
c-max-rate 500M; # 최대 500MB/s
}
}
# Resync 진행 상황 확인
$ drbdadm status r0
r0 role:Primary
disk:UpToDate
peer role:Secondary
replication:SyncSource peer-disk:Inconsistent
done:42.8 # 42.8% 완료
# Resync 강제 시작 (전체 동기화)
$ drbdadm invalidate-remote r0 # 피어를 무효화하고 전체 Resync
$ drbdadm invalidate r0 # 자신을 무효화하고 피어에서 받기
resync-rate 대신 가변 속도를 사용하면 서비스 I/O 부하에 따라 Resync 속도를 자동 조절합니다. 서비스 I/O가 바쁘면 Resync를 늦추고, 한가하면 빠르게 진행합니다. DRBD 8.4+에서 지원됩니다.
Online Verify (무정지 무결성 검증)
Online Verify는 서비스를 중단하지 않고 Primary와 Secondary의 데이터 일치 여부를 블록 단위 해시 비교로 검증하는 기능입니다. 정기적으로 실행하여 Silent Data Corruption(비트 부패)을 조기에 발견합니다.
# /etc/drbd.d/r0.res — Online Verify 설정
resource r0 {
net {
verify-alg sha256; # 검증 해시 알고리즘
}
}
# 수동 Online Verify 실행
$ drbdadm verify r0
# 진행 상황 확인
$ drbdadm status r0
r0 role:Primary
disk:UpToDate
peer role:Secondary
replication:VerifyS peer-disk:UpToDate
done:67.3
# cron으로 주간 스케줄 (매주 일요일 새벽 2시)
$ crontab -e
0 2 * * 0 /usr/sbin/drbdadm verify r0
# 검증 결과 확인
$ dmesg | grep "Online verify"
drbd r0: Online verify done. sector count=204800000 oos=0 # oos=0: 불일치 없음
drbd r0: Online verify done. sector count=204800000 oos=8 # oos=8: 8 섹터 불일치
# 불일치 발견 시 수동 Resync
$ drbdadm disconnect r0
$ drbdadm connect r0
# → 자동으로 out-of-sync 블록만 재동기화
verify-alg는 정기적 무결성 검증에, csums-alg는 Resync 시 네트워크 절약에 사용됩니다. 둘 다 해시를 사용하지만 목적이 다릅니다. 프로덕션에서는 둘 모두 설정하는 것을 권장합니다.
Multi-Volume 리소스
DRBD 9는 하나의 리소스 내에 여러 볼륨을 포함할 수 있습니다. 모든 볼륨이 같은 TCP 연결을 공유하여 연결 오버헤드를 줄이고, 볼륨 간 쓰기 순서를 보장합니다.
# /etc/drbd.d/multi.res — Multi-Volume 설정
resource multi {
options {
quorum majority;
on-no-quorum io-error;
}
volume 0 { # 데이터 볼륨
device /dev/drbd0;
disk /dev/vg0/data;
meta-disk internal;
}
volume 1 { # 로그 볼륨
device /dev/drbd1;
disk /dev/vg0/log;
meta-disk internal;
}
volume 2 { # 메타데이터 볼륨
device /dev/drbd2;
disk /dev/vg0/meta;
meta-disk internal;
}
on node1 { address 10.0.0.1:7789; }
on node2 { address 10.0.0.2:7789; }
on node3 { address 10.0.0.3:7789; }
}
| 특성 | 단일 볼륨 | Multi-Volume |
|---|---|---|
| TCP 연결 수 | 리소스당 2개 (data + meta) | 리소스당 2개 (공유) |
| 쓰기 순서 보장 | 볼륨 내에서만 | 모든 볼륨에 걸쳐 보장 |
| 역할 전환 | 개별 | 리소스 단위 일괄 전환 |
| 볼륨 추가 | N/A | 온라인 추가 가능 (DRBD 9) |
| 용도 | 단순 복제 | DB (데이터+WAL+메타) 일괄 복제 |
WAN 환경과 DRBD Proxy
WAN(Wide Area Network) 환경에서 DRBD를 사용할 때의 핵심 과제는 높은 RTT(Round-Trip Time)와 제한된 대역폭입니다. DRBD Proxy는 이 문제를 버퍼링과 압축으로 해결합니다.
# /etc/drbd.d/dr.res — WAN DRBD Proxy 설정
resource dr {
protocol A; # WAN에서는 Protocol A 필수
net {
sndbuf-size 0; # TCP 자동 튜닝
rcvbuf-size 0;
max-buffers 36k; # 충분한 버퍼 (36000)
max-epoch-size 36k;
}
# Proxy 설정
proxy {
memlimit 2G; # 프록시 버퍼 메모리 2GB
plugin {
zstd 4; # zstd 압축 레벨 4
}
}
on primary-site {
address 10.0.0.1:7789;
proxy on primary-site {
inside 127.0.0.1:7789; # DRBD → 로컬 Proxy
outside 203.0.113.1:7789; # 외부 주소 (WAN)
}
}
on dr-site {
address 10.0.1.1:7789;
proxy on dr-site {
inside 127.0.0.1:7789;
outside 198.51.100.1:7789;
}
}
}
# Proxy 상태 확인
$ drbd-proxy-ctl -c "show drbd-proxy"
... buffer usage: 23% (471MB/2048MB)
... compression ratio: 3.2:1
... transfer rate: 45MB/s (compressed)
| 환경 | Protocol | Proxy | RPO | 처리량 |
|---|---|---|---|---|
| LAN (RTT < 1ms) | C | 불필요 | 0 | 로컬의 50~80% |
| 캠퍼스 (RTT 1~5ms) | B 또는 C | 선택 | 0 | RTT에 반비례 |
| WAN (RTT 10~100ms) | A | 필수 | 수 초 | 대역폭 한계 |
| 대륙 간 (RTT > 100ms) | A | 필수 + 압축 | 수십 초 | 대역폭 x 압축률 |
Quorum 메커니즘
DRBD 9.x에서 도입된 Quorum은 3노드 이상 클러스터에서 Split-Brain을 예방하는 메커니즘입니다. 과반수(N/2 + 1)의 노드가 동의해야 Primary가 될 수 있습니다.
# /etc/drbd.d/r0.res — 3노드 Quorum 설정
resource r0 {
options {
quorum majority; # 과반수 Quorum
on-no-quorum io-error; # Quorum 없을 때 I/O 오류 반환
# on-no-quorum suspend-io; # 또는 I/O를 대기 상태로 유지
# on-no-quorum io-fence-peer; # 또는 피어를 펜싱
# Quorum 최소 redundancy 설정 (DRBD 9.1+)
quorum-minimum-redundancy 1; # 최소 1개 피어 복제본 필요
}
volume 0 {
device /dev/drbd0;
disk /dev/sdb1;
meta-disk internal;
}
on node1 { address 192.168.10.1:7789; }
on node2 { address 192.168.10.2:7789; }
on node3 { address 192.168.10.3:7789; }
}
| 시나리오 | 투표수 | Quorum | 처리 |
|---|---|---|---|
| 3/3 노드 정상 | 3 votes | 달성 (2 필요) | 정상 동작 |
| 2/3 노드 정상 | 2 votes | 달성 | 정상 동작 (1노드 장애 허용) |
| 1/3 노드만 남음 | 1 vote | 미달 | I/O 중단 또는 오류 |
| 네트워크 분리 (1 vs 2) | 1 vs 2 | 2측 달성 | Split-Brain 예방 |
| 5노드 중 3노드 정상 | 3 votes | 달성 (3 필요) | 정상 동작 |
| 5노드 중 2노드 정상 | 2 votes | 미달 | I/O 중단 |
quorum-minimum-redundancy 0으로 설정하거나, 외부 Quorum 디바이스(SCSI Persistent Reservation)를 사용해야 합니다. 가장 좋은 방법은 3번째 노드를 diskless로 추가하는 것입니다.
Split-Brain 탐지 및 복구
Split-Brain은 네트워크 분리로 두 노드가 동시에 Primary가 되어 독립적으로 데이터를 변경한 상황입니다. DRBD는 재연결 시 이를 탐지하고 복구 정책을 적용합니다.
Split-Brain 탐지
# dmesg에서 Split-Brain 확인
$ dmesg | grep "Split-Brain"
drbd r0/0 drbd0: Split-Brain detected, dropping connection!
# DRBD 상태 확인
$ drbdadm status
r0 role:Primary
disk:UpToDate
peer connection:StandAlone # 연결 끊김 상태
# Split-Brain 복구 — 승자/패자 결정 후 수동 해결
# 패자 노드에서 (데이터 포기)
$ drbdadm disconnect r0
$ drbdadm secondary r0
$ drbdadm -- --discard-my-data connect r0
# 승자 노드에서
$ drbdadm connect r0
# → Resync 시작: 승자 → 패자 방향으로 데이터 동기화
자동 복구 정책
# /etc/drbd.d/r0.res 자동 복구 설정
resource r0 {
net {
# Split-Brain 발생 시 자동 복구 정책
after-sb-0pri discard-younger-primary; # Primary 없을 때: 더 최근에 Primary된 쪽 포기
after-sb-1pri discard-secondary; # Primary 1개일 때: Secondary 데이터 포기
after-sb-2pri disconnect; # Primary 2개일 때: 연결 해제 (수동 처리)
}
handlers {
# Split-Brain 알림 핸들러
split-brain "/usr/lib/drbd/notify-split-brain.sh admin@example.com";
}
}
| 정책 | 0pri 상황 | 1pri 상황 | 2pri 상황 |
|---|---|---|---|
disconnect | 연결 해제 | 연결 해제 | 연결 해제 |
discard-younger-primary | 더 최근 Primary 포기 | - | - |
discard-secondary | - | Secondary 포기 | - |
discard-least-changes | 변경 적은 쪽 포기 | - | - |
call-pri-lost-after-sb | - | - | 핸들러 호출 |
after-sb-2pri disconnect를 기본으로 하고, 반드시 관리자가 두 노드의 데이터를 비교한 후 수동으로 승자를 결정하는 것이 안전합니다. Quorum을 사용하면 Split-Brain 자체를 예방할 수 있습니다.
Pacemaker/Corosync 통합
Pacemaker는 DRBD OCF Resource Agent를 통해 DRBD 역할(Primary/Secondary)과 상위 서비스(NFS, Samba 등)를 함께 관리합니다. 노드 장애 시 자동 페일오버가 이루어집니다.
Pacemaker 설정 예제 (2노드 HA)
# Corosync 클러스터 초기화
$ pcs host auth node1 node2
$ pcs cluster setup ha-cluster node1 node2
$ pcs cluster start --all
# STONITH 설정 (필수 — 없으면 Split-Brain 위험)
$ pcs stonith create fence_node1 fence_ipmilan \
ipaddr=192.168.1.101 login=admin passwd=secret \
pcmk_host_list=node1
$ pcs stonith create fence_node2 fence_ipmilan \
ipaddr=192.168.1.102 login=admin passwd=secret \
pcmk_host_list=node2
# DRBD 리소스 에이전트 등록
$ pcs resource create drbd_r0 ocf:linbit:drbd \
drbd_resource=r0 \
op monitor interval=20 role=Promoted \
op monitor interval=30 role=Unpromoted
# Master/Slave 프로모션 가능한 리소스 그룹
$ pcs resource promotable drbd_r0 \
promoted-max=1 promoted-node-max=1 \
clone-max=2 clone-node-max=1 \
notify=true
# 파일시스템 마운트 리소스 (DRBD Primary 노드에서만)
$ pcs resource create fs_r0 Filesystem \
device="/dev/drbd0" \
directory="/mnt/drbd" \
fstype="xfs"
# NFS 서버 리소스
$ pcs resource create nfs_server systemd:nfs-server
# NFS IP (Floating IP)
$ pcs resource create nfs_ip IPaddr2 \
ip=192.168.1.100 cidr_netmask=24
# 의존성 설정: DRBD Primary → 파일시스템 → NFS → IP
$ pcs constraint order promote drbd_r0-clone then start fs_r0
$ pcs constraint order start fs_r0 then start nfs_server
$ pcs constraint order start nfs_server then start nfs_ip
# 같은 노드에서 실행하도록 colocation 설정
$ pcs constraint colocation add fs_r0 with promoted drbd_r0-clone
$ pcs constraint colocation add nfs_server with fs_r0
$ pcs constraint colocation add nfs_ip with nfs_server
페일오버 흐름
# 수동 페일오버 테스트
$ pcs node standby node1
# Pacemaker 자동 처리 순서:
# 1. node1 STONITH (서버 전원 차단)
# 2. node2에서 drbd_r0 Promote (Secondary → Primary)
# 3. /dev/drbd0을 /mnt/drbd에 마운트
# 4. nfs-server 시작
# 5. Floating IP를 node2로 이전
# → NFS 클라이언트는 IP 그대로 재연결
# 페일오버 소요 시간 (목표)
# STONITH: 10~30초 (IPMI 기반)
# DRBD Promote: 즉시 (UpToDate인 경우)
# XFS 마운트: 1~5초 (저널 리플레이)
# NFS 서비스: 2~5초
# 총 소요: 약 15~45초
# 클러스터 상태 확인
$ pcs status
$ drbdadm status
LINSTOR 볼륨 관리
LINSTOR는 DRBD 기반 소프트웨어 정의 스토리지(SDS) 솔루션입니다. LINBIT에서 개발했으며, 여러 DRBD 볼륨을 중앙에서 프로비저닝하고 Kubernetes CSI와 통합됩니다.
# LINSTOR Controller와 Satellite 설치
$ systemctl enable --now linstor-controller # 중앙 관리 서버
$ systemctl enable --now linstor-satellite # 각 노드
# 노드 등록
$ linstor node create node1 192.168.1.1
$ linstor node create node2 192.168.1.2
$ linstor node create node3 192.168.1.3
# 스토리지 풀 생성 (LVM thin 기반)
$ linstor storage-pool create lvmthin node1 DrbdPool vg0/thin_pool
$ linstor storage-pool create lvmthin node2 DrbdPool vg0/thin_pool
$ linstor storage-pool create lvmthin node3 DrbdPool vg0/thin_pool
# 리소스 그룹 생성 (자동 배치 정책)
$ linstor resource-group create ha-group \
--place-count 2 \
--storage-pool DrbdPool
# 볼륨 그룹 생성
$ linstor volume-group create ha-group
# 리소스 생성 (자동 2개 복제)
$ linstor resource-group spawn-resources ha-group myVol 100GiB
# 생성된 DRBD 디바이스 확인
$ linstor resource list
$ drbdadm status myVol
Kubernetes CSI 통합
# linstor-csi Helm 차트로 설치
$ helm repo add linstor https://charts.linstor.io
$ helm install linstor-csi linstor/linstor-csi \
--set linstor.controllerEndpoint=http://linstor-controller:3370
# StorageClass 생성
$ kubectl apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: linstor-drbd
provisioner: linstor.csi.linbit.com
parameters:
linstor.csi.linbit.com/storagePool: DrbdPool
linstor.csi.linbit.com/placementCount: "2"
linstor.csi.linbit.com/allowRemoteVolumeAccess: "false"
EOF
# PVC 생성 시 자동 DRBD 볼륨 프로비저닝
$ kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
storageClassName: linstor-drbd
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 50Gi
EOF
DRBD + CTDB + NFS/SMB HA NAS 구성
DRBD는 블록 복제를 담당하고, Pacemaker/Corosync가 역할 전환을 제어하며, NFS/Samba(ksmbd)가 파일 서비스를 제공하는 완전한 HA NAS 아키텍처입니다.
===================== HA NAS 구성 예제 =====================
# 하드웨어 구성
# node1, node2: 각 1Gbps 이더넷 (서비스), 별도 10GbE (DRBD 복제)
# /dev/sdb: DRBD 백엔드 디스크
# /dev/sdc: STONITH SBD 디바이스 (공유 iSCSI)
# 1단계: DRBD 초기화
$ drbdadm create-md r0 # 메타데이터 초기화
$ drbdadm up r0 # 리소스 활성화
$ drbdadm primary --force r0 # 초기 Primary 지정
# 2단계: 파일시스템 생성 (Primary 노드에서)
$ mkfs.xfs /dev/drbd0
$ mount /dev/drbd0 /mnt/drbd
# 3단계: NFS exports 설정
$ echo '/mnt/drbd 192.168.0.0/24(rw,sync,no_subtree_check)' >> /etc/exports
# 4단계: Pacemaker 자원 설정 (위의 Pacemaker 예제 참조)
# drbd_r0 → fs_r0 → nfs_server → nfs_ip 순서로 의존성 설정
# 5단계: 클라이언트 연결
$ mount -t nfs -o vers=4.2 192.168.1.100:/mnt/drbd /mnt/nas
# Floating IP(192.168.1.100)에 마운트 → 페일오버 시 자동 재연결
| 구성 요소 | 역할 | 장애 시 동작 |
|---|---|---|
| DRBD | 블록 레벨 실시간 복제 | Secondary → Primary 승격 |
| Pacemaker | 클러스터 리소스 관리 | 자동 페일오버 오케스트레이션 |
| Corosync | 노드 멤버십, 하트비트 | 노드 장애 감지 알림 |
| STONITH | 노드 펜싱 (강제 차단) | Split-Brain 방지 보장 |
| XFS/ext4 | 파일시스템 | 저널 리플레이 후 마운트 |
| NFS/Samba | 파일 공유 서비스 | 새 Primary에서 재시작 |
| Floating IP | 서비스 가상 IP | 새 노드로 IP 이전 |
성능 튜닝
네트워크 튜닝
# /etc/drbd.d/r0.res — 네트워크 성능 최적화
resource r0 {
net {
max-buffers 20000; # 최대 버퍼 수 (기본 8000)
max-epoch-size 20000; # 에포크 최대 크기
sndbuf-size 0; # TCP 자동 튜닝 (0=auto, 권장)
rcvbuf-size 0; # TCP 자동 튜닝
protocol C;
# 무결성 검증 (디버깅용, 성능 영향)
# data-integrity-alg sha256;
# 압축 (WAN용)
# use-rle yes; # DRBD 8: 비트맵 RLE 압축
}
}
디스크 튜닝
# /etc/drbd.d/r0.res — 디스크 성능 최적화
resource r0 {
disk {
al-extents 6433; # Activity Log 크기 (최대 65534)
al-updates yes; # AL 갱신 (no=위험하지만 빠름)
# 배터리 백업 RAID 컨트롤러가 있는 경우만
# md-flushes no; # 메타데이터 플러시 비활성화
# disk-flushes no; # 데이터 플러시 비활성화
# disk-barrier no; # 디스크 배리어 비활성화
# Resync 속도
resync-rate 500M; # 고정 속도 (기본 250K)
c-plan-ahead 20; # 가변 속도 활성화
c-fill-target 100;
c-min-rate 50M;
c-max-rate 1000M;
# 읽기 밸런싱 (DRBD 8.4+)
read-balancing least-pending; # 부하 적은 노드에서 읽기
}
}
# 시스템 레벨 튜닝
$ echo deadline > /sys/block/sdb/queue/scheduler # I/O 스케줄러
$ echo 256 > /sys/block/sdb/queue/nr_requests # 큐 깊이
$ sysctl -w net.core.rmem_max=16777216 # TCP 수신 버퍼 최대
$ sysctl -w net.core.wmem_max=16777216 # TCP 송신 버퍼 최대
$ sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
$ sysctl -w net.ipv4.tcp_wmem="4096 87380 16777216"
성능 벤치마크
| 환경 | Protocol | 순차 쓰기 (MB/s) | 랜덤 쓰기 (IOPS) | 레이턴시 (us) |
|---|---|---|---|---|
| 로컬 SSD (비교 기준) | N/A | 520 | 75,000 | 20 |
| DRBD + 10GbE | C | 450 | 42,000 | 80 |
| DRBD + 10GbE | A | 510 | 70,000 | 25 |
| DRBD + 25GbE | C | 490 | 58,000 | 55 |
| DRBD + RDMA | C | 500 | 65,000 | 35 |
transport rdma를 net 블록에 추가합니다.
커널 소스 구조
| 경로 | 내용 | 핵심 함수 |
|---|---|---|
drivers/block/drbd/drbd_main.c | 블록 디바이스 등록, make_request | drbd_make_request(), drbd_init() |
drivers/block/drbd/drbd_receiver.c | 피어 데이터 수신 처리 | receive_Data(), drbd_receiver() |
drivers/block/drbd/drbd_sender.c | 데이터 전송, Resync 스케줄링 | drbd_send_dblock() |
drivers/block/drbd/drbd_bitmap.c | 변경 비트맵 관리 | drbd_bm_set_bits(), drbd_bm_clear_bit() |
drivers/block/drbd/drbd_worker.c | 워커 스레드, 재동기화 I/O | drbd_worker(), w_e_end_data_req() |
drivers/block/drbd/drbd_nl.c | Netlink 사용자 공간 인터페이스 | drbd_adm_new_resource() |
drivers/block/drbd/drbd_actlog.c | Activity Log 구현 | drbd_al_begin_io(), drbd_al_complete_io() |
drivers/block/drbd/drbd_state.c | 역할/디스크 상태 머신 | drbd_change_state() |
drivers/block/drbd/drbd_req.c | 요청 처리, 에포크 관리 | drbd_request_prepare() |
drivers/block/drbd/drbd_int.h | 내부 헤더 (구조체 정의) | 모든 핵심 구조체 선언 |
# 커널 소스 탐색
$ find drivers/block/drbd/ -name "*.c" | wc -l
12
# DRBD 모듈 정보
$ modinfo drbd
filename: /lib/modules/.../kernel/drivers/block/drbd/drbd.ko
version: 8.4.11
description: drbd - Distributed Replicated Block Device v8.4.11
author: Philipp Reisner <phil@linbit.com>, Lars Ellenberg <lars@linbit.com>
license: GPL
# 소스 코드 통계
$ wc -l drivers/block/drbd/*.c drivers/block/drbd/*.h
3215 drbd_main.c
5834 drbd_receiver.c
2138 drbd_sender.c
1685 drbd_bitmap.c
1894 drbd_worker.c
2345 drbd_nl.c
687 drbd_actlog.c
1456 drbd_state.c
2879 drbd_int.h
# 총 약 25,000줄
디버깅 및 모니터링
# DRBD 전체 상태 확인
$ drbdadm status
$ cat /proc/drbd
# 이벤트 로그 실시간 모니터링
$ drbdadm events2 --statistics r0
# 비트맵 동기화 진행 상황
$ drbdsetup show r0/0 # 볼륨 상세 정보
$ drbdsetup status r0 --verbose --statistics
# 커널 로그에서 DRBD 메시지 확인
$ dmesg | grep drbd
$ journalctl -k -u drbd
# 성능 통계
$ cat /sys/kernel/debug/drbd/r0/0/proc_drbd
# Resync 강제 시작 (전체 동기화)
$ drbdadm invalidate-remote r0
# 모니터링 스크립트 (Prometheus Exporter)
# drbd_exporter: DRBD 메트릭을 Prometheus로 수집
$ drbd_exporter --listen 0.0.0.0:9370
# 주요 모니터링 지표
# - drbd_connection_state (Connected/StandAlone)
# - drbd_disk_state (UpToDate/Inconsistent)
# - drbd_out_of_sync_bytes (남은 동기화 바이트)
# - drbd_send_rate / drbd_recv_rate (전송/수신 속도)
# - drbd_al_writes (Activity Log 쓰기 수)
트러블슈팅 체크리스트
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| StandAlone 상태 | Split-Brain 또는 수동 disconnect | GI 비교 후 --discard-my-data connect |
| Resync 느림 | resync-rate 낮음 또는 I/O 경합 | resync-rate 증가, c-plan-ahead 활성화 |
| Inconsistent 상태 지속 | Resync 미완료 또는 실패 | drbdadm invalidate r0로 강제 재동기화 |
| WFConnection 상태 | TCP 연결 실패 | 방화벽(7789포트), 네트워크 연결 확인 |
| 높은 al-writes | 랜덤 I/O 패턴, al-extents 부족 | al-extents 증가 (워킹셋 크기/4MB) |
| Protocol C 레이턴시 높음 | 네트워크 RTT 높음 | 전용 네트워크 사용, RDMA 고려 |
| SplitBrain detected | 양쪽 모두 Primary로 동작 | 승자 결정 후 패자에서 --discard-my-data |
| Diskless 상태 | 로컬 디스크 오류 | 디스크 교체 후 drbdadm attach r0 |
# bpftrace로 DRBD bio 처리 레이턴시 측정
$ bpftrace -e '
kprobe:drbd_make_request { @start[tid] = nsecs; }
kretprobe:drbd_make_request /@start[tid]/ {
@us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
RDMA 전송 계층
DRBD 9는 TCP/IP 외에 RDMA(Remote Direct Memory Access) 전송을 지원합니다. InfiniBand, RoCE(RDMA over Converged Ethernet), iWARP를 통해 네트워크 복제 레이턴시를 극적으로 줄일 수 있습니다.
RDMA 아키텍처
| 전송 | 레이턴시 | 대역폭 | CPU 사용 | 요구사항 |
|---|---|---|---|---|
| TCP/IP (1GbE) | ~100µs | ~120MB/s | 높음 | 표준 NIC |
| TCP/IP (10GbE) | ~50µs | ~1.2GB/s | 높음 | 10G NIC |
| TCP/IP (25GbE) | ~30µs | ~3GB/s | 매우 높음 | 25G NIC |
| RoCEv2 (25GbE) | ~5µs | ~3GB/s | 매우 낮음 | RoCE NIC |
| InfiniBand (HDR) | ~1µs | ~25GB/s | 최소 | IB HCA + Switch |
# /etc/drbd.d/r0.res — RDMA 전송 설정
resource r0 {
net {
# RDMA 전송 사용 (SDP 또는 ssocks)
transport rdma;
# RDMA 관련 파라미터
sndbuf-size 1048576; # 1MB 송신 버퍼
rcvbuf-size 1048576; # 1MB 수신 버퍼
max-buffers 8192; # 최대 전송 버퍼 수
max-epoch-size 8192; # 에포크당 최대 쓰기
}
on node1 {
device /dev/drbd0;
disk /dev/sdb1;
address 192.168.10.1:7789; # RDMA 주소
}
on node2 {
device /dev/drbd0;
disk /dev/sdb1;
address 192.168.10.2:7789;
}
}
DRBD Reactor 자동화
DRBD Reactor는 DRBD 이벤트에 반응하여 자동으로 서비스를 시작/중지하는 데몬입니다. Pacemaker/Corosync보다 가벼운 HA 솔루션으로, 2노드 구성에 적합합니다.
# /etc/drbd-reactor.d/r0.toml
[[promoter]]
id = "r0-promoter"
[promoter.resources.r0]
# Primary 승격 시 실행할 서비스
start = [
"systemctl start postgresql",
"systemctl start keepalived",
]
# Secondary 강등 시 실행할 서비스
stop = [
"systemctl stop keepalived",
"systemctl stop postgresql",
]
# Quorum 손실 시 동작
on-quorum-loss = "freeze-io"
# 자동 승격 조건
[promoter.resources.r0.auto-promote]
# 디스크 상태가 UpToDate인 노드가 자동 Primary
diskless = false
| 기능 | Pacemaker/Corosync | DRBD Reactor |
|---|---|---|
| 아키텍처 | 범용 클러스터 매니저 | DRBD 이벤트 기반 데몬 |
| 설정 복잡도 | 높음 (CIB XML) | 낮음 (TOML) |
| 지원 노드 수 | 다중 노드 (16+) | 2~4 노드 |
| 장애 감지 시간 | ~5초 (기본값) | <1초 |
| 의존성 | Pacemaker + Corosync + resource-agents | drbd-reactor 단일 패키지 |
| 리소스 관리 | VIP, 파일시스템, 서비스 등 | systemd 서비스 연동 |
DRBD 9 메시 복제
DRBD 9는 2노드 제한을 넘어 최대 32개 노드 간 메시 복제를 지원합니다. 모든 노드가 서로 직접 연결되는 full-mesh 토폴로지를 사용합니다.
# /etc/drbd.d/r0.res — 4노드 메시 설정
resource r0 {
net {
protocol C;
# 메시 연결에서 각 노드 쌍은 독립적인 TCP 연결
}
# connection-mesh: 모든 노드 쌍을 자동 연결
connection-mesh {
hosts node1 node2 node3 node4;
}
on node1 {
node-id 0;
address 10.0.0.1:7789;
volume 0 {
device /dev/drbd0;
disk /dev/sdb1;
meta-disk internal;
}
}
on node2 {
node-id 1;
address 10.0.0.2:7789;
volume 0 {
device /dev/drbd0;
disk /dev/sdb1;
meta-disk internal;
}
}
on node3 {
node-id 2;
address 10.0.0.3:7789;
volume 0 {
device /dev/drbd0;
disk /dev/sdb1;
meta-disk internal;
}
}
on node4 {
node-id 3;
address 10.0.0.4:7789;
volume 0 {
device /dev/drbd0;
disk /dev/sdb1;
meta-disk internal;
}
}
}
- Primary 노드가 쓰기를 받으면 모든 Secondary에 동시에 복제합니다
- Secondary 간에는 비트맵 정보만 교환하여 재동기화 최적화에 활용합니다
- Quorum은 과반수(N/2+1) 노드가 UpToDate여야 쓰기를 허용합니다
- 장애 노드 복구 시 비트맵 기반 증분 재동기화로 빠르게 복구합니다
| 노드 수 | TCP 연결 수 | Quorum 최소 | 장애 허용 | 용도 |
|---|---|---|---|---|
| 2 | 1 | 없음* | 0 (fencing 필요) | 기본 HA |
| 3 | 3 | 2 | 1 | Quorum 지원 HA |
| 4 | 6 | 3 | 1 | 고가용성 |
| 5 | 10 | 3 | 2 | 지리적 분산 |
quorum majority가 의미가 없습니다. 대신 quorum off + STONITH(fencing)를 사용하거나, Diskless 3번째 노드를 추가하여 Quorum을 확보합니다.
I/O 흐름 상세
DRBD의 쓰기 I/O는 커널 블록 계층을 통해 처리됩니다. drbd_make_request()가 bio를 가로채어 로컬 디스크와 원격 노드에 동시에 전달합니다.
/* drivers/block/drbd/drbd_req.c — 요청 처리 핵심 (간략화) */
static blk_qc_t drbd_make_request(struct request_queue *q,
struct bio *bio)
{
struct drbd_device *device = q->queuedata;
struct drbd_request *req;
/* 1. DRBD 요청 구조체 할당 */
req = drbd_req_new(device, bio);
/* 2. Activity Log 업데이트 (쓰기만) */
if (bio_data_dir(bio) == WRITE) {
drbd_al_begin_io(device, &req->i);
/* AL 엔트리 확보: I/O 스로틀 가능 */
}
/* 3. 로컬 디스크 제출 */
if (device->ldev) {
req->private_bio = bio_alloc_clone(bio, GFP_NOIO);
submit_bio_noacct(req->private_bio);
}
/* 4. 원격 복제 제출 */
if (device->state.conn >= C_CONNECTED) {
drbd_send_dblock(device->peer, req);
/* Protocol C: WriteAck 대기 상태 진입 */
}
/* 5. 완료 추적: 양쪽 모두 완료될 때까지 대기 */
drbd_req_complete(req, &m);
return BLK_QC_T_NONE;
}
- Protocol A (비동기): 로컬 디스크 완료 + 네트워크 송신 버퍼 전달 = bio 완료
- Protocol B (반동기): 로컬 디스크 완료 + 원격 수신 확인(RecvAck) = bio 완료
- Protocol C (동기): 로컬 디스크 완료 + 원격 디스크 완료(WriteAck) = bio 완료
재동기화 심화
재동기화 유형
| 유형 | 트리거 | 동작 | 소요 시간 |
|---|---|---|---|
| 전체 재동기화 (Full Sync) | 수동 invalidate, 첫 연결 | 전체 디스크 복사 | 디스크 크기 / 네트워크 속도 |
| 비트맵 재동기화 (Bitmap Resync) | 일시적 연결 끊김 후 재연결 | 변경된 영역만 복사 | 변경량 / 네트워크 속도 |
| 온라인 검증 (Online Verify) | cron / 수동 | SHA-256 비교, 불일치만 복사 | 디스크 크기 / 읽기 속도 |
| c-plan-ahead 적응형 | 자동 | 네트워크 대역폭에 맞춰 조절 | 동적 |
적응형 재동기화 (c-plan-ahead)
# 적응형 재동기화 설정
disk {
# 고정 속도 재동기화 (기본)
resync-rate 100M; # 100MB/s 상한
# 적응형 재동기화 (권장)
c-plan-ahead 20; # 20초 미리보기 (0=비활성)
c-delay-target 10; # 목표 전송 지연 (10 × 0.1초 = 1초)
c-fill-target 0; # 목표 버퍼 채움량 (0=자동)
c-max-rate 250M; # 최대 전송 속도
c-min-rate 10M; # 최소 전송 속도
}
데이터 무결성
DRBD는 네트워크 전송 중 데이터 손상을 탐지하기 위한 체크섬 기능을 제공합니다.
# 네트워크 데이터 무결성 설정
net {
# 전송 데이터 체크섬 (선택적)
data-integrity-alg sha256; # crc32c, sha1, sha256
# TCP keepalive
ping-int 10; # 10초 간격 ping
ping-timeout 5; # 5 × 0.1초 = 0.5초 타임아웃
timeout 60; # 6초 연결 타임아웃
}
disk {
# 재동기화/검증 체크섬
resync-after 0; # 자동 재동기화 순서
verify-alg sha256; # 온라인 검증 알고리즘
csums-alg sha256; # 재동기화 체크섬 (변경 탐지)
}
| 알고리즘 | CPU 비용 | 정밀도 | 용도 |
|---|---|---|---|
crc32c | 매우 낮음 (HW 가속) | 32비트 | 빠른 무결성 검사 |
sha1 | 중간 | 160비트 | 일반 검증 |
sha256 | 높음 | 256비트 | 고신뢰 검증 |
crc32c(CPU 가속 활용)를 사용하거나, 비활성화하고 TCP 체크섬에 의존하는 것을 고려하세요.
DRBD + Kubernetes
LINSTOR CSI 플러그인을 통해 Kubernetes에서 DRBD 볼륨을 PersistentVolume으로 사용할 수 있습니다.
# StorageClass 정의 (LINSTOR CSI)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: linstor-drbd
provisioner: linstor.csi.linbit.com
parameters:
# DRBD 복제 수
autoPlace: "2"
# 스토리지 풀
storagePool: "lvm-thin"
# DRBD 리소스 그룹
resourceGroup: "drbd-default"
# 파일시스템
csi.storage.k8s.io/fstype: "xfs"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
# PVC 요청
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes: [ReadWriteOnce]
storageClassName: linstor-drbd
resources:
requests:
storage: 100Gi
- 볼륨 자동 프로비저닝/삭제 (Dynamic PV)
- Pod 마이그레이션 시 자동 DRBD 연결 전환
- 볼륨 스냅샷/복원 (VolumeSnapshot API)
- 볼륨 확장 (Online resize)
- 토폴로지 인식 스케줄링 (Pod가 DRBD 복제본이 있는 노드에 배치)
고급 튜닝 파라미터
| 파라미터 | 기본값 | 권장 (SSD) | 설명 |
|---|---|---|---|
al-extents | 1237 | 6433 | Activity Log 크기 (4MB 단위, 워킹셋 커버) |
max-buffers | 2048 | 8192 | 최대 미완료 네트워크 버퍼 |
max-epoch-size | 2048 | 8192 | 에포크당 최대 쓰기 요청 |
sndbuf-size | 0 (자동) | 1048576 | TCP 송신 버퍼 (1MB) |
rcvbuf-size | 0 (자동) | 1048576 | TCP 수신 버퍼 (1MB) |
unplug-watermark | 16 | 128 | 블록 계층 언플러그 트리거 |
c-plan-ahead | 0 (비활성) | 20 | 적응형 재동기화 미리보기 (초) |
c-max-rate | - | 250M | 적응형 최대 재동기화 속도 |
disk-barrier | yes | no (배터리 RAID) | 디스크 배리어 사용 여부 |
disk-flushes | yes | no (배터리 RAID) | 디스크 플러시 사용 여부 |
# AL 크기 계산 공식
# al-extents = 워킹셋(GB) × 256
# 예: 25GB 워킹셋 → 25 × 256 = 6400 (최대 6433)
# 네트워크 튜닝 (DRBD 전용 인터페이스)
ip link set eth1 txqueuelen 10000
ethtool -G eth1 rx 4096 tx 4096
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
# I/O 스케줄러 설정 (NVMe는 none, HDD는 mq-deadline)
echo none > /sys/block/sdb/queue/scheduler
echo 256 > /sys/block/sdb/queue/nr_requests
# DRBD 디바이스 I/O 스케줄러 (항상 none)
echo none > /sys/block/drbd0/queue/scheduler
재해 복구 시나리오
장애 시나리오별 복구 절차
| 시나리오 | 증상 | 복구 절차 | 데이터 손실 |
|---|---|---|---|
| Secondary 디스크 장애 | Diskless/Inconsistent | 디스크 교체 → attach → 자동 재동기화 | 없음 |
| Primary 디스크 장애 | I/O 에러, Diskless 전환 | Secondary를 Primary 승격, 원래 Primary 디스크 교체 | 없음 (Protocol C) |
| 네트워크 단절 | StandAlone/WFConnection | 네트워크 복구 → connect → 비트맵 재동기화 | 없음 |
| Split-Brain | 양쪽 Primary | 패자 선택 → --discard-my-data connect | 패자 측 변경분 손실 |
| Primary 서버 전체 장애 | Secondary만 생존 | Secondary Primary 승격 → 서비스 시작 | Protocol A: 일부 가능 |
| 양쪽 서버 동시 장애 | 모든 노드 다운 | 먼저 복구된 노드 Primary → 나머지 재동기화 | 마지막 쓰기 일부 |
# 시나리오 1: Secondary 디스크 교체 후 복구
$ drbdadm create-md r0 # 새 디스크에 메타데이터 생성
$ drbdadm attach r0 # 디스크 연결
$ drbdadm status r0 # Inconsistent 확인
# 자동으로 비트맵 기반 재동기화 시작
# 시나리오 2: Split-Brain 복구
# 패자 노드에서:
$ drbdadm disconnect r0
$ drbdadm secondary r0
$ drbdadm connect --discard-my-data r0
# 승자 노드에서:
$ drbdadm connect r0
# 패자 → 승자 방향으로 전체 재동기화
# 시나리오 3: Primary 비상 승격 (Secondary에서)
$ drbdadm primary --force r0 # 강제 Primary 승격
$ mount /dev/drbd0 /data # 파일시스템 마운트
$ systemctl start postgresql # 서비스 시작
--force 옵션은 데이터 일관성 검사를 건너뜁니다. 원래 Primary가 아직 살아있으면 Split-Brain이 발생합니다. 반드시 원래 Primary가 완전히 중단되었음을 확인한 후에만 사용하세요.
참고 자료
| 자료 | 유형 | 설명 |
|---|---|---|
| docs.linbit.com | 공식 문서 | LINBIT DRBD/LINSTOR 공식 문서 |
| DRBD User's Guide | 가이드 | DRBD 9 사용자 가이드 |
drivers/block/drbd/ | 소스 | DRBD 커널 모듈 소스 코드 |
| github.com/LINBIT/drbd | 소스 | DRBD 9 커널 모듈 GitHub |
| LINSTOR Server | 소스 | LINSTOR 볼륨 관리 서버 |
| LWN: DRBD overview | 기사 | DRBD 아키텍처 개요 |
Wire Protocol 심화 — 소켓 구조와 핸드셰이크
DRBD 연결은 두 개의 TCP 소켓으로 구성됩니다. 데이터 소켓(data socket)은 실제 블록 데이터(P_DATA, P_RS_DATA_REPLY 등)를 전송하고, 메타 소켓(meta socket)은 제어 메시지(P_PING, P_STATE, ACK 패킷 등)를 처리합니다. 이 분리 설계는 대용량 데이터 전송이 제어 메시지를 지연시키지 않도록 보장합니다.
이중 소켓 설계 상세
| 소켓 | 전송 내용 | 스레드 | 설계 이유 |
|---|---|---|---|
| data socket | P_DATA, P_RS_DATA_REPLY, P_BARRIER, bulk 데이터 | sender / receiver | 대용량 블록 데이터 전용, TCP_CORK 활용 |
| meta socket | P_WRITE_ACK, P_RECV_ACK, P_PING, P_PING_ACK, P_STATE | asender / worker | 저지연 제어 메시지, 데이터 전송과 독립적 처리 |
/* drbd_receiver.c — 연결 핸드셰이크 구현 (간략화) */
static int conn_connect(struct drbd_connection *connection)
{
struct socket *sock, *msock;
/* 1. 데이터 소켓과 메타 소켓 동시에 연결 시도 */
sock = drbd_try_connect(connection, connection->my_addr,
connection->peer_addr);
msock = drbd_try_connect(connection, connection->my_addr,
connection->peer_addr);
/* 2. TCP_NODELAY 설정 (제어 메시지 즉시 전송) */
tcp_sock_set_nodelay(msock->sk);
/* 3. data socket에 TCP_CORK (Nagle 유사 배치 전송) */
tcp_sock_set_cork(sock->sk, 1);
connection->data_socket = sock;
connection->meta_socket = msock;
/* 4. 프로토콜 버전 교환 */
drbd_send_features(connection);
/* 5. 선택적 인증 (shared-secret) */
if (connection->net_conf->shared_secret[0])
drbd_do_auth(connection);
/* 6. 상태/UUID 교환 → 동기화 방향 결정 */
drbd_send_uuids(peer_device);
drbd_send_state(peer_device);
return 0;
}
패킷 수명 주기
/* P_DATA 패킷의 전체 수명 주기 (Protocol C) */
/* Primary (sender 스레드) */
drbd_send_dblock(peer_device, req) {
/* p_data 헤더 구성 */
p.sector = cpu_to_be64(req->i.sector);
p.block_id = (u64)req; /* 요청 추적용 식별자 */
p.seq_num = atomic_inc_return(&connection->packet_seq);
p.dp_flags = bio_flags_to_wire(req);
/* data socket으로 헤더 + 데이터 전송 */
drbd_send_command(peer_device, sock, P_DATA,
&p, sizeof(p), req->data, req->size);
}
/* Secondary (receiver 스레드) */
receive_Data(connection, pi) {
/* 1. 페이로드 수신 */
peer_req = read_in_block(peer_device, pi);
/* 2. Activity Log 업데이트 */
drbd_al_begin_io(device, &peer_req->i);
/* 3. 로컬 디스크에 기록 */
drbd_submit_peer_request(device, peer_req);
/* 4. 디스크 쓰기 완료 대기 */
wait_for_completion(&peer_req->done);
/* 5. P_WRITE_ACK 전송 (meta socket) */
drbd_send_ack(peer_device, P_WRITE_ACK, peer_req);
}
Activity Log 내부 구현 심화
Activity Log는 LRU(Least Recently Used) 캐시 위에 구축된 트랜잭션 로그입니다. AL extent가 교체될 때마다 메타데이터 디바이스에 트랜잭션을 기록하며, 이 로그는 순환 버퍼 형태로 관리됩니다.
/* drbd_actlog.c — AL 트랜잭션 구조 */
struct al_transaction_on_disk {
__be32 magic; /* DRBD_AL_MAGIC = 0xDRBD_AL */
__be32 tr_number; /* 순환 트랜잭션 번호 */
__be32 crc32c; /* 트랜잭션 CRC */
__be16 n_updates; /* 이 트랜잭션의 슬롯 갱신 수 */
__be16 context_size; /* 전체 context 크기 */
__be16 context_start_slot_nr;
/* 뒤이어 update[] 배열: (slot_nr, extent_nr) 쌍 */
/* 뒤이어 context[] 배열: 전체 AL 슬롯 스냅샷 (복구용) */
} __packed;
/* AL 트랜잭션 기록 과정 */
static int drbd_al_begin_io_commit(struct drbd_device *device)
{
struct lru_cache *al = device->act_log;
/* 1. 변경된 LRU 슬롯을 트랜잭션 버퍼에 기록 */
al_write_transaction(device);
/* 2. 메타 디바이스에 동기 쓰기 (REQ_FUA) */
/* 원자성 보장: 전체 트랜잭션이 쓰이거나 안 쓰이거나 */
drbd_md_sync_page_io(device, md_sector, REQ_OP_WRITE | REQ_FUA);
/* 3. 트랜잭션 번호 증가 */
device->al_tr_number++;
/* 크래시 복구: 메타 디바이스에서 가장 최근 유효 트랜잭션을 찾아 */
/* AL 상태를 재구성 → 해당 extent 범위만 Resync */
return 0;
}
al-extents를 워킹셋 크기에 맞게 늘리면 교체 빈도가 줄어 메타데이터 I/O 오버헤드가 감소합니다.
| al-extents 값 | 커버 범위 | AL 메타 크기 | 권장 환경 |
|---|---|---|---|
| 1237 (기본) | ~4.8 GB | ~40 KB | 소규모 워크로드, HDD |
| 3833 | ~15 GB | ~120 KB | 중규모 DB, SSD |
| 6433 | ~25 GB | ~200 KB | 대규모 DB, NVMe |
| 16337 | ~64 GB | ~512 KB | 대용량 랜덤 워크로드 |
| 65534 (최대) | ~256 GB | ~2 MB | 전체 디스크 범위 커버 |
Bitmap 내부 구조와 OOS 추적 심화
DRBD 비트맵은 디스크 상의 각 4KB 블록(BM_BLOCK_SIZE)에 대해 1비트를 할당하여 피어와의 동기화 상태를 추적합니다. 비트맵은 인메모리 캐시와 온디스크 영속 저장소에 동시에 존재합니다.
/* drbd_bitmap.c — 비트맵 핵심 함수 */
/* 쓰기 시: 변경된 섹터 범위를 비트맵에 표시 */
int drbd_bm_set_bits(struct drbd_device *device,
unsigned long s, unsigned long e)
{
/* s, e: 시작/끝 비트맵 비트 번호 */
/* 4KB 단위: sector >> (BM_BLOCK_SHIFT - 9) */
unsigned long *p_addr;
int changed = 0;
for (bitnr = s; bitnr <= e; bitnr++) {
p_addr = __bm_map_pidx(device->bitmap, bm_bit_to_page_idx(bitnr));
if (!test_and_set_bit(bitnr % BITS_PER_PAGE, p_addr)) {
changed++;
bm_set_page_need_writeout(device->bitmap, bm_bit_to_page_idx(bitnr));
}
}
return changed;
}
/* Resync 완료 시: 동기화된 블록의 비트 해제 */
int drbd_bm_clear_bit(struct drbd_device *device, unsigned long bitnr)
{
unsigned long *p_addr = __bm_map_pidx(device->bitmap,
bm_bit_to_page_idx(bitnr));
clear_bit(bitnr % BITS_PER_PAGE, p_addr);
bm_set_page_need_writeout(device->bitmap, bm_bit_to_page_idx(bitnr));
return 0;
}
/* 디스크에 비트맵 기록 (연결 해제/모듈 언로드 시) */
int drbd_bm_write(struct drbd_device *device)
{
struct drbd_bitmap *b = device->bitmap;
int i;
for (i = 0; i < b->bm_number_of_pages; i++) {
if (bm_test_page_need_writeout(b, i)) {
bm_page_io(device, i, REQ_OP_WRITE);
bm_clear_page_need_writeout(b, i);
}
}
return 0;
}
Online Verify 알고리즘 상세
Online Verify는 서비스 중단 없이 전체 디스크를 블록 단위로 해시 비교하여 Silent Data Corruption(비트 로트)을 감지합니다. VerifySource가 주도적으로 검증을 진행하며, 양쪽 데이터가 불일치하면 해당 블록을 out-of-sync 비트맵에 기록합니다.
/* drbd_worker.c — Online Verify 핵심 흐름 (간략화) */
/* VerifySource (Primary): 검증 주도 */
static int w_ov_next(struct drbd_work *w, int cancel)
{
struct drbd_peer_device *peer_device = w->peer_device;
sector_t sector = peer_device->ov_position;
unsigned int size = BM_BLOCK_SIZE; /* 4KB 또는 설정값 */
/* 1. 로컬 블록 읽기 */
struct page *page = drbd_alloc_page();
drbd_backing_dev_read(device, sector, page, size);
/* 2. 해시 계산 (verify-alg: sha256 등) */
crypto_shash_digest(device->verify_tfm, page_data, size, digest);
/* 3. P_OV_REQUEST + 로컬 해시를 피어에 전송 */
drbd_send_ov_request(peer_device, sector, size);
/* ov_position을 다음 블록으로 이동 */
peer_device->ov_position += (size >> 9);
return 0;
}
/* VerifyTarget (Secondary): 해시 비교 응답 */
static int receive_OVRequest(struct drbd_connection *conn,
struct packet_info *pi)
{
/* 1. 동일 섹터의 로컬 블록 읽기 */
drbd_backing_dev_read(device, sector, page, size);
/* 2. 로컬 해시 계산 */
crypto_shash_digest(device->verify_tfm, page_data, size, local_digest);
/* 3. P_OV_REPLY로 로컬 해시 전송 */
drbd_send_ov_reply(peer_device, sector, size, local_digest);
return 0;
}
/* VerifySource: 수신한 해시와 로컬 해시 비교 */
static int receive_OVReply(struct drbd_connection *conn,
struct packet_info *pi)
{
if (memcmp(local_digest, peer_digest, digest_size) != 0) {
/* 불일치: out-of-sync 비트맵에 표시 */
drbd_bm_set_bits(device, sector_to_bm_bit(sector),
sector_to_bm_bit(sector + (size >> 9) - 1));
peer_device->ov_last_oos_size += size;
drbd_err(device, "Online verify: sector %llu mismatch\n", sector);
}
peer_device->ov_left--;
if (peer_device->ov_left == 0) {
/* 검증 완료 */
drbd_info(device, "Online verify done. oos=%lu\n",
peer_device->ov_last_oos_size);
}
return 0;
}
| 매개변수 | 기본값 | 설명 |
|---|---|---|
verify-alg | (미설정) | 해시 알고리즘 (sha256, sha1, crc32c). 미설정 시 Online Verify 비활성 |
ov-timeout | 0 (무제한) | 검증 타임아웃 (초). 0이면 전체 디스크 검증까지 진행 |
| 검증 단위 | BM_BLOCK_SIZE (4KB) | 한 번에 비교하는 블록 크기 |
| 네트워크 부하 | 해시 크기만 전송 | SHA-256: 블록당 32바이트 전송 (데이터 전송 없음) |
| 디스크 부하 | 순차 읽기 | 양쪽 노드 모두 전체 디스크 순차 읽기 |
drbdadm disconnect r0 && drbdadm connect r0를 실행합니다.
Split-Brain 정책과 GI(Generation Identifier) 심화
Split-Brain 탐지의 핵심은 Generation Identifier(GI) 비교입니다. GI는 4개의 UUID로 구성되며, 재연결 시 양쪽 GI를 교환하여 "누가 더 최신인지", "양쪽이 독립적으로 변경되었는지"를 결정합니다.
Split-Brain 정책 상세
| 정책 | 상황 | 동작 | 위험도 | 자동화 적합 |
|---|---|---|---|---|
disconnect | 모든 상황 | 연결 해제, 수동 개입 필요 | 없음 (안전) | 낮음 (수동 복구) |
discard-younger-primary | 0pri | 더 최근에 Primary가 된 쪽 데이터 폐기 | 중간 | 높음 |
discard-older-primary | 0pri | 더 오래전에 Primary였던 쪽 데이터 폐기 | 중간 | 높음 |
discard-zero-changes | 0pri | 변경이 없는 쪽 데이터 폐기 (변경 없으면 안전) | 낮음 | 높음 |
discard-least-changes | 0pri | 변경량이 적은 쪽 데이터 폐기 | 높음 | 중간 |
discard-secondary | 1pri | 현재 Secondary 데이터 폐기 | 중간 | 높음 |
call-pri-lost-after-sb | 2pri | pri-lost 핸들러 호출 (fencing 등) | 높음 | 높음 |
# 최대 안전 Split-Brain 설정 (프로덕션 권장)
resource r0 {
net {
# 0pri: 양쪽 모두 Secondary인 경우
after-sb-0pri discard-zero-changes; # 변경 없는 쪽 안전하게 폐기
# 1pri: 한쪽만 Primary인 경우
after-sb-1pri discard-secondary; # Secondary 데이터 폐기
# 2pri: 양쪽 모두 Primary인 경우 (가장 위험)
after-sb-2pri disconnect; # 연결 해제, 수동 처리 필수
}
handlers {
# Split-Brain 알림
split-brain "/usr/lib/drbd/notify-split-brain.sh admin@example.com";
# pri-lost 핸들러 (call-pri-lost-after-sb 사용 시)
pri-lost-after-sb "echo 1 > /proc/sysrq-trigger; reboot -f";
# 경고: 위 핸들러는 노드를 강제 재부팅합니다
}
}
# GI 수동 확인
$ drbdadm get-gi r0
UUID: current=A1B2C3D4:bitmap=E5F6G7H8:history1=...
# 양쪽 UUID를 비교하여 Split-Brain 유형 판단 가능
DRBD Proxy WAN 복제 심화
DRBD Proxy는 DRBD 노드 사이에 위치하여 버퍼링, 압축, 대역폭 제어를 제공합니다. Protocol A와 결합하여 WAN 환경에서도 쓰기 레이턴시를 최소화하면서 안정적인 복제를 보장합니다.
프록시 버퍼링 메커니즘
| 파라미터 | 기본값 | 설명 | 튜닝 가이드 |
|---|---|---|---|
memlimit | 0 (무제한) | 프록시 버퍼 메모리 상한 | 쓰기 버스트 × 2~3배 (예: 4GB) |
sndbuf-size | 0 (자동) | 프록시→WAN TCP 송신 버퍼 | BDP(대역폭 × RTT) 이상 |
rcvbuf-size | 0 (자동) | 프록시←WAN TCP 수신 버퍼 | BDP 이상 |
compression | 없음 | 압축 알고리즘 및 레벨 | zstd 3~6 (압축률/CPU 균형) |
bwlimit | 0 (무제한) | 대역폭 제한 (KB/s) | WAN 링크의 70~80% |
# 고급 DRBD Proxy 설정 (대륙 간 복제)
resource dr-wan {
protocol A;
proxy {
memlimit 4G; # 4GB 버퍼 (쓰기 버스트 흡수)
plugin {
zstd 6; # zstd 압축 레벨 6 (높은 압축률)
}
bwlimit 100000; # 100MB/s 대역폭 제한
}
net {
# BDP 계산: 100Mbps × 100ms RTT = 1.25MB
# TCP 버퍼는 BDP의 2배 이상 권장
sndbuf-size 4194304; # 4MB 송신 버퍼
rcvbuf-size 4194304; # 4MB 수신 버퍼
max-buffers 36000;
max-epoch-size 36000;
ko-count 7; # 7회 타임아웃 후 연결 해제
ping-int 10;
ping-timeout 50; # 5초 ping 타임아웃 (WAN 고려)
}
on dc-primary {
address 10.0.0.1:7789;
proxy on dc-primary {
inside 127.0.0.1:7789;
outside 203.0.113.1:7800;
}
}
on dc-dr {
address 10.1.0.1:7789;
proxy on dc-dr {
inside 127.0.0.1:7789;
outside 198.51.100.1:7800;
}
}
}
# Proxy 실시간 모니터링
$ drbd-proxy-ctl -c "show drbd-proxy"
buffer usage: 847MB / 4096MB (20.7%)
compression ratio: 4.1:1
data rate (in): 125 MB/s (애플리케이션 쓰기 속도)
data rate (out): 31 MB/s (압축 후 WAN 전송)
pending bytes: 523MB (아직 미전송)
# BDP(Bandwidth-Delay Product) 계산 도구
# BDP = Bandwidth(bytes/s) × RTT(seconds)
# 예: 1Gbps × 50ms = 125MB/s × 0.05s = 6.25MB
# TCP 버퍼 최소: BDP × 2 = 12.5MB
에포크(Epoch)와 배리어(Barrier) 처리
DRBD는 에포크(epoch) 개념으로 쓰기 순서를 보장합니다. P_BARRIER 패킷이 에포크 경계를 표시하며, Secondary는 에포크 내의 모든 쓰기가 완료된 후에야 P_BARRIER_ACK를 전송합니다.
/* drbd_receiver.c — 에포크/배리어 처리 */
/* 에포크 구조체: 하나의 배리어 구간 */
struct drbd_epoch {
struct list_head list; /* 에포크 체인 */
unsigned int barrier_nr; /* 배리어 번호 */
atomic_t epoch_size; /* 이 에포크의 쓰기 수 */
atomic_t active; /* 활성 I/O 수 */
unsigned long flags; /* BARRIER_ACKED 등 */
};
/* P_BARRIER 수신 시 */
static int receive_Barrier(struct drbd_connection *conn,
struct packet_info *pi)
{
struct drbd_epoch *epoch = conn->current_epoch;
/* 현재 에포크의 모든 쓰기가 완료될 때까지 대기 */
wait_event(conn->ee_wait,
atomic_read(&epoch->active) == 0);
/* 디스크 플러시 (쓰기 순서 보장) */
if (test_bit(DE_CONTAINS_A_BARRIER, &epoch->flags))
drbd_flush(device);
/* P_BARRIER_ACK 전송 */
drbd_send_barrier_ack(conn, epoch->barrier_nr,
atomic_read(&epoch->epoch_size));
/* 새 에포크 시작 */
conn->current_epoch = drbd_new_epoch(conn);
return 0;
}
/* max-epoch-size 초과 시 자동 배리어 삽입 */
/* 설정: max-epoch-size 8192 → 8192 쓰기마다 자동 배리어 */
/* 목적: 에포크가 너무 커지면 Secondary가 OOM 위험 */
max-epoch-size가 크면 배리어 빈도가 줄어 처리량이 향상되지만, Secondary의 메모리 사용량이 증가합니다. max-buffers와 함께 조절하여 Secondary OOM을 방지합니다. NVMe SSD 환경에서는 8192~16384가 적절합니다.
성능 벤치마크 방법론
DRBD 성능을 정확히 측정하려면 fio(Flexible I/O Tester)를 사용하여 체계적으로 벤치마크해야 합니다. 기준선(로컬 디스크)과 DRBD 디바이스를 동일 조건으로 비교합니다.
# ========= DRBD 성능 벤치마크 스크립트 =========
# 1. 기준선: 로컬 디스크 성능 (DRBD 없이)
# 순차 쓰기 처리량
$ fio --name=seq-write --filename=/dev/sdb1 \
--ioengine=libaio --direct=1 --bs=1M \
--rw=write --numjobs=1 --iodepth=32 \
--size=10G --runtime=60 --time_based
# 랜덤 4K 쓰기 IOPS
$ fio --name=rand-write --filename=/dev/sdb1 \
--ioengine=libaio --direct=1 --bs=4k \
--rw=randwrite --numjobs=4 --iodepth=32 \
--size=10G --runtime=60 --time_based
# 쓰기 레이턴시 (clat: 완료 레이턴시)
$ fio --name=lat-write --filename=/dev/sdb1 \
--ioengine=libaio --direct=1 --bs=4k \
--rw=randwrite --numjobs=1 --iodepth=1 \
--size=1G --runtime=60 --time_based
# 2. DRBD 디바이스 동일 테스트
$ fio --name=drbd-seq-write --filename=/dev/drbd0 \
--ioengine=libaio --direct=1 --bs=1M \
--rw=write --numjobs=1 --iodepth=32 \
--size=10G --runtime=60 --time_based
$ fio --name=drbd-rand-write --filename=/dev/drbd0 \
--ioengine=libaio --direct=1 --bs=4k \
--rw=randwrite --numjobs=4 --iodepth=32 \
--size=10G --runtime=60 --time_based
$ fio --name=drbd-lat-write --filename=/dev/drbd0 \
--ioengine=libaio --direct=1 --bs=4k \
--rw=randwrite --numjobs=1 --iodepth=1 \
--size=1G --runtime=60 --time_based
# 3. Protocol A vs C 비교
# Protocol A 설정 후 동일 테스트 반복
$ drbdadm net-options --protocol=A r0
# ... fio 테스트 실행 ...
$ drbdadm net-options --protocol=C r0
# ... fio 테스트 실행 ...
# 4. 결과 비교 핵심 지표
# - BW (MB/s): 순차 쓰기 대역폭
# - IOPS: 랜덤 쓰기 초당 I/O
# - clat p99 (us): 99th 백분위 완료 레이턴시
# - DRBD 오버헤드 = (로컬 - DRBD) / 로컬 × 100%
| 테스트 | 로컬 NVMe | DRBD Proto C (10GbE) | DRBD Proto A (10GbE) | DRBD Proto C (RDMA) |
|---|---|---|---|---|
| 순차 쓰기 1M (MB/s) | 3200 | 1150 | 3050 | 2800 |
| 랜덤 쓰기 4K (IOPS) | 450K | 120K | 420K | 380K |
| 쓰기 레이턴시 p99 (us) | 15 | 180 | 22 | 45 |
| 순차 읽기 1M (MB/s) | 3500 | 3500 | 3500 | 3500 |
| 오버헤드 (순차 쓰기) | - | 64% | 5% | 13% |
--direct=1(O_DIRECT)을 사용하여 페이지 캐시를 우회해야 합니다. 페이지 캐시 사용 시 로컬과 DRBD 차이가 거의 없어 보이지만, 실제 서비스 부하에서는 캐시 미스 시 Protocol C 레이턴시가 드러납니다. 또한 워밍업(--ramp_time=10)을 적용하여 초기 불안정 구간을 제외합니다.
클라우드 환경의 DRBD
AWS, Azure, GCP 등 퍼블릭 클라우드에서 DRBD를 운영할 때의 고려사항과 클라우드 네이티브 스토리지 복제 서비스와의 비교입니다.
| 비교 항목 | DRBD on Cloud | AWS EBS Multi-Attach | Azure Shared Disk | GCE Regional PD |
|---|---|---|---|---|
| 복제 방식 | 블록 레벨, 커널 모듈 | 단일 EBS, 여러 인스턴스 연결 | 단일 디스크, 여러 VM 연결 | 2개 존 간 자동 복제 |
| 동기/비동기 | 선택 가능 (A/B/C) | N/A (단일 볼륨) | N/A (단일 볼륨) | 동기 (자동) |
| 파일시스템 | ext4/XFS (단일 Primary) | 클러스터 FS 필요 (GFS2) | 클러스터 FS 필요 | ext4/XFS (단일 마운트) |
| 가용 영역(AZ) 간 | 가능 (Protocol C + 낮은 RTT) | 동일 AZ만 | 동일 리전 내 | 2개 AZ 자동 |
| 리전 간 DR | 가능 (Protocol A + Proxy) | EBS Snapshot 복사 | ASR (비동기) | 디스크 스냅샷 복사 |
| 비용 | EC2/VM 비용 + 네트워크 | EBS 비용 × 인스턴스 수 | Shared Disk 프리미엄 | Regional PD 2배 비용 |
| 레이턴시 오버헤드 | Protocol C: +RTT | 없음 (단일 볼륨) | 없음 | 자동 관리 |
| 유연성 | 매우 높음 (커널 레벨) | 낮음 (AWS 종속) | 낮음 (Azure 종속) | 낮음 (GCP 종속) |
# AWS EC2에서 DRBD 구성 (AZ 간 Protocol C)
# 1. 인스턴스 배치: 같은 리전, 다른 AZ
# node1: us-east-1a (10.0.1.10)
# node2: us-east-1b (10.0.2.10)
# AZ 간 RTT: ~1ms (Protocol C 적합)
# 2. EBS gp3 볼륨 생성 (각 노드에 독립 볼륨)
$ aws ec2 create-volume --availability-zone us-east-1a \
--size 100 --volume-type gp3 --iops 16000 --throughput 1000
# 3. DRBD 설정 (AZ 간 전용 서브넷 사용)
resource r0 {
protocol C; # AZ 간 1ms RTT → Protocol C 적합
net {
max-buffers 8192;
max-epoch-size 8192;
sndbuf-size 0;
rcvbuf-size 0;
}
disk {
al-extents 6433;
# EBS gp3는 내부적으로 복제됨 → disk-flushes 유지
disk-flushes yes;
}
on node1 {
address 10.0.1.10:7789;
device /dev/drbd0;
disk /dev/nvme1n1; # EBS gp3
meta-disk internal;
}
on node2 {
address 10.0.2.10:7789;
device /dev/drbd0;
disk /dev/nvme1n1;
meta-disk internal;
}
}
# 4. 클라우드 환경 특수 고려사항
# - EC2 Enhanced Networking(ENA) 필수: 25Gbps+ 대역폭
# - Placement Group 사용: 클러스터 또는 Spread 배치
# - EBS 최적화 인스턴스 사용: io2 Block Express for 고성능
# - Security Group: TCP 7789 포트 허용
# - STONITH: AWS Fencing Agent (fence_aws) 사용
drbd_request 수명 주기와 완료 처리
DRBD의 모든 I/O 요청은 drbd_request 구조체로 관리됩니다. 이 구조체는 bio 수신부터 완료(bio_endio)까지의 전체 수명 주기를 추적합니다.
/* drivers/block/drbd/drbd_int.h — 요청 구조체 */
struct drbd_request {
struct drbd_device *device;
struct bio *master_bio; /* 원본 bio */
struct bio *private_bio; /* 로컬 디스크 제출용 복제 bio */
struct drbd_interval i; /* 섹터 범위 (start, size) */
unsigned long rq_state; /* 요청 상태 비트마스크 */
/* rq_state 비트 플래그: */
/* RQ_LOCAL_PENDING — 로컬 I/O 진행 중 */
/* RQ_LOCAL_COMPLETED — 로컬 I/O 완료 */
/* RQ_NET_PENDING — 네트워크 전송 대기 */
/* RQ_NET_SENT — 네트워크 전송 완료 */
/* RQ_NET_DONE — ACK 수신 완료 */
/* RQ_IN_ACT_LOG — Activity Log에 등록됨 */
unsigned int seq_num; /* 시퀀스 번호 (순서 보장) */
unsigned long start_jif; /* 요청 시작 시각 (레이턴시 측정) */
struct list_head tl_requests; /* Transfer Log 체인 */
};
/* 요청 완료 판단: 프로토콜별 완료 조건 */
static bool drbd_req_is_done(struct drbd_request *req)
{
unsigned long s = req->rq_state;
/* 로컬 I/O 완료 확인 */
if (!(s & RQ_LOCAL_COMPLETED))
return false;
/* Protocol별 네트워크 완료 조건 */
switch (connection->net_conf->wire_protocol) {
case DRBD_PROT_A:
/* TCP 송신 버퍼에 전달되면 완료 */
return (s & RQ_NET_SENT);
case DRBD_PROT_B:
/* 피어 TCP 수신 확인(P_RECV_ACK) 필요 */
return (s & RQ_NET_DONE);
case DRBD_PROT_C:
/* 피어 디스크 쓰기 완료(P_WRITE_ACK) 필요 */
return (s & RQ_NET_DONE);
}
return false;
}
/* 완료 처리: bio_endio 호출 */
static void drbd_req_complete(struct drbd_request *req)
{
if (drbd_req_is_done(req)) {
/* Activity Log 해제 */
if (req->rq_state & RQ_IN_ACT_LOG)
drbd_al_complete_io(req->device, &req->i);
/* 원본 bio 완료 콜백 */
bio_endio(req->master_bio);
/* 요청 구조체 해제 */
drbd_req_free(req);
}
}
| 상태 전이 | 트리거 | 동작 |
|---|---|---|
| 생성 → RQ_LOCAL_PENDING | drbd_make_request() | bio 복제 후 로컬 디스크 제출 |
| → RQ_LOCAL_COMPLETED | 로컬 디스크 I/O 완료 인터럽트 | 로컬 완료 기록 |
| 생성 → RQ_NET_PENDING | drbd_send_dblock() | 네트워크 전송 대기열 진입 |
| → RQ_NET_SENT | TCP 송신 버퍼 전달 완료 | Protocol A: 네트워크 완료 |
| → RQ_NET_DONE (P_RECV_ACK) | 피어 TCP 수신 확인 | Protocol B: 네트워크 완료 |
| → RQ_NET_DONE (P_WRITE_ACK) | 피어 디스크 쓰기 완료 | Protocol C: 네트워크 완료 |
| RQ_LOCAL_COMPLETED + 네트워크 완료 | drbd_req_complete() | bio_endio() 호출, 요청 해제 |
drbd_request는 Transfer Log(TL)에 연결됩니다. 네트워크 장애 시 TL에 남아있는 요청들의 비트맵 비트를 설정하여, 재연결 시 해당 블록이 재동기화 대상이 됩니다. TL은 DRBD의 데이터 일관성을 보장하는 핵심 메커니즘입니다.
Multi-Volume 구성: Stacked vs Side-by-Side
DRBD에서 여러 블록 디바이스를 복제하는 두 가지 패턴이 있습니다. Side-by-Side(DRBD 9 Multi-Volume)와 Stacked(DRBD 8 레거시) 방식입니다.
| 특성 | Side-by-Side (Multi-Volume) | Stacked (레거시) |
|---|---|---|
| DRBD 버전 | DRBD 9+ (권장) | DRBD 8 (호환용) |
| TCP 연결 | 리소스당 2개 (data + meta) 공유 | 스택 단계별 2개씩 |
| 쓰기 순서 | 모든 볼륨 간 순서 보장 | 각 스택 계층 독립 |
| 역할 전환 | 리소스 단위 일괄 | 각 계층 개별 전환 |
| 구성 복잡도 | 낮음 | 높음 |
| 사용 사례 | DB (데이터+WAL+메타) | 3사이트 DR (2단계 복제) |
# Side-by-Side (DRBD 9 Multi-Volume) — 권장 방식
resource db-cluster {
options {
quorum majority;
on-no-quorum io-error;
}
volume 0 { # PostgreSQL 데이터
device /dev/drbd10;
disk /dev/vg0/pg_data;
meta-disk internal;
}
volume 1 { # PostgreSQL WAL
device /dev/drbd11;
disk /dev/vg0/pg_wal;
meta-disk internal;
}
volume 2 { # PostgreSQL pg_stat_tmp
device /dev/drbd12;
disk /dev/vg0/pg_tmp;
meta-disk internal;
}
on node1 { address 10.0.0.1:7789; }
on node2 { address 10.0.0.2:7789; }
on node3 { address 10.0.0.3:7789; }
}
# Stacked (DRBD 8 레거시) — 3사이트 DR 용도
# 1단계: 로컬 HA (LAN, Protocol C)
resource r0-local {
protocol C;
volume 0 {
device /dev/drbd0;
disk /dev/sdb1;
meta-disk internal;
}
on node1 { address 10.0.0.1:7789; }
on node2 { address 10.0.0.2:7789; }
}
# 2단계: 원격 DR (WAN, Protocol A)
resource r0-dr {
protocol A;
stacked-on-top-of r0-local { # 1단계 위에 쌓기
device /dev/drbd10;
address 127.0.0.1:7790;
}
on dr-site {
device /dev/drbd10;
disk /dev/sdb1;
address 198.51.100.1:7790;
meta-disk internal;
}
}
관련 문서
DRBD와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.