
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: VecHeatMapTimeline, // 该租户的所有 timeline pub(super) upload_period_ms: Optionu128, // 上传周期提示 }一个租户Tenant可能包含多个时间线Timeline例如主分支和多个开发分支因此 HeatMap 会包含该租户下所有 Timeline 的层文件列表。2. 有多少数据HeatMap 本身是元数据其数据量远小于实际存储的层文件。数据量估算假设一个典型生产租户的配置参数典型值说明Timeline 数量1-10主分支 几个活跃分支每个 Timeline 的层文件数50-500取决于 WAL 写入量单个层文件大小1MB-128MBL0 较小L1/Image 较大HeatMap JSON 大小50KB-500KB纯元数据不含实际数据举例说明以一个名为orders-db的电商核心库租户为例Timeline 1主分支1TB 数据L1 Image Layer: 128MB × 3 个 384MBL1 Delta Layer: 32MB × 10 个 320MBL0 Delta Layer: 4MB × 20 个 80MBHeatMap 条目33 个层文件Timeline 2分支100GB 数据约 10 个层文件HeatMap JSON 总大小约 150KB数据量总结项目大小HeatMap JSON 元数据50KB-500KB层文件总大小S3 中几百 MB 到几 GB次级缓存大小热层根据热度分数通常 10-100GB3. 生产时 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 包含多个租户吗❌ 否每个租户一个独立 HeatMapHeatMap 包含一个租户的多个 Timeline 吗✅ 是包含该租户所有 TimelineHeatMap JSON 大小50KB-500KB元数据次级缓存的实际数据量10GB-100GB取决于热度分数和阈值生成何时结束所有 Timeline 就绪 内容变化 上传成功生成频率默认 5 分钟一次heatmap_period300s6. 次级缓存过滤机制次级位置不需要缓存 HeatMap 中所有时间线的所有数据。系统设计了多层过滤机制来确保只下载和缓存最可能被访问的“热”数据从而显著减少次级位置的存储开销和网络带宽消耗。6.1 层级别过滤只下载“热层”HeatMap 中的每个层文件都有一个cold布尔标志。次级位置在下载时会通过hot_layers()方法过滤掉所有标记为冷coldtrue的层。// pageserver/src/tenant/secondary/heatmap.rs:86-91 impl HeatMapTimeline { // 只返回非冷层热层 pub(crate) fn hot_layers(self) - impl IteratorItem HeatMapLayer { self.layers.iter().filter(|l| !l.cold) // ← 过滤掉冷层 } // 返回所有层不常用 pub(crate) fn all_layers(self) - impl IteratorItem HeatMapLayer { self.layers.iter() } }实际下载逻辑中只遍历热层// pageserver/src/tenant/secondary/downloader.rs:1098 for layer in timeline.into_hot_layers() { // ← 只遍历热层 // 下载该层... }这意味着HeatMap 中包含了冷层coldtrue和热层coldfalse的完整元数据。次级位置只下载热层coldfalse。冷层被显式过滤掉完全不占用次级缓存空间。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 (LSN800) coldfalse ✓ 热层 │ │ ·L1 Delta (LSN500-800) coldfalse ✓ 热层 │ │ ·L1 Delta (LSN200-500) coldtrue ✗ 冷层 │ │ ·L0 Delta (LSN800-1000) coldfalse ✓ 热层 │ │ │ │ Timeline 2 (分支, 100GB 数据) │ │ ·Image Layer (LSN500) coldfalse ✓ 热层 │ │ ·L1 Delta (LSN300-500) coldtrue ✗ 冷层 │ │ ·L1 Delta (LSN100-300) coldtrue ✗ 冷层 │ │ │ │ Timeline 3-5 (已合并/归档) │ │ ·所有层都是 coldtrue ✗ 冷层 │ └─────────────────────────────────────────────────────────────┘次级位置实际缓存的内容为┌─────────────────────────────────────────────────────────────┐ │ Timeline 1: │ │ ·Image Layer (LSN800) ←下载 │ │ ·L1 Delta (LSN500-800) ←下载 │ │ ·L0 Delta (LSN800-1000) ←下载 │ │ ·L1 Delta (LSN200-500) ←不下载冷层 │ │ │ │ Timeline 2: │ │ ·Image Layer (LSN500) ←下载 │ │ ·L1 Delta (LSN300-500) ←不下载冷层 │ │ ·L1 Delta (LSN100-300) ←不下载冷层 │ │ │ │ Timeline 3-5: │ │ ·不下载冷层且可能因 deadline 跳过 │ └─────────────────────────────────────────────────────────────┘实际缓存大小约 200-400MB而非全量 1TB。6.5 关键设计总结过滤层级机制效果层级别只下载coldfalse的层排除长期不访问的冷层节省存储空间时间线级别deadline 超时停止处理避免下载过多时间线保证系统响应性租户级别按租户独立处理一个租户的下载不影响其他租户