作者介绍
左元,尚硅谷高级讲师,中科院电子所硕士,精通C、Python、Javascript、Golang等开发语言,多年云计算、大数据、数据分析、分布式爬虫、前端和后端开发经验,对比特币、以太坊以及超级账本等区块链技术有源码级别的深入研究。热爱技术,尤其喜欢研究各种算法,在Web开发、分布式系统、区块链技术以及机器学习等方面有深厚的积累。
P1缘起
在尚硅谷教授Flink框架期间,我们计划写一本有关Flink实践的书籍,将我们在讲课过程中碰到的难点以及碰撞出的火花沉淀下来,供业界参考,方便大家快速上手Flink,以及能够实现一些复杂的需求,编写出高性能的Flink流处理程序。
在教学与教研的过程中,其实我们很少去深入研究源码,而是着重于熟练掌握Flink提供的API,以及如何在应用程序层面写出高性能的Flink程序。
为什么不去深入研究源码?
盲目去阅读源码是学习编程最低效的一种方式。
Flink的源码非常庞大,有一百多万行Java代码以及几十万行Scala代码。不带任何目的去读,非常容易迷失在源码里。
想要去理解一个框架的原理,最好的方式是自己去实现一个,也就是广为人知的“造轮子”。想学习操作系统,就自己实现一个小的内核;想学习编译原理,就自己实现一个小的编译器;想学习Web框,就自己实现一个小的Web框架;想研究编辑器,可以自己动手实现一个简单的文本编辑器;想学习Hadoop,就自己写一个小的MapReduce计算引擎……(以上这些轮子,在下都造过,有的还造过不止一个)
学习编程最重要的方法依然是不断去写代码!
不是看代码。碰到问题,第一时间应该是去查看官方文档,使用搜索引擎搜索,以及去StackOverflow这样的网站去提问,如果碰到实在解决不了的问题,才去阅读源码,而这个时候,也必然对Flink框架的使用已经非常熟练了,阅读源码也就有的放矢了。Torvalds Linus所说的“read the fucking source code”说的也就是这种情况。
Flink源码很庞大,迭代了很多年,贡献人数接近1000人,重构过很多次。所以源码里面使用了很多编程中比较高级的技术,例如:
- 无处不在的依赖注入,这样的好处是解耦,但不好的地方就是阅读起来很困难,因为各种类的实现非常分散。当然解耦是大型项目必须要做的事情。
- 大量使用了设计模式:工厂模式,观察者模式,还有例如访问者模式这样很少用到的设计模式(如果没有使用Java写过编译器或者解释器,基本不可能对访问者模式有比较深刻的理解,因为访问者模式最常用的场景就是遍历抽象语法树)。
- 大量使用了一些Java的高级特性,例如Java的异步编程:Future特性、Java用来实现线程池的Executor接口、海量的匿名函数等等。
- 底层通信大量使用了Scala编写的Akka以及Java的Netty。所以涉及到了Scala和Java的混合编程,这也是一个挑战。诸如此类就不一一列举了,如果对相关知识没有深入的理解,那么看源码本身既很难读懂,也没有多少收获。
为什么现在又要开始读Flink的源码呢?因为写书的话,必须对Flink的底层有一个深入的理解,所以就涉及到了读源码的问题。那么,接下来要解决的问题就是如何阅读源码?
P2如何阅读源码
这就说到了本文的重点——
通过制作一个迷你版的Flink来熟悉Flink的源码。
阅读源码最重要的一点就是: 一定要去调试和修改源码!
这样才能真正理解源码。我的方法就是通过删除和修改Flink的源码,使得删改以后的Flink源码可以运行基本的Flink程序,例如:word count程序。
经过删改以后,我将Flink原本的100多万行代码删改到了10万行,而没有损失Flink的基本功能,也就是说核心的计算引擎并没有受到破坏。还顺便发现了一处变量的命名错误,并提交了pull request,成为了Flink的源码贡献者 :)
我制作的迷你版Flink的仓库地址:
针对Flink源码,我大概做了以下修改:
- 删除了Flink的一些库,例如flink table,flink cep等libraries。到最后删到只剩下这几个库:flink-core、 flink-runtime、flink-java、flink-streaming-java、flink-metrics、flink-optimizer。
- 将Flink每个库的测试代码全部删除,也就是每个lib的tests文件夹。进行到现在,大概剩下30万行Java代码。接下来,真正的挑战开始了,因为这几个模块有互相依赖的关系,随便删除一个模块甚至一个文件,都会爆红一大片。
- 将Flink核心代码库例如flink-core、flink-runtime等lib中的统计模块代码flink-metrics删除,由于metrics代码耦合在Flink源码的很多文件中,所以删除起来很麻烦,因为需要修改很多函数的签名或者类的定义等等。
- 将flink-optimizer这个优化模块删除。
- 将文件系统相关的代码删除,因为word count程序并没有用到文件的读写。而且文件系统相关代码也不是Flink计算引擎的核心部件。这部分工作量也很大,因为文件系统的代码也分散在了Flink源码中很多的地方。
- 修改代码:将一些运行代码时用不到的接口实现、条件语句、异常处理等代码删去,因为这些代码在运行的时候用不到,而且在阅读源码时,使我们抓不到重点。如果将这些代码删去,在看源码时将会很清爽,也方便加注释。
举个例子:由于我在运行程序时,使用的是Intellij IDEA本地运行,所以其实使用的是MiniCluster这个迷你集群。而Flink的执行器接口是PipelineExecutor,共有好几个实现:LocalExecutor、EmbeddedExecutor、RemoteExecutor、AbstractJobClusterExecutor、AbstractSessionClusterExecutor,而由于我们只使用了LocalExecutor这一个实现,所以其余的都可以删除,这样读代码会方便很多。
- 由于Flink的master节点会开启一个web ui,所以web ui也需要去掉,由于web ui中涉及到Flink的metrics数据的展示,以及耦合在其他的一些代码里,在删除的时候颇费了一些周折。
- 逐一阅读各个模块,找出没有用到的代码,然后移除。
- 很重要的一点:使用Git来管理项目,每删改一些代码,就进行commit操作,这样在改了代码以后,如果程序跑不通,可以回滚到之前可以跑通的代码!
经过以上一系列步骤,可以将Flink源码删改至10万行左右,迷你Flink就诞生了。
P3收获了什么
其实收获非常多,比如成了源码贡献者,哈哈,当然这个并不重要。
这里要强调一点: 提高写程序能力的唯一方法就是不断的写代码,不断的写没写过的代码,不断的写不熟悉的代码!
收获大概有以下这些:
- 在删改Flink源码的过程中,由于需要保证word count程序能跑通,所以碰到的报错信息都必须要修复掉。在修复的过程中,逼迫我去认真阅读代码,从而搞懂了Flink的整个执行流程。这就是我之前所说的为什么阅读源码要去运行它、修改它的原因,只有这样,才能把源码彻底搞清楚。
- 看到了Java高手是如何写代码的。其实我之前并没有阅读过大型的Java项目代码,所以很多Java开发才使用到的技术并没有特别关注。之前读过大型的代码库大概是像C语言、Python、JavaScript、Golang,还有一些比较冷门的语言如OCaml、Rust之类的。这次阅读Flink代码,确实学到了不少Java开发相对高级一些的技术。例如,Java的设计模式具体是如何使用的。如何使用各种设计模式将大型项目解耦,可以说各种设计模式,对于Java而言是不得不用的技术,对于这一点,我有了更深的体会。
- 其他语言例如Python的协程,JavaScript的Promise这些异步编程方式在Java中是如何使用的,具体来讲就是Java的Future这一特性,在Flink中得到了大量的使用,有关并发的操作基本都是由Future这一特性来实现的。
- Flink源码中大量使用了泛型,虽然Java的泛型比Scala、Haskell、Ocaml等有所欠缺,但Java本着实用主义的态度,设计出来的泛型也能达到很好的使用效果。
- Flink底层通信依赖Scala编写的Akka这一著名的Actor模式的并发库(来源于Erlang),由于之前并没有Akka的使用经验,因此借此机会好好的学习了一下Actor模式的并发是如何实现的,以及在Java中如何去使用Scala编写的库,或者说如何进行Java和Scala的混合编程。
- 最重要的一点就是发现Flink中蕴含了很多优秀的设计思想,可以说是集很多年来分布式系统领域发展的大成,基本是流处理框架的巅峰之作。
- 还有很多很多小的收获,例如学到使用Java如何实现元组,Either这样的在函数式编程语言(Scala、Haskell、OCaml)中才有的数据结构,如何正确的使用Executor管理线程池……
我在发现一个微小的拼写错误并提交修复以后,代码很快就得到了合并。说明Flink社区非常有活力,发展速度非常快,未来大有可期,非常值得认真学习并使用。
往期文章推荐: