去年,我们帮助一个电商团队完成了从 PostgreSQL 到 Apache Doris 的迁移。他们的业务数据规模达到 80TB,已经为此搭建了三套独立的系统来分别支撑仪表盘、商品搜索和推荐向量服务。三个月后,团队发现自己在调试各系统之间的数据同步问题上花费的时间,比开发新功能还多。最终我们将所有业务收敛到 Apache Doris 一套系统上。做这个决定的核心理由很简单:维护一套经过验证的系统,比维护三套勉强拼凑的系统要省心得多。
这不是一个关于技术选型优劣的泛泛讨论,而是一个关于在数据规模增长到某个临界点后,如何正确识别架构瓶颈并做出有效应对的真实复盘。你的技术栈永远不应该比业务实际能够运维的复杂度更高。每增加一个系统,就意味着多出一份部署、监控、调试、值班和招聘专业人员的成本。目标是找到适合当前规模的最小可用基础设施。但首先,你需要确认自己是否真的遇到了一个值得解决的问题。
一、如何判断 PostgreSQL 是否真的遇到了瓶颈
1.1 缓慢逼近的性能天花板
PostgreSQL 的性能问题从来不会突然爆发,它们总是以不易察觉的方式缓慢积累。团队通常会在每个阶段尝试最直接的解决方案:添加索引、做分区、建反范式表、用物化视图、增加只读副本,或者引入 Citus 这样的分片工具。每一步都带来了暂时的缓解,但系统也变得越来越复杂、越来越难以扩展。最终团队会意识到一个根本问题:PostgreSQL 是为单机设计的,而不是为大规模分布式分析设计的。以下是一些需要警惕的信号:
| 信号 | 典型表现 | 介入阈值 |
|---|---|---|
| 仪表盘延迟 | 原本 2 秒返回的查询现在需要 20 秒 | 相同查询出现超过 10 倍的性能退化 |
| 查询超时 | 日志中出现 statement timeout | 分析类查询每小时超时超过 5 次 |
| 只读副本膨胀 | 为分析负载不断添加只读副本 | 超过 3 个只读副本专门用于分析 |
| 大表顺序扫描 | EXPLAIN ANALYZE 显示 Seq Scan | 在超过百万行的表上出现全表扫描 |
| 复制延迟 | 分析负载高峰时期只读副本落后于主库 | 仪表盘刷新期间延迟超过 5 秒 |
| 磁盘持续紧张 | 存储扩容成为常规应急操作 | 频繁进行存储容量紧急扩容 |
以下查询可以帮助你快速检查业务库中的全表扫描情况:
sql
SELECT schemaname, relname, seq_scan, seq_tup_read,
idx_scan, idx_tup_fetch,
CASE WHEN seq_scan > 0 THEN seq_tup_read / seq_scan
ELSE 0 END as avg_rows_per_seq_scan
FROM pg_stat_user_tables
WHERE seq_scan > 100
AND seq_tup_read / GREATEST(seq_scan, 1) > 100000
ORDER BY seq_tup_read DESC
LIMIT 10;
如果这个查询返回了平均每次顺序扫描超过 10 万行的表,说明你的分析查询正在进行大量全表扫描,而列式存储处理这类查询的效率通常是行式存储的 10 倍以上。
1.2 为什么只读副本解决不了根本问题
出现这些警告信号后,很多团队的第一反应是增加只读副本。但这样做并不能解决问题。只读副本解决的是并发问题,而不是查询性能问题。根本原因在于 OLTP 数据库与 OLAP 数据库在架构上存在本质差异,而这种差异是任何数量的只读副本都无法弥补的。
行存储与列存储的根本差异。PostgreSQL 按行存储数据,这在检索完整记录时非常高效。但当仪表盘查询从一张 50 列表格中只选择 2 列时,数据库仍然需要从磁盘读取完整的行,然后丢弃未使用的列。这意味着实际扫描的数据量远远超过了查询真正需要的数据量。列式数据库按列分别存储数据,查询时只读取需要的列,这对于分析型负载来说效率高得多。列式布局还能实现更高的压缩比,通常在 5 到 10 倍之间,而行式存储的压缩比大约只有 2 到 3 倍,因为相邻的值共享相同的数据类型。
逐行处理与向量化执行的差异。PostgreSQL 通过执行器逐行处理数据,每一行都要经过表达式求值、谓词检查和聚合计算。分析型数据库使用向量化执行,一次处理 1000 到 4000 个值的批次,利用 CPU 的 SIMD 指令在单次操作中完成对整个列的计算。扫描一千万行数据的查询,向量化执行比逐行处理快 10 到 50 倍。
单节点规划与分布式规划的差异。PostgreSQL 的查询优化器针对单机进行优化,一个查询只能在一台机器上运行。你不能把一个分析查询并行分发到多个只读副本上执行。只读副本能让你同时运行更多的查询,但每个单独的查询并不会变得更快。分析型数据库使用分布式查询规划器,将工作负载拆分到多个节点上。扫描 1TB 数据的查询会在 10 个节点上同时执行,每个节点处理 100GB,然后在协调节点合并结果。
| 对比维度 | PostgreSQL (OLTP) | 分析型数据库 (OLAP) |
|---|---|---|
| 存储布局 | 行式存储 | 列式存储 |
| 列选择性 | 读取所有列 | 只读取查询涉及的列 |
| 执行模型 | 逐行处理 | 向量化批处理 |
| 查询规划 | 单节点优化器 | 分布式查询规划器 |
| 并行能力 | 受限(单查询单节点) | 完整(单查询跨多节点) |
| 压缩比 | 2-3 倍 | 5-10 倍 |
无论增加多少只读副本,都无法弥合这个架构层面的鸿沟。你需要的是一个真正的分析型数据库。但在选择替代方案之前,你需要先理解一件事:你从 PostgreSQL 获得的能力可能比你意识到的要多得多。
二、你可能不知道自己正在使用 PostgreSQL 的哪些实时分析能力
2.1 PostgreSQL 不只是 OLTP 数据库
很多团队始终将 PostgreSQL 视为 OLTP 数据库,但实际上已经在不知不觉中把它用作了实时分析引擎。
在数据库世界中,有两个众所周知的极端:OLTP 以高并发事务、行级操作和亚毫秒级延迟为特征;批处理分析以大规模数据转换、分钟到小时级的延迟和高吞吐量为特征。两者之间是实时分析,要求数据的新鲜度达到亚秒级,且数据立即可用。
PostgreSQL 恰好位于 OLTP 和实时分析之间的位置。因为你把它归为 OLTP,所以很容易忽略那些实际上非常关键的实时分析能力。
亚秒级数据新鲜度。你的只读副本通常落后于主库不到 1 秒。仪表盘显示的是当前数据,而不是 10 分钟之前的数据。对于业务决策来说,这种实时性具有不可替代的价值。
实时更新。订单状态从待支付变更为已发货时,执行一条 UPDATE 语句,变更立即反映在分析结果中。无需批处理任务,无需等待下一个 ETL 窗口。如果新的分析系统不支持更新,整个数据管道就需要重构。
轻量级事务性 ETL。执行 INSERT INTO summary_table SELECT ... FROM orders GROUP BY ... 这个操作要么完全成功,要么完全失败。部分写入永远不会破坏你的聚合数据。这种事务性保证使数据管道维护变得可预期。
多表关联查询。BI 查询在一个 SQL 中关联订单表、用户表、商品表和物流表。查询优化器自动处理,无需考虑是否支持多表 JOIN。如果新的分析系统在 JOIN 能力上存在短板,大量现有的 BI 报表将无法迁移。
全文搜索。使用 tsvector 和 tsquery 搜索日志消息、商品描述和用户评论,不需要额外部署一套独立的搜索集群。这省去了跨系统数据同步的麻烦。
向量搜索。通过 pgvector,运行语义搜索和 AI 驱动的功能。嵌入向量与关系型数据存储在一起,不需要额外引入独立的向量数据库。数据的自然关联关系得以保留。
这六项能力你很可能已经习以为常了,直到迁移之后才发现它们的可贵。国内某零售团队就经历过这样的教训:他们的库存仪表盘在新分析系统上出了问题,因为新系统不支持 UPDATE 操作。他们不得不重建整个数据管道,用删除加重新插入的方式替代更新操作。原计划两周的迁移拖成了两个月。
大多数分析型数据库至少会牺牲上述能力中的一项。如果你知道哪些能力对自己是必需的,这可以接受;如果你是在迁移之后才发现,那就会很痛苦。
2.2 其他系统的取舍
在评估替代方案时,了解市场上其他系统能提供什么、不能提供什么是至关重要的。
以 Snowflake、Databricks、BigQuery 为代表的批处理数仓,擅长大规模历史分析和复杂数据转换,生态完善。但在实时数据新鲜度方面存在明显短板,数据延迟通常在分钟到小时级别。它们通常不内置文本搜索和向量搜索能力,需要额外集成独立的搜索系统。
以 ClickHouse、Druid、Pinot 为代表的实时分析数据库,在仅追加工作负载上查询速度极快,已经在 Uber 和 Cloudflare 等公司验证了处理数十亿事件的能力。但它们的事务支持有限,更新和删除操作受限或效率低下。多表关联 JOIN 是它们的弱项,更倾向于使用反范式化的单表查询。大多不内置全文搜索和向量搜索。
如果你的业务需要亚秒级数据新鲜度和实时更新能力,批处理数仓方案就会被排除。如果你需要事务支持和强关联查询能力,大部分实时分析数据库也无法单独胜任。剩下的选择只有两条路:要么组合使用实时分析数据库和批处理数仓,要么选择一个同时支持以上全部六项能力的全功能实时分析数据库。
三、为什么最终选择了 Apache Doris
3.1 PostgreSQL 原生能力在 Doris 中的对应实现
Doris 的设计目标之一就是保留那些 PostgreSQL 用户真正依赖的能力。以下是六项核心能力的对应实现:
亚秒级数据新鲜度。数据写入后数秒内即可查询,不需要等待批处理 ETL 窗口。对于物流补货场景,延迟要求通常在 1 到 2 秒内数据必须可见。在大多数场景下,Doris 能在 10 秒内完成数据可见。
实时更新。Unique Key 模型使 UPDATE 和 DELETE 按预期方式工作,变更在数秒内可见。Doris 的 Merge-on-Write 模式结合 Delete Bitmap 和 Primary Index 技术,在写入阶段为旧数据打上删除标记,查询时直接跳过已删除行,无需实时计算。补货业务、库存管理、订单处理和物流轨迹追踪都需要这种实时更新能力。
轻量级事务性 ETL。ACID 支持保证了 INSERT INTO SELECT 要么完全成功要么完全失败。在菜鸟的实践中,Doris 的实时更新能力确保了物流全流程状态变更数据的准确性和及时性,补货业务决策基于最新的库存状态,避免了缺货或过量采购。
多表关联查询。基于成本的优化器确保了复杂关联查询的稳定性能。菜鸟仓内数据产品的包裹生产进度监控场景涉及多张亿级别大表的 JOIN 和大量 AD-HOC 查询,迁移后平均查询响应时间降低了 72%。复杂场景下的多表关联聚合查询通常在 1 秒内返回,极复杂场景也能在 4 到 5 秒内完成。
全文搜索。内置倒排索引和 BM25 排序,不需要额外维护一套独立的搜索集群。菜鸟的物流轨迹查询和包裹状态追踪场景受益于此。
向量搜索。内置向量索引,语义搜索不需要额外的向量数据库。商品推荐和嵌入向量分析可以在同一个系统中完成。
Doris 从 0 开始在菜鸟落地,逐步验证和推广,到如今已经部署了 25 个以上集群、上万核的规模,覆盖 3 个地域,整个迁移过程未发生一起线上故障。菜鸟的实时数据架构已经逐步收敛,Doris 成为 OLAP 分析的最优选型。
3.2 架构之外的额外收益
Doris 除了提供能力对等的功能之外,还解决了几个随着 PostgreSQL 分析负载增长而逐渐暴露的运维难题。
存储成本优化。Doris 的列式存储和 5 到 10 倍的压缩比显著降低了存储成本。在菜鸟的包裹生产进度场景中,迁移到 Doris 后存储成本直接降低了 90%。对于电商业务,历史数据清理后可以释放出可观的存储空间,尤其是对于云上资源来说,云资源成本较为昂贵,这样做可以极大降低存储成本,将预算投入到更重要的业务中。
高并发点查能力。Doris 的单表主键查询 QPS 可达 1000 到 2000,查询响应时间在几十毫秒到 100 到 200 毫秒之间。这得益于主键索引技术和 LSM tree 存储结构的协同作用,能够快速定位和检索数据。在菜鸟的快递包裹生产进度场景中,点查能力直接支撑了仓库生产监控的实时性需求。
运维效率提升。将一个三系统拼凑的架构收敛为单一 Doris 集群后,每减少一个系统,就少了一套需要监控的组件、少了一条凌晨需要调试的集成链路、少了一家需要谈判的供应商。运维复杂度从三套独立系统的管理收敛为统一平台的管理,这是架构收敛带来的长期红利。
四、迁移方案与实施路径
4.1 迁移方式的选型矩阵
Doris 提供了多种从 PostgreSQL 迁移数据的方案,可以根据数据量、同步时效性和是否需要 CDC 进行选择。
| 方案 | 适用场景 | 同步模式 | 是否支持整库 | 是否支持增量 |
|---|---|---|---|---|
| Multi-Catalog | 一次性迁移、即席查询 | 离线按需 | 通过 SQL 实现 | 否 |
| Flink Doris Connector (CDC) | 实时全量 + 增量同步 | 实时 | 支持 | 支持 |
| Streaming Job | Doris 内置持续同步 | 流式持续 | 支持 | 支持 |
| 第三方工具 | 已有同步平台、可视化运维 | 离线/实时 | 支持 | 视工具而定 |
如果需要在不引入外部计算引擎的情况下完成迁移,推荐使用 Multi-Catalog 或 Streaming Job。如果需要实时增量同步,推荐使用 Flink Doris Connector 配合 CDC,或直接使用 Doris 内置的 Streaming Job。
4.2 数据同步策略与细节
数据同步是保障分析数据准确性与实时性的核心环节。
全量同步阶段用于初始化场景,通过 Multi-Catalog 将 PostgreSQL 表映射为外表后使用 CTAS 一步建表并导入。这种方式适合一次性大规模迁移,不需要手动在 Doris 端创建和源端一致的表结构,避免了繁琐的 DDL 转换工作。
增量同步阶段针对业务数据的实时变动。可以通过 Flink CDC 捕获 PostgreSQL 中的 INSERT、UPDATE、DELETE 操作日志实时传输至 Doris。Flink CDC 支持断点续传和数据去重,保证增量数据的完整性与一致性。
一个容易忽略的细节是 DDL 变更的同步。源端 PostgreSQL 的表结构发生变化时,需要同步更新 Doris 端的表定义,否则同步链路会中断。通过成熟的 CDC 工具可以自动捕获 DDL 变更并在 Doris 端执行,保证业务变更能够稳定进行。
4.3 核心验证与灰度迁移策略
菜鸟在 Doris 推广过程中积累了一套可复用的验证方法。他们选择了一个核心场景------仓内数据产品使用频率最高的包裹生产进度监控,而不是选择边缘场景做验证。理由是:如果在最重要的核心场景都无法验证通过,基本不可能推动业务侧做后续的迁移。这个场景是多表级联的 AD-HOC 场景,涉及众多维度指标组合和多张亿级大表 JOIN,且稳定性要求极高,不能容忍数据延迟和查询超时。
在验证过程中,菜鸟团队选择了新老集群双跑方案,并没有急于做灰度切流。他们利用流计算能力,1:1 回放线上 SQL,验证查询 RT 是否达到预期以及语法兼容性。持续跑了一段时间后,才开始进行仓库粒度的灰度切流。整个灰度过程中逐步对 Doris 集群扩容,直至 100% 流量全部切到 Doris。
2023 年的双 11,第一个小集群完成大促验证,集群规模约 300 个 CU。核心场景验证通过后,2024 年新财年确定大规模推广计划。9 月首批核心集群全部完成迁移,11 月第一次大规模部署征战双 11,在成本和稳定性上均表现出色。整个迁移过程未发生一起线上故障。
4.4 表结构设计的要点
为适配分库分表数据和实时分析需求,Doris 表结构设计需要遵循以下原则。
分区设计按时间维度或业务维度进行,减少查询时的数据扫描范围。对于电商订单数据,按月分区或按日分区是常见做法。分桶设计针对大表按高频查询字段进行,使数据均匀分布在 Doris 各个节点,充分发挥 MPP 并行计算能力。
表引擎选择方面,核心业务表采用 Merge-on-Write 表引擎,支持行级近实时更新;离线分析表采用 Aggregate 表引擎,通过预聚合提升聚合查询效率。Unique Key 模型用于处理 UPSERT 场景,适合订单状态持续更新的业务。
五、关键收益与量化结果
5.1 成本层面的收获
在菜鸟的包裹生产进度场景中,迁移到 Doris 后存储成本降低了 90%。在森马服饰的案例中,将 Elasticsearch 加分布式 MySQL 架构统一为 SelectDB 版后,复杂查询的 QPS 提升了 400%,达到 200 以上的 QPS。天眼查将 Hive、MySQL、PostgreSQL 和 Elasticsearch 的混合架构替换为 Apache Doris 后,数据写入效率提升了 75%,用户分群延迟降低了 70%。
5.2 稳定性层面的收获
在菜鸟的实践中,25 个以上集群遍布 3 个地域,日常上万核规模,整个迁移过程未发生一起线上故障。双 11 大促期间的成本和稳定性均表现出色,Doris 的高并发写入能力和强一致性保证了库存数据的准确性和实时性。补货业务的延迟要求基本在 1 到 2 秒内数据需要可见,Doris 能够在大多数场景下达到这一标准。
5.3 运维效率层面的收获
当将原先维持三套独立系统的架构收敛为 Doris 单一集群后,每减少一个系统,就少了一套监控配置、少了一条需要调试的数据管道、少了一家需要保持技术对齐的供应商。菜鸟的实时数据架构经过最近 3 年的优化和迭代已经逐步收敛,在 OLAP 方向上 Doris 已经成为最优选型,运维团队不需要再为多个系统的版本兼容、数据一致性和故障隔离耗费精力。
六、这不是技术选型的终点
PostgreSQL 是一个优秀的事务处理数据库,它理应在电商业务中继续担任在线事务处理的核心角色。当数据分析负载增长到 80TB 这个量级时,更好的选择不是在 PostgreSQL 这条路上继续加码,而是把分析查询迁移到专门为此设计的 Apache Doris 上,让 PostgreSQL 专注于自己最擅长的事务处理。
许多团队在只有 80GB 数据时就开始焦虑未来的扩展问题,另一些团队在 80TB 时还在尝试用更多只读副本救急。真实的分界线其实很清楚:当你的分析查询开始出现 10 倍以上的性能退化,当你需要 3 个以上的只读副本专门支撑分析负载,当存储扩容成为常规操作,就该正视这个问题了。
Doris 不一定适合所有的场景,但它解决了我们在 PostgreSQL 分析路径上遇到的几乎所有问题。选择 Doris 不是因为它是完美的,而是因为维护一套能运转的系统,比维护三套勉强度日的系统要好得多。