转转技术

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

  • 引言
  • 第一阶段:微服务架构的困境分析
    • 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流程将数据集中处理,所有维度关联在计算前已完成,避免运行时依赖外部服务。
  • 计算向数据移动的思想大数据框架(如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_amountFROM dwd_order_detailWHERE dt = '${biz_date}'

    2. 多维度关联计算(替代Java服务调用)

    -- 替代原Java的多服务调用逻辑SELECT  o.order_id,  o.original_amount,-- 用户维度关联计算CASEWHEN u.vip_level = 'PLATINUM' THEN o.original_amount * 0.9ELSE 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_amountFROM dwd_order_detail oLEFT 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_amountFROM dwd_order_detailWHERE dt BETWEEN date_sub('${biz_date}', 30) AND '${biz_date}'

    4.使用UDF封装复杂逻辑

    -- 注册UDFspark.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的数据全部进入同一个ReducerSELECT    a.order_id,    b.sku_name,SUM(a.quantity) AS total_qtyFROM fact_orders aJOIN dim_sku b ON a.sku_id = b.sku_idGROUPBY a.order_id, b.sku_name;

    解决方案2.2 优化后SQL(解决倾斜)

    步骤1:对倾斜键添加随机后缀

    -- 对事实表中的倾斜sku添加随机后缀(0-99)WITH skewed_data AS (SELECT        order_id,CASEWHEN sku_id = '888' THENCONCAT(sku_id, '_', CAST(FLOOR(RAND() * 100) AS INT)ELSE                sku_id END AS skewed_sku_id,        quantityFROM fact_orders),-- 维度表复制多份(与后缀范围匹配)expanded_dim AS (SELECT        sku_id,        sku_name,        posFROM dim_skuLATERAL VIEW EXPLODE(ARRAY_RANGE(0, 100)) t AS posWHERE sku_id = '888'UNION ALLSELECT        sku_id,        sku_name,NULL AS posFROM dim_skuWHERE sku_id != '888')

    步骤2:关联计算

    -- 关联时匹配后缀SELECT    a.order_id,COALESCE(b.sku_name, c.sku_name) AS sku_name,SUM(a.quantity) AS total_qtyFROM skewed_data aLEFT 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_idAND 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. 扩充财报数据接入范围: 结合数据中台的理念,囊括整个集团财务毛利、费用相关财务指标。为后续做预测分析打好基础,提升整体项目价值。

    结语

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

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


    关于作者

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

    想了解更多转转公司的业务实践,欢迎点击关注下方公众号: