24. 저장 I/O 내구성: 페이지 캐시·write-back·fsync/fdatasync·O_DIRECT
난이도 상내 답안
모범답안
해설 — 저장 I/O 내구성: 페이지 캐시·fsync·O_DIRECT
난이도: 상
1) write() 이후 데이터의 여정
- 일반적인 버퍼드
write()는 데이터를 커널의 페이지 캐시에 복사하고 즉시 반환한다. 해당 페이지는 "더티(dirty)"로 표시되고, 커널의 write-back(플러셔) 스레드가 나중에(주기·더티 비율 임계에 따라) 디스크로 내려보낸다. 이것이 지연 기록(write-back) 이다. - 따라서
write()반환은 "커널이 데이터를 받았다"일 뿐 "디스크에 안전하게 남았다"가 아니다. 이 순간 크래시/정전이 나면 아직 내려가지 않은 더티 페이지는 유실된다. 내구성(durability)과 가시성(같은 파일을 다시 read 하면 보임)은 다르다 — 페이지 캐시 덕에 read 는 최신값을 보지만 디스크엔 아직 없을 수 있다.
2) fsync / fdatasync
- 내구성을 얻으려면
fsync(fd)를 호출해 해당 파일의 더티 페이지와 메타데이터가 저장장치에 영속화될 때까지 블록한다. fdatasync(fd)는 파일 데이터와 (그 데이터를 읽는 데 필요한) 최소 메타데이터만 플러시하고, 크기가 안 바뀌었다면mtime같은 부가 메타데이터 갱신은 생략한다. → fsync 보다 메타데이터 저널 쓰기가 적어 빠르지만, 파일이 커지는(크기 변경) 경우엔 그 메타데이터도 필요하다.- 디렉터리 fsync: 새 파일을 만들거나 rename 하면, 파일 데이터를 fsync 해도 그 파일의 디렉터리 엔트리가 아직 디스크에 없을 수 있다. 크래시 후 파일 내용은 있는데 디렉터리에서 안 보이는(또는 반대) 상황을 피하려면 부모 디렉터리도 fsync 해야 한다. "원자적 교체" 패턴(temp 파일에 쓰고 fsync → rename → 디렉터리 fsync)이 정석이다.
3) 디스크 쓰기 캐시와 배리어
- 저장장치(HDD/SSD)는 자체 휘발성 쓰기 캐시(DRAM) 를 갖고, 커널이 내려보낸 쓰기를 캐시에만 담고 "완료"라고 응답할 수 있다. 그러면 fsync 가 반환됐어도 정전 시 그 캐시 내용이 날아가 내구성이 깨진다.
- 방어책: (a) FLUSH/캐시 플러시 명령(fsync 가 파일시스템을 거쳐 장치 캐시까지 비우도록), (b) FUA(Force Unit Access) 로 특정 쓰기를 캐시 우회해 매체에 직접, (c) 하드웨어적으로 배터리/커패시터 백업 캐시(BBU/PLP, power-loss protection) 를 둬 정전 시에도 캐시를 매체로 내리게 한다. 엔터프라이즈 SSD/RAID 컨트롤러의 PLP 가 이 역할이다.
- 파일시스템의 write barrier 는 저널과 데이터의 순서를 강제해, 캐시 재정렬로 인해 "저널은 커밋됐는데 데이터는 안 내려간" 불일치를 막는다.
4) O_DIRECT 와 페이지 캐시 우회
O_DIRECT는 페이지 캐시를 우회해 사용자 버퍼 ↔ 저장장치 간 직접 DMA 로 I/O 한다. 이중 버퍼링(사용자 버퍼 + 페이지 캐시)을 없앤다.- 쓰는 이유: DB(예: 자체 버퍼 풀을 가진 엔진)는 캐시 정책을 스스로 관리하므로 커널 페이지 캐시가 메모리를 이중으로 먹고 예측 불가한 지연을 넣는 것을 원치 않는다. O_DIRECT 로 캐시를 우회하고 내부적으로 어떤 페이지를 캐시할지 직접 결정한다.
- 함의: (a) 정렬 제약 — 버퍼 주소·오프셋·길이가 보통 섹터/블록(512B~4KB) 단위로 정렬돼야 한다. (b) 내구성 — O_DIRECT 라도 장치 쓰기 캐시가 있으면 여전히 fsync/FUA 로 매체까지 내려야 안전하다(O_DIRECT ≠ 내구성). (c) 페이지 캐시의 read 가속을 잃으므로 캐시 관리를 애플리케이션이 책임진다.
5) 게임 서버 설계 적용
- 내구성 등급 분리:
- 재화 원장·결제(유실 치명적): 쓰기를 커밋할 때 반드시 fsync(또는 DB 의 durable commit)로 매체까지 확정한 뒤 클라이언트에 성공을 응답한다. WAL 에 먼저 순차 기록·fsync 하고 본 데이터는 나중에 반영(write-ahead) 하면, 순차 쓰기라 빠르면서 크래시 복구가 가능하다.
- 이동/행동 로그(약간 유실 허용): 버퍼드 write + 주기적 flush 로 처리량을 우선한다. 크래시 시 마지막 몇 초 로그가 날아가도 감수(높은 RPO 허용).
- 그룹 커밋 / fsync 배칭: 여러 트랜잭션의 커밋을 모아 한 번의 fsync 로 확정하면, fsync 당 비용(수 ms)을 분할 상환해 처리량이 크게 오른다. 대신 개별 커밋 지연이 약간 늘어난다 — 내구성·지연 vs 처리량 트레이드오프. 대부분의 DB(그룹 커밋)와 고성능 서버가 이 방식을 쓴다.
- RPO(Recovery Point Objective): "크래시 시 최대 얼마만큼의 최근 데이터 유실을 허용하는가". 재화는 RPO≈0(모든 커밋 durable), 로그는 RPO 수초. 이 목표가 fsync 정책·복제·백업 주기를 결정한다. RPO 를 낮출수록 fsync/복제 비용이 커지므로 데이터 등급별로 다르게 설정한다.
핵심 요약
write() 반환은 내구성이 아니다 — 데이터는 페이지 캐시에 있다가 write-back 으로 늦게 내려간다. 내구성은 fsync/fdatasync(+새 파일이면 디렉터리 fsync)로, 그리고 장치 쓰기 캐시까지 플러시(FUA/PLP)해야 완성된다. O_DIRECT 는 페이지 캐시를 우회할 뿐 내구성을 주지 않는다. 게임 서버는 데이터 등급별로 fsync 정책·WAL·그룹 커밋을 나눠 내구성(RPO)과 처리량을 균형 잡는다.