文章目录
- 每日一句正能量
- 1. 背景与问题
- 2. 环境与数据
- 2.1 关系与文档表
- 2.2 向量表
- 2.3 时序表
- 3. 复现过程
- 3.1 统计当前数据规模
- 3.2 复现向量维度变化的容量影响
- 3.3 复现时序数据保留过长问题
- 4. 方案实施
- 4.1 建立容量估算公式
- 4.2 以业务指标驱动估算
- 4.3 关系表的分区和索引
- 4.4 文档存储策略
- 4.5 时序数据分层保留
- 4.6 向量容量与索引规划
- 4.7 压测模型
- 4.8 容量监控 SQL
- 4.9 容量告警阈值
- 5. 结果对比
- 6. 风险与复盘
- 6.1 常见风险
- 6.2 上线检查清单
- 6.3 复盘结论
每日一句正能量
生命的境界:山海在心,从容于行
“心有山海,静而无边。”
内心拥有山海般的壮阔与深度,因而能获得无边无际的宁静。
1. 背景与问题
快速增长业务在早期最容易出现一种错觉:数据库当前还能运行,所以容量暂时没有问题。
这种判断只适用于数据量和访问量都比较稳定的系统。对于同时包含关系、文档、时序和向量数据的平台,容量增长通常不是线性的,也不是只增加几张表那么简单。
以企业知识库和智能问答平台为例,一篇文档可能经历如下处理:
原始文件 -> 文档正文 -> 文档分段 -> 多个知识块 -> 每个知识块一个向量 -> 向量索引 -> 查询日志和访问审计如果一篇文档平均切分为 12 个知识块,那么新增 10 万篇文档可能产生 120 万条向量记录。与此同时,向量索引、WAL、备份文件和查询日志还会产生额外空间。
平台中不同数据的增长特点不同:
| 数据类型 | 主要增长来源 | 典型增长特点 |
|---|---|---|
| 关系数据 | 客户、权限、任务、订单 | 行数稳定增长,索引较多 |
| 文档数据 | PDF、HTML、工单、附件 | 单行大小差异大 |
| 时序数据 | 指标、日志、访问事件 | 高频写入,按时间增长 |
| 向量数据 | 文档块、会话摘要 | 与分块数量和模型维度相关 |
| 索引数据 | B-tree、GIN、HNSW | 通常大于表本身的预期 |
| WAL 与备份 | 写入、归档、快照 | 受更新峰值和保留周期影响 |
容量规划需要回答:
未来 6 个月需要多少磁盘? 向量维度从 768 升到 1536 后增加多少? 时序数据保留 30 天还是 180 天? 索引重建是否需要额外临时空间? 高峰导入和在线查询是否争抢 CPU、IO 和内存? 备份保留策略是否会耗尽对象存储?本文建立一个可执行的估算模型,并结合压测验证误差。
2. 环境与数据
测试环境:
| 组件 | 用途 |
|---|---|
| PostgreSQL 15 | 关系、文档元数据和向量表 |
| pgvector | 向量存储与 HNSW 索引 |
| TimescaleDB | 指标、日志和访问事件 |
| JSONB | 标签、扩展属性和动态字段 |
| 对象存储 | 原始文档、附件和备份 |
| 压测工具 | 模拟写入、检索和混合负载 |
| 监控系统 | CPU、内存、IO、WAL 和延迟 |
2.1 关系与文档表
CREATETABLEknowledge_document(document_id UUIDPRIMARYKEY,tenant_idTEXTNOTNULL,titleTEXTNOTNULL,contentTEXTNOTNULL,content_hashTEXTNOTNULL,version_noINTNOTNULLDEFAULT1,statusTEXTNOTNULLDEFAULT'published',metadata JSONBNOTNULLDEFAULT'{}'::jsonb,created_at TIMESTAMPTZNOTNULLDEFAULTnow(),updated_at TIMESTAMPTZNOTNULLDEFAULTnow());CREATEINDEXidx_document_tenant_statusONknowledge_document(tenant_id,status,updated_atDESC);CREATEINDEXidx_document_metadataONknowledge_documentUSINGGIN(metadata);2.2 向量表
CREATEEXTENSIONIFNOTEXISTSvector;CREATETABLEknowledge_chunk(chunk_id UUIDPRIMARYKEY,document_id UUIDNOTNULLREFERENCESknowledge_document(document_id),tenant_idTEXTNOTNULL,version_noINTNOTNULL,chunk_noINTNOTNULL,contentTEXTNOTNULL,embedding_modelTEXTNOTNULL,embedding_dimensionINTNOTNULL,embedding VECTOR(768)NOTNULL,metadata JSONBNOTNULLDEFAULT'{}'::jsonb,statusTEXTNOTNULLDEFAULT'active',UNIQUE(document_id,version_no,chunk_no));CREATEINDEXidx_chunk_embedding_hnswONknowledge_chunkUSINGhnsw(embedding vector_cosine_ops);CREATEINDEXidx_chunk_tenant_statusONknowledge_chunk(tenant_id,status,document_id);2.3 时序表
CREATETABLEplatform_metric(ts TIMESTAMPTZNOTNULL,tenant_idTEXTNOTNULL,metric_nameTEXTNOTNULL,entity_idTEXTNOTNULL,valueDOUBLEPRECISIONNOTNULL,labels JSONBNOTNULLDEFAULT'{}'::jsonb);SELECTcreate_hypertable('platform_metric',by_range('ts'),if_not_exists=>TRUE);3. 复现过程
3.1 统计当前数据规模
SELECT'document'ASdata_type,count(*)ASrow_count,pg_total_relation_size('knowledge_document')AStotal_bytesFROMknowledge_documentUNIONALLSELECT'chunk',count(*),pg_total_relation_size('knowledge_chunk')FROMknowledge_chunk;统计向量索引:
SELECTindexrelname,pg_size_pretty(pg_relation_size(indexrelid))ASindex_sizeFROMpg_stat_user_indexesWHERErelname='knowledge_chunk';统计时序数据:
SELECTmetric_name,count(*)ASrow_count,min(ts)ASmin_ts,max(ts)ASmax_tsFROMplatform_metricGROUPBYmetric_nameORDERBYrow_countDESC;3.2 复现向量维度变化的容量影响
768 维单精度向量的理论数据部分约为:
768 × 4 字节 = 3072 字节1536 维时:
1536 × 4 字节 = 6144 字节实际占用还包括:
行头 可变字段 TOAST 索引项 HNSW 图结构 WAL 备份副本因此,向量维度翻倍后,最终存储不一定只增加两倍,索引和写放大可能让实际增幅更高。
3.3 复现时序数据保留过长问题
假设系统每秒写入 5000 条指标:
每日行数 = 5000 × 86400 = 432,000,000 行如果每行平均占用 120 字节,仅数据部分就是:
432,000,000 × 120 ≈ 51.8 GB / 天加上索引、WAL、压缩前副本和备份后,实际容量可能达到数据部分的 1.5 到 3 倍。
因此,时序表不能只按“每天多少行”规划,还要计算:
保留周期 压缩比例 索引数量 写入峰值 备份保留 查询窗口4. 方案实施
4.1 建立容量估算公式
总容量可以拆分为:
总容量 = 关系表容量 + 文档容量 + 向量表容量 + 向量索引容量 + 时序热数据容量 + 时序归档容量 + WAL 保留空间 + 备份空间 + 临时维护空间 + 安全余量关系数据估算:
relation_size = row_count × avg_row_bytes + btree_index_size + gin_index_size向量数据估算:
vector_data_size = vector_count × (dimension × 4 + row_overhead)向量索引估算不能完全依赖理论公式,应使用小规模样本测量比例:
vector_index_size = vector_data_size × observed_index_ratio时序数据估算:
daily_ts_size = events_per_second × 86400 × avg_row_bytes × index_factor最终容量:
planned_capacity = current_size + monthly_growth × planning_months + backup_retention_size + maintenance_headroom建议保留至少 25% 至 35% 的安全余量,重建 HNSW 或大规模排序时则需要更多临时空间。
4.2 以业务指标驱动估算
不应只用“数据库总行数”规划。应从业务变量出发:
每日新增文档数 平均文档字数 平均切块数 Embedding 维度 每日用户查询数 每次查询候选数 每秒指标写入量 时序保留天数 文档版本保留数量 向量模型并存数量示例:
每日新增文档:20,000 平均每篇切块:8 每日新增向量:160,000 向量维度:768 时序写入:3,000 行/秒 时序保留:90 天 模型并存:2 个版本年度向量数量:
160,000 × 365 = 58,400,000 条如果模型升级后新旧版本并存一段时间,峰值向量量可能接近:
58,400,000 × 2 = 116,800,000 条模型升级和灰度阶段必须单独规划容量,不能只看稳定运行时的单模型规模。
4.3 关系表的分区和索引
大表可以按租户、业务域或时间进行分区。文档和向量表通常更适合按租户或业务域拆分,时序表适合按时间分区。
关系索引示例:
CREATEINDEXidx_document_domain_statusONknowledge_document(tenant_id,status,updated_atDESC);CREATEINDEXidx_chunk_active_tenantONknowledge_chunk(tenant_id,document_id,version_no)WHEREstatus='active';不要为所有字段建立索引。每个索引都会增加:
存储空间 写入成本 WAL VACUUM 成本 备份体积4.4 文档存储策略
文档正文和原始附件可以分层:
数据库: 标题、正文摘要、content_hash、权限、版本、状态和检索字段。 对象存储: 原始 PDF、图片、Office 文件、压缩包和大附件。 向量表: 文档块正文、Embedding、模型版本和块级元数据。对于超长文档,数据库中可以保存搜索所需的规范化文本,原始文件放入对象存储。这样既能保证检索,又能减少数据库膨胀。
4.5 时序数据分层保留
推荐把时序数据分为:
热数据:最近 7 至 30 天,在线查询; 温数据:最近 90 至 180 天,压缩保存; 冷数据:更早数据,归档到对象存储或低成本数据库。压缩统计:
SELECTtime_bucket('1 day',ts)ASday,metric_name,count(*)ASrows,avg(value)ASavg_value,max(value)ASmax_valueFROMplatform_metricWHEREts>=now()-interval'30 days'GROUPBYday,metric_nameORDERBYday,metric_name;归档前需要确认业务是否仍需要原始粒度。故障复盘常常需要秒级数据,不能因为容量压力直接全部聚合成小时级。
4.6 向量容量与索引规划
向量容量估算:
单条向量理论大小 = 维度 × 4 字节例如 768 维:
768 × 4 = 3072 字节1000 万条向量理论数据量:
10,000,000 × 3072 ≈ 30.72 GB实际还要加上:
行开销 文档块正文 JSONB 元数据 HNSW 索引 WAL 备份 旧模型并存如果向量表同时保存较长的content,实际容量可能远高于理论向量大小。因此可以考虑:
向量表只保存必要的块文本和元数据; 完整正文放在文档表或对象存储; 查询阶段通过 document_id 回表; 模型升级期间使用独立向量表。4.7 压测模型
压测要同时模拟写入和查询:
写入负载: - 每秒新增文档 - 每秒新增向量 - 文档更新比例 - 删除比例 查询负载: - 关键词查询 - 向量 Top-K - 带 JSONB 过滤 - 关系与向量联合查询 - 时序窗口查询 维护负载: - VACUUM - 索引构建 - 备份 - WAL 归档真实联合查询:
WITHeligible_docsAS(SELECTd.document_id,d.title,d.metadataFROMknowledge_document dWHEREd.tenant_id='tenant_a'ANDd.status='published'ANDd.metadata @>'{"domain":"database"}'),vector_candidatesAS(SELECTc.document_id,c.chunk_id,1-(c.embedding<=>$1::vector)ASsemantic_scoreFROMknowledge_chunk cJOINeligible_docs dONd.document_id=c.document_idWHEREc.tenant_id='tenant_a'ANDc.status='active'ORDERBYc.embedding<=>$1::vectorLIMIT100)SELECTv.chunk_id,v.document_id,d.title,d.metadata,v.semantic_scoreFROMvector_candidates vJOINeligible_docs dONd.document_id=v.document_idORDERBYv.semantic_scoreDESCLIMIT10;关联时间序列指标:
SELECTts,metric_name,valueFROMplatform_metricWHEREtenant_id='tenant_a'ANDentity_id=$1ANDts>=now()-interval'15 minutes'ORDERBYts,metric_name;4.8 容量监控 SQL
数据库和索引空间:
SELECTrelname,pg_size_pretty(pg_total_relation_size(relid))AStotal_size,pg_size_pretty(pg_relation_size(relid))AStable_size,pg_size_pretty(pg_indexes_size(relid))ASindex_sizeFROMpg_catalog.pg_statio_user_tablesORDERBYpg_total_relation_size(relid)DESC;表增长趋势:
CREATETABLEcapacity_metric(ts TIMESTAMPTZNOTNULL,asset_nameTEXTNOTNULL,metric_nameTEXTNOTNULL,metric_valueDOUBLEPRECISIONNOTNULL,labels JSONBNOTNULLDEFAULT'{}'::jsonb);SELECTcreate_hypertable('capacity_metric',by_range('ts'),if_not_exists=>TRUE);查询近 30 天容量趋势:
SELECTtime_bucket('1 day',ts)ASday,asset_name,metric_name,max(metric_value)ASmax_valueFROMcapacity_metricWHEREts>=now()-interval'30 days'GROUPBYday,asset_name,metric_nameORDERBYday,asset_name;4.9 容量告警阈值
建议设置分层阈值:
| 指标 | 观察 | 告警 | 紧急 |
|---|---|---|---|
| 数据库磁盘使用率 | 65% | 75% | 85% |
| WAL 保留空间 | 10 GB | 30 GB | 60 GB |
| 向量索引增长率 | 基线 + 10% | 基线 + 25% | 基线 + 50% |
| 时序写入延迟 | 100 ms | 300 ms | 800 ms |
| 向量查询 P95 | 150 ms | 300 ms | 600 ms |
| 备份窗口 | 目标 + 10% | 目标 + 25% | 超过恢复窗口 |
阈值不能脱离磁盘总容量和业务增长速度。75% 使用率在小磁盘上可能很危险,在大磁盘上则可能仍有充足时间扩容。
5. 结果对比
以下为示例压测结果,模拟每日新增 2 万篇文档、平均每篇 8 个知识块、每秒 3000 条时序写入。
| 方案 | 6 个月预计容量 | 向量查询 P95 | 备份窗口 | 扩容复杂度 |
|---|---|---|---|---|
| 所有正文和附件放数据库 | 14.8 TB | 210 ms | 8 小时 | 高 |
| 正文入库、附件对象存储 | 9.6 TB | 175 ms | 5 小时 | 中 |
| 文档分层 + 时序压缩 + 向量分表 | 6.4 TB | 128 ms | 3.2 小时 | 中 |
| 分层 + 旧模型清理 + 冷数据归档 | 4.9 TB | 135 ms | 2.4 小时 | 可控 |
示例只用于说明估算方式,不能作为具体资源采购依据。
压测需要比较以下场景:
1. 只有关系查询。 2. 只有向量 Top-K 查询。 3. 关系过滤 + 向量排序。 4. 关系过滤 + JSONB 标签 + 向量排序。 5. 向量查询后关联时序窗口。 6. 导入、备份和在线查询同时运行。真实查询执行计划:
EXPLAIN(ANALYZE,BUFFERS)SELECTc.chunk_id,d.title,1-(c.embedding<=>$1::vector)ASsimilarityFROMknowledge_chunk cJOINknowledge_document dONd.document_id=c.document_idWHEREc.tenant_id='tenant_a'ANDc.status='active'ANDd.status='published'ANDd.metadata @>'{"domain":"database"}'ORDERBYc.embedding<=>$1::vectorLIMIT20;结果评估不应只看平均延迟,还应观察:
P50、P95、P99; 导入期间延迟; 索引构建期间延迟; 磁盘增长; WAL 峰值; 备份窗口; 恢复耗时; Top-K 结果稳定性。容量规划建议至少预留:
25% 至 35% 常规安全余量; 一次索引重建所需临时空间; 一次全量备份所需空间; 模型升级期间新旧向量并存空间; 故障恢复和临时导入空间。6. 风险与复盘
6.1 常见风险
| 风险 | 表现 | 应对 |
|---|---|---|
| 只按当前数据量规划 | 数月后空间耗尽 | 使用增长模型和峰值模型 |
| 忽略向量索引 | 表空间够但索引空间不足 | 单独统计向量索引 |
| 忽略模型并存 | 模型升级时磁盘翻倍 | 提前计算双版本容量 |
| 时序无限保留 | 写入稳定但空间持续增长 | 热温冷分层和压缩 |
| 备份占用未计算 | 备份仓库先耗尽 | 规划保留周期和副本数 |
| 只压测查询 | 导入和备份影响线上 | 采用混合负载压测 |
| JSONB 过度使用 | 过滤性能不稳定 | 核心字段结构化 |
| 估算不含临时空间 | 索引重建失败 | 预留维护余量 |
| 只看平均值 | 长尾请求超时 | 重点监控 P95/P99 |
6.2 上线检查清单
[ ] 已拆分关系、文档、时序、向量、索引和备份容量 [ ] 已记录每日新增文档、文档块和向量数量 [ ] 已计算向量维度、模型并存和索引增长 [ ] 已计算时序写入速率、保留周期和压缩比例 [ ] 已计算 WAL、备份、副本和恢复临时空间 [ ] 已完成关系、向量、时序混合负载压测 [ ] 已观察导入、备份、索引构建期间的在线延迟 [ ] 已设置磁盘、WAL、P95 和增长率告警 [ ] 已定义热温冷数据分层和归档策略 [ ] 已定义扩容、降级、清理和回滚流程 [ ] 已验证真实联合查询和权限过滤 [ ] RTO、RPO 和未来六个月资源预算已确认6.3 复盘结论
多模场景下的容量规划,不能只把数据库容量理解成“数据行数乘以平均行大小”。真正的容量模型应包括:
业务数据; 文档正文; 向量字段; 向量索引; 关系索引; JSONB 索引; 时序热数据; 时序归档; WAL; 备份副本; 索引重建临时空间; 模型升级并存空间; 安全余量。关系、文档、时序和向量数据的规划重点不同:
关系数据关注行数、索引和事务写放大; 文档数据关注正文大小、版本和附件分层; 时序数据关注每秒写入、时间保留和压缩; 向量数据关注维度、分块数量、模型并存和索引比例。推荐采用以下容量规划流程:
采集当前基线 -> 建立业务增长假设 -> 分别估算四类数据和索引 -> 加入 WAL、备份和维护空间 -> 执行混合负载压测 -> 按月滚动预测 -> 设置阈值和扩容窗口 -> 通过真实查询验证体验最终验收需要同时满足:
- 未来规划周期内磁盘和备份空间足够。
- 向量索引重建有临时空间。
- 时序写入和查询在高峰期不互相拖垮。
- 关系过滤、文档标签和向量检索可以联合执行。
- 容量增长趋势、查询延迟和资源消耗可以被持续观测。
- 扩容、归档、降级和恢复都有明确操作步骤。
容量规划的价值不在于预测一个绝对准确的数字,而在于提前看见增长路径、资源瓶颈和故障窗口,让数据库扩容从临时救火变成可验证的工程计划。
转载自:
欢迎 👍点赞✍评论⭐收藏,欢迎指正