Neon Pageserver Storage Shard 扩容分析

Neon pageserver 的"扩容"通过 Storage Shard 机制实现。这不是传统意义上对单个 pageserver 水平扩展,而是两层扩容策略:

1. Tenant Sharding(分片) —将租户数据打散到多个 pageserver 节点上
2. Node Scale-out(节点伸缩) —增减 pageserver 物理节点

两者由 storage controller 统一管理。

---
一、架构总览

架构总览

Neon Storage Shard 架构概览
用户/控制面
neon_local CLI / storcon_cli / REST API
↓ HTTP calls
Storage Controller (Rust)
Scheduler
(AZ感知)
Reconciler
(执行差异)
TenantShard
(Intent/Observed)
DB: PostgreSQL (tenant_shards, nodes 表)
↓ gRPC/HTTP
Pageserver
(shard 0)
Pageserver
(shard 1)
Pageserver
(shard N)

关键角色

组件位置职责
Storage Controllerstorage_controller/src/service.rs (~10K行)核心编排器,管理所有 shard placement、split、migration
Pageserverpageserver/数据平面,存储实际 data layers
Tenant Managerpageserver/src/tenant/mgr.rs管理本地 shard 集合
DBstorage_controller/migrations/持久化 tenant_shards + nodes

---
二、Shard 概念模型

核心类型定义在 libs/utils/src/shard.rs 和 libs/pageserver_api/src/shard.rs:

┌────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────┐
│ 类型 │ 说明 │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ TenantShardId = {tenant_id, shard_number, shard_count} │ 全局唯一标识 │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ ShardCount (u8) │ 总分片数,0=unsharded │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ ShardNumber (u8) │ 0-based 索引,最多 255 │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ ShardStripeSize │ 默认 4096 页(16 MiB),决定连续 block 同放一个 shard │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ ShardIdentity │ {number, count, stripe_size, layout} —包含 key→shard映射 │
└────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────┘

Key 到 Shard 的映射用 MurmurHash32(与 Postgres smgr 兼容):
hash = murmurhash32(rel_node) + murmurhash32(block_num / stripe_size)
shard_number = hash % shard_count

数据分三类:
- local —只存一个 shard
- global —存所有 shard(如 rel_size)
- disposable —split 后可丢弃(不属于本 shard 的数据)

---
三、三种扩容操作

1. Tenant Shard Split(租户分片拆分)

将租户从一个 shard 拆成多个 shards,分布到更多 pageserver 上。

API: PUT /control/v1/tenant/:tenant_id/shard_split

触发方式:
# CLI
storcon_cli tenant-shard-split --tenant-id=<id> --new-shard-count=N

# API
curl -X PUT http://controller:PORT/control/v1/tenant/<tid>/shard_split \
-d '{"new_shard_count": N}'

执行流程 (service.rs →tenant_shard_split()):
1. 标记父 shard 为 Splitting(数据库锁)
2. 创建 child shard 行(继承 generation)
3. 对每个 pageserver 上的 parent shard 调用 PUT /v1/tenant/<id>/shard_split
4. Pageserver 侧执行 (mgr.rs →do_shard_split()):
- Phase 1: Prepare —下载 index_part.json,上传到各 child remote path
- Phase 2: Hardlink —硬链接 resident layer files(零拷贝冷分片)
- Phase 3: Spawn children —创建 child location
- Phase 4: WAL catch-up —等 children 追平 WAL
- Phase 5: Shutdown parent —关闭 parent shard
- Phase 6: Release lock —清理 splitting flag
5. 完成 —DB 中删除 parent 行,child 行 splitting=0
6. 后台 warmup —child shard 在 secondary mode 下预热

限制:
- 只能 2 的幂次扩展(1→2,2→4,4→8...)
- stripe_size 只在第一次 split (1→N)时能设,之后不可改

2. Node Drain/Fill/Delete(节点级别伸缩)

Drain —把所有 shard 从一个 pageserver 迁出:
curl -X PUT http://controller:PORT/control/v1/node/<node_id>/drain
- 最多并发 64 个 reconcile
- 优先用 warm secondary 做 cutover(零停机迁移)
- 用于维护/退役节点

