Files
document-haness/docs/virtualization/tech-log-studio/storage-virtualization/concept/concept-write-completion-is-not-durability.md
T

13 KiB

id, kind, slug, title, topic, topicName, project, status, basisVersion, studio, assets, sourceRevision, source
id kind slug title topic topicName project status basisVersion studio assets sourceRevision source
8fafaeea-8746-4537-87a4-679a3a91aa4c CONCEPT write-completion-is-not-durability 완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존 storage-virtualization 스토리지 가상화 virtualization 초안 Linux O_DIRECT 와 QEMU/libvirt disk 의 cache=none · cache=writeback 두 설정 기준 https://hyeonworks.com/studio/documents/8fafaeea-8746-4537-87a4-679a3a91aa4c/edit
key file
write-completion-boundaries ../../../final/assets/diagrams/write-completion-boundaries/write-completion-boundaries.svg
no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
final/document.md#147-vm에서는-page-cache가-두-번-나타날-수-있다
final/document.md#148-write-완료와-영속화는-다르다
final/document.md#149-direct-i-o
final/document.md#150-fsync-가-필요한-이유
final/document.md#151-flush
final/document.md#152-가장-위험한-상황-거짓-완료
final/document.md#153-qemu-cache-mode
final/document.md#154-cache=none
final/document.md#155-cache=writeback
final/document.md#156-writeback-=-위험-이라고-단정하면-안-되는-이유
final/document.md#157-device-side-cache
final/document.md#170-postgresql-예시-wal과-durability
final/document.md#133-page-cache-write-가-바로-ssd-write는-아니다
final/document.md#174-핵심-claim

완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존

buffered I/O 에서 write() 는 페이지 캐시까지만 내려간다. 가상 머신에서는 그 페이지 캐시가 게스트와 호스트 양쪽에 두 번 나타날 수 있다. 게스트가 성공을 돌려받은 데이터가 호스트 메모리에 머무는 동안 호스트 전원이 나가면 물리 SSD 에는 이전 상태가 남는다. 그래서 완료라는 말이 네 단계로 갈린다 — write() 완료, writeback 완료, fsync/flush 완료, 전원 장애에도 안전한 영속성. Direct I/O 와 QEMU 캐시 모드는 이 가운데 어디까지 갔는지를 바꾼다. 게스트에게 FLUSH 완료라고 응답했는데 데이터가 실제로는 호스트 메모리에만 있으면, 그것은 느린 구성이 아니라 영속성 계약이 깨진 상태다. 페이지 캐시와 fsync 를 아는 사람을 독자로 둔다. Keycloak 과 PostgreSQL 을 가상 머신 위에서 돌리는 실험에서 이 구분을 먼저 세워 두면, 지연을 재는 것과 데이터가 남는지를 재는 것을 한 실험에 섞지 않게 된다.

관계

  • Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk 게스트 쪽 경로를 VFS 부터 virtqueue 까지 이어 그린 글이다. 이 글은 그 경로 위에서 완료 응답이 무엇을 보장하는지만 본다.
  • /dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device 백엔드가 파일이면 게스트 파일시스템 아래에 호스트 파일시스템과 호스트 페이지 캐시가 한 겹 더 놓인다. 두 번째 페이지 캐시가 왜 생기는지를 그 글이 설명한다.
  • VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe fsync 요구가 호스트로 내려간 뒤 어느 계층을 지나는지는 그 글이 맡는다.
  • 빨라진 구성이 durability contract 를 지운 것은 아닌지 먼저 가른다 이 개념이 세운 네 경계를 설정 선택과 성능 개선의 판정에 적용하는 규칙이다.
  • 이 VM 들의 QEMU disk cache mode 는 무엇인가 이 호스트가 cache=none 구성인지 cache=writeback 구성인지 SSOT 에 적혀 있지 않다.
  • Guest 의 fsync() 지연과 Host storage 지연은 같이 오르는가 fsync() 지연 값은 그 질문이 게스트와 호스트를 같은 시간축으로 잰 뒤에 이 글에 들어온다.

본문

게스트와 호스트에 페이지 캐시가 두 번 생긴다

일반적인 buffered I/O 에서는 write() 가 호출될 때마다 물리 SSD 까지 즉시 내려갈 필요가 없다. 커널은 그 내용을 메모리의 페이지 캐시에 받아 두고, 나중에 writeback 으로 파일시스템과 블록 계층을 지나 저장장치 쪽에 내려보낸다. 저장장치보다 최신인 페이지를 dirty page 라고 한다. 저장장치에 ABC 가 있는 상태에서 애플리케이션이 DEF 를 추가하면 페이지 캐시는 ABCDEF 를 dirty 상태로 들고 있고 SSD 에는 아직 ABC 만 있다.

