본문으로 건너뛰기

3.4 Storage 관찰: LSN, Layer, MinIO

branch가 "복사 없는 복제"라는 말은 pageserver 디스크와 object storage를 직접 보면 확인됩니다. 이 장에서는 3.3까지 진행한 상태를 기준으로 timeline 상세 정보, layer 파일, MinIO 버킷, tenant 설정을 차례로 살펴봅니다.

Timeline 상세 정보

. ./.env # TENANT_ID, TIMELINE_ID, BRANCH_TIMELINE_ID 를 현재 셸로 읽는다
curl -s localhost:9898/v1/tenant/$TENANT_ID/timeline/$TIMELINE_ID | jq

main timeline에서 관심 있는 필드만 고르면 다음과 같습니다.

{
"timeline_id": "7eac85e3d7199edb170e1e283707fcdd",
"ancestor_timeline_id": null,
"ancestor_lsn": null,
"last_record_lsn": "0/F4E5478",
"disk_consistent_lsn": "0/F4D9190",
"remote_consistent_lsn": "0/F4D9190",
"current_logical_size": 88866816,
"current_physical_size": 315105280,
"pg_version": 17,
"state": "Active"
}

복구용 branch는 다음과 같습니다.

{
"timeline_id": "be2f70e318f31e16f55facced3a655bb",
"ancestor_timeline_id": "7eac85e3d7199edb170e1e283707fcdd",
"ancestor_lsn": "0/D902000",
"last_record_lsn": "0/10962E80",
"disk_consistent_lsn": "0/D902000",
"remote_consistent_lsn": "0/D902000",
"current_logical_size": 88850432,
"current_physical_size": 0
}

세 LSN의 관계는 pageserver의 내구성 단계를 보여 줍니다.

필드main 값
last_record_lsnpageserver가 처리한 마지막 WAL 레코드 끝0/F4E5478
disk_consistent_lsn여기까지는 로컬 디스크 layer에 flush 완료0/F4D9190
remote_consistent_lsn여기까지는 S3(MinIO)에 업로드 완료. pageserver가 죽어도 살아남는 지점0/F4D9190

main에서는 last_record_lsndisk_consistent_lsn보다 약 50 KB 앞서 있습니다. 그 구간의 WAL은 in-memory layer에만 있습니다. pageserver를 재시작하면 safekeeper에서 다시 받아 처리합니다. safekeeper가 WAL을 오래 보관하는 이유가 여기에 있습니다.

복구 branch의 disk_consistent_lsn은 분기점 0/D902000 그대로입니다. 이 timeline은 자체 layer를 아직 하나도 디스크에 쓰지 않았습니다. 따라서 current_physical_size가 0입니다. 반면 current_logical_size는 부모와 거의 같은 84 MB입니다.

Layer 파일 직접 보기

pageserver 컨테이너 안에서 timeline 디렉터리를 확인합니다. 경로 규칙은 tenants/<tenant>/timelines/<timeline>/입니다.

docker compose exec pageserver ls -la /data/.neon/tenants/$TENANT_ID/timelines/$TIMELINE_ID
23240704 000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014E8F20-00000000014E8F99-v1-00000001
291864576 000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014E8F99-000000000F4D9191-v1-00000001
0 ephemeral-5

파일명 구조는 2.3에서 설명한 내용과 같습니다. 앞부분 000...000-FFF...FFF는 key 범위입니다. 전체 key 공간을 덮으므로 L0 layer입니다. __ 뒤는 LSN 범위입니다.

  • 첫 파일: LSN 범위 [0/14E8F20, 0/14E8F99), 22 MB. layer의 LSN 범위는 시작 포함, 끝 제외입니다. initdb 결과물이 들어간 최초 layer입니다. status.sh에서 처음 확인한 physical size 22.1 MB가 이 파일입니다.
  • 둘째 파일: LSN 14E8F99부터 F4D9191까지, 278 MB. 50만 건 적재, 인덱스 생성, 1,000건 삽입, 25만 건 삭제가 모두 이 한 파일에 들어 있습니다. checkpoint_distance 256 MB를 넘자 in-memory layer가 flush됐습니다.
  • ephemeral-5: 현재 열려 있는 in-memory layer가 메모리 부족 시 spill할 파일입니다. 크기가 0이면 아직 spill이 발생하지 않은 상태입니다.

복구 branch 디렉터리에는 layer 파일이 없습니다. ephemeral-4 하나만 약 50 MB를 차지합니다. compute2 기동 때 생긴 WAL은 in-memory layer에 있습니다. 그중 일부가 ephemeral 파일에 저장된 상태입니다. branch가 부모 데이터를 위해 만든 파일은 하나도 없습니다.

L0 layer가 10개(compaction_threshold) 쌓이면 compaction이 key 범위에 따라 다시 나눈 L1 layer를 만듭니다. image layer 생성은 별도 조건인 image_creation_threshold(이 tenant 기본값 3)와 휴리스틱을 따르므로 L1 생성과 같은 시점에 보장되지는 않습니다. 이 실습 규모에서는 L0 두 개에서 끝났습니다. 따라서 compaction 결과를 보려면 수 GB 이상 쓰기가 필요합니다.

MinIO 버킷

pageserver와 safekeeper는 각자 prefix를 구분해 같은 neon 버킷에 업로드합니다. mc 클라이언트 컨테이너로 확인합니다.

