共计 10300 个字符,预计需要花费 26 分钟才能阅读完成。
摘要:本文整顿自汽车之家实时计算平台负责人邸星星在 Flink Forward Asia 2021 平台建设专场的演讲。次要内容包含:
- 利用场景
- 估算资源管控
- Flink 伸缩容
- 湖仓一体
- PyFlink 实际
- 后续布局
点击查看直播回放 & 演讲 PDF
一、利用场景
咱们的利用场景与其余公司很相似,涵盖了实时指标统计、监控预警、实时数据处理、实时用户行为、实时入湖、实时数据传输这几个方面:
- 施行指标统计包含实时的流量剖析、车展大屏、818 实时大屏等,能够间接反对实时地查看重大流动的成果,不便及时调整经营策略;
- 监控预警包含各个利用的后端日志剖析报警、利用的性能监控预警、C 端用户的视频播放品质监控预警,这也是实时计算很典型的利用场景。通过定义正当的告警策略,能够在第一工夫感知到外围零碎的问题,并联合实时的数据分析疾速定位问题;
- 实时数据处理次要反对实时数仓建设、内容中台、电商中台等业务;
- 实时用户行为次要依据用户在 APP 上的各种行为来记录用户的画像及特色,这部分利用的成果最终会间接晋升用户体验。典型的场景就是智能举荐,咱们会联合用户最近感兴趣的内容,来为用户实时举荐文章、小视频等优质资源,晋升用户的应用体验;
- 实时入湖是咱们往年在平台建设方面重点发力的方向。咱们落地的湖仓一体架构相比齐全基于 Hive 的架构,在很多方面都有晋升。目前曾经在多个主题落地,晋升成果也比较显著;
- 最初是根底的实时数据传输场景,用户能够将业务库或 Kafka 中的数据便捷地散发到多种存储引擎中应答不同的业务需要,比方散发到 ES 中反对疾速检索。
咱们最早是应用 Storm 平台,基于 Spout、Bolt 开发模型,实现了根底的实时计算开发,这个阶段是齐全基于 Java 编码方式实现开发,开发门槛及学习老本都比拟高。
第二阶段,咱们在 18、19 年引入 Flink,并建设了 AutoStream 1.0 平台。这个阶段咱们次要的指标是提效、升高开发门槛和学习老本。将之前纯 Java 开发方式转变为基于 SQL + UDF 的开发方式。因为 Flink 晚期还不反对 DDL,所以咱们很大一部分工作就是建设本人的 meta server,并通过 DDL 定义 source sink 组件,同时欠缺业务库数据的实时接入,并将公司外部罕用的存储引擎集成到平台上,实现整个实时开发链路的买通。
第三阶段,咱们将 Flink 降级到 1.9 版本,并将平台降级为 AutoStream 2.0 版本,反对原生 DDL,同时反对自助上传 UDF,简化了 UDF 应用流程。同时随着工作数及平台用户的减少,咱们的日常 on call 工夫也随之减少,所以咱们上线了工作的衰弱评分机制,从多个方面剖析工作的衰弱度,帮忙用户理解工作能够优化的点并附带解决方案。咱们还上线了在线诊断性能,反对动静批改日志级别,查看线程栈和火焰图,晋升用户定位问题的效率,同时也升高了咱们平台方的日常 on call 老本。
AutoStream 3.0 代表了咱们往年次要的工作,首先是将 Flink 降级到 1.12 版本,这次降级带给咱们的最间接的收益就是反对湖仓一体、Native on K8S、PyFlink。同时本着降本提效的思路,新增了智能伸缩容的性能,一方面能够晋升实时计算资源的利用率,另一方面也进一步升高用户优化工作资源的难度。
上图是 AutoStream 2.0 的架构,它蕴含了很多内容,涵盖了平台整体的性能与定位。但不可避免地存在诸多痛点。
因为实时计算离线的存储资源是混用的,离线 Hadoop 集群独自为实时计算拆出了一部分服务器并独自部署了一套 Yarn 供实时计算应用,这部分服务器的磁盘用来反对离线数据的存储,CPU 内存次要用来反对运行 Flink 工作,所以 Flink 计算资源并没有独占服务器,咱们也没有对计算资源作严格的管控,所以导致有很多任务分配的资源是不合理的,通常是申请了过多的 CPU 资源但理论的利用率却比拟低。
随着公司容器化建设的逐步推进,往年咱们曾经反对了离线和在线混部并错峰分配资源的形式。也就意味着 Hadoop 集群的 CPU 内存除了反对 Flink 实时计算,也能够反对在线业务的部署,对 Flink 计算资源管控的重要性及紧迫水平就凸显进去了。
接下来最重要的就是推动用户做资源的调优,这部分工作对用户来说存在肯定难度。首先,要了解 CPU 内存和并行度的调整对工作的影响就是有老本的,而且通常批改工作资源、重启工作就须要几分钟,用户还须要继续察看是否对业务产生了影响,比方呈现提早或内存溢出等。简略来说,用户的调优老本是比拟高的。
现有的基于 Hive 的数仓架构须要降级,t+1 或 h+1 的时效性曾经无奈满足很多业务场景的需要,咱们最终选定 Flink 和 Iceberg 来构建实时湖仓一体的架构。
最初是实时计算反对的生态不够欠缺。咱们的人工智能团队次要以 Python 语言为主,基于 SQL + UDF 的形式显然对他们不够敌对,所以咱们做了 PyFlink 的集成工作,解决了这一痛点。
上图是 AutoStream 3.0 的整体介绍。
基于 2.0 版本的痛点,除了性能和应用性降级之外,咱们次要还做了以下几个方面的工作:
首先是增强了估算管控,上线了主动伸缩容性能,建设并落地了实时湖仓的架构,并上线了 PyFlink,反对 Python 开发实时工作。同时咱们还基于 Flink + StarRocks 引擎,对实时多维分析的链路做了进一步简化。
二、估算资源管控
为什么须要做估算资源管控策略?
首先是服务器资源没有按团队做估算划分,先用先得,没有下限,工作资源利用率低,个别团队存在重大的资源节约状况。同时没有外力的推动,大部分用户被动优化资源的意识很单薄。
咱们做的第一步就是启用估算的强控机制。与外部的资产云零碎做对接初始化团队的可用估算,超出预算后工作将无奈启动。还对此定义了标准,用户须要先优化团队内的低利用率工作来开释估算,原则上资源利用率低的工作数应该管制在 10% 以内,如果无奈优化,能够在资产云零碎上发动团队间估算调拨的流程,也就是借资源;如果还是失败,则会由平台开白名单长期反对业务。
平台标准里,咱们对资源利用率低的工作也进行了定义,同时展现出低利用率的起因及解决方案。
目前咱们次要是针对 CPU 使用率、内存使用率和闲暇 slot 这几个外围规定来辨认低利用率工作。早在 AutoStream 2.0 版本,咱们就上线了 Flink 工作的衰弱评分机制,失去了丰盛的细粒度得分数据,所以能够很容易地辨认低利用率工作。
咱们通过引入强控流程来严控计算资源的用量,通过制订标准来晋升用户被动优化资源的意识,通过开发主动伸缩容性能升高用户的调优老本。由此达到的收益是,在实时计算业务稳步增长的前提下咱们全年没有新增服务器。
三、Flink 伸缩容
为什么须要主动伸缩容性能?
- 首先是降本、晋升资源利用率;
- 其次是升高资源调优的老本;
- 最初是升高资源调优过程自身对工作稳定性的影响。
上图是主动伸缩容配置的页面,能够指定主动伸缩容的触发工夫,比方能够指定在夜里低峰期间执行,升高伸缩容对业务的影响,反对指定 CPU 并行度、内存维度伸缩容的策略,每次执行伸缩容都会通过钉钉和邮件告诉工作负责人,并且会记录伸缩容的触发起因和伸缩容之后的最新资源配置。
上图是主动伸缩容性能的整体设计。咱们在 jobmanager 中减少了一个新的组件 RescaleCoordinator,它应用 ha 保护其生命周期,且与 dispatcher 之间彼此通信。RescaleCoordinator 会定期拜访 AutoStream 提供的接口,AutoStream 平台会依据用户配置的伸缩容策略判断是否须要执行伸缩容。
整体的流程如下:RescaleCoordinator 获取到 leader 后会定期检查是否须要伸缩容,如果须要则向 dispatcher 告诉 jobmanager 开始伸缩容。jobmaster 会向 resourcemanager 申请 taskmanager,待所有申请的 taskmanager 都准备就绪,就会将旧的 taskmanager 开释掉,而后基于新的 taskmanager 从新调度,最终把这次后果长久化到 zk 和 HDFS 上。
平台的 Flink 本就应用了 zk 和 HDFS 做 ha,所以咱们不须要引入新的组件。此外,因为新的 container 是提前申请好的,又能省去 container 申请的工夫,防止了因为资源不够而申请不到 slot 导致工作 recover 失败。如果是做并行度的伸缩容,须要在发动调度前批改 jobgraph 的并行度来实现。
以 CPU 内存为例,第一步是向 ResourceManager 申请 container 并为之打标记。新的 taskmanager container 通过 slot pool 向 resultmanager 申请,这一步须要在 slot pool 中保护新的资源配置,对应上图中的 CPU 2 核,内存 2GB,且须要反对回滚机制。如果这次伸缩容失败,资源设置回滚到 CPU 1 核,内存 1G。
第二步,停掉工作,删除 ExecutionGraph。
第三步,开释旧 taskmanager,从新构建 ExecutionGraph,并在标记的 taskmanager 上从保留点复原工作。
第四步,将此次伸缩容的资源设置长久化到 zk 和 HDFS,如果 jobmanager 在这里挂掉,那么之前伸缩容的配置都会失落,所以须要将伸缩容后的配置保留在 zk 和 HDFS 上,数据存在基于 HDFS 的 block server 中,在 zk 中会保留 block server 的 key。
最初,对伸缩容策略进行一个粗略的总结:
首先是基于并行度的伸缩容:
- 如果存在生产 Kafka 提早且 CPU 使用率较低,很可能是 io 密集型工作,能够减少并行度;
- 如果是存在闲暇 slot 则执行缩容,防止资源节约;
- CPU 维度的伸缩容次要依据 CPU 使用率来断定, 会依据 taskmanager 过程调配的 CPU 核数和 taskmanager 过程理论的 CPU 使用率,来计算出 CPU 使用率作为 CPU 扩缩容的要害指标;
- 内存维度次要依据内存使用率和 GC 状况来断定是否须要扩容和缩容。
四、湖仓一体
基于 Hive 的数据仓库次要存在以下几个痛点:
- 首先是时效性,目前基于 Hive 的数仓绝大部分是 t+1,数据产生后至多要一个小时能力在数仓中查问到。随着公司整体技术能力的晋升,很多场景对数据的时效性要求越来越高,比方须要准实时的样本数据来反对模型训练,须要准实时的多维分析来帮忙排查点击率降落的根因;
- 其次是 Hive 2.0 无奈反对 upsert 需要,业务库数据入仓只能 t+1 全量同步,数据修改老本很高,同时不反对 upsert 意味着存储层面无奈实现批流一体;
- 最初 Hive 的 Schema 属于写入型,一旦数据写入之后 Schema 就难以变更。
通过一番选型,咱们决定抉择基于 Iceberg 来构建湖仓一体架构,解决基于 Hive 的数据仓库的痛点。
Iceberg 的定位是凋谢的表格,不绑定某一种存储或计算引擎,同时它能提供增量快照机制,能够轻松实现准实时的数据写入和读取。Iceberg 的 v2 格局反对 acid 语义,能够满足 upsert 需要,为后续做存储层面的批流一体提供了可能性。读取型的 schema 对 schema 的变更也非常敌对。目前次要的查问引擎都和 Iceberg 做了集成,读写门路上也都反对了流和批的形式,从流批一体的角度来看,也是非常敌对的。
上图右侧是 Iceberg 增量快照机制的基本原理。每次针对表的 commit 操作都会产生一个新的快照,比方针对表的第一次数据写入的 commit 会生成 snapshot0 快照 (上图中的 s0),第二次写入的 commit 会生成 s1。每个快照对应一个 manifest list 对象,会指向多个 manifest file,每个 manifest file 又会指向多个 data file,也就是存储数据的文件。图中的 current metadata pointer 会为每一个 Iceberg 表指向一个最新的 metadata file,即最新生成的快照。
上图是 Iceberg 目前在咱们外部的集成状况,最底层是基于 Hive Metastore 来对立 Hive 表和 Iceberg 表的元数据,基于 HDFS 来对立 Hive 表和 Iceberg 表的存储,这也是湖仓一体的根底。往上一层是表格局,即 Iceberg 对本身的定位:介于存储引擎和计算引擎之间的凋谢的表格局。再往上是计算引擎,目前 Flink 次要负责数据的实时入湖工作,Spark 和 Hive 作为次要的产品引擎。最下面是计算平台,Autostream 反对点击流和日志类的数据实时入湖,AutoDTS 反对关系型数据库中的数据实时入湖,离线平台与 Iceberg 做了集成,反对像应用 Hive 表一样来应用 Iceberg,在晋升数据时效性的同时,尽量避免减少额定的应用老本。
上面是咱们在湖仓一体架构落地过程中的一些典型的实际。
首先是实时数据入湖方面。
在 Iceberg 场景中,须要确保主键雷同的数据写入到同一个 bucket 的下。因为 Flink 表的 DDL 并不反对 Iceberg 的 bucket 的定义,所以咱们做的第一件事就是反对在 Flink DDL 的 property 中定义 bucket。
第二个问题是 Iceberg 表自身无奈间接反映数据的写入进度,离线调度难以准确地触发上游工作,所以咱们借助 Flink 良好的 watermark 机制,间接在入湖的阶段将 watermark 长久化到 Iceberg 表的元数据中,这样能够通过简略的脚本调用,就能晓得 Iceberg 表的数据写入进度,从而准确地触发上游的调度工作。
第三个问题是实时入湖阶段和离线团队账号体系的买通。Flink 向 Iceberg 写入数据的时候,须要拜访 HDFS 和 Hivemetastore,所以必然要和离线既有的团队账号体系买通。一个离线的 HDFS 目录只能给一个用户调配写入权限,所以在引入 Iceberg 之前,所有的 Flink 工作都是通过一个固定的 Hadoop 账号运行的,这样的益处就是不便咱们做对立的资源管理,包含 checkpoint 目录的对立治理。咱们通过批改 Iceberg 创立 Hadoop Filesystem 实例的代码,减少了账号代理的机制,实现应用自定义账号向 Iceberg 写入数据,同时扩大 HiveMetaStoreClient 减少代理机制来买通对 HiveMetaStore 的拜访。
接下来是可用性和稳定性方面的实际。
为了湖仓一体元数据的对立,咱们保持和离线数仓复用同一套 HiveMetaStore 服务,期间也遇到了很多稳定性和数据正确性的问题。
首先是拜访 HiveMetaStore 异样,这是因为咱们的 Hadoop 集群启用了 kerberos 机制,并且 Hive config 的过渡办法被误用,导致 Hive 客户端 kerberos 相干配置被笼罩,造成拜访 HiveMetaStore 异样。咱们做了相应做的修复,也已反馈给社区。
其次咱们引入了基于 zk 的分布式锁用来替换默认的 HiveMetaStore 的锁。Iceberg 默认基于 HiveMetaStore 分布式锁来管制单表的并发 commit,然而存在一种状况,当 Flink 过程意外退出时,代码无奈触达 unlock 逻辑,导致针对表级别的锁始终被占用,无奈开释。Flink 工作被主动拉起后无奈再次获取到锁,导致后续无奈失常写入数据。随着入湖任务量的减少,这个问题每周都会至多呈现一次,每次都须要人工染指去、手动拜访 HiveMetaStore 开释锁,能力让 Flink 工作恢复正常。如果解决不及时,可能导致数据入湖提早几个小时。
针对这一问题,在高版本的 Hive 中其实曾经有了解决方案,就是针对分布式锁设置超时工夫,超时之后会主动将锁开释。但咱们是基于 Hive 2.0.1 版本,整体降级和拉取 patch 的老本都比拟高,所以咱们针对 Iceberg 做了革新,应用基于 zk 的分布式锁替换之前的锁机制,上线后这一问题也失去了基本的解决。
还有 Iceberg 表元数据文件被误删的问题,这个问题会导致呈现找不到数据文件的异样,间接影响 Iceberg 表无奈被拜访。解决办法就是在批改 metadata_location 属性的时候,减少容错机制,优先尝试重试并查看是否批改胜利,仅在确认元数据未保留胜利的状况下,才会对 metadata.json 文件做删除操作。
接下来是对 v2 格局小文件合并方面的一些改良。
Iceberg 实现 upsert 语义的原理是,它的 v2 格局通过引入 sequence number 并联合 position delete file 和 equality delete file 来实现的,写入思路是首先将 delete 行写在 equality delete file 中,如果 delete 行在以后的事务中 insert 过,就把 insert 行所在的文件行号和地址给 position delete file。
举个例子,假如有两个事务:
- 第一个事务 insert 两条数据对应 ID 为 1 和 2 的数据,事务 commit 后会生成一个 datafile,此时的 sequence number 是 1。
- 第二个事务先 insert 一条 ID 为 3 的数据,紧接着把它对应的 value 从 300 批改为 301,最初删除 ID 为 2 的数据。
这三条 SQL 产生的行为如下:首先生成了一个 data file2,外面包含了新写入的 ID 为 3 的数据即 I (3,300),还蕴含了 ID 为 3 的数据 update 之后的数据即 I (300,301),并把 I (3,300) 写入到 equality delete file 中。因为 ID 为 3 的数据在以后的事务中写入过,所以还会生成一个 position delete file,而后把 (3,300) 这条数据对应的 position 标记为删除。而因为 ID 为 2 的数据不是在以后事务中写入的,所以把 ID 为 2 的数据即 I (2,200) 追加到 equality delete file,并标记为删除即可。
upsert 语义在读门路上的实现思路如下:首先 position delete file 与不大于本人 sequence number 的 datafile 做 join,equality delete file 则与小于本人 sequence number 的 datafile 做 join。因为上图中,position delete file 会把 datafile2 中的第零条数据即 I (3,300) 删除,equality delete file 会把 datafile1 中 I (2,200) 删除掉,最终查问后果只有 ID 为 1 和 3 的两条数据。
因为 delete file 越来越多,查问性能也会随之升高。为了保障查问性能,咱们每个小时都会对 Iceberg 表进行小文件合并。
然而在引入 sequence number 之前,针对 v1 格局的小文件合并无奈保障 v2 格局数据在合并后的正确性。所以在实际的过程中,咱们针对 v2 格局的小文件合并做了一些革新。
针对 v1 格局的小文件合并思路和读取思路完全一致,行将两种 delete file apply 到适合的 datafile 上,合并后删除对旧文件的援用,改为援用新生成的 datafile。依据之前 sequence number 的定义,它会始终递增,所以合并后的 datafile 对应的 sequence number 也会变大,会导致 v2 格局的数据在合并小文件场景下的抵触。
首先小文件合并过程中,批改 sequence number 会导致 Flink 实时写入的事务抵触,导致上图中事务 3 的 delete 语句生效。事务 3 将主键 3 的数据 delete,但事务 4 的小文件合并胜利后会把这条数据又加回来,因为这条数据的 sequence number 变成 4,此时 delete file 的 sequence number 是 3,不会再把 sequence number 为 4 的数据删掉。
针对上述抵触,咱们对 Iceberg 的小文件合并做了革新,革新的思路是合并小文件实质上并不会对最终的数据做批改,仅仅是优化文件的存储。所以在合并过程中,复用被合并的文件中最大的 sequence number 即可。
依照新的思路,事务 4 之后的 datafile 对应的新 sequence number 为 2,也就是复用了被合并的文件中的最大的 sequence number。sequence numbe2 仍旧小于事务 3 的 sequence number3,所以能够保障 delete file 的语义正确性。
湖仓一体架构的落地为咱们带来了不少的收益:
- 首先,流量、内容、线索主题的数据时效性失去了大幅晋升,从之前的天级 / 小时级晋升到 10 分钟以内,数仓外围工作的 SLA 提前两个小时实现;
- 其次,特色工程得以提效,在不扭转原先架构的状况下,模型训练的实效性从天级 / 小时级晋升到 10 分钟级;
- 最初,业务库数据能够在咱们的 AutoStream 平台上通过配置实现准实时入湖,相应的也能够通过 Spark SQL 做准实时的剖析查问。同时咱们也在小范畴地测试将聚合工作的后果准实时入湖,通过 Flink+Iceberg 打造基于 Iceberg 的准实时的物化视图,能够大幅度晋升剖析的效率和体验。
五、PyFlink 实际
引入 PyFlink,次要因为咱们想把 Flink 弱小的实时计算能力输入给人工智能团队。人工智能团队因为技术自身的特点,大部分开发人员都是基于 Python 语言开发,而 Python 自身的分布式和多线程反对比拟弱,他们须要一个能疾速上手又具备分布式计算能力的框架,来简化他们日常的程序开发和保护。咱们正好也须要补全平台上对 Python 生态的反对有余,所以很天然地想到了把 PyFlink 集成到咱们的平台上。
上图是 PyFlink 的根本架构。python vm 和 jvm 双向通信的架构,实现了 Python API 和 Java API 的一一映射,用户能够通过编写 Python 代码来实现 Flink 工作的开发。
通过比照,咱们最终抉择了用 Kubernetes 形式来部署 PyFlink 环境。Kubernetes 除了能够较好地反对资源隔离,也能够不便地集成 Python 环境和其余机器学习的依赖。
对于 PyFlink,咱们次要反对了三种依赖治理:
- 首先是 Java 的 Jar 依赖,这依赖 AutoStream 自身的文件治理服务,将运行时须要的 jar 增加到 Flink 的运行环境中;
- 其次是 Python 文件的依赖,用户也能够应用 AutoStream 平台的文件服务对立治理 Python 文件。正确配置依赖后,提交工作时就会主动下载到镜像的外部;
- 最初是 Python 的第三方依赖,用户提交工作时会主动下载依赖配置,依附 PyFlink 的主动装置,在启动 Python 过程前会主动进行装置。
Flink 原生基于 K8S 部署的状况下,每次批改用户程序都须要从新制作镜像,费时费力。为了简化用户的开发难度、进步部署效率,咱们在集成 PyFlink 的时候进行了优化。依赖平台提供的文件服务,用户启动工作时只须要将所依赖的文件上传到文件服务上,而后通过批改镜像的入口脚本,真正启动 jobmanager 和 taskmanager 过程之前,依据传入的参数,让所需的依赖也就是 jar 文件、Python 文件等下载到容器的外部目录下,这样程序启动就能够加载到对应的文件了。
咱们还和 AutoStream 平台的 catalog 做了买通,Python 用户能够间接复用平台上曾经申明过的表和 UDF,也能够本人开发注册 Python UDF。用户能够通过 SQL + UDF 的形式疾速实现业务需要的开发。
举个自定义工作的事例。在开发 PyFlink 程序时,能够依附 PyFlink 提供的 Gateway 调用平台内置的 Catelog 和 UDF 注册类实现 Catelog 和 UDF 的注册,防止开发时的反复定义和反复开发。
再来看一下 PyFlink UDF 的开发事例。用户能够在原有的我的项目中进行开发,放弃原来的我的项目构造不变,而后依照 PyFlink UDF 开发标准,在 eval 办法中调用相应的解决逻辑来减少新的额 Python 代码。
而应用 Python UDF,只须要在 SQL 工作中创立 function 时指定 language 为 Python,并在高级配置中增加 Python UDF 所需的文件。
咱们通过集成 PyFlink + AutoStream 实现了对 Python 生态的根底反对,解决了 Python 用户难以开发实时工作的痛点。同时他们也能够不便地将之前部署的单机程序迁徙到 AutoStream 平台上,享受 Flink 弱小的分布式计算能力。
六、后续布局
将来,咱们会继续优化计算资源,让计算资源的利用更加合理化,进一步降低成本。一方面充分利用主动伸缩容的性能,扩大伸缩容策略,实现实时离线计算资源的混部,利用实时离线错峰计算的劣势进一步升高实时计算的服务器老本。同时咱们也会尝试优化 Yarn 的细粒度资源调度,比方调配给 jobmanager 和 taskmanager 少于一核的资源,做更精细化的优化。
在流批一体方面,咱们筹备利用 Flink 的批处理能力小范畴做批处理的利用和 web 场景的试水。同时在数据湖架构的根底上,持续摸索存储层面批流一体的可能性。最近咱们还在关注 FLIP-188 提案,它提出了一个全新的思路,将流表和批处理表进行肯定水平的对立,能够实现一次 insert 就把数据同时写入到 Logstore 和 Filestore 中,让上游能够实时生产 Logstore 的数据做实时 Pipeline,也能够应用 filestore 的批式数据做 ad_hoc 查问。后续咱们心愿也能做相似的尝试。
点击查看直播回放 & 演讲 PDF
更多 Flink 相干技术问题,可扫码退出社区钉钉交换群
第一工夫获取最新技术文章和社区动静,请关注公众号~