1. 项目背景:单机瓶颈下的日常挣扎
报表又慢了。这已经是业务部门这个月第四次来找我们,截图里是一张再正常不过的经营日报,后台跑了一分多钟才出数。放在三年前,这根本不算问题;但今天,从承保、理赔、收付到再保,核心明细表已经累计到了几十亿行,单机方案早就到了极限。
我们团队负责公司的数据平台,平时主要支撑三类人:经营分析岗要日、周、月维度的多视角汇总,财务和精算那边要跑跑批逻辑,还有监管报送类需求动不动要拖一年甚至几年的全量明细。过去这套架构并不复杂:业务库每天凌晨批量同步到一台大内存统计库,再用 SQL 出报表。问题在于,这台机器能撑到的天花板肉眼可见——CPU 常年居高不下,凌晨跑批从 2 点延到 6 点,查询一多连接就排队,业务方在群里直接问“数还能不能出”。
这次我们决定不再靠加内存续命,而是做一次彻底的 OLAP 架构升级。方案对比了一圈之后,团队一致选择了 StarRocks,从单机统计库迁到了分布式集群。整个过程谈不上优雅,踩了不少坑,但结果值得写下来。如果你也在为报表平台性能发愁,或者正处在选型纠结期,这篇文章应该能给你省点时间。
1.1 保险行业典型的 OLAP 场景长什么样
很多人以为保险公司查数就是查几张简单的保单明细,实际远比这复杂。以车险为例,一张保单从录单、核保、缴费、出单,到后续批改、退保、理赔立案、赔款支付,一条承保链路能衍生出七八张关联表。再加上渠道、中介、代理人、机构层级这些维度,分析场景天然就是“大宽表 + 多维度透视 + 时间范围过滤”的组合拳。
我们线上最常见的查询大致分三类:
- 经营分析类:按机构、渠道、险种统计保费、赔款、费用,通常带同比环比,需要扫描大量历史数据做聚合。
- 报表拖数类:业务人员从 BI 平台拖一张“承保清单”,前端分页展示几千行,后台却要 join 保单、收付、批改等多张表。
- 财务对账类:以单号维度核对保费、手续费、税金的勾稽关系,偶尔要捞出异常数据,查询条件非常偏门。
这些场景的共同点是:单次 SQL 扫描的数据量大、维度组合多、对响应时间敏感。旧的单机库在小数据量时还能扛,一旦事实表明细达到亿级甚至十亿级,索引、缓存全部失效,基本就是全表扫描加磁盘 IO 死磕。
1.2 旧架构瓶颈的具体表现
我们原来的架构是一条很朴素的链路:业务系统是 MySQL,每天凌晨通过定时任务把增量数据同步到一台 32C64G 的统计库实例,再由这套库支撑所有报表查询。数据量小的时候还能玩,等到明细表从几千万行涨到二十多亿行,问题开始集中爆发。
最直观的痛点是凌晨跑批永远跑不完。同步任务从 2 点开始跑,中间需要做清洗、关联、汇总,正常情况下 5 点能出日表;随着数据量上涨,任务经常跑到早上 8 点还没结束,直接撞上业务方上班查数的高峰期。查询和跑批抢资源,SQL 互相拖累,整台机器沦为一场混战。
第二个痛点是并发一高就锁等待。业务方常用 BI 平台,一个报表背后往往有十几个图表同时发查询请求。单机数据库在几十个并发聚合查询面前毫无招架之力,经常出现连接堆积、超时报错。我们试过的办法无非是加索引、加缓存、用 auxiliary 汇总表,但本质上都是延缓症状,数据的增长曲线永远比优化速度快。
第三个痛点是硬件升级到顶了。这台机器已经从 16G 一路加到 64G,CPU 也换过两代,还是不够。想再往上就得换小型机或者高端存储,价格翻几倍不说,容量增长依然有限。横向加一台机器做读写分离也试过,但只是把报表查询拆出去,跑批和核心求数依然卡在那台主库上,治标不治本。
1.3 升级前定下的硬性指标
架构改造不能凭感觉拍脑袋,我们在立项时给这次升级定了几个硬指标,后来的选型和验收都围着它们转:
- 核心经营报表查询 P95 耗时从原来的 20 秒以上压到 5 秒以内。
- 支持至少 50 个并发查询同时跑,不准因为互相抢资源而集体超时。
- 明细数据必须支持更新和删除,保单批改、理赔状态变更这类场景不能推倒重来。
- 引擎要支持水平扩展,今年加十亿行数据,明年再加十亿行,加节点就行而不是换机器。
- 运维不能太重,最好能和现有 MySQL 生态兼容,BI 工具改个数据源就能接。
这些指标现在回头看并不夸张,但在当时已经足够筛掉一批候选方案。我们拿着这几条硬标准去聊 OLAP 引擎,发现很多产品要么强在聚合弱在更新,要么实时性有了并发上不来。最终真正对得上号的,就是 StarRocks。
2. 技术选型:为什么是 StarRocks
2.1 几个 OLAP 引擎的横向对比
选型阶段我们认真看了四个方向:ClickHouse、Apache Doris、Presto/Trino、StarRocks。我没有要把其他引擎一棍子打死的意思,每个产品都有自己的最佳场景,但放到保险数仓这个具体环境里,对比优缺点就比较明显了。
| 引擎 | 架构 | 数据更新能力 | 多表 JOIN 与并发 | 生态与兼容性 | 适配度 |
|---|---|---|---|---|---|
| ClickHouse | 分布式列存 | 弱,Mutation 成本高 | 大 JOIN 较吃力,并发受限于表引擎 | 协议不兼容 MySQL,BI 接入要改 | 偏低 |
| Apache Doris | MPP 列存 | Unique 模型支持部分更新 | 较好 | MySQL 协议兼容 | 中等 |
| Presto/Trino | 无状态查询引擎 | 依赖底层数仓 | 并发好但延迟偏高 | 需要配合 Hive/外表 | 中等 |
| StarRocks | MPP 列存 | 主键模型支持实时更新 | 好,CBO 优化较强 | MySQL 协议兼容 | 高 |
ClickHouse 的查询速度确实快,尤其单表聚合,业界名声很大。但我们测试后发现两个硬伤:一是多表 join 复杂场景不稳定,我们很多报表要关联三四张表,这点比较致命;二是数据更新能力偏弱,保险保单状态天天在变,总不能为了改一条状态把整个分区重写一遍。Presto/Trino 的并发和扩展性不错,但它本质是一个查询引擎,要接在 Hive 或对象存储上,延迟很难压到秒级。Doris 和 StarRocks 同源,基础能力接近,在后续比较中我们更倾向于 StarRocks 的主键模型和物化视图写法。
2.2 StarRocks 真正打动我们的特性
StarRocks 吸引我们的第一个点是列式存储加向量化执行。保险分析绝大多数 SQL 是“从几十列里取几列,过滤加聚合”,列式存储能显著减少 IO。向量化执行则让 CPU 密集型的大聚合计算跑得很痛快,不需要靠堆机器来换速度。
第二个点是主键模型的数据更新能力。StarRocks 的 Unique Key 模型和主键表支持按主键更新甚至部分列更新。保单批改、理赔状态流转这类高频变化数据,可以直接以主键 upsert 进去,不需要做“拉全量、删旧表、重写分区”的笨操作。这一点解决了我们最头疼的实时更新诉求,也是最终一票定音的关键。
第三个点是物化视图。保险报表的查询模式高度重复,无非按机构、渠道、险种、时间聚合。物化视图能把预计算结果透明地暴露给查询,业务方写的还是原来的 SQL,引擎自动选择走物化视图,不用改应用。这个特性对 BI 平台太友好了。
第四个点是MySQL 协议兼容。公司一堆报表工具、临时查询脚本都是照着 MySQL 方言写的,StarRocks 可以直接当 MySQL 实例接,几乎零改动。团队学习成本也低,DBA 稍微培训一下就能上手,不需要养一支专门的大数据开发队伍。
2.3 部署形态与版本选择
我们最终采用的是社区版 StarRocks 3.x,没有上商业版本。对于一个中等规模的数据团队来说,社区版功能已经完全覆盖需求,文档和社区案例也足够多,没必要多花预算。
部署形态上,我们用了 3 台 FE(前端节点)+ 6 台 BE(后端节点)。FE 之间用选举保证高可用,BE 按数据量和查询并发来估算。以我们目前 20 多亿行明细、数百张表的规模,6 台 16C64G 的 BE 节点在第一阶段足够用,后续如果数据翻倍,直接横向扩 BE 就行。
硬件上我们给 BE 配了 SSD 盘,万兆网卡。道理很简单,StarRocks 的列式存储对随机读要求不高,但对扫描带宽敏感,SSD 能明显压缩大查询的响应时间;万兆网卡则保证 Shuffle 和 Join 阶段的数据传输不拖后腿。这点预算别省,省了后面调优全是眼泪。
3. 架构焕新:链路重建与模型设计
3.1 从业务库到报表的新数据链路
架构改造不是把数据库换掉就完事,整个数据管道都得重新铺。我们最终确定的链路分三条,按数据新鲜度分成离线、准实时和实时:
- 离线批量:业务库每天凌晨导出的全量/增量文件,先落到 HDFS,再由 Broker Load 导入 StarRocks。用于跑批类报表和日级汇总。
- 准实时同步:通过 CDC 工具监听 MySQL binlog,写入 Kafka,StarRocks 用 Routine Load 持续消费数据。这条链路保证保单批改、理赔状态变更在分钟级内更新到数仓。
- 应用直连查询:BI 平台、报表系统、临时查询工具统一走 MySQL 协议访问 StarRocks,不再绕一层中间件。
这套设计的好处是把“批量大吞吐”和“实时流式写入”分开,互不干扰。离线导入用 Broker Load 并行加载,吞吐高;实时变化用小文件持续写入 Routine Load,延迟低。两条链路最终在 StarRocks 内部自动合并,查询侧完全无感。
3.2 三种数据模型的选型与建模细节
StarRocks 有三种数据模型,分别是明细表(Duplicate Key)、聚合表(Aggregate Key)和主键表(Unique Key / Primary Key)。我们的建模原则很简单:不改不删的明细用 Duplicate,需要覆盖更新的维表/状态表用主键表,纯指标聚合用 Aggregate。
举一个实际例子。承保明细表是数仓最核心的事实表,记录每一张保单的保费、手续费、保额等信息。它不需要覆盖更新,历史记录必须原样保留,所以我们把它建成 Duplicate Key 明细表:
CREATE TABLE dwd_insurance_policy_detail ( biz_date DATE, channel_id INT, agency_id INT, product_id INT, policy_no VARCHAR(64), premium DECIMAL(18,2), commission DECIMAL(18,2), etl_time DATETIME ) DUPLICATE KEY(biz_date, channel_id, agency_id, product_id, policy_no) PARTITION BY RANGE(biz_date) ( START ("2024-01-01") END ("2025-01-01") EVERY (INTERVAL 1 MONTH) ) DISTRIBUTED BY HASH(policy_no) BUCKETS 24 PROPERTIES ("replication_num" = "2");注意我把biz_date放在排序键的最前面,这是有意为之。StarRocks 的排序键不仅决定存储顺序,还直接影响分区裁剪和前缀过滤效率。我们 80% 以上的查询都带日期范围,把日期放最前面能最大程度减少扫描数据量。
理赔状态表则用主键表。同一张保单的理赔状态会从报案变成立案、结案,每次变化都要保留最新值,所以用主键覆盖更新:
CREATE TABLE dwd_claim_status ( policy_no VARCHAR(64), claim_no VARCHAR(64), status TINYINT, update_time DATETIME, etl_time DATETIME ) PRIMARY KEY(policy_no, claim_no) DISTRIBUTED BY HASH(policy_no) BUCKETS 12 PROPERTIES ("replication_num" = "2");主键表在导入时直接做去重合并,查询时不需要像旧版 Unique 模型那样做读时合并,性能稳定很多,尤其适合高频 upsert 场景。
3.3 分区、分桶和排序键的落地经验
分区和分桶是 StarRocks 建模里最容易出错的两个环节,我们前期也吃过亏。分区的核心目的是“能剪就剪”,必须按照最常用的过滤维度来设计。我们的事实表全部按业务日期做 RANGE 分区,按天或者按月,看数据量决定。日表分区太细会导致元数据膨胀,反而拖慢查询;我们月粒度数据量在 30GB 左右,按月分区刚刚好。
分桶则要同时考虑查询效率和导入并行度。分桶字段一般选择高基数字段,比如保单号,这样数据在各个 BE 之间分布均匀,避免某几个桶数据量特别大成为热点。分桶数有个经验值:每个桶的大小控制在 500MB 到 1GB 之间,太少了单桶扫描过重,太多了调度开销变大。
排序键的设计需要跟查询模式配合。这里特别提醒一句:排序键不等于主键,不要为了追求唯一性把所有字段塞进 Key 里。合理做法是把查询过滤频率最高的字段放在前面,比如日期、机构、渠道;字段顺序一旦确定,后期修改成本很高,所以建表前一定多做几次查询模式分析。
4. 实操过程与性能调优实录
4.1 存量迁移、增量同步与双跑方案
迁移我们分成了三步,每一步都有明确验证标准。
第一步是存量数据迁移。我们把历史二十多亿行明细从旧库导出,转换格式后落到 HDFS,再用 Broker Load 并行导入 StarRocks。Broker Load 对文件大小和数量比较友好,我们按天分文件,每个文件 200~500MB,整体导入速度可以达到每小时数亿行。这个阶段最需要注意的是字段类型映射,比如旧库的 DATE 和 VARCHAR 到 StarRocks 的映射规则,避免导入后才发现大量转换错误。
第二步是增量链路搭建。CDC 工具监听业务库 binlog 写入 Kafka,StarRocks 的 Routine Load 负责消费。刚开始我们试过直接把增量也走离线批量,后来发现每天只跑一次跟不上业务变化速度,保单状态下午改了,第二天报表才反映出来,业务方根本不接受。Routine Load 的延迟可以控制在分钟级,完全满足需求。
第三步是双跑验证。新老两套系统并行跑了两个星期,每天自动核对关键指标的总数、SUM、COUNT,比如总保费、总赔款、机构层级汇总这些口径。两边一旦对不上,优先查数据重复和漏数问题,而不是直接怀疑引擎算错。双跑的数据一致性验证通过后,才正式把报表流量切到新集群。
4.2 关键参数与导入链路调优
部署完成后,性能调优是工作量最大的一环。我们遇到的第一类问题是导入链路不稳定。Routine Load 默认的消费并发对 Kafka 分区数量很敏感,分区少的时候消费不过来,消息积压;分区多的时候又容易把小文件刷得太频繁。调优方案是把 Kafka topic 的分区数控制在 24~36 个,和 Routine Load 的并发任务数匹配,单批次数据量控制在 10~20MB 左右,既不会太小产生过多文件碎片,也不会太大导致单次导入内存波动。
第二类问题是大查询把 BE 内存吃满。StarRocks 默认允许单个查询使用较大内存,多张大聚合同时跑就可能触顶。我们通过会话级变量限制了查询内存上限:
SET query_mem_limit = 8589934592; -- 8GB同时把 BE 进程的mem_limit配到物理内存的 80%,剩下 20% 留给文件系统缓存。这样即使某个查询失控,也只是这条查询失败,不会拖垮整个节点。
导入参数上,Stream Load 的max_filter_ratio我建议不要拍脑袋设成 1,至少设 0.01 或 0.1。这样既允许少量脏数据跳过,又能在数据质量严重问题时快速暴露,避免报表里出现莫名其妙的缺失。生产环境里宁可导入失败重跑,也不要让脏数据悄悄混进去。
4.3 三个典型慢查询优化案例
案例一:函数导致分区裁剪失效。有张报表按天统计保费,查询条件是DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-06-01'。这个写法在 MySQL 里没问题,但到了 StarRocks 里优化器没法把函数等价改写,结果全表扫描,耗时 30 秒。改成范围条件后:
WHERE create_time >= '2024-06-01' AND create_time < '2024-06-02'扫描数据量直接降了两个数量级,查询秒回。这个案例告诉我们,任何对分区分桶字段做函数包裹的写法都要警惕,能用范围就用范围。
案例二:多表 Join 时过滤条件下推不足。明细表 join 维表做渠道分析时,查询在 StarRocks 里变成大表广播,网络 IO 很高。我们打开执行计划一看,发现过滤条件没有下推到 Join 前的扫描阶段。解法是调整表结构和查询写法,把 join 字段和过滤字段尽可能放进排序键,同时保证参与 Join 的字段类型完全一致。字段类型对不上导致隐式转换,也是 Join 变慢的常见原因。
案例三:重复聚合计算。经营日报每天统计机构、渠道、险种维度的保费、赔款,这些指标每天算一遍很浪费。我们在明细表上建了物化视图:
CREATE MATERIALIZED VIEW mv_daily_business AS SELECT biz_date, channel_id, agency_id, product_id, SUM(premium) AS total_premium, SUM(commission) AS total_commission, COUNT(*) AS policy_count FROM dwd_insurance_policy_detail GROUP BY biz_date, channel_id, agency_id, product_id;物化视图建好之后,原来的聚合查询会自动改写走物化视图,报表响应时间从十几秒降到一秒左右。这一步的性价比非常高,但注意物化视图会占用额外存储,只对高频查询建,别什么都建。
5. 踩坑实录:常见问题与排查手册
5.1 BE 内存失控:并行查询一多就 OOM
上线后第一次被业务方围攻,是因为 BI 平台同时刷新 20 张报表,BE 节点集体内存飙高,有几个直接 OOM 重启。
排查过程分了三步。先看SHOW BACKENDS里的内存指标,发现每个 BE 的内存使用都超过 60GB;再看活跃查询,大量聚合查询同时跑,每个算下来的内存加一起远超节点容量。最后定位到根因:我们没有限制单个查询的内存上限,所有查询公平竞争,大查询和小查询抢在一起,节点直接爆掉。
解决思路很直接:
- 在会话级把所有 BI 连接的内存限制调低,配合 BI 平台限流。
- 调整 BE 的
mem_limit,从默认值降到物理内存的 80% 左右,留出余量。 - 对后台跑批任务和前端报表查询做资源组隔离,避免单一种类任务占满整个集群。
这里要提醒一下,StarRocks 的查询本身设计为“能多用就多用”,不加限制确实更快,但生产环境一定要加上限,宁可单个查询慢一点,也不能让一两个大查询把整个集群拖挂。
5.2 查询超时但资源利用率不高
有段时间我们收到反馈,某个报表查询不稳定,有时候秒开,有时候等二三十秒,看着 BE 的 CPU 和内存都不高,很奇怪。
用EXPLAIN看执行计划发现,问题出在分区裁剪不稳定。同样的 SQL,带月份参数时能正确裁剪分区,不带参数或者参数格式变了就变成全表扫描。根源是 BI 平台对日期参数的处理不统一,有的传字符串,有的传日期对象,优化器解析结果不同。
解决方法是统一 BI 平台的日期参数格式,同时在数据模型层加一层“默认最近 30 天”的视图封装,把过滤条件固化在视图里。这类问题不是引擎 bug,而是应用层和优化器之间的协作没做好,排查思路要从 SQL 本身切入,别急着怀疑基础设施。
5.3 主键表出现重复记录
主键表设计的初衷就是去重,但我们在上线早期大量出现过同一条保单记录在表里存在两条版本的问题,而且报表汇总时把两条都算进去了。
查下来原因很典型:同一个 Kafka 分区里,同一主键的变更消息出现了乱序,后产生的更新先到,先产生的更新后到,主键表按导入批次顺序去重,结果保留了旧版本。这是流式更新的经典难题,解决方案有两层:
- 消息侧保证同一主键走同一个 Kafka 分区,并且开启消息时间戳排序,让新数据一定后写。
- StarRocks 侧,如果场景允许,把主键表配置成写时合并,导入时直接按主键合并,而不是读时合并,能大幅减少这种概率。
最终我们把主键表全部切到写时合并模式,重复问题基本绝迹。
5.4 Routine Load 链路中断的排查
实时链路跑了一段时间后出现过一次事故:某天的增量数据没有更新到报表,业务方追问为什么前一天的理赔状态迟迟没变化。
排查步骤是:先看 Routine Load 的作业状态,发现任务还在,但是 lag 越来越大;再看 Kafka 侧,发现某个 topic 的 offset 已经超过保留时间,最早的未消费消息被清掉,导致消费位点失效。根因是 Routine Load 消费速度跟不上业务高峰写入量,积压超过一天后触达 Kafka 数据清理红线。
解决方案是双管齐下:一方面增大 Kafka topic 的保留时长,从 12 小时调到 24 小时以上;另一方面提高 Routine Load 的并发和单批大小,把消费速度提上去。同时在监控上加告警,lag 超过一定阈值就立即通知,不能等业务方来问。
6. 收尾:成果与几条真心话
6.1 上线后的主要成果
架构切换完成后,我们做了一次系统性的复盘统计,结果比预期好不少。
核心经营报表的 P95 响应时间从原来的 20 秒以上降到了 3 秒以内,日常查询基本秒开。凌晨跑批任务从最早跑到 8 点,提前到了 3 点前全部出数,业务方早上上班就能看到完整日报。集群具备水平扩展能力,后面数据增长直接加节点就行,不再面临单机天花板。整个迁移从搭建到核心报表切换,团队两个数据开发加一个 DBA,大概花了两个多月,学习成本完全在可控范围内。
6.2 后续还可以怎么演进
这次改造只解决了 OLAP 底座的问题,离“数仓现代化”还有一段距离。我们已经在规划下一步:
- 把物化视图覆盖范围扩大,对高频的财务、精算对账报表做统一预计算。
- 将更多业务线接入实时链路,做到经营指标分钟级可见。
- 引入资源组和负载隔离机制,让跑批、报表、临时查询互不干扰。
- 完善数据质量和血缘监控,从数据源头减少脏数据进入数仓。
我个人在实际操作中最大的体会是:架构升级最难的部分从来不是技术选型,而是对业务查询习惯和数据增长曲线的理解。StarRocks 给了我们一把好用的工具,但真正让改造成功的,是把排序键、分区、物化视图这些细节跟业务场景一步步对清楚。如果这篇文章能让你在选型和建模阶段少走几条弯路,那就值了。