亿级数据算不准?某财务中台的架构“换血“实录

  • 引言

  • 第一阶段:微服务架构的困境分析

    • 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:源数据来源方式不相同,有用接口的,有用云窗的,有人工后台录入的;

由于早期数据量并不大,基于以上业务特点,采用了如下的存储方式。

image

引入 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 下一步如何抉择

我们评估了三种可能的演进方向:

方案

优点

缺点

适用场景

增强现有微服务架构

改动小,延续现有技术栈

无法根本解决分析瓶颈

小规模数据

引入大数据技术栈

专业分析能力

学习成本高

中大规模数据分析

采用商业解决方案

开箱即用

成本高,灵活性差

快速上线需求

最终决策因素

  1. 业务数据量已超过单机处理能力。

  2. 每月需要离线处理千万级、亿级别数据。

  3. 财务分析需求日益复杂(需要支持多维分析)。

  4. 团队有3个月窗口期进行技术转型。

经过对比,尝试引入大数据技术栈来解决目前的痛点。

2.4 数据处理的本质差异

  1. 微服务实时调用 vs 大数据批处理

    • 微服务RPC 需实时调用多个服务获取维度及度量,网络延迟、服务故障会导致调用链断裂(如超时率高达5%),且分布式事务难以保证一致性。

    • 大数据技术(如Spark/Hadoop)采用批处理模式,通过ETL流程将数据集中处理,所有维度关联在计算前已完成,避免运行时依赖外部服务。

  2. 计算向数据移动的思想
    大数据框架(如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服务调用的计算逻辑,可以通过以下方式实现:

  1. 基础订单金额计算(单表)

-- 直接基于订单事实表计算
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场景下的财报需求,但随着业务发展,我们对实时财报也提出了更高要求。下一步计划:

  1. Lambda架构:批流结合,在保持离线处理可靠性的同时增加实时处理能力。

  2. 技术栈升级:引入Flink实现流式计算,Kudu提供实时分析能力。

  3. 服务质量保障:借鉴微服务架构中的熔断降级理念,如Sentinel提供的“错误率监控+人工干预+主动告警”机制,确保实时管道的稳定性。

  4. 扩充财报数据接入范围: 结合数据中台的理念,囊括整个集团财务毛利、费用相关财务指标。为后续做预测分析打好基础,提升整体项目价值。

结语

敏捷财报架构演进过程印证了一个核心理念:没有最好的架构,只有最适合的架构。从微服务到大数据的转型不是简单的技术替换,而是根据业务发展阶段做出的理性选择。希望我们的实践经验能为面临类似挑战的团队提供参考。

最后,在这里想起苏格拉底说过的一句话:我唯一知道的就是我一无所知。 学得越多,越能察觉过去的局限,从而以更成熟的视角轻松解决曾困扰自己的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值