사전 체크포인트
개념

체크포인트

gabury1고친 사람 github-actions[bot]

체크포인트는 데이터베이스가 메모리에만 반영해 둔 변경을 한꺼번에 디스크의 데이터 파일로 내려쓰는 일입니다. 다 내려쓰고 나면 변경을 순서대로 적어 두는 로그에 체크포인트 레코드를 남깁니다. 이 레코드는 「이 위치 앞의 변경은 디스크에 있다」고 알려 줍니다. 덕분에 서버가 죽었다 다시 뜰 때 로그를 처음부터 읽지 않아도 됩니다. 이 항목은 데이터베이스의 체크포인트를 중심으로 봅니다. 같은 이름을 다른 분야에서 쓰는 뜻은 끝에서 따로 봅니다.

쉽고 빠른 이해

체크포인트는 메모리에 쌓아 둔 변경을 데이터 파일에 몰아서 쓰는 일입니다. 다 쓰면 로그에 「이 위치 앞은 반영 끝」이라고 적어 둡니다. 로그는 데이터베이스가 변경을 순서대로 적어 두는 파일입니다. 게임의 저장 지점과 비슷합니다.

데이터베이스는 커밋을 빠르게 하려고 변경을 로그에만 먼저 적습니다. 데이터 파일은 나중에 고칩니다. 체크포인트가 없으면 로그가 끝없이 쌓입니다. 서버가 죽었을 때 그 긴 로그를 전부 다시 읽어야 합니다.

도는 순서:

  1. 지금 로그가 어디까지 왔는지 위치를 기억합니다
  2. 메모리에서 바뀐 데이터 조각들을 데이터 파일에 씁니다
  3. 디스크에 확실히 닿았는지 확인합니다
  4. 로그에 체크포인트 레코드를 남깁니다. 레코드에는 1에서 기억한 위치가 담깁니다. 그 위치 앞의 로그는 이제 지워도 됩니다

대가는 디스크 쓰기가 한 번에 몰린다는 것입니다. 자주 하면 평소가 느려집니다. 드물게 하면 서버가 다시 뜨는 데 오래 걸립니다.

상세

이 절은 데이터베이스가 왜 체크포인트를 두는지부터 봅니다. 그다음 한 번의 체크포인트가 어떤 순서로 돌고, 서버가 다시 뜰 때 그 레코드를 어떻게 쓰는지 따라갑니다. 끝으로 간격이 무엇을 바꾸는지 봅니다.

게임을 떠올리면 쉽습니다. 저장 지점을 지나면 캐릭터가 죽어도 처음이 아니라 거기서 다시 시작합니다. 다만 게임은 저장 지점 이후의 진행을 잃습니다. 데이터베이스는 그 뒤의 변경을 로그로 되살리므로 잃지 않습니다.

변경이 머무는 두 곳

데이터베이스는 테이블과 인덱스를 페이지라는 고정 크기 덩이로 나눠 디스크에 둡니다. 행 하나를 고치려면 그 행이 든 페이지를 메모리로 읽어 와서 고칩니다. 디스크는 한 바이트씩이 아니라 이런 덩이 단위로 읽고 쓰는 편이 빠릅니다.

읽어 온 페이지는 한 번 쓰고 버리지 않고 메모리에 모아 둡니다. 같은 페이지를 또 찾으면 디스크까지 가지 않아도 되기 때문입니다. 이 메모리 공간이 버퍼 풀입니다.

버퍼 풀에서 고쳤지만 아직 디스크에 안 쓴 페이지를 dirty 페이지라고 합니다. 디스크에 있는 원본과 메모리의 사본이 서로 다른 상태입니다. 이 상태로 전원이 나가면 메모리의 변경은 사라집니다.

그래서 데이터베이스는 페이지를 고치기 전에 무엇을 고칠지부터 로그에 적습니다. 이 방식을 WAL(Write-Ahead Logging, 미리 쓰기 로그)이라고 부릅니다. 로그는 파일 끝에 순서대로 덧붙이기만 하므로 여기저기 흩어진 페이지를 쓰는 것보다 빠릅니다.

커밋할 때는 로그만 디스크에 확실히 내려쓰면 됩니다. 데이터 파일의 페이지는 나중에 써도 됩니다. 서버가 죽더라도 로그를 다시 읽어 변경을 되살릴 수 있기 때문입니다.

체크포인트가 풀어 주는 두 문제

WAL 로 커밋은 빨라졌지만 문제가 둘 남습니다. 하나는 로그가 끝없이 자란다는 것입니다. 어떤 로그 레코드가 더는 필요 없는지 알 방법이 없으면 하나도 못 지웁니다.

