营销自动化OLAP架构演进:从ClickHouse到Doris的选型与实战
2026/9/9 3:21:41 网站建设 项目流程

从一套ClickHouse扛住全部查询,到后来不得不把实时链路单独拆出来,再到现在把Doris、ES和一些轻量计算引擎混着用,这套营销自动化的OLAP架构,前前后后演进了一年多。中间踩过的坑不少,但更值得记录的,是每一次选型切换背后的真实业务驱动——不是技术追新,而是数据量、查询模式和人效逼着你往前走。

这套系统解决的核心问题,简单说就是:把分散在广告平台、CRM、埋点日志、订单库里的数据统一收进来,再让运营、增长、投放同学能快速圈选人群、看转化漏斗、分析活动效果。整个过程涉及多源数据的接入、清洗、建模、存储和查询,而OLAP引擎在其中扮演的是“查询加速”和“多维分析”的关键角色。

这篇东西主要写给两类人看。一类是正在做营销数据中台或者用户画像系统的工程师,可以参考我们在引擎选型、数据建模、实时链路设计上的取舍;另一类是想把数据驱动真正落到业务里的数据分析师或运营负责人,看完至少能明白:为什么一个简单的人群圈选,后台要养那么一大堆组件,以及哪些环节最容易出幺蛾子。

1. 内容整体设计与思路拆解

1.1 营销自动化场景对OLAP的真实需求

先聊一个容易被低估的点:营销自动化和普通BI报表,对OLAP的要求完全是两码事。普通报表查询是“少量人、低频次、大查询”,比如每天早上的经营日报,或者管理层偶尔看的趋势图,并发低、查询慢一点也能接受。但营销自动化平台面对的是“大量人、高频次、复杂查询”——几百个运营同时在创建人群包,每个圈选操作背后都是一次多维组合查询;活动上线后,实时效果看板每秒都在刷新;自动化流程触发时,需要在毫秒级判断用户是否符合进入某个分群的条件。

这种场景下,OLAP引擎要扛住的压力有三个方面。

第一是查询的维度组合非常自由。运营圈人群的时候,不会按你预先设计好的索引去查,亿级用户表上可能任意组合性别、年龄、地域、最近30天消费次数、客单价、活跃渠道等十几个维度。传统MySQL在这种查询下基本就是灾难,就算建了联合索引也扛不住高并发,更别提还要做聚合计算。

第二是数据的时效性要求很高。营销活动不像财务报表可以T+1算,用户今天领了券、下了单、看了某个商品详情页,这些行为可能几小时后就要进入人群筛选逻辑。如果OLAP链路只能做到小时级更新,很多自动化营销策略根本跑不起来。

第三是多源数据之间存在复杂的关联关系。一个用户可能在微信小程序里浏览,在App里下单,在天猫旗舰店退款,还通过客服CRM系统提交过投诉工单。要完整评估这个用户的价值、偏好和生命周期阶段,必须把这些分散在不同系统的数据按用户ID关联起来,形成统一视图。

OLAP架构演进的核心,其实就是在回答一个问题:在数据规模、查询复杂度和实时性三者之间,怎么找到当前阶段最合适的平衡点。没有银弹,只有取舍。

1.2 架构演进的三个核心阶段

我们的演进路径大概分成三个阶段,每个阶段都有清晰的技术选型逻辑。

第一阶段叫“明细查询时代”。系统刚上线时,数据量只有几千万级,查询也不复杂,直接用MySQL分库分表加Elasticsearch就够用。用户ID索引放在MySQL里做精确匹配,行为明细、订单数据丢进ES做倒排索引查询。这个阶段的优点是简单直接,开发速度快,业务跑得起来;缺点是只支持简单筛选,复杂聚合基本做不了,数据量翻几倍之后ES的查询性能衰减非常明显。

第二阶段叫“OLAP引擎引入时代”。数据量涨到几亿后,ES集群越扩越大,查询却越来越慢,我们就引入了Apache Doris作为核心OLAP引擎,把用户标签、行为汇总、订单事实表全部迁到Doris里。这个阶段解决了多维分析的核心痛点,通过前缀索引、分区分桶、向量化执行引擎,实现了秒级响应的高并发多维查询。但问题也随之而来——实时性要求高的场景还是依赖Kafka+Flink的实时链路,Doris这边只做离线批量的标签加工,两套数据之间存在不一致的风险。

第三阶段是“多引擎协同时代”。当前我们正在推进的架构形态,核心思路是让专业引擎干专业的事:Doris负责高并发多维分析和人群圈选,ES继续承担日志明细的全文检索和部分灵活查询,StarRocks(或者继续用Doris)负责大规模离线聚合,ClickHouse则保留在特定实时报表场景中。

这套演进的核心逻辑是:业务需求永远走在前头,技术选型永远在成本和性能之间做权衡。不会因为Doris功能多就强迫所有场景都用Doris,也不会因为重构麻烦就拒绝引入新技术。架构演进的核心判断标准只有一个——当前的系统是否还能以合理的成本满足业务增长预期。

1.3 为什么选择Doris作为核心OLAP引擎

在深入细节前,想单独聊聊为什么Doris在中后期会成为核心引擎。这个选型其实经历了很长时间的调研和对比。

当时摆在面前的主要候选是ClickHouse、Doris、StarRocks三选一。ClickHouse的优势是单表查询速度极快,特别是大宽表场景下性能非常猛,社区生态也成熟,很多大厂都在用。但它的痛点也很明显:多表JOIN支持不友好,数据更新成本高,并发查询能力相对有限,运维门槛也不低。对我们这种需要频繁做多表关联、高并发人群圈选的营销场景,ClickHouse并不合适。

Doris当时打动我们的点有三个。一是完善的分布式架构设计,支持弹性扩缩容,数据自动均衡,运维省心。二是强一致性的数据模型,支持主键模型、聚合模型、Unique模型,对需要频繁更新标签、删除过期数据的场景非常友好。三是优秀的查询优化器,多表JOIN性能远好于ClickHouse,在高并发小查询的场景下表现优秀。

选型过程中我们做了一个对比压测:用同一批2亿行的事实表加1亿行的用户维表做JOIN聚合查询,Doris在20并发下P95响应时间为180毫秒左右,ClickHouse在相同条件下会退化到2秒以上,甚至部分复杂查询会OOM。这个结果基本就定了方向。

注意:当前没有哪款OLAP引擎能通吃所有场景。不要迷信某个引擎的“全链路解决方案”,要根据自己业务的实际场景做取舍。下一篇会详细讲我们在引擎选型时踩过的坑。

2. 核心细节解析与实操要点

2.1 多源数据接入的统一规范

数据接入是整个OLAP架构的地基,这块一旦乱了,后面建模、查询、分析全都会出问题。我们的多源数据接入主要分成四类,这四类的接入方式和处理逻辑完全不同。

第一类是客户端行为数据,包括App、小程序、Web端埋点。统一走服务端上报到Kafka,通过Flink实时清洗后写入Doris和消息队列。这块的核心规范是埋点事件命名和参数结构必须统一,不然清洗逻辑会走到崩溃。我们的做法是定义了一套标准事件模型,所有端上报的数据都围绕event_name、event_time、distinct_id、properties这四要素展开,其中properties是JSON格式,允许各端自定义扩展,但必须遵循统一的字段命名规范。这套标准我们维护在一个JSON Schema里,每次埋点上线前都要做一次校验。

第二类是业务数据库数据,包括订单表、用户表、优惠券表、积分流水表等。通过Canal监听MySQL Binlog,解析后写入Kafka,再分流到不同下游。订单表同步到Doris做事实分析,用户表同步到Doris做维表,同时同步一份到ES供搜索场景使用。这里最关键的是Binlog同步的幂等性和数据一致性保障,因为Binlog中同一个主键的多次变更会依次到达,下游必须按顺序处理,否则会产生数据覆盖错乱。

