0
0
0
0
专栏/.../

TiDB + Flink 构建实时数仓,开启保险行业降本创新之路

 TiDB社区小助手  发表于  2025-04-15
原创

近年来,保险行业在数字化转型过程中面临着数据处理成本高昂、实时性不足等诸多挑战。随着大数据技术的不断发展,实时数仓成为解决这些问题的关键。TiDB + Flink 的组合,为保险行业带来了新的机遇。本文将基于马飞老师在 TiDB 社区活动(深圳站)的分享,深入探讨 TiDB + Flink 在保险行业实时数仓构建中的应用优势、架构设计要点、性能优化策略以及未来发展方向,希望能为保险行业的小伙伴们在数字化转型过程中提供有价值的参考。

保险行业实时数据处理需求及痛点

随着数据规模持续扩大、业务场景日益复杂,在数据应用中,实时数据变得愈发不可或缺。同时伴随业务对数据依赖程度加深,对数据时效性的要求也越来越高。

在保险业务中,实时数据处理需求无处不在:1)直播场景实时转化率直接影响业务收益。2)用户推荐:当用户搜索寿险相关内容时,需实时推荐高排名产品以提升用户体验。3)业务跟进:用户添加规划师微信或下单后,需实时推送信息以便业务员及时响应。基于这些业务需求,构建实时数仓势在必行。然而,传统 Flink 架构面临 内存占用高、数据一致性难保障、开发周期长、资源消耗大等挑战。

在业务痛点与降本增效的压力下,我们选择了 “All in TiDB”

实时数仓架构的变更

谈及实时数仓架构,通常会将 Lambda 架构与 Kappa 架构进行对比。

  • Lambda 架构:微批次与离线处理、实时与离线相结合的双架构模式,架构复杂性较高,需要维护两套代码,但它也具有能保证数据最终一致性的优点。
  • Kappa 架构: Flink 与 Kafka 结合的全套数据处理方案,架构相对简单。但其问题在于会将所有数据存储到 Kafka 中,一旦数据出现问题,每次计算都需重新来过,这会给系统带来较大压力。

其次是稳定性问题。与离线数据处理相比,实时数据处理一直存在稳定性较弱的诟病。我们最初采用的是 Kappa 架构,在使用过程中,却出现内存占用较大且中间层无法复用的问题。当时还尚未使用 TiDB,而是 Clear House,由于资源有限,使用 Clear House 时,CPU 使用率飙升且居高不下。

随后,我们更换为调度+ TiDB 的架构,这实际上是 Lambda 架构的一种变体,采用 VP4 与 TiDB 离线处理相结合的方式。但该架构也存在问题:一是 VP4 批次更新无法全面覆盖 update 数据。例如,公司业务员 1A1 号原本在 A 部门,次日调至 B 部门,公司维度数据频繁变更,这种情况下,VP4 计算时,若今日该业务员在 A 部门的成交额为 100 亿,到明天可能会出现 A、B 两个部门的成交额数据。二是无法过滤非关键字段的更新。比如用户维度表中的登录时间,若与后续业务需求无关,在 VP4 批次中仍会进行计算,造成资源浪费。三是任务阻塞问题,由于 VP4 时间间隔较短,一旦出现数据洪峰,数据阻塞情况会非常严重。

最终,我们选择了 Flink + TiDB 的架构。在社区或相关文章中,提及 Flink + TiDB 时,主要会讲到 TiDB 的 KV Scan 功能以及 TiDB CDC 的数据更新功能,本次分享也会涉及这两点,并有新的补充。

在公司自主搭建的架构方面,与 Kappa 架构相比存在诸多区别。Kappa 架构把中间层数据存储于 Kafka,而该公司架构将数据写入 TiDB。在实时计算的缓存环节,常规做法是用 Redis 快速获取维度相关数据,我们则采用 TiKV 替代 Redis。至于 Flink 的状态存储,将大部分状态数据存于 TiKV,小部分存于 Flink 状态,以此优化内存占用。