다른 하나는 복구가 길어진다는 것입니다. 서버가 다시 뜨면 디스크에 반영되지 않은 변경을 로그에서 찾아 다시 적용합니다. 이 일이 크래시 복구입니다. 어디서부터 읽어야 할지 모르면 로그 맨 처음부터 읽을 수밖에 없습니다.

체크포인트는 이 둘을 한 번에 풉니다. 체크포인트를 시작할 때 로그가 와 있던 위치를 시작 위치라고 하겠습니다. 그때 있던 dirty 페이지를 전부 데이터 파일에 내려쓰고 나면, 시작 위치 앞의 로그가 기록한 변경은 전부 디스크에 있습니다. 그러면 그 앞의 로그는 지워도 됩니다. 복구도 시작 위치부터 읽으면 됩니다.

flowchart TD
    subgraph 앞["시작 위치 앞의 로그"]
        A["변경 기록들 · 데이터 파일에 이미 반영됨"]
    end
    subgraph 경계["시작 위치"]
        C["체크포인트를 시작할 때 로그가 와 있던 위치"]
    end
    subgraph 뒤["시작 위치 뒤의 로그"]
        B["변경 기록들 · 아직 메모리에만 있을 수 있음"]
    end
    A --> C --> B
    A -.-> X["지우거나 다시 쓸 수 있다"]
    B -.-> Y["복구할 때 다시 적용한다"]

한 번의 체크포인트가 도는 순서

이 소절은 트랜잭션을 멈추지 않고 도는 흔한 방식을 따라갑니다. 멈추고 하는 방식과의 차이는 뒤 소절에서 봅니다.

체크포인트는 대개 정해진 시간이 지나거나 로그가 일정량 쌓이면 시작합니다. 관리자가 명령으로 직접 부를 수도 있습니다. 시작하면 아래 순서로 돕니다.

먼저 지금 로그가 어디까지 왔는지, 곧 시작 위치를 기억합니다. 그다음 버퍼 풀의 dirty 페이지를 데이터 파일에 씁니다.

운영체제는 파일에 쓴 내용을 곧바로 디스크에 보내지 않고 잠시 메모리에 들고 있습니다. 그래서 다 쓴 뒤에 fsync 를 불러 디스크에 닿을 때까지 기다립니다. fsync 는 파일의 내용을 저장 장치에 확실히 내려쓰라고 운영체제에 요청하는 호출입니다.

마지막으로 로그에 체크포인트 레코드를 하나 적습니다. 이 레코드에는 처음에 기억해 둔 시작 위치가 담깁니다. 이 레코드까지 디스크에 닿아야 체크포인트가 끝난 것으로 칩니다.

sequenceDiagram
    participant DB as 데이터베이스
    participant 파일 as 데이터 파일
    participant 로그
    Note over DB,로그: 지금 로그 위치를 시작 위치로 기억한다
    DB->>파일: 버퍼 풀의 dirty 페이지를 쓴다
    DB->>파일: fsync · 디스크에 닿을 때까지 기다린다
    DB->>로그: 체크포인트 레코드 · 시작 위치를 담는다
    Note over 로그: 시작 위치 앞의 로그는 지워도 된다

체크포인트 레코드가 시작 위치보다 뒤에 놓이는 까닭은 페이지를 쓰는 동안에도 다른 트랜잭션이 계속 돌기 때문입니다. 그 사이의 새 변경은 이번에 쓴 페이지에 들어갔을 수도 있고 안 들어갔을 수도 있습니다. 그래서 복구는 레코드가 아니라 시작 위치부터 읽어야 어느 쪽이든 빠뜨리지 않습니다.

이미 페이지에 들어간 변경을 한 번 더 적용하면 두 번 반영될까요? 그렇지 않습니다. 페이지마다 자신에게 마지막으로 반영된 로그 위치를 적어 둡니다. 복구는 그 위치를 보고 이미 들어간 변경을 건너뜁니다.

서버가 다시 뜰 때

서버가 다시 뜨면 로그에서 마지막으로 끝난 체크포인트 레코드를 찾습니다. 그 레코드가 가리키는 시작 위치로 가서 로그 끝까지 변경을 차례로 다시 적용합니다. 다 적용하면 로그에 적힌 마지막 변경까지 되살아납니다.

복구에는 일이 하나 더 있습니다. 로그를 다시 적용하면 커밋하지 못하고 죽은 트랜잭션의 변경도 함께 살아납니다. 다시 적용을 마친 뒤 이런 변경을 없던 것으로 돌려야 죽기 직전의 커밋된 상태가 됩니다. 돌리는 방법은 데이터베이스마다 다르고, 체크포인트가 맡는 일은 아닙니다.

체크포인트가 도중에 끊겼다면 그 체크포인트는 레코드가 없으므로 없던 일이 됩니다. 복구는 그 앞의 체크포인트 레코드가 담은 시작 위치부터 읽습니다. 조금 더 오래 걸릴 뿐 빠뜨리는 변경은 없습니다.

flowchart TD
    S["서버가 다시 뜬다"] --> F["끝까지 적힌 마지막 체크포인트 레코드를 찾는다"]
    F --> P["레코드에 담긴 시작 위치로 간다"]
    P --> N["다음 로그 레코드를 읽는다"]
    N --> Q{"그 변경이 페이지에 이미 들어갔나"}
    Q -->|예| K["건너뛴다"]
    Q -->|아니오| R["페이지에 다시 적용한다"]
    K --> E{"로그 끝인가"}
    R --> E
    E -->|아니오| N
    E -->|예| U["커밋 못 한 트랜잭션의 변경을 없던 것으로 돌린다"]

멈추고 하는 방식과 돌리면서 하는 방식

가장 단순한 체크포인트는 새 트랜잭션을 잠시 막고 모든 dirty 페이지를 쓴 뒤 다시 엽니다. 쓰는 동안 새 변경이 없으니 시작 위치가 체크포인트 레코드와 딱 맞아 이해하기 쉽습니다. 대신 쓰는 동안 데이터베이스가 멈추므로 운영 중인 서비스에서는 받아들이기 어렵습니다.

그래서 대부분의 데이터베이스는 트랜잭션을 막지 않고 체크포인트를 돌립니다. 이 방식을 퍼지 체크포인트라고 부릅니다. 위 「한 번의 체크포인트가 도는 순서」가 이 방식입니다. 시작 위치가 체크포인트 레코드보다 앞에 있는 것은 그 사이에도 변경이 계속 들어왔기 때문입니다.

체크포인트 간격

체크포인트 간격은 두 비용을 맞바꿉니다. 간격을 좁히면 복구 때 다시 적용할 로그가 짧아집니다. 남겨 둘 로그도 줄어듭니다. 대신 페이지를 자주 쓰므로 평소의 디스크 쓰기가 늘어납니다.

간격을 넓히면 한 페이지를 여러 번 고쳐도 한 번만 쓰면 되므로 쓰기가 줄어듭니다. 대신 로그가 많이 쌓입니다. 서버가 죽었을 때 다시 적용할 양이 늘어 복구가 길어집니다.

간격 평소 디스크 쓰기 쌓이는 로그 복구 시간
좁게 많다 적다 짧다
넓게 적다 많다 길다

간격과 별개로 한 번에 몰아 쓰는 것도 문제입니다. dirty 페이지를 한꺼번에 쓰면 그동안 디스크가 바빠져 다른 쿼리의 응답이 튑니다. 그래서 많은 데이터베이스가 쓰기를 다음 체크포인트 전까지 천천히 나눠 흘립니다.

다른 분야의 체크포인트

체크포인트라는 말은 데이터베이스 밖에서도 흔합니다. 머신러닝에서는 학습 도중의 모델 가중치를 파일로 저장한 것을 가리킵니다. 학습이 중간에 끊기면 처음이 아니라 그 파일부터 이어서 학습합니다.

스트림 처리에서는 흘러가는 데이터를 처리하던 중간 상태를 주기적으로 저장하는 일을 말합니다. 운영체제 쪽에서는 실행 중인 프로세스의 메모리와 상태를 파일로 떠 두었다가 나중에 되살리는 일도 체크포인트입니다.

분야는 달라도 뼈대는 같습니다. 중간 상태를 안전한 곳에 저장해서, 문제가 생겼을 때 처음이 아니라 그 지점부터 다시 시작합니다. 저장한 상태를 파일로 옮기는 일에는 대개 직렬화가 쓰입니다.

관련 항목

체크포인트가 기대는 로그 방식

WAL · 로그 · 리두 로그 · 언두 로그 · ARIES

체크포인트가 내려쓰는 저장 구조

페이지 · 버퍼 풀 · dirty 페이지 · 데이터 파일 · fsync · 페이지 캐시

체크포인트 뒤에 이어지는 복구와 정리 단계

크래시 복구 · 롤포워드 복구 · 세그먼트 · 재활용 · 아카이빙 · 백업과 복구 · 특정 시점 복구

체크포인트를 돌리는 방식

퍼지 체크포인트 · 샤프 체크포인트 · 증분 체크포인트

체크포인트 간격이 맞바꾸는 지표

쓰기 증폭 · 지연 · 처리량 · 복구 시간 목표

체크포인트를 쓰는 데이터베이스

PostgreSQL · SQLite · MySQL · InnoDB

체크포인트가 지키는 성질

트랜잭션 · 커밋 · 내구성 · ACID

같은 이름으로 중간 상태를 저장하는 다른 분야

머신러닝 · 스트림 처리 · 프로세스 · 직렬화 · 프로세스 마이그레이션

다른 이름: checkpoint · 검사점