01 - 前言
软件架构的复杂性通常并不是由功能性需求来决定,而是取决于非功能性需求,例如高性能、高可用、易扩展、易运维、低成本等要求,功能性需求通常是易于实现的,但是为了满足非功能性需求需要去做不同的技术方案选型对比、架构设计等,比喻说为了实现高性能,要去做缓存、分库分表、预计算、异步等方案,这些方案会提高系统的复杂程度。对于Flink程序开发同样会面临这些问题,在设计、实现之初除了需要考虑如何满足功能性需求外,还需要考虑性能、容错等非功能需求。本文将结合自己的实际开发经验从以下几个方面来介绍做一个实时Flink程序设计需要关注的一些问题:
适合性
当前的业务需求是否适合使用Flink去实现。
准确性
由Flink程序提供出去的数据是否正确。
延时
在流量高峰期或者是非高峰期,Flink任务是不是丝滑般处理,保证不延时。
容错能力
是否有一套完备的故障检测、恢复的能力,保证其可用性。
可维护性
在面对版本升级、程序升级时,是否可以方便的维护升级。
02 - 合适性
在做程序设计的时候经常会有一个误区,为了技术而技术,也就是一味的去追求技术的火热,某个技术很牛逼因而在项目中去使用它,比喻分析的数据量只有几万、几千级别,使用MySql 就可以轻松的处理,但是却为了追求Hadoop而去使用Hadoop做数据处理,很明显得不偿失。对于是否适合使用Flink去实现业务需求,从两个方面去考虑:业务、技术。
业务是否需要?对于数据业务开发来说,是需要通过数据技术手段解决业务问题或者是辅助业务决策,因此对于开发人员的要求就是需要熟悉业务,需要我们去辅助判断这个实时需求对于业务的增量情况,通常影响用户实时决策,例如实时效果指标、监控指标等,这个时候我们可以去考虑使用实时技术去解决。
技术是否合适?Flink在当下流计算很火热,并不是任何计算场景都使用Flink来完成,需要充分考虑其实现的成本,有没有更好的方案进行代替。
03 - 准确性
这里的准确性代表数据质量中数据准确性,也就是Flink程序提供出去的数据是否正确,保证我们交付出去的数据得到业务的认可。在不同的业务领域内对数据的正确性要求不一样,例如做实时监控,是允许存在数据丢失情况,但是在一些金融领域或者是跟钱相关统计计算的对数据正确性要求极高,应对不同的正确性要求会做出不同的设计,接下来列举几个常见会影响数据正确性的设计。
checkpoint, Flink checkpoint 是对状态数据周期性的全局快照,保证任务失败从快照恢复仍然能够保证数据的正确性。第一个考虑就是是否需要使用checkpoint, 如果可以忍受数据丢失,那么就没有必要开启checkpoint机制;第二个考虑是使用exactly-once语义还是at-least-once语义,这二者语义都是Flink内部数据的一种保证机制 , 使用at-least-once语义在任务重启的情况下,会存在重复状态计算,导致最终的数据结果有偏差。
事件时间语义,事件时间语义的支持处理是Flink区别于其他流式计算的重要特性,可以根据数据的时间执行相应的处理,比较常见的就是事件时间窗口,同时事件时间语义可以支持数据回放。如何决定可以触发一个事件时间操作(窗口计算), 在Flink中使用Watermark来衡量数据的处理进度,决定是否触发计算,但是这是一种理论情况,在实际中只能说是相对的减少数据丢失(可以监控numLateRecordsDropped指标),使用EventTime 语义并不能百分之百保证数据没有丢失,也就是计算指标可能会存在一定的误差。那么如何规避这种误差?常见的方式就是使用离线的语义去完成实时处理方式,以窗口计算为例:
代码语言:javascript复制SELECT user,
TUMBLE_START(rowtime, INTERVAL '1' DAY) as wStart,
SUM(amount) FROM Orders
GROUP BY TUMBLE(rowtime, INTERVAL '1' DAY), user
可改写为:
代码语言:javascript复制SELECT user,userFunction(rowtime) as wStart,SUM(amount)
FROM Orders
GROUP BY userFunction(rowtime), user
端到端一致性,这个是流式计算中比较难的一个话题,其涉及数据源端、Flink计算端、输出端,这三端的保证机制:数据源可回放、Flink checkpoint机制、输出端幂等或者事务的机制,通过这些机制保证数据的准确性。
除了上面提到的几点还与一些比较常见的技术实现相关,比喻使用BloomFilter、Hyperloglog 本身存在一定误差的数据结构等。
04 - 延时
延时大小代表了当前任务处理数据的进度,一般会通过监控消费Kafka的Lag或者是在数据源处数据时间与当前系统时间差值来判断任务是否延时,同时延时代表了Flink程序的处理数据的能力。如果程序的处理能力跟不上数据流量,最终的表现就是任务延时,得到的数据指标是滞后的,影响业务及时决策。因此我们在开发过程中需要考虑到可能会影响任务性能瓶颈的卡点,同时需要通过提前压测的方式,检测任务是否出现延时情况。简单列举几点常见影响性能的问题。
数据倾斜,大量的数据被分配给同一个task处理,也就是流量分配不均匀,常常出现在大商家流量统计或者是热门话题流量统计中,数据倾斜导致的直接问题就是出现"木桶效应",该task成为整个任务处理的性能瓶颈,最终触发反压,降低了消费速率。常出现在group by 、join 一些操作中,其优化手段与离线优化大体相同,预聚合、两阶段计算等方式。
大状态,flink 任务保留了大量的状态数据,例如全局group by 操作、regular-join 操作,这种保留数据时间比较长的计算,如果选用FsStateBackend那么可能会造成严重的gc问题,拖慢整个任务, 对于大状态一般会选用RocksDBStateBackend,使用rocksdb存储状态数据,即便如此可能会带来比较大的io消耗,最终导致io延时比较高,这时可以考虑:使用SSD作为存储、MiniBatch机制减少对状态的访问、或者是regular-join转变为interval-join等优化手段。
外部IO交互,常常发生维度扩充或者是结果数据输出需要与外部存储交互,这在个过程中出现与外部交互延时比较高通常是因为使用的姿势不正确或者是使用存储不合理,可以使用异步IO、LRU缓存机制、批量输出机制、使用写优化类型的存储等方式优化。
05 - 容错能力
在分布式系统中,可能会因为程序处理异常、网络故障、机器故障等原因导致系统失败,因此在设计之初会充分考虑其容错能力,也就是可用性。Flink本身设计了一套先进的容错能力,从故障检测到恢复,保证在各种不可预知的问题下可以自动恢复并且数据恢复到正确的状态。
故障检测分为两个方面:一、守护进程检测, JobManager、ResourceManager 、TaskManager 之间会通过心跳检测相互判断对方是否存活;二、task异常上报,某个task处理异常,TaskManager会将异常信息上报给JobManager。通过这两方面的检测最终触发恢复策略,任务恢复会依赖持久化的一致性状态数据,也就是会依赖checkpoint机制,保证失败重启可以从最近一次成功的checkpoint状态数据中恢复。
除了Flink程序本身容错机制的保证,在实际的开发中还需要考虑以下几点:
- 缓存数据是否加入状态
通常为了减轻输出压力使用批量方式输出,那么批量缓存数据也需要加入到一致性状态中,保证失败恢复数据不会丢失
- 双链路
双链通常应对保障等级比较高的任务,做计算、存储双链路保障机制,类似于同城双机房或者异地多活策略。
- 平台层面支持
计算平台支持检测任务状态、自动拉起任务并从最近一次checkpoint中恢复。
06 - 可维护性
可维护性是指当面对Flink版本升级、修改程序bug、功能扩展等情况能够很方便的对程序做出修改。列举几种常见的处理手段:
- 模块化处理,比较常见一点就是数仓里面分层、数据抽象化,将通用的部分提取出来单独处理
- 尽量选择易懂的编程方式,对于分析类型程序来说,通俗易懂的就是sql, 使用sql化的方式完成程序开发
- 编程质量保证,从表、字段、注释等制定开发规范
- 当使用DataStream API时,给每一个算子设置一个唯一的uid, 当任务拓扑图发生改变时仍然能恢复对应的状态数据;当使用自定义状态时, 需要考虑并行度改变状态的伸缩变化以及使用的状态数据结构是否可支持变更与版本兼容性。
07 - 总结
本文主要从架构的视角去窥探如何比较全面考虑、设计一个Flink程序,包括适用性、数据质量、性能(延时)、容错能力、可维护性上的思考。