此外,公司架构中虽有 Kafka,但并非用于存储中间数据,而是因 Flink 存在反压问题,若完全从 TiDB 或 TiKV 读取数据可能引发反压,引入 Kafka 是为缓解此情况。整体而言,公司架构中消息队列存储采用 TiKV 加 CDC,缓存和状态存储也都运用 TiKV,而 TiDB 本身基于 TiKV,所以是一个 “All in TiDB” 的过程。在 TiDB 消息队列部分,我制作了一个 Pack Reader,在 GetHard 和 Get 意义上可搜索到,能直观展示相关内容,通过 Sky 或 Release 方式可获取表的整条数据。另外,ICBC 类似 Kafka 界面,可用于获取 TiDB CDC 的数据。

利用 TiDB 解决实时计算行业痛点

内存占用问题

Flink 双流 Join 底层主要通过窗口和状态两种方式处理数据,最终数据都会存入内存,导致内存占用较大。将日常常用的两张表进行 Join 对比,各取 50 万条数据。若基于状态处理,内存占比约为 859 MB,而使用 TiDB 优化后,仅为 20.4 MB。以 Same Point 为基准衡量内存占比,由于 Flink 将数据全部汇集到内存,所以只能通过 Same Point 的占比变化来评估优化效果,最终优化结果显示,内存占比缩小近 40 倍。

状态 & 中间层的复用

Flink Join 将数据存储在状态中,但 Flink 的状态如同黑匣子,内部操作不可见、数据不可查看和更改,且任务之间状态无法共享。我们团队的处理方式是:在存储状态信息时,仅存储 TiDB 中 TiKV 的 Row ID(一种 Long 数据类型),当后续需要获取状态数据时,利用该 Row ID 对 TiKV 进行查询,从而获取完整数据。通过这种方式,不仅优化了内存,还能确保获取完整数据。

在数据开发架构方面中,以往数据是存储于 Kafka 之中。在进行重算时往往需要从头开始计算全量数据。以一些商业化产品为例,Kafka 的数据保存时间可能设置为半年、一年甚至永久。为解决这一问题,我们团队的优化策略是将所有数据存储在 TiDB 中。当需要从头计算时,可以借助 TiKV Scan 功能获取全量数据,从而避免了耗时费力的从头计算过程。同时,若只需部分数据,还能通过编写 SQL 语句过滤不需要的数据,以此实现整体性能的优化。

Join 数据时间间隔问题

在入职面试时曾遇这样一个问题:假设有两张表进行 Join 操作,其中一张表的数据源自两三年前,如今该表数据已更新,此时应如何处理 Join 过程?在行业实践中,一般采用旁路缓存或多级缓存的方案来应对此类问题,但在实际应用过程中,这些方法仍暴露出诸多问题。

以两张表 Join 为例,假定 2022 年 B 表中的某条数据发生了变更,在与 A 表进行 Join 操作时,显然不可能将 2022 年 B 表的全部数据都加载至内存之中。通过编写 SQL 语句,能够呈现出两张表 Join 时时间间隔的分布情况,从所得结果可以看出,该分布的离散性较大。若采用传统的二八法则或三西格玛数据分析方法来确定内存中数据的存储时长,会发现所确定的时间间隔同样偏大。

在传统的 Flink Join 操作中,Interview Join 需要预先指定时间间隔,一旦时间超出该间隔,相关数据便会被删除;Look up Join 虽然能够获取全量数据,然而其按照固定间隔拉取数据的方式,若在间隔时间段内数据发生变更,极易导致数据不准确;基于状态和窗口的 Join 操作通常需要设定状态保留时间。总体而言,普通的 Flink Join 难以在资源利用、处理速度以及数据准确性这三个方面实现兼顾。

