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_lsn | pageserver가 처리한 마지막 WAL 레코드 끝 | 0/F4E5478 |
| disk_consistent_lsn | 여기까지는 로컬 디스크 layer에 flush 완료 | 0/F4D9190 |
| remote_consistent_lsn | 여기까지는 S3(MinIO)에 업로드 완료. pageserver가 죽어도 살아남는 지점 | 0/F4D9190 |
main에서는 last_record_lsn이 disk_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_distance256 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_distance | 256 MB | in-memory layer를 L0로 flush하는 WAL 양 |
| checkpoint_timeout | 10분 | WAL 양과 무관하게 마지막 flush 후 이 시간이 지나면 flush |
| image_creation_threshold | 3 | image layer 생성을 검토하는 delta layer 누적 수 |
| compaction_threshold | 10 | L0가 이만큼 쌓이면 L1 생성 |
| compaction_period | 20초 | compaction 필요 여부 확인 주기 |
| gc_horizon | 64 MB | 최신 LSN에서 이만큼은 항상 보존 |
| gc_period | 1시간 | GC 실행 주기 |
| pitr_interval | 7일 | 이 기간 안의 시점은 branch 생성 가능 |
tenant별 override는 PUT /v1/tenant/config에 tenant_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.json의 shared_buffers가 1 MB이기 때문입니다. upstream 테스트 설정을 그대로 가져온 값이므로 성능 측정에는 적합하지 않습니다. pageserver가 가장 많은 메모리를 사용합니다. page 재구성 캐시가 여기에 할당됩니다.
연습 문제
- main에 300 MB 이상 쓰기를 추가해 L0 layer가 하나 더 생기는 순간을
ls로 관찰하고,remote_consistent_lsn이 따라 올라가는 시간 차를 측정합니다. - compute2가 실행 중인 상태에서 복구 branch에 큰 UPDATE를 실행합니다. branch 디렉터리에 첫 layer 파일이 생기는 시점과 크기를 기록합니다.
- 위의
mc컨테이너 명령을 재사용해mc find m/neon/pageserver --name index_part.json으로 경로를 찾고,mc cat <경로>로 내용을 읽어 layer 파일 목록과 대조합니다. aliasm은docker run --rm컨테이너와 함께 사라지므로 한 컨테이너 실행 안에서 alias 설정과 조회를 함께 수행합니다.