SCSI / iSCSI 서브시스템
Linux 커널의 SCSI 서브시스템을 호스트 어댑터부터 원격 타겟 스토리지까지 이어지는 전체 데이터 경로 관점으로 심층 분석합니다. SCSI 아키텍처 모델(SAM)과 Upper/Mid/Lower 3계층 역할 분리, 핵심 구조체(Struct)와 큐잉 모델, 명령 제출·완료·재시도 흐름, 오류 처리(EH) 스레드(Thread)와 타임아웃 복구 전략, SAS/FC/iSCSI/UAS 전송별 특성과 병목(Bottleneck) 포인트, open-iscsi와 LIO를 포함한 iSCSI 실무 구성, SCSI LLD 드라이버 개발 패턴, sysfs/tracepoint 기반 관측과 문제 재현 절차까지 안정성 중심 운영에 필요한 내용을 폭넓게 다룹니다.
핵심 요약
- SCSI — 스토리지 디바이스와 통신하는 명령 프로토콜 패밀리입니다. NVMe를 제외한 거의 모든 블록 디바이스가 경유합니다.
- 3계층 구조 — Upper(sd, sr, sg) / Mid(SCSI 코어) / Lower(HBA 드라이버) 계층으로 분리됩니다.
- CDB — Command Descriptor Block. SCSI 명령의 표준 형식(READ/WRITE/INQUIRY 등)입니다.
- iSCSI — TCP/IP 위에서 SCSI 명령을 전송하는 프로토콜. 원격 스토리지를 로컬처럼 사용합니다.
- libata — SATA 디바이스를 SCSI 명령으로 변환하여 SCSI 스택에서 처리합니다.
단계별 이해
- SCSI 디바이스 확인 —
lsscsi로 시스템에 연결된 SCSI 디바이스를 확인합니다.SATA HDD/SSD도 libata를 통해 SCSI 디바이스로 표시됩니다.
- 3계층 이해 — Upper Layer(sd 드라이버)가 읽기/쓰기 요청을 SCSI 명령으로 변환하고, Mid Layer가 큐잉/에러 처리를 하며, Lower Layer(HBA)가 하드웨어에 전달합니다.
/sys/class/scsi_host/에서 HBA 정보를 확인할 수 있습니다. - SCSI 명령 관찰 —
sg_inq /dev/sda로 INQUIRY 명령을 보내 디바이스 정보를 조회합니다.sg3_utils패키지가 다양한 SCSI 명령 도구를 제공합니다. - 에러 처리 — SCSI 명령 실패 시 Mid Layer의 Error Handler(EH)가 복구를 시도합니다.
명령 재시도 → 디바이스 리셋 → 버스(Bus) 리셋 → 호스트 리셋 순으로 에스컬레이션합니다.
SCSI 서브시스템 개요
SCSI(Small Computer System Interface)는 1986년 ANSI 표준(X3.131)으로 시작하여, 40년 가까이 스토리지 I/O의 핵심 프로토콜 패밀리로 진화해왔습니다. 물리 버스 규격에서 출발했지만, 오늘날에는 전송 프로토콜과 독립적인 명령 세트(Command Set)로 기능하며, SAS·Fibre Channel·iSCSI·USB(UAS) 등 다양한 전송 계층 위에서 동작합니다.
Linux 커널의 SCSI 서브시스템(drivers/scsi/)은 이 SCSI 프로토콜 패밀리 전체를 지원하는 통합 프레임워크를 제공합니다. 직접적인 SCSI 디바이스뿐 아니라, libata를 통해 SATA 디바이스도 SCSI 명령으로 변환하여 처리하므로, 사실상 NVMe를 제외한 거의 모든 블록 스토리지가 SCSI 스택을 경유합니다.
프로토콜 진화
| 세대 | 전송 방식 | 최대 대역폭(Bandwidth) | 최대 거리 | 비고 |
|---|---|---|---|---|
| Parallel SCSI (SPI) | 병렬 버스 | 640 MB/s (Ultra-640) | ~12m | 레거시, 커널에서 단종 |
| SAS (Serial Attached SCSI) | 직렬 점대점 | 22.5 Gb/s (SAS-4) | ~10m | 엔터프라이즈 표준 |
| Fibre Channel (FC) | 직렬 (광섬유/구리) | 128 Gb/s (64GFC) | ~10km (광) | SAN 환경 |
| iSCSI | TCP/IP 네트워크 | 네트워크 대역 의존 | 무제한 | IP 기반 SAN |
| UAS (USB Attached SCSI) | USB 3.x | 20 Gb/s (USB 3.2) | ~5m | 외장 스토리지 |
SCSI 명령 세트 표준
SCSI 표준은 T10 위원회(현 INCITS)에서 관리하며, 용도별로 분리된 명령 세트로 구성됩니다:
| 표준 | 대상 디바이스 | 주요 명령 |
|---|---|---|
| SPC (SCSI Primary Commands) | 모든 SCSI 디바이스 공통 | INQUIRY, TEST UNIT READY, REQUEST SENSE, MODE SELECT/SENSE |
| SBC (SCSI Block Commands) | 디스크 (HDD/SSD) | READ(10/16), WRITE(10/16), SYNCHRONIZE CACHE, UNMAP |
| SSC (SCSI Stream Commands) | 테이프 드라이브 | READ, WRITE, REWIND, SPACE |
| MMC (Multi-Media Commands) | CD/DVD/BD | READ TOC, GET CONFIGURATION, READ DISC INFORMATION |
| SES (SCSI Enclosure Services) | 디스크 인클로저 | RECEIVE DIAGNOSTIC, SEND DIAGNOSTIC |
sd 드라이버는 SBC, sr은 MMC, st는 SSC, ses는 SES 명령 세트를 구현합니다. sg(SCSI Generic)는 사용자 공간(User Space)에서 임의의 CDB를 전송할 수 있는 범용 인터페이스입니다.SCSI 아키텍처 모델 (SAM)
SAM(SCSI Architecture Model)은 SCSI 프로토콜의 추상적 동작 모델을 정의합니다. 전송 매체와 무관하게 Initiator–Target 간 명령 전달 체계를 규격화합니다.
핵심 구성 요소
| SAM 개념 | 설명 | Linux 커널 대응 |
|---|---|---|
| Initiator | 명령을 발행하는 주체 | struct Scsi_Host (HBA 드라이버) |
| Target | 명령을 수신·실행하는 주체 | struct scsi_target |
| LUN (Logical Unit) | Target 내 논리적 디바이스 | struct scsi_device |
| HBA (Host Bus Adapter) | 호스트와 SCSI 버스를 연결하는 하드웨어 | struct scsi_host_template + LLD |
| CDB (Command Descriptor Block) | SCSI 명령 패킷 (6/10/12/16/32바이트) | scsi_cmnd.cmnd[] |
| Sense Data | 오류/상태 상세 정보 | scsi_cmnd.sense_buffer[] |
SCSI 명령 모델
SCSI 명령은 CDB(Command Descriptor Block)라는 고정/가변 길이 패킷으로 구성됩니다. CDB의 첫 바이트가 Operation Code이며, 명령 길이를 결정합니다:
/* CDB 구조 예시: READ(10) — 10바이트 CDB */
/* Byte 0: Operation Code (0x28 = READ(10)) */
/* Byte 1: [7:5] Reserved, [4:0] Service Action (보통 0) */
/* Byte 2-5: Logical Block Address (빅엔디안) */
/* Byte 6: Reserved / Group Number */
/* Byte 7-8: Transfer Length (블록 수, 빅엔디안) */
/* Byte 9: Control */
u8 cdb[10] = {
0x28, /* READ(10) */
0x00, /* flags */
(lba >> 24) & 0xff, /* LBA [31:24] */
(lba >> 16) & 0xff, /* LBA [23:16] */
(lba >> 8) & 0xff, /* LBA [15:8] */
lba & 0xff, /* LBA [7:0] */
0x00, /* group */
(len >> 8) & 0xff, /* length MSB */
len & 0xff, /* length LSB */
0x00 /* control */
};
SCSI 상태 코드(Status Byte)는 명령 완료 시 반환됩니다:
| 상태 코드 | 값 | 의미 |
|---|---|---|
| GOOD | 0x00 | 명령 성공 |
| CHECK CONDITION | 0x02 | 오류 발생 — Sense Data 확인 필요 |
| BUSY | 0x08 | Target 처리 중, 재시도 필요 |
| RESERVATION CONFLICT | 0x18 | 다른 Initiator가 예약한 리소스 |
| TASK SET FULL | 0x28 | 큐 포화, 나중에 재시도 |
| ACA ACTIVE | 0x30 | 자동 비상 연합 활성 |
주요 SCSI 명령 코드
자주 사용되는 SCSI Operation Code(Opcode) 목록입니다. CDB 첫 바이트가 Opcode를 결정하며, 명령의 길이도 함께 결정됩니다:
| 명령 | Opcode | CDB 길이 | 설명 | 주요 용도 |
|---|---|---|---|---|
| TEST UNIT READY | 0x00 | 6 | 디바이스 준비 상태 확인 | 초기화, 미디어 감지 |
| REQUEST SENSE | 0x03 | 6 | Sense Data 읽기 | 오류 상세 정보 수집 |
| INQUIRY | 0x12 | 6 | 디바이스 정보 조회 | 장치 식별, Vital Product Data |
| READ(6) | 0x08 | 6 | 블록 읽기 (21비트 LBA) | 레거시 호환 |
| READ(10) | 0x28 | 10 | 블록 읽기 (32비트 LBA) | 일반 디스크 읽기 |
| READ(16) | 0x88 | 16 | 블록 읽기 (64비트 LBA) | 대용량 디스크 (>2TB) |
| WRITE(6) | 0x0A | 6 | 블록 쓰기 (21비트 LBA) | 레거시 호환 |
| WRITE(10) | 0x2A | 10 | 블록 쓰기 (32비트 LBA) | 일반 디스크 쓰기 |
| WRITE(16) | 0x8A | 16 | 블록 쓰기 (64비트 LBA) | 대용량 디스크 |
| SYNCHRONIZE CACHE | 0x35 | 10 | 캐시(Cache) 플러시 | 데이터 일관성 보장 |
| READ CAPACITY(10) | 0x25 | 10 | 디바이스 용량 조회 | 파티션 크기 확인 |
| READ CAPACITY(16) | 0x9E | 16 | 64비트 용량 조회 | 대용량 디스크 |
| MODE SENSE(6/10) | 0x1A/0x5A | 6/10 | 모드 페이지(Page) 읽기 | 캐싱 정책, 에러 복구 |
| MODE SELECT(6/10) | 0x15/0x55 | 6/10 | 모드 페이지 설정 | 디바이스 구성 |
| START STOP UNIT | 0x1B | 6 | 스핀업/다운, 이젝트 | 전원 관리(Power Management) |
| UNMAP | 0x42 | 10 | 블록 할당 해제 | TRIM/DISCARD (SSD) |
| COMPARE AND WRITE | 0x89 | 16 | 원자적(Atomic) 비교-쓰기 | 클러스터 잠금(Lock) |
READ(10)과 WRITE(10)이 가장 빈번히 사용되며, 2TB 이상 디스크에서는 READ(16)/WRITE(16)으로 자동 전환됩니다. UNMAP은 파일시스템(Filesystem)의 fstrim 또는 discard 마운트(Mount) 옵션과 연결됩니다.Linux SCSI 스택 구조
Linux SCSI 서브시스템은 3계층으로 구성됩니다. Upper Layer가 블록/문자 디바이스 인터페이스를 제공하고, Mid Layer(scsi_mod)가 명령 관리·큐잉·오류 처리를 담당하며, Lower Layer의 LLD(Low-Level Driver)가 실제 HBA 하드웨어를 제어합니다.
Upper Layer 드라이버
| 드라이버 | 모듈 | 디바이스 노드 | 용도 |
|---|---|---|---|
| sd | sd_mod | /dev/sd[a-z] | 디스크 (HDD, SSD, SAS, USB 스토리지) |
| sr | sr_mod | /dev/sr[0-9] | CD/DVD/Blu-ray |
| st | st | /dev/st[0-9] | 테이프 드라이브 |
| sg | sg | /dev/sg[0-9] | SCSI Generic — 사용자 공간에서 직접 CDB 전송 |
| ses | ses | — | SES 인클로저 서비스 |
SCSI Mid Layer (scsi_mod)
커널 모듈(Kernel Module) scsi_mod는 SCSI 서브시스템의 핵심입니다. 주요 책임:
- 디바이스 스캔: 호스트 등록 시 INQUIRY 명령으로 Target/LUN 탐색
- 명령 큐잉: blk-mq와 연동하여 SCSI 명령을 LLD에 디스패치(Dispatch)
- 오류 처리(EH): 타임아웃·오류 발생 시 에스컬레이션 기반 복구
- 전송 클래스: SAS/FC/iSCSI 등 전송별 sysfs 속성 관리
- 전원 관리: suspend/resume, 런타임 PM 연동
Lower Layer — LLD (Low-Level Driver)
| LLD | HBA / 전송 | 벤더 |
|---|---|---|
mpt3sas | SAS 3xxx HBA | Broadcom (LSI) |
megaraid_sas | MegaRAID SAS | Broadcom (LSI) |
hpsa / smartpqi | Smart Array / SmartPQI | HPE (Microchip) |
aacraid | Adaptec RAID | Microsemi |
qla2xxx | Fibre Channel | QLogic (Marvell) |
lpfc | Fibre Channel | Emulex (Broadcom) |
iscsi_tcp | iSCSI (소프트웨어) | Open-iSCSI |
libata + ahci | SATA → SCSI 변환 | (표준) |
uas | USB Attached SCSI | (표준) |
scsi_debug | 가상 SCSI 디바이스 | 테스트/개발용 |
핵심 구조체
SCSI 서브시스템의 동작을 이해하려면 4가지 핵심 구조체를 알아야 합니다.
scsi_host_template
LLD가 Mid Layer에 자신의 능력(capabilities)과 콜백(Callback)을 알리는 템플릿입니다. scsi_host_alloc() 호출 시 이 템플릿을 기반으로 Scsi_Host가 생성됩니다.
/* include/scsi/scsi_host.h — 핵심 필드만 발췌 */
struct scsi_host_template {
const char *name; /* 드라이버 이름 */
const char *proc_name; /* /proc/scsi/ 디렉터리명 */
/* ── 필수 콜백 ── */
int (*queuecommand)(struct Scsi_Host *, struct scsi_cmnd *);
/* CDB를 HBA에 제출 — 반드시 구현 */
/* ── 오류 처리 콜백 ── */
int (*eh_abort_handler)(struct scsi_cmnd *);
int (*eh_device_reset_handler)(struct scsi_cmnd *);
int (*eh_target_reset_handler)(struct scsi_cmnd *);
int (*eh_host_reset_handler)(struct scsi_cmnd *);
/* ── 디바이스 수명 주기 ── */
int (*slave_alloc)(struct scsi_device *);
int (*slave_configure)(struct scsi_device *);
void (*slave_destroy)(struct scsi_device *);
/* ── 큐 제한 ── */
int can_queue; /* HBA가 동시에 처리 가능한 최대 명령 수 */
int cmd_per_lun; /* LUN당 최대 동시 명령 */
short sg_tablesize; /* scatter-gather 세그먼트 최대 수 */
unsigned short max_sectors; /* 단일 명령 최대 섹터 수 */
struct module *module;
};
Scsi_Host
HBA 인스턴스를 나타내며, scsi_host_alloc()으로 할당됩니다. LLD는 이 구조체의 hostdata[] 영역에 private 데이터를 저장합니다.
/* include/scsi/scsi_host.h — 핵심 필드만 발췌 */
struct Scsi_Host {
struct scsi_host_template *hostt; /* 템플릿 포인터 */
struct list_head __devices; /* scsi_device 리스트 */
struct list_head __targets; /* scsi_target 리스트 */
unsigned int host_no; /* 고유 호스트 번호 */
unsigned int max_id; /* 최대 Target ID */
unsigned int max_lun; /* 최대 LUN */
unsigned int max_channel; /* 최대 채널 */
unsigned int can_queue; /* 동시 명령 제한 */
struct device shost_dev; /* sysfs 장치 */
struct workqueue_struct *tmf_work_q; /* Task Management 워크큐 */
unsigned long hostdata[0] /* LLD private data */
__attribute__((aligned(sizeof(unsigned long))));
};
scsi_device
SCSI LUN 하나를 나타냅니다. 디바이스 스캔 시 INQUIRY 응답을 기반으로 Mid Layer가 생성합니다.
/* include/scsi/scsi_device.h — 핵심 필드만 발췌 */
struct scsi_device {
struct Scsi_Host *host;
struct request_queue *request_queue;
struct scsi_target *sdev_target;
unsigned int id; /* Target ID */
u64 lun; /* Logical Unit Number */
unsigned int channel; /* SCSI 채널 */
unsigned sector_size; /* 논리 섹터 크기 (보통 512) */
unsigned char type; /* SCSI 디바이스 타입 (TYPE_DISK 등) */
unsigned char scsi_level; /* SCSI 버전 */
char vendor[8]; /* INQUIRY 벤더 */
char model[16]; /* INQUIRY 모델 */
char rev[4]; /* INQUIRY 리비전 */
unsigned int queue_depth; /* 동시 명령 큐 깊이 */
struct device sdev_gendev; /* sysfs 장치 */
};
scsi_cmnd
하나의 SCSI 명령을 나타내는 구조체입니다. blk-mq의 request에 내장(embed)되어 할당됩니다.
/* include/scsi/scsi_cmnd.h — 핵심 필드만 발췌 */
struct scsi_cmnd {
struct scsi_device *device;
struct request *request; /* blk-mq request */
unsigned char cmnd[32]; /* CDB 버퍼 (최대 32바이트) */
unsigned short cmd_len; /* CDB 길이 */
unsigned int result; /* 완료 상태 (host_byte | msg_byte | status_byte) */
unsigned char sense_buffer[96]; /* Sense Data */
enum dma_data_direction sc_data_direction;
unsigned int transfersize; /* 전송 블록 크기 */
struct scsi_data_buffer sdb; /* scatter-gather 데이터 */
unsigned int allowed; /* 최대 재시도 횟수 */
int retries; /* 현재 재시도 횟수 */
};
SCSI 명령 실행 경로
제출 경로 (Submit Path)
블록 I/O가 SCSI 명령으로 변환되어 HBA에 도달하기까지의 경로입니다:
submit_bio()→ blk-mq 소프트웨어 큐에 bio 삽입- blk-mq가 bio를
struct request로 병합·변환 scsi_queue_rq()— SCSI blk-mq ops의.queue_rq콜백scsi_dispatch_cmd()— CDB 구성,scsi_cmnd준비hostt->queuecommand()— LLD에 명령 전달- LLD가 HBA DMA 레지스터(Register)에 명령을 기록하거나 네트워크로 전송
완료 경로 (Completion Path)
HBA가 명령 완료를 통지하면 다음 경로로 처리됩니다:
- HBA 인터럽트(Interrupt) → LLD ISR(Interrupt Service Routine) 실행
- LLD가
scsi_done(scsi_cmnd)호출 scsi_decide_disposition()— 상태 판단 (성공/재시도/EH)- 성공 시:
scsi_finish_command()→blk_mq_complete_request() - 오류 시:
scsi_eh_scmd_add()→ EH 큐에 추가
blk-mq와 SCSI 연동
현대 커널의 메인라인 SCSI 경로는 blk-mq 기반으로 동작합니다. 다만 특정 구형 커널/벤더 포크에서는 예외가 있을 수 있으므로, 대상 트리에서 큐잉 경로를 확인해야 합니다. SCSI Mid Layer는 scsi_mq_ops를 blk-mq에 등록합니다:
/* drivers/scsi/scsi_lib.c */
static const struct blk_mq_ops scsi_mq_ops = {
.queue_rq = scsi_queue_rq,
.complete = scsi_complete,
.timeout = scsi_timeout,
.init_request = scsi_mq_init_request,
.exit_request = scsi_mq_exit_request,
.map_queues = scsi_map_queues,
};
/* scsi_cmnd는 blk-mq request의 driver private 영역에 내장됨 */
struct scsi_cmnd *scsi_cmd = blk_mq_rq_to_pdu(rq);
can_queue는 HBA가 동시에 처리 가능한 명령 수를 결정하며, blk-mq의 queue_depth에 직접 영향을 줍니다. NVMe 수준의 높은 동시성이 필요한 SAS HBA에서는 이 값을 수천 이상으로 설정합니다.SCSI Tagged Command Queuing
현대 SCSI 디바이스는 Tagged Command Queuing을 지원하여 여러 명령을 동시에 처리할 수 있습니다. 각 명령에는 고유한 태그가 부여되며, 디바이스는 순서에 관계없이 완료된 명령부터 응답할 수 있습니다(Out-of-Order Completion). 이는 디스크 헤드 스케줄링 최적화와 높은 IOPS에 필수적입니다.
SCSI-2부터는 Simple, Ordered, Head of Queue 태그 타입을 지원했으며, SCSI-3(SPC-3 이후)에서는 ACA(Auto Contingent Allegiance) 메커니즘도 추가되었습니다. 현대 리눅스 커널은 대부분 Simple 태그만 사용하며, blk-mq의 태그 할당과 연동됩니다.
/sys/block/sda/device/queue_depth에서 디바이스별 큐 깊이를 확인하고, 워크로드에 따라 조정할 수 있습니다. SSD에서는 32-64, 엔터프라이즈 HDD에서는 128-256이 일반적입니다.SCSI 오류 처리 (EH)
SCSI 오류 처리(Error Handling)는 타임아웃이나 CHECK CONDITION 등 오류 발생 시 에스컬레이션 단계별 복구를 시도하는 메커니즘입니다. 커널 스레드(Kernel Thread) scsi_eh_[N]가 EH를 담당합니다.
EH 동작 흐름
- 타임아웃 감지: blk-mq 타이머(Timer)가
scsi_timeout()을 호출 - EH 큐 등록:
scsi_eh_scmd_add()가 실패한 명령을 EH 목록에 추가 - EH 스레드 기상:
scsi_eh_[N]커널 스레드가 깨어남 - 에스컬레이션: abort → device reset → target reset → host reset 순서로 시도
- 복구 완료: 성공한 단계에서 실패 명령을 재시도하거나, 모든 단계 실패 시 디바이스를 오프라인으로 전환
/* drivers/scsi/scsi_error.c — EH 에스컬레이션 핵심 로직 */
static void scsi_unjam_host(struct Scsi_Host *shost)
{
/* 1단계: 명령 abort 시도 */
if (!scsi_eh_abort_cmds(&eh_work_q, &done_q))
goto done;
/* 2단계: 디바이스 리셋 */
if (!scsi_eh_bus_device_reset(shost, &eh_work_q, &done_q))
goto done;
/* 3단계: 타겟 리셋 */
if (!scsi_eh_target_reset(shost, &eh_work_q, &done_q))
goto done;
/* 4단계: 호스트 리셋 (가장 파괴적) */
scsi_eh_bus_reset(shost, &eh_work_q, &done_q);
scsi_eh_host_reset(shost, &eh_work_q, &done_q);
done:
scsi_eh_flush_done_q(&done_q); /* 복구된 명령 재발행 */
}
Sense Data 해석
CHECK CONDITION 상태 시 Target이 반환하는 Sense Data는 3단계 키로 오류를 분류합니다:
| Sense Key | 값 | 의미 | 대표적 원인 |
|---|---|---|---|
| NO SENSE | 0x0 | 오류 없음 | 정보성 응답 |
| RECOVERED ERROR | 0x1 | 복구된 오류 | ECC 교정 성공 |
| NOT READY | 0x2 | 디바이스 준비 안 됨 | 스핀업 중, 매체 없음 |
| MEDIUM ERROR | 0x3 | 매체 오류 | 불량 섹터, 읽기 불가 |
| HARDWARE ERROR | 0x4 | 하드웨어 오류 | 컨트롤러 고장 |
| ILLEGAL REQUEST | 0x5 | 잘못된 명령/파라미터 | 지원하지 않는 CDB |
| UNIT ATTENTION | 0x6 | 상태 변경 알림 | 매체 교체, 리셋 발생 |
| DATA PROTECT | 0x7 | 쓰기 보호(Write Protection) | 읽기 전용(Read-Only) 매체 |
| ABORTED COMMAND | 0xB | 명령 중단 | 패리티 오류, 타임아웃 |
SCSI 전송 프로토콜
SCSI 명령은 다양한 물리/네트워크 전송 계층을 통해 전달됩니다. Linux 커널은 각 전송 방식에 대한 전송 클래스(Transport Class)를 sysfs로 노출합니다.
| 전송 | 매체 | 최대 대역폭 | 최대 거리 | 주요 용도 | 커널 모듈 |
|---|---|---|---|---|---|
| SAS | 직렬 구리 | 22.5 Gb/s | ~10m | 서버/DAS | scsi_transport_sas |
| FC | 광섬유/구리 | 128 Gb/s | ~10km | SAN | scsi_transport_fc |
| iSCSI | TCP/IP | 네트워크 의존 | 무제한 | IP SAN | scsi_transport_iscsi |
| UAS | USB | 20 Gb/s | ~5m | 외장 | uas |
| SATA | 직렬 구리 | 6 Gb/s | ~1m | 데스크톱/서버 | libata + ahci |
SAS (Serial Attached SCSI)
엔터프라이즈 스토리지의 표준 전송입니다. Expander를 통한 팬아웃, 듀얼 포트, SATA 디바이스 호환이 특징입니다. 커널에서는 scsi_transport_sas 클래스가 SAS 주소(WWN), PHY 속성, Expander 토폴로지(Topology)를 sysfs에 노출합니다.
Fibre Channel (FC)
대규모 SAN(Storage Area Network) 환경에서 사용됩니다. FC 스위치를 통한 Fabric 토폴로지가 일반적이며, WWNN/WWPN으로 노드와 포트를 식별합니다. scsi_transport_fc 클래스가 포트 상태, 속도, 통계를 관리합니다.
iSCSI (개요)
TCP/IP 네트워크 위에서 SCSI 명령을 전달합니다. 기존 이더넷 인프라를 활용할 수 있어 비용 효율적입니다. 자세한 내용은 iSCSI 섹션을 참조하세요.
UAS (USB Attached SCSI)
USB BOT(Bulk-Only Transport)를 대체하는 프로토콜로, 명령 큐잉과 스트림 파이프를 지원하여 USB 스토리지 성능을 크게 향상시킵니다. uas 모듈이 담당합니다.
SATA (libata)
SATA 디바이스는 원래 ATA 프로토콜을 사용하지만, Linux에서는 libata가 ATA 명령을 SCSI 명령으로 변환(SAT — SCSI-ATA Translation)하여 SCSI 스택을 그대로 활용합니다. 따라서 SATA HDD/SSD도 /dev/sd*로 나타납니다.
sd 드라이버와 /dev/sd* 인터페이스를 공유합니다. smartctl -d ata /dev/sda로 ATA 네이티브 명령을 직접 실행할 수도 있습니다.iSCSI
iSCSI(Internet Small Computer System Interface)는 TCP/IP 네트워크를 통해 SCSI 명령을 캡슐화(Encapsulation)하여 전송하는 프로토콜입니다. RFC 3720(2004)에서 정의되었으며, RFC 7143(2014)에서 통합 개정되었습니다.
iSCSI 프로토콜 기초
iSCSI는 기존 이더넷 인프라 위에서 블록 스토리지 접근을 제공하므로, Fibre Channel 대비 낮은 비용으로 SAN을 구축할 수 있습니다. 주요 개념:
- IQN(iSCSI Qualified Name): 전역적으로 고유한 노드 식별자 —
iqn.yyyy-mm.도메인역순:식별자 - Portal: Target의 IP:포트 쌍 (기본 TCP 3260)
- Session: Initiator–Target 간 논리적 연결 (하나 이상의 TCP 연결)
- Connection: Session 내 단일 TCP 연결
- PDU(Protocol Data Unit): iSCSI 계층의 전송 단위
iSCSI Discovery 절차
iSCSI Initiator가 네트워크상의 Target을 발견하는 방법에는 여러 가지가 있습니다. 가장 일반적인 SendTargets Discovery 흐름은 다음과 같습니다:
Discovery가 완료되면 Initiator는 얻은 Target IQN과 Portal 정보를 사용하여 Normal Session을 수립하고 실제 SCSI 명령을 전송합니다. 다른 Discovery 방법으로는 iSNS(Internet Storage Name Service) 서버를 이용한 중앙집중식 Discovery, SLP(Service Location Protocol) 등이 있습니다.
PDU 구조
모든 iSCSI 통신은 PDU 단위로 이루어집니다. PDU는 48바이트 BHS(Basic Header Segment)로 시작합니다:
| PDU 타입 | Opcode | 방향 | 설명 |
|---|---|---|---|
| Login Request/Response | 0x03/0x23 | I↔T | 인증 및 세션 협상 |
| SCSI Command | 0x01 | I→T | CDB 전달 |
| SCSI Response | 0x21 | T→I | 명령 완료 상태 |
| Data-Out / Data-In | 0x05/0x25 | I→T / T→I | 데이터 전송 |
| NOP-Out / NOP-In | 0x00/0x20 | I↔T | 연결 유지 (keepalive) |
| Text Request/Response | 0x04/0x24 | I↔T | Discovery, 파라미터 교환 |
| Logout Request/Response | 0x06/0x26 | I↔T | 세션/연결 종료 |
| TMF (Task Mgmt) | 0x02/0x22 | I→T | 태스크(Task) abort/리셋 |
세션 관리
iSCSI 세션은 3단계로 수립됩니다:
Linux iSCSI Initiator — open-iscsi
open-iscsi는 Linux의 표준 iSCSI Initiator 구현입니다. 커널 모듈(iscsi_tcp, libiscsi)과 사용자 공간 데몬(iscsid)으로 구성됩니다.
# 1. Target 검색 (Discovery)
$ iscsiadm -m discovery -t sendtargets -p 192.168.1.100:3260
192.168.1.100:3260,1 iqn.2024-01.com.example:storage.lun0
# 2. Target에 로그인 (세션 수립)
$ iscsiadm -m node -T iqn.2024-01.com.example:storage.lun0 \
-p 192.168.1.100:3260 --login
# 3. 세션 상태 확인
$ iscsiadm -m session -P 3
Target: iqn.2024-01.com.example:storage.lun0
Current Portal: 192.168.1.100:3260,1
Attached scsi disk sdb State: running
# 4. 자동 로그인 설정 (부팅 시 자동 연결)
$ iscsiadm -m node -T iqn.2024-01.com.example:storage.lun0 \
-p 192.168.1.100:3260 --op update \
-n node.startup -v automatic
# 5. 로그아웃
$ iscsiadm -m node -T iqn.2024-01.com.example:storage.lun0 \
-p 192.168.1.100:3260 --logout
Linux iSCSI Target — LIO
LIO(Linux-IO Target)는 커널에 내장된 iSCSI Target 프레임워크입니다. targetcli를 사용하여 설정합니다.
# targetcli를 사용한 iSCSI Target 설정 예제
# 1. 블록 디바이스를 백스토어로 등록
$ targetcli
/> /backstores/block create myblock /dev/sdc
# 2. iSCSI Target 생성
/> /iscsi create iqn.2024-01.com.example:storage.lun0
# 3. LUN 매핑
/> /iscsi/iqn.2024-01.com.example:storage.lun0/tpg1/luns \
create /backstores/block/myblock
# 4. ACL 설정 (Initiator IQN 허용)
/> /iscsi/iqn.2024-01.com.example:storage.lun0/tpg1/acls \
create iqn.2024-01.com.example:client1
# 5. 포털 확인 (기본 0.0.0.0:3260)
/> /iscsi/iqn.2024-01.com.example:storage.lun0/tpg1/portals ls
# 6. 설정 저장
/> saveconfig
/> exit
tcm_fc), SRP(ib_srpt), vhost(tcm_vhost) 등 다양한 fabric을 지원하는 통합 Target 프레임워크입니다. /sys/kernel/config/target/에서 configfs로 직접 설정도 가능합니다.iSER (iSCSI Extensions for RDMA)
iSER은 InfiniBand/RoCE RDMA 전송 위에서 iSCSI를 실행하여, TCP 오버헤드(Overhead)를 제거하고 제로카피 전송을 가능하게 합니다. ib_iser(Initiator) 및 ib_isert(Target) 모듈이 담당합니다. 일반 iSCSI 대비 대역폭과 레이턴시에서 큰 이점이 있으며, HPC/AI 스토리지에서 활용됩니다.
iSCSI 성능 튜닝
| 파라미터 | 기본값 | 권장값 (10GbE+) | 설명 |
|---|---|---|---|
| node.session.cmds_max | 128 | 256–1024 | 세션당 최대 동시 명령 |
| node.session.queue_depth | 32 | 64–256 | LUN당 큐 깊이 |
| node.conn[0].iscsi.MaxRecvDataSegmentLength | 262144 | 262144 | 최대 수신 데이터 세그먼트 (바이트) |
| node.conn[0].iscsi.FirstBurstLength | 65536 | 262144 | 비솔리시티드(unsolicited) 데이터 최대 크기 |
| node.conn[0].iscsi.MaxBurstLength | 16776192 | 16776192 | 단일 Data-Out 시퀀스 최대 크기 |
| node.session.nr_sessions | 1 | 2–4 | 멀티세션 (MC/S) |
# 파라미터 조정 예시
$ iscsiadm -m node -T iqn.2024-01.com.example:storage.lun0 \
--op update -n node.session.queue_depth -v 128
$ iscsiadm -m node -T iqn.2024-01.com.example:storage.lun0 \
--op update -n node.session.cmds_max -v 512
# Jumbo Frame 활성화 (네트워크 측)
$ ip link set eth0 mtu 9000
LIO 타겟 프레임워크 개요
LIO(Linux I/O Target)는 리눅스 커널 2.6.38부터 메인라인에 포함된 범용 스토리지 타겟 프레임워크입니다. iSCSI 이니시에이터(open-iscsi)와 짝을 이루어, 리눅스 서버를 SAN 스토리지 타겟으로 구성할 수 있습니다.
configfs 기반 설정 흐름
LIO는 /sys/kernel/config/target/ configfs를 통해 설정됩니다. targetcli는 이 인터페이스를 래핑한 사용자 친화적 도구입니다.
# targetcli 핵심 명령 예제
# 1. Block 백엔드 생성 (/dev/sdb 블록 디바이스)
$ targetcli /backstores/block create name=lun0 dev=/dev/sdb
# 2. iSCSI 타겟 생성
$ targetcli /iscsi create iqn.2024-01.com.example:storage.lun0
# 3. Network Portal 추가 (포트 3260)
$ targetcli /iscsi/iqn.2024-01.com.example:storage.lun0/tpgt1/portals/ create
# 4. LUN 매핑
$ targetcli /iscsi/iqn.2024-01.com.example:storage.lun0/tpgt1/luns/ create \
/backstores/block/lun0
# 5. 이니시에이터 ACL 설정
$ targetcli /iscsi/iqn.2024-01.com.example:storage.lun0/tpgt1/acls/ create \
iqn.1991-05.com.redhat:client
# 6. 설정 저장
$ targetcli saveconfig
iSCSI 타겟 단계별 설정
# 이니시에이터(클라이언트)에서 타겟 발견
$ iscsiadm -m discovery -t sendtargets -p 192.168.1.100
# 타겟에 로그인
$ iscsiadm -m node -T iqn.2024-01.com.example:storage.lun0 \
-p 192.168.1.100 --login
# 연결된 iSCSI 디바이스 확인
$ lsscsi | grep disk
$ ls /dev/sd*
# LIO 타겟 상태 확인
$ targetcli ls /
# 활성 iSCSI 세션 확인
$ cat /proc/net/iscsi/session 2>/dev/null || \
ls /sys/kernel/config/target/iscsi/
LIO Fabric 모듈 비교
| Fabric | 전송 | 포트 | 커널 모듈 | 용도 |
|---|---|---|---|---|
| iSCSI | TCP/IP | 3260 | iscsi_target_mod | 범용 SAN (이더넷) |
| FC | Fibre Channel | - | tcm_qla2xxx | 엔터프라이즈 SAN |
| NVMe-oF/TCP | TCP/IP | 4420 | nvmet-tcp | 저레이턴시 NVMe SAN |
| NVMe-oF/RDMA | InfiniBand/RoCE | 4420 | nvmet-rdma | 초저레이턴시 SAN |
SCSI 드라이버 스켈레톤
최소한의 가상 SCSI HBA 드라이버 예제입니다. 실제 하드웨어 대신 요청을 즉시 완료하는 구조입니다.
#include <linux/module.h>
#include <linux/kernel.h>
#include <scsi/scsi_host.h>
#include <scsi/scsi_cmnd.h>
#include <scsi/scsi_device.h>
#define DRIVER_NAME "my_scsi_hba"
#define MAX_CMDS 64
#define MAX_SECTORS 1024
#define SG_TABLESIZE 128
/* ── queuecommand: CDB를 HBA에 제출 ── */
static int my_queuecommand(struct Scsi_Host *shost,
struct scsi_cmnd *cmd)
{
/* 실제 드라이버: DMA 매핑 → HBA 레지스터/메일박스에 CDB 기록 */
/* 여기서는 즉시 완료 시뮬레이션 */
cmd->result = (DID_OK << 16);
scsi_done(cmd);
return 0;
}
/* ── EH 콜백 ── */
static int my_eh_abort(struct scsi_cmnd *cmd)
{
dev_warn(&cmd->device->sdev_gendev, "aborting cmd\\n");
return SUCCESS;
}
static int my_eh_host_reset(struct scsi_cmnd *cmd)
{
dev_warn(&cmd->device->sdev_gendev, "host reset\\n");
return SUCCESS;
}
/* ── Host Template ── */
static struct scsi_host_template my_sht = {
.module = THIS_MODULE,
.name = DRIVER_NAME,
.proc_name = DRIVER_NAME,
.queuecommand = my_queuecommand,
.eh_abort_handler = my_eh_abort,
.eh_host_reset_handler = my_eh_host_reset,
.can_queue = MAX_CMDS,
.this_id = -1,
.sg_tablesize = SG_TABLESIZE,
.max_sectors = MAX_SECTORS,
.cmd_per_lun = 32,
};
static struct Scsi_Host *my_shost;
static int __init my_scsi_init(void)
{
my_shost = scsi_host_alloc(&my_sht, 0);
if (!my_shost)
return -ENOMEM;
if (scsi_add_host(my_shost, NULL)) {
scsi_host_put(my_shost);
return -ENODEV;
}
scsi_scan_host(my_shost);
pr_info("my_scsi_hba: loaded\\n");
return 0;
}
static void __exit my_scsi_exit(void)
{
scsi_remove_host(my_shost);
scsi_host_put(my_shost);
pr_info("my_scsi_hba: unloaded\\n");
}
module_init(my_scsi_init);
module_exit(my_scsi_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal SCSI HBA skeleton driver");
sysfs 인터페이스
SCSI 서브시스템은 sysfs를 통해 풍부한 디바이스 정보와 제어 인터페이스를 제공합니다.
| 경로 | 설명 |
|---|---|
/sys/class/scsi_host/host<N>/ | HBA 속성 (scan, state, proc_name 등) |
/sys/class/scsi_device/<H:C:T:L>/ | SCSI 디바이스 속성 |
/sys/block/sd<X>/device/ | 블록 디바이스에서 SCSI 디바이스로의 링크 |
/sys/class/scsi_disk/ | SCSI 디스크 클래스 |
/sys/class/iscsi_session/ | iSCSI 세션 속성 |
/sys/class/iscsi_connection/ | iSCSI 연결 속성 |
/sys/class/sas_phy/ | SAS PHY 속성 |
/sys/class/fc_host/ | FC HBA 포트 속성 |
# H:C:T:L 형식으로 디바이스 식별 (Host:Channel:Target:LUN)
# HBA 재스캔 — 새로 추가된 디바이스 탐지
$ echo "- - -" > /sys/class/scsi_host/host0/scan
# 특정 LUN 재스캔
$ echo "0 0 1" > /sys/class/scsi_host/host0/scan # Channel:0, Target:0, LUN:1
# 디바이스 큐 깊이 조정
$ echo 64 > /sys/block/sda/device/queue_depth
# 디바이스 안전 제거 (hot-remove)
$ echo 1 > /sys/block/sdb/device/delete
# SCSI 디바이스 상태 확인
$ cat /sys/block/sda/device/state # running / offline / blocked
$ cat /sys/block/sda/device/vendor
$ cat /sys/block/sda/device/model
디버깅(Debugging) 도구
lsscsi
# 모든 SCSI 디바이스 목록
$ lsscsi
[0:0:0:0] disk ATA SAMSUNG MZ7LH1T9 404Q /dev/sda
[2:0:0:0] disk LIO-ORG block0 4.0 /dev/sdb
# 상세 정보 (-g: sg 디바이스, -t: 전송, -L: LUN 정보)
$ lsscsi -gtvL
# HBA 호스트 정보
$ lsscsi -H
sg3_utils
# INQUIRY 명령 전송
$ sg_inq /dev/sda
Vendor identification: ATA
Product identification: SAMSUNG MZ7LH1T9
Product revision level: 404Q
# 디바이스 상태 확인 (TEST UNIT READY)
$ sg_turs /dev/sda
# Sense Data 직접 조회
$ sg_requests /dev/sda
# 로그 페이지 조회 (에러 카운터 등)
$ sg_logs /dev/sda
# SMART/Health 정보 (SBC)
$ sg_sat_identify /dev/sda
커널 로깅
# SCSI 로깅 레벨 설정 (비트마스크)
# 0x1=error, 0x2=timeout, 0x4=scan, 0x8=mlqueue, 0x10=mlcomplete
# 0x20=llqueue, 0x40=llcomplete, 0x80=hlqueue, 0x100=hlcomplete
$ echo 0x1ff > /proc/sys/dev/scsi/logging_level
# scsi_debug 가상 디바이스로 테스트
$ modprobe scsi_debug num_tgts=1 max_luns=2 dev_size_mb=256
$ lsscsi | grep scsi_debug
# 커널 메시지 필터링
$ dmesg | grep -i scsi
$ journalctl -k --grep="scsi|sd |sr "
/proc/scsi
# SCSI 서브시스템 요약
$ cat /proc/scsi/scsi
Attached devices:
Host: scsi0 Channel: 00 Id: 00 Lun: 00
Vendor: ATA Model: SAMSUNG MZ7LH1T9 Rev: 404Q
Type: Direct-Access ANSI SCSI revision: 05
# 특정 HBA 드라이버 정보
$ ls /proc/scsi/
$ cat /proc/scsi/sg/devices
주요 Kconfig 옵션 및 소스 구조
주요 Kconfig 옵션
| 옵션 | 모듈 | 설명 |
|---|---|---|
CONFIG_SCSI | scsi_mod | SCSI 서브시스템 핵심 (필수) |
CONFIG_BLK_DEV_SD | sd_mod | SCSI 디스크 (sd) 드라이버 |
CONFIG_CHR_DEV_SG | sg | SCSI Generic 인터페이스 |
CONFIG_CHR_DEV_ST | st | SCSI 테이프 드라이버 |
CONFIG_BLK_DEV_SR | sr_mod | SCSI CD-ROM 드라이버 |
CONFIG_SCSI_SAS_ATTRS | scsi_transport_sas | SAS 전송 클래스 |
CONFIG_SCSI_FC_ATTRS | scsi_transport_fc | FC 전송 클래스 |
CONFIG_ISCSI_TCP | iscsi_tcp | 소프트웨어 iSCSI Initiator |
CONFIG_ISCSI_TARGET | iscsi_target_mod | LIO iSCSI Target |
CONFIG_SCSI_DEBUG | scsi_debug | 가상 SCSI 디바이스 (테스트) |
CONFIG_SATA_AHCI | ahci | AHCI SATA 컨트롤러 |
커널 소스 디렉터리 구조
| 경로 | 내용 |
|---|---|
drivers/scsi/ | SCSI 서브시스템 핵심 + LLD 드라이버 |
drivers/scsi/scsi_lib.c | 블록 계층 연동, scsi_mq_ops |
drivers/scsi/scsi_error.c | 오류 처리(EH) 로직 |
drivers/scsi/scsi_scan.c | 디바이스 스캔/탐지 |
drivers/scsi/scsi_sysfs.c | sysfs 인터페이스 |
drivers/scsi/sd.c | SCSI 디스크 (sd) 드라이버 |
drivers/scsi/sg.c | SCSI Generic 드라이버 |
drivers/scsi/scsi_transport_*.c | 전송 클래스 (SAS, FC, iSCSI, SRP) |
drivers/target/ | LIO Target 프레임워크 |
drivers/target/iscsi/ | iSCSI Target 구현 |
drivers/ata/ | libata + AHCI/SATA 드라이버 |
include/scsi/ | SCSI 헤더 파일 |
SCSI Error Handling
기본적인 EH 에스컬레이션 개요는 앞 절에서 다루었습니다. 이 절에서는 scsi_eh_* 함수 체인의 내부 구현, EH 스레드 수명주기, 타임아웃 정책, 그리고 LLD가 EH를 커스터마이즈하는 방법을 심층적으로 분석합니다.
EH 스레드 수명주기
SCSI 호스트가 등록될 때(scsi_add_host()), 커널 스레드 scsi_eh_<N>가 생성됩니다. 이 스레드는 평소에는 TASK_INTERRUPTIBLE 상태로 잠들어 있다가, 오류가 발생하면 깨어나 복구를 수행합니다.
주요 EH 함수 체인
| 함수 | 역할 | 호출 시점 |
|---|---|---|
scsi_timeout() | blk-mq 타이머 콜백, 명령 타임아웃 판단 | 요청 타임아웃 만료 |
scsi_eh_scmd_add() | 실패 명령을 EH 큐에 등록, EH 스레드 기상 | 타임아웃 또는 CHECK CONDITION |
scsi_error_handler() | EH 스레드 메인 루프 | 스레드 생성 시 시작 |
scsi_unjam_host() | 에스컬레이션 로직 총괄 | EH 스레드 내부 |
scsi_eh_abort_cmds() | 개별 명령 abort | 1단계 |
scsi_eh_bus_device_reset() | LUN/디바이스 리셋 | 2단계 |
scsi_eh_target_reset() | 타겟 전체 리셋 | 3단계 (선택적) |
scsi_eh_bus_reset() | SCSI 버스 리셋 | 4단계 |
scsi_eh_host_reset() | HBA 전체 리셋 | 5단계 (최후 수단) |
scsi_eh_flush_done_q() | 복구된 명령 재발행 또는 에러 반환 | EH 완료 시 |
타임아웃 정책과 커스터마이즈
SCSI 명령 타임아웃은 scsi_device->timeout 필드(기본 30초)와 블록 계층의 rq_timeout이 함께 결정합니다. LLD는 scsi_host_template의 콜백으로 타임아웃 동작을 커스터마이즈할 수 있습니다:
/* LLD가 EH를 커스터마이즈하는 scsi_host_template 콜백들 */
struct scsi_host_template my_template = {
/* 개별 명령 abort — 성공 시 SUCCESS, 실패 시 FAILED */
.eh_abort_handler = my_abort,
/* 디바이스(LUN) 레벨 리셋 */
.eh_device_reset_handler = my_device_reset,
/* 타겟(ID) 레벨 리셋 — 같은 타겟의 모든 LUN 영향 */
.eh_target_reset_handler = my_target_reset,
/* 버스 리셋 — 같은 채널의 모든 디바이스 영향 */
.eh_bus_reset_handler = my_bus_reset,
/* 호스트 리셋 — HBA 전체 재초기화 */
.eh_host_reset_handler = my_host_reset,
/* 타임아웃 시 blk-mq가 호출 — BLK_EH_DONE / BLK_EH_RESET_TIMER */
.eh_timed_out = my_timed_out,
.cmd_per_lun = 32,
.can_queue = 256,
};
복구 패턴과 실무 대응
- 일시적 오류 — BUSY/TASK SET FULL 상태는
scsi_queue_insert()가 지수 백오프로 재시도합니다. - 매체 오류 — MEDIUM ERROR(Sense Key 0x3)는 재시도가 무의미하므로, sd 드라이버가 I/O 에러를 블록 계층에 보고합니다.
- 경로 장애 — multipath 환경에서 EH 실패 시 dm-mpath가 대체 경로로 failover합니다.
- EH 데드락 방지 —
shost->eh_deadline(sysfs:eh_deadline)을 설정하면, 지정 시간 내에 abort가 실패하면 즉시 host reset으로 건너뜁니다.
# EH 데드라인 설정 (초 단위, 0=무제한)
$ echo 30 > /sys/class/scsi_host/host0/eh_deadline
# 디바이스별 타임아웃 확인/설정
$ cat /sys/block/sda/device/timeout # 기본 30초
$ echo 60 > /sys/block/sda/device/timeout # 60초로 변경
# EH 동작 실시간 관찰
$ echo 0x3 > /proc/sys/dev/scsi/logging_level # error+timeout
$ dmesg -w | grep -i "scsi_eh\|abort\|reset"
Tagged Command Queuing과 blk-mq 매핑(Mapping)
TCQ(Tagged Command Queuing)는 SCSI 디바이스에 여러 명령을 동시에 전송하여 병렬 처리하는 메커니즘입니다. 초기에는 레거시 single-queue 모델이었으나, Linux 3.16 이후 blk-mq(Multi-Queue Block I/O)로 전환되면서 SCSI 스택의 큐잉 모델이 근본적으로 변경되었습니다.
SCSI blk-mq 구조
SCSI 서브시스템은 scsi_mq_ops를 블록 계층에 등록하여 blk-mq와 통합됩니다. 핵심 매핑 관계는 다음과 같습니다:
| blk-mq 개념 | SCSI 대응 | 설명 |
|---|---|---|
blk_mq_hw_ctx (hctx) | HBA 하드웨어 큐 | LLD가 nr_hw_queues로 개수 결정 |
blk_mq_ctx (ctx) | Per-CPU 소프트웨어 큐 | CPU별 요청 수집 |
blk_mq_tag_set | Scsi_Host->tag_set | 태그 관리 (큐 깊이 = 태그 수) |
.queue_rq() | scsi_queue_rq() | 요청을 SCSI 명령으로 변환, LLD에 전달 |
/* SCSI blk-mq ops — drivers/scsi/scsi_lib.c */
static const struct blk_mq_ops scsi_mq_ops = {
.queue_rq = scsi_queue_rq, /* 요청 → scsi_cmnd 변환 → LLD dispatch */
.complete = scsi_complete, /* 완료 처리 */
.timeout = scsi_timeout, /* 타임아웃 → EH 트리거 */
.init_request = scsi_mq_init_request,
.exit_request = scsi_mq_exit_request,
.map_queues = scsi_map_queues, /* CPU → hctx 매핑 */
};
/* LLD가 멀티 큐를 지원하는 예시 */
static struct scsi_host_template my_template = {
.nr_hw_queues = 4, /* 4개 하드웨어 큐 */
.can_queue = 1024, /* 전체 최대 outstanding 명령 */
.cmd_per_lun = 64, /* LUN당 최대 명령 */
.map_queues = my_map_queues, /* NUMA 인식 매핑 */
};
큐 깊이 튜닝
SCSI 디바이스의 큐 깊이는 성능에 직접적인 영향을 미칩니다. 큐가 너무 얕으면 디바이스 활용도가 떨어지고, 너무 깊으면 지연(Latency)이 증가합니다:
# 현재 큐 깊이 확인
$ cat /sys/block/sda/device/queue_depth
32
# 큐 깊이 변경
$ echo 128 > /sys/block/sda/device/queue_depth
# blk-mq 큐 매핑 확인
$ cat /sys/block/sda/mq/*/cpu_list
# hctx별 CPU 매핑을 보여줌
# SCSI 태그 사용 현황
$ cat /sys/kernel/debug/block/sda/hctx*/tags
queue_depth=64-128, 엔터프라이즈 HDD는 queue_depth=32-64가 일반적입니다. TASK SET FULL 상태가 빈번하면 큐 깊이를 줄여야 합니다.SAS 토폴로지
SAS(Serial Attached SCSI)는 엔터프라이즈 스토리지의 표준 직렬 인터커넥트입니다. 점대점 연결, Expander를 통한 팬아웃, 듀얼 포트 고가용성이 특징이며, SATA 디바이스와 하위 호환됩니다.
SAS 핵심 개념
| 개념 | 설명 |
|---|---|
| SAS 주소 (WWN) | 64비트 고유 식별자. 50:xx:xx:xx:xx:xx:xx:xx 형식. PHY, Expander, 디바이스마다 할당 |
| PHY | 단일 직렬 링크. 하나의 물리적 연결 단위 |
| Wide Port | 동일 SAS 주소로 묶인 여러 PHY. 대역폭 집약 (예: 4× Wide = 4배 대역폭) |
| Narrow Port | 단일 PHY로 구성된 포트 |
| Expander | SAS 스위치. Fanout Expander(상위)와 Edge Expander(하위)로 구분 |
| SMP (SAS Management Protocol) | Expander 관리용 프로토콜. 토폴로지 탐색, PHY 제어 |
| STP (SATA Tunneling Protocol) | SAS 도메인에서 SATA 디바이스를 투명하게 연결하는 프로토콜 |
| Dual-port | SAS 디바이스의 두 포트를 서로 다른 경로로 연결하여 고가용성 확보 |
libsas 프레임워크
Linux 커널의 libsas(drivers/scsi/libsas/)는 SAS HBA 드라이버 개발을 위한 공용 프레임워크입니다. 토폴로지 탐색, Expander 관리, PHY 이벤트 처리를 담당합니다:
/* libsas가 LLD에 요구하는 콜백 — struct sas_ha_struct */
struct sas_ha_struct {
/* PHY 제어 */
int (*phy_enable)(struct asd_sas_phy *, int enable);
int (*phy_reset)(struct asd_sas_phy *, int hard_reset);
int (*phy_set_link_rate)(struct asd_sas_phy *,
struct sas_phy_linkrates *);
/* 태스크 실행 (SCSI 명령 전송) */
int (*lldd_execute_task)(struct sas_task *, gfp_t);
/* 디바이스 탐색 이벤트 */
void (*lldd_dev_found)(struct domain_device *);
void (*lldd_dev_gone)(struct domain_device *);
};
# SAS 토폴로지 확인
$ lsscsi -t
[0:0:0:0] disk sas:0x5000c50098765432 /dev/sda
# SAS PHY 속성
$ cat /sys/class/sas_phy/phy-0:0/negotiated_linkrate
12.0 Gbit
# Expander 정보
$ cat /sys/class/sas_expander/expander-0:0/product_id
$ smp_discover /dev/bsg/expander-0:0
# SAS 주소 확인
$ cat /sys/class/sas_device/end_device-0:0:0/sas_address
0x5000c50098765432
SCSI Reservation
SCSI Reservation은 여러 Initiator가 같은 LUN에 접근하는 공유 스토리지 환경에서 배타적 또는 공유 접근을 제어하는 메커니즘입니다. 레거시 RESERVE/RELEASE에서 발전한 Persistent Reservation (PR)이 현대 클러스터의 표준입니다.
Reservation 유형 비교
| 구분 | 레거시 RESERVE/RELEASE | Persistent Reservation (PR) |
|---|---|---|
| 명령 | RESERVE(6/10), RELEASE(6/10) | PERSISTENT RESERVE IN/OUT (0x5E/0x5F) |
| Initiator 식별 | I_T Nexus (포트 기반) | Registration Key (소프트웨어 기반) |
| 리셋 후 유지 | 소멸 | 유지 (Persistent) |
| 공유 접근 | 불가 (배타적만) | Write Exclusive, Exclusive Access 등 다양한 모드 |
| 클러스터 지원 | 미약 | 펜싱, 다중 등록, Preempt & Abort |
PR 타입과 활용
| PR 타입 | 코드 | 설명 | 사용 사례 |
|---|---|---|---|
| Write Exclusive | 0x01 | 등록된 한 Initiator만 쓰기 가능 | 단일 마스터 쓰기 |
| Exclusive Access | 0x03 | 등록된 한 Initiator만 읽기/쓰기 가능 | 배타적 단일 접근 |
| Write Exclusive, Registrants Only | 0x05 | 등록된 모든 Initiator가 쓰기 가능 | 클러스터 공유 쓰기 |
| Exclusive Access, Registrants Only | 0x06 | 등록된 Initiator만 접근 가능 | 클러스터 배타적 접근 |
| Write Exclusive, All Registrants | 0x07 | 모든 등록자가 쓰기 가능, 예약 보유자 개념 없음 | 다중 등록자 쓰기 |
| Exclusive Access, All Registrants | 0x08 | 모든 등록자만 접근 가능 | 다중 등록자 배타적 |
클러스터 펜싱 활용
HA 클러스터(Pacemaker, VCS 등)는 SCSI PR을 스토리지 기반 펜싱에 활용합니다. 장애 노드를 공유 스토리지에서 격리(Isolation)하여 split-brain을 방지합니다:
# sg_persist를 이용한 PR 조작 (sg3_utils)
# 1. Registration Key 등록
$ sg_persist --out --register --param-sark=0xABCD0001 /dev/sda
# 2. 등록된 키 확인
$ sg_persist --in --read-keys /dev/sda
PR generation=1, 2 registered reservation key(s):
0xabcd0001
0xabcd0002
# 3. Reservation 획득 (Write Exclusive, Registrants Only)
$ sg_persist --out --reserve --param-rk=0xABCD0001 --prout-type=5 /dev/sda
# 4. 현재 Reservation 확인
$ sg_persist --in --read-reservation /dev/sda
# 5. 장애 노드 강제 추방 (Preempt & Abort)
$ sg_persist --out --preempt-abort --param-rk=0xABCD0001 \
--param-sark=0xABCD0002 --prout-type=5 /dev/sda
# Pacemaker에서 SCSI PR 펜싱 설정
# stonith:fence_scsi 리소스 사용
pr_preempt은 기존 예약을 즉시 무효화(Invalidation)하므로, 프로덕션에서는 반드시 정확한 키를 지정해야 합니다.Sense Data 해석
SCSI 디바이스가 CHECK CONDITION 상태를 반환하면, Sense Data를 통해 오류의 상세 원인을 제공합니다. Sense Data는 Sense Key, ASC(Additional Sense Code), ASCQ(Additional Sense Code Qualifier)의 3단계 계층으로 오류를 분류합니다.
자주 발생하는 ASC/ASCQ 코드
| Sense Key | ASC | ASCQ | 의미 | 일반적 원인 / 대응 |
|---|---|---|---|---|
| 0x02 (NOT READY) | 0x04 | 0x00 | Logical Unit Not Ready, Cause Not Reportable | 디바이스 초기화 중, 잠시 대기 후 재시도 |
| 0x02 | 0x04 | 0x01 | Becoming Ready | 스핀업 중, START STOP UNIT 명령 대기 |
| 0x02 | 0x04 | 0x02 | Initializing Command Required | FORMAT UNIT 또는 START 명령 필요 |
| 0x03 (MEDIUM ERROR) | 0x11 | 0x00 | Unrecovered Read Error | 불량 섹터, 디스크 교체 검토 |
| 0x03 | 0x11 | 0x04 | Unrecovered Read Error - Auto Reallocate Failed | 자동 재할당 실패, 즉시 백업 |
| 0x03 | 0x0C | 0x00 | Write Error | 쓰기 실패, 매체 손상 |
| 0x04 (HARDWARE ERROR) | 0x09 | 0x00 | Track Following Error | 헤드 위치 오류, HDD 교체 |
| 0x04 | 0x44 | 0x00 | Internal Target Failure | 컨트롤러 내부 오류 |
| 0x05 (ILLEGAL REQUEST) | 0x20 | 0x00 | Invalid Command Operation Code | 미지원 명령, CDB 확인 |
| 0x05 | 0x24 | 0x00 | Invalid Field in CDB | 잘못된 파라미터, CDB 필드 검증 |
| 0x06 (UNIT ATTENTION) | 0x28 | 0x00 | Not Ready to Ready Transition | 매체 교체, 자동 재시도 |
| 0x06 | 0x29 | 0x00 | Power On, Reset, or Bus Device Reset | 리셋 후 첫 명령, 자동 재시도 |
| 0x0B (ABORTED COMMAND) | 0x47 | 0x00 | SCSI Parity Error | 케이블/신호 문제, 하드웨어 점검 |
| 0x0B | 0x4E | 0x00 | Overlapped Commands | 태그 충돌, 큐잉 설정 확인 |
# Sense Data 직접 조회
$ sg_requests -d /dev/sda
Sense Key: Medium Error [0x3]
ASC/ASCQ: 0x11/0x00 — Unrecovered Read Error
# 커널 로그에서 Sense Data 확인
$ dmesg | grep -A5 "sense key"
sd 0:0:0:0: [sda] Sense Key : Medium Error [current]
sd 0:0:0:0: [sda] Add. Sense: Unrecovered read error
# SCSI 로그에서 Sense Data 디코딩
$ sg_decode_sense --hex "70 00 03 00 00 00 00 0a 00 00 00 00 11 00"
scsi_debug 가상 장치
scsi_debug는 실제 하드웨어 없이 SCSI 디바이스를 에뮬레이션하는 커널 모듈입니다. SCSI 스택 테스트, 에러 주입, 성능 벤치마크, 드라이버 개발 시 필수적인 도구입니다.
주요 파라미터
| 파라미터 | 기본값 | 설명 |
|---|---|---|
dev_size_mb | 8 | 가상 디바이스 크기 (MB) |
num_tgts | 1 | 타겟 수 |
max_luns | 1 | 타겟당 최대 LUN 수 |
sector_size | 512 | 섹터 크기 (512 또는 4096) |
ndelay | 0 | 명령 완료 지연 (나노초) |
every_nth | 0 | N번째 명령마다 에러 발생 |
opts | 0 | 동작 옵션 비트마스크 |
medium_error_start | -1 | 매체 오류를 발생시킬 시작 LBA |
medium_error_count | 1 | 매체 오류 블록 수 |
submit_queues | 1 | blk-mq 하드웨어 큐 수 |
zbc | none | Zoned Block 에뮬레이션 (managed/host-aware) |
테스트 시나리오
# 기본: 256MB 가상 디스크 생성
$ modprobe scsi_debug dev_size_mb=256 num_tgts=1 max_luns=2
$ lsscsi | grep scsi_debug
# 4K 섹터 디바이스 에뮬레이션
$ modprobe scsi_debug sector_size=4096 physblk_exp=3 dev_size_mb=512
# 에러 주입: 100번째 명령마다 CHECK CONDITION
$ modprobe scsi_debug every_nth=100 opts=1 dev_size_mb=128
# 특정 LBA 범위에서 매체 오류 발생
$ modprobe scsi_debug medium_error_start=1000 medium_error_count=5 dev_size_mb=128
$ dd if=/dev/sdX bs=512 skip=1000 count=1 of=/dev/null # Medium Error 발생
# 명령 타임아웃 시뮬레이션
$ modprobe scsi_debug opts=0x20 every_nth=50 # 50번째마다 타임아웃
# 지연 시뮬레이션 (1ms 지연)
$ modprobe scsi_debug ndelay=1000000 dev_size_mb=128
# 멀티 큐 테스트 (4 hw queues)
$ modprobe scsi_debug submit_queues=4 dev_size_mb=256
# 런타임 파라미터 변경
$ echo 50 > /sys/bus/pseudo/drivers/scsi_debug/every_nth
$ echo 0 > /sys/bus/pseudo/drivers/scsi_debug/every_nth # 에러 주입 중지
# 정리
$ rmmod scsi_debug
scsi_debug는 CI/CD 파이프라인(Pipeline)에서 SCSI 스택 관련 테스트를 자동화하는 데 이상적입니다. opts 비트마스크를 조합하면 다양한 장애 시나리오를 재현할 수 있습니다.sysfs 인터페이스
기본적인 sysfs 경로는 앞 절에서 다루었습니다. 이 절에서는 /sys/class/scsi_host, /sys/class/scsi_device의 주요 속성을 상세히 분석하고, 운영 중 활용 가능한 제어 인터페이스를 정리합니다.
scsi_host 주요 속성
| 속성 | 읽기/쓰기 | 설명 |
|---|---|---|
scan | W | 디바이스 재스캔. "- - -"(와일드카드) 또는 "C T L" 형식 |
state | R/W | 호스트 상태: running, recovery, cancel, del |
proc_name | R | HBA 드라이버 이름 |
can_queue | R | 호스트 최대 outstanding 명령 수 |
cmd_per_lun | R | LUN당 기본 큐 깊이 |
sg_tablesize | R | scatter/gather 세그먼트 최대 수 |
eh_deadline | R/W | EH 데드라인 (초). 0=무제한 |
host_busy | R | 현재 실행 중인 명령 수 |
active_mode | R | Initiator/Target 모드 |
nr_hw_queues | R | 하드웨어 큐 수 (blk-mq) |
scsi_device 주요 속성
| 속성 | 읽기/쓰기 | 설명 |
|---|---|---|
state | R/W | 디바이스 상태: running, cancel, del, quiesce, offline, transport-offline, blocked |
queue_depth | R/W | LUN 큐 깊이 |
queue_type | R/W | 큐 타입: none, simple, ordered |
timeout | R/W | 명령 타임아웃 (초) |
delete | W | 1 기록 시 디바이스 안전 제거 |
rescan | W | 1 기록 시 디바이스 재스캔 |
vendor | R | 제조사 식별 문자열 |
model | R | 제품 모델명 |
rev | R | 펌웨어(Firmware) 리비전 |
type | R | SCSI 디바이스 타입 (0=disk, 1=tape, 5=cdrom ...) |
scsi_level | R | SCSI 규격 버전 |
max_sectors | R/W | 단일 요청 최대 섹터 수 |
# 디바이스 전체 속성 탐색
$ ls /sys/class/scsi_device/0:0:0:0/device/
# 디바이스를 quiesce 모드로 전환 (I/O 일시 중지)
$ echo "quiesce" > /sys/class/scsi_device/0:0:0:0/device/state
# 다시 running으로 복귀
$ echo "running" > /sys/class/scsi_device/0:0:0:0/device/state
# 전송 클래스별 속성 확인
$ ls /sys/class/sas_phy/phy-*
$ ls /sys/class/fc_host/host*
$ ls /sys/class/iscsi_session/session*
SCSI 성능 카운터와 통계
SCSI 서브시스템의 성능 분석은 /proc/scsi, scsi_logging_level, blktrace/ftrace를 조합하여 수행합니다. 이 절에서는 각 도구의 활용법과 성능 병목 식별 방법을 다룹니다.
scsi_logging_level 비트마스크
| 비트 | 값 | 카테고리 | 설명 |
|---|---|---|---|
| 0 | 0x1 | SCSI_LOG_ERROR | 오류 로그 |
| 1 | 0x2 | SCSI_LOG_TIMEOUT | 타임아웃 이벤트 |
| 2 | 0x4 | SCSI_LOG_SCAN | 디바이스 스캔 |
| 3 | 0x8 | SCSI_LOG_MLQUEUE | Mid Layer 큐잉 (제출) |
| 4 | 0x10 | SCSI_LOG_MLCOMPLETE | Mid Layer 완료 |
| 5 | 0x20 | SCSI_LOG_LLQUEUE | Low Level 큐잉 (HBA 전달) |
| 6 | 0x40 | SCSI_LOG_LLCOMPLETE | Low Level 완료 |
| 7 | 0x80 | SCSI_LOG_HLQUEUE | High Level 큐잉 (sd/sr) |
| 8 | 0x100 | SCSI_LOG_HLCOMPLETE | High Level 완료 |
blktrace 연동
# blktrace로 SCSI I/O 추적
$ blktrace -d /dev/sda -o trace &
$ fio --name=test --filename=/dev/sda --rw=randread --bs=4k --numjobs=4 --time_based --runtime=10
$ kill %1
# 추적 결과 분석
$ blkparse -i trace.blktrace.* -d trace.bin
$ btt -i trace.bin
# D2C (dispatch-to-complete) 지연 — SCSI 명령 실행 시간
$ btt -i trace.bin | grep D2C
D2C Q2C Avg Min Max N
sda 0.000234 0.000098 0.001234 52340
# ftrace로 SCSI 함수 추적
$ echo "scsi_dispatch_cmd" > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo function > /sys/kernel/debug/tracing/current_tracer
$ cat /sys/kernel/debug/tracing/trace_pipe
# SCSI 트레이스포인트 사용
$ perf trace -e "scsi:*" -- fio --name=test --filename=/dev/sda --rw=read --bs=4k --size=1M
# 디바이스 수준 에러 통계 (SCSI 로그 페이지)
$ sg_logs -p 0x03 /dev/sda # Read Error Counter
$ sg_logs -p 0x05 /dev/sda # Write Error Counter
# iostat으로 SCSI 디바이스 통계
$ iostat -x sda 1
Device r/s w/s rkB/s wkB/s await svctm %util
sda 12500 3200 50000 12800 0.23 0.06 75.2
Multipath I/O
Multipath I/O는 동일한 스토리지 LUN에 여러 물리적 경로를 통해 접근하는 기술입니다. 경로 장애 시 자동 failover를 제공하고, 정상 시에는 부하 분산(Load Balancing)으로 성능을 향상시킵니다. Linux에서는 dm-mpath(Device Mapper Multipath)가 이를 구현합니다.
핵심 개념
| 개념 | 설명 |
|---|---|
| dm-mpath | Device Mapper Multipath. 여러 SCSI 경로를 하나의 블록 디바이스로 통합 |
| Path Group | 같은 우선순위(Priority)의 경로 묶음. Active/Standby로 구분 |
| Path Selector | I/O를 어느 경로로 보낼지 결정하는 알고리즘 |
| ALUA | Asymmetric Logical Unit Access. 스토리지 컨트롤러별 경로 최적화 상태 보고 |
| Failover | 장애 경로에서 정상 경로로 자동 전환 |
| Failback | 장애 복구 후 원래 경로로 자동 복귀 |
| multipathd | 유저 스페이스 데몬. 경로 모니터링, 설정 관리 |
Path Selector 알고리즘
| Selector | 알고리즘 | 적합한 워크로드 |
|---|---|---|
round-robin 0 | 순환 방식, 모든 경로에 균등 분배 | 동일 지연의 경로들 |
queue-length 0 | 큐 길이가 가장 짧은 경로 선택 | 비대칭 부하 |
service-time 0 | 예상 서비스 시간이 가장 짧은 경로 선택 | 이기종 경로 혼합 |
ALUA 우선순위
ALUA(Asymmetric Logical Unit Access)는 스토리지 어레이가 각 컨트롤러의 LUN 접근 상태를 Initiator에 보고하는 메커니즘입니다. multipathd는 ALUA 상태에 따라 경로 우선순위를 자동 설정합니다:
| ALUA 상태 | 값 | 의미 | 우선순위 |
|---|---|---|---|
| Active/Optimized | 0x0 | 최적 경로, 지연 최소 | 최고 (50) |
| Active/Non-Optimized | 0x1 | 접근 가능하지만 비최적 | 중간 (10) |
| Standby | 0x2 | 대기 상태, 활성화 필요 | 낮음 (1) |
| Unavailable | 0x3 | 접근 불가 | 0 |
| Transitioning | 0xF | 상태 전환 중 | 0 |
# multipath 설치 및 활성화
$ apt install multipath-tools # Debian/Ubuntu
$ yum install device-mapper-multipath # RHEL/CentOS
$ systemctl enable --now multipathd
# 기본 설정 생성
$ mpathconf --enable
# multipath 상태 확인
$ multipath -ll
mpath0 (360000000000000001) dm-0 VENDOR,PRODUCT
size=100G features='1 queue_if_no_path' hwhandler='1 alua' wp=rw
|-+- policy='service-time 0' prio=50 status=active
| |- 0:0:0:0 sda 8:0 active ready running
| `- 1:0:0:0 sdb 8:16 active ready running
`-+- policy='service-time 0' prio=10 status=enabled
|- 0:0:1:0 sdc 8:32 active ready running
`- 1:0:1:0 sdd 8:48 active ready running
# 경로 장애 시뮬레이션
$ echo offline > /sys/block/sda/device/state
$ multipath -ll # sda가 faulty로 변경됨을 확인
# 경로 복구
$ echo running > /sys/block/sda/device/state
$ multipathd reconfigure
/etc/multipath.conf에서 벤더별 최적 설정을 적용하세요.Zoned Block 장치
Zoned Block 장치는 디스크 영역을 Zone 단위로 나누어 순차 쓰기를 강제하는 스토리지 모델입니다. SMR(Shingled Magnetic Recording) HDD가 대표적이며, SCSI에서는 ZBC(Zoned Block Commands), ATA에서는 ZAC(Zoned-device ATA Commands)로 표준화되어 있습니다.
ZBC/ZAC 핵심 개념
| 개념 | 설명 |
|---|---|
| Zone | 디스크 공간의 고정 크기 영역 (일반적으로 256MB) |
| Write Pointer (WP) | Zone 내 다음 쓰기 위치. 순차 Zone은 WP 위치에만 쓰기 가능 |
| Conventional Zone | 기존 디스크처럼 랜덤 쓰기 가능한 Zone (CMR 영역) |
| Sequential Write Required | 반드시 WP에서 순차적으로 써야 하는 Zone |
| Sequential Write Preferred | 순차 쓰기를 권장하지만 랜덤 쓰기도 허용 (성능 저하 가능) |
| Host-Managed | 디바이스가 Zone 규칙을 엄격히 적용, 위반 시 에러 반환 |
| Host-Aware | Zone 규칙을 권장하지만, 위반을 허용 (호환 모드) |
ZBC SCSI 명령
| 명령 | Opcode | 설명 |
|---|---|---|
| REPORT ZONES | 0x95 | Zone 목록과 상태 조회 |
| OPEN ZONE | 0x94 (SA=0x03) | Zone을 명시적으로 오픈 |
| CLOSE ZONE | 0x94 (SA=0x01) | Zone 닫기 (리소스 해제) |
| FINISH ZONE | 0x94 (SA=0x02) | Zone을 FULL로 전환 |
| RESET WRITE POINTER | 0x94 (SA=0x04) | Zone을 EMPTY로 리셋 (데이터 삭제) |
Linux 커널의 Zoned Block 지원
Linux 커널 4.10부터 ZBC/ZAC 디바이스를 네이티브로 지원합니다. sd 드라이버가 Zone 관리를 수행하고, 블록 계층이 쓰기 순서를 보장합니다:
# Zoned Block 디바이스 확인
$ cat /sys/block/sda/queue/zoned
host-managed
# Zone 크기 확인
$ cat /sys/block/sda/queue/chunk_sectors
524288 # 256MB (512바이트 섹터 기준)
# Zone 상태 조회 (sg3_utils)
$ sg_rep_zones /dev/sda
Report zones response:
Zone type: Sequential write required, Zone cond: Empty, Start: 0x0, Len: 0x80000, WP: 0x0
Zone type: Sequential write required, Zone cond: Full, Start: 0x80000, Len: 0x80000, WP: 0x100000
# blkzone 도구로 Zone 관리
$ blkzone report /dev/sda
$ blkzone reset -o 0 -l 1 /dev/sda # 첫 번째 Zone 리셋
# scsi_debug로 Zoned 디바이스 에뮬레이션
$ modprobe scsi_debug zbc=managed dev_size_mb=1024 zone_nr_conv=4
$ cat /sys/block/sdX/queue/zoned # host-managed
Zoned Block 대응 파일시스템
| 파일시스템 | ZBC 지원 | 특징 |
|---|---|---|
| Btrfs | 5.12+ | Zoned 모드 네이티브 지원, 순차 할당 보장 |
| f2fs | 5.10+ | LFS 기반 순차 쓰기에 자연 적합 |
| zonefs | 5.6+ | Zone을 파일로 1:1 매핑하는 특수 파일시스템 |
| dm-zoned | 4.13+ | Device Mapper로 Conventional Zone을 버퍼(Buffer)로 사용하여 랜덤 쓰기 지원 |
dm-zoned를 중간 계층으로 사용하거나, Zoned 인식 파일시스템을 사용해야 합니다.iSCSI HBA 오프로드
iSCSI HBA 오프로드는 iSCSI 프로토콜 처리의 일부 또는 전부를 전용 하드웨어로 이관하여 호스트 CPU 부하를 줄이는 기술입니다. 소프트웨어 iSCSI(open-iscsi/iscsid)가 모든 처리를 커널 TCP 스택과 사용자 공간 데몬에 의존하는 반면, HBA 오프로드는 TCP 처리부터 iSCSI PDU 조립까지 하드웨어에서 수행합니다.
소프트웨어 iSCSI vs HBA 오프로드
소프트웨어 iSCSI는 open-iscsi 패키지의 iscsid 데몬과 커널 모듈(iscsi_tcp, libiscsi)로 구현됩니다. 네트워크 카드(NIC)는 일반 이더넷 프레임만 처리하고, TCP/IP 스택과 iSCSI 계층은 모두 호스트 CPU가 담당합니다. 반면 HBA 오프로드는 전용 ASIC이 TCP 세그먼트 조립, iSCSI PDU 파싱, 데이터 DMA 전송을 하드웨어에서 처리합니다.
| 유형 | TCP 처리 | iSCSI PDU | 데이터 전송 | CPU 부하 |
|---|---|---|---|---|
| SW iSCSI | 커널 TCP 스택 | iscsid / iscsi_tcp | CPU bounce buffer | 높음 |
| Partial HW (TOE) | HW TCP (TOE) | 커널 iSCSI | HW DMA | 중간 |
| Full HW iSCSI | HW TCP | HW iSCSI 엔진 | HW DMA (zero-copy) | 낮음 |
주요 iSCSI 오프로드 드라이버
리눅스 커널은 다양한 iSCSI HBA 오프로드 드라이버를 포함합니다:
| 드라이버 | 벤더/칩셋 | 오프로드 수준 | 특징 |
|---|---|---|---|
| cxgb4i | Chelsio T6 | Full HW (TOE + iSCSI) | TCP Offload Engine 기반, DDP(Direct Data Placement), 10/25/40/100GbE |
| qla4xxx | QLogic/Marvell | Full HW | iSCSI/FCoE 통합 HBA, 하드웨어 iSCSI 엔진, Boot from iSCSI |
| bnx2i | Broadcom NetXtreme II | Partial (TOE) | TOE 기반 iSCSI 오프로드, CNIC 계층 활용 |
| be2iscsi | Emulex/Broadcom BE | Full HW | BladeEngine 기반 iSCSI 오프로드, MCC ring |
TOE 기반 iSCSI 오프로드
TOE(TCP Offload Engine)는 TCP/IP 프로토콜 스택 전체를 NIC 하드웨어로 이관합니다. Chelsio T6 어댑터의 cxgb4i 드라이버는 TOE 위에 iSCSI 오프로드를 구현하여, TCP 연결 관리부터 iSCSI PDU 처리까지 하드웨어에서 수행합니다. DDP(Direct Data Placement)는 수신 데이터를 중간 버퍼 없이 최종 목적지 메모리에 직접 배치하여 CPU 개입과 메모리 복사를 제거합니다.
# Chelsio cxgb4i 드라이버 로드
$ modprobe cxgb4i
# iSCSI HBA 오프로드 인터페이스 확인
$ iscsiadm -m iface
# cxgb4i.00:0a:00.4 tcp,cxgb4i,...
# 오프로드 인터페이스로 타겟 검색
$ iscsiadm -m discovery -t sendtargets -p 192.168.1.100:3260 -I cxgb4i.00:0a:00.4
# 세션 로그인 (HW 오프로드 사용)
$ iscsiadm -m node -T iqn.2024.com.example:storage -p 192.168.1.100:3260 -I cxgb4i.00:0a:00.4 -l
# 오프로드 상태 확인
$ iscsiadm -m session -P 3 | grep -i offload
iSER — iSCSI Extensions for RDMA
iSER(iSCSI Extensions for RDMA)은 RDMA(Remote Direct Memory Access) 기반으로 iSCSI 데이터를 전송하는 확장 프로토콜입니다. InfiniBand, RoCE(RDMA over Converged Ethernet), iWARP를 통해 zero-copy 데이터 전송을 실현하며, TCP 오버헤드를 완전히 제거합니다. 커널에서는 ib_iser 모듈이 initiator를, LIO 타겟의 iser-target 모듈이 target을 담당합니다.
# iSER initiator 모듈 로드
$ modprobe ib_iser
# RDMA 인터페이스로 iSCSI 검색
$ iscsiadm -m discovery -t sendtargets -p 192.168.1.100:3260 -I iser
# iSER 세션 로그인
$ iscsiadm -m node -T iqn.2024.com.example:target -p 192.168.1.100:3260 -I iser -l
커널 소스 분석: iscsi_host_template 구조체
/* drivers/scsi/libiscsi.c — iSCSI 호스트 템플릿 핵심 필드 */
struct iscsi_transport cxgb4i_iscsi_transport = {
.owner = THIS_MODULE,
.name = "cxgb4i",
.caps = CAP_RECOVERY_L0 | CAP_MULTI_R2T |
CAP_HDRDGST | CAP_DATADGST |
CAP_DIGEST_OFFLOAD |
CAP_TEXT_NEGO,
.create_session = cxgb4i_session_create,
.destroy_session = iscsi_session_teardown,
.create_conn = cxgb4i_conn_create,
.bind_conn = cxgb4i_conn_bind,
.send_pdu = iscsi_conn_send_pdu,
.xmit_task = iscsi_tcp_task_xmit,
.get_ep_param = cxgb4i_ep_get_param,
.ep_connect = cxgb4i_ep_connect, /* HW TCP 연결 수립 */
.ep_disconnect = cxgb4i_ep_disconnect,
};
코드 설명
-
핵심
iscsi_transport구조체는 iSCSI 전송 드라이버의 인터페이스를 정의합니다.caps필드의CAP_DIGEST_OFFLOAD는 CRC 다이제스트 계산을 하드웨어에서 수행함을 나타냅니다.ep_connect와ep_disconnect는 TOE 기반 TCP 연결을 하드웨어에서 관리하며, 소프트웨어 iSCSI(iscsi_tcp)는 이 콜백 대신 커널 소켓 API를 사용합니다.
SCSI XCOPY와 SMB ODX
SCSI Extended Copy(XCOPY)는 SPC-4 표준에 정의된 third-party copy 메커니즘으로, 데이터를 호스트 CPU를 거치지 않고 스토리지 장치 간에 직접 복사합니다. 기존 복사 방식이 소스에서 호스트 메모리로 읽고 다시 대상에 쓰는 2단계 과정을 거치는 반면, XCOPY는 스토리지 컨트롤러가 직접 소스 LUN에서 대상 LUN으로 데이터를 전송합니다.
XCOPY 동작 메커니즘
XCOPY 명령(Opcode 0x83)은 호스트가 복사 관리자(Copy Manager) 역할의 디바이스에 전송합니다. 명령에는 소스 디스크립터, 대상 디스크립터, 복사할 블록 범위가 포함됩니다. 복사 관리자는 소스에서 데이터를 읽어 대상에 직접 쓰며, 호스트는 완료 상태만 수신합니다.
Token 기반 복사: POPULATE TOKEN / WRITE USING TOKEN
SPC-4에서 추가된 Token 기반 복사는 XCOPY의 확장으로, 복사 작업을 2단계로 분리합니다:
- POPULATE TOKEN: 소스 디바이스에서 지정된 블록 범위의 데이터를 나타내는 ROD(Representation of Data) 토큰을 생성합니다.
- WRITE USING TOKEN: 생성된 토큰을 사용하여 대상 디바이스에 데이터를 씁니다.
이 방식은 소스와 대상이 서로 다른 스토리지 어레이에 있어도 동작할 수 있으며, 토큰이 유효한 동안 여러 대상에 반복 쓰기가 가능합니다.
SMB ODX (Offloaded Data Transfer)
SMB ODX는 Windows Server의 SMB 3.0+ 프로토콜에서 SCSI Token 기반 복사를 활용하는 기능입니다. 파일 복사 시 데이터가 클라이언트를 경유하지 않고 서버 측 스토리지에서 직접 처리됩니다. Linux에서는 LIO 타겟이 XCOPY 및 Token 기반 복사를 지원하여, Samba를 통한 ODX 호환 파일 서버 구성이 가능합니다.
# LIO 타겟에서 XCOPY 지원 확인
$ cat /sys/kernel/config/target/core/iblock_0/lun0/attrib/emulate_3pc
1 # 1 = Third-Party Copy 활성화
# Token 기반 복사 최대 크기 설정
$ echo 65536 > /sys/kernel/config/target/core/iblock_0/lun0/attrib/max_write_same_len
# sg3_utils로 XCOPY 수동 테스트
$ sg_xcopy --src=/dev/sda --dst=/dev/sdb --count=10000
커널 소스 분석: target_xcopy_do_work() 핵심 경로
/* drivers/target/target_core_xcopy.c — XCOPY 핵심 처리 경로 */
static void target_xcopy_do_work(struct work_struct *work)
{
struct xcopy_op *xop = container_of(work, struct xcopy_op, xop_work);
struct se_cmd *ec_cmd = xop->xop_se_cmd; /* Extended Copy 명령 */
sector_t src_lba = xop->src_lba;
sector_t dst_lba = xop->dst_lba;
unsigned int nolb = xop->nolb; /* Number of Logical Blocks */
unsigned int max_sectors;
int rc;
/* 한 번에 복사할 최대 블록 수 결정 */
max_sectors = min(xop->src_dev->dev_attrib.max_bytes_per_io,
xop->dst_dev->dev_attrib.max_bytes_per_io)
/ xop->src_dev->dev_attrib.block_size;
while (nolb > 0) {
unsigned int cur_nolb = min(nolb, max_sectors);
/* 1단계: 소스에서 읽기 */
rc = target_xcopy_read_source(ec_cmd, xop,
src_lba, cur_nolb);
if (rc < 0) break;
/* 2단계: 대상에 쓰기 */
rc = target_xcopy_write_destination(ec_cmd, xop,
dst_lba, cur_nolb);
if (rc < 0) break;
src_lba += cur_nolb;
dst_lba += cur_nolb;
nolb -= cur_nolb;
}
/* 완료 상태를 호스트에 보고 */
target_complete_cmd(ec_cmd, rc ? SAM_STAT_CHECK_CONDITION
: SAM_STAT_GOOD);
}
코드 설명
-
핵심
target_xcopy_do_work()는 LIO 타겟에서 XCOPY 명령을 처리하는 워커 함수입니다. 소스 LBA에서 블록을 읽고(target_xcopy_read_source) 대상 LBA에 쓰는(target_xcopy_write_destination) 루프를 반복합니다.max_sectors로 한 번에 처리할 블록 수를 제한하여 메모리 사용량을 조절합니다. 전체 복사가 완료되면 원래 XCOPY 명령에 대한 완료 상태를 호스트에 보고합니다.
Fibre Channel HBA
Fibre Channel(FC)은 SAN(Storage Area Network) 환경에서 가장 널리 사용되는 고속 스토리지 전송 프로토콜입니다. FC HBA(Host Bus Adapter)는 서버를 FC 스위치 패브릭에 연결하여 블록 수준의 스토리지 접근을 제공합니다. 현재 16/32/64 Gbps 속도를 지원하며, 낮은 지연 시간과 무손실 전송 특성으로 엔터프라이즈 스토리지의 핵심 인프라입니다.
주요 FC HBA 드라이버
| 드라이버 | 벤더 | 속도 | 특징 |
|---|---|---|---|
| lpfc | Emulex / Broadcom | 8/16/32/64 Gbps | FC, FCoE, FC-NVMe 지원. scsi_transport_fc 기반, NPIV, SR-IOV |
| qla2xxx | QLogic / Marvell | 8/16/32/64 Gbps | 가장 폭넓은 FC 호환성. 멀티큐, Target 모드(qla2x00t), FC-NVMe |
| zfcp | IBM System z (s390) | FICON/FC | 메인프레임용 FC 어댑터, QDIO 기반 I/O |
| bfa | Brocade / Broadcom | 8/16 Gbps | Brocade HBA, CEE/FCoE, BFA 펌웨어 계층 |
FC 아키텍처
FC는 패브릭(Fabric) 토폴로지를 기반으로, 각 디바이스가 FC 스위치를 통해 연결됩니다. 각 포트는 고유한 WWPN(World Wide Port Name)과 WWNN(World Wide Node Name)으로 식별되며, 패브릭 로그인 시 동적으로 N_Port ID가 할당됩니다.
# FC HBA 목록 확인
$ ls /sys/class/fc_host/
host0 host1
# HBA 포트 상태 및 속도
$ cat /sys/class/fc_host/host0/port_state
Online
$ cat /sys/class/fc_host/host0/speed
32 Gbit
# WWPN / WWNN 확인
$ cat /sys/class/fc_host/host0/port_name
0x2100001b329a5f00
$ cat /sys/class/fc_host/host0/node_name
0x2000001b329a5f00
# 패브릭에 연결된 원격 포트 (타겟) 확인
$ ls /sys/class/fc_remote_ports/
rport-0:0-0 rport-0:0-1
# 원격 포트 정보
$ cat /sys/class/fc_remote_ports/rport-0:0-0/port_name
0x5000c5000a1b2c3d
$ cat /sys/class/fc_remote_ports/rport-0:0-0/roles
FCP Target
# LIP (Loop Initialization Primitive) — 패브릭 재스캔
$ echo 1 > /sys/class/fc_host/host0/issue_lip
NPIV (N_Port ID Virtualization)
NPIV는 하나의 물리 FC 포트에 여러 가상 N_Port ID를 할당하여, 가상 머신마다 독립된 FC 포트를 제공하는 기술입니다. 하이퍼바이저(KVM, VMware)에서 VM에 FC LUN을 직접 매핑할 때 필수적입니다. 각 가상 포트는 고유한 WWPN을 가지므로, 스토리지 측에서 VM별 LUN 마스킹과 조닝이 가능합니다.
# NPIV 지원 확인
$ cat /sys/class/fc_host/host0/npiv_vports_inuse
0
$ cat /sys/class/fc_host/host0/max_npiv_vports
254
# 가상 포트 생성 (WWPN:WWNN)
$ echo "2100001b329a5f10:2000001b329a5f10" > /sys/class/fc_host/host0/vport_create
# 생성된 가상 포트 확인
$ ls /sys/class/fc_host/
host0 host1 host2 # host2가 NPIV 가상 포트
FC-NVMe: NVMe over Fibre Channel
FC-NVMe는 기존 FC 패브릭 인프라 위에서 NVMe 프로토콜을 전송하는 기술입니다. SCSI 대신 NVMe 명령을 FC 프레임에 캡슐화하여, FC SAN의 안정성과 NVMe의 낮은 지연 시간을 결합합니다. Linux 커널의 nvme-fc 모듈이 initiator를, nvmet-fc 모듈이 target을 담당합니다.
# FC-NVMe initiator 모듈
$ modprobe nvme-fc
# lpfc 드라이버에서 FC-NVMe 활성화
$ modprobe lpfc lpfc_enable_fc4_type=3 # 3 = FCP + NVMe
# FC-NVMe 서브시스템 검색
$ nvme discover -t fc -a traddr:nn-0x2000001b329a5f00:pn-0x2100001b329a5f00 \
-w traddr:nn-0x2000001b329a6f00:pn-0x2100001b329a6f00
FC vs iSCSI vs NVMe-oF 비교
| 항목 | Fibre Channel | iSCSI | NVMe-oF (RDMA/TCP) |
|---|---|---|---|
| 전송 매체 | FC 광섬유 / 구리 | 이더넷 (TCP/IP) | 이더넷 / InfiniBand / FC |
| 최대 속도 | 64 Gbps (Gen7) | 100 Gbps (이더넷) | 400 Gbps (이더넷) |
| 지연 시간 | ~5 μs | ~50-100 μs (SW), ~10 μs (HW) | ~2-10 μs (RDMA) |
| CPU 부하 | 매우 낮음 (HW) | 높음 (SW) / 낮음 (HW) | 매우 낮음 (RDMA) |
| 인프라 비용 | 높음 (전용 스위치) | 낮음 (기존 이더넷) | 중간 (RDMA NIC 필요) |
| 프로토콜 오버헤드 | 낮음 (경량 FC 프레임) | 높음 (TCP/IP 스택) | 매우 낮음 (NVMe 네이티브) |
| 멀티패스 | dm-multipath, ALUA | dm-multipath, ALUA | nvme multipath (in-kernel) |
| 적합 환경 | 대규모 SAN, 미션 크리티컬 | 소규모~중규모, NAS/SAN 통합 | 고성능 올플래시, AI/ML |
외부 참고 자료
- 커널 공식 SCSI 서브시스템 문서 — 리눅스 커널 SCSI 계층의 공식 문서입니다
- drivers/scsi/ (Bootlin Elixir) — SCSI 미들레이어 및 드라이버 소스 코드를 온라인에서 탐색할 수 있습니다
- include/scsi/ (Bootlin Elixir) — SCSI 공통 헤더 파일 및 프로토콜 상수 정의입니다
- LWN: The SCSI mid-layer (2012) — SCSI 미들레이어의 아키텍처와 드라이버 인터페이스를 설명하는 기사입니다
- LWN: Modernizing the SCSI mid-layer (2017) — blk-mq 기반으로 전환되는 SCSI 계층의 현대화 과정을 다룹니다
- T10 Technical Committee (INCITS) — SCSI 표준(SPC, SBC, SAM 등)을 관리하는 공식 기관입니다
- Open-iSCSI 프로젝트 — 리눅스 iSCSI 이니시에이터(iscsid, iscsiadm)의 공식 사이트입니다
- iscsiadm(8) man page — iSCSI 타겟 검색, 로그인, 세션 관리 명령어 레퍼런스입니다
- iscsid.conf(5) man page — iSCSI 데몬 설정 파일의 파라미터를 설명합니다
- SCSI Mid-Level/Low-Level API — 커널 SCSI 드라이버 작성을 위한 API 문서입니다
관련 문서
SCSI/iSCSI와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.