-
引言
-
第一阶段:微服务架构的困境分析
-
1.1 初始架构设计
-
1.2 存储设计
-
1.3 调度模型设计
-
1.4 处理模型设计
-
-
第二阶段:架构演进的考量
-
2.1 核心问题分析
-
2.2 解决思路
-
2.3 下一步如何抉择
-
2.4 数据处理的本质差异
-
-
第三阶段:新架构设计
-
3.1 数据模型设计
-
3.2 大数据技术选型
-
3.3 数仓体系架构图
-
3.4 数据处理示例
-
3.5 过程中遇到的一些问题
-
3.6 架构对比成果
-
-
未来展望
-
结语
引言
在业务快速发展的过程中,业财系统作为连接业务与财务的核心枢纽,其重要性日益凸显。早期我们基于微服务架构构建了“金字塔”系统,通过统一收集上下游业务数据并加工财务指标,支撑了公司初期的财务分析需求。然而,随着业务复杂度提升和数据量激增,这套架构逐渐暴露出诸多问题:指标差异难以溯源、数据处理效率低下、系统稳定性不足等。本文将详细分享我们如何通过架构演进,最终构建出高效可靠的敏捷财报系统。
第一阶段:微服务架构的困境分析
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 解决思路
1. 尽量规避RPC调用:
-
如果还采用微服务架构,则需要成功将维度数据预加载到本地缓存,但维度之多,以及非维度的数据也可能需要RPC才能获取到,所以不能完全解决。
-
不用RPC来关联各微服务的数据,而采用数据中心的思想,将多个微服务的数据库同步到数据中心进行统一处理。
2. 解耦分析场景:将分析型负载从交易系统中剥离。 例如将多个微服务数据关联后分类汇总的功能,从微服务中拆分出来,微服务只做OLTP相关功能。
3. 采用统一的调度生态:如可以采用云窗统一的信号调度。不过整体架构得跟着改造,不能再用微服务处理。
4. 多机器并行处理:可以采用分布式计算框架Spark进行并行处理,优化单机处理瓶颈。
2.3 下一步如何抉择
我们评估了三种可能的演进方向:
方案 |
优点 |
缺点 |
适用场景 |
---|---|---|---|
增强现有微服务架构 |
改动小,延续现有技术栈 |
无法根本解决分析瓶颈 |
小规模数据 |
引入大数据技术栈 |
专业分析能力 |
学习成本高 |
中大规模数据分析 |
采用商业解决方案 |
开箱即用 |
成本高,灵活性差 |
快速上线需求 |
最终决策因素:
-
业务数据量已超过单机处理能力。
-
每月需要离线处理千万级、亿级别数据。
-
财务分析需求日益复杂(需要支持多维分析)。
-
团队有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操作 |
单节点处理所有 |
多节点并行处理子分区 |
结果合并 |
无需合并 |
通过 |
通过这种优化,我们在实际生产中成功将作业任务从 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提供的“错误率监控+人工干预+主动告警”机制,确保实时管道的稳定性。
-
扩充财报数据接入范围: 结合数据中台的理念,囊括整个集团财务毛利、费用相关财务指标。为后续做预测分析打好基础,提升整体项目价值。
结语
敏捷财报架构演进过程印证了一个核心理念:没有最好的架构,只有最适合的架构。从微服务到大数据的转型不是简单的技术替换,而是根据业务发展阶段做出的理性选择。希望我们的实践经验能为面临类似挑战的团队提供参考。
最后,在这里想起苏格拉底
说过的一句话:我唯一知道的就是我一无所知。 学得越多,越能察觉过去的局限,从而以更成熟的视角轻松解决曾困扰自己的问题。