dirty 페이지
고친 사람 github-actions[bot]
dirty 페이지는 메모리에서 고쳤지만 아직 디스크에 다시 쓰지 않은 페이지입니다. 페이지는 디스크와 메모리 사이를 오가는 일정한 크기의 데이터 덩이입니다. 고친 내용은 디스크에 쓰기 전까지 메모리에만 있습니다. 그래서 데이터베이스와 운영체제는 이런 페이지에 표시를 붙여 두었다가 나중에 디스크에 씁니다.
쉽고 빠른 이해
dirty 페이지는 메모리에서 고쳐 놓고 디스크에는 아직 안 옮긴 데이터 덩이입니다. 데이터베이스에서 행 하나를 고치면 그 행이 든 덩이가 메모리에서만 바뀝니다. 디스크에는 옛 내용이 남아 있습니다.
고칠 때마다 디스크에 쓰면 느립니다. 디스크는 메모리보다 훨씬 느리기 때문입니다. 그래서 고친 덩이에 「아직 안 씀」 표시만 붙이고 쓰기를 미룹니다.
- 데이터를 고치면 메모리의 덩이만 바꾸고 표시를 붙입니다
- 같은 덩이를 또 고쳐도 표시는 하나면 됩니다. 디스크에는 마지막 내용만 쓰면 됩니다
- 메모리가 모자라거나 정해 둔 때가 오면 표시된 덩이를 디스크에 쓰고 표시를 뗍니다
대가는 전원이 나가면 표시된 덩이의 변경이 사라진다는 점입니다. 데이터베이스는 이를 막으려고 변경 내용만 순서대로 적어 두는 별도 파일을 둡니다. 이 파일이 로그입니다. 고친 덩이를 디스크에 쓰기보다 먼저 로그를 디스크에 적어 둡니다.
또 표시된 덩이가 많이 쌓이면 한꺼번에 쓰느라 디스크가 바빠집니다.
상세
이 절은 페이지가 메모리에 올라오는 데서 출발합니다. 고친 페이지가 왜 바로 디스크로 가지 않는지, 언제 내려가는지, 그 사이에 전원이 나가면 무엇이 변경을 지키는지를 차례로 봅니다. 끝으로 운영체제가 같은 이름으로 부르는 것과 dirty 페이지가 쌓일 때의 대가를 봅니다.
회의 중에 화이트보드에 일정을 고쳐 적었다고 해 봅시다. 팀 공유 달력에는 아직 옮기지 않았습니다. 화이트보드를 지우기 전에 달력에 옮겨 적어야 고친 일정이 남습니다. 화이트보드가 메모리, 달력이 디스크입니다. 지우기 전에 옮겨야 하는 고친 일정이 dirty 페이지입니다.
페이지와 메모리 사본
디스크는 데이터를 한 바이트씩 읽고 쓰지 않습니다. 일정한 크기의 덩이 단위로 다룹니다. 이 덩이가 페이지입니다. 크기는 시스템마다 다릅니다. 몇 킬로바이트가 흔합니다.
데이터베이스라면 테이블이 디스크의 데이터 파일 안에 페이지 단위로 나뉘어 있습니다. 행 하나를 고치려면 그 행이 든 페이지를 메모리로 읽어 옵니다. 그리고 메모리에 올라온 사본을 고칩니다. 디스크에 있는 원본은 이때 손대지 않습니다.
읽어 온 페이지는 다 쓴 뒤에도 메모리에 모아 둡니다. 같은 페이지가 또 필요할 때 디스크까지 다시 가지 않으려는 것입니다. 데이터베이스에서는 이 메모리 공간을 버퍼 풀이라고 부릅니다.
깨끗한 페이지와 dirty 페이지
디스크에서 읽어 온 뒤 고치지 않은 페이지는 원본과 내용이 같습니다. 이런 페이지를 깨끗한 페이지(clean page)라고 합니다. 깨끗한 페이지는 메모리가 모자라면 그냥 버려도 됩니다. 다시 필요하면 디스크에서 읽어 오면 됩니다.
페이지를 고치는 순간 사정이 바뀝니다. 메모리의 사본과 디스크의 원본이 서로 달라집니다. 이 페이지를 그냥 버리면 고친 내용은 어디에도 남지 않습니다. 이런 페이지가 dirty 페이지입니다.
그래서 시스템은 페이지마다 표시 칸을 하나 둡니다. 페이지를 고치면 이 칸을 켭니다. 디스크에 다 쓰고 나면 끕니다. 이 칸을 흔히 dirty 비트라고 부릅니다.
한 페이지를 여러 번 고쳐도 표시는 하나뿐입니다. 이미 켜진 칸을 또 켤 일은 없습니다. 디스크에 쓸 때도 마지막 내용 한 벌만 쓰면 됩니다.
아래 그림은 한 페이지가 메모리에 올라온 뒤 거치는 상태입니다. 버릴 수 있는 것은 깨끗한 페이지뿐입니다.
stateDiagram-v2
state "깨끗한 페이지" as C
state "dirty 페이지" as D
[*] --> C: 디스크에서 읽어 온다
C --> D: 고친다
D --> D: 또 고친다
D --> C: 디스크에 쓴다
C --> [*]: 메모리에서 버린다
쓰기를 미루는 까닭
고칠 때마다 바로 디스크에 쓰면 dirty 페이지는 생기지 않습니다. 대신 쓰기마다 디스크를 기다려야 합니다. 디스크는 메모리보다 훨씬 느립니다.
쓰기를 미루면 먼저 같은 페이지에 대한 쓰기가 줄어듭니다. 한 페이지를 열 번 고쳐도 디스크에는 한 번만 씁니다. 자주 고치는 페이지일수록 아끼는 쓰기가 많습니다.
여러 페이지를 모아 두었다가 한꺼번에 쓸 수도 있습니다. 흩어진 쓰기를 모아 순서를 정리하면 디스크의 서로 떨어진 위치를 오가는 일이 줄어듭니다.
캐싱에서는 바로 쓰는 쪽과 미루는 쪽에 이름이 있습니다. 쓰기를 미루는 쪽이 write-back입니다(write-behind 라고도 부릅니다). 고칠 때마다 원본에도 바로 쓰는 쪽은 write-through입니다. 두 방식을 견주면 이렇습니다.
| 방식 | 언제 디스크에 쓰나 | dirty 페이지 | 전원이 나가면 |
|---|---|---|---|
| write-back | 나중에 몰아서 | 생긴다 | 아직 안 쓴 변경을 잃을 수 있다 |
| write-through | 고칠 때마다 바로 | 안 생긴다 | 디스크까지 간 변경은 남는다 |
데이터베이스의 버퍼 풀과 운영체제가 파일을 다루는 메모리는 대개 write-back 으로 돕니다. 그래서 두 곳 모두 dirty 페이지를 다룹니다.
dirty 페이지가 디스크로 내려가는 때
미뤄 둔 쓰기는 언젠가 해야 합니다. dirty 페이지가 디스크로 내려가는 때는 크게 셋입니다. 표의 셋째 줄에 나오는 로그와 복구는 아래 「전원이 나갈 때 변경을 지키는 로그」 소절에서 풉니다.
| 때 | 무엇이 계기인가 | 노리는 것 |
|---|---|---|
| 메모리가 찰 때 | 새 페이지를 올릴 빈 공간이 필요하다 | 공간을 비운다 |
| 평소 틈틈이 | 백그라운드에서 도는 쓰기 작업 | dirty 페이지를 미리 줄여 둔다 |
| 체크포인트 | 정해 둔 시간이나 쌓인 로그의 양 | 복구 때 다시 읽을 로그를 줄인다 |
첫째는 축출입니다. 축출은 메모리가 찼을 때 페이지 하나를 골라 내보내는 일입니다.
고른 페이지가 깨끗하면 바로 비웁니다. dirty 페이지면 디스크에 먼저 쓰고 나서 비웁니다. 새 페이지를 기다리던 요청은 그 쓰기까지 함께 기다립니다.
둘째는 이 기다림을 줄이려고 둡니다. 백그라운드 작업이 dirty 페이지를 틈틈이 디스크에 써서 깨끗하게 만들어 둡니다. 그러면 축출할 때 고를 깨끗한 페이지가 늘어납니다.
셋째는 체크포인트입니다. 체크포인트는 어느 시점까지 생긴 dirty 페이지를 전부 디스크에 쓰는 일입니다. 다 쓰고 나면 그 시점 앞의 변경은 디스크에 있다고 믿을 수 있습니다. 이것이 왜 쓸모 있는지는 다음 소절의 로그와 이어집니다.
전원이 나갈 때 변경을 지키는 로그
dirty 페이지는 메모리에만 있습니다. 디스크에 쓰기 전에 전원이 나가면 그 변경은 사라집니다.
데이터베이스는 사용자에게 「저장했다」고 답한 변경을 잃으면 안 됩니다. 이 답을 주는 단계가 커밋입니다. 그렇다고 커밋할 때마다 dirty 페이지를 디스크에 쓰면 쓰기를 미룬 보람이 없습니다.
그래서 데이터베이스는 페이지 대신 변경 내용을 로그에 적습니다. 로그는 변경을 일어난 순서대로 파일 끝에 덧붙이는 기록입니다. 끝에 덧붙이기만 하므로 흩어진 페이지를 쓰는 것보다 빠릅니다.
커밋할 때는 이 로그만 디스크에 확실히 써 둡니다. dirty 페이지는 나중에 써도 됩니다. 서버가 죽었다 다시 뜨면 로그를 읽어 dirty 페이지에 있던 변경을 되살리기 때문입니다. 이렇게 서버가 다시 뜰 때 로그를 읽어 데이터를 바로잡는 일이 크래시 복구입니다.
이 방식을 WAL(Write-Ahead Logging, 미리 쓰기 로그)이라고 부릅니다. WAL 이 지키는 순서는 하나입니다. dirty 페이지를 디스크에 쓰기 전에, 그 페이지를 바꾼 로그가 먼저 디스크에 있어야 합니다.
순서가 뒤집힌 장면을 봅시다. 축출 때는 아직 커밋하지 않은 변경이 든 페이지도 디스크로 내려갈 수 있습니다. 그런 페이지가 먼저 내려가고, 로그를 쓰기 전에 서버가 죽었다고 합시다. 커밋 안 된 변경은 복구 때 되돌려야 합니다. 그런데 무엇을 어떻게 바꿨는지 적은 로그가 없으니 되돌릴 근거가 없습니다.
아래 그림은 행 하나를 고치고 커밋할 때 로그와 dirty 페이지가 디스크에 닿는 순서입니다. 커밋은 로그만 기다리고, 페이지는 한참 뒤에 내려갑니다.
sequenceDiagram
participant DB as 데이터베이스
participant 풀 as 버퍼 풀
participant 로그
participant 파일 as 데이터 파일
DB->>풀: 페이지를 고친다 · dirty 표시를 켠다
DB->>로그: 변경 내용을 적는다
Note over DB,로그: 커밋 · 로그가 디스크에 닿을 때까지 기다린다
Note over 풀,파일: 한참 뒤
풀->>파일: dirty 페이지를 쓴다 · 표시를 끈다
로그는 계속 자랍니다. 체크포인트가 dirty 페이지를 다 써 두면 그 시점 앞의 로그는 복구에 필요 없어집니다. 체크포인트가 잦을수록 복구 때 다시 읽을 로그가 짧아집니다.
운영체제의 dirty 페이지
운영체제도 같은 이름을 씁니다. 운영체제는 파일을 읽고 쓸 때 그 내용을 페이지 단위로 메모리에 둡니다. 이 공간이 페이지 캐시입니다.
프로그램이 파일에 쓰기를 요청하면 운영체제는 페이지 캐시의 페이지만 고칩니다. 그리고 곧바로 성공을 돌려줍니다. 그 페이지가 dirty 페이지입니다. 디스크에 쓰는 일은 운영체제가 나중에 백그라운드에서 합니다.
그래서 쓰기가 성공했어도 전원이 나가면 내용이 사라질 수 있습니다. 디스크에 확실히 닿게 하려면 프로그램이 fsync 를 부릅니다. fsync 는 그 파일의 dirty 페이지를 디스크에 다 쓸 때까지 기다리게 하는 호출입니다.
데이터베이스는 버퍼 풀과 페이지 캐시, 이 두 겹을 함께 겪습니다. 버퍼 풀의 dirty 페이지를 파일에 써도 그 내용은 먼저 페이지 캐시에 dirty 페이지로 머뭅니다.
로그 파일에 쓴 내용도 똑같이 페이지 캐시에 먼저 머뭅니다. 데이터베이스가 커밋할 때 로그 파일에 fsync 를 부르는 까닭입니다. 데이터 파일 쪽은 서두르지 않고 나중에, 예를 들어 체크포인트 때 디스크에 닿게 합니다.
dirty 페이지가 쌓일 때의 대가
쓰기를 미룬 만큼 할 일도 쌓입니다. dirty 페이지가 많을수록 한 번에 디스크로 내려보낼 양이 늘어납니다. 몰아서 쓰는 동안 디스크가 바빠집니다. 그만큼 다른 요청의 응답이 느려집니다.
메모리가 모자랄 때도 문제가 됩니다. 내보낼 후보가 전부 dirty 페이지면 비우기 전에 쓰기부터 기다려야 합니다. 새 페이지를 기다리던 요청이 그만큼 늦어집니다.
복구도 길어집니다. 로그를 쓰는 데이터베이스는 커밋한 변경을 잃지 않습니다. 대신 되살릴 변경이 많아 다시 뜨는 데 오래 걸립니다.
데이터베이스와 운영체제는 대개 dirty 페이지가 차지할 몫에 한도를 둡니다. 한도를 넘으면 백그라운드 쓰기를 서두릅니다. 그래도 넘치면 쓰기를 요청한 쪽을 잠시 기다리게 합니다.
관련 항목
dirty 페이지를 담아 두는 메모리 공간
dirty 페이지를 디스크로 내려보내는 작업
체크포인트 · 축출 · fsync · 플러시 · 백그라운드 쓰기
dirty 페이지의 변경을 지키는 로그와 복구
WAL · 로그 · 리두 로그 · 크래시 복구 · 커밋 · ARIES
고친 내용을 원본에 쓰는 시점을 정하는 쓰기 정책
쓰기 정책 · write-behind · write-through · write-around
내보낼 페이지를 고르는 교체 알고리즘
페이지 교체 · LRU · LFU · CLOCK 알고리즘
dirty 표시가 붙는 단위와 구조
페이지 · dirty 비트 · 깨끗한 페이지 · 페이지 테이블 · 캐시 라인 · 블록
dirty 페이지가 내려가는 저장소
디스크 · SSD · 데이터 파일 · 스왑 영역
dirty 페이지를 다루는 시스템
다른 이름: dirty page · 더티 페이지 · dirty buffer · 더티 버퍼