第三类是广告平台数据,包括巨量引擎、腾讯广告等渠道的消耗数据、转化回传数据。这类数据最麻烦的地方在于各平台的字段口径不统一——有的叫“消耗”,有的叫“花费”;有的按点击时间归因,有的按转化时间归因。我们的做法是多层清洗:第一层做字段映射和单位统一,第二层做渠道去重和归因口径匹配,第三层做数据校验(比如消耗金额不能为负、转化数不能超过点击数),全部校验通过后才进入Doris的广告分析主题。

第四类是CRM和客服数据,覆盖工单记录、售后记录、用户反馈内容。这类数据量相对不大,但结构差异化极大,文本类数据多,且对实时性要求不高。处理方式是每天定时批量同步到Doris和ES,Doris侧用于用户生命周期分析,ES侧用于文本检索场景。

这里特别想强调多源数据接入时一个常见的认知误区:大部分团队在接入初期只关心“怎么把数据导进来”,比较少考虑“不同来源的数据冲突时以谁为准”。我们自己在这个问题上吃了不少亏——比如用户表里的手机号,CRM系统和订单库都可能更新,但更新时机和准确性完全不同。经过几轮踩坑,我们才建立起一套完整的血缘管理机制:每个字段都标注来源系统、更新频率、置信度等级,同一字段出现冲突时按照置信度从高到低覆盖写入。这套机制一开始看起来像是增加工作量,但在后期排查数据质量问题时能省下大量时间。

2.2 数据建模的核心逻辑:宽表优先,维表为辅

在OLAP场景中,数据建模的核心原则是“宽表优先、维表为辅”。这和传统数仓的范式建模思路不一样,传统数仓讲究的是标准化和减少冗余,通过多层ETL把数据拆成事实表和维表,再用JOIN把它们关联起来。但在OLAP引擎里,JOIN是要消耗大量计算资源的,尤其在高并发场景下,每多一个JOIN,查询性能可能成倍下降。

我们的做法是面向具体业务场景构建大宽表。比如“用户标签宽表”,一个用户一行记录,几十个标签字段作为列,字段包括基础属性、消费能力、活跃度、偏好类目、生命周期阶段等。运营圈人群时,90%的场景就是在这个宽表上做“字段过滤+计数/去重”,不需要JOIN任何其他表,性能自然就快。

宽表的设计看起来简单,但实际操作中有几个关键点需要特别注意。

第一是字段粒度必须对齐。一个宽表里所有字段都要围绕同一个粒度,比如用户宽表就是一人一行,订单字段必须预先做聚合(比如近30天订单数、近30天消费总额),不能直接在宽表里冗余订单明细。否则就会出现严重的数据膨胀——一个用户如果下了一百单,宽表里对应一百行,那所有基于用户的统计维度都会翻车。

第二是空值和默认值的处理策略。标签宽表里大量字段可能为空,比如用户没有填过职业、没有绑定过会员卡。如果空值直接留给查询端处理,Count、AVG这类聚合函数的结果会和预期完全不一样。我们的做法是ETL阶段就把所有空值统一为默认值,比如“未知”“0”“-1”等,具体用哪个默认值取决于字段语义。比如年龄字段用-1表示未填写,收入字段用0表示无数据。这个细节看似简单,但处理不到位的话会在后期分析结果中埋下巨大隐患。

第三是维表要控制数量。理想状态下宽表关联的维表不超过三张。超过这个数量会带来两个问题:一是查询性能下降严重,二是数据更新时的关联逻辑变得复杂,很容易出现数据不一致。我们遇到过的最极端案例是某个活动分析宽表关联了7张维表,结果每次任务调度光是在JOIN环节就要跑四个小时,而且一旦某个维表数据刷新失败,整条链路都要重跑。

2.3 小样本场景下模型拟合与物理模型泛化的理解