가상 머신에서는 이 페이지 캐시가 한 번 더 나타날 수 있다. 게스트가 buffered I/O 를 쓰고 디스크 백엔드가 호스트의 파일이며 호스트도 페이지 캐시를 쓰는 구성이라고 하자. 게스트 PostgreSQL 의 쓰기는 게스트 ext4 와 게스트 페이지 캐시를 지나 게스트 블록 계층과 virtio-blk 를 거쳐 virtqueue 로 나간다. 가상 머신 경계를 넘은 다음에는 QEMU 와 디스크 이미지 파일을 지나 호스트 페이지 캐시에 다시 담긴다. 같은 데이터가 게스트 메모리와 호스트 메모리 양쪽에 캐시될 수 있다.

write() 성공이 어디까지를 뜻하는가

write() 가 성공을 돌려준 시점에 데이터는 게스트 페이지 캐시에 있고, virtio 를 지나 호스트 페이지 캐시까지 갔을 수도 있다. 그 상태에서 호스트 전원이 나가면 물리 SSD 에는 그 데이터가 없기 때문에, 완료라는 말이 네 단계로 갈린다.

write() 완료
     ≠
writeback 완료
     ≠
fsync/flush 완료
     ≠
전원 장애에도 안전한 durability

fsync() 는 요구를 계층 전체로 내려보낸다

애플리케이션이 부르는 쓰기 호출은 이렇게 생겼다.

write(fd, data, size);

필요한 시점에 fsync() 를 불러 변경 내용을 필요한 영속성 경계까지 반영하도록 요청한다.

fsync(fd);

가상 머신에서는 이 요청이 한 계층에서 끝나지 않는다. 게스트 PostgreSQL 의 fsync() 가 게스트 파일시스템으로 가고, 게스트 블록 계층에서 FLUSH 등으로 바뀐다. 그다음 virtio-blk 를 통해 QEMU 와 백엔드로 내려가고, 호스트 스토리지 계층을 지나 물리 저장장치까지 그 의미가 전달되어야 한다.

PostgreSQL 이 부른 fsync() 가 Guest Filesystem 으로 내려가고, Guest Block Layer 에서 FLUSH 등으로 바뀌어 virtio-blk 와 QEMU / Backend, Host Storage Stack 을 지나 맨 아래 Physical Storage 까지 닿는 흐름도. 여섯 번 손이 바뀌는 동안 한 곳이라도 그 의미를 지키지 않으면 게스트가 받은 완료 응답이 거짓이 된다.

WRITEFLUSH 가 요구하는 것은 서로 다르다.

WRITE
  ↓
"이 데이터를 써라"

FLUSH
  ↓
"앞서 쓴 데이터를 필요한 영속성 경계까지
반영하고 완료 상태를 보장해라"

실제 순서 보장과 영속성의 의미는 이 두 줄보다 복잡하다. SSOT 도 그렇게 밝혀 두었고, 이 글은 두 요청의 구분까지만 말한다.

Direct I/O 가 우회하는 대상은 페이지 캐시다

buffered I/O 에서는 QEMU 의 쓰기가 호스트 페이지 캐시를 거쳐 호스트 파일시스템과 블록 계층을 지나 SSD 로 간다. Direct I/O 에서는 호스트 페이지 캐시를 건너뛰고 호스트 파일시스템과 블록 I/O 경로로 바로 들어간다. Linux 의 O_DIRECT 가 여기에 쓰인다. 우회하는 대상은 페이지 캐시이고, Direct I/O 로 썼다고 영속성이 자동으로 보장되지는 않는다.

cache=none 과 cache=writeback 이 정하는 것

QEMU 와 libvirt 로 정의한 디스크에서 대표적으로 볼 수 있는 설정은 cache=nonecache=writeback 이다. 두 값이 정하는 것은 QEMU 가 호스트 페이지 캐시와 쓰기 완료·플러시의 의미를 어떤 방식으로 쓰는가다. 이름만으로는 캐시가 아예 없는지, 무조건 위험한지가 정해지지 않는다.

설정 호스트 페이지 캐시를 어떻게 쓰나
cache=none 우회하는 방향으로 구성한다. 이중 캐시를 줄일 수 있다
cache=writeback 쓸 수 있게 구성한다. 일반 쓰기가 호스트 메모리에서 빠르게 완료될 수 있다

호스트 페이지 캐시를 우회한다고 해서 쓰기가 곧바로 영속 매체에 반영되지는 않는다. cache=writeback 으로 두어도 게스트의 fsync()FLUSH 가 무시되지 않는다. 정상적인 계층이라면 게스트의 fsyncFLUSH 가 virtio FLUSH 로 이어진다. QEMU 와 백엔드에서 호스트 동기화와 플러시로 이어지고, 저장장치에서 필요한 완료를 확인한 뒤 게스트 완료로 돌아온다.

