引言
在转转业务快速发展的过程中,业财系统作为连接业务与财务的核心枢纽,其重要性日益凸显。早期我们基于微服务架构构建了“金字塔”系统,通过统一收集上下游业务数据并加工财务指标,支撑了公司初期的财务分析需求。然而,随着业务复杂度提升和数据量激增,这套架构逐渐暴露出诸多问题:指标差异难以溯源、数据处理效率低下、系统稳定性不足等。本文将详细分享我们如何通过架构演进,最终构建出高效可靠的敏捷财报系统。
第一阶段:微服务架构的困境分析
1.1 初始架构设计
产品架构
数据分层加工
1.2 存储设计
业务特点分析:
特点1:各业务数据字段基本不相同,几乎没有可抽取出的统一字段,如果想统一存储,只能以JSON字符串的形式;
特点2:需要对加工后的财务数据实时可查,若以JSON存储,不方便结构化查询;
特点3:如果不统一存储,来一个业务新建一些表,维护成本很高,万一数据量大,还涉及到分库分表问题;
特点4:源数据来源方式不相同,有用接口的,有用云窗的,有人工后台录入的;
由于早期数据量并不大,基于以上业务特点,采用了如下的存储方式。 引入 ES 用于支撑多维数据查询,采用监听 binlog 同步 ES 的方式进行数据同步。
各模块源数据统一组装成 JSON 字符串,存储在金字塔项目的一张表(JSON 表,以下简称source_data表)中,源数据的每个模块都有自己的唯一Code,binlog处理程序根据Code统一将 JSON 表数据按照每个模块解析到 ES 对应索引中,这样可以支持数据实时结构化查询,以及后续新增模块接入的话,不需要再开发同步ES的代码。在 MySQL 库中source_data表的数据量达到一定量级后,只针对source_data表分表即可。
1.3 调度模型设计
由于需要离线处理多个模块的指标数据,采用离线定时任务的方式进行处理,这里使用了分布式调度任务框架xxl-job。
调度流程
调度模型可以简化为上图流程所示。
可能这里有同学会想,为啥不采用xxl-job的分片广播的形式进行处理, 而采用mq广播消费的方式。
- 一方面主要是考虑到执行DWD任务种类很多,涉及物流费、支付手续费等任务。并且每个模块处理的参数比较个性化。这里主要是做任务分发,针对一个模块的任务只能一台机器获取(简化处理模型),然后内部多线程处理这个模块。而一台机器可以争抢0~N个模块的任务。而xxl-job分片任务的初衷是多台机器共同处理同一模块的分片数据。
- 另一方面是想在业务层面判断某些模块任务是否执行完成,做一些更精细化的控制。这里xxl-job框架层面不支持。
1.4 处理模型设计
任务处理
在内存中通过RPC进行维度关联并进行指标计算,计算完的结果存入dwd_financial,之后分别根据各个指标的要求汇总统计存入dws_financial表中,后续定时将指标数据同步到Hive中供分析部门使用。
第二阶段:架构演进的考量
2.1 核心问题分析
问题1:数据完整性难以保证
微服务架构下,数据分散在各个微服务所对应的数据库中,数据形成孤岛。财务计算需要跨多个服务获取维度和度量数据:
// 订单金额计算需要调用多个服务
public BigDecimal calculateOrderAmount(Long orderId) {
Order order = orderService.getOrder(orderId); // 1. 获取基础订单
User user = userService.getUser(order.getUserId()); // 2. 获取用户等级
Coupon coupon = couponService.getCoupon(...); // 3. 获取优惠信息
// ...更多服务调用
}
- RPC调用不稳定,难以保证维度不缺失,即使重试也不一定能100%成功,维度缺失率高达10%。
- 如果某条数据处理失败后,简单重试几次还失败,那就整体失败了,对后续的处理链路会存在阻断。如果不阻断,计算的数据维度缺失,最终统计的结果也不准。两者之间存在博弈。
问题2:任务调度与数据同步的不可靠性:
- ES同步状态不可见,只能预留大致时间缓冲,容易出现ES没同步完成,就开始计算指标数据了,造成差异。
- xxl-job任务调度与云窗(58大数据平台)数据抽取不能联动,可能存在xxl-job还没执行完,就开始了数据抽取任务,导致数据不准确。
问题3:扩展性瓶颈
随着数据量增长:
- 单机处理能力达到上限(最高延迟6小时)。
- 新增指标需要修改代码并重新部署。
- 资源无法弹性扩展。
2.2 解决思路
- 尽量规避RPC调用:
- 如果还采用微服务架构,则需要成功将维度数据预加载到本地缓存,但维度之多,以及非维度的数据也可能需要RPC才能获取到,所以不能完全解决。
- 不用RPC来关联各微服务的数据,而采用数据中心的思想,将多个微服务的数据库同步到数据中心进行统一处理。
- 解耦分析场景:将分析型负载从交易系统中剥离。 例如将多个微服务数据关联后分类汇总的功能,从微服务中拆分出来,微服务只做OLTP相关功能。
- 采用统一的调度生态:如可以采用云窗统一的信号调度。不过整体架构得跟着改造,不能再用微服务处理。
- 多机器并行处理:可以采用分布式计算框架Spark进行并行处理,优化单机处理瓶颈。
2.3 下一步如何抉择
我们评估了三种可能的演进方向:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 增强现有微服务架构 | 改动小,延续现有技术栈 | 无法根本解决分析瓶颈 | 小规模数据 |
| 引入大数据技术栈 | 专业分析能力 | 学习成本高 | 中大规模数据分析 |
| 采用商业解决方案 | 开箱即用 | 成本高,灵活性差 | 快速上线需求 |
最终决策因素:
- 业务数据量已超过单机处理能力(日均订单>30万)。
- 每月需要离线处理千万级、亿级别数据。
- 财务分析需求日益复杂(需要支持多维分析)。
- 团队有3个月窗口期进行技术转型。
经过对比,尝试引入大数据技术栈来解决目前的痛点。
2.4 数据处理的本质差异
-
微服务实时调用 vs 大数据批处理
- 微服务RPC 需实时调用多个服务获取维度及度量,网络延迟、服务故障会导致调用链断裂(如超时率高达5%),且分布式事务难以保证一致性。
- 大数据技术(如Spark/Hadoop)采用批处理模式,通过ETL流程将数据集中处理,所有维度关联在计算前已完成,避免运行时依赖外部服务。
-
计算向数据移动的思想
大数据框架(如MapReduce)将计算任务分发到数据存储节点执行,减少网络传输;而微服务RPC需跨网络频繁传输数据,增加不可靠性。
第三阶段:新架构设计
3.1 数据模型设计
3.1.1 分层设计(核心基础) :
数据流向
- ODS层(Operational Data Store):操作数据存储层,存储原始数据镜像。
- DW层(Data Warehouse):数据仓库层,存储经过标准规范化处理(即数据清洗)后的运营数据,是基础事实数据明细层。如:收入成本明细数据、mysql各业务数据经过ETL处理后的表。
- DIM层: 维度数据层,主要包含一些字典表、维度数据。如:品类字典表、城市字典表、渠道字典表、终端类型表、支付状态表等。
- DM层(Data Market):数据集市层,按部门按专题进行划分,支持OLAP分析、数据分发等。如:日活用户业务分析表,商业广告多维分析报表,销售回收明细宽表。
- ADS(Aplication Data Store)层:直接面向应用的数据服务层。
3.1.2 维度建模:
选择业务过程 --> 声明粒度 --> 确定维度 --> 设计事实表
星型模型示例
基于事实和维度描述业务场景,构建星型模型。
-- 共享维度表
CREATE TABLE dim_time (
date_key INT PRIMARY KEY,
full_date DATE,
day_of_week TINYINT,
month TINYINT,
quarter TINYINT,
year SMALLINT
);
-- 订单事实表
CREATE TABLE fact_orders (
order_id BIGINT,
date_key INT REFERENCES dim_time,
-- 其他字段...
);
-- 库存事实表
CREATE TABLE fact_inventory (
sku_id BIGINT,
date_key INT REFERENCES dim_time,
-- 其他字段...
);
建模后的好处:
- 方便一致性维度的复用和管理。多个事实可以关联相同维度。
- 扩展灵活性高:维度变化不影响现有事实,各自独立更新。
- ETL开发高效,方便扩充不同的分析主题。
3.1.3 调度系统:
- 统一采用云窗任务依赖的方式,实现父子任务管理,统一调度模型。
- 关键路径监控和自动重试。
3.2 大数据技术选型
核心组件对比:
计算引擎:
| 引擎 | 计算模型 | 适用规模 | 典型延迟 | SQL兼容性 | 容错机制 | 资源消耗 | 学习曲线 | 最佳场景 | 企业案例 |
|---|---|---|---|---|---|---|---|---|---|
| Hive(MR) | 批处理(MapReduce) | <10TB | 小时级 | HiveQL | 磁盘Checkpoint | 高(IO) | 低 | 历史数据分析、小规模ETL | 传统银行数仓 |
| Hive(Tez) | DAG批处理 | <50TB | 分钟-小时 | HiveQL | 任务重试 | 中 | 低 | 中等规模数仓 | 电信运营商 |
| Spark SQL | 内存批处理 | 10PB+ | 秒-分钟 | ANSI SQL | 内存Lineage | 高(内存) | 中 | 大规模ETL、迭代计算 | 互联网公司 |
| Flink Batch | 流批一体 | 1PB+ | 秒级 | ANSI SQL | 精确一次(Checkpoint) | 高 | 高 | 流批统一架构 | 实时数仓场景 |
OLAP引擎对比:
| 维度 | StarRocks | Doris | ClickHouse |
|---|---|---|---|
| 单表查询性能 | 快(向量化引擎 + SIMD 优化) | 较快(均衡性能) | 极快(大宽表聚合最优,SIMD 深度优化) |
| 多表关联性能 | 最优(支持多种 JOIN 策略) | 良好(依赖 CBO 优化) | 较弱(需预计算宽表) |
| 实时写入能力 | 支持秒级更新(主键模型) | 支持实时导入(Kafka/Flink) | 仅批量写入,更新需替换分区 |
| 并发能力 | 高并发(千级 QPS) | 中高并发(百级 QPS) | 低并发(单查询资源消耗高) |
| 数据压缩率 | 高(列式压缩) | 高(类似 StarRocks) | 最高(列式压缩优化) |
- 单表性能:ClickHouse > StarRocks > Doris
- 多表关联:StarRocks > Doris > ClickHouse
- 实时性:StarRocks ≈ Doris > ClickHouse
- 高并发场景:StarRocks > Doris > ClickHouse
通过对比可知,在计算引擎采用SparkSQL支持大规模的ETL且结合目前58云窗大数据平台的现有功能支持,实现成本和上手成本较低,也为后面数据增长预留支撑,所以选择SparkSQL。
在OLAP引擎方面,StarRocks/Doris在多表关联的查询性能及并发能力显著优于ClickHouse。从多表关联查询能力以及后期扩展性上我们考虑使用StarRocks。
3.3 数仓体系架构图
基于上述选型,以及结合转转数仓规范,构建了如下架构。
转转数仓架构体系
财报整体架构图
3.4 数据处理示例
在数据仓库环境中,使用SparkSQL替代Java服务调用的计算逻辑,可以通过以下方式实现:
- 基础订单金额计算(单表)
-- 直接基于订单事实表计算
SELECT
order_id,
original_amount,
shipping_fee,
original_amount + shipping_fee AS total_amount
FROM dwd_order_detail
WHERE dt = '${biz_date}'
2. 多维度关联计算(替代Java服务调用)
-- 替代原Java的多服务调用逻辑
SELECT
o.order_id,
o.original_amount,
-- 用户维度关联计算
CASE
WHEN u.vip_level = 'PLATINUM' THEN o.original_amount * 0.9
ELSE o.original_amount
END AS vip_adjusted_amount,
-- 优惠券维度关联计算
COALESCE(c.coupon_amount, 0) AS coupon_deduction,
-- 最终实付金额
(o.original_amount + o.shipping_fee - COALESCE(c.coupon_amount, 0)) AS final_amount
FROM dwd_order_detail o
LEFT JOIN dim_user u ON o.user_id = u.user_id AND u.dt = '${biz_date}'
LEFT JOIN dim_coupon c ON o.coupon_id = c.coupon_id AND c.dt = '${biz_date}'
WHERE o.dt = '${biz_date}'
3. 高级分析场景(窗口函数等)
-- 计算用户最近3单平均金额(替代Java内存计算)
SELECT
order_id,
user_id,
amount,
AVG(amount) OVER (
PARTITION BY user_id
ORDER BY create_time
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_amount
FROM dwd_order_detail
WHERE dt BETWEEN date_sub('${biz_date}', 30) AND '${biz_date}'
4.使用UDF封装复杂逻辑:
-- 注册UDF
spark.udf.register("calculate_tax", (amount DECIMAL) -> {...});
-- SQL调用
SELECT order_id, calculate_tax(amount) FROM orders
关键转换逻辑对比:
| Java代码场景 | SparkSQL等效方案 | 优势对比 |
|---|---|---|
| 多服务RPC调用 | 多表JOIN | 减少网络开销,性能提升10x+ |
| 内存中计算聚合 | GROUP BY/窗口函数 | 分布式计算,无OOM风险 |
| 循环处理业务逻辑 | CASE WHEN表达式链 | 向量化执行,效率更高 |
| 异常处理try-catch | COALESCE/NULLIF等函数 | 声明式编程更简洁 |
3.5 过程中遇到的一些问题
1. 数据一致性问题
例如某次财报分析发现:销售部门的GMV数据与财务系统存在部分差异,追溯发现:
- 销售部门使用订单创建时间统计。
- 财务系统使用支付成功时间统计(凌晨创建的订单若在次日支付,会导致日期差异)。
解决方案
- 统一统计口径:协调各部门拉齐统计口径。
- 校验机制:每日跑批对比关键指标差异率。
2. 数据倾斜问题
大促期间,某个爆款的标品(例如SKU=888,充电器)的出库单量占总量比例比较大,属于热点数据。导致Spark任务卡在最后一个Reducer,拖慢了整体进度。
2.1 原始SQL(存在倾斜)
-- 直接Join导致sku=888的数据全部进入同一个Reducer
SELECT
a.order_id,
b.sku_name,
SUM(a.quantity) AS total_qty
FROM fact_orders a
JOIN dim_sku b ON a.sku_id = b.sku_id
GROUP BY a.order_id, b.sku_name;
解决方案
2.2 优化后SQL(解决倾斜)
步骤1:对倾斜键添加随机后缀
-- 对事实表中的倾斜sku添加随机后缀(0-99)
WITH skewed_data AS (
SELECT
order_id,
CASE
WHEN sku_id = '888' THEN
CONCAT(sku_id, '_', CAST(FLOOR(RAND() * 100) AS INT)
ELSE
sku_id
END AS skewed_sku_id,
quantity
FROM fact_orders
),
-- 维度表复制多份(与后缀范围匹配)
expanded_dim AS (
SELECT
sku_id,
sku_name,
pos
FROM dim_sku
LATERAL VIEW EXPLODE(ARRAY_RANGE(0, 100)) t AS pos
WHERE sku_id = '888'
UNION ALL
SELECT
sku_id,
sku_name,
NULL AS pos
FROM dim_sku
WHERE sku_id != '888'
)
步骤2:关联计算
-- 关联时匹配后缀
SELECT
a.order_id,
COALESCE(b.sku_name, c.sku_name) AS sku_name,
SUM(a.quantity) AS total_qty
FROM skewed_data a
LEFT JOIN expanded_dim b
ON a.skewed_sku_id = CONCAT(b.sku_id, '_', b.pos)
AND b.sku_id = '888'
LEFT JOIN dim_sku c
ON a.skewed_sku_id = c.sku_id
AND c.sku_id != '888'
GROUP BY a.order_id, COALESCE(b.sku_name, c.sku_name);
2.3 执行过程对比
| 阶段 | 优化前 | 优化后 |
|---|---|---|
| Shuffle前 | sku=888 全部进入同一分区 | sku=888 分散到100个分区(888_0 ~ 888_99) |
| Join操作 | 单节点处理所有sku=888 | 多节点并行处理子分区 |
| 结果合并 | 无需合并 | 通过COALESCE合并相同sku的结果 |
通过这种优化,我们在实际生产中成功将作业任务从 3小时 缩短到 25分钟,关键点在于:将倾斜数据的计算压力分散到多个节点,最后合并结果。
3.6 架构对比成果
| 维度 | 微服务架构问题 | 大数据架构解决方案 | 改进效果 |
|---|---|---|---|
| RPC稳定性 | 频繁超时,影响线上稳定性 | 完全消除RPC,批处理模式 | 故障率接近于0 |
| 任务可靠性 | 人工干预多,成功率92% | 自动化调度,成功率99.8% | 运维人力减少75% |
| 数据准确性 | 差异率最高10% | 统一加工逻辑,准确率99.9%+ | 质量大幅提升 |
| 处理能力 | 单机瓶颈,最大延迟6小时 | 分布式计算,任务量提升5倍 | 扩展性显著增强 |
| 重跑效率 | 需4小时+,产生大量碎片 | 30分钟内完成,insert overwrite模式 | 效率提升87.5% |
未来展望
当前的离线数仓架构很好地解决了T+1场景下的财报需求,但随着业务发展,我们对实时财报也提出了更高要求。下一步计划:
- Lambda架构:批流结合,在保持离线处理可靠性的同时增加实时处理能力。
- 技术栈升级:引入Flink实现流式计算,Kudu提供实时分析能力。
- 服务质量保障:借鉴微服务架构中的熔断降级理念,如Sentinel提供的“错误率监控+人工干预+主动告警”机制,确保实时管道的稳定性。
- 扩充财报数据接入范围: 结合数据中台的理念,囊括整个集团财务毛利、费用相关财务指标。为后续做预测分析打好基础,提升整体项目价值。
结语
转转敏捷财报架构演进过程印证了一个核心理念:没有最好的架构,只有最适合的架构。从微服务到大数据的转型不是简单的技术替换,而是根据业务发展阶段做出的理性选择。希望我们的实践经验能为面临类似挑战的团队提供参考。
最后,在这里想起苏格拉底说过的一句话: 我唯一知道的就是我一无所知。 学得越多,越能察觉过去的局限,从而以更成熟的视角轻松解决曾困扰自己的问题。
关于作者
廖儒豪。转转交易中台研发工程师
转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~`