数据驱动在营销自动化中有一个容易被忽视的边界问题:小样本场景下,纯数据驱动的模型会失效。这个话题和OLAP架构本身关系不大,但和我们这套系统最终服务的业务目标——精准人群圈选和效果归因——关系极为密切。简单说就是:数据量太少的时候,数据驱动模型可以拟合得很好,但这种好是有水分的;真正靠谱的判断,还得靠物理模型(业务逻辑)来兜底。

举个例子,某个新品刚上线三天,只有两百多个用户产生了购买行为。你想基于这些数据做一个“高转化人群包”,如果纯靠数据驱动方式去训练模型——比如把性别、年龄段、渠道来源、设备型号全部纳入LR或者树模型——模型在训练集上的AUC可能能到0.95以上,看起来效果拔群。但实际上呢?这两百多个样本远远覆盖不了真实用户的全貌,一个在训练集里“表现优秀”的特征组合,很可能只是市场推广初期的流量特征,一旦投放节奏变化,模型立刻失效。

“传统数据驱动模型易于拟合,物理模型泛化不足”这句话,我的理解是分为两层的。前半句是说小样本下数据驱动模型一定会过拟合——因为模型会把训练集中的噪声也学进去,在样本内表现得无比精准,但这种精准是虚假的。后半句说的物理模型泛化不足,不是指物理模型不好用,而是说当前的物理模型覆盖范围有限、泛化能力还有待提升。在我们营销场景里,物理模型指的是那些基于业务规则、业务逻辑构建的判定模型——比如“高价值用户=近30天消费≥5次且客单价≥200元且近7天有活跃行为”。这类模型的优势是逻辑透明、解释性强、不怕过拟合;劣势则是依赖业务经验,覆盖不了那些不符合规则但实际转化极高的“边缘用户”。

正确的处理方式是两者结合:小样本场景下,以物理模型(业务规则)为主,数据驱动模型为辅,数据模型只对物理模型做补充和微调;当样本量积累到一定规模后,再逐步加大数据驱动模型的占比。这套策略我们落地成了一套自动分流机制:新活动上线初期,人群圈选完全走规则引擎;数据积累超过5万样本后,自动切换到机器学习模型与规则双跑的模式;超过10万样本后,才让模型独立承担人群圈选任务。这条边界线是我们团队反复验证后确定的,不同业务可以调整,但思路是一致的:数据驱动是油门,物理模型是刹车,两者配合好了车速才能上去。

3. 实操过程与核心环节实现

3.1 人群圈选功能从MySQL到Doris的完整改造

人群圈选是营销自动化平台最核心的功能,没有之一。这个功能说白了就是让运营同学通过组合条件筛选用户,然后对筛选出的用户执行发券、推送、短信等动作。改造前,这个功能跑在MySQL和Elasticsearch上,逻辑大致是:把用户标签数据存到ES,查询时拼DSL,对满足条件的用户ID做分页返回。

这个方案在数据量达到3亿用户的时候彻底撑不住了,几个典型案例让人很头疼:选“最近7天活跃且消费能力为高”的人群,相当于一次要扫几百万甚至上千万条记录做过滤再聚合,ES集群CPU直接被打满,查询耗时从最初的2秒恶化到40秒以上;运营同学点击“预估人数”按钮后,经常等几分钟才能出结果;到晚间投放高峰,两个重查询就能把一个数据节点拖死,全网查询全部变慢。

改造的思路不是推翻ES,而是把“人群圈选”这个核心场景迁移到Doris,ES继续保留作日志检索和部分灵活查询。Doris侧的关键设计有两块。

第一块是建表模型的选择。我们使用Doris的Duplicate Key模型加明细数据存储,配合Range分区和Hash分桶。具体配置如下:

CREATE TABLE tag_user_wide ( user_id BIGINT NOT NULL COMMENT '用户ID', tag_gender TINYINT COMMENT '性别:0未知,1男,2女', tag_age_group TINYINT COMMENT '年龄段:0未知,1<18,2[18,24],3[25,30],4[31,40],5>40', tag_active_7d INT COMMENT '近7天活跃天数', tag_total_orders INT COMMENT '累计订单数', tag_consume_level TINYINT COMMENT '消费等级:0未知,1低,2中,3高', tag_last_order_time DATETIME COMMENT '最近下单时间', tag_register_time DATETIME COMMENT '注册时间' ) DUPLICATE KEY(user_id) PARTITION BY RANGE(tag_register_time) ( PARTITION p2020 VALUES LESS THAN ('2021-01-01'), PARTITION p2021 VALUES LESS THAN ('2022-01-01'), PARTITION p2022 VALUES LESS THAN ('2023-01-01'), PARTITION p2023 VALUES LESS THAN ('2024-01-01') ) DISTRIBUTED BY HASH(user_id) BUCKETS 48 PROPERTIES ("replication_num" = "3");

这里有几个实践细节可以分享。分桶数设置为48,是结合集群节点数、单表数据量、以及查询并发度综合算出来的。理论上分桶数量等于集群BE节点数的倍数效果较好,这样数据能均匀分布到所有节点。同时分桶数也不宜过大,否则导入会产生大量小文件,影响查询性能。我们当前集群是6个BE节点,每个节点16核64G内存,48个分桶相当于每个节点8个分桶,实测效果较为均衡。

第二块是查询改写。原来在ES上的DSL查询,需要改写成Doris的SQL。比如“近7天活跃天数≥3天且消费等级为高”的圈选逻辑,改写后的SQL如下:

SELECT user_id FROM tag_user_wide WHERE tag_active_7d >= 3 AND tag_consume_level = 3 LIMIT 10000;

预先聚合后没有JOIN、没有复杂计算,就是一次前缀索引匹配加过滤扫描,Doris的性能完全可以轻松扛住。

这个改造最核心的收益是:人群圈选的P95响应时间从改造前的40多秒降到了1秒左右,并发能力提升了10倍以上,而且ES集群的负载大幅下降,日志检索场景的体验也恢复正常了。

3.2 Elasticsearch在OLAP场景下的过渡方案

上面提到ES在大规模人群圈选场景下撑不住,但在整个OLAP架构中,ES依然占有一席之地。聊这个是想回应一下“elasticsearch实现olap”这个话题——确实有团队在初期用ES来顶OLAP的活儿,这也是一条可行的过渡路径,只是要清楚它的边界在哪里。

ES做OLAP的核心思路是:利用倒排索引+Pipeline Aggregation,在明细数据上做过滤、分组、聚合计算。语法上用DSL表达,复杂度和SQL完全不同。下面是一个实际项目中使用ES做“按渠道统计近30天消耗和转化”的DSL示例:

{ "size": 0, "query": { "bool": { "filter": [ { "range": { "stat_date": { "gte": "2024-11-01", "lte": "2024-11-30" } } } ] } }, "aggs": { "by_channel": { "terms": { "field": "channel_id", "size": 20 }, "aggs": { "total_cost": { "sum": { "field": "cost" } }, "total_convert": { "sum": { "field": "conversions" } }, "avg_cpa": { "bucket_script": { "buckets_path": { "cost": "total_cost", "conversions": "total_convert" }, "script": "params.cost / params.conversions" } } } } } }

这段DSL的实际作用就是:从ES中筛选出11月份的全部广告消耗明细记录,按渠道ID分组,求每个渠道的总消耗、总转化数、平均转化成本。用ES的好处是部署简单、扩展方便、对全文检索能力强,适合数据量在几千万到一两亿级别、查询模式以过滤+简单聚合为主的场景。

但ES做OLAP的边界也很明显。第一是JOIN支持极差,虽然ES 6.x之后有Join类型和Nested类型,但查询性能和灵活度完全无法和真正的OLAP引擎相比,复杂关联场景下基本绕不开宽表。第二是聚合性能下降快,数据量超过亿级后,需要大量内存做Fielddata或Doc Values,集群堆内存配置稍有不慎就会OOM。第三是精确去重计数(Cardinality Aggregation)在超大基数下误差会出现,因为实现基于HyperLogLog,精确度受限于精度参数配置。营销场景里的人均消费次数、累计下单人数这类指标对精确度要求极高,误差一旦出现很难解释。