docker run --rm --network neon-lab_default --entrypoint sh minio/mc -c \
"mc alias set m http://minio:9000 minio password >/dev/null && mc du m/neon/pageserver && mc du m/neon/safekeeper"
302MiB 7 objects neon/pageserver
326MiB 21 objects neon/safekeeper

pageserver prefix에는 위에서 확인한 layer 파일 2개가 올라가 있습니다. timeline별 index_part.json(layer 목록 메타데이터)도 함께 올라가 있습니다. safekeeper prefix에는 16 MB WAL segment가 21개 있습니다. safekeeper 3대가 같은 segment를 각각 올리지는 않습니다. quorum이 확정한 segment를 한 벌만 올립니다.

브라우저에서 http://localhost:9001에 접속합니다. minio / password로 로그인하면 같은 내용을 확인할 수 있습니다.

Tenant 설정 읽기

curl -s localhost:9898/v1/tenant/$TENANT_ID/config | jq '.effective_config | {checkpoint_distance, checkpoint_timeout, compaction_threshold, image_creation_threshold, compaction_period, gc_horizon, gc_period, pitr_interval}'
{
"checkpoint_distance": 268435456,
"checkpoint_timeout": "10m",
"compaction_threshold": 10,
"image_creation_threshold": 3,
"compaction_period": "20s",
"gc_horizon": 67108864,
"gc_period": "1h",
"pitr_interval": "7days"
}
설정영향
checkpoint_distance256 MBin-memory layer를 L0로 flush하는 WAL 양
checkpoint_timeout10분WAL 양과 무관하게 마지막 flush 후 이 시간이 지나면 flush
image_creation_threshold3image layer 생성을 검토하는 delta layer 누적 수
compaction_threshold10L0가 이만큼 쌓이면 L1 생성
compaction_period20초compaction 필요 여부 확인 주기
gc_horizon64 MB최신 LSN에서 이만큼은 항상 보존
gc_period1시간GC 실행 주기
pitr_interval7일이 기간 안의 시점은 branch 생성 가능

tenant별 override는 PUT /v1/tenant/configtenant_id를 포함한 본문({"tenant_id": "<tenant>", "pitr_interval": "1day"} 형태)을 보내 바꿉니다. 조회 경로(GET /v1/tenant/{tenant}/config)와 변경 경로가 다릅니다. 클라우드 서비스의 플랜별 history window는 결국 pitr_interval을 플랜에 맞게 설정한 값입니다.

Timeline 삭제와 부모 보호

자식이 있는 timeline은 삭제할 수 없습니다.

curl -s -w ' HTTP %{http_code}\n' -X DELETE localhost:9898/v1/tenant/$TENANT_ID/timeline/$TIMELINE_ID
{"msg":"Precondition failed: Cannot delete timeline which has child timelines: [be2f70e3..., 9ceb8759...]"} HTTP 412

자식은 compute를 중지한 뒤 삭제합니다. 삭제는 비동기로 처리됩니다. 202로 응답하고 잠시 뒤 목록에서 사라집니다.

docker compose --profile branch stop compute2
curl -s -w ' HTTP %{http_code}\n' -X DELETE localhost:9898/v1/tenant/$TENANT_ID/timeline/<feature-a id>
null HTTP 202

이 규칙 때문에 클라우드 서비스에서도 자식 branch가 있는 branch는 바로 삭제할 수 없습니다. 먼저 자식을 정리하거나 detach_ancestor로 독립시켜야 합니다. openapi에는 detach_ancestor 엔드포인트가 있습니다. 자식 timeline이 부모 layer에 의존하지 않도록 필요한 데이터를 복사해 root timeline으로 만드는 작업입니다.

자원 사용량

docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
neon-lab-pageserver-1 0.13% 146.5MiB
neon-lab-minio-1 0.03% 121.8MiB
neon-lab-compute1-1 0.63% 49.1MiB
neon-lab-compute2-1 0.98% 47.8MiB
neon-lab-safekeeper1-1 0.26% 25.8MiB
neon-lab-safekeeper2-1 0.36% 29.0MiB
neon-lab-safekeeper3-1 0.21% 22.7MiB
neon-lab-storage_broker-1 0.26% 7.2MiB

compute의 메모리가 50 MB 미만인 이유는 config.jsonshared_buffers가 1 MB이기 때문입니다. upstream 테스트 설정을 그대로 가져온 값이므로 성능 측정에는 적합하지 않습니다. pageserver가 가장 많은 메모리를 사용합니다. page 재구성 캐시가 여기에 할당됩니다.

연습 문제

  1. main에 300 MB 이상 쓰기를 추가해 L0 layer가 하나 더 생기는 순간을 ls로 관찰하고, remote_consistent_lsn이 따라 올라가는 시간 차를 측정합니다.
  2. compute2가 실행 중인 상태에서 복구 branch에 큰 UPDATE를 실행합니다. branch 디렉터리에 첫 layer 파일이 생기는 시점과 크기를 기록합니다.
  3. 위의 mc 컨테이너 명령을 재사용해 mc find m/neon/pageserver --name index_part.json으로 경로를 찾고, mc cat <경로>로 내용을 읽어 layer 파일 목록과 대조합니다. alias mdocker run --rm 컨테이너와 함께 사라지므로 한 컨테이너 실행 안에서 alias 설정과 조회를 함께 수행합니다.

참고