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

652 阅读16分钟

引言

在转转业务快速发展的过程中,业财系统作为连接业务与财务的核心枢纽,其重要性日益凸显。早期我们基于微服务架构构建了“金字塔”系统,通过统一收集上下游业务数据并加工财务指标,支撑了公司初期的财务分析需求。然而,随着业务复杂度提升和数据量激增,这套架构逐渐暴露出诸多问题:指标差异难以溯源、数据处理效率低下、系统稳定性不足等。本文将详细分享我们如何通过架构演进,最终构建出高效可靠的敏捷财报系统。

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

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来关联各微服务的数据,而采用数据中心的思想,将多个微服务的数据库同步到数据中心进行统一处理。
  1. 解耦分析场景:将分析型负载从交易系统中剥离。 例如将多个微服务数据关联后分类汇总的功能,从微服务中拆分出来,微服务只做OLTP相关功能。
  2. 采用统一的调度生态:如可以采用云窗统一的信号调度。不过整体架构得跟着改造,不能再用微服务处理。
  3. 多机器并行处理:可以采用分布式计算框架Spark进行并行处理,优化单机处理瓶颈。

2.3 下一步如何抉择

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

方案优点缺点适用场景
增强现有微服务架构改动小,延续现有技术栈无法根本解决分析瓶颈小规模数据
引入大数据技术栈专业分析能力学习成本高中大规模数据分析
采用商业解决方案开箱即用成本高,灵活性差快速上线需求

最终决策因素

  1. 业务数据量已超过单机处理能力(日均订单>30万)。
  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引擎对比

维度StarRocksDorisClickHouse
单表查询性能快(向量化引擎 + 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, 0AS 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}'30AND '${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-catchCOALESCE/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() * 100AS 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(0100)) 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. 扩充财报数据接入范围: 结合数据中台的理念,囊括整个集团财务毛利、费用相关财务指标。为后续做预测分析打好基础,提升整体项目价值。

结语

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

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


关于作者

廖儒豪。转转交易中台研发工程师

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~`