HeatMap 概述
HeatMap 是按租户维度生成的,每个租户拥有一个独立的 HeatMap。
1. HeatMap 包含哪些数据?
HeatMap 记录了单个租户内所有 Timeline 的层文件(Layer)元数据,用于指导次级位置的缓存预热。
单租户 HeatMap 结构
一个 HeatMap 本质上是一个 JSON 文件,其核心结构如下:
{ "generation": 42, "upload_period_ms": 60000, "timelines": [ { "timeline_id": "abc123-def456...", "layers": [ { "name": "00000001...Image_0000000160", "metadata": { "file_size": 33554432, ... }, "access_time": "2026-08-02T10:00:00Z", "cold": false }, { "name": "00000001...Delta_0000000150", "metadata": { "file_size": 4194304, ... }, "access_time": "2026-08-02T09:55:00Z", "cold": false }, ... ] }, { "timeline_id": "xyz789-uvw012...", "layers": [...] } ] }关键数据结构
对应的 Rust 数据结构(简化)如下:
// pageserver/src/tenant/secondary/heatmap.rs:14-20 pub(crate) struct HeatMapTenant { pub(super) generation: Generation, // 主位置的 generation 号 pub(super) timelines: Vec<HeatMapTimeline>, // 该租户的所有 timeline pub(super) upload_period_ms: Option<u128>, // 上传周期提示 }一个租户(Tenant)可能包含多个时间线(Timeline,例如主分支和多个开发分支),因此 HeatMap 会包含该租户下所有 Timeline 的层文件列表。
2. 有多少数据?
HeatMap 本身是元数据,其数据量远小于实际存储的层文件。
数据量估算
假设一个典型生产租户的配置:
| 参数 | 典型值 | 说明 |
|---|---|---|
| Timeline 数量 | 1-10 | 主分支 + 几个活跃分支 |
| 每个 Timeline 的层文件数 | 50-500 | 取决于 WAL 写入量 |
| 单个层文件大小 | 1MB-128MB | L0 较小,L1/Image 较大 |
| HeatMap JSON 大小 | 50KB-500KB | 纯元数据,不含实际数据 |
举例说明
以一个名为orders-db的电商核心库租户为例:
- Timeline 1(主分支,1TB 数据)
- L1 Image Layer: 128MB × 3 个 = 384MB
- L1 Delta Layer: 32MB × 10 个 = 320MB
- L0 Delta Layer: 4MB × 20 个 = 80MB
- HeatMap 条目:33 个层文件
- Timeline 2(分支,100GB 数据)
- 约 10 个层文件
- HeatMap JSON 总大小:约 150KB
数据量总结
| 项目 | 大小 |
|---|---|
| HeatMap JSON 元数据 | 50KB-500KB |
| 层文件总大小(S3 中) | 几百 MB 到几 GB |
| 次级缓存大小(热层) | 根据热度分数,通常 10-100GB |
3. 生产时 HeatMap 生成何时结束?
HeatMap 的生成和上传是一个周期性、有条件的过程。
生成触发条件
由定时器驱动,周期由配置项heatmap_period决定(默认 300 秒)。
// pageserver/src/tenant/secondary/heatmap_uploader.rs:147-165 let period = match tenant.get_heatmap_period() { None => { // Heatmap 上传已禁用 return; } Some(period) => period, }; // 定时器触发:每隔 period 毫秒生成一次 state.next_upload = Some(now.checked_add(period_warmup(period)).unwrap_or(now));生成完成与上传流程
生成过程会遍历租户的所有 Timeline,收集层文件信息,并最终决定是否上传。
// pageserver/src/tenant/secondary/heatmap_uploader.rs:380-400 for (timeline_id, timeline) in timelines { let heatmap_timeline = timeline.generate_heatmap().await; match heatmap_timeline { None => { // 某个 timeline 未就绪 → 整个 HeatMap 放弃本次生成 return Ok(UploadHeatmapOutcome::Skipped); } Some(heatmap_timeline) => { heatmap.timelines.push(heatmap_timeline); } } } // 序列化 let bytes = serde_json::to_vec(&heatmap)?; // 去重:如果没有变化,不上传 let digest = md5::compute(&bytes); if Some(&digest) == last_upload.as_ref().map(|d| &d.uploaded_digest) { return Ok(UploadHeatmapOutcome::NoChange); } // 上传到 S3 remote_storage.upload(...).await?;生成结束的条件
| 条件 | 结果 |
|---|---|
| 所有 Timeline 都就绪 | ✅ 生成完成,上传 S3 |
| 任何一个 Timeline 未就绪 | ❌ 放弃本次生成 |
| HeatMap 内容无变化 | ⚠️ 不上传(节省带宽) |
| Tenant 正在关闭 | ❌ 取消生成 |
4. 多租户场景
在多租户环境中,HeatMap 的生成和管理是完全隔离的。
每个租户独立生成 HeatMap
┌─────────────────────────────────────────────────────────────┐ │ Pageserver(主位置) │ │ │ │ Tenant-1: HeatMap #1 ──→S3 (orders-tenant-heatmap.json) │ │ Tenant-2: HeatMap #2 ──→S3 (users-tenant-heatmap.json) │ │ Tenant-3: HeatMap #3 ──→S3 (products-tenant-heatmap.json)│ │ │ └─────────────────────────────────────────────────────────────┘ │ ▼ 对象存储 (S3) │ ┌───────────┼───────────┐ ▼ ▼ ▼ 次级 Tenant-1 次级 Tenant-2 次级 Tenant-3 下载 HeatMap #1 下载 HeatMap #2 下载 HeatMap #3
关键点
- 每个租户的 HeatMap 是独立的,不存在“多租户混合 HeatMap”。
- 次级位置按租户维度管理缓存,每个租户有独立的 HeatMap 和缓存策略。
- Storage Controller 为每个租户单独配置 Secondary 位置。
5. 总结
| 问题 | 答案 |
|---|---|
| HeatMap 包含多个租户吗? | ❌ 否,每个租户一个独立 HeatMap |
| HeatMap 包含一个租户的多个 Timeline 吗? | ✅ 是,包含该租户所有 Timeline |
| HeatMap JSON 大小? | 50KB-500KB(元数据) |
| 次级缓存的实际数据量? | 10GB-100GB(取决于热度分数和阈值) |
| 生成何时结束? | 所有 Timeline 就绪 + 内容变化 + 上传成功 |
| 生成频率? | 默认 5 分钟一次(heatmap_period=300s) |
6. 次级缓存过滤机制
次级位置不需要缓存 HeatMap 中所有时间线的所有数据。系统设计了多层过滤机制来确保只下载和缓存最可能被访问的“热”数据,从而显著减少次级位置的存储开销和网络带宽消耗。
6.1 层级别过滤:只下载“热层”
HeatMap 中的每个层文件都有一个cold布尔标志。次级位置在下载时,会通过hot_layers()方法过滤掉所有标记为冷(cold=true)的层。
// pageserver/src/tenant/secondary/heatmap.rs:86-91 impl HeatMapTimeline { // 只返回非冷层(热层) pub(crate) fn hot_layers(&self) -> impl Iterator<Item = &HeatMapLayer> { self.layers.iter().filter(|l| !l.cold) // ← 过滤掉冷层! } // 返回所有层(不常用) pub(crate) fn all_layers(&self) -> impl Iterator<Item = &HeatMapLayer> { self.layers.iter() } }实际下载逻辑中,只遍历热层:
// pageserver/src/tenant/secondary/downloader.rs:1098 for layer in timeline.into_hot_layers() { // ← 只遍历热层 // 下载该层... }这意味着:
- HeatMap 中包含了冷层(
cold=true)和热层(cold=false)的完整元数据。 - 次级位置只下载热层(
cold=false)。 - 冷层被显式过滤掉,完全不占用次级缓存空间。
6.2 时间线级别过滤:deadline 超时控制
为了避免单个租户的下载过程耗时过长,影响其他租户的缓存预热,系统设置了时间线级别的超时控制。
// pageserver/src/tenant/secondary/downloader.rs:829-845 // 计算下载截止时间:2 倍上传周期 let deadline = Instant::now() + period * 2; // 遍历所有时间线 for timeline in heatmap.timelines { // 检查是否超时 if Instant::now() > deadline { return (Err(UpdateError::Restart), touched); // ← 停止处理! } // 下载该时间线的层 self.download_timeline(timeline, timeline_state, deadline, ctx).await; }这意味着:
- 即使 HeatMap 中包含了 10 个时间线。
- 如果下载前 5 个时间线后已经超时。
- 剩下的 5 个时间线将不会被处理,本次下载任务提前结束。
6.3 下载过程日志
下载过程的日志也反映了过滤机制,只统计热层数量:
// pageserver/src/tenant/secondary/downloader.rs:1236 tracing::debug!( timeline_id=%timeline_id, "Downloading layers, {} in heatmap", timeline.hot_layers().count() // ← 只打印热层数量 );6.4 实际缓存情况示例
假设一个租户有 5 个时间线,其 HeatMap 内容如下:
┌─────────────────────────────────────────────────────────────┐ │ Tenant: orders-db │ │ Timeline 1 (主分支, 1TB 数据) │ │ ·Image Layer (LSN=800) cold=false ✓ 热层 │ │ ·L1 Delta (LSN=500-800) cold=false ✓ 热层 │ │ ·L1 Delta (LSN=200-500) cold=true ✗ 冷层 │ │ ·L0 Delta (LSN=800-1000) cold=false ✓ 热层 │ │ │ │ Timeline 2 (分支, 100GB 数据) │ │ ·Image Layer (LSN=500) cold=false ✓ 热层 │ │ ·L1 Delta (LSN=300-500) cold=true ✗ 冷层 │ │ ·L1 Delta (LSN=100-300) cold=true ✗ 冷层 │ │ │ │ Timeline 3-5 (已合并/归档) │ │ ·所有层都是 cold=true ✗ 冷层 │ └─────────────────────────────────────────────────────────────┘
次级位置实际缓存的内容为:
┌─────────────────────────────────────────────────────────────┐ │ Timeline 1: │ │ ·Image Layer (LSN=800) ←下载 │ │ ·L1 Delta (LSN=500-800) ←下载 │ │ ·L0 Delta (LSN=800-1000) ←下载 │ │ ·L1 Delta (LSN=200-500) ←不下载(冷层) │ │ │ │ Timeline 2: │ │ ·Image Layer (LSN=500) ←下载 │ │ ·L1 Delta (LSN=300-500) ←不下载(冷层) │ │ ·L1 Delta (LSN=100-300) ←不下载(冷层) │ │ │ │ Timeline 3-5: │ │ ·不下载(冷层,且可能因 deadline 跳过) │ └─────────────────────────────────────────────────────────────┘
实际缓存大小:约 200-400MB(而非全量 1TB+)。
6.5 关键设计总结
| 过滤层级 | 机制 | 效果 |
|---|---|---|
| 层级别 | 只下载cold=false的层 | 排除长期不访问的冷层,节省存储空间 |
| 时间线级别 | deadline 超时停止处理 | 避免下载过多时间线,保证系统响应性 |
| 租户级别 | 按租户独立处理 | 一个租户的下载不影响其他租户 |