电商分析平台这个系列写到第二篇,先把话挑明:上一篇我们把Hadoop集群、Hive数仓、调度框架这些底座搭了起来,能跑通全流程,但离“给业务同学直接看数据”还差着十万八千里。这一篇的核心就一件事——让数据真正开始跑业务:从实时接入、数仓分层建模,到Flink实时计算、Doris加速查询,最后落到一个业务同学每天一上班就打开看一眼的大屏和报表。如果你手里正好有一个类似的电商数据项目,或者你正在做数仓、实时计算相关的搭建和排障,这篇里的选型思路和踩坑记录应该能帮你省掉不少弯路。
老规矩,先交代一下背景。项目代号就叫“某电商分析平台”,模拟的是一家中小规模电商公司的数据分析场景,业务库是MySQL,日订单量在百万级别,大促期间能冲到千万。第一季我们解决了“数据从哪来、怎么存、怎么跑批”的问题,这一季重点解决“数据怎么算得快、算得准、业务怎么用起来”。技术栈换了不少血,也踩了不少坑,我按实际推进的顺序一个一个讲。
1. 这个续篇到底要补什么——从数据底座到业务闭环
1.1 第一季的成果和遗留问题
第一季我们把离线链路打通了:Canal监听MySQL的binlog,Flume收集Nginx日志,数据落到Kafka,再进Hive做离线清洗。调度平台每天凌晨跑一遍全量/增量同步,数仓基本形态已经有了。但真正用起来的时候问题一堆:报表T+1,大促当天运营想看实时数据只能干等;指标口径没人统一,运营看GMV是含退款金额的,财务看的是剔除退款之后的,两个人拉出来的数永远对不上;明细查询也慢,一张订单明细表在Hive里用HiveQL离线算,一个中等粒度的聚合查询要跑几分钟,完全不现实。
所以第二季的需求非常明确,归纳下来三条:第一,实时链路必须建起来,核心指标当天当天看,大屏能滚起来;第二,指标口径必须统一,最好有一套可靠的指标字典,谁查谁都一个数;第三,查询必须快,业务同事和看板接口不能等太久,SQL出去几秒出不来就是失败。这三个问题对应的正好就是三块工作:实时计算、数仓精细化建模、OLAP加速。
1.2 这一篇适合谁看
如果你是第一次接触类似项目的初学者,建议先把第一季的离线架构过一遍,再来看这篇,否则直接上Flink和Doris容易懵。如果你已经有数仓基础,正在做实时化改造,这篇的价值主要在选型对比和排障细节,尤其是我踩过的状态后端、双流Join、指标对账这些坑。如果你纯粹是想把电商分析平台的整个技术链条串起来,当作毕设或者面试项目储备,那这一篇可以帮你把“实时数仓 + OLAP + 可视化”这条主线理清楚。
我尽量把每个决策背后的原因讲明白,不只是告诉你怎么配置,而是告诉你为什么这样配。比方说为什么实时链路用Doris而不是ClickHouse,为什么Flink的checkpoint要这样设置,为什么UV不能用count distinct硬算。这些“为什么”才是项目里真正值钱的东西。
2. 技术选型背后的取舍——这套组合拳是怎么定的
2.1 离线链路:Hive + 调度平台打底
离线这条线没有推翻重来,还是在Hive上做。Hive吞吐量高、生态成熟、团队上手快,做T+1的宽表和汇总层没有任何问题。唯一改进的是把原来大量用HiveQL写的清洗任务逐步改成SparkSQL跑,原因是随着数据量上来,MapReduce的响应时间实在太感人,一个简单的join动辄十几分钟,SparkSQL配合向量化执行能把大部分任务压到两三分钟以内。
调度这一块,我们当时对比了一下几个开源调度器,最后选了自己维护成本较低的一个,原因是团队小,不想在这个环节引入太重的东西。调度频率按数据依赖设:日志类数据每半小时同步一次,业务库binlog每15分钟增量落地一次,凌晨2点统一跑DWD到DWS的加工任务,凌晨5点前把ADS层全量产出完。这样业务同事早上打开报表,数据已经是齐的。
2.2 实时链路:Kafka + Flink + Doris的组合
实时这条线选型磨了很久。第一版考虑过Spark Streaming,但仔细评估后发现,我们要做的是订单流和支付流的双流Join、状态管理、乱序数据的处理,Spark Streaming的微批模型在这种场景下写起来非常别扭。最后定了Flink,原因很朴素:流式计算的状态管理、checkpoint、watermark这些机制成熟,双流Join的API是现成的,生态也够完整。
OLAP这一层,我们在ClickHouse和Doris之间犹豫过。ClickHouse的查询性能确实猛,但它的数据更新语义不如Doris灵活,而电商场景里有大量订单状态变更,需要支持主键更新,这点Doris的Unique模型天然契合。再加上Doris兼容MySQL协议,我们内部开发同学基本零成本上手,运维也轻量,最终选了Doris。上线跑了一段时间后证明这个决定是对的,尤其是后面对账和明细修正的需求越来越多,Doris的实时更新能力帮了大忙。
2.3 为什么不上一套“全家桶”
这个项目进行到一半,也讨论过要不要上一套完整的商业智能平台,或者直接买个现成的数据产品。最终没这么干,原因有三:一是预算有限,商业产品按节点收费,我们这种规模一年下来并不便宜;二是数据源和业务口径需要深度定制,现成平台很难完全贴合;三是团队本身需要积累大数据工程能力,这个项目是最好的练兵场。自己做虽然投入人力,但换来的是完全可控的链路和一批能打的技术骨干,长期看是划算的。
还有一个细节是版本选型。Flink我们直接用了较新的稳定版,没用老的1.7之类。Doris也尽量保持小版本跟进。我的经验是,做新项目尽量用新版本,不要用过于保守的版本,否则后面踩到老版本bug,排查起来成本更高。
3. 数仓分层与指标体系——口径不统一,全盘皆输
3.1 分层的设计思路
第二季的数仓在原来基础上做了重构,严格按ODS、DWD、DWS、ADS四层来设计。ODS层保持原样,就是原始数据落地,表结构和源系统一致,加了分区、压缩,存储用ORC。DWD层做清洗和标准化处理:脱敏手机号、剔除测试订单、统一时间字段格式、解析埋点参数,然后做维度退化,把商品、店铺、类目、用户等维度字段直接退化到事实表里。这样后面聚合的时候少很多Join,查询快得多。
DWS层是轻度汇总层,按业务过程建模。举个例子,订单事实表在DWS层会按照“店铺+类目+小时+天”粒度预聚合一张表,字段包括订单数、支付订单数、GMV、退款金额、用户数等。ADS层面向具体应用,比如大屏指标表、经营日报表、渠道分析表,都是非常窄的宽表,接口查询直接打这里。
这套分层的核心价值在于:每一层都有清晰的职责,依赖方向是单向的,不会出现底层直接对接业务方然后又反向上游补数据的情况。某个指标的口径要调整,只需要改DWD或DWS对应逻辑,ADS不用大动。
3.2 电商核心指标的口径统一
这个项目最折腾的不是技术,是指标口径。我们一度出现运营和财务对同一个“GMV”争论不休的场景。最后我们定了一套指标字典,核心口径用文档固化下来,并且在数仓加工逻辑里强制统一。
举几个例子:
- GMV:支付成功的订单金额,剔除全额退款订单,未支付订单不计入。部分退款订单按实际支付金额计算。
- 下单金额:用户提交订单但未支付时的订单金额,和GMV是两条线。
- UV:按设备/用户ID去重后的访客数,移动端按设备ID去重,PC端按登录用户ID去重,两者不做跨端合并。
- 转化率:支付用户数 / 访问用户数,这里的支付用户数按支付事件去重的用户数算。
- 复购率:统计周期内购买两次及以上的用户数 / 总购买用户数。
这些口径看起来简单,落地时一堆细节。比如“支付成功”的时间点到底以支付成功回调为准还是以支付流水入库为准?我们统一为支付流水表入库时间。比如退款计算,是当天的退款申请时间还是退款完成时间?我们统一为退款完成时间。口径不定,后面Flink实时作业和离线任务永远对不上数。
3.3 指标字典和维度建模
有了指标字典之后,我们还做了一张维度字典表,把常用维度收敛起来:时间维度(天、小时、分钟)、渠道维度(自然搜索、广告投放、促销活动)、终端维度(iOS、Android、H5、小程序)、地域维度(省份、城市)、商品维度(SPU、SKU、类目、品牌)。
维度字典的好处是,数仓建模的时候明确知道哪些字段可以退化到事实表,哪些必须保留维表关联。比如地域这个维度,我们在DWD层就把订单关联到了省市,因为在订单表里本来就有收货地址。而品牌、类目这种偏商品主数据的维度,我们保留维表,用维度表的快照和事实表关联,避免事实表字段无限膨胀。
这套建模规范做完以后,新增一个报表需求的时间从之前的两三天压缩到半天以内,因为90%的字段直接从DWS/ADS层取,不需要重新写复杂SQL。
4. Flink实时计算核心实现——直播大屏背后的作业
4.1 第一个实时场景:实时GMV大屏
实时链路第一个落地的业务是“今天实时看板”:大屏上滚动刷新的当日GMV、订单量、支付用户数、热销商品Top10。这个场景初期用Flink SQL就能覆盖,读Kafka里的订单支付流,做开窗聚合,结果写入Doris,然后接口查询。
Flink SQL的写法很简洁,Kafka表源和水位线声明如下:
CREATE TABLE kafka_pay_stream ( order_id BIGINT, user_id BIGINT, shop_id BIGINT, sku_id BIGINT, pay_amount DECIMAL(20,2), pay_time TIMESTAMP(3), WATERMARK FOR pay_time AS pay_time - INTERVAL '5' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'ods_pay_done', 'properties.bootstrap.servers' = 'kafka-1:9092,kafka-2:9092,kafka-3:9092', 'properties.group.id' = 'flink-realtime-dashboard', 'scan.startup.mode' = 'latest-offset', 'format' = 'json' );窗口聚合按1分钟粒度产出,同时在Doris里做一层分钟级汇总,大屏按5分钟刷一次,这样既保证了实时性,又不至于让查询压力太大。注意这里Kafka的topic分区数很关键,我们按24个分区预先规划好,因为Flink作业的并行度理论上不能超过分区数,分区数太少后面想扩容都没得扩。
4.2 订单流与支付流的双流Join
实时大屏只是开胃菜,真正麻烦的是订单流和支付流的双流Join。业务上要分析“下单到支付的平均时长”“未支付订单的流失漏斗”,光看支付流不够,必须把订单创建流和支付流水关联起来。这两条流不是同时到达的,用户可能下单后10秒支付,也可能拖了半小时才支付,极端的还有次日支付。
这个场景用Flink的interval join非常合适。左右两条流约定一个时间边界,只关联在指定时间窗口内的数据。我们当时的代码关键逻辑是这样:
DataStream<OrderRecord> orderStream = ...; DataStream<PayRecord> payStream = ...; SingleOutputStreamOperator<JoinedRecord> result = orderStream .keyBy(OrderRecord::getOrderId) .intervalJoin(payStream.keyBy(PayRecord::getOrderId)) .between(Time.minutes(-5), Time.minutes(60)) .upperBoundExclusive() .process(new ProcessJoinFunction<OrderRecord, PayRecord, JoinedRecord>() { @Override public void processElement(OrderRecord left, PayRecord right, Context ctx, Collector<JoinedRecord> out) { out.collect(buildJoinedRecord(left, right)); } });这里的核心是between(Time.minutes(-5), Time.minutes(60)),含义是订单创建时间减5分钟到订单创建时间加60分钟这个范围内,如果支付事件落在区间内就能匹配上。设置上界60分钟是因为内部统计发现99%的支付发生在下单后1小时内,太长的等待只会浪费状态存储。下界设负5分钟是为了容忍上游数据乱序稍微超过watermark的情况。
4.3 Watermark和Checkpoint的实战配置
整个实时链路里,最容易被忽视但又最关键的是时间语义。我们全部采用事件时间,watermark策略是允许5秒的乱序。这个值不能设太大,否则指标延迟敏感;也不能太小,否则晚到数据太多直接丢。5秒是我们压测之后选的折中值。
checkpoint配置也踩了大坑。初期默认配置下,checkpoint频繁失败,作业隔几天就要重启恢复一次。后来我们调整成60秒触发一次,最小间隔30秒,超时5分钟,并且状态后端选了RocksDB,存储在HDFS上。达到精确一次的语义依赖Kafka和Flink的两阶段提交机制,Sink配置了exactly-once模式,这样Kafka消费偏移量和Doris写入结果才能做到不重不丢。
env.enableCheckpointing(60 * 1000L); CheckpointConfig config = env.getCheckpointConfig(); config.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE); config.setMinPauseBetweenCheckpoints(30000L); config.setCheckpointTimeout(5 * 60 * 1000L); config.setMaxConcurrentCheckpoints(1); config.enableExternalizedCheckpoints(CleanupMode.RETAIN_ON_CANCELLATION); RocksDBStateBackend backend = new RocksDBStateBackend("hdfs://nameservice/flink/checkpoint"); env.setStateBackend(backend);状态TTL也要记得设置,尤其是interval join的过期状态。我们给订单流的状态设置TTL为2小时,避免同一个订单一直占用存储。如果不设置TTL,Flink状态会越积越大,RocksDB的磁盘占用涨到几十G的时候,查询性能直线下降。
4.4 实时指标从Flink落到Doris
实时计算的结果最终要落到Doris供查询。Doris建表这块有几个注意点。DWD明细用Unique模型,配合主键更新实现订单状态变更;DWS汇总用Aggregate模型,聚合函数用SUM和BITMAP_UNION。UV去重这个场景,如果直接用COUNT(DISTINCT user_id),数据量一大性能就不行,正确做法是Doris里用Bitmap,上游Flink计算UV时把用户ID收集成Bitmap,Doris做Bitmap做并集去重,速度能快上几个数量级。
用一条示例DDL说明:
CREATE TABLE dws_trade_shop_daily ( ds DATE, shop_id BIGINT, category_id BIGINT, order_cnt BIGINT SUM, pay_gmv DECIMAL(20,2) SUM, pay_user_bitmap BITMAP BITMAP_UNION, pay_user_cnt BIGINT REPLACE ) ENGINE=OLAP AGGREGATE KEY(ds, shop_id, category_id) DISTRIBUTED BY HASH(shop_id) BUCKETS 16 PROPERTIES ("replication_num" = "2");注意这里的pay_user_cnt字段,既想通过Bitmap算UV又想在同一个查询里直接返回数值,可以用一个REPLACE字段在Doris聚合模型中更新,也可以用MATERIALIZED VIEW,这个看具体场景取舍。我们因为业务经常要看不同维度组合的UV,直接采用了“Bitmap字段+原子指标”双写的方式,查询的时候灵活切换。
5. 从明细到看板——查询服务和大屏展示
5.1 指标查询接口的架构设计
数据都进了Doris,但前端和业务方不可能直接写SQL查Doris,中间必须有一个查询服务层。我们用SpringBoot封装了一层指标查询接口,统一入参和出参格式,底层路由到Doris执行SQL。接口的粒度设计非常关键,大屏需要分钟级趋势,报表需要小时级和天级,我们直接在接口参数层做了限制,避免一次查询拉太粗或太细的数据把Doris打爆。
具体的参数化设计例如:查询实时GMV趋势,接口会接收timeGranularity参数,支持minute/hour/day三种粒度,同时限定返回条数上限。服务端拿到请求后先拼SQL,但绝不使用动态拼接用户传入的字段名,避免注入问题。所有可查询的指标和维度都从指标字典表里读出来,前端传什么就校验什么,而不是自由发挥加一堆聚合条件。
缓存这块我们分了两层:一层是Doris的物化视图和预聚合,另一层是Redis热点缓存。大屏数据轮询频率高,但同一天的“今日GMV”这个数字5秒内变化不大,所以我们针对小时级以上的指标做了Redis缓存,缓存时长60秒,命中率大概70%,有效降低了Doris的查询压力。
5.2 大屏前端的关键细节
大屏前端用的是Vue加ECharts。第一版大屏效果很炫,但有个致命问题:生产环境的屏幕分辨率不统一,有的1920,有的是1080,还有的甚至用竖屏拼接屏。最开始我们用px写死,结果换一台电脑样式全乱。后来统一改成了rem加transform: scale的适配方案。简单说就是设计稿按1920x1080做,页面根元素动态计算缩放比例,整屏缩放,效果稳定多了。
刷新技术路线也纠结了一下,WebSocket定长推送还是HTTP轮询?我们最终选了HTTP轮询,间隔3到5秒。原因有二:第一,轮询实现简单,不容易出连接泄漏的服务端问题;第二,大屏对“实时性”的要求其实就是5秒以内的数据新鲜度,3秒轮询完全满足,没必要引入WebSocket增加运维复杂度。真到了需要秒级触发的场景(比如秒杀大屏),再考虑WebSocket也不迟。
ECharts本身有几个细节值得提。大屏上的滚动排名数字,用setInterval刷新数据时,不用整图重绘,只需要调setOption部分更新的series数据,否则会闪烁。折线图的平滑要用smooth参数,而不是自己后端造数据插值。地图类组件分辨率要匹配业务地图插件,省市区县级的图尽量用项目内置的地图数据,不要加载外部资源,不然内网环境直接白屏。
5.3 一眼看懂的数据可视化怎么设计
这块我说点运营同学教我的经验。数据可视化不是炫技,业务方打开大屏第一眼要看到的是今日核心指标和同比环比。所以大屏的整体布局是:顶部放今日GMV、订单量、支付用户数、客单价四个大数字,中间看成单趋势折线图,右侧放渠道占比环形图,底部放区域销售分布和热销商品滚动榜单。
最容易被忽视的是异常标识。比如某个指标同比下跌超过20%,大屏上直接标红显示,并附带一段后台配置的异常原因文案。这个功能其实只需要接口返回的数据加一个trendFlag字段,前端根据字段值渲染不同颜色。但它的业务价值非常大,运营一看红点就知道今天有问题,不用自己翻报表逐个核对。
6. 一路踩过的坑与排查实录
6.1 数据倾斜:大促当天热SKU把任务拖垮
第一次大促压测时,Flink作业的某个子任务处理量明显高于其他子任务,背压一路往上顶,整个作业的延迟从几秒涨到几分钟。查Flink Web UI,发现某个SKU的聚合子任务输入记录数是其他子任务的上百倍。原因很典型:一个爆款SKU的流量占全网下单量的40%,这个key自然倾斜。
当时用了个比较简单粗暴但有效的方案:对聚合key做加盐处理。在keyBy之前把shop_id拼上一个随机后缀,把一条大流拆成多条小流,第一层聚合先做部分聚合,第二层再按真实的shop_id汇总。这个方案对SUM、COUNT这类算子有效,但对去重类算子不适用,去重场景还是得靠调整并行度或者用近似去重算法。另外,如果是双流Join时出现热点Key,最靠谱的方式是拆成两段,先按用户ID关联,再按商品维度做补充聚合。
6.2 实时指标和离线指标永远对不上
这是项目里最让人头疼的问题,也是数据团队和业务团队冲突的高发地带。我们的实时今日GMV是1.53亿,离线T+1算出来是1.61亿,两边差了5%。排查了很久,原因主要有三块。第一是乱序数据问题,实时作业的5秒watermark只容忍5秒内乱序,但支付回调偶尔有几十秒的延迟,晚到的数据被丢掉了;第二是窗口边界不一致,实时任务用的窗口是严格按事件时间切的,离线任务按业务日期加自然天切,两边在跨天订单上天然对不齐;第三是口径细节,离线任务剔除了一部分自动化测试订单,实时任务当时没有过滤。
解决办法是分步走的:首先,实时任务也按统一口径过滤,测试订单和无效订单直接从源端过滤掉;其次,允许更大的乱序容忍度同时把晚到数据写到侧输出流,每天凌晨离线侧再把这些迟到数据补算一次;最后就是最重要的对账机制,每天凌晨3点离线数据产出之后,自动跑一个实时与离线的对账任务,按天对比核心指标,偏差率超过1%就告警告到数据团队群里。
6.3 Kafka消费积压问题处理实录
有一次凌晨突然报警,实时大屏数据延迟两个多小时,查下来是Kafka消费端Lag数据飙升。第一反应看消费者组的lag详情,发现某个Flink作业的消费速率掉到了接近0。进一步看日志,是下游Doris写入超时导致作业频繁回滚重启,重启期间数据自然就积压了。
处理思路是这个顺序:先看Doris侧并发写入是否过大,导致导入任务排队;再把Flink的Sink并行度降下来,控制写入压力;最后一次性将消费组的offset重置到最新或指定时间点,把积压的旧数据跳过。注意这里不能随便重置offset,要评估跳过数据对指标的影响,如果大屏只是看当日实时值,直接跳过错峰数据影响不大;如果数据还要进数仓做历史分析,就必须重放数据,那就麻烦得多。
我把这次经历沉淀成了一条排查规则:任何消费积压问题,先查下游,再查网络,最后才怀疑作业逻辑。顺序反了,大概率白忙半天。
6.4 常见问题速查表
平时被问得最多的问题我整理了一张表,适合直接贴到团队文档里:
| 现象 | 排查方向 | 解决思路 |
|---|---|---|
| 实时大屏数据延迟上涨 | 看Kafka消费组Lag、Flink背压 | 先检查下游写入是否超时,再考虑扩容并行度 |
| 同一个订单实时和离线金额不一致 | 检查口径、时区、迟到数据 | 统一口径,增加迟到数据侧输出流,做每日对账 |
| Doris查询偶尔超时 | 看是否大查询扫了全表 | 限制单次查询粒度,增加物化视图,命中Redis缓存 |
| Flink作业频繁Checkpoint失败 | 查RocksDB磁盘、HDFS写入 | 调整checkpoint间隔,缩短超时时间,检查状态大小 |
| 大屏打开白屏 | 排查资源路径和跨域 | 生产环境资源统一打包,接口确认跨域配置 |
| UV指标严重偏高 | 确认是否做了跨端去重 | 按终端分别统计,或用Bitmap去重 |
这张表不是理论,每一个都是实际报障记录里汇总出来的。遇到类似问题,按表里的思路排查,基本能解决80%的异常。
7. 最后的经验与后续规划
整个第二季做下来,我最大的体会是:实时数仓项目的瓶颈从来不是框架本身,而是口径、链路和运维这三个词。框架选型一天能定,但口径对齐、链路排查、监控告警这些细活,才是决定项目上线后是“平稳运行”还是“天天救火”的关键。如果你也在做类似项目,我建议从第一天就建立指标字典和血缘关系表,把每个指标的来源、口径、加工逻辑用文档锁死,同时把实时和离线的对账机制当成上线必要条件来做,不要“有时间再补”。
这部分的成本大约会占到整个项目四分之一的工作量,但绝对值回票价。我们后期的绝大多数问题,都是靠这些基础文档和监控规则快速定位的。没有它们,大概率还在数据库里翻SQL、在日志里找线索。
后续这一套平台还有几个方向可以扩展。一个是增加实时推荐场景:利用用户实时行为流,结合离线训练好的推荐模型,在用户浏览时实时产出推荐结果,这块需要引入线上推理服务。另一个是把指标管理升级成真正的指标平台,支持业务方自助配置指标、自助看板,核心是把指标定义DSL化,和底层计算逻辑解耦。第三个方向是湖仓一体实践,把Hive和Doris的元数据统一管理,让实时和离线共享同一套表结构和数据目录。这些都是下一步值得啃的硬骨头,也是这类电商分析平台项目能持续演进的价值所在。