摘要:本文由网易云音乐数据智能部资深数据平台开发工程师蒋文伟分享,主要介绍 Flink SQL 在云音乐的产品化实践。分享内容如下:
- 简介
- 产品功能
- 性能优化
- 运维完善
- 未来规划
一、背景简介
1.Flink in Music
先简单的介绍下云音乐的现状,目前音乐这边的客户端日志,服务端日志大概在每日大千亿条左右,维度表数据源像 Redis,MySQL 这些大概有上百个。而服务的实时计算任务开发的人员有上百名,其中不仅包扩数据开发工程师,分析师,也包括算法,后台业务等同学。这些同学们也累积开发了上千个实时计算任务,这些任务不仅有统计任务,还有些一线业务,比如排行榜,实时热度等。
2.应用场景
这里我稍微列举了一些业务场景,比如我们的内容分发、实时数仓、算法推荐,还有索引任务、实时监控,AB test 等,几乎涵盖了整个数据链路中的大部分业务场景,可以说我们现在的业务已经离不开 Flink 的体系,那后面我们会以其中几个场景为例,看看我们在这些场景中使用原生的 Flink 会遇到那些问题。
■ 内容分发
第一个介绍的场景是分发,分发是个非常典型的场景。它会根据一定条件对数据流进行划分,把输入数据流切分成多个子流。
一般情况下分发会是整个数据链路的上游,所以相对来说这类任务非常重要,那么我们在这个场景中会遇到什么问题呢?
- 问题1:开发效率低,业务标准流程难以复用
首先是开发效率低下,这类业务逻辑非常简单,核心开发工作其实就是个 where 筛选,但是传统的开发方式需要用户了解很多额外的东西,比如 HDFS 的定时清理功能,如果这个组件交由用户开发,势必要开放权限,那么就可能会导致 HDFS 仓库文件被误删等安全事故,所以我们需要建立一套统一框架,且可以提供一系列标准化的组件,用户仅需要关心其核心业务逻辑即可。
- 问题2:学习成本高
第二个问题是学习成本较高,SQL 是一种非常优秀的数据处理语言,很多同学也都会,但是 Flink SQL 的配置却没普通 SQL 那么简单。Flink SQL 要求用户对每个组件的配置都非常熟悉,这是一个 HDFS 的 sink 操作,需要在 SQL 中配置输出目录,分区字段,文件大小,keytab,压缩格式等一系列的参数,而这些参数需要用户通过文档来学习。
针对这个,我们需要一种前端上的优化,通过给用户可视化的提示,来简化配置,降低学习成本。
- 问题3:外部环境混乱
第三,对一个稳定性,以及性能要求比较高的任务来说,所有的这些监控、报警的配套体系建设也都是必不可少的。
■ 特征快照
第二个例子,特征快照,先简单的说下什么是特征快照,特征快照简单的可以理解成把特征的历史版本进行存储,为什么要这么做呢,因为特征是动态变化的,每个事件未必能保准顺序到达,一个用户先点喜欢 DJ,在播放歌曲,再点击喜欢了动漫,我们最终这次播放关联的应该是 DJ 而不是动漫,但按照时间序可能就是错的,所以,对一每个版本的特征和 tag,都会有其唯一的 traceid 来进行管理,也就是一个 traceid 一个特征版本,这块在实时机器学习场景使用的非常广泛。
这边可以看到任务的流程图,包括数据清洗,收集,抽样,去重,join 等流程,这些流程也有很多业务逻辑,像 join 这个流程如果 join 不上怎么办,是放在内存里等,还是再次回流到 kafka 等待下一轮匹配,亦或是使用降级方案。相对来说,对稳定性、兜底方案等都有较多要求。
我们来看特征快照场景下会遇到哪些问题。
- 第一,开发成本较高,调试也比较复杂。上述提到我们有很多种模块共同组成这个任务,如果通过传统的日志打印,很难进行调试和维护。
- 第二,特殊的需求没有办法被快速的满足。像抽样功能完全取决于业务,比较难以抽象和复用。
其它的一些问题,包括调优问题、运维问题、监控的问题,我归纳为技术赋能。这些问题不能推给用户来做,只能由平台来进行统一的管理和维护。
综上所述,我们的产品化目标也出来了,我们希望打造一套能降低用户学习成本、运维成本,提升开发效率,并在全方位进行一个赋能的一站式的实时计算平台。也就是我们的主要介绍的产品,音乐的实时计算 Notebook 服务。
二、云音乐的实时计算 Notebook 服务
1.NoteBook with block
我们的 notebook 服务是由多个 block 快组成的,每个 block 可以自由组合,比如 SQL 类型的块之后加 2 个 sink 的组件,再加一个 source 组件,都可以。
同样,每个 block 块拥有子类型,也就是二级类型,比如一个 sink,会有一些类似 MySQL sink,Redis sink 的子类型,他们都以插件的形式进行加载,因此主类型可以被快速扩展,子类型也同样可以被快速扩展,组件开发就变得非常方便。
而整个 notebook 执行的过程,也会按照顺序由上至下的执行每一个 block,同时我们的 block 中除了 sink 之外都支持在页面的 debug 服务。
详细分享一下目前支持的 block 类型。
- 第一种就是 SQL block,支持 Flink SQL,当然我们也在中间做了一些优化。比如建立自己的 catalog 以及 function 管理中心,自定义的一些参数 set 语法等。
- 第二种是 custom block,我们会提供给用户一套标准的 API,通过 API 就可以进行个性化的开发,类似一个有限条件下的 Flink 的 jar 包任务。
source 和 sink 类型,他们比纯 SQL 类型有更多的优势,比如同样的功能在 SQL 中也可以实现,但是需要加很多 set config 配置。而现在,不仅 catalog DB 支持搜索,并且有些配置也将更加直观的被呈现。就像 HDFS sink,他的归档大小,序列化类型,前缀,等等常规的配置就直接可以让用户进行选择。
第二个优势,我们可以通过可视化 block 来提供组件,比如 source 端的动态限流,可以开启一个 source 的 slot 来做全局 config 的轮训,比如 sink 有类似 HDFS 文件过期清理或者小文件合并的功能。类似的标准组件可以通过 block 块更直观的提供给用户。
整个平台的页面大概介绍到这里,这里是些小的归纳总结,良好的交互也就是跟简单直观的操作我们刚刚介绍了一些,良好的扩展,也就是我们的 block 的插件化,元数据中心以及监控配套等,我们先看如何用这套 notebook 来解决实际例子。
■ Snapshot 场景的应用
首先是 snapshot 场景的应用,我们可以看到这个场景中,链路上的 4 种任务分别通过不同的 block 类型实现。
- Clollect,通过 SQL 就能完成;
- ETL,会有一些业务上的抽样方法和去重逻辑,所以会通过自定义 source 来实现;
- Snapshut join,除了 join 操作之外,可能会有业务降级方案等一系列非标需求,通过 SQL 无法完成,需要通过自定义的 transform 组件来实现 ;
- Extrect,通过可视化 sink来进行最后样本文件的落盘。
2.功能实现
说完前端功能,我们再来说下实现。
整套 block notebook 在执行的时候,都会找到对应类型的 Interpreter,每个 Interpreter 再通过子类型去发现真正的实现类。像第一个 source 指向 MySQL 的 blog 日志订阅,最终会创建一个 MySQL 的 blog 日志订阅 source 实现,每个 Interpreter 都会接收到所有上游执行过的所有结果,这个结果统一以 table 的方式进行流转,在没有指定的情况下,默认使用离自己最近的一个 table 作为输入。
■ Block Structure
这是 notebook 的数据结构,属于 JobContext。作为一个全局的共享的内容,里面包含着一个可执行环境和一些全局共享的配置。blockList 数组里每一个都是加载一个 interpreter 具体事件类。如果业务上有需求,扩展一个 block 也非常简单,只要实现一个接口即可,其他的部分都交给框架来完成。
■ 提交执行
以上是 block 的内部执行逻辑,我们再来单独讲一下提交的服务。
首先,服务会判断任务中的所用到的插件和依赖的文件,然后通过一些机制来确定主程序版本,这么做的原因很简单,我们经常会做升级,而升级之后可能执行计划可能会变动,有些用户停止任务后再通过 checkpoint 可能就起不来了,通过这个服务来实现多版本就非常简单了。我们还可以根据一些条件,比如任务是否是 checkpoint 启动的等来判断主程序版本。最后提交服务会通过一些逻辑对集群和队列的进行智能选择。
■ 整体架构
通过以上对整体架构的分享,想必大家也就比较清晰了。上层会有一层 notebook 的 server 的服务,下层会有一个 submit 的提交服务和一个 debug server。外围会有元数据管理中心来管理我们的 catalog,然后还有权限或者一些其他的外部系统来进行辅助。
整个体系最大的好处在于,用户的代码都在框架的范围内进行执行,这样的话我们可以快速的进行性能优化和功能调整。
三、性能优化
团队对整个平台有非常多的性能优化,这里只能抛砖引玉的说几点。
1.Table source 优化插件
第一个就是 Table source 的优化。生产过程中的遇到的性能问题,如:
- 原生的 KafkaTableSource 由于需要解析 Schema 无法将反序列化过程提取出来,而 Kafka 的并发受制于业务的 partition 数量,如果反序列化计算量要求较大,会造成性能瓶颈。
- 维表 Join 如果先进行 keyBy,可以提升缓存命中率。
我们的解决方案包括 3 个大的步骤。
1、动态代理方式获取 byte 流。
2、将 byte 流进行二次处理:
- 动态的加载所需执行的插件。
- 分配插件到 Map 和 Filter 算子 中去(保证插件变动时 ckp 启动)。
- 调整并发。
- 反序列化成目标 Row。
3、返回 ProxyTableSource。这样就可以无限的扩展功能。
这是一个优化实例:降低由于序列化计算量过大而导致的 source 端性能瓶颈我们把有 5 个 kafka partition 的一个任务拆分成 5 个 source 跟 10 个 Deserialization 的并发,从而提升了整个任务的吞吐。
2.多 Sink 合并
第二个是多 sink 合并,这块高版本已经有了,因为我们还在使用 1.10,所以才需要优化,这块就不详细说了。
3.流量降级方案
第三个是我们的流量降级方案,这块优化涉及到一些体系的建设。一般来说,我们的分发任务会有 2 个 sink,会分发到主消息队列和备份消息队列,这 2 个队列的 catalog 完全一致,只会通过 tag 进行区分,所以相关代码开发也完全一致,下游读取的时候,会根据任务的 tag 分析说我们读主还是备。举例,归档任务,测试任务,等会从备份流来读取,这样我们就对 kafka 集群进行了压力分流。
四、运维监控增强
介绍完性能优化,我们来聊下运维和监控。Flink 自己已经带有不少监控,比如failover 次数、QPS、checkpoint 成功率,但是在生产过程中,这可能还不够,所以会增加很多内置监控,并且对血缘进行收集。
此外我们做了 4 个智能诊断功能,包括内存诊断、性能诊断、checkpoint 诊断和日志诊断,从而来判断我们的任务运行情况。
这是我们之前看过架构图,红色就是监控诊断部分,由一个统一的数据收集服务来获取信息,并提供给监控报警等服务使用。
监控增强这块其实就是在在框架中增加很多内置的指标,比如 sink 平均写入时间,序列化错误率,维表 join 命中率,sink 写入时间等等一系列的指标,这些参数也有助于我们更多维度的了解任务实际情况,当然我们可以选择是否开启这些监控,以保证高峰期任务性能。
第二块是血缘,它是数据治理中非常重要的一块。我们收集数据血缘比较简单,通过Block 的参数进行一个静态解析来看所有的输入源和输出表,然后上报给数据中心,最终组成一个血缘的关系图。
我们的智能诊断其实是利用所有监控数据来综合判断一个任务是不是处于正常状态,包括四块。
- 第一,内存诊断
- 第二,性能诊断
- 第三,Checkpoint 诊断
- 第四,日志诊断,举例说明,内存诊断,它主要通过 GC 如何触发报警,会有很多条判断标准,比如 young gc 频率,full gc 频率, gc 耗时等等是否大于或者小于一定值,通过这些条件综合来判断一个任务是否存在 gc 异常,以及异常的等级。而日志会通过一定条件对错误日志进行筛选,这个条件可以是用户自定义的,比如统计某个具体的 exception 的次数,当达到统计次数时候进行报警的触发。这样的话,某些业务不想被 failover,又想收到异常报警,就通过简单的 try catch 就能完成。
这是我们智能诊断的一个内存诊断的前端的页面,展示了一个实际任务的内存的使用情况分布。
五、未来规划
最后讲一下对未来的一些规划。
第一,体系建设:权限体系将更加灵活,数据安全/用户管理等配套将更新完善。
第二,Notebook 体系将引入其他数据计算引擎(如 druid 等)。
第三,拥抱新版本,现在的 Flink 1.12 有非常多的新特性,有些也非常吸引人。我们希望能积极的跟上社区版本。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。