我们借助 TiDB 对该问题进行了优化。在资源损耗方面,内存仅保留 Row ID,而不保留其他冗余数据;在访问速度方面,TiKV 基于 RocksDB 构建,具有极快的访问速度,经线下测试,使用 TiKV 请求数据时,对内存和 CPU 的影响几乎可以忽略不计;在数据完整性方面,每次从表中获取数据时,均可确保数据完整性达到百分之百 。

Supply Join 方式

核心思路

Supply Join 的核心思路在于:在执行 Join 操作时,把整个 Join 流程拆分为两个或多个部分。举例来说,若 C 表是 A 表与 B 表 Join 后的结果,那么可将其拆分为 A 表部分和 B 表部分。具体的操作步骤为,首先把 A 表的数据直接写入 C 表,接着提取 A 表的主键,将其与 B 表进行关联,随后再把关联结果写入目标 C 表,如此便能得到 A 表 Join B 表的最终结果。从理论层面分析,在这一操作过程中,内存仅需保存 A 表的主键 ID,对内存的优化效果极为显著。如果大家对这一过程理解存在困难,这里的 SQL 语句,可辅助理解其中的逻辑关联。

主键传递

当面临多表 Join 的情况,比如 A 表 Join B 表 Join C 表,并且在 B 表 Join C 表时以 B 表作为主表。以最终生成 C 表的结果流程来看,先获取 A 表 Join B 表的结果,然后将其与 C 表进行 Join 操作。在此过程中,B 表在 Join C 表时,会把从 A 表获取到的主键传递给 C 表,最后将相关数据写入目标表,以此完成 C 表 Join 部分的补充。整体而言,这个过程就如同打篮球时传球,A 表的主键类似于球,从 A 表传递至 B 表,再传递到 C 表,所以团队将其命名为主键传递过程。在多表 Join(以三表 Join 为例)的场景下,理论上仅需保存主表的 ID,从而有效优化了内存的使用。

刚刚阐述了 Supply Join,对于 Supply Join 而言,必然会涉及到聚合函数的运用。我们基于 TiDB CDC(其带有特定的数据类型),利用 Flink 以数据驱动的计算模式(即每输入一条数据便进行一次计算),编写了相应的计算方法,用于计算诸如 Sum 等聚合函数。以当前项目为例,当数据输入时,在状态中仅保留 Sum 值,若遇到 Insert 操作,便对 Sum 值进行累加,整个过程中内存仅需保留 Sum 这一条数据。

以上便是对 Supply Join 的总结。实际上,整个操作流程可以视为将 SQL 语句转化为 Flink 任务的过程。我们在实际操作中,会运用 Call Set 来解析 SQL 语句,最终实现从 SQL 到 Flink 任务的转换。这种方式具备多个优势:其一,属于低代码开发模式,由于完全基于 SQL 进行开发,极大地降低了开发成本;其二,能够实现实时计算和离线计算使用同一套代码,若后续涉及系统迁移,可直接进行操作,对整体业务的影响较小;其三,有利于数据治理以及数据血缘关系的梳理。目前,Flink 实时计算大多基于代码和 Stream API 进行开发,SQL 的应用占比较少,若能够直接通过编写 SQL 来实现转换,便可以规划出完整的数据血缘关系。

未来展望

我们当前在状态存储中仅保留 row_id,团队未来设想进一步优化该机制。即不再存储 row_id,而是在数据到来时,直接通过索引获取 row_id,从而使状态中完全不保存数据。不过,在探索实践过程中,团队尚未成功实现这一设想,将此思路分享给大家鼓励感兴趣者可尝试探索。

总体来看,我们团队期望达成将 TiKV 完全托管至 Flink 的目标,使 Flink 承担全部的计算任务,TiKV 负责全部的存储任务。TiDB 作为国内较早实现 HTAP 功能的数据库,其天然具备的 HTAP 功能,与 Flink 和 TiDB CDC 相结合,在实时计算领域与应用需求高度契合。我们坚信,TiDB 在未来的实时计算领域将占据重要地位,同时也期待 TiDB 能够推出更多新颖且实用的功能!

分享 PPT 下载

0
0
0
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论