1、背景
在数仓分层架构体系中,从 ODS层到 DWD层数据转换需要进行数据清洗、脱敏、列式压缩等步骤。在B站用户行为埋点数据 ODS到 DWD层转换过程中,为了解决日增千亿条、20+TB/天增量规模下数据重复摄取带来的资源严重消耗的问题,引入了北极星(B站用户埋点行为分析链路)分流,按照部门进行分表。在埋点设计中使用spmid模型,将事件类型拆分为浏览 pv、曝光 show、点击 click等多个事件类型,并以这些事件类型作为除天、小时分区以外的第三级分区,再以事件类型产品来源作为四级分区。通过基于部门业务区分按照埋点事件类型+产品来源以多表多分区控制的形式,最大程度降低下游任务文件数据摄取数量以减少资源消耗。
如图所示,用户埋点由边缘上报 bfe-agent到网关 gateway,经过kafka数据缓冲后通过 lancer collector数据分发至 ods hive,再通过北极星分流完成 ODS到 DWD层数据转换。DWD层数据服务于搜索推荐、推荐、广告、AI等应用场景。在原有 ODS到 DWD数据转换中使用Spark离线分流方案。
2、Spark离线分流方案
Spark小时任务定期调起 ETL任务完成 DWD分流、数据同步,由于在读 ods时由于源表数据量过大造成 Spark缓存 miss,继续分流需要重新读全表数据存在读放大问题造成文件重复摄取。
离线分流读放大问题
随着各部门中心业务的扩展,分流表日益增加,而在使用离线Spark sql分流过程中由于多表写入会重复读取源表数据,而源表的数据规模过大造成缓存失效,从而重复摄取源表数据的读放大问题日渐显现。
分流任务资源消耗高
ODS-DWD同步资源消耗因重复摄取 ODS源表文件跟随分流表扩展持续增加,部分资源使用不合理。
DWD同步时效性低
在分区通知调度模式下,DWD层数据只会在ODS表分区通知才会进行同步,为了保证 DWD表的及时产出需大量资源满足同步需要。在高峰期资源使用出现堆积时 ods-dwd同步容易超过1h+。
为了解决这些问题,我们引入新的解决方案--基于Flink的实时增量计算。
3、Flink实时增量计算
如下图所示,实时北极星增量计算方案由 Flink HDFS File Source通过扫描 Lancer任务每次 checkpoint产出的可见文件进行增量消费计算,与维表数据join之后打宽,分发至 Flink Multi Hive Sink,在这里完成多表多分区分流,sink内部集成 Archer(B站大数据任务调度系统)调度下游搜索、推荐、广告等数据分析业务。由于 Main表文件数量在实时分区写入场景下文件数依然过高,因此在sink表之后对Main表单独添加基于Spark的小文件合并。
增量计算方案预期收益主要包含:
- 读放大问题解决
Flink DAG支持 Source数据下发之后可自定义分区输出无需重复摄取,用以解决读放大问题。
- 分流资源降低
在解决读放大问题后,源表数据摄取只会执行一次,降低资源消耗。另外在 ODS产生一批可见文件即进行计算,最大程度降低分流任务同步资源消耗。
- 时效性提升
时效性由小时级最高可提高至分钟级,增量计算即在 ODS产生一批文件之后就会对文件进行消费,理论最高可在 ODS分区归档之后的一次 Checkpoint间隔即可完成 DWD表数据完全同步。
4、多级分区小文件解决方案
实时分流在解决以上几个问题同时,在灰度上线过程中发现文件数量相比离线分流方案增长超100倍,下游Spark分析任务在读取实时分流表加载文件时由于文件读放大问题导致内存不足执行失败。于是解决小文件问题将成为该方案最终落地是否成功的关键。由于实时分流在每5min一次 Checkpoint执行文件斩断会产生大量小文件,导致ns读写压力变大,下游Spark在读取目录过程中也增加资源消耗导致任务执行超时。在分析了落地文件后发现很多小文件是由于四级分区并发度分配不合理导致 bucket的数量增加从而产生大量的小文件。因此通过在保证计算能力下尽力减少 bucket数量则可以降低打开的文件数量。
4.1 基于 Flink Partitioner Shuffle优化
在270+四级分区下,按照全并发分配模式,每天将产生约1亿4千万文件数。通过使用 Flink Partitioner,对于 Reader下发的数据按照所属四级分区进行加签(tag),根据每个 tag对应历史分区落地数据大小比例配比计算subtask分配区间,在分配区间内随机分发至某个 subtask,文件数量由原来一亿四千万/天降为150w/天。文件数缩减100+倍。
优化前
270 (四级分区) * 1800 (并发度) * 12 (每小时文件斩断次数) * 24 (每天小时数) = 139968000 (约14000w)。
优化后
5000 (Shuffle数量) * 12(每小时文件斩断次数) * 24 (每天小时数) = 1440000(150w) 。
如上图所示,可能存在大量 partition仅需一个 bucket分桶即可完成文件落地,不需要所有Bucket处理。因此按照 partition所需 bucket数量进行合理分配是解决问题的关键。
但是这里有个弊端,在出现流量激增场景下,该方案可能会导致部分subtask热点从而导致任务出现严重堆积(如佩洛西事件,导致部分subtask流量超过平时12+倍),需要手动调整 shuffle方案以消除热点。这样导致运维成本较高,并且用户在使用该方案时门槛较高,需要长时间的压测调试才能将多分区之间的比例调整均匀。如果能够将实时作业处理能力与文件数量之间根据流量自动平衡,这样运维成本可以降低另外用户在使用时无门槛,只需配置开关即可。因此提出 Auto Shuffle推测执行以解决小文件合并问题。
4.2 Auto Shuffle推测执行小文件解决方案
1、支持自定义分桶 Tag规则
根据 row的字段来确认分桶的规则,支持根据udf自定义。
2、计算 row的大小
直接按照 row字节数大小计算,即为row的压缩前大小。
3、滚动窗口+类背包算法+统一字典排序
滚动窗口
以环形数组的形式记录配额,配额在分配后,各个 subtask对桶内的更新相互间未知的,很容易造成单桶超过8g,现在想到的解决办法是通过8G/一个小时内滚动时间窗口的次数/并发度来调整。
统一字典排序
主要目标是为了合并背包算法结果,尽可能将不同 subtask相同tag分发到相同的桶里(由于tag分发排序不稳定)。上线选择使用 tag hashcode排序,减少计算量。
加签背包算法
类似Flink1.12小文件合并采用的BinPack策略,在此基础上添加Tag识别,每个分桶归属于单个Tag。注意在使用以上基于weight加签背包的计算结果 shuffle时,容易受到作业反压的影响从而导致上图 shuffle operator接收到的数据变少,由于JM无法区分流量降低和反压影响,因此会根据 weight主动降低 subtask配额,这样会导致shuffle算子后续算子处理能力下降,继而增加反压陷入恶性循环,在测试过程中效果表现不佳。后续在参考根据各四级分区落地文件大小预设比例的思想,取消主动降低 subtask配额的操作,按照上游分发的大小按比例分配subtask,效果表现良好,但文件数量会略高于预设比例(比例调整会导致文件数量增加)。
4、维护比例模型状态
在堆积恢复时按照重启前最后一次生成的比例模型来计算 subtask分发,减少因启动造成文件数膨胀问题(486000单次checkpoint增量)。
5、冷启动问题解决
由于冷启动时,没有流量参考,为了降低文件数只能通过计算tag占用方式分发subtask,这样的累加操作为O(n),在初始化时cpu压力较大,吞吐不达预期。因此支持UDF预设置tag规则以及比例,按照该比例进行预分发,在第一次窗口计算前按照预设比例进行O(1)分发。
5、Flink增量计算方案落地
在落地过程中,我们面临很多问题和挑战,尤其是在降本增效的大背景下,对于新方案落地提出了高要求。首先面临的是在资源紧缺情况下如何适应相对物理机集群而言环境较为恶劣的混部集群。在混部环境下需要实时任务做到以下几点:
- (事前)分流任务稳定性提升
- (事中)分流任务需快速恢复,即 Fast-FailOver
- (事后)分流任务频繁重启下不影响数据质量
基于这样的要求,在实时分流任务中在 Flink Runtime\SQL\Connector以及实时平台层应用很多功能优化以满足要求。
5.1 分流任务稳定性提升
首先影响任务稳定的主要有以下几点:
- JobManager稳定性问题
- Subtask间负载均衡
- Subtask热点倾斜
解决方案:
5.1.1 JobManager稳定性问题解决
- Metrics Disabled
我们在查找 JobManager挂的RC过程中,发现经常由于 JobManager OOM导致任务重启,尤其在打开原生监控时经常出现。在 Dump内存进行分析后发现,Jobmanager内存80%以上存储的是各个 TM上报的 Metrics,由于打开原生监控会主动 pull额外的Metrics从而加重内存压力导致 OOM。因此实现 Metrics Disabled关闭部分Metrics对JM上报,问题解决。
- JobManager HA
在混部环境下 JobManger常会因所在 Container被驱逐而导致Jobmanager挂掉。因此通过开启JM HA在JobManger挂掉的过程中,保持TM运行状态,并重连JobMaster,取消社区JM心跳超时就Cancel Task的行为以保证任务持续稳定运行。
5.1.2 Subtask间负载均衡
- 基于backlog负载均衡
非hash shuffle场景下,Flink默认提供了rebalance或rescale partitioner用于在下游算子的不同并行度间均匀地分发数据(round-robin方式)。在环境问题(例如机器异构等)导致下游算子的不同并行度之间处理能力不均衡时,会导致部分subtask数据堆积,造成反压。为此,我们引入了下游subtask之间的负载均衡机制,并默认提供了基于backlog进行负载均衡的实现。通过运用该负载均衡机制,可以使得数据根据下游subtask的处理能力进行分发,减少环境问题导致的反压等问题。
5.1.3 Subtask倾斜问题解决
- Reader File Split负载均衡
File Split在 Round Robin分发时,由于split大小不同以及机器异构等原因,造成部分subtask处理split速度变慢导致热点堆积。通过JobManager维护Reader算子运行状态,在Monitor异步线程分发时根据各reader算子是否空闲来分配split,以类似生产者-消费者模式实现Reader算子对于File split处理负载均衡。
5.2 分流任务快速恢复
由于实时分流任务以较小资源流式增量消费,在北极星较大流量场景下任务在重启的几分钟内会造成严重堆积,另外在重启过程中可能出现资源抢占造成实时任务无法及时恢复,因此需要实时分流任务具备快速恢复的能力。主要从以下几点出发,增加恢复速度
- Checkpoint快速恢复
- 维表Join支持FailOver
- Yarn调度资源抢占解决
5.2.1 Checkpoint快速恢复
- Regional Checkpoint
北极星分流场景下Flink作业的并行度非常大,非常容易因为环境波动等原因导致部分subtask的checkpoint失败。默认配置下,这会导致作业的checkpoint失败,从而导致在作业恢复时需要重放大量的数据,造成不必要的资源浪费。通过引入regional checkpoint,可以做到在部分subtask的checkpoint失败时,作业的checkpoint仍然可以成功。配合Flink社区提供的region failover的功能,可以极大地提高作业在部分subtask失败时从checkpoint恢复的速度。
配置参数:execution.checkpointing.regional.enabled=true,execution.checkpointing.regional.max-tolerable-consecutive-failures-or-expiratinotallow=3,execution.checkpointing.regional.max-tolerable-failure-or-expiration-ratio=1,execution.checkpointing.tolerable-failed-checkpoints=3
5.2.2 维表Join支持FailOver
- ShutDown Hook Failover
在北极星分流场景下,使用 HDFS维表 Left Join。HDFS维表加载过程是定期将 HDFS文件反序列化并以 KV形式放入内存和 RocksDB中,缓存级别为 TM级。一旦出现 slot通信失败将 shutdown整个 TM,缓存需重新加载。通过 JDK1.0提供的 ShutDown Hook在 slot失败时单独清理 Slot对象,保留 TM级别缓存,支持 Region FailOver在 slot单独恢复时提高恢复速度。
5.2.3 Yarn调度资源抢占
- Session提交
在集群资源紧张的情况下,任务重启时会发生由于资源被Pending任务抢占而无法启动的问题。这会导致高优任务的资源需求无法满足,时常需要人工介入处理。通过Session提交方式,在任务漂移时保留占用的资源不释放,保证任务 FailOver成功。
5.3 分流任务数据质量保证
在任务频繁重启过程中,容易触发各功能点的Corner Case导致数据质量异常。在考虑功能的健壮性基础上,结合Flink两阶段提交能力保证数据处理Exactly Once。ODS数据在分流任务处理过程中主要经过Flink File Source以及Multi Hive Sink,在Flink connectors实现过程中结合Checkpoint实现数据处理 Exactly Once。另外在维表Join处理上,也可能发生维表Join异常导致DQC异常。
5.3.1 File Source两阶段提交
- 文件处理Exactly Once
通过扫描ODS表目录并根据目录下索引文件得到可见文件,基于分区写入文件修改时间单调递增的特性,Checkpoint记录已转换Splits的文件最大Modify Time。任务重启后扫描的文件过滤出小于记录的modify time即可保证文件处理精确一次。
- Split分发Exactly Once
文件在转换Split之后,将会由Monitor统一下发至Reader算子,在分发过程中,Monitor负责记录未发送的split,Reader算子记录已接收的split,保证split分发不丢不重。
- Split转换RowData Exactly Once
Split在转换为RowData过程中,原生的 HiveTableFileInput不支持Checkpoint,没有记录split,任务在重启时会导致split重复读取导致数据重复。通过改造在checkpoint时记录当前每个Split处理的SplitNumber,在重启恢复Reopen Split时从上次记录的Split Number处开始消费,保证Split转换RowData时精确一次。
5.3.2 维表加载数据准确性
- 维表加载降级
由于维表加载需要访问外部系统,容易产生异常导致维表加载失败。由于业务存在根据维表加载的数据进行where过滤,一旦维表数据异常则会发生数据丢失。因此在维表加载数据异常时主动降级至上一个分区,虽然可能会导致部分新的数据join miss,但在最大程度上降低数据丢失风险。
- 文件锁保证原子性
内部在使用维表join时,选择了直接通过加载hdfs目录的方式加载数据。在没有使用分区通知机制的情况下,加载是否完成只能通过Spark是否写完作为最终标志,由于是天级别目录小时级更新场景,因此对于检查SUCCESS文件的方法并不具备原子性。通过加文件锁的方式,即判断加载数据前后的文件时间是否发生变更保证HDFS维表加载原子性。
5.3.3 Multi Hive Sink数据质量保证
- 文件两阶段处理
这里使用社区版本,即在写出文件时为隐藏文件,执行 Checkpoint时Close文件,在下一次checkpoint成功之后notify执行rename操作保证数据一致性。
- 多表多级分区提前调度问题
在内部分流场景下,为了减小下游数据摄取数量,由二级分区分流成为四级分区,四级分区在社区版本分区提交过程中,由于调度是小时级别,则需要判断该分区下所有四级分区全部ready之后才能通知下游调度,仅通过watermark无法满足该要求。我们通过在状态中记录Flink Bucket的Open和Close状态,来判断当前小时分区下所有的四级分区是否完全结束。
- 集成archer新增Archer commit policy
传统实时调度离线的方法通过打时间差方式进行,需要平台侧通过定时调度拉起下游,为了保证不被提前调起,还要加分区是否创建兜底保障,调度任务拉起与上游分区通知存在gap。通过archer commit主动通知方式可以解决这一gap带来的调度不准确的问题,因此通过集成archer在hive commit算子内增加archer commit policy,对分流表下游调度基于主动通知的模式拉起,保障数据质量和调度准确性。
6、实时增量计算落地效果
实时增量计算在北极星分流场景落地后,相比原有离线分流方案在各方面有显著提升。
资源使用降低
在资源使用上整体资消耗降低约20%,峰值资源消耗降低约46%。
数据时效性提升
小时级分区归档时间平均提升20%,在 ODS-DWD ETL平均2TB每小时数据场景下,小时级同步99线保持30min内,50线在17min内。
分区可扩展性增强
支持在同步资源不变条件下继续拆分多表多级分区。
7、未来展望
在实时数仓流批一体的大背景下,实践通过 Flink+Hudi方式打造北极星分流流批一体,整合实时离线链路降低资源开销,并且通过 Hudi clustering能力进一步降低读取数据量,达到查询加速的效果。
8、参考资料
[1]https://mp.weixin.qq.com/s/PQYylmHBjnnH9pX7-nxvQA
[2]https://mp.weixin.qq.com/s/E23JO7YvzJrocbOIGO5X-Q
[3]https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/dev/table/sourcessinks/
[4]https://mp.weixin.qq.com/s/O0AXF74j6UvjtPQp5JQrTw
[5]https://mp.weixin.qq.com/s/NawxeiP-_DFpyoekRrzlLQ
本期作者
朱正军
哔哩哔哩资深开发工程师