所以我的判断是:ES可以作为一种过渡方案、辅助方案,解决数据量中等、查询模式相对简单的OLAP需求;但一旦演进到营销自动化这种高并发、多维度、强一致性的场景,还是需要引入专业的OLAP引擎。ES也不应该被从架构里拿掉,它做日志检索、明细查询、自定义灵活分析依然很好用。

3.3 实时OLAP链路的搭建过程

营销自动化的很多场景依赖实时数据,典型的有两个:一个是“用户进入某个触发型活动后,需要在秒级内判断是否给Ta推送优惠券”的实时触发;另一个是“活动开始后,运营需要实时看到参与人数、转化率、ROI”的实时大屏。

我们最初期的实时链路比较简单——Kafka到Flink,窗口聚合后写入Redis,大屏和应用直接读Redis。但很快发现两个问题:一是聚合维度和查询维度对不上,Flink里只能预聚合固定维度组合,运营临时想按渠道、按城市拆一个维度看数据,Redis里根本没有这些粒度;二是状态管理复杂,Flink的状态后端存储压力大,作业重启恢复非常耗时。

后来演进到Kafka + Flink + Doris的架构,核心思路是Flink做实时清洗和轻度预聚合,Doris负责提供灵活的实时查询能力。Flink作业通过标准的JDBC连接器把实时数据写入Doris,Doris侧使用Unique模型,通过主键模型实现数据的实时更新。比如订单实时数据,以order_id为主键,新增和更新都走同一条写入链路,Doris内部自动处理UPSERT语义。

具体到实现层面,有一个比较关键的调优参数是Doris的Stream Load并发度和批次大小。我们的Flink作业通常设置每批次攒够10万条记录或5秒定时触发一次Stream Load,并发度控制在3-5之间。批次太小会导致导入过于频繁,产生大量小版本问题;批次太大会增加单次导入延迟,影响实时性。10万条和5秒这个配比,是我们在生产环境压测多轮得出的相对平衡点。

实时大屏侧的SQL,常见写法是基于Doris的实时聚合表,比如按分钟汇总的活动实时效果:

SELECT channel_id, COUNT(DISTINCT user_id) AS uv, SUM(order_amount) AS gmv, COUNT(DISTINCT order_id) AS order_cnt FROM dwd_order_realtime WHERE activity_id = 'ACT20250101' AND stat_minute >= DATE_FORMAT(NOW() - INTERVAL 60 MINUTE, '%Y-%m-%d %H:%i') GROUP BY channel_id ORDER BY gmv DESC;

这条查询在Doris上跑得很快,一方面因为实时表按活动ID+分钟做了分区裁剪,另一方面COUNT(DISTINCT)在Doris里做过针对性的并行化优化。即便如此,这类查询仍然要求底层数据模型的重复度不能太高,否则COUNT(DISTINCT)的内存开销会非常大。

3.4 从ES迁移到Doris的迁移流程

迁移是OLAP架构演进中风险最高的环节,没有之一。直接说我们总结的迁移方法论,分四步走。

第一步是双跑校验。新老两套系统并行运行,每天同一套任务分别写入ES和Doris,第二天对账。对账粒度不能只比对总数,要按关键维度分组对比——按渠道、按活动、按日期。我们处理过很多“总数对得上,拆开全不对”的案例,这类问题最隐蔽,也最伤害数据信任度。双跑周期我们建议至少持续两周,覆盖一个完整的业务小周期。

第二步是流量灰度。Doris数据校验通过后,开始把查询流量逐步切过去。灰度策略不是按用户切,而是按查询类型切:先把离线报表类查询切过去,再切人群圈选类,最后才切实时在线查询。

第三步是性能压测。切流之前必须压过,不能带着未知数上线。压测要模拟真实场景的并发模型——一个营销自动化平台里,日常有几百个运营在操作,每个操作背后可能对应一次查询;活动高峰期在线查询QPS会翻好几倍。我们用JMeter和内部自研的压测工具,按日常3倍峰值来做压测,如果新系统扛不住这个压力就不上线。

第四步是回滚预案。数据库架构类变更最怕“上线之后跑一个月发现要回滚”,面临两边数据不一致、增量数据难以合并的问题。我们的预案是:不论新系统上线多久,确保ES侧的数据同步链路至少保留30天不关闭。这样一旦Doris侧出现难以快速修复的问题,可以立即切回ES,虽然查询性能会退化,但数据不丢、业务不停。

整个迁移过程最耗时的是双跑校验阶段,数据校验脚本的编写要对业务逻辑有很深的理解,哪些指标口径对了就算通、哪些字段需要批量对比采样,都需要和数据团队反复确认。但这段投入是值得的,它直接决定了后续系统能不能从“能跑”走向“可信”。

4. 常见问题与排查技巧实录

4.1 JOIN数据膨胀导致的查询结果翻倍

OLAP场景中最容易踩的坑之一,就是JOIN导致的数据膨胀问题。举个例子,用户维表和订单事实表JOIN,一个用户如果下过10个订单,那JOIN结果集里这个用户就会对应10行;如果接下来再JOIN一张优惠券表,优惠券数量也是10张,那这个用户会对应100行。数据量小的场景感觉不到,但到了几十亿行级别,膨胀后的中间结果是灾难级的。

我们曾在排查一个营销活动效果报表翻倍问题时,花了两天才定位到根因:活动效果宽表里有两个业务过程字段——“优惠券领取数”和“优惠券核销数”,两者各自来自不同的事实地表,ETL里直接做了两次JOIN,导致订单、领取、核销三张表相乘,数据膨胀10倍以上。报表结果自然怎么算都不对。

排查过程也比较有代表性:先看结果数据,发现部分活动的参与人数比真实用户数还多;再看ETL日志,发现宽表产出行数比预期高了一个数量级;最后逐段拆SQL,才发现是JOIN导致的膨胀。修复方案也不复杂:不要用多表JOIN的方式构建宽表,而是在事实表侧先分别按用户粒度做预聚合,再用用户ID去关联维表。这样每个事实过程在JOIN前已经压缩为“一个用户一行”,膨胀问题自然消失。

4.2 数据倾斜导致BE节点负载不均

Doris的分布式架构实现了数据的自动均衡,但所谓“自动”是指分桶数据的初始分布,并不代表所有查询负载会自动均分到每个BE节点上。我们在实际运行中遇到的问题集中在热点用户和海量分区两个方面。

热点用户的产生方式非常典型:某头部主播直播带货时,大量用户涌进一个活动中,这个活动的数据全部落在同一个分区内;或者某个秒杀活动中,几个渠道贡献了90%的流量。由于分桶策略是基于分桶键的哈希,同一个渠道、同一个活动的数据天然会落在少数几个分桶里,对应BE节点的CPU和内存就会明显高于其他节点。

排查方法是用Doris的Profile功能定位耗时节点,再用表统计信息确认数据分布情况。比如执行下面的查询看数据分布是否均匀:

SHOW TABLETS FROM activity_order_fact;

如果发现少量Tablet的数据行数远超平均值,基本可以确认数据倾斜。常规解法有两类:一是将分桶键拆分成联合键,比如原来是活动ID,现在改成活动ID+渠道ID,输入数据的散列粒度会更细;二是适当增加分桶数量,让数据分布更均匀。但如果倾斜源是少数超大用户(比如平台头部商家账号产生大量订单),联合分桶键也无法根除,只能在查询优化器层面做双层聚合。

4.3 实时导入延迟与查询性能的平衡

