포스트

페이지 캐시와 파일 IO - buffered와 direct, fsync가 실제로 하는 일

write()가 성공으로 돌아왔다고 데이터가 디스크에 있는 것은 아니다. 데이터베이스의 커밋 설계, Kafka의 내구성 모델, 로그 유실 사고가 모두 이 사실에서 출발한다. 커널이 파일 쓰기를 어떻게 다루는지 보면 그 이유가 나온다.


write()가 끝나는 곳은 메모리다

일반적인 write()는 페이지 캐시(커널이 파일 내용을 메모리에 들고 있는 영역)에 데이터를 복사하고 그 페이지를 dirty로 표시한 뒤 돌아온다. 디스크에는 나중에 커널의 writeback 스레드가 쓴다. 언제 쓰는지는 dirty_ratio, dirty_expire_centisecs 같은 커널 파라미터가 정한다.

그래서 얻는 것.

  • 쓰기가 빠르다. 디스크 지연이 호출자에게 보이지 않는다.
  • 읽기 캐시가 공짜로 생긴다. 방금 쓴 것을 읽으면 디스크에 가지 않는다. 같은 데이터를 여러 프로세스가 읽어도 복사본은 하나다.
  • 작은 쓰기가 병합된다. 커널이 모아서 순차로 내보낸다.

잃는 것 하나. 전원이 나가면 페이지 캐시의 dirty 페이지는 사라진다. 프로세스가 죽는 것은 괜찮다. 페이지 캐시는 커널에 있으므로 프로세스가 죽어도 남고, 결국 디스크에 쓰인다. 사라지는 것은 커널이 함께 죽을 때(전원, 커널 패닉)다.


fsync가 보장하는 것과 못 하는 것

fsync(fd)는 그 파일의 dirty 페이지를 디스크에 내보내고 장치가 확인할 때까지 기다린다. man 페이지는 장치에 디스크 캐시가 있으면 그것을 비우는 일까지 포함한다고 적는다(fsync(2)). 이것이 내구성의 유일한 문지기다.

몇 가지 함정이 있다.

디렉터리 엔트리는 별개다. 새 파일을 만들고 쓴 뒤 fsync하면 파일 내용과 그 파일의 메타데이터는 안전하지만 디렉터리 엔트리는 아닐 수 있다. 그래서 부모 디렉터리도 fsync해야 “그 파일이 존재한다”가 보장된다(같은 man 페이지). 데이터베이스와 로그 시스템이 디렉터리를 따로 동기화하는 이유다.

fdatasync는 이후 읽기에 필요하지 않은 메타데이터(수정 시각 등)를 건너뛴다. 파일 크기는 읽기에 필요하므로 여전히 내보낸다. 그래서 파일 크기가 안 바뀌는 덮어쓰기라면 이쪽이 싸다. DB가 미리 파일을 할당해 두고 덮어쓰는 이유 중 하나다.

실패를 한 번만 알려 줄 수 있다. 하드웨어 오류로 writeback이 실패하면 파일시스템은 대개 해당 페이지의 데이터를 버리고 clean으로 표시한다. 오류 상태도 다른 호출(백그라운드 writeback, sync())이 먼저 소비하면 이후의 fsync에는 안 보일 수 있다. 그래서 fsync가 오류를 반환한 뒤 다시 호출하면 성공이 돌아올 수 있고, 재시도할 원본도 이미 없다. 2018년 3월 말 PostgreSQL 쪽에서 이 문제가 보고됐다(LWN, 2018, fsyncgate). PostgreSQL은 이후 data_sync_retry를 두고, 기본값(off)에서는 데이터 파일 flush 실패 시 PANIC으로 서버를 멈추게 했다. 문서는 재시도가 위험한 이유를 “the second attempt may be reported as successful, when in fact the data has been lost”라고 적는다(PostgreSQL: Error Handling). 두 번째 시도가 성공으로 보고돼도 데이터는 이미 사라졌을 수 있다는 뜻이다. fsync 실패는 재시도 대상이 아니라 즉시 중단 대상이라는 결론이 여기서 나왔다.

장치가 거짓말할 수 있다. 쓰기 캐시가 write-back인 디스크는 캐시에 담고 성공을 돌려준다. fsync는 디스크 캐시를 비우도록 되어 있지만, 오래된 커널과 일부 파일시스템은 디스크 캐시를 비우는 법을 모른다(fsync(2)). 이때 배터리 백업이 없으면 그 데이터도 전원과 함께 사라진다. 소비자용 SATA 드라이브와 상당수 SSD가 이런 캐시를 가진다(PostgreSQL: Reliability and the Write-Ahead Log).


O_DIRECT: 캐시를 건너뛴다