cache=writeback 을 위험하다는 한 낱말로 줄이지 않으려고 SSOT 는 정확한 표현을 따로 적어 두었다.

writeback caching에서는 volatile cache가 존재할 수 있으므로, Guest의 flush/fsync semantics가 전체 backend/storage stack에서 올바르게 보존되는지가 중요하다.

그래서 보는 것은 게스트가 요구한 영속성이 게스트 파일시스템에서 게스트 블록 계층과 virtio, QEMU 와 백엔드, 호스트 스토리지를 지나 장치까지 가는 동안 그 의미가 깨지지 않는지다.

두 설정 가운데 어느 쪽을 쓸지 이 프로젝트는 정하지 않았다. SSOT 는 두 설정의 뜻과 각각의 오해를 갈라 놓기만 했고, 어느 쪽을 쓰기로 했다고도 그렇게 해서 무엇을 감수했다고도 적지 않았다. 이 호스트의 지금 값이 무엇인지부터 확인되지 않아서, 관계로 걸어 둔 질문이 그것을 먼저 답한다.

호스트 페이지 캐시를 우회해도 장치 안에 캐시가 있다

Direct I/O 로 호스트 페이지 캐시를 건너뛰어도 요청은 호스트 블록 계층과 NVMe 드라이버, NVMe 컨트롤러를 지나 장치 쪽 캐시를 거쳐 플래시에 닿는다. 스토리지 컨트롤러나 장치가 휘발성 쓰기 캐시를 가질 수 있다.

RAM에서 나갔다
    ≠
Device에 command가 전달됐다
    ≠
전원이 끊겨도 살아남는 상태가 됐다

운영에서는 장치의 플러시와 FUA(Force Unit Access) 의미, 전원 손실 보호(power-loss protection)를 갖췄는지도 확인 대상이 된다. SSOT 는 그것이 중요할 수 있다고만 적었고, 이 호스트의 장치가 무엇을 갖췄는지는 적지 않았다.

거짓 완료는 성능 문제가 아니다

게스트가 WRITE 에 이어 FLUSH 를 요청했는데 데이터는 호스트 메모리에만 있고 물리 저장장치에는 이전 데이터가 들어 있다고 하자. 이때 게스트에게 FLUSH 완료 라고 응답하면 PostgreSQL 은 영속성이 확보됐다고 판단한다. 직후에 호스트 전원이 나가면 메모리의 데이터가 사라진다. 이것은 영속성 계약이 깨지는 정확성(correctness) 문제다.

PostgreSQL 로 보면 무엇이 어긋나는지 드러난다. 잔액을 바꾸고 커밋하는 트랜잭션을 생각해 본다.

BEGIN;

UPDATE users
SET balance = 1000
WHERE id = 1;

COMMIT;

PostgreSQL 은 WAL(Write-Ahead Log) 등의 영속성 프로토콜을 쓰며 필요한 시점에 스토리지 동기화를 수행한다. WAL 쓰기는 게스트 페이지 캐시와 fsync 를 지나 게스트 파일시스템과 게스트 블록 계층, virtio-blk, QEMU, 호스트 스토리지를 거쳐 물리 저장장치까지 간다. 그 완료가 되돌아와야 필요한 영속성 조건이 충족됐다고 보고 COMMIT 을 성공으로 처리한다. 가상 머신 스토리지 계층이 플러시와 fsync 의 의미를 제대로 보존하지 않으면, PostgreSQL 의 영속성 가정과 실제 저장장치 동작이 어긋날 수 있다.

이 호스트에서 확인하지 않은 것

이 글에 이 테스트 호스트에서 잰 값은 하나도 없다. 두 가상 머신의 디스크 캐시 모드가 무엇으로 설정돼 있는지 SSOT 에 없어서, 이 호스트가 cache=none 구성인지 cache=writeback 구성인지 여기서 말하지 않는다. 거짓 완료가 이 환경에서 실제로 일어나는지도 재현하지 않았다. 장치의 플러시와 FUA 의미가 어떻게 동작하는지, 전원 손실 보호를 갖췄는지도 확인하지 않았다. fsync() 지연 값은 관계로 걸어 둔 질문이 게스트 지연과 호스트 스토리지 지연을 같은 시간축으로 잰 뒤에 이 글에 들어온다. 이 글은 이 구조에서 완료 응답이 무엇을 보장하는지까지 설명하고, 이 서버가 그 보장을 지키는지는 재지 않았다.