实时链路里最头疼的问题,莫过于实时性要求和查询性能之间的平衡关系。Doris数据文件是分版本管理的,过多次数的导入会产生大量的小版本,查询时需要合并这些版本,导致读放大严重。我们在活动大促期间就遇到过这个问题——实时导入间隔设置得太短,版本数量激增,查询性能直接掉了一半。

排查时通过SHOW TABLET查看版本数量,如果发现某个Tablet的版本数超过了100,基本能确定是导入频率过高。解决思路有两个方向。

第一是调整Stream Load批次大小和触发间隔,把导入频率降下来。前面提到我们采用“10万条或5秒”的配置,大幅降低了小版本产生的概率。第二是定期执行Compaction,将小版本合并成大版本。Doris有自动Compaction机制,但对写入高频的表,建议手动触发,比如每天凌晨业务低峰期执行:

ALTER TABLE activity_order_realtime COMPACT;

Compaction的粒度、频率和资源消耗需要根据表设计来调整,不能一刀切。我们的实践是:核心高频更新表每天一次手工Compaction,普通实时表依赖系统自动Compaction就够用。

4.4 常见问题速查表

把实际运维过程中积累的排查经验整理成一个速查表,方便遇到问题时快速定位方向。

问题现象可能原因排查方法解决方案
查询结果数据翻倍多表JOIN数据膨胀对比宽表行数与源表预估行数事实表侧预聚合后再JOIN
BE节点负载不均衡分桶键选择不当或热点数据集中SHOW TABLETS查看数据分布调整联合分桶键或增加分桶数
实时查询越来越慢小版本过多导致读放大查看Tablet版本数增大导入批次,手动Compaction
内存溢出错误大查询并发过高查看Profile中内存占用限制单查询内存,增加资源组隔离
COUNT(DISTINCT)结果不准用户ID精度超过HyperLogLog默认配置核对基数预估的误差率或改用精确去重
数据更新后查询不一致部分副本数据未刷新检查副本状态和版本号手动刷新或重建物化视图

每个问题背后其实都有一套方法论。我个人的习惯是:遇到性能问题先看数据分布,再看执行计划,然后才动参数。很多人一上来就调内存、改并发,方向错了会越调越歪。

4.5 架构演进过程中的几点避坑心得

最后聊几条从这一路演进过程中沉淀下来的切实体会,不一定都是技术层面的,但对做同类系统的团队应该会有参考价值。

第一,OLAP选型要紧紧围绕业务形态来走,不要被“别人都在用”带着跑。如果你的场景是广告投放报表(高并发、中等数据量、多维度组合查询偏多),Doris这类MPP数据库会明显更顺手;如果你的场景是海量日志明细分析、单表极宽且极少JOIN,ClickHouse的优势会更大;如果你当前只有几千万级数据,ES配合适当的宽表设计也完全可行。每一次架构升级应该都是业务增长倒逼的,不要为了技术面子提前升级。

第二,数据模型设计的重要程度超过引擎选型。真实经验是:Doris用得好不好,七分在建模,三分在调优。同一个引擎,用宽表模型和用范式模型的查询性能差距可以超过10倍。在搭建OLAP系统时,真正值得花时间的部分是数据模型的梳理——哪些字段是高基维、哪些是低基维、哪些查询组合是高频的、哪些聚合指标是必须实时的,这些想清楚了,后面的工程量能省一大半。

第三,多源数据接入一定要在一开始就建立元数据管理机制。每个表、每个字段、每个指标的来源、口径、更新频率、负责人,都必须在元数据系统里登记清楚。这项工作不做,后期一定会在跨部门沟通、数据质量排查、新人上手等环节反复交学费。

第四,也是最真实的一条经验:架构演进从来不只是技术问题,还是组织协同问题。OLAP架构升级会直接影响数据团队的ETL逻辑、算法团队的特征工程、运营团队的查询习惯。任何一次技术切换,都要提前和这些下游团队对齐预期,规划好迁移窗口期。技术方案可以快速定,但业务侧的适应和反馈才是最影响落地节奏的因素——毕竟再好的架构,也得有人愿意用、用得好,才算真正成功。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询