电商分析平台这个系列写到第二篇先把话挑明上一篇我们把Hadoop集群、Hive数仓、调度框架这些底座搭了起来能跑通全流程但离“给业务同学直接看数据”还差着十万八千里。这一篇的核心就一件事——让数据真正开始跑业务从实时接入、数仓分层建模到Flink实时计算、Doris加速查询最后落到一个业务同学每天一上班就打开看一眼的大屏和报表。如果你手里正好有一个类似的电商数据项目或者你正在做数仓、实时计算相关的搭建和排障这篇里的选型思路和踩坑记录应该能帮你省掉不少弯路。老规矩先交代一下背景。项目代号就叫“某电商分析平台”模拟的是一家中小规模电商公司的数据分析场景业务库是MySQL日订单量在百万级别大促期间能冲到千万。第一季我们解决了“数据从哪来、怎么存、怎么跑批”的问题这一季重点解决“数据怎么算得快、算得准、业务怎么用起来”。技术栈换了不少血也踩了不少坑我按实际推进的顺序一个一个讲。1. 这个续篇到底要补什么——从数据底座到业务闭环1.1 第一季的成果和遗留问题第一季我们把离线链路打通了Canal监听MySQL的binlogFlume收集Nginx日志数据落到Kafka再进Hive做离线清洗。调度平台每天凌晨跑一遍全量/增量同步数仓基本形态已经有了。但真正用起来的时候问题一堆报表T1大促当天运营想看实时数据只能干等指标口径没人统一运营看GMV是含退款金额的财务看的是剔除退款之后的两个人拉出来的数永远对不上明细查询也慢一张订单明细表在Hive里用HiveQL离线算一个中等粒度的聚合查询要跑几分钟完全不现实。所以第二季的需求非常明确归纳下来三条第一实时链路必须建起来核心指标当天当天看大屏能滚起来第二指标口径必须统一最好有一套可靠的指标字典谁查谁都一个数第三查询必须快业务同事和看板接口不能等太久SQL出去几秒出不来就是失败。这三个问题对应的正好就是三块工作实时计算、数仓精细化建模、OLAP加速。1.2 这一篇适合谁看如果你是第一次接触类似项目的初学者建议先把第一季的离线架构过一遍再来看这篇否则直接上Flink和Doris容易懵。如果你已经有数仓基础正在做实时化改造这篇的价值主要在选型对比和排障细节尤其是我踩过的状态后端、双流Join、指标对账这些坑。如果你纯粹是想把电商分析平台的整个技术链条串起来当作毕设或者面试项目储备那这一篇可以帮你把“实时数仓 OLAP 可视化”这条主线理清楚。我尽量把每个决策背后的原因讲明白不只是告诉你怎么配置而是告诉你为什么这样配。比方说为什么实时链路用Doris而不是ClickHouse为什么Flink的checkpoint要这样设置为什么UV不能用count distinct硬算。这些“为什么”才是项目里真正值钱的东西。2. 技术选型背后的取舍——这套组合拳是怎么定的2.1 离线链路Hive 调度平台打底离线这条线没有推翻重来还是在Hive上做。Hive吞吐量高、生态成熟、团队上手快做T1的宽表和汇总层没有任何问题。唯一改进的是把原来大量用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非常合适。左右两条流约定一个时间边界只关联在指定时间窗口内的数据。我们当时的代码关键逻辑是这样DataStreamOrderRecord orderStream ...; DataStreamPayRecord payStream ...; SingleOutputStreamOperatorJoinedRecord result orderStream .keyBy(OrderRecord::getOrderId) .intervalJoin(payStream.keyBy(PayRecord::getOrderId)) .between(Time.minutes(-5), Time.minutes(60)) .upperBoundExclusive() .process(new ProcessJoinFunctionOrderRecord, PayRecord, JoinedRecord() { Override public void processElement(OrderRecord left, PayRecord right, Context ctx, CollectorJoinedRecord 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小时避免同一个订单一直占用存储。如果不设置TTLFlink状态会越积越大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收集成BitmapDoris做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 ) ENGINEOLAP 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亿离线T1算出来是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的元数据统一管理让实时和离线共享同一套表结构和数据目录。这些都是下一步值得啃的硬骨头也是这类电商分析平台项目能持续演进的价值所在。