Fill —把 shard 迁回到新节点:
curl -X PUT http://controller:PORT/control/v1/node/<node_id>/fill
反向操作,配合 drain 用于滚动升级

Delete —物理删除节点:
curl -X DELETE http://controller:PORT/control/v1/node/<node_id>/delete
先 drain 完再 delete

3. Single Shard Migrate(单 shard 迁移)

手动指定某个 shard 迁到特定 pageserver:
curl -X PUT http://controller:PORT/control/v1/tenant/<shard_id>/migrate \
-d '{"node_id": <target>}'

---
四、自动扩缩容

HSCC (Hyper Scalable Compute Controller) 利用以下指标驱动 autoscaling:

sql_exporter_autoscaling 暴露的指标(端口 9499)

从 neon_collector_autoscaling.jsonnet 引入,全部跟 LFC 相关:

┌────────────────────────────────────────────────────────────┬────────────────────────────────────────────┐
│ 指标名 │ 说明 │
├────────────────────────────────────────────────────────────┼────────────────────────────────────────────┤
│ lfc_approximate_working_set_size_seconds{duration_seconds} │ 过去 1~60 分钟每个窗口的工作集大小(核心) │
├────────────────────────────────────────────────────────────┼────────────────────────────────────────────┤
│ lfc_cache_size_limit │ LFC 缓存上限 │
├────────────────────────────────────────────────────────────┼────────────────────────────────────────────┤
│ lfc_hits / lfc_misses │ 缓存命中/未命中计数 │
├────────────────────────────────────────────────────────────┼────────────────────────────────────────────┤
│ lfc_used / lfc_writes │ 缓存使用量和写入量 │
└────────────────────────────────────────────────────────────┴────────────────────────────────────────────┘

其中 lfc_approximate_working_set_size_windows 是最关键的 —看历史 working set 趋势来判断是否需要 split。

Auto-Split 机制

见 test_runner/performance/test_sharding_autosplit.py —storage controller 监控 tenant size / working set,超过阈值时自动触发 split。

---
五、关键文件清单

┌──────────────────────────────────────────────────────┬──────────────────────────────────────────────────┐
│ 文件 │ 内容 │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/service.rs │ 核心编排:split/migrate/drain/fill/reconcile_all │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/tenant_shard.rs │ TenantShard 状态机 (Intent/Observed/Policy) │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/scheduler.rs │ 节点打分/AZ感知调度 │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/reconciler.rs │ 将 intent diff 转化为 pageserver API 调用 │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/http.rs │ REST API 端点注册 │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/background_node_operations.rs │ Drain/Fill/Delete │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/persistence.rs │ DB CRUD (tenant_shards/nodes 表) │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/src/schema.rs │ Diesel table definitions │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ storage_controller/migrations/* │ DB schema evolution │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ libs/utils/src/shard.rs │ ShardCount/ShardNumber/TenantShardId │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ libs/pageserver_api/src/shard.rs │ ShardIdentity/key_to_shard_number │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ pageserver/src/tenant/mgr.rs │ do_shard_split() —pageserver 侧 split 逻辑 │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ pageserver/src/http/routes.rs │ pageserver API endpoint │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ control_plane/storcon_cli/src/main.rs │ CLI 入口 │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ compute/vm-image-spec-bookworm.yaml │ VM 中 sql_exporter 启动配置 │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ docs/rfcs/2025-02-14-storage-controller.md │ Storage Controller RFC │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ docs/rfcs/028-pageserver-migration.md │ Zero-downtime migration procedure │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ test_runner/regress/test_sharding.py │ 分片功能测试 │
└──────────────────────────────────────────────────────┴──────────────────────────────────────────────────┘

---
六、验证方案

1. 查看当前 shard 分布: curl http://controller:PORT/debug/v1/tenant/<tenant_id>
2. 定位 shard 在哪台 pageserver: curl http://controller:PORT/debug/v1/tenant/<tid>/locate
3. 手动 split: 用 storcon_cli tenant-shard-split 或 HTTP API
4. 观察 reconciliation: 看 /debug/v1/tenant/<tid>/shards 的 intent vs observed
5. 跑 test_sharding.py 确认 split 后数据正确性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值