O_DIRECT는 페이지 캐시를 우회해 사용자 버퍼에서 장치로 직접 보낸다. 데이터베이스가 쓰는 이유는 속도가 아니라 이중 캐싱을 피하고 자기가 캐시를 통제하기 위해서다. DB는 이미 자기 버퍼 풀을 갖고 있고, 어떤 페이지를 언제 내보낼지 자기가 안다. 커널이 그 위에서 또 캐싱하면 메모리만 두 배로 쓴다.

대가가 크다. 정렬 요구(블록 크기 배수), 캐시 없음, 작은 IO에 매우 불리. 일반 애플리케이션이 쓸 도구는 아니다.

O_DIRECT도 fsync를 대신하지 않는다. 장치 캐시는 여전히 남아 있다.


세 가지 정책

정책예내구성비용
buffered, fsync 안 함일반 로그 파일, Kafka 기본커널 크래시에 취약가장 빠름
buffered + 커밋마다 fsyncPostgreSQL 기본, innodb_flush_log_at_trx_commit=1개별 커밋 보장fsync 지연이 커밋 지연
buffered + 주기적 fsyncinnodb_flush_log_at_trx_commit=2최근 1초 유실 가능중간

innodb_flush_log_at_trx_commit=2는 커밋마다 로그를 쓰고 디스크 flush는 초당 한 번 하므로, flush되지 않은 트랜잭션은 크래시에서 잃을 수 있다(MySQL: InnoDB Startup Options and System Variables).

Kafka가 fsync를 기본으로 하지 않는 것은 내구성을 포기한 것이 아니라 다른 곳에서 얻기 때문이다. acks=all + min.insync.replicas로 여러 노드의 페이지 캐시에 들어간 것을 확인한다. 노드 하나의 전원이 나가도 다른 노드에 있다. 운영 문서도 장애 난 노드는 복제본에서 복구된다는 이유로 fsync를 끈 기본 설정을 권한다(Kafka Operations). 복제를 내구성의 수단으로 쓰는 설계이고, 그래서 단일 노드 Kafka는 이 전제가 빠진 상태다.


이 설명이 깨지는 곳

  • 컨테이너와 가상화가 한 겹 더 있다. 호스트의 페이지 캐시, 하이퍼바이저의 캐시 정책, 네트워크 스토리지의 캐시까지 겹치면 fsync의 의미가 층마다 달라진다.
  • 클라우드 블록 스토리지는 자체 복제를 한다. 로컬 디스크의 fsync 모델과 다르고, 지연 특성도 다르다.
  • 그룹 커밋이 fsync 비용을 나눈다. 여러 트랜잭션의 커밋을 모아 한 번에 fsync하므로, 동시성이 높을수록 커밋당 비용이 준다. “fsync는 N ms”라는 단일 수치는 부하에 따라 달라진다.
  • 페이지 캐시가 크다고 항상 좋은 것은 아니다. 큰 순차 읽기 한 번이 캐시를 밀어내(cache pollution) 다른 워크로드를 느리게 만들 수 있다.

무엇을 재면 확인되는가

  1. innodb_flush_log_at_trx_commit이나 synchronous_commit을 바꿔 가며 커밋 지연과 처리량을 재고, 그 사이에 전원 차단(컨테이너 강제 종료가 아니라 호스트 수준)에서 무엇이 사라지는지 본다.
  2. /proc/meminfo의 Dirty와 Writeback을 부하 중에 관찰한다. 쓰기가 실제로 언제 나가는지 보인다.
  3. 동시성을 올려 가며 커밋당 fsync 비용이 줄어드는지(그룹 커밋) 본다.

이후 ParityPay 8편에서 acks별 유실을 쟀지만 단일 노드였고, 프로세스를 죽였지 커널을 죽이지는 않았다. 프로세스 종료로는 페이지 캐시 유실을 재현할 수 없다는 것이 이 글을 쓰며 정리된 한계다.


실무와의 접점

ISMS 대응 로그 수집 같은 작업에서 “로그가 유실되지 않는다”를 어떻게 보장했는지 지금 다시 보면, 대부분 애플리케이션 버퍼와 네트워크 전송까지만 보고 있었다. 수집기가 fsync를 하는지, 컨테이너가 죽을 때와 노드가 죽을 때가 어떻게 다른지는 묻지 않았다. “유실되지 않는다”는 문장은 어느 층까지의 보장인지를 적어야 검증 가능한 문장이 된다.


정리

  • write()는 페이지 캐시까지다. 프로세스가 죽어도 남지만 커널이 죽으면 사라진다. 그래서 프로세스 종료 실험으로는 이 유실을 볼 수 없다.
  • 파일 내용과 디렉터리 엔트리는 따로 동기화해야 한다.
  • 그룹 커밋 때문에 “fsync 비용”은 단일 수치가 아니라 동시성의 함수다.
참고한 자료외부 출처 6

외부 출처

장애 대응과 관측
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다