first commit
This commit is contained in:
135
专栏/大规模数据处理实战/00开篇词从这里开始,带你走上硅谷一线系统架构师之路.md
Normal file
135
专栏/大规模数据处理实战/00开篇词从这里开始,带你走上硅谷一线系统架构师之路.md
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 从这里开始,带你走上硅谷一线系统架构师之路
|
||||
你好,我是蔡元楠。目前是Google Brain的软件工程师。
|
||||
|
||||
在接下来的4个月时间里,我会与你一起探索大规模数据处理的世界。
|
||||
|
||||
在开始我们的系统性学习之前,我想先和你分享两个我亲历的故事,借此告诉你,我为什么要开这个专栏。
|
||||
|
||||
2014年,我刚开始在美国找工作,在一次面试中,面试官让我解释一下C++的smart pointer和string view。我完全回答不上来。
|
||||
|
||||
当时我面露难色怀疑人生,难道我的前半生学的是“假的”C++吗?
|
||||
|
||||
回想学习经历,虽然我在一个还可以的高校里学习了C++课程,考试成绩也是90分以上。但我的学习资料只有按C++ 98标准编写的教材,和当时流行的CSDN论坛文章。
|
||||
|
||||
而当时的教材里根本没有提到过smart pointer和string view。
|
||||
|
||||
没想到工业界早就进入了C++ 0x/11标准甚至是C++ 17(当时是试验标准)。我后悔没有学习紧跟时代的最新技术,而信息的不对称就会造成巨大的认知偏差。
|
||||
|
||||
那学习“最新技术”一定就是好事情吗?我想通过第二个故事来回答这个问题。
|
||||
|
||||
2017年,我帮Google Ventures(Google的风险投资基金,会寻找并帮助优秀初创公司)在投的初创公司做导师,那时候经常参加一些对方公司的技术架构评审。
|
||||
|
||||
一次评审中,对方的技术VP眉飞色舞地介绍他们的技术框架和××大厂一样,罗列了Kylin和Tornado等一些时髦的技术名词。
|
||||
|
||||
因为我并不了解提到的几个技术,就好奇地问为什么Kylin适合他们团队,Kylin为他们解决了哪些独特的问题?
|
||||
|
||||
他当时的回答并没能说服我和别的同事:“因为××大厂也在用,这就是未来的技术方向”。
|
||||
|
||||
看得出来,这位技术VP还没有真的搞清楚使用一个技术的原因。
|
||||
|
||||
学会用一个技术只是第一步,最重要的是要追问自己:
|
||||
|
||||
|
||||
这个技术解决了哪些痛点?
|
||||
别的技术为什么不能解决?
|
||||
这个技术用怎样的方法解决问题?
|
||||
采用这个技术真的是最好的方法吗?
|
||||
如果不用这个技术,你会怎样独立解决这类问题?
|
||||
|
||||
|
||||
如果没有这些深层次的思考,你就永远只是在赶技术的时髦而已,不会拥有影响他人的技术领导力。
|
||||
|
||||
事实上在Google,类似这样的“灵魂追问”每天都在发生。
|
||||
|
||||
这里敢于打碎任何权威,所有的技术设计都是从问题出发。每一个工程师都会独立思考究竟什么是最佳方案,而不是照搬现有结论。
|
||||
|
||||
正如乔布斯所说,过去的点最终会连成线。这两个经历让我深感有使命去帮助更多技术同行,比如第一个故事里的我,或者是第二个故事里的初创公司VP。我想设计一个专栏,去解决故事里的问题。
|
||||
|
||||
第一,我想要介绍硅谷最前沿技术和真实的案例。
|
||||
|
||||
比如,在大规模数据处理领域,MapReduce或者Apache Storm的不少设计理念已经无法胜任最新的挑战。
|
||||
|
||||
所以,我会介绍最新的知识,例如框架层面的前后端分离理念,和批处理流处理统一的思想。
|
||||
|
||||
第二,我不想只停留在照本宣科的层面。
|
||||
|
||||
正如上文所说,学会用一个或者两个技术是不够的。
|
||||
|
||||
更重要的是,我会剖析技术框架产生的原因和它们解决的问题。这样当下一次你再碰到相似的问题时,就不用照搬别人的方法。
|
||||
|
||||
为什么写大规模数据处理?
|
||||
|
||||
为什么会选择大规模数据处理这个主题呢?并不是因为我觉得这个主题受众多、销量好,相反,我认为大部分人都还没有正确理解数据处理技术,常常见到的误区有如下几种。
|
||||
|
||||
第一,低估了数据处理的重要性。
|
||||
|
||||
因为我在Google Brain的AI应用领域工作,切身感受到过,没有高质量的数据处理的话,人工智能是只有人工没有智能的。
|
||||
|
||||
Google也曾在很长的一段时间里低估过数据处理。
|
||||
|
||||
例如,在语义理解上,Google认为自己有最多的搜索文本数据,最好的算法,那就一定能把语义理解做的最好。
|
||||
|
||||
可是到2016年左右,一个名不见经传的德国小公司却一举超过了Google,大家都很惊讶。后来发现原来他们凭借的是高质量的数据标注和处理。
|
||||
|
||||
第二,低估了数据处理工程师在组织架构上的重要性。
|
||||
|
||||
许多工程师都喜欢自嘲自己的工作是“搬砖”,事实也正是如此。
|
||||
|
||||
包括我在内,很多人的工作内容都避不开数据的搬运和处理。把数据从这个格式处理成那个格式,把数据从这个数据库搬到那个数据库,这个服务器搬到那个服务器,这个客户端搬到那个客户端。
|
||||
|
||||
可能连你自己都还没有意识到,即使是一个写前端的工程师,他的很多工作还是数据处理。
|
||||
|
||||
大数据领域泰斗级人物Jesse Anderson曾做过一项研究,一个人工智能团队的合理组织架构,需要4/5的数据处理工程师。很不幸,很多团队没有认识到这一点。
|
||||
|
||||
第三,低估了数据处理规模变大带来的复杂度。
|
||||
|
||||
我把这个专栏定位在“大规模”数据处理,因为我想着重在数据规模变大时需要的技术思想。
|
||||
|
||||
很多人可能还没有遇到过“大规模”数据的问题,容易把问题想简单了。
|
||||
|
||||
我在Google面试过很多优秀的候选人,应对普通的编程问题,他们能够用算法和数据结构解决得很好。可是当我追问数据规模变大时怎么设计系统,他们的回答却并不让人满意。
|
||||
|
||||
当你的产品从1万用户到1亿用户,技术团队从10个人到1000个人,之前的方法还能奏效吗?
|
||||
|
||||
第四,高估了上手数据处理的难度。
|
||||
|
||||
一方面我们需要认识到大规模的数据处理是有复杂的因素的。但另一方面,我想在这个专栏里教会你,有了正确的工具和技术理念,现在上手数据处理并不困难。
|
||||
|
||||
在Google,我见到很多应届生来了半年后也能轻松应对上亿的数据量。
|
||||
|
||||
我给开篇词起名为《从这里开始,带你走上硅谷一线系统架构师之路》,就是为了给你设计切实可操作的学习路径,让你比别人更准确深入地掌握实用的大规模数据处理技术,最终通往硅谷一线系统架构师的水平。
|
||||
|
||||
因此,我们的学习路径会是这样的。
|
||||
|
||||
第一部分,先会用原汁原味最实际的硅谷一线大厂的案例,向你解释MapReduce为什么不能应对最新的技术挑战。然后我会从实际的问题出发,从头开始引导你怎样从顶层设计一个数据处理框架。
|
||||
|
||||
第二部分,同样是结合实战案例,来讲解在数据处理框架的使用和设计中必需的一些基础知识。这些案例紧贴应用,可能就是你的团队明天会碰到的问题。
|
||||
|
||||
第三、第四部分深入拆解了Apache Spark和Apache Beam。不仅会用实际的案例教会你如何使用,还要教会你为什么它们这么设计。你会发现它们的设计其实大致和第一部分的顶层设计是一致的。下一次,即使这个世界一无所有,你也能构建类似的框架解决一系列问题。
|
||||
|
||||
第五部分按Google T6级别设计,是带着代码的真枪实弹的架构设计。毫不夸张地说,能完整掌握第五部分的思想精髓,你就能比肩硅谷一线大规模数据处理架构师。
|
||||
|
||||
第六部分着重培养你的技术远见。因为,是否能现在就开始准备应对10年后人类社会的技术挑战,是你拉开与别人差距的重要一站。
|
||||
|
||||
|
||||
|
||||
在刚开始动笔写专栏的时候,我就在设想,什么样的人会是这个专栏的目标读者呢?
|
||||
|
||||
直到专栏快上线,我写下这篇开篇词,我才真正定义读者的标签——应该就是跟我一样渴望成长的人。是的,我和你一样,都渴望成长——渴望知识的成长,渴望经验的成长,渴望财富的成长。
|
||||
|
||||
所以我想把这个专栏设计成一份共同的成长规划,而不是一本死板的教材。
|
||||
|
||||
正如我在开头的小故事里所说的,这个世界没有谁是绝对的权威。
|
||||
|
||||
我希望你每一期都能在留言栏里质疑、提问和讨论。这些互动能帮助我和别的同学一起提高。
|
||||
|
||||
最后,我期待和你一起开始学习,共同成长!
|
||||
|
||||
|
||||
|
||||
|
189
专栏/大规模数据处理实战/01为什么MapReduce会被硅谷一线公司淘汰?.md
Normal file
189
专栏/大规模数据处理实战/01为什么MapReduce会被硅谷一线公司淘汰?.md
Normal file
@@ -0,0 +1,189 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 为什么MapReduce会被硅谷一线公司淘汰?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“为什么MapReduce会被硅谷一线公司淘汰”。
|
||||
|
||||
我有幸几次与来Google参观的同行进行交流,当谈起数据处理技术时,他们总是试图打探MapReduce方面的经验。
|
||||
|
||||
这一点让我颇感惊讶,因为在硅谷,早已没有人去谈论MapReduce了。
|
||||
|
||||
今天这一讲,我们就来聊聊为什么MapReduce会被硅谷一线公司淘汰。
|
||||
|
||||
我们先来沿着时间线看一下超大规模数据处理的重要技术以及它们产生的年代。
|
||||
|
||||
|
||||
|
||||
我认为可以把超大规模数据处理的技术发展分为三个阶段:石器时代,青铜时代,蒸汽机时代。
|
||||
|
||||
石器时代
|
||||
|
||||
我用“石器时代”来比喻MapReduce诞生之前的时期。
|
||||
|
||||
数据的大规模处理问题早已存在。早在2003年的时候,Google就已经面对大于600亿的搜索量。
|
||||
|
||||
但是数据的大规模处理技术还处在彷徨阶段。当时每个公司或者个人可能都有自己的一套工具处理数据。却没有提炼抽象出一个系统的方法。
|
||||
|
||||
青铜时代
|
||||
|
||||
2003年,MapReduce的诞生标志了超大规模数据处理的第一次革命,而开创这段青铜时代的就是下面这篇论文《MapReduce: Simplified Data Processing on Large Clusters》。
|
||||
|
||||
|
||||
|
||||
杰夫(Jeff Dean)和桑杰(Sanjay Ghemawat)从纷繁复杂的业务逻辑中,为我们抽象出了Map和Reduce这样足够通用的编程模型。后面的Hadoop仅仅是对于GFS、BigTable、MapReduce 的依葫芦画瓢,我这里不再赘述。
|
||||
|
||||
蒸汽机时代
|
||||
|
||||
到了2014年左右,Google内部已经几乎没人写新的MapReduce了。
|
||||
|
||||
2016年开始,Google在新员工的培训中把MapReduce替换成了内部称为FlumeJava(不要和Apache Flume混淆,是两个技术)的数据处理技术。
|
||||
|
||||
这标志着青铜时代的终结,同时也标志着蒸汽机时代的开始。
|
||||
|
||||
我跳过“铁器时代”之类的描述,是因为只有工业革命的概念才能解释从MapReduce进化到FlumeJava的划时代意义。
|
||||
|
||||
Google内部的FlumeJava和它后来的开源版本Apache Beam所引进的统一的编程模式,将在后面的章节中为你深入解析。
|
||||
|
||||
现在你可能有一个疑问 :为什么MapReduce会被取代?今天我将重点为你解答。
|
||||
|
||||
高昂的维护成本
|
||||
|
||||
使用MapReduce,你需要严格地遵循分步的Map和Reduce步骤。当你构造更为复杂的处理架构时,往往需要协调多个Map和多个Reduce任务。
|
||||
|
||||
然而,每一步的MapReduce都有可能出错。
|
||||
|
||||
为了这些异常处理,很多人开始设计自己的协调系统(orchestration)。例如,做一个状态机(state machine)协调多个MapReduce,这大大增加了整个系统的复杂度。
|
||||
|
||||
如果你搜 “MapReduce orchestration” 这样的关键词,就会发现有很多书,整整一本都在写怎样协调MapReduce。
|
||||
|
||||
你可能会惊讶于MapReduce的复杂度。我也经常会看到一些把MapReduce说得过度简单的误导性文章。
|
||||
|
||||
例如,“把海量的××数据通过MapReduce导入大数据系统学习,就能产生××人工智能”。似乎写文的“专家”动动嘴就能点石成金。
|
||||
|
||||
而现实的MapReduce系统的复杂度是超过了“伪专家”的认知范围的。下面我来举个例子,告诉你MapReduce有多复杂。
|
||||
|
||||
想象一下这个情景,你的公司要预测美团的股价,其中一个重要特征是活跃在街头的美团外卖电动车数量,而你负责处理所有美团外卖电动车的图片。
|
||||
|
||||
在真实的商用环境下,为了解决这个问题,你可能至少需要10个MapReduce任务:
|
||||
|
||||
|
||||
|
||||
首先,我们需要搜集每日的外卖电动车图片。
|
||||
|
||||
数据的搜集往往不全部是公司独自完成,许多公司会选择部分外包或者众包。所以在数据搜集(Data collection)部分,你至少需要4个MapReduce任务:
|
||||
|
||||
|
||||
数据导入(data ingestion):用来把散落的照片(比如众包公司上传到网盘的照片)下载到你的存储系统。
|
||||
|
||||
数据统一化(data normalization):用来把不同外包公司提供过来的各式各样的照片进行格式统一。
|
||||
|
||||
数据压缩(compression):你需要在质量可接受的范围内保持最小的存储资源消耗 。
|
||||
|
||||
数据备份(backup):大规模的数据处理系统我们都需要一定的数据冗余来降低风险。
|
||||
|
||||
|
||||
仅仅是做完数据搜集这一步,离真正的业务应用还差得远。
|
||||
|
||||
真实的世界是如此不完美,我们需要一部分数据质量控制(quality control)流程,比如:
|
||||
|
||||
|
||||
数据时间有效性验证 (date validation):检测上传的图片是否是你想要的日期的。
|
||||
|
||||
照片对焦检测(focus detection):你需要筛选掉那些因对焦不准而无法使用的照片。
|
||||
|
||||
|
||||
最后才到你负责的重头戏——找到这些图片里的外卖电动车。而这一步因为人工的介入是最难控制时间的。你需要做4步:
|
||||
|
||||
|
||||
数据标注问题上传(question uploading):上传你的标注工具,让你的标注者开始工作。
|
||||
|
||||
标注结果下载(answer downloading):抓取标注完的数据。
|
||||
|
||||
标注异议整合(adjudication):标注异议经常发生,比如一个标注者认为是美团外卖电动车,另一个标注者认为是京东快递电动车。
|
||||
|
||||
标注结果结构化(structuralization): 要让标注结果可用,你需要把可能非结构化的标注结果转化成你的存储系统接受的结构。
|
||||
|
||||
|
||||
这里我不再深入每个MapReduce任务的技术细节,因为本章的重点仅仅是理解MapReduce的复杂度。
|
||||
|
||||
通过这个案例,我想要阐述的观点是,因为真实的商业MapReduce场景极端复杂,像上面这样10个子任务的MapReduce系统在硅谷一线公司司空见惯。
|
||||
|
||||
在应用过程中,每一个MapReduce任务都有可能出错,都需要重试和异常处理的机制。所以,协调这些子MapReduce的任务往往需要和业务逻辑紧密耦合的状态机。
|
||||
|
||||
这样过于复杂的维护让系统开发者苦不堪言。
|
||||
|
||||
时间性能“达不到”用户的期待
|
||||
|
||||
除了高昂的维护成本,MapReduce的时间性能也是个棘手的问题。
|
||||
|
||||
MapReduce是一套如此精巧复杂的系统,如果使用得当,它是青龙偃月刀,如果使用不当,它就是一堆废铁。不幸的是并不是每个人都是关羽。
|
||||
|
||||
在实际的工作中,不是每个人都对MapReduce细微的配置细节了如指掌。
|
||||
|
||||
在现实中,业务往往需求一个刚毕业的新手在3个月内上线一套数据处理系统,而他很可能从来没有用过MapReduce。这种情况下开发的系统是很难发挥好MapReduce的性能的。
|
||||
|
||||
你一定想问,MapReduce的性能优化配置究竟复杂在哪里呢?
|
||||
|
||||
我想Google500多页的MapReduce性能优化手册足够说明它的复杂度了。这里我举例讲讲MapReduce的分片(sharding)难题,希望能窥斑见豹,引发大家的思考。
|
||||
|
||||
Google曾经在2007年到2012年间做过一个对于1PB数据的大规模排序实验,来测试MapReduce的性能。
|
||||
|
||||
从2007年的排序时间12小时,到2012年的排序时间缩短至0.5小时。即使是Google,也花了5年的时间才不断优化了一个MapReduce流程的效率。
|
||||
|
||||
2011年,他们在Google Research的博客上公布了初步的成果。
|
||||
|
||||
|
||||
|
||||
其中有一个重要的发现,就是他们在MapReduce的性能配置上花了非常多的时间。包括了缓冲大小(buffer size),分片多少(number of shards),预抓取策略(prefetch),缓存大小(cache size)等等。
|
||||
|
||||
所谓的分片,是指把大规模的的数据分配给不同的机器/工人,流程如下图所示。
|
||||
|
||||
|
||||
|
||||
选择一个好的分片函数(sharding function)为何格外重要?让我们来看一个例子。
|
||||
|
||||
假如你在处理Facebook的所有用户数据,你选择了按照用户的年龄作为分片函数(sharding function)。我们来看看这时候会发生什么。
|
||||
|
||||
因为用户的年龄分布不均衡(假如在20~30这个年龄段的Facebook用户最多),导致我们在下图中worker C上分配到的任务远大于别的机器上的任务量。
|
||||
|
||||
|
||||
|
||||
这时候就会发生掉队者问题(stragglers)。别的机器都完成了Reduce阶段,只有worker C还在工作。
|
||||
|
||||
当然它也有改进方法。掉队者问题可以通过MapReduce的性能剖析(profiling)发现。 如下图所示,箭头处就是掉队的机器。
|
||||
|
||||
|
||||
|
||||
图片引用:Chen, Qi, Cheng Liu, and Zhen Xiao. “Improving MapReduce performance using smart speculative execution strategy.” IEEE Transactions on Computers 63.4 (2014): 954-967.
|
||||
|
||||
回到刚刚的Google大规模排序实验。
|
||||
|
||||
因为MapReduce的分片配置异常复杂,在2008年以后,Google改进了MapReduce的分片功能,引进了动态分片技术 (dynamic sharding),大大简化了使用者对于分片的手工调整。
|
||||
|
||||
在这之后,包括动态分片技术在内的各种崭新思想被逐渐引进,奠定了下一代大规模数据处理技术的雏型。
|
||||
|
||||
小结
|
||||
|
||||
这一讲中,我们分析了两个MapReduce之所以被硅谷一线公司淘汰的“致命伤”:高昂的维护成本和达不到用户期待的时间性能。
|
||||
|
||||
文中也提到了下一代数据处理技术雏型。这就是2008年左右在Google西雅图研发中心诞生的FlumeJava,它一举解决了上面MapReduce的短板。
|
||||
|
||||
另外,它还带来了一些别的优点:更好的可测试性;更好的可监控性;从1条数据到1亿条数据无缝扩展,不需要修改一行代码,等等。
|
||||
|
||||
在后面的章节中,我们将具体展开这几点,通过深入解析Apache Beam(FlumeJava的开源版本),揭开MapReduce继任者的神秘面纱。
|
||||
|
||||
思考题
|
||||
|
||||
如果你在Facebook负责处理例子中的用户数据,你会选择什么分片函数,来保证均匀分布的数据分片?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起探讨。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
147
专栏/大规模数据处理实战/02MapReduce后谁主沉浮:怎样设计下一代数据处理技术?.md
Normal file
147
专栏/大规模数据处理实战/02MapReduce后谁主沉浮:怎样设计下一代数据处理技术?.md
Normal file
@@ -0,0 +1,147 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02 MapReduce后谁主沉浮:怎样设计下一代数据处理技术?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
在上一讲中,我们介绍了2014年之前的大数据历史,也就是MapReduce作为数据处理的默认标准的时代。重点探讨了MapReduce面对日益复杂的业务逻辑时表现出的不足之处,那就是:1. 维护成本高;2. 时间性能不足。
|
||||
|
||||
同时,我们也提到了2008年诞生在Google西雅图研发中心的FlumeJava,它成为了Google内部的数据处理新宠。
|
||||
|
||||
那么,为什么是它扛起了继任MapReduce的大旗呢?
|
||||
|
||||
要知道,在包括Google在内的硅谷一线大厂,对于内部技术选择是非常严格的,一个能成为默认方案的技术至少满足以下条件:
|
||||
|
||||
|
||||
经受了众多产品线,超大规模数据量例如亿级用户的考验;
|
||||
|
||||
自发地被众多内部开发者采用,简单易用而受开发者欢迎;
|
||||
|
||||
能通过内部领域内专家的评审;
|
||||
|
||||
比上一代技术仅仅提高10%是不够的,必须要有显著的比如70%的提高,才能够说服整个公司付出技术迁移的高昂代价。就看看从Python 2.7到Python 3的升级花了多少年了,就知道在大厂迁移技术是异常艰难的。
|
||||
|
||||
|
||||
今天这一讲,我不展开讲任何具体技术。
|
||||
|
||||
我想先和你一起设想一下,假如我和你站在2008年的春夏之交,在已经清楚了MapReduce的现有问题的情况下,我们会怎么设计下一代大规模数据处理技术,带领下一个十年的技术革新呢?
|
||||
|
||||
我们需要一种技术抽象让多步骤数据处理变得易于维护
|
||||
|
||||
上一讲中我提到过,维护协调多个步骤的数据处理在业务中非常常见。
|
||||
|
||||
|
||||
|
||||
像图片中这样复杂的数据处理在MapReduce中维护起来令人苦不堪言。
|
||||
|
||||
为了解决这个问题,作为架构师的我们或许可以用有向无环图(DAG)来抽象表达。因为有向图能为多个步骤的数据处理依赖关系,建立很好的模型。如果你对图论比较陌生的话,可能现在不知道我在说什么,你可以看下面一个例子,或者复习一下极客时间的《数据结构与算法之美》。
|
||||
|
||||
|
||||
|
||||
西红柿炒鸡蛋这样一个菜,就是一个有向无环图概念的典型案例。
|
||||
|
||||
比如看这里面番茄的处理,最后一步“炒”的步骤依赖于切好的番茄、打好的蛋、热好的油。而切好的番茄又依赖于洗好的番茄等等。如果用MapReduce来实现的话,在这个图里面,每一个箭头都会是一个独立的Map或Reduce。
|
||||
|
||||
为了协调那么多Map和Reduce,你又难以避免会去做很多检查,比如:番茄是不是洗好了,鸡蛋是不是打好了。
|
||||
|
||||
最后这个系统就不堪重负了。
|
||||
|
||||
但是,如果我们用有向图建模,图中的每一个节点都可以被抽象地表达成一种通用的数据集,每一条边都被表达成一种通用的数据变换。如此,你就可以用数据集和数据变换描述极为宏大复杂的数据处理流程,而不会迷失在依赖关系中无法自拔。
|
||||
|
||||
我们不想要复杂的配置,需要能自动进行性能优化
|
||||
|
||||
上一讲中提到,MapReduce的另一个问题是,配置太复杂了。以至于错误的配置最终导致数据处理任务效率低下。
|
||||
|
||||
这种问题怎么解决呢?很自然的思路就是,如果人容易犯错,就让人少做一点,让机器多做一点呗。
|
||||
|
||||
我们已经知道了,得益于上一步中我们已经用有向图对数据处理进行了高度抽象。这可能就能成为我们进行自动性能优化的一个突破口。
|
||||
|
||||
回到刚才的番茄炒鸡蛋例子,哪些情况我们需要自动优化呢?
|
||||
|
||||
设想一下,如果我们的数据处理食谱上又增加了番茄牛腩的需求,用户的数据处理有向图就变成了这个样子了。
|
||||
|
||||
|
||||
|
||||
理想的情况下,我们的计算引擎要能够自动发现红框中的两条数据处理流程是重复的。它要能把两条数据处理过程进行合并。这样的话,番茄就不会被重复准备了。
|
||||
|
||||
同样的,如果需求突然不再需要番茄炒蛋了,只需要番茄牛腩,在数据流水线的预处理部分也应该把一些无关的数据操作优化掉,比如整个鸡蛋的处理过程就不应该在运行时出现。
|
||||
|
||||
另一种自动的优化是计算资源的自动弹性分配。
|
||||
|
||||
比如,还是在番茄炒蛋这样一个数据处理流水线中,如果你的规模上来了,今天需要生产1吨的番茄炒蛋,明天需要生产10吨的番茄炒蛋。你发现有时候是处理1000个番茄,有时候又是10000个番茄。如果手动地去做资源配置的话,你再也配置不过来了。
|
||||
|
||||
我们的优化系统也要有可以处理这种问题的弹性的劳动力分配机制。它要能自动分配,比如100台机器处理1000个番茄,如果是10000个番茄那就分配1000台机器,但是只给热油1台机器可能就够了。
|
||||
|
||||
这里的比喻其实是很粗糙也不精准的。我想用这样两个例子表达的观点是,在数据处理开始前,我们需要有一个自动优化的步骤和能力,而不是按部就班地就把每一个步骤就直接扔给机器去执行了。
|
||||
|
||||
我们要能把数据处理的描述语言,与背后的运行引擎解耦合开来
|
||||
|
||||
前面两个设计思路提到了很重要的一个设计就是有向图。
|
||||
|
||||
用有向图进行数据处理描述的话,实际上数据处理描述语言部分完全可以和后面的运算引擎分离了。有向图可以作为数据处理描述语言和运算引擎的前后端分离协议。
|
||||
|
||||
举两个你熟悉的例子可能能更好理解我这里所说的前后端分离(client-server design)是什么意思:
|
||||
|
||||
比如一个网站的架构中,服务器和网页通过HTTP协议通信。
|
||||
|
||||
|
||||
|
||||
比如在TensorFlow的设计中,客户端可以用任何语言(比如Python或者C++)描述计算图,运行时引擎(runtime) 理论上却可以在任何地方具体运行,比如在本地,在CPU,或者在TPU。
|
||||
|
||||
|
||||
|
||||
那么我们设计的数据处理技术也是一样的,除了有向图表达需要数据处理描述语言和运算引擎协商一致,其他的实现都是灵活可拓展的。
|
||||
|
||||
比如,我的数据描述可以用Python描述,由业务团队使用;计算引擎用C++实现,可以由数据底层架构团队维护并且高度优化;或者我的数据描述在本地写,计算引擎在云端执行。
|
||||
|
||||
|
||||
|
||||
我们要统一批处理和流处理的编程模型
|
||||
|
||||
关于什么是批处理和流处理概念会在后面的章节展开。这里先简单解释下,批处理处理的是有界离散的数据,比如处理一个文本文件;流处理处理的是无界连续的数据,比如每时每刻的支付宝交易数据。
|
||||
|
||||
MapReduce的一个局限是它为了批处理而设计的,应对流处理的时候不再那么得心应手。即使后面的Apache Storm、Apache Flink也都有类似的问题,比如Flink里的批处理数据结构用DataSet,但是流处理用DataStream。
|
||||
|
||||
但是真正的业务系统,批处理和流处理是常常混合共生,或者频繁变换的。
|
||||
|
||||
比如,你有A、B两个数据提供商。其中数据提供商A与你签订的是一次性的数据协议,一次性给你一大波数据,你可以用批处理。而数据提供商B是实时地给你数据,你又得用流处理。更可怕的事情发生了,本来是批处理的数据提供商A,突然把协议修改了,现在他们实时更新数据。这时候你要是用Flink就得爆炸了。业务需求天天改,还让不让人活了?!
|
||||
|
||||
因此,我们设计的数据处理框架里,就得有更高层级的数据抽象。
|
||||
|
||||
不论是批处理还是流处理的,都用统一的数据结构表示。编程的API也需要统一。这样不论业务需求什么样,开发者只需要学习一套API。即使业务需求改变,开发者也不需要频繁修改代码。
|
||||
|
||||
我们要在架构层面提供异常处理和数据监控的能力
|
||||
|
||||
真正写过大规模数据处理系统的人都深有感触:在一个复杂的数据处理系统中,难的不是开发系统,而是异常处理。
|
||||
|
||||
事实正是如此。一个Google内部调研表明,在大规模的数据处理系统中,90%的时间都花在了异常处理中。常常发生的问题的是,比如在之前的番茄炒鸡蛋处理问题中,你看着系统log,明明买了1000个鸡蛋,炒出来的菜却看起来只有999个鸡蛋,你仰天长叹,少了一个蛋到底去哪里了!
|
||||
|
||||
这一点和普通的软件开发不同。比如,服务器开发中,偶尔一个RPC请求丢了就丢了,重试一下,重启一下能过就行了。可如果在数据处理系统中,数据就是钱啊,不能随便丢。比如我们的鸡蛋,都是真金白银买回来的。是超市买回来数错了?是打蛋时候打碎了?还是被谁偷吃了?你总得给老板一个合理的交代。
|
||||
|
||||
我们要设计一套基本的数据监控能力,对于数据处理的每一步提供自动的监控平台,比如一个监控网站。
|
||||
|
||||
在番茄炒蛋系统中,要能够自动的记录下来,超市买回来是多少个蛋,打蛋前是多少个蛋,打完蛋是多少个蛋,放进锅里前是多少个蛋等等。也需要把每一步的相关信息进行存储,比如是谁去买的蛋,哪些人打蛋。这样出错后可以帮助用户快速找到可能出错的环节。
|
||||
|
||||
小结
|
||||
|
||||
通过上面的分析,我们可以总结一下。如果是我们站在2008年春夏之交来设计下一代大规模数据处理框架,一个基本的模型会是图中这样子的:
|
||||
|
||||
|
||||
|
||||
但是这样粗糙的设计和思想实验离实现还是太远。你可能还是会感到无从下手。
|
||||
|
||||
后面的章节会给你补充一些设计和使用大规模数据处理架构的基础知识。同时,也会深入剖析两个与我们这里的设计理念最接近的大数据处理框架,Apache Spark和Apache Beam。
|
||||
|
||||
思考题
|
||||
|
||||
你现在在使用的数据处理技术有什么问题,你有怎样的改进设计?
|
||||
|
||||
欢迎你把自己的想法写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
204
专栏/大规模数据处理实战/03大规模数据处理初体验:怎样实现大型电商热销榜?.md
Normal file
204
专栏/大规模数据处理实战/03大规模数据处理初体验:怎样实现大型电商热销榜?.md
Normal file
@@ -0,0 +1,204 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
03 大规模数据处理初体验:怎样实现大型电商热销榜?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“怎样实现大型电商热销榜”。
|
||||
|
||||
我在Google面试过很多优秀的候选人,应对普通的编程问题coding能力很强,算法数据结构也应用得不错。
|
||||
|
||||
可是当我追问数据规模变大时该怎么设计系统,他们却说不出所以然来。这说明他们缺乏必备的规模增长的技术思维(mindset of scaling)。这会限制这些候选人的职业成长。
|
||||
|
||||
因为产品从1万用户到1亿用户,技术团队从10个人到1000个人,你的技术规模和数据规模都会完全不一样。
|
||||
|
||||
今天我们就以大型电商热销榜为例,来谈一谈从1万用户到1亿用户,从GB数据到PB数据系统,技术思维需要怎样的转型升级?
|
||||
|
||||
同样的问题举一反三,可以应用在淘宝热卖,App排行榜,抖音热门,甚至是胡润百富榜,因为实际上他们背后都应用了相似的大规模数据处理技术。
|
||||
|
||||
|
||||
|
||||
真正的排序系统非常复杂,仅仅是用来排序的特征(features)就需要多年的迭代设计。
|
||||
|
||||
为了便于这一讲的讨论,我们来构想一个简化的玩具问题,来帮助你理解。
|
||||
|
||||
假设你的电商网站销售10亿件商品,已经跟踪了网站的销售记录:商品id和购买时间 {product_id, timestamp},整个交易记录是1000亿行数据,TB级。作为技术负责人,你会怎样设计一个系统,根据销售记录统计去年销量前10的商品呢?
|
||||
|
||||
举个例子,假设我们的数据是:
|
||||
|
||||
|
||||
|
||||
我们可以把热销榜按 product_id 排名为:1, 2, 3。
|
||||
|
||||
小规模的经典算法
|
||||
|
||||
如果上过极客时间的《数据结构与算法之美》,你可能一眼就看出来,这个问题的解法分为两步:
|
||||
|
||||
|
||||
|
||||
第一步,统计每个商品的销量。你可以用哈希表(hashtable)数据结构来解决,是一个O(n)的算法,这里n是1000亿。
|
||||
|
||||
第二步,找出销量前十,可以用经典的Top K算法,也是O(n)的算法。
|
||||
|
||||
如果你考虑到了这些,先恭喜你答对了。
|
||||
|
||||
在小规模系统中,我们确实完全可以用经典的算法简洁漂亮地解决。以Python编程的话可能是类似这样的:
|
||||
|
||||
def CountSales(sale_records):
|
||||
"""Calculate number of sales for each product id.
|
||||
|
||||
Args:
|
||||
sales_records: list of SaleRecord, SaleRecord is a named tuple,
|
||||
e.g. {product_id: “1”, timestamp: 1553721167}.
|
||||
Returns:
|
||||
dict of {product_id: num_of_sales}. E.g. {“1”: 1, “2”: 1}
|
||||
"""
|
||||
sales_count = {}
|
||||
for record in sale_records:
|
||||
sales_count[record[product_id]] += 1
|
||||
|
||||
return sales_count
|
||||
|
||||
def TopSellingItems(sale_records, k=10):
|
||||
"""Calculate the best selling k products.
|
||||
|
||||
Args:
|
||||
sales_records: list of SaleRecord, SaleRecord is a named tuple,
|
||||
e.g. {product_id: “1”, timestamp: 1553721167}.
|
||||
K: num of top products you want to output.
|
||||
Returns:
|
||||
List of k product_id, sorted by num of sales.
|
||||
"""
|
||||
sales_count = CountSales(sale_records)
|
||||
return heapq.nlargest(k, sales_count, key=sales_count.get)
|
||||
|
||||
|
||||
但在一切系统中,随着尺度的变大,很多方法就不再适用。
|
||||
|
||||
比如,在小尺度经典物理学中适用的牛顿力学公式是这样的:
|
||||
|
||||
|
||||
|
||||
这在高速强力的物理系统中就不再适用,在狭义相对论中有另外的表达。
|
||||
|
||||
|
||||
|
||||
在社会系统中也是一样,管理10人团队,和治理14亿人口的国家,复杂度也不可同日而语。
|
||||
|
||||
具体在我们这个问题中,同样的Top K算法当数据规模变大会遇到哪些问题呢?
|
||||
|
||||
第一,内存占用。
|
||||
|
||||
对于TB级的交易记录数据,很难找到单台计算机容纳那么大的哈希表了。你可能想到,那我不要用哈希表去统计商品销售量了,我把销量计数放在磁盘里完成好了。
|
||||
|
||||
比如,就用一个1000亿行的文件或者表,然后再把销量统计结果一行一行读进后面的堆树/优先级队列。理论上听起来不错,实际上是否真的可行呢,那我们看下一点。
|
||||
|
||||
第二,磁盘I/O等延时问题。
|
||||
|
||||
当数据规模变大,我们难以避免地需要把一些中间结果存进磁盘,以应对单步任务出错等问题。一次磁盘读取大概需要10ms的时间。
|
||||
|
||||
如果按照上一点提到的文件替代方法,因为我们是一个O(n * log k)的算法,就需要10ms * 10^9 = 10 ^ 7 s = 115 天的时间。你可能需要贾跃亭附体,才能忽悠老板接受这样的设计方案了。
|
||||
|
||||
这些问题怎么解决呢?你可能已经想到,当单台机器已经无法适应我们数据或者问题的规模,我们需要横向扩展。
|
||||
|
||||
大规模分布式解决方案
|
||||
|
||||
之前的思路依然没错。但是,我们需要把每一步从简单的函数算法,升级为计算集群的分布式算法。
|
||||
|
||||
|
||||
|
||||
统计每个商品的销量
|
||||
|
||||
我们需要的第一个计算集群,就是统计商品销量的集群。
|
||||
|
||||
例如,1000台机器,每台机器一次可以处理1万条销售记录。对于每台机器而言,它的单次处理又回归到了我们熟悉的传统算法,数据规模大大缩小。
|
||||
|
||||
下图就是一个例子,图中每台机器输入是2条销售记录,输出是对于他们的本地输入而言的产品销量计数。
|
||||
|
||||
|
||||
|
||||
找出销量前K
|
||||
|
||||
我们需要的第二个计算集群,则是找出销量前十的集群。
|
||||
|
||||
这里我们不妨把问题抽象一下,抽象出是销量前K的产品。因为你的老板随时可能把产品需求改成前20销量,而不是前10了。
|
||||
|
||||
在上一个统计销量集群得到的数据输出,将会是我们这个处理流程的输入。所以这里需要把分布在各个机器分散的产品销量汇总出来。例如,把所有product_id = 1的销量全部叠加。
|
||||
|
||||
下图示例是K = 1的情况,每台机器先把所有product_id = 1的销量叠加在了一起,再找出自己机器上销量前K = 1的商品。可以看到对于每台机器而言,他们的输出就是最终排名前K = 1的商品候选者。
|
||||
|
||||
|
||||
|
||||
汇总最终结果
|
||||
|
||||
到了最后一步,你需要把在“销量前K集群”中的结果汇总出来。也就是说,从所有排名前K=1的商品候选者中找出真正的销量前K=1的商品。
|
||||
|
||||
这时候完全可以用单一机器解决了。因为实际上你汇总的就是这1000台机器的结果,规模足够小。
|
||||
|
||||
|
||||
|
||||
看到这里,你已经体会到处理超大规模数据的系统是很复杂的。
|
||||
|
||||
当你辛辛苦苦设计了应对1亿用户的数据处理系统时,可能你就要面临另一个维度的规模化(scaling)。那就是应用场景数量从1个变成1000个。每一次都为不同的应用场景单独设计分布式集群,招募新的工程师维护变得不再“可持续发展”。
|
||||
|
||||
这时,你需要一个数据处理的框架。
|
||||
|
||||
大规模数据处理框架的功能要求
|
||||
|
||||
在第二讲“MapReduce后谁主沉浮:怎样设计现代大规模数据处理技术”中,我们对于数据处理框架已经有了基本的方案。
|
||||
|
||||
今天这个实际的例子其实为我们的设计增加了新的挑战。
|
||||
|
||||
很多人面对问题,第一个想法是找有没有开源技术可以用一下。
|
||||
|
||||
但我经常说服别人不要先去看什么开源技术可以用,而是从自己面对的问题出发独立思考,忘掉MapReduce,忘掉Apache Spark,忘掉Apache Beam。
|
||||
|
||||
如果这个世界一无所有,你会设计怎样的大规模数据处理框架?你要经常做一些思维实验,试试带领一下技术的发展,而不是永远跟随别人的技术方向。
|
||||
|
||||
在我看来,两个最基本的需求是:
|
||||
|
||||
|
||||
高度抽象的数据处理流程描述语言。作为小白用户,我肯定再也不想一一配置分布式系统的每台机器了。作为框架使用者,我希望框架是非常简单的,能够用几行代码把业务逻辑描述清楚。
|
||||
|
||||
根据描述的数据处理流程,自动化的任务分配优化。这个框架背后的引擎需要足够智能,简单地说,要把那些本来手动配置的系统,进行自动任务分配。
|
||||
|
||||
|
||||
那么理想状况是什么?对于上面的应用场景,我作为用户只想写两行代码。
|
||||
|
||||
第一行代码:
|
||||
|
||||
sales_count = sale_records.Count()
|
||||
|
||||
|
||||
这样简单的描述,在我们框架设计层面,就要能自动构建成上文描述的“销量统计计算集群”。
|
||||
|
||||
第二行代码
|
||||
|
||||
top_k_sales = sales_count.TopK(k)
|
||||
|
||||
|
||||
这行代码需要自动构建成上文描述的“找出销量前K集群”。
|
||||
|
||||
看到这里,你能发现这并不复杂。我们到这里就已经基本上把现代大规模数据处理架构的顶层构造掌握了。而背后的具体实现,我会在后面的专栏章节中为你一一揭晓。
|
||||
|
||||
小结
|
||||
|
||||
这一讲中,我们粗浅地分析了一个电商排行榜的数据处理例子。
|
||||
|
||||
从GB数据到TB数据,我们从小规模算法升级到了分布式处理的设计方案;从单一TB数据场景到1000个应用场景,我们探索了大规模数据处理框架的设计。
|
||||
|
||||
这些都是为了帮助你更好地理解后面所要讲的所有知识。比如,为什么传统算法不再奏效?为什么要去借助抽象的数据处理描述语言?希望在后面的学习过程中,你能一直带着这些问题出发。
|
||||
|
||||
思考题
|
||||
|
||||
在你的工作中,有没有随着数据规模变大,系统出问题的情况,你又是怎么解决的?
|
||||
|
||||
欢迎你把自己的想法写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
139
专栏/大规模数据处理实战/04分布式系统(上):学会用服务等级协议SLA来评估你的系统.md
Normal file
139
专栏/大规模数据处理实战/04分布式系统(上):学会用服务等级协议SLA来评估你的系统.md
Normal file
@@ -0,0 +1,139 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
04 分布式系统(上):学会用服务等级协议SLA来评估你的系统
|
||||
你好,我是蔡元楠。
|
||||
|
||||
从今天开始,我们进入专栏的第二模块。通过这一模块的学习,带你一起夯实大规模数据处理的基础。
|
||||
|
||||
首先,我将结合硅谷顶尖科技公司的最佳实践 (Best Practice) ,和你一起分享在设计分布式系统架构时,我们有可能会碰到哪些雷区?又有哪些必备的基础知识?
|
||||
|
||||
在硅谷一线大厂所维护的系统服务中,我们经常可以看见SLA这样的承诺。
|
||||
|
||||
例如,在谷歌的云计算服务平台Google Cloud Platform中,他们会写着“99.9% Availability”这样的承诺。那什么是“99.9% Availability”呢?
|
||||
|
||||
要理解这个承诺是什么意思,首先,你需要了解到底什么是SLA?
|
||||
|
||||
SLA(Service-Level Agreement),也就是服务等级协议,指的是系统服务提供者(Provider)对客户(Customer)的一个服务承诺。这是衡量一个大型分布式系统是否“健康”的常见方法。
|
||||
|
||||
|
||||
|
||||
在开发设计系统服务的时候,无论面对的客户是公司外部的个人、商业用户,还是公司内的不同业务部门,我们都应该对自己所设计的系统服务有一个定义好的SLA。
|
||||
|
||||
因为SLA是一种服务承诺,所以指标可以多种多样。根据我的实践经验,给你介绍最常见的四个SLA指标,可用性、准确性、系统容量和延迟。
|
||||
|
||||
1. 可用性(Availabilty)
|
||||
|
||||
可用性指的是系统服务能正常运行所占的时间百分比。
|
||||
|
||||
如果我们搭建了一个拥有“100%可用性”的系统服务,那就意味着这个系统在任何时候都能正常运行。是不是很完美?但真要实现这样的目标其实非常困难,并且成本也会很高。
|
||||
|
||||
我们知道,即便是大名鼎鼎的亚马逊AWS云计算服务这样大型的、对用户来说极为关键的系统,也不能承诺100%的可用性,它的系统服务从推出到现在,也有过服务中断(Service Outage)的时候。
|
||||
|
||||
|
||||
|
||||
对于许多系统而言,四个9的可用性(99.99% Availability,或每年约50分钟的系统中断时间)即可以被认为是高可用性(High availability)。
|
||||
|
||||
说到这里,我来为你揭开一开始所提到的“99.9% Availability”的真实含义。
|
||||
|
||||
“99.9% Availability”指的是一天当中系统服务将会有大约86秒的服务间断期。服务间断也许是因为系统维护,也有可能是因为系统在更新升级系统服务。
|
||||
|
||||
86秒这个数字是怎么算出来的呢?
|
||||
|
||||
99.9%意味着有0.1%的可能性系统服务会被中断,而一天中有24小时 × 60分钟 × 60秒,也就是有(24 × 60 × 60 × 0.001) = 86.4秒的可能系统服务被中断了。而上面所说的四个9的高可用性服务就是承诺可以将一天当中的服务中断时间缩短到只有(24 × 60 × 60 × 0.0001) = 8.64秒。
|
||||
|
||||
2. 准确性(Accuracy)
|
||||
|
||||
准确性指的是我们所设计的系统服务中,是否允许某些数据是不准确的或者是丢失了的。如果允许这样的情况发生,用户可以接受的概率(百分比)是多少?
|
||||
|
||||
这该怎么衡量呢?不同的系统平台可能会用不同的指标去定义准确性。很多时候,系统架构会以错误率(Error Rate)来定义这一项SLA。
|
||||
|
||||
怎么计算错误率呢?可以用导致系统产生内部错误(Internal Error)的有效请求数,除以这期间的有效请求总数。
|
||||
|
||||
|
||||
|
||||
例如,我们在一分钟内发送100个有效请求到系统中,其中有5个请求导致系统返回内部错误,那我们可以说这一分钟系统的错误率是 5 / 100 = 5%。
|
||||
|
||||
下面,我想带你看看硅谷一线公司所搭建的架构平台的准确性SLA。
|
||||
|
||||
Google Cloud Platform的SLA中,有着这样的准确性定义:每个月系统的错误率超过5%的时间要少于0.1%,以每分钟为单位来计算。
|
||||
|
||||
而亚马逊AWS云计算平台有着稍微不一样的准确性定义:以每5分钟为单位,错误率不会超过0.1%。
|
||||
|
||||
你看,我们可以用错误率来定义准确性,但具体该如何评估系统的准确性呢?一般来说,我们可以采用性能测试(Performance Test)或者是查看系统日志(Log)两种方法来评估。
|
||||
|
||||
具体的做法我会在后面展开讲解,今天你先理解这项指标就可以了。
|
||||
|
||||
3. 系统容量(Capacity)
|
||||
|
||||
在数据处理中,系统容量通常指的是系统能够支持的预期负载量是多少,一般会以每秒的请求数为单位来表示。
|
||||
|
||||
我们常常可以看见,某个系统的架构可以处理的QPS (Queries Per Second)是多少又或者RPS(Requests Per Second)是多少。这里的QPS或者是RPS就是指系统每秒可以响应多少请求数。
|
||||
|
||||
我们来看看之前Twitter发布的一项数据,Twitter系统可以响应30万的QPS来读取Twitter Timelines。这里Twitter系统给出的就是他们对于系统容量(Capacity)的SLA。
|
||||
|
||||
你可能会问,我要怎么给自己设计的系统架构定义出准确的QPS呢?以我的经验看,可以有下面这几种方式。
|
||||
|
||||
第一种,是使用限流(Throttling)的方式。
|
||||
|
||||
如果你是使用Java语言进行编程的,就可以使用Google Guava库中的RateLimiter类来定义每秒最多发送多少请求到后台处理。
|
||||
|
||||
假设我们在每台服务器都定义了一个每秒最多处理1000个请求的RateLimiter,而我们有N台服务器,在最理想的情况下,我们的QPS可以达到1000 * N。
|
||||
|
||||
这里要注意的雷区是,这个请求数并不是设置得越多越好。因为每台服务器的内存有限,过多的请求堆积在服务器中有可能会导致内存溢出(Out-Of-Memory)的异常发生,也就是所有请求所需要占用的内存超过了服务器能提供的内存,从而让整个服务器崩溃。
|
||||
|
||||
第二种,是在系统交付前进行性能测试(Performance Test)。
|
||||
|
||||
我们可以使用像Apache JMeter又或是LoadRunner这类型的工具对系统进行性能测试。这类工具可以测试出系统在峰值状态下可以应对的QPS是多少。
|
||||
|
||||
当然了,这里也是有雷区的。
|
||||
|
||||
有的开发者可能使用同一类型的请求参数,导致后台服务器在多数情况下命中缓存(Cache Hit)。这个时候得到的QPS可能并不是真实的QPS。
|
||||
|
||||
打个比方,服务器处理请求的正常流程需要查询后台数据库,得到数据库结果后再返回给用户,这个过程平均需要1秒。在第一次拿到数据库结果后,这个数据就会被保存在缓存中,而如果后续的请求都使用同一类型的参数,导致结果不需要从数据库得到,而是直接从缓存中得到,这个过程我们假设只需要0.1秒。那这样,我们所计算出来的QPS就会比正常的高出10倍。所以在生成请求的时候,要格外注意这一点。
|
||||
|
||||
第三种,是分析系统在实际使用时产生的日志(Log)。
|
||||
|
||||
系统上线使用后,我们可以得到日志文件。一般的日志文件会记录每个时刻产生的请求。我们可以通过系统每天在最繁忙时刻所接收到的请求数,来计算出系统可以承载的QPS。
|
||||
|
||||
不过,这种方法不一定可以得到系统可以承载的最大QPS。
|
||||
|
||||
在这里打个比喻,一家可以容纳上百桌客人的餐馆刚开业,因为客流量还比较小,在每天最繁忙的时候只接待了10桌客人。那我们可以说这家餐馆最多只能接待10桌客人吗?不可以。
|
||||
|
||||
同样的,以分析系统日志的方法计算出来的QPS并不一定是服务器能够承载的最大QPS。想要得到系统能承受的最大QPS,更多的是性能测试和日志分析相结合的手段。
|
||||
|
||||
4. 延迟(Latency)
|
||||
|
||||
延迟指的是系统在收到用户的请求到响应这个请求之间的时间间隔。
|
||||
|
||||
在定义延迟的SLA时,我们常常看到系统的SLA会有p95或者是p99这样的延迟声明。这里的p指的是percentile,也就是百分位的意思。如果说一个系统的p95 延迟是1秒的话,那就表示在100个请求里面有95个请求的响应时间会少于1秒,而剩下的5个请求响应时间会大于1秒。
|
||||
|
||||
下面我们用一个具体的例子来说明延迟这项指标在SLA中的重要性。
|
||||
|
||||
假设,我们已经设计好了一个社交软件的系统架构。这个社交软件在接收到用户的请求之后,需要读取数据库中的内容返回给用户。
|
||||
|
||||
为了降低系统的延迟,我们会将数据库中内容放进缓存(Cache)中,以此来减少数据库的读取时间。在系统运行了一段时间后,我们得到了一些缓存命中率(Cache Hit Ratio)的信息。有90%的请求命中了缓存,而剩下的10%的请求则需要重新从数据库中读取内容。
|
||||
|
||||
这时服务器所给我们的p95或者p99延迟恰恰就衡量了系统的最长时间,也就是从数据库中读取内容的时间。作为一个优秀架构师,你可以通过改进缓存策略从而提高缓存命中率,也可以通过优化数据库的Schema或者索引(Index)来降低p95或p99 延迟。
|
||||
|
||||
总而言之,当p95或者p99过高时,总会有5%或者1%的用户抱怨产品的用户体验太差,这都是我们要通过优化系统来避免的。
|
||||
|
||||
小结
|
||||
|
||||
通过今天的内容,你可以发现,定义好一个系统架构的SLA对于一个优秀的架构师来说是必不可少的一项技能,也是一种基本素养。
|
||||
|
||||
特别是当系统架构在不停迭代的时候,有了一个明确的SLA,我们可以知道下一代系统架构的改进目标以及优化好的系统架构是否比上一代的系统SLA更加优秀。
|
||||
|
||||
我们通常会使用可用性、准确性、系统容量、延迟这四个指标来定义系统架构的SLA。
|
||||
|
||||
思考题
|
||||
|
||||
你可以思考一下,在自己所在的开发项目中,系统的SLA是什么呢?又有什么方面可以优化的呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
130
专栏/大规模数据处理实战/05分布式系统(下):架构师不得不知的三大指标.md
Normal file
130
专栏/大规模数据处理实战/05分布式系统(下):架构师不得不知的三大指标.md
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
05 分布式系统(下):架构师不得不知的三大指标
|
||||
你好,我是蔡元楠。
|
||||
|
||||
上一讲中,我们学习了如何用服务等级协议(SLA)来评估我们设计的分布式系统,并了解了几个常见的SLA指标。
|
||||
|
||||
今天我们继续来探索分布式系统的另外几个重要基础概念。
|
||||
|
||||
可扩展性
|
||||
|
||||
还是从我们为什么需要分布式系统讲起。原因是我们要面对的数据量越来越大,从GB到TB再到现在的PB级,单机无法胜任这样的工作。
|
||||
|
||||
工作中也常有这样的场景,随着业务变得越来越复杂,之前设计的系统无法处理日渐增长的负载。这时,我们就需要增加系统的容量。
|
||||
|
||||
分布式系统的核心就是可扩展性(Scalability)。
|
||||
|
||||
最基本而且最流行的增加系统容量的模型有两种: 水平扩展(Horizontal Scaling)和垂直扩展(Vertical Scaling)。
|
||||
|
||||
所谓水平扩展,就是指在现有的系统中增加新的机器节点。
|
||||
|
||||
|
||||
|
||||
垂直扩展就是在不改变系统中机器数量的情况下,“升级”现有机器的性能,比如增加机器的内存。
|
||||
|
||||
|
||||
|
||||
举个例子,假设你现在负责一批木材采伐的操作。你有3辆卡车,每辆车一次可以运25根木材。那么1小时最多可以运3辆卡车 * 25根木材 * 1小时=75根木材/小时。
|
||||
|
||||
如果要使这个系统的负荷量增加一倍,用水平扩展的办法,我们可以将卡车的数量增加到6辆;用垂直扩展的办法,我们可以使每辆卡车的运输量增加一倍,或者使每辆卡车的速度增加一倍。
|
||||
|
||||
你是不是已经发现了,水平扩展的适用范围更广,操作起来更简单,并且会提升系统的可用性(Availability)。
|
||||
|
||||
如果你的系统部署在AWS或者其他主流的云服务上,你只需要点几个按钮,就可以在现有的机器集群中增加一个新的节点。
|
||||
|
||||
但是,无节制地增加机器数量也会带来一些问题,比如机器的管理、调度、通信会变得更加复杂,出错的可能性会更高,更难保证数据的一致性等等。
|
||||
|
||||
与之相反,垂直扩展并没有让整个系统变得更加复杂,控制系统的代码也不需要做任何调整,但是它受到的限制比较多。多数情况下,单个机器的性能提升是有限的。而且受制于摩尔定律,提高机器的性能往往比购买新的机器更加昂贵。
|
||||
|
||||
所以在工作中,我们要对这两种模式进行取舍,要具体情况具体分析。
|
||||
|
||||
同样地,在大数据的时代,数据增长速度越来越快,数据规模越来越大,对数据存储系统的扩展性要求也越来越高。
|
||||
|
||||
传统的关系型数据库因为表与表之间的数据有关联,经常要进行Join操作,所有数据要存放在单机系统中,很难支持水平扩展。而NoSQL型的数据库天生支持水平扩展,所以这类存储系统的应用越来越广,如BigTable、MongoDB和Redis等。
|
||||
|
||||
一致性
|
||||
|
||||
可用性对于任何分布式系统都很重要。一般来说,构成分布式系统的机器节点的可用性要低于系统的可用性。
|
||||
|
||||
举个例子,如果我们想要构建一个可用性99.999%的分布式系统(每年约5分钟的宕机时间),但是我们使用的单台机器节点的可用性是99.9%(每年约8个小时的宕机时间)。那么想要达到我们的目标,最简单的办法就是增加系统中机器节点的数量。这样即使有部分机器宕机了,其他的机器还在持续工作,所以整个系统的可用性就提高了。
|
||||
|
||||
这种情况下,我们要思考一个问题:如何保证系统中不同的机器节点在同一时间,接收到和输出的数据是一致的呢?这时就要引入一致性(Consistency)的概念。
|
||||
|
||||
回到之前的例子,要保证分布式系统内的机器节点有相同的信息,就需要机器之间定期同步。
|
||||
|
||||
然而,发送信息也会有失败的可能,比如信息丢失或者有的节点正好宕机而无法接收。因此,一致性在高可用性的系统里是非常核心的概念。
|
||||
|
||||
接下来,我会给你介绍几个在工程中常用的一致性模型,分别是:强一致性(Strong Consistency),弱一致性(Weak Consistency),最终一致性(Eventual Consistency)。
|
||||
|
||||
|
||||
强一致性:系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值。所以在任意时刻,同一系统所有节点中的数据是一样的。
|
||||
弱一致性:系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。但经过“不一致时间窗口”这段时间后,后续对该数据的读取都是更新后的值。
|
||||
最终一致性:是弱一致性的特殊形式。存储系统保证,在没有新的更新的条件下,最终所有的访问都是最后更新的值。
|
||||
|
||||
|
||||
上面这三点我描述得比较正式,但其实都不难理解。这里,我进一步给你做个说明。
|
||||
|
||||
在强一致性系统中,只要某个数据的值有更新,这个数据的副本都要进行同步,以保证这个更新被传播到所有备份数据库中。在这个同步进程结束之后,才允许服务器来读取这个数据。
|
||||
|
||||
所以,强一致性一般会牺牲一部分延迟性,而且对于全局时钟的要求很高。举个例子,Google Cloud的Cloud Spanner就是一款具备强一致性的全球分布式企业级数据库服务。
|
||||
|
||||
在最终一致性系统中,我们无需等到数据更新被所有节点同步就可以读取。尽管不同的进程读同一数据可能会读到不同的结果,但是最终所有的更新会被按时间顺序同步到所有节点。所以,最终一致性系统支持异步读取,它的延迟比较小。比如亚马逊云服务的DynamoDB就支持最终一致的数据读取。
|
||||
|
||||
除了以上三个,分布式系统理论中还有很多别的一致性模型,如顺序一致性(Sequential Consistency),因果一致性(Casual Consistency)等,如果你感兴趣的话,可以自己查资料了解一下。
|
||||
|
||||
在实际应用系统中,强一致性是很难实现的,应用最广的是最终一致性。我们一起来看两个例子。
|
||||
|
||||
很多人认为银行间转账应该是强一致的。但是你仔细分析一下就会发现,事实并非如此。
|
||||
|
||||
举个例子,小王给小张转账1000元,小王的账户扣除了1000,此时小张并不一定立刻就收到1000元。这里可能会存在一个不一致的时间窗口:小王的钱扣除了1000元,小张还没收到1000元的时候。
|
||||
|
||||
另外一个例子,在12306网站买票的功能,也不是强一致的。
|
||||
|
||||
|
||||
|
||||
如果你在12306上发现一趟列车还剩余10张车票,你发起请求订了一张票,系统给你返回的可能是“正在排队,剩余10张票,现在有15人在购买”。
|
||||
|
||||
这时,你可能就需要去查询未完成订单,因为系统并没有给你及时返回订票成功或失败的结果。如果有人退了一张票,这张票也不会立即返回到票池中。这里明显也存在不一致的时间窗口。
|
||||
|
||||
但是,最终10张票只会卖给10个人,不可能卖给11个人,这就是最终一致性所谓的“最终所有数据都会同步”。
|
||||
|
||||
讲到这里,你对分布式系统的扩展性和一致性就很清楚了吧?接下来再给你介绍一个重要概念。
|
||||
|
||||
持久性
|
||||
|
||||
数据持久性(Data Durability)意味着数据一旦被成功存储就可以一直继续使用,即使系统中的节点下线、宕机或数据损坏也是如此。
|
||||
|
||||
不同的分布式数据库拥有不同级别的持久性。有些系统支持机器/节点级别的持久性,有些做到了集群级别,而有些系统压根没有持久性。
|
||||
|
||||
想要提高持久性,数据复制是较为通用的做法。因为把同一份数据存储在不同的节点上,即使有节点无法连接,数据仍然可以被访问。
|
||||
|
||||
在分布式数据处理系统中,还有一个持久性概念是消息持久性。什么意思呢?在分布式系统中,节点之间需要经常相互发送消息去同步以保证一致性。对于重要的系统而言,常常不允许任何消息的丢失。
|
||||
|
||||
分布式系统中的消息通讯通常由分布式消息服务完成,比如RabbitMQ、Kafka。这些消息服务能支持(或配置后支持)不同级别的消息送达可靠性。消息持久性包含两个方面:
|
||||
|
||||
|
||||
当消息服务的节点发生了错误,已经发送的消息仍然会在错误解决之后被处理;
|
||||
如果一个消息队列声明了持久性,那么即使队列在消息发送之后掉线,仍然会在重新上线之后收到这条消息。
|
||||
|
||||
|
||||
小结
|
||||
|
||||
在这一讲中,我们探讨了分布式处理系统的三个重要指标:扩展性,一致性和持久性。
|
||||
|
||||
结合前边提到的延迟性、可用性以及准确性,我们不难发现,这些设计分布式系统所要考虑的量化指标存在一定程度上的冲突。不可能有一个分布式处理系统在不牺牲某一指标的前提下,让每一个指标都达到最好。
|
||||
|
||||
作为优秀的系统架构师,我们一定要学会具体情况具体分析,找到最适合自己系统的指标,适当做出取舍。但是这一点说起来容易做起来难,到底该怎么取舍呢?你可以先思考一下这个问题,下一讲中我会结合CAP定理和你进一步讨论。
|
||||
|
||||
思考题
|
||||
|
||||
对于微信朋友圈的评论功能,你觉得哪种一致性模型更适用?为什么?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
135
专栏/大规模数据处理实战/06如何区分批处理还是流处理?.md
Normal file
135
专栏/大规模数据处理实战/06如何区分批处理还是流处理?.md
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
06 如何区分批处理还是流处理?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天,我将会带领你一起学习在进行大规模数据处理时,无论如何也绕不开的两个处理模式:批处理(Batching Processing)和流处理(Streaming Processing)。
|
||||
|
||||
在我看来,大规模的视频流系统、大规模物联网(IoT)数据监控系统等各种现代大规模数据系统的出现,已经成为了一种必然的历史潮流。
|
||||
|
||||
无论你是在从事哪一种开发方向,都不可避免地要与这些海量数据打交道。如何能既满足实际应用场景的需求,又高效地处理好大规模数据,在整个项目开发架构中都是非常重要的一个环节。
|
||||
|
||||
在开始讲解批处理和流处理之前,我想先介绍一下几个必要的背景知识。
|
||||
|
||||
无边界数据和有边界数据
|
||||
|
||||
这个世界上的数据可以抽象成为两种,分别是无边界数据(Unbounded Data)和有边界数据(Bounded Data)。
|
||||
|
||||
顾名思义,无边界数据是一种不断增长,可以说是无限的数据集。
|
||||
|
||||
这种类型的数据,我们无法判定它们到底什么时候会停止发送。
|
||||
|
||||
例如,从手机或者从传感器发送出来的信号数据,又比如我们所熟知的移动支付领域中的交易数据。因为每时每刻都会有交易产生,所以我们不能判定在某一刻这类数据就会停止发送了。
|
||||
|
||||
|
||||
|
||||
在国外的一些技术文章上,有时候我们会看到“流数据(Streaming Data)”这一说法,其实它和无边界数据表达的是同一个概念。
|
||||
|
||||
与此相反,有边界数据是一种有限的数据集。
|
||||
|
||||
这种数据更常见于已经保存好了的数据中。例如,数据库中的数据,或者是我们常见的CSV格式文件中的数据。
|
||||
|
||||
当然了,你可能会问,那我们把无边界数据按照时间窗口提取一小份出来,那这样的数据是什么数据呢?
|
||||
|
||||
拿我们之前提到过的移动支付中的交易数据来说吧。移动支付中的交易数据可以看作是无边界数据。那我们按2019年4月29日这个时间窗口提取出来的数据呢?这个当日的交易数据就变成了有边界数据了。
|
||||
|
||||
所以,有边界数据其实可以看作是无边界数据的一个子集。
|
||||
|
||||
事件时间和处理时间
|
||||
|
||||
在处理大规模数据的时候,我们通常还会关心时域(Time Domain)的问题。
|
||||
|
||||
我们要处理的任意数据都会有两种时域,分别是事件时间(Event Time)和处理时间(Precessing Time)。
|
||||
|
||||
事件时间指的是一个数据实际产生的时间点,而处理时间指的是处理数据的系统架构实际接收到这个数据的时间点。
|
||||
|
||||
下面我来用一个实际的例子进一步说明这两个时间概念。
|
||||
|
||||
现在假设,你正在去往地下停车场的路上,并且打算用手机点一份外卖。选好了外卖后,你就用在线支付功能付款了,这个时候是12点05分。恰好这时,你走进了地下停车库,而这里并没有手机信号。因此外卖的在线支付并没有立刻成功,而支付系统一直在重试(Retry)“支付”这个操作。
|
||||
|
||||
当你找到自己的车并且开出地下停车场的时候,已经是12点15分了。这个时候手机重新有了信号,手机上的支付数据成功发到了外卖在线支付系统,支付完成。
|
||||
|
||||
在上面这个场景中你可以看到,支付数据的事件时间是12点05分,而支付数据的处理时间是12点15分。事件时间和处理时间的概念,你明白了吗?
|
||||
|
||||
在了解完上面的4个基本概念后,我将开始为你揭开批处理和流处理模式的面纱。
|
||||
|
||||
批处理
|
||||
|
||||
数据的批处理,可以理解为一系列相关联的任务按顺序(或并行)一个接一个地执行。批处理的输入是在一段时间内已经收集保存好的数据。每次批处理所产生的输出也可以作为下一次批处理的输入。
|
||||
|
||||
绝大部分情况下,批处理的输入数据都是有边界数据,同样的,输出结果也一样是有边界数据。所以在批处理中,我们所关心的更多会是数据的事件时间。
|
||||
|
||||
举个例子,你在每年年初所看到的“支付宝年账单”就是一个数据批处理的典型例子。
|
||||
|
||||
|
||||
|
||||
支付宝会将我们在过去一年中的消费数据存储起来,并作为批处理输入,提取出过去一年中产生交易的事件时间,然后经过一系列业务逻辑处理,得到各种有趣的信息作为输出。
|
||||
|
||||
在许多情况下,批处理任务会被安排,并以预先定义好的时间间隔来运行,例如一天,一个月或者是一年这样的特定时间。
|
||||
|
||||
在银行系统中,银行信用卡消费账单和最低还款额度也都是由批处理系统以预先定义好的一个月的时间间隔运行,所产生出来的。
|
||||
|
||||
批处理架构通常会被设计在以下这些应用场景中:
|
||||
|
||||
|
||||
日志分析:日志系统是在一定时间段(日,周或年)内收集的,而日志的数据处理分析是在不同的时间内执行,以得出有关系统的一些关键性能指标。
|
||||
|
||||
计费应用程序:计费应用程序会计算出一段时间内一项服务的使用程度,并生成计费信息,例如银行在每个月末生成的信用卡还款单。
|
||||
|
||||
数据仓库:数据仓库的主要目标是根据收集好的数据事件时间,将数据信息合并为静态快照 (static snapshot),并将它们聚合为每周、每月、每季度的报告等。
|
||||
|
||||
|
||||
由Google MapReduce衍生出来的开源项目Apache Hadoop或者是Apache Spark等开源架构都是支持这种大数据批处理架构的。
|
||||
|
||||
由于完成批处理任务具有高延迟性,一般可以需要花费几小时,几天甚至是几周的时间。要是在开发业务中有快速响应用户的时间需求,我们则需要考虑使用流处理/实时处理来处理大数据。
|
||||
|
||||
流处理
|
||||
|
||||
数据的流处理可以理解为系统需要接收并处理一系列连续不断变化的数据。例如,旅行预订系统,处理社交媒体更新信息的有关系统等等。
|
||||
|
||||
流处理的输入数据基本上都是无边界数据。而流处理系统中是关心数据的事件时间还是处理时间,将视具体的应用场景而定。
|
||||
|
||||
例如,像网页监控系统这样的流处理系统要计算网站的QPS,它所关心的更多是处理时间,也就是网页请求数据被监控系统接收到的时间,从而计算QPS。
|
||||
|
||||
而在一些医疗护理监控系统的流处理系统中,他们则更关心数据的事件时间,这种系统不会因为接收到的数据有网络延时,而忽略数据本来产生的时间。
|
||||
|
||||
流处理的特点应该是要足够快、低延时,以便能够处理来自各种数据源的大规模数据。流处理所需的响应时间更应该以毫秒(或微秒)来进行计算。像我们平时用到的搜索引擎,系统必须在用户输入关键字后以毫秒级的延时返回搜索结果给用户。
|
||||
|
||||
流处理速度如此之快的根本原因是因为它在数据到达磁盘之前就对其进行了分析。
|
||||
|
||||
当流处理架构拥有在一定时间间隔(毫秒)内产生逻辑上正确的结果时,这种架构可以被定义为实时处理(Real-time Processing)。
|
||||
|
||||
而如果一个系统架构可以接受以分钟为单位的数据处理时间延时,我们也可以把它定义为准实时处理(Near real-time Processing)。
|
||||
|
||||
还记得我们在介绍批处理架构中所说到的不足吗?没错,是高延迟。而流处理架构则恰恰拥有高吞度量和低延迟等特点。
|
||||
|
||||
流处理架构通常都会被设计在以下这些应用场景中:
|
||||
|
||||
|
||||
实时监控:捕获和分析各种来源发布的数据,如传感器,新闻源,点击网页等。
|
||||
实时商业智能:智能汽车,智能家居,智能病人护理等。
|
||||
销售终端(POS)系统:像是股票价格的更新,允许用户实时完成付款的系统等。
|
||||
|
||||
|
||||
在如今的开源架构生态圈中,如Apache Kafka、Apache Flink、Apache Storm、Apache Samza等,都是流行的流处理架构平台。
|
||||
|
||||
在介绍完这两种处理模式后,你会发现,无论是批处理模式还是流处理模式,在现实生活中都有着很广泛的应用。你应该根据自己所面临的实际场景来决定到底采用哪种数据处理模式。
|
||||
|
||||
小结
|
||||
|
||||
批处理模式在不需要实时分析结果的情况下是一种很好的选择。尤其当业务逻辑需要处理大量的数据以挖掘更为深层次数据信息的时候。
|
||||
|
||||
而在应用需求需要对数据进行实时分析处理时,或者说当有些数据是永无止境的事件流时(例如传感器发送回来的数据时),我们就可以选择用流处理模式。
|
||||
|
||||
思考题
|
||||
|
||||
相信在学习完这一讲后,你会对批处理模式和流处理模式有着清晰的认识。今天的思考题是,在你的日常开发中,所面临的数据处理模式又是哪一种模式呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
139
专栏/大规模数据处理实战/07Workflow设计模式:让你在大规模数据世界中君临天下.md
Normal file
139
专栏/大规模数据处理实战/07Workflow设计模式:让你在大规模数据世界中君临天下.md
Normal file
@@ -0,0 +1,139 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
07 Workflow设计模式:让你在大规模数据世界中君临天下
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Workflow设计模式”。
|
||||
|
||||
在上一讲中,我们一起学习了大规模数据处理的两种处理模式——批处理和流处理。
|
||||
|
||||
利用好这两种处理模式,作为架构师的你就可以运筹帷幄,根据实际需求搭建出一套符合自己应用的数据处理系统。
|
||||
|
||||
然而,光是掌握了这两种数据处理模式就足够自如应对大规模数据世界中的需求挑战吗?从我的实战经验中看来,其实未必。
|
||||
|
||||
我们每个人在最开始学习大规模数据处理的时候,可能都是以WordCount作为教学例子来进行学习的。
|
||||
|
||||
WordCount这个例子,只需要一个单词集合作为输入,数据处理的结果是统计单词出现的次数,中间只需要经过一次数据处理的转换,就如同下图所示。
|
||||
|
||||
|
||||
|
||||
但在现实的应用场景种中,各式各样的应用需求决定了大规模数据处理中的应用场景会比WordCount复杂很多倍。
|
||||
|
||||
我还是以我在第一讲中所提到过的例子来说明吧。
|
||||
|
||||
在根据活跃在街头的美团外卖电动车的数量来预测美团的股价这个例子中,我们的输入数据集有可能不止一个。
|
||||
|
||||
例如,会有自己团队在街道上拍摄到的美团外卖电动车图片,会有第三方公司提供的美团外卖电动车数据集等等。
|
||||
|
||||
整个数据处理流程又会需要至少10个处理模块,每一个处理模块的输出结果都将会成为下一个处理模块的输入数据,就如同下图所示。
|
||||
|
||||
|
||||
|
||||
像上面的图示一样,我们将这种由多个不同的处理模块连接在一起,最后得出一个自己需要结果的有向无环图(Directed Acyclic Graph/DAG),称为一个工作流系统(Workflow System)。
|
||||
|
||||
在工作流系统的每个处理模块里,系统要执行的操作有可能不是单单一个数据转换的操作这么简单。像在上面例子的Ingestion这个模块中,我们需要将多个不同的数据集合并在一起,也需要将不合格的一些图片过滤掉。
|
||||
|
||||
如果你用过Apache Spark 1.4以上的版本的话,Spark平台里面的Execution DAG就可以为你展示一个完整的工作流图。
|
||||
|
||||
今天,我为你解释四种工作流系统的设计模式,希望你能够很好地理解它们,并运用在自己的数据处理系统设计中。在遇到各种复杂的应用场景的时候能够从容面对。
|
||||
|
||||
复制模式(Copier Pattern)
|
||||
|
||||
复制模式通常是将单个数据处理模块中的数据,完整地复制到两个或更多的数据处理模块中,然后再由不同的数据处理模块进行处理。工作流系统图通常如下图所示。
|
||||
|
||||
|
||||
|
||||
当我们在处理大规模数据时,需要对同一个数据集采取多种不同的数据处理转换,我们就可以优先考虑采用复制模式。
|
||||
|
||||
我来举个在YouTube视频平台中,系统处理视频数据集的一个例子吧。
|
||||
|
||||
我们都知道,视频平台很多时候都会提供不同分辨率的视频。4K或1080P的视频可以提供给网络带宽很高的用户。而在网络很慢的情况下,视频平台系统会自动转换成低分辨率格式的视频,像360P这样的视频给用户。
|
||||
|
||||
而在YouTube视频平台中,如果你将鼠标放在视频缩略图上,它会自动播放一段已经生成好的动画缩略图(Animated GIF Thumbnail )。
|
||||
|
||||
不仅如此,在平台的背后,一个视频的数据集可能被自然语言理解(NLP)的数据处理模块分析,用以自动生成视频字幕;还有可能被视频分析的数据处理模块分析,用以产生更好的内容推荐系统。那么,它的整个工作流系统就会如下图所示一样。
|
||||
|
||||
|
||||
|
||||
我们可以看到,在这个工作流系统中,每个数据处理模块的输入是相同的,而下面的5个数据处理模块都可以单独并且同步地运行处理。
|
||||
|
||||
过滤模式(Filter Pattern)
|
||||
|
||||
过滤模式的作用是过滤掉不符合特定条件的数据。
|
||||
|
||||
在数据集通过了这个数据处理模块后,数据集会缩减到只剩下符合条件的数据。工作流系统图通常如下图所示。
|
||||
|
||||
|
||||
|
||||
当我们在处理大规模数据时,需要针对一个数据集中某些特定的数据采取数据处理时,我们就可以优先考虑采用过滤模式。
|
||||
|
||||
我举个商城会员系统的例子来解释吧。
|
||||
|
||||
在商城会员系统中,系统通常会根据用户的消费次数、用户消费金额还有用户的注册时间,将用户划分成不同的等级。
|
||||
|
||||
假设现在商城有五星会员(Five-stars Membership)、金牌会员(Golden Membership)和钻石会员(Diamond Membership)。
|
||||
|
||||
而系统现在打算通过邮件,只针对身份是钻石会员的用户发出钻石会员活动邀请。这个时候,我们就可以通过过滤模式,将钻石会员的用户从所有用户中筛选出来,如下图所示。
|
||||
|
||||
|
||||
|
||||
在这个工作流系统中,一个数据处理模块会将输入的数据集过滤成符合条件的数据,然后传输到下一个数据处理模块进行单独处理。
|
||||
|
||||
分离模式(Splitter Pattern)
|
||||
|
||||
如果你在处理数据集时并不想丢弃里面的任何数据,而是想把数据分类为不同的类别来进行处理时,你就需要用到分离模式来处理数据。它的工作流系统图通常如下图所示。
|
||||
|
||||
|
||||
|
||||
需要注意的是,分离模式并不会过滤任何数据,只是将原来的数据集分组了。
|
||||
|
||||
还是以刚刚商城会员系统为例。假设现在商城有五星会员、金牌会员和钻石会员。
|
||||
|
||||
系统现在打算通过邮件,针对全部的会员用户发出与他们身份相符的不同活动的邀请。
|
||||
|
||||
这个时候,我们就可以通过分离模式将用户按照会员等级分组,然后发送相应的活动内容,如下图所示。
|
||||
|
||||
|
||||
|
||||
需要注意的是,在分离模式下,同样的数据其实是可以被划分到不同的数据处理模块的。
|
||||
|
||||
|
||||
|
||||
数据B是可以同时划分到工作流1和工作流2中。其实这种情况挺常见的,我可以给你举个例子来解释。
|
||||
|
||||
在银行系统上,用户可以通过勾选以短信通知或者以邮件通知的方式来提醒用户一笔交易成功。如果用户同时勾选了短信和邮件两种方式,那么属于这个用户的交易信息既会通过短信通知的数据处理模块来处理,也会通过邮件通知数据处理模块来处理。
|
||||
|
||||
合并模式(Joiner Pattern)
|
||||
|
||||
合并模式会将多个不同的数据集转换集中到一起,成为一个总数据集,然后将这个总的数据集放在一个工作流中进行处理。
|
||||
|
||||
|
||||
|
||||
还是以根据活跃在街头的美团外卖电动车的数量来预测美团的股价这个例子来说吧。
|
||||
|
||||
数据接入这一处理模块里,我们的输入数据有自己团队在街道上拍摄到的美团外卖电动车图片和第三方公司提供的美团外卖电动车图片。
|
||||
|
||||
如果我们打算先整合所有数据,然后进行其它数据处理的话,工作流系统图通常如下图所示。
|
||||
|
||||
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起学习了在大规模数据处理中用到的四种设计模式,分别是复制模式、过滤模式、分离模式和合并模式。
|
||||
|
||||
在设计大规模数据处理系统的时候,我们都希望能事先设计好一个工作流系统图出来作为参考。
|
||||
|
||||
有了这样一个大规模数据处理的整体蓝图之后,对于我们理解不同的处理模块是如何相互关联或者对未来优化系统设计是有很大帮助的。
|
||||
|
||||
思考题
|
||||
|
||||
在一个航空预定系统中,我们需要处理用户注册、购买机票和出行前24小时的提醒等功能。在这整个过程中,系统的数据处理运用了哪几个设计模式呢?
|
||||
|
||||
欢迎你把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
153
专栏/大规模数据处理实战/08发布_订阅模式:流处理架构中的瑞士军刀.md
Normal file
153
专栏/大规模数据处理实战/08发布_订阅模式:流处理架构中的瑞士军刀.md
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
08 发布_订阅模式:流处理架构中的瑞士军刀
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我想要与你分享的是在处理大规模数据中十分流行的一种设计模式:发布/订阅模式(Publish/Subscribe Pattern),有些地方也称它为Pub/Sub。
|
||||
|
||||
在了解发布/订阅模式之前,我想先简单介绍几个基础概念——消息(Message)和消息队列(Message Queue)。
|
||||
|
||||
消息
|
||||
|
||||
消息是什么呢?
|
||||
|
||||
在分布式架构里,架构中的各个组件(Component)需要相互联系沟通。组件可以是后台的数据库,可以是前端的浏览器,也可以是公司内部不同的服务终端(Service Endpoint)。
|
||||
|
||||
而各个组件间就是依靠通过发送消息互相通讯的。如下图所示。
|
||||
|
||||
|
||||
|
||||
消息可以是任意格式的。例如,我们可以利用JSON格式来传输一个消息,也能利用XML格式来传输一个消息,甚至可以使用一种自己定义的格式。
|
||||
|
||||
消息队列
|
||||
|
||||
知道了消息的含义后,你知道消息队列有什么作用吗?
|
||||
|
||||
消息队列在发布/订阅模式中起的是一个持久化缓冲(Durable Buffer)的作用。
|
||||
|
||||
消息的发送方可以发送任意消息至这个消息队列中,消息队列在接收到消息之后会将消息保存好,直到消息的接收方确认已经从这个队列拿到了这个消息,才会将这条消息从消息队列中删除。
|
||||
|
||||
有的消息系统平台如Apache Kafka,它能够让用户自己定义消息队列对消息的保留时间,我将会在介绍Apache Kafka的时候讲到。
|
||||
|
||||
有了消息队列后,整个发送消息的流程就变成下图所示。
|
||||
|
||||
|
||||
|
||||
发布/订阅模式
|
||||
|
||||
在了解了消息和消息队列后,现在我想和你正式地介绍发布/订阅模式的概念。
|
||||
|
||||
发布/订阅模式指的是消息的发送方可以将消息异步地发送给一个系统中不同组件,而无需知道接收方是谁。在发布/订阅模式中,发送方被称为发布者(Publisher),接收方则被称作订阅者(Subscriber)。
|
||||
|
||||
发布者将消息发送到消息队列中,订阅者可以从消息队列里取出自己感兴趣的消息。
|
||||
|
||||
在发布/订阅模式里,可以有任意多个发布者发送消息,也可以有任意多个订阅者接收消息,如下图所示。
|
||||
|
||||
|
||||
|
||||
读到这里,你可能会有一个疑问,从概念上看,发布/订阅模式只是简单地在消息发送方和消息接收方中间多加了一个消息队列,为什么这种设计架构在实际应用会如此流行呢?我来给你举个例子说明一下。
|
||||
|
||||
现在假设,你是一个移动支付App公司里支付团队的开发人员,App里所有的支付操作都是由你的团队来开发的。而公司最近新成立了一个欺诈预防团队,他们希望从你的支付团队里获取交易数据。
|
||||
|
||||
也就是说,每次有交易发生的时候,你都需要通知他们交易的金额、地点、时间这些数据,让他们能够实时分析这次的交易是否存在欺诈行为。
|
||||
|
||||
按照传统的做法,两个团队需要开会交流,以确定数据消息传输的API和传输数据的格式。确定后,两个团队系统的交流方式会如下图所示。
|
||||
|
||||
|
||||
|
||||
欺诈预防团队将自己需要的数据格式定义在API中告诉支付团队,每次有交易产生的时候,支付系统就会通过调用欺诈预防系统API的方式通知他们。
|
||||
|
||||
一段时间过后,公司希望和商家一起合作推动一项优惠活动,不同的商家会有不同的优惠。公司希望能够精准投放优惠活动的广告给感兴趣的用户,所以又成立了一个新部门,我们叫它广告推荐组吧。
|
||||
|
||||
广告推荐组的同事也希望从你的支付团队里获取交易数据。这个时候,你有两种选择,一种是选择第六讲中说到的批处理方式,另一种就是今天讲的发布/订阅模式。
|
||||
|
||||
批处理方式会从数据库中一次性读取全部用户的交易数据来进行推荐分析。
|
||||
|
||||
这种做法有几个不好的地方。
|
||||
|
||||
它需要你开放自己数据库的权限给广告推荐组,推荐组每次大量读取数据时,可能也会造成你自己的数据库性能下降。同时,还要考虑广告推荐组也想维护一份自己的数据库的需求。
|
||||
|
||||
如果还是按照之前欺诈预防团队的做法,让广告推荐组分享API给你,每次有交易产生的时候再通知他们的话,系统的运行模式会如文中图片所示。
|
||||
|
||||
|
||||
|
||||
看到这里你应该明白了。每一次有一个新的系统想从支付团队里读取数据的话,都要双方开会讨论,定义一个新的API,然后修改支付团队现有的系统,将API加入系统中。
|
||||
|
||||
而且这些API通常都是同步调用的,过多的API调用会让系统的延迟越来越大。这样的设计模式被称作观察者模式(Observer Pattern),系统中的各个组件紧耦合(Tightly Coupled)在一起。
|
||||
|
||||
如果是采用发布/订阅模式来重新设计呢?整个系统就如下图所示:
|
||||
|
||||
|
||||
|
||||
采用这样的数据处理模式,作为消息发布者的支付团队无需过多考虑以后有多少其它的团队需要读取交易数据,只需要设计好自己提供的数据内容与格式,在每次交易发生时发送消息进消息队列中就可以了。任何对这些数据感兴趣的团队只需要从消息队列中自行读取便可。
|
||||
|
||||
发布/订阅模式的优缺点
|
||||
|
||||
说到这里,我们可以看到发布/订阅模式会有以下几个优点:
|
||||
|
||||
|
||||
松耦合(Loose Coupling):消息的发布者和消息的订阅者在开发的时候完全不需要事先知道对方的存在,可以独立地进行开发。
|
||||
|
||||
高伸缩性(High Scalability):发布/订阅模式中的消息队列可以独立的作为一个数据存储中心存在。在分布式环境中,更是消息队列更是可以扩展至上千个服务器中。我们从Linkedin公司的技术博客中可以得知,光在2016年,Linkedin公司就维护开发了将近1400个消息队列。
|
||||
|
||||
系统组件间通信更加简洁:因为不需要为每一个消息的订阅者准备专门的消息格式,只要知道了消息队列中保存消息的格式,发布者就可以按照这个格式发送消息,订阅者也只需要按照这个格式接收消息。
|
||||
|
||||
|
||||
虽然发布/订阅模式的数据处理模式优点多多,但是还是存在着自身的缺点的。
|
||||
|
||||
例如,在整个数据模式中,我们不能保证发布者发送的数据一定会送达订阅者。如果要保证数据一定送达的话,需要开发者自己实现响应机制。
|
||||
|
||||
在硅谷,很多大型云平台都是运用这个发布/订阅数据处理模式。
|
||||
|
||||
例如,Google的Cloud Pub/Sub平台,AWS的Amazon Simple Notification Service(SNS)。被Linkedin、Uber等硅谷大厂所广泛使用的开源平台Apache Kafka也是搭建在发布/订阅数据处理模式之上的。甚至是连Redis也支持原生的发布/订阅模式。
|
||||
|
||||
Apache Kafka作为一个被在硅谷大厂与独角兽广泛使用的开源平台,如果你是在创业阶段的话,说不定可以用得上,所以在这里我想简单介绍一下Apache Kafka。
|
||||
|
||||
在Apache Kafka中,消息的发送方被称为Producer,消息的接收方被称为Consumer,而消息队列被称为Topic。
|
||||
|
||||
|
||||
|
||||
Apache Kafka在判断消息是否被接收方接收是利用了Log offset机制。
|
||||
|
||||
什么是Log offset机制呢?我举个例子给你解释一下。
|
||||
|
||||
假设发送方连续发送了5条数据到消息队列Topics中,这5条消息被编号为10000、10001、10002、10003和10004。
|
||||
|
||||
如果接收方读取数据之后回应消息队列它接收的Log offset是10000、10001和10003,那么消息队列就会认为接收方最多只接收了消息10000和10001,剩下的消息10002、10003和10004则会继续发送给接收方,直到接收方回应接收了消息10002、10003和10004。
|
||||
|
||||
发布/订阅模式的适用场景
|
||||
|
||||
我们说回到发布/订阅模式来,看看它能用在哪些场景。
|
||||
|
||||
如果你在处理数据的时候碰到以下场景,那么就可以考虑使用发布/订阅的数据处理模式。
|
||||
|
||||
|
||||
系统的发送方需要向大量的接收方广播消息。
|
||||
|
||||
系统中某一个组件需要与多个独立开发的组件或服务进行通信,而这些独立开发的组件或服务可以使用不同的编程语言和通信协议。
|
||||
|
||||
系统的发送方在向接收方发送消息之后无需接收方进行实时响应。
|
||||
|
||||
系统中对数据一致性的要求只需要支持数据的最终一致性(Eventual Consistency)模型。
|
||||
|
||||
|
||||
要提醒你注意的一点是,如果系统的发送方在向接收方发送消息之后,需要接收方进行实时响应的话,那么绝大多数情况下,都不要考虑使用发布/订阅的数据处理模式。
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起学习了大规模数据处理中一种十分流行的设计模式——发布/订阅模式。它能够很好地解耦(Decouple)系统中不同的组件,许多实时的流处理架构就是利用这个数据处理的设计模式搭建起来的。因为发布/订阅模式同时具有很好的伸缩性。
|
||||
|
||||
如果你在开发的场景适合我所讲到的适应场景,可以优先考虑使用发布/订阅模式。
|
||||
|
||||
思考题
|
||||
|
||||
你认为微信的朋友圈功能适合使用发布/订阅模式吗?为什么?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
177
专栏/大规模数据处理实战/09CAP定理:三选二,架构师必须学会的取舍.md
Normal file
177
专栏/大规模数据处理实战/09CAP定理:三选二,架构师必须学会的取舍.md
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
09 CAP定理:三选二,架构师必须学会的取舍
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是CAP定理。
|
||||
|
||||
在分布式系统的两讲中,我们一起学习到了两个重要的概念:可用性和一致性。
|
||||
|
||||
而今天,我想和你讲解一个与这两个概念相关,并且在设计分布式系统架构时都会讨论到的一个定理——CAP定理(CAP Theorem)。
|
||||
|
||||
CAP定理
|
||||
|
||||
CAP这个概念最初是由埃里克·布鲁尔博士(Dr. Eric Brewer)在2000年的ACM年度学术研讨会上提出的。
|
||||
|
||||
如果你对这次演讲感兴趣的话,可以翻阅他那次名为“Towards Robust Distributed Systems”的演讲deck。
|
||||
|
||||
在两年之后,塞思·吉尔伯特(Seth Gilbert)和麻省理工学院的南希·林奇教授(Nancy Ann Lynch)在他们的论文“Brewer’s conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services”中证明了这一概念。
|
||||
|
||||
|
||||
|
||||
他们在这篇论文中证明了:在任意的分布式系统中,一致性(Consistency),可用性(Availability)和分区容错性(Partition-tolerance)这三种属性最多只能同时存在两个属性。
|
||||
|
||||
下面,我来为你解读一下这三种属性在这篇论文里的具体意思。
|
||||
|
||||
C属性:一致性
|
||||
|
||||
一致性在这里指的是线性一致性(Linearizability Consistency)。在线性一致性的保证下,所有分布式环境下的操作都像是在单机上完成的一样,也就是说图中Sever A、B、C的状态一直是一致的。
|
||||
|
||||
|
||||
|
||||
打个比方,现在有两个操作(Operation),操作A和操作B,都需要在同一个分布式系统上完成。
|
||||
|
||||
我们假设操作A作用在系统上的时候,所看见的所有系统状态(State)叫作状态A。而操作B作用在系统上的时候,所看见的所有系统状态叫作状态B。
|
||||
|
||||
如果操作A是在操作B之前发生的,并且操作A成功了。那么系统状态B必须要比系统状态A更加新。
|
||||
|
||||
可能光看理论的话你还是会觉得这个概念有点模糊,那下面我就以一个具体例子来说明吧。
|
||||
|
||||
假设我们设计了一个分布式的购物系统,在这个系统中,商品的存货状态分别保存在服务器A和服务器B中。我们把存货状态定义为“有货状态”或者“无货状态”。在最开始的时候,服务器A和服务器B都会显示商品为有货状态。
|
||||
|
||||
|
||||
|
||||
等一段时间过后,商品卖完了,后台就必须将这两台服务器上的商品状态更新为无货状态。
|
||||
|
||||
因为是在分布式的环境下,商品状态的更新在服务器A上完成了,显示为无货状态。而服务器B的状态因为网络延迟的原因更新还未完成,还是显示着有货状态。
|
||||
|
||||
这时,恰好有两个用户使用着这个购物系统,先后发送了一个查询操作(Query Operation)到后台服务器中查询商品状态。
|
||||
|
||||
我们假设是用户A先查询的,这个查询操作A被发送到了服务器A上面,并且成功返回了商品是无货状态的。用户B在随后也对同一商品进行查询,而这个查询操作B被发送到了服务器B上面,并且成功返回了商品是有货状态的。
|
||||
|
||||
|
||||
|
||||
我们知道,对于整个系统来说,商品的系统状态应该为无货状态。而操作A又是在操作B之前发送并且成功完成的,所以如果这个系统有线性一致性这个属性的话,操作B所看到的系统状态理论上应该是无货状态。
|
||||
|
||||
但在我们这个例子中,操作B却返回了有货状态。所以我们说,这个分布式的购物系统并不满足论文里所讲到的线性一致性。
|
||||
|
||||
聊完了一致性,我们一起来看看可用性的含义。
|
||||
|
||||
A属性:可用性
|
||||
|
||||
可用性的概念比较简单,在这里指的是在分布式系统中,任意非故障的服务器都必须对客户的请求产生响应。
|
||||
|
||||
当系统满足可用性的时候,不管出现什么状况(除非所有的服务器全部崩溃),都能返回消息。
|
||||
|
||||
|
||||
|
||||
也就是说,当客户端向系统发送请求,只要系统背后的服务器有一台还未崩溃,那么这个未崩溃的服务器必须最终响应客户端。
|
||||
|
||||
P属性:分区容错性
|
||||
|
||||
在了解了可用性之后,你还需要了解分区容错性。它分为两个部分,“分区”和“容错”。
|
||||
|
||||
在一个分布式系统里,如果出现一些故障,可能会使得部分节点之间无法连通。由于这些故障节点无法联通,造成整个网络就会被分成几块区域,从而使数据分散在这些无法连通的区域中的情况,你可以认为这就是发生了分区错误。
|
||||
|
||||
|
||||
|
||||
如图所示,如果你要的数据只在Sever A中保存,当系统出现分区错误,在不能直接连接Sever A时,你是无法获取数据的。我们要“分区容错”,意思是即使出现这样的“错误”,系统也需要能“容忍”。也就是说,就算错误出现,系统也必须能够返回消息。
|
||||
|
||||
分区容错性,在这里指的是我们的系统允许网络丢失从一个节点发送到另一个节点的任意多条消息。
|
||||
|
||||
我们知道,在现代网络通信中,节点出现故障或者网络出现丢包这样的情况是时常会发生的。
|
||||
|
||||
如果没有了分区容错性,也就是说系统不允许这些节点间的通讯出现任何错误的话,那我们日常所用到的很多系统就不能再继续工作了。
|
||||
|
||||
所以在大部分情况下,系统设计都会保留P属性,而在C和A中二选一。
|
||||
|
||||
论文中论证了在任意系统中,我们最多可以保留CAP属性中的两种,也就是CP或者AP或者CA。关于具体的论证过程,如果你感兴趣的话,可以自行翻阅论文查看。
|
||||
|
||||
你可能会问,在我们平常所用到的开发架构中,有哪些系统是属于CP系统,有哪些是AP系统又有哪些是CA系统呢?我来给你介绍一下:
|
||||
|
||||
|
||||
CP系统:Google BigTable, Hbase, MongoDB, Redis, MemCacheDB,这些存储架构都是放弃了高可用性(High Availablity)而选择CP属性的。
|
||||
AP系统:Amazon Dynamo系统以及它的衍生存储系统Apache Cassandra和Voldemort都是属于AP系统
|
||||
CA系统:Apache Kafka是一个比较典型的CA系统。
|
||||
|
||||
|
||||
我在上面说过,P属性在现代网络时代中基本上是属于一个必选项,那为什么Apache Kafka会放弃P选择CA属性呢?我来给你解释一下它的架构思想。
|
||||
|
||||
放弃了P属性的Kafka Replication
|
||||
|
||||
在Kafka发布了0.8版本之后,Kafka系统引入了Replication的概念。Kafka Relocation通过将数据复制到不同的节点上,从而增强了数据在系统中的持久性(Durability)和可用性(Availability)。在Kafka Replication的系统设计中,所有的数据日志存储是设计在同一个数据中心(Data Center)里面的,也就是说,在同一个数据中心里网络分区出现的可能性是十分之小的。
|
||||
|
||||
它的具体架构是这样的,在Kafka数据副本(Data Replication)的设计中,先通过Zookeeper选举出一个领导者节点(Leader)。这个领导者节点负责维护一组被称作同步数据副本(In-sync-replica)的节点,所有的数据写入都必须在这个领导者节点中记录。
|
||||
|
||||
我来举个例子,假设现在数据中心有三台服务器,一台被选为作为领导者节点,另外两台服务器用来保存数据副本,分别是Replication1和Replication2,它们两个节点就是被领导者节点维护的同步数据副本了。领导者节点知道它维护着两个同步数据副本。
|
||||
|
||||
如果用户想写入一个数据,假设是“Geekbang”
|
||||
|
||||
|
||||
用户会发请求到领导者节点中想写入“Geekbang”。
|
||||
|
||||
领导者节点收到请求后先在本地保存好,然后也同时发消息通知Replication1和Replication2。
|
||||
|
||||
Replication1和Replication2收到消息后也保存好这条消息并且回复领导者节点写入成功。
|
||||
|
||||
领导者节点记录副本1和副本2都是健康(Healthy)的,并且回复用户写入成功。
|
||||
|
||||
|
||||
红色的部分是领导者节点本地日志,记录着有哪些同步数据副本是健康的。
|
||||
|
||||
|
||||
|
||||
往后用户如果想查询写入的数据,无论是领导者节点还是两个副本都可以返回正确同步的结果。
|
||||
|
||||
那假如分区出现了该怎么办呢?例如领导者节点和副本1无法通讯了,这个时候流程就变成这样了。
|
||||
|
||||
|
||||
用户会发请求到领导者节点中想写入“Geekbang”。
|
||||
|
||||
领导者节点收到请求后先在本地保存好,然后也同时发消息通知Replication1和Replication2。
|
||||
|
||||
只有Replication2收到消息后也保存好这条消息并且回复领导者节点写入成功。
|
||||
|
||||
领导者节点记录副本2是健康的,并且回复用户写入成功。
|
||||
|
||||
|
||||
同样,红色的部分是领导者节点本地日志,记录着有哪些同步数据副本是健康的。
|
||||
|
||||
|
||||
|
||||
如果所有副本都无法通讯的时候,Apache Kafka允许系统只有一个节点工作,也就是领导者节点。这个时候所有的写入都只保存在领导者节点了。过程如下,
|
||||
|
||||
|
||||
用户会发请求到领导者节点中想写入“Geekbang”。
|
||||
|
||||
领导者节点收到请求后先在本地保存好,然后也同时发消息通知Replication1和Replication2。
|
||||
|
||||
没有任何副本回复领导者节点写入成功,领导者节点记录无副本是健康的,并且回复用户写入成功。
|
||||
|
||||
|
||||
|
||||
|
||||
当然,在最坏的情况下,连领导者节点也挂了,Zookeeper会重新去寻找健康的服务器节点来当选新的领导者节点。
|
||||
|
||||
小结
|
||||
|
||||
通过今天的学习,我们知道在CAP定理中,一致性,可用性和分区容错性这三个属性最多只能选择两种属性保留。CAP定理在经过了差不多20年的讨论与演化之后,大家对这三个属性可能会有着自己的一些定义。
|
||||
|
||||
例如在讨论一致性的时候,有的系统宣称自己是拥有C属性,也就拥有一致性的,但是这个一致性并不是论文里所讨论到的线性一致性。
|
||||
|
||||
在我看来,作为大规模数据处理的架构师,我们应该熟知自己的系统到底应该保留CAP中的哪两项属性,同时也需要熟知,自己所应用到的平台架构是保留着哪两项属性。
|
||||
|
||||
思考题
|
||||
|
||||
如果让你重新设计微博系统中的发微博功能,你会选择CAP的哪两个属性呢?为什么呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
153
专栏/大规模数据处理实战/10Lambda架构:Twitter亿级实时数据分析架构背后的倚天剑.md
Normal file
153
专栏/大规模数据处理实战/10Lambda架构:Twitter亿级实时数据分析架构背后的倚天剑.md
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
10 Lambda架构:Twitter亿级实时数据分析架构背后的倚天剑
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是Lambda架构。
|
||||
|
||||
通过这一讲,你可以了解什么是Lambda架构,以及它为什么能够成为Twitter亿级实时数据分析架构背后的“倚天剑”。
|
||||
|
||||
在学习了架构师的必备技能后,你是否已经摩拳擦掌,跃跃欲试地想要上手一个实际项目了呢?没问题,我们一起来看一个我的架构经历里的真实项目。
|
||||
|
||||
情况是这样的,我们正运行着广告精准投放业务,并且拥有海量的用户网站访问行为。我们需要进行用户行为分析来建立一个模型,然后根据这个模型来投放用户喜好的广告。
|
||||
|
||||
你可能想到了批处理架构。没错,这个时候数据批处理架构无疑是一种很好的选择。
|
||||
|
||||
可是我们不要忘了,之前讲过批处理架构有着高延时性的不足,而互联网用户行为的数据往往可以达到Pb或Eb,甚至是Zb的级别。做这种分析挖掘用户行为的任务,往往能耗时好几个小时甚至是几天。这样的话,我们根据模型精准投放给特定用户的广告就会有一定延时了。
|
||||
|
||||
那我们只用流处理架构行不行呢?
|
||||
|
||||
在广告精准投放的业务需求下,只用流处理架构会造成忽略了用户的历史网站访问行为,一些异常行为可能会让我们的服务投放错误的广告。
|
||||
|
||||
例如,用户A的电脑暂时借给用户B使用了一下,而用户B浏览了一些新的网站类型(与用户A不同)。这种情况下,我们无法判断用户A实际上是否对这类型的广告感兴趣,所以不能根据这些新的浏览记录给用户A推送广告。
|
||||
|
||||
这个时候应该怎么优化我们的架构呢?我们先把问题放一放,在介绍完了Lambda架构之后或许会对你有所启发。
|
||||
|
||||
Lambda架构
|
||||
|
||||
Lambda架构(Lambda Architecture)是由Twitter工程师南森·马茨(Nathan Marz)提出的大数据处理架构。这一架构的提出基于马茨在BackType和Twitter上的分布式数据处理系统的经验。
|
||||
|
||||
Lambda架构使开发人员能够构建大规模分布式数据处理系统。它具有很好的灵活性和可扩展性,也对硬件故障和人为失误有很好的容错性。
|
||||
|
||||
Lambda架构总共由三层系统组成:批处理层(Batch Layer),速度处理层(Speed Layer),以及用于响应查询的服务层(Serving Layer)。
|
||||
|
||||
|
||||
|
||||
在Lambda架构中,每层都有自己所肩负的任务。
|
||||
|
||||
批处理层存储管理主数据集(不可变的数据集)和预先批处理计算好的视图。
|
||||
|
||||
批处理层使用可处理大量数据的分布式处理系统预先计算结果。它通过处理所有的已有历史数据来实现数据的准确性。这意味着它是基于完整的数据集来重新计算的,能够修复任何错误,然后更新现有的数据视图。输出通常存储在只读数据库中,更新则完全取代现有的预先计算好的视图。
|
||||
|
||||
速度处理层会实时处理新来的大数据。
|
||||
|
||||
速度层通过提供最新数据的实时视图来最小化延迟。速度层所生成的数据视图可能不如批处理层最终生成的视图那样准确或完整,但它们几乎在收到数据后立即可用。而当同样的数据在批处理层处理完成后,在速度层的数据就可以被替代掉了。
|
||||
|
||||
本质上,速度层弥补了批处理层所导致的数据视图滞后。比如说,批处理层的每个任务都需要1个小时才能完成,而在这1个小时里,我们是无法获取批处理层中最新任务给出的数据视图的。而速度层因为能够实时处理数据给出结果,就弥补了这1个小时的滞后。
|
||||
|
||||
所有在批处理层和速度层处理完的结果都输出存储在服务层中,服务层通过返回预先计算的数据视图或从速度层处理构建好数据视图来响应查询。
|
||||
|
||||
好了,我们回到刚刚的问题中。我们如何做到既能实时分析用户新的网站浏览行为又能兼顾到用户的网站浏览行为历史呢?没错,就是利用Lambda架构。
|
||||
|
||||
所有的新用户行为数据都可以同时流入批处理层和速度层。批处理层会永久保存数据并且对数据进行预处理,得到我们想要的用户行为模型并写入服务层。而速度层也同时对新用户行为数据进行处理,得到实时的用户行为模型。
|
||||
|
||||
而当“应该对用户投放什么样的广告”作为一个查询(Query)来到时,我们从服务层既查询服务层中保存好的批处理输出模型,也对速度层中处理的实时行为进行查询,这样我们就可以得到一个完整的用户行为历史了。
|
||||
|
||||
一个查询就如下图所示,既通过批处理层兼顾了数据的完整性,也可以通过速度层弥补批处理层的高延时性,让整个查询具有实时性。
|
||||
|
||||
|
||||
|
||||
Lambda架构在硅谷一线大公司的应用已经十分广泛,我来带你一起看看一些实际的应用场景。
|
||||
|
||||
Twitter的数据分析案例
|
||||
|
||||
Twitter在欧美十分受欢迎,而Twitter中人们所发Tweet里面的Hashtag也常常能引爆一些热搜词汇,也就是Most Popular Hashtags。下面我来给你讲述一下如何利用Lambda架构来实时分析这些Hashtags。
|
||||
|
||||
|
||||
|
||||
在这个实际案例里,我们先用twitter4J的流处理API抓取实时的Twitter推文,同时利用Apache Kafka将抓取到的数据保存并实时推送给批处理层和速度层。
|
||||
|
||||
因为Apache Spark平台中既有批处理架构也兼容了流处理架构,所以我们选择在批处理层和速度层都采用Apache Spark来读取来自Apache Kafka的数据。
|
||||
|
||||
批处理层和速度层在分析处理好数据后会将数据视图输出存储在服务层中,我们将使用Apache Cassandra平台来存储他们的数据视图。Apache Cassandra将批处理层的视图数据和速度层的实时视图数据结合起来,就可以得到一系列有趣的数据。
|
||||
|
||||
例如,我们根据每一条Tweet中元数据(Metadata)里的location field,可以得知发推文的人的所在地。而服务层中的逻辑可以根据这个地址信息进行分组,然后统计在不同地区的人所关心的Hashtag是什么。
|
||||
|
||||
时间长达几周或者的几个月的数据,我们可以结合批处理层和速度层的数据视图来得出,而快至几个小时的数据我们又可以根据速度层的数据视图来获知,怎么样?这个架构是不是十分灵活?
|
||||
|
||||
看到这里,你可能会问,我在上面所讲的例子都是来自些科技巨头公司,如果我在开发中面对的数据场景没有这么巨大,又或者说我的公司还在创业起步阶段,我是否可以用到Lambda架构呢?
|
||||
|
||||
答案是肯定的!我下面将和你一起分享一个在硅谷旧金山创业公司的App后台架构。
|
||||
|
||||
Smart Parking案例分析
|
||||
|
||||
在硅谷旧金山地区上班生活的小伙伴肯定都知道,找停车位是一大难题。这里地少车多,每次出行,特别是周末,找停车位都要绕个好几十分钟才能找得到。
|
||||
|
||||
智能停车App就是在这样的背景下诞生的。这个App可以根据大规模数据所构建的视图推荐最近的车位给用户。
|
||||
|
||||
看到这里,我想先请你结合之前所讲到的广告精准投放案例,思考一下Lambda架构是如何应用在这个App里的,然后再听我娓娓道来。
|
||||
|
||||
好,我们来梳理一下各种可以利用到的大数据。
|
||||
|
||||
首先是可以拿到各类停车场的数据。这类数据的实时性虽然不一定高,但是数据的准确性高。那我们能不能只通过这类大数据来推荐停车位呢?
|
||||
|
||||
我来给你举个极端的例子。假设在一个区域有三个停车场,停车场A现在只剩下1个停车位了。
|
||||
|
||||
停车场B和C还有非常多的空位。而在这时候距离停车场比A较近的位置有10位车主在使用这个App寻求推荐停车位。如果只通过车主和停车场的距离和停车场剩余停车位来判断的话,App很有可能会将这个只剩下一个停车位的停车场A同时推荐给这10位用户。
|
||||
|
||||
|
||||
|
||||
结果可想而知,只有一位幸运儿能找到停车位,剩下的9位车主需要重新寻找停车位。
|
||||
|
||||
如果附近又出现了只有一个停车位的停车场呢?同理,这个App又会推荐这个停车场给剩下的9位用户。这时又只能有一位幸运儿找到停车位。
|
||||
|
||||
如此反复循环,用户体验会非常差,甚至会导致用户放弃这个App。
|
||||
|
||||
那我们有没有办法可以改进推荐的准确度呢?
|
||||
|
||||
你可能会想到我们可以利用这些停车场的历史数据,建立一个人工智能的预测模型,在推荐停车位的时候,不单单考虑到附近停车场的剩余停车位和用户与停车场的相邻距离,还能将预测模型应用在推荐里,看看未来的一段时间内这个停车场是否有可能会被停满了。
|
||||
|
||||
这时候我们的停车位推荐系统就变成了一个基于分数(Score)来推荐停车位的系统了。
|
||||
|
||||
好了,这个时候的系统架构是否已经达到最优了呢?你有想到应用Lambda架构吗?
|
||||
|
||||
没错,这些停车场的历史数据或者每隔半小时拿到的停车位数据,我们可以把它作为批处理层的数据。
|
||||
|
||||
那速度层的数据呢?我们可以将所有用户的GPS数据聚集起来,这些需要每秒收集的GPS数据刚好又是速度层所擅长的实时流处理数据。从这些用户的实时GPS数据中,我们可以再建立一套预测模型来预测附近停车场位置的拥挤程度。
|
||||
|
||||
|
||||
|
||||
服务层将从批处理层和速度层得到的分数结合后将得到最高分数的停车场推荐给用户。这样利用了历史数据(停车场数据)和实时数据(用户GPS数据)能大大提升推荐的准确率。
|
||||
|
||||
小结
|
||||
|
||||
在了解Lambda架构后,我们知道Lambda架构具有很好的灵活性和可扩展性。我们可以很方便地将现有的开源平台套用入这个架构中,如下图所示。
|
||||
|
||||
|
||||
|
||||
当开发者需要迁移平台时,整体的架构不需要改变,只需要将逻辑迁移到新平台中。
|
||||
|
||||
例如,可以将Apache Spark替换成Apache Storm。而因为我们有批处理层这一概念,又有了很好的容错性。
|
||||
|
||||
假如某天开发者发现逻辑出现了错误,只需要调整算法对永久保存好的数据重新进行处理写入服务层,经过多次迭代后整体的逻辑便可以被纠正过来。
|
||||
|
||||
从我的开发经验来看,现在有很多的开发项目可能已经有了比较成熟的架构或者算法了。
|
||||
|
||||
但是如果我们平时能多思考一下现有架构的瓶颈,又或者想一想现在的架构能不能改善得更好,有了这样的思考,在学习到这些经典优秀架构之后,说不定真的能让现有的架构变得更好。
|
||||
|
||||
也就是说,作为一名优秀的架构师,“现有的架构能不能做得更好?”应该是一个需要经常思考的问题。
|
||||
|
||||
思考题
|
||||
|
||||
你所做的项目开发能否利用Lambda架构呢?在生活中有没有哪些大数据处理场景可以利用Lambda架构呢?
|
||||
|
||||
欢迎在评论中留言,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得今天有些不一样的收获,也欢迎你把这篇文章分享给你的好友。
|
||||
|
||||
|
||||
|
||||
|
145
专栏/大规模数据处理实战/11Kappa架构:利用Kafka锻造的屠龙刀.md
Normal file
145
专栏/大规模数据处理实战/11Kappa架构:利用Kafka锻造的屠龙刀.md
Normal file
@@ -0,0 +1,145 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
11 Kappa架构:利用Kafka锻造的屠龙刀
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要分享的主题是Kappa架构。
|
||||
|
||||
同样身为大规模数据处理架构,Kappa架构这把利用Kafka锻造的“屠龙刀”,它与Lambda架构的不同之处在哪里呢?
|
||||
|
||||
上一讲中,我讲述了在处理大规模数据时所用到经典架构,Lambda架构。我先来带你简要回顾一下。
|
||||
|
||||
|
||||
|
||||
Lambda架构结合了批处理和流处理的架构思想,将进入系统的大规模数据同时送入这两套架构层中,分别是批处理层(Batch Layer)和速度层(Speed Layer),同时产生两套数据结果并存入服务层。
|
||||
|
||||
批处理层有着很好的容错性,同时也因为保存着所有的历史记录,使产生的数据集具有很好的准确性。速度层可以及时地处理流入的数据,因此具有低延迟性。最终服务层将这两套数据结合,并生成一个完整的数据视图提供给用户。
|
||||
|
||||
Lambda架构也具有很好的灵活性,你可以将现有开源生态圈中不同的平台套入这个架构,具体请参照上一讲内容。
|
||||
|
||||
Lambda架构的不足
|
||||
|
||||
虽然Lambda架构使用起来十分灵活,并且可以适用于很多的应用场景,但在实际应用的时候,Lambda架构也存在着一些不足,主要表现在它的维护很复杂。
|
||||
|
||||
使用Lambda架构时,架构师需要维护两个复杂的分布式系统,并且保证他们逻辑上产生相同的结果输出到服务层中。
|
||||
|
||||
举个例子吧,我们在部署Lambda架构的时候,可以部署Apache Hadoop到批处理层上,同时部署Apache Flink到速度层上。
|
||||
|
||||
我们都知道,在分布式框架中进行编程其实是十分复杂的,尤其是我们还会针对不同的框架进行专门的优化。所以几乎每一个架构师都认同,Lambda架构在实战中维护起来具有一定的复杂性。
|
||||
|
||||
那要怎么解决这个问题呢?我们先来思考一下,造成这个架构维护起来如此复杂的根本原因是什么呢?
|
||||
|
||||
维护Lambda架构的复杂性在于我们要同时维护两套系统架构:批处理层和速度层。我们已经说过了,在架构中加入批处理层是因为从批处理层得到的结果具有高准确性,而加入速度层是因为它在处理大规模数据时具有低延时性。
|
||||
|
||||
那我们能不能改进其中某一层的架构,让它具有另外一层架构的特性呢?
|
||||
|
||||
例如,改进批处理层的系统让它具有更低的延时性,又或者是改进速度层的系统,让它产生的数据视图更具准确性和更加接近历史数据呢?
|
||||
|
||||
另外一种在大规模数据处理中常用的架构——Kappa架构(Kappa Architecture),便是在这样的思考下诞生的。
|
||||
|
||||
Kappa架构
|
||||
|
||||
Kappa架构是由LinkedIn的前首席工程师杰伊·克雷普斯(Jay Kreps)提出的一种架构思想。克雷普斯是几个著名开源项目(包括Apache Kafka和Apache Samza这样的流处理系统)的作者之一,也是现在Confluent大数据公司的CEO。
|
||||
|
||||
克雷普斯提出了一个改进Lambda架构的观点:
|
||||
|
||||
|
||||
我们能不能改进Lambda架构中速度层的系统性能,使得它也可以处理好数据的完整性和准确性问题呢?我们能不能改进Lambda架构中的速度层,使它既能够进行实时数据处理,同时也有能力在业务逻辑更新的情况下重新处理以前处理过的历史数据呢?
|
||||
|
||||
|
||||
他根据自身多年的架构经验发现,我们是可以做到这样的改进的。
|
||||
|
||||
在前面Publish–Subscribe模式那一讲中,我讲到过像Apache Kafka这样的流处理平台是具有永久保存数据日志的功能的。通过平台的这一特性,我们可以重新处理部署于速度层架构中的历史数据。
|
||||
|
||||
下面我就以Apache Kafka为例来讲述整个全新架构的过程。
|
||||
|
||||
第一步,部署Apache Kafka,并设置数据日志的保留期(Retention Period)。这里的保留期指的是你希望能够重新处理的历史数据的时间区间。
|
||||
|
||||
例如,如果你希望重新处理最多一年的历史数据,那就可以把Apache Kafka中的保留期设置为365天。如果你希望能够处理所有的历史数据,那就可以把Apache Kafka中的保留期设置为“永久(Forever)”。
|
||||
|
||||
第二步,如果我们需要改进现有的逻辑算法,那就表示我们需要对历史数据进行重新处理。
|
||||
|
||||
我们需要做的就是重新启动一个Apache Kafka作业实例(Instance)。这个作业实例将重头开始,重新计算保留好的历史数据,并将结果输出到一个新的数据视图中。我们知道Apache Kafka的底层是使用Log Offset来判断现在已经处理到哪个数据块了,所以只需要将Log Offset设置为0,新的作业实例就会重头开始处理历史数据。
|
||||
|
||||
第三步,当这个新的数据视图处理过的数据进度赶上了旧的数据视图时,我们的应用便可以切换到从新的数据视图中读取。
|
||||
|
||||
第四步,停止旧版本的作业实例,并删除旧的数据视图。
|
||||
|
||||
这个架构就如同下图所示。
|
||||
|
||||
|
||||
|
||||
与Lambda架构不同的是,Kappa架构去掉了批处理层这一体系结构,而只保留了速度层。你只需要在业务逻辑改变又或者是代码更改的时候进行数据的重新处理。
|
||||
|
||||
当然了,你也可以在我上面讲到的步骤中做一些优化。
|
||||
|
||||
例如不执行第4步,也就是不删除旧的数据视图。这样的好处是当你发现代码逻辑出错时可以及时回滚(Roll Back)到上一个版本的数据视图中去。又或者是你想在服务层提供A/B测试,保留多个数据视图版本将有助于你进行A/B测试。
|
||||
|
||||
在介绍完Kappa架构的概念后,我想通过一个实战例子,来和你进一步学习Kappa架构是如何应用在现实场景中的。
|
||||
|
||||
《纽约时报》内容管理系统架构实例
|
||||
|
||||
《纽约时报》是一个在美国纽约出版,在整个美国乃至全世界都具有相当影响力的日报。
|
||||
|
||||
|
||||
|
||||
《纽约时报》的内容管理系统收集、保存着各种各样来源的文档。这些文档有从第三方收集来的资料,也有自己报社编辑部所撰写的故事。当你访问《纽约时报》网站主页时,甚至能够查到162年前的新闻报道。
|
||||
|
||||
可想而知,要处理这么大规模的内容,并将这些内容提供于在线搜索、订阅的个性化推荐以及前端应用程序等等的服务,是一个非常棘手的任务。
|
||||
|
||||
我们先来看看他们曾经使用过的一个老式系统架构。
|
||||
|
||||
|
||||
|
||||
我们可以看到,这种系统架构是一种相当典型的基于API的架构,无论是在系统调度上还是使用场景上都存在着自身的不足。我来给你举一些例子。
|
||||
|
||||
|
||||
不同的内容API可能由不同的团队开发,从而造成API有不同的语义,也有可能需要不同的参数。
|
||||
|
||||
调用不同API所得到的内容结果可能有不同的格式,在应用端需要重新进行规范化(Standardization)。
|
||||
|
||||
如果客户端上会实时推送一些新的热点新闻或者突发新闻(Breaking News),那么在上述基于API的架构中,想要实时获知新闻的话,就需要让客户端不停地做轮询操作(Polling)。轮询操作在这里指的是客户端定期地重复调用系统API来查看是否有新的新闻内容,这无疑增加了系统的复杂性。
|
||||
|
||||
客户端很难访问以前发布过的内容。即便我们知道这些已发布过的新闻列表需要从哪里获取,进行API调用去检索每个单独的新闻列表还是需要花很长的时间。而过多的API调用又会给服务器产生很大的负荷。
|
||||
|
||||
|
||||
那现在你再来看看当《纽约时报》采取了Kappa架构之后,新的系统架构是什么样的。
|
||||
|
||||
|
||||
|
||||
首先,Kappa架构在系统调度这个层面上统一了开发接口。
|
||||
|
||||
你可以看到,中间的Kappa架构系统规范好了输入数据和输出数据的格式之后,任何需要传送到应用端的数据都必须按照这个接口输入给Kappa架构系统。而所有的应用端客户都只需要按照Kappa架构系统定义好的输出格式接收传输过来的数据。这样就解决了API规范化的问题。
|
||||
|
||||
我们再来看看增加了中间一层Kappa架构之后数据传输速度上的变化。
|
||||
|
||||
因为Apache Kafka是可以实时推送消息数据的,这样一来,任何传输进中间Kappa架构的数据都会被实时推送到接收消息的客户端中。这样就避免了在应用层面上做定期轮询,从而减少了延时。而对于重新访问或者处理发布过的新闻内容这一问题,还记得我之前和你讲述过的Kafka特性吗?只需要设置Log Offset为0就可以重新读取所有内容了。
|
||||
|
||||
在讲述完Kappa架构和它的应用实例之后,我想强调一下,Kappa架构也是有着它自身的不足的。
|
||||
|
||||
因为Kappa架构只保留了速度层而缺少批处理层,在速度层上处理大规模数据可能会有数据更新出错的情况发生,这就需要我们花费更多的时间在处理这些错误异常上面。
|
||||
|
||||
还有一点,Kappa架构的批处理和流处理都放在了速度层上,这导致了这种架构是使用同一套代码来处理算法逻辑的。所以Kappa架构并不适用于批处理和流处理代码逻辑不一致的场景。
|
||||
|
||||
小结
|
||||
|
||||
在最近两讲中,我们学习到了Lambda架构和Kappa架构这两种大规模数据处理架构,它们都各自有着自身的优缺点。我们需要按照实际情况来权衡利弊,看看我们在业务中到底需要使用到哪种架构。
|
||||
|
||||
如果你所面对的业务逻辑是设计一种稳健的机器学习模型来预测即将发生的事情,那么你应该优先考虑使用Lambda架构,因为它拥有批处理层和速度层来确保更少的错误。
|
||||
|
||||
如果你所面对的业务逻辑是希望实时性比较高,而且客户端又是根据运行时发生的实时事件来做出回应的,那么你就应该优先考虑使用Kappa架构。
|
||||
|
||||
思考题
|
||||
|
||||
在学习完Lambda架构和Kappa架构之后,你能说出Kappa架构相对Lambda架构的优势吗?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
131
专栏/大规模数据处理实战/12我们为什么需要Spark?.md
Normal file
131
专栏/大规模数据处理实战/12我们为什么需要Spark?.md
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 我们为什么需要Spark?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“我们为什么需要Spark”。
|
||||
|
||||
也许你之前没有做过大规模数据处理的项目,但是Spark这个词我相信你一定有所耳闻。
|
||||
|
||||
Spark是当今最流行的分布式大规模数据处理引擎,被广泛应用在各类大数据处理场景。
|
||||
|
||||
2009年,美国加州大学伯克利分校的AMP实验室开发了Spark。2013年,Spark成为Apache软件基金会旗下的孵化项目。
|
||||
|
||||
而现在,Spark已经成为了该基金会管理的项目中最活跃的一个。Spark社区也是成长迅速,不仅有数以千计的个人贡献者在不断地开发维护,还有很多大公司也加入了这个开源项目,如Databricks、IBM和华为。
|
||||
|
||||
在技术不断高速更迭的程序圈,一个新工具的出现与流行,必然是因为它满足了很大一部分人长期未被满足的需求,或是解决了一个长期让很多人难受的痛点。
|
||||
|
||||
所以,在学一个新技术之前,你有必要先了解这门技术出现的意义。这样,你才能更好地理解:它是应用到什么场景的?与同类工具相比,它的优缺点是什么?什么时候用它比其它工具好(或差)?……
|
||||
|
||||
至少理解了这些,你才好说自己是真正掌握了这个工具,否则只能说是浅尝辄止,半生不熟。
|
||||
|
||||
学习Spark同样是如此。
|
||||
|
||||
我们首先要问自己,既然已经有了看似很成熟的Hadoop和MapReduce,为什么我们还需要Spark?它能帮我们解决什么实际问题?相比于MapReduce,它的优势又是什么?
|
||||
|
||||
MapReduce的缺陷
|
||||
|
||||
MapReduce通过简单的Map和Reduce的抽象提供了一个编程模型,可以在一个由上百台机器组成的集群上并发处理大量的数据集,而把计算细节隐藏起来。各种各样的复杂数据处理都可以分解为Map或Reduce的基本元素。
|
||||
|
||||
这样,复杂的数据处理可以分解为由多个Job(包含一个Mapper和一个Reducer)组成的有向无环图(DAG),然后每个Mapper和Reducer放到Hadoop集群上执行,就可以得出结果。
|
||||
|
||||
我们在第一讲中讲到过MapReduce被硅谷一线公司淘汰的两大主要原因:高昂的维护成本、时间性能“达不到”用户的期待。不过除此之外,MapReduce还存在诸多局限。
|
||||
|
||||
第一,MapReduce模型的抽象层次低,大量的底层逻辑都需要开发者手工完成。
|
||||
|
||||
打个比方,写MapReduce的应用就好比用汇编语言去编写一个复杂的游戏。如果你是开发者,你会习惯用汇编语言,还是使用各种高级语言如Java、C++的现有框架呢?
|
||||
|
||||
第二,只提供Map和Reduce两个操作。
|
||||
|
||||
很多现实的数据处理场景并不适合用这个模型来描述。实现复杂的操作很有技巧性,也会让整个工程变得庞大以及难以维护。
|
||||
|
||||
举个例子,两个数据集的Join是很基本而且常用的功能,但是在MapReduce的世界中,需要对这两个数据集做一次Map和Reduce才能得到结果。这样框架对于开发者非常不友好。正如第一讲中提到的,维护一个多任务协调的状态机成本很高,而且可扩展性非常差。
|
||||
|
||||
第三,在Hadoop中,每一个Job的计算结果都会存储在HDFS文件存储系统中,所以每一步计算都要进行硬盘的读取和写入,大大增加了系统的延迟。
|
||||
|
||||
由于这一原因,MapReduce对于迭代算法的处理性能很差,而且很耗资源。因为迭代的每一步都要对HDFS进行读写,所以每一步都需要差不多的等待时间。
|
||||
|
||||
第四,只支持批数据处理,欠缺对流数据处理的支持。
|
||||
|
||||
因此,在Hadoop推出后,有很多人想办法对Hadoop进行优化,其中发展到现在最成熟的就是Spark。
|
||||
|
||||
接下来,就让我们看一下Spark是如何对上述问题进行优化的。
|
||||
|
||||
Spark的优势
|
||||
|
||||
Spark最基本的数据抽象叫作弹性分布式数据集(Resilient Distributed Dataset, RDD),它代表一个可以被分区(partition)的只读数据集,它内部可以有很多分区,每个分区又有大量的数据记录(record)。
|
||||
|
||||
RDD是Spark最基本的数据结构。Spark定义了很多对RDD的操作。对RDD的任何操作都可以像函数式编程中操作内存中的集合一样直观、简便,使得实现数据处理的代码非常简短高效。这些我们会在这一模块中的后续文章中仔细阐述。
|
||||
|
||||
Spark提供了很多对RDD的操作,如Map、Filter、flatMap、groupByKey和Union等等,极大地提升了对各种复杂场景的支持。开发者既不用再绞尽脑汁挖掘MapReduce模型的潜力,也不用维护复杂的MapReduce状态机。
|
||||
|
||||
相对于Hadoop的MapReduce会将中间数据存放到硬盘中,Spark会把中间数据缓存在内存中,从而减少了很多由于硬盘读写而导致的延迟,大大加快了处理速度。
|
||||
|
||||
Databricks团队曾经做过一个实验,他们用Spark排序一个100TB的静态数据集仅仅用时23分钟。而之前用Hadoop做到的最快记录也用了高达72分钟。此外,Spark还只用了Hadoop所用的计算资源的1/10,耗时只有Hadoop的1/3。
|
||||
|
||||
这个例子充分体现出Spark数据处理的最大优势——速度。
|
||||
|
||||
在某些需要交互式查询内存数据的场景中,Spark的性能优势更加明显。
|
||||
|
||||
根据Databricks团队的结果显示,Spark的处理速度是Hadoop的100倍。即使是对硬盘上的数据进行处理,Spark的性能也达到了Hadoop的10倍。
|
||||
|
||||
由于Spark可以把迭代过程中每一步的计算结果都缓存在内存中,所以非常适用于各类迭代算法。
|
||||
|
||||
Spark第一次启动时需要把数据载入到内存,之后的迭代可以直接在内存里利用中间结果做不落地的运算。所以,后期的迭代速度快到可以忽略不计。在当今机器学习和人工智能大热的环境下,Spark无疑是更好的数据处理引擎。
|
||||
|
||||
下图是在Spark和Hadoop上运行逻辑回归算法的运行时间对比。
|
||||
|
||||
|
||||
|
||||
可以看出,Hadoop做每一次迭代运算的时间基本相同,而Spark除了第一次载入数据到内存以外,别的迭代时间基本可以忽略。
|
||||
|
||||
在任务(task)级别上,Spark的并行机制是多线程模型,而MapReduce是多进程模型。
|
||||
|
||||
多进程模型便于细粒度控制每个任务占用的资源,但会消耗较多的启动时间。
|
||||
|
||||
而Spark同一节点上的任务以多线程的方式运行在一个JVM进程中,可以带来更快的启动速度、更高的CPU利用率,以及更好的内存共享。
|
||||
|
||||
从前文中你可以看出,Spark作为新的分布式数据处理引擎,对MapReduce进行了很多改进,使得性能大大提升,并且更加适用于新时代的数据处理场景。
|
||||
|
||||
但是,Spark并不是一个完全替代Hadoop的全新工具。
|
||||
|
||||
因为Hadoop还包含了很多组件:
|
||||
|
||||
|
||||
数据存储层:分布式文件存储系统HDFS,分布式数据库存储的HBase;
|
||||
数据处理层:进行数据处理的MapReduce,负责集群和资源管理的YARN;
|
||||
数据访问层:Hive、Pig、Mahout……
|
||||
|
||||
|
||||
从狭义上来看,Spark只是MapReduce的替代方案,大部分应用场景中,它还要依赖于HDFS和HBase来存储数据,依赖于YARN来管理集群和资源。
|
||||
|
||||
当然,Spark并不是一定要依附于Hadoop才能生存,它还可以运行在Apache Mesos、Kubernetes、standalone等其他云平台上。
|
||||
|
||||
|
||||
|
||||
此外,作为通用的数据处理平台,Spark有五个主要的扩展库,分别是支持结构化数据的Spark SQL、处理实时数据的Spark Streaming、用于机器学习的MLlib、用于图计算的GraphX、用于统计分析的SparkR。
|
||||
|
||||
这些扩展库与Spark核心API高度整合在一起,使得Spark平台可以广泛地应用在不同数据处理场景中。
|
||||
|
||||
小结
|
||||
|
||||
通过今天的学习,我们了解了Spark相较于MapReduce的主要优势,那就是快、易于开发及维护,和更高的适用性。我们还初步掌握了Spark系统的架构。
|
||||
|
||||
MapReduce作为分布式数据处理的开山鼻祖,虽然有很多缺陷,但是它的设计思想不仅没有过时,而且还影响了新的数据处理系统的设计,如Spark、Storm、Presto、Impala等。
|
||||
|
||||
Spark并没有全新的理论基础,它是一点点地在工程和学术的结合基础上做出来的。可以说,它站在了Hadoop和MapReduce两个巨人的肩膀上。在这一模块中,我们会对Spark的架构、核心概念、API以及各个扩展库进行深入的讨论,并且结合常见的应用例子进行实战演练,从而帮助你彻底掌握这一当今最流行的数据处理平台。
|
||||
|
||||
思考题
|
||||
|
||||
你认为有哪些MapReduce的缺点是在Spark框架中依然存在的?用什么思路可以解决?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
132
专栏/大规模数据处理实战/13弹性分布式数据集:Spark大厦的地基(上).md
Normal file
132
专栏/大规模数据处理实战/13弹性分布式数据集:Spark大厦的地基(上).md
Normal file
@@ -0,0 +1,132 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
13 弹性分布式数据集:Spark大厦的地基(上)
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“弹性分布式数据集”。
|
||||
|
||||
上一讲中提到,Spark最基本的数据抽象是弹性分布式数据集(Resilient Distributed Dataset, 下文用RDD代指)。
|
||||
|
||||
Spark基于RDD定义了很多数据操作,从而使得数据处理的代码十分简洁、高效。所以,要想深入学习Spark,我们必须首先理解RDD的设计思想和特性。
|
||||
|
||||
为什么需要新的数据抽象模型?
|
||||
|
||||
传统的MapReduce框架之所以运行速度缓慢,很重要的原因就是有向无环图的中间计算结果需要写入硬盘这样的稳定存储介质中来防止运行结果丢失。
|
||||
|
||||
而每次调用中间计算结果都需要要进行一次硬盘的读取,反复对硬盘进行读写操作以及潜在的数据复制和序列化操作大大提高了计算的延迟。
|
||||
|
||||
因此,很多研究人员试图提出一个新的分布式存储方案,不仅保持之前系统的稳定性、错误恢复和可扩展性,还要尽可能地减少硬盘I/O操作。
|
||||
|
||||
一个可行的设想就是在分布式内存中,存储中间计算的结果,因为对内存的读写操作速度远快于硬盘。而RDD就是一个基于分布式内存的数据抽象,它不仅支持基于工作集的应用,同时具有数据流模型的特点。
|
||||
|
||||
RDD的定义
|
||||
|
||||
弹性分布式数据集是英文直译的名字,乍一看这个名字相信你会不知所云。如果你去Google或者百度搜索它的定义,你会得到如下结果:
|
||||
|
||||
RDD表示已被分区、不可变的,并能够被并行操作的数据集合。
|
||||
|
||||
这个定义很不直观,我认识的很多Spark初学者在查阅了很多资料后还是对RDD一头雾水,很难理解这个抽象的概念。接下来,让我们一起来对这个晦涩的概念抽丝剥茧,见其真义。
|
||||
|
||||
在上述定义以及RDD的中文译名中,我们不难发现,RDD有以下基本特性:分区、不可变和并行操作。接下来让我分别讲解这些特点。
|
||||
|
||||
分区
|
||||
|
||||
顾名思义,分区代表同一个RDD包含的数据被存储在系统的不同节点中,这也是它可以被并行处理的前提。
|
||||
|
||||
逻辑上,我们可以认为RDD是一个大的数组。数组中的每个元素代表一个分区(Partition)。
|
||||
|
||||
在物理存储中,每个分区指向一个存放在内存或者硬盘中的数据块(Block),而这些数据块是独立的,它们可以被存放在系统中的不同节点。
|
||||
|
||||
所以,RDD只是抽象意义的数据集合,分区内部并不会存储具体的数据。下图很好地展示了RDD的分区逻辑结构:
|
||||
|
||||
|
||||
|
||||
RDD中的每个分区存有它在该RDD中的index。通过RDD的ID和分区的index可以唯一确定对应数据块的编号,从而通过底层存储层的接口中提取到数据进行处理。
|
||||
|
||||
在集群中,各个节点上的数据块会尽可能地存放在内存中,只有当内存没有空间时才会存入硬盘。这样可以最大化地减少硬盘读写的开销。
|
||||
|
||||
虽然 RDD 内部存储的数据是只读的,但是,我们可以去修改(例如通过 repartition 转换操作)并行计算单元的划分结构,也就是分区的数量。
|
||||
|
||||
不可变性
|
||||
|
||||
不可变性代表每一个RDD都是只读的,它所包含的分区信息不可以被改变。既然已有的RDD不可以被改变,我们只可以对现有的RDD进行转换(Transformation)操作,得到新的RDD作为中间计算的结果。从某种程度上讲,RDD与函数式编程的Collection很相似。
|
||||
|
||||
lines = sc.textFile("data.txt")
|
||||
lineLengths = lines.map(lambda s: len(s))
|
||||
totalLength = lineLengths.reduce(lambda a, b: a + b)
|
||||
|
||||
|
||||
在上述的简单例子中,我们首先读入文本文件data.txt,创建了第一个RDD lines,它的每一个元素是一行文本。然后调用map函数去映射产生第二个RDD lineLengths,每个元素代表每一行简单文本的字数。最后调用reduce函数去得到第三个RDD totalLength,它只有一个元素,代表整个文本的总字数。
|
||||
|
||||
那么这样会带来什么好处呢?显然,对于代表中间结果的RDD,我们需要记录它是通过哪个RDD进行哪些转换操作得来,即依赖关系,而不用立刻去具体存储计算出的数据本身。
|
||||
|
||||
这样做有助于提升Spark的计算效率,并且使错误恢复更加容易。
|
||||
|
||||
试想,在一个有N步的计算模型中,如果记载第N步输出RDD的节点发生故障,数据丢失,我们可以从第N-1步的RDD出发,再次计算,而无需重复整个N步计算过程。这样的容错特性也是RDD为什么是一个“弹性”的数据集的原因之一。后边我们会提到RDD如何存储这样的依赖关系。
|
||||
|
||||
并行操作
|
||||
|
||||
由于单个RDD的分区特性,使得它天然支持并行操作,即不同节点上的数据可以被分别处理,然后产生一个新的RDD。
|
||||
|
||||
RDD的结构
|
||||
|
||||
通过上述讲解,我们了解了RDD的基本特性——分区、不可变和并行计算。而且,我们还提到每一个RDD里都会包括分区信息、所依赖的父RDD以及通过怎样的转换操作才能由父RDD得来等信息。
|
||||
|
||||
实际上RDD的结构远比你想象的要复杂,让我们来看一个RDD的简易结构示意图:
|
||||
|
||||
|
||||
|
||||
SparkContext是所有Spark功能的入口,它代表了与Spark节点的连接,可以用来创建RDD对象以及在节点中的广播变量等。一个线程只有一个SparkContext。SparkConf则是一些参数配置信息。感兴趣的同学可以去阅读官方的技术文档,一些相对不重要的概念我就不再赘述了。
|
||||
|
||||
Partitions前文中我已经提到过,它代表RDD中数据的逻辑结构,每个Partition会映射到某个节点内存或硬盘的一个数据块。
|
||||
|
||||
Partitioner决定了RDD的分区方式,目前有两种主流的分区方式:Hash partitioner和Range partitioner。Hash,顾名思义就是对数据的Key进行散列分区,Range则是按照Key的排序进行均匀分区。此外我们还可以创建自定义的Partitioner。
|
||||
|
||||
依赖关系
|
||||
|
||||
Dependencies是RDD中最重要的组件之一。如前文所说,Spark不需要将每个中间计算结果进行数据复制以防数据丢失,因为每一步产生的RDD里都会存储它的依赖关系,即它是通过哪个RDD经过哪个转换操作得到的。
|
||||
|
||||
细心的读者会问这样一个问题,父RDD的分区和子RDD的分区之间是否是一对一的对应关系呢?Spark支持两种依赖关系:窄依赖(Narrow Dependency)和宽依赖(Wide Dependency)。
|
||||
|
||||
|
||||
|
||||
窄依赖就是父RDD的分区可以一一对应到子RDD的分区,宽依赖就是父RDD的每个分区可以被多个子RDD的分区使用。
|
||||
|
||||
|
||||
|
||||
显然,窄依赖允许子RDD的每个分区可以被并行处理产生,而宽依赖则必须等父RDD的所有分区都被计算好之后才能开始处理。
|
||||
|
||||
如上图所示,一些转换操作如map、filter会产生窄依赖关系,而Join、groupBy则会生成宽依赖关系。
|
||||
|
||||
这很容易理解,因为map是将分区里的每一个元素通过计算转化为另一个元素,一个分区里的数据不会跑到两个不同的分区。而groupBy则要将拥有所有分区里有相同Key的元素放到同一个目标分区,而每一个父分区都可能包含各种Key的元素,所以它可能被任意一个子分区所依赖。
|
||||
|
||||
Spark之所以要区分宽依赖和窄依赖是出于以下两点考虑:
|
||||
|
||||
|
||||
窄依赖可以支持在同一个节点上链式执行多条命令,例如在执行了 map 后,紧接着执行 filter。相反,宽依赖需要所有的父分区都是可用的,可能还需要调用类似 MapReduce 之类的操作进行跨节点传递。
|
||||
|
||||
从失败恢复的角度考虑,窄依赖的失败恢复更有效,因为它只需要重新计算丢失的父分区即可,而宽依赖牵涉到 RDD 各级的多个父分区。
|
||||
|
||||
|
||||
小结
|
||||
|
||||
弹性分布式数据集作为Spark的基本数据抽象,相较于Hadoop/MapReduce的数据模型而言,各方面都有很大的提升。
|
||||
|
||||
首先,它的数据可以尽可能地存在内存中,从而大大提高的数据处理的效率;其次它是分区存储,所以天然支持并行处理;而且它还存储了每一步骤计算结果之间的依赖关系,从而大大提升了数据容错性和错误恢复的正确率,使Spark更加可靠。
|
||||
|
||||
下一讲,我们会继续深入研究RDD的容错机制、任务执行机制,以及Spark定义在RDD上的各种转换与动作操作。
|
||||
|
||||
思考题
|
||||
|
||||
窄依赖是指父RDD的每一个分区都可以唯一对应子RDD中的分区,那么是否意味着子RDD中的一个分区只可以对应父RDD中的一个分区呢?如果子RDD的一个分区需要由父RDD中若干个分区计算得来,是否还算窄依赖?
|
||||
|
||||
最后,欢迎你把对弹性分布式数据集的疑问写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
201
专栏/大规模数据处理实战/14弹性分布式数据集:Spark大厦的地基(下).md
Normal file
201
专栏/大规模数据处理实战/14弹性分布式数据集:Spark大厦的地基(下).md
Normal file
@@ -0,0 +1,201 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
14 弹性分布式数据集:Spark大厦的地基(下)
|
||||
你好,我是蔡元楠。
|
||||
|
||||
上一讲我们介绍了弹性分布式数据集(RDD)的定义、特性以及结构,并且深入讨论了依赖关系(Dependencies)。
|
||||
|
||||
今天让我们一起来继续学习RDD的其他特性。
|
||||
|
||||
RDD的结构
|
||||
|
||||
首先,我来介绍一下RDD结构中其他的几个知识点:检查点(Checkpoint)、存储级别( Storage Level)和迭代函数(Iterator)。
|
||||
|
||||
|
||||
|
||||
通过上一讲,你应该已经知道了,基于RDD的依赖关系,如果任意一个RDD在相应的节点丢失,你只需要从上一步的RDD出发再次计算,便可恢复该RDD。
|
||||
|
||||
但是,如果一个RDD的依赖链比较长,而且中间又有多个RDD出现故障的话,进行恢复可能会非常耗费时间和计算资源。
|
||||
|
||||
而检查点(Checkpoint)的引入,就是为了优化这些情况下的数据恢复。
|
||||
|
||||
很多数据库系统都有检查点机制,在连续的transaction列表中记录某几个transaction后数据的内容,从而加快错误恢复。
|
||||
|
||||
RDD中的检查点的思想与之类似。
|
||||
|
||||
在计算过程中,对于一些计算过程比较耗时的RDD,我们可以将它缓存至硬盘或HDFS中,标记这个RDD有被检查点处理过,并且清空它的所有依赖关系。同时,给它新建一个依赖于CheckpointRDD的依赖关系,CheckpointRDD可以用来从硬盘中读取RDD和生成新的分区信息。
|
||||
|
||||
这样,当某个子RDD需要错误恢复时,回溯至该RDD,发现它被检查点记录过,就可以直接去硬盘中读取这个RDD,而无需再向前回溯计算。
|
||||
|
||||
存储级别(Storage Level)是一个枚举类型,用来记录RDD持久化时的存储级别,常用的有以下几个:
|
||||
|
||||
|
||||
MEMORY_ONLY:只缓存在内存中,如果内存空间不够则不缓存多出来的部分。这是RDD存储级别的默认值。
|
||||
|
||||
MEMORY_AND_DISK:缓存在内存中,如果空间不够则缓存在硬盘中。
|
||||
|
||||
DISK_ONLY:只缓存在硬盘中。
|
||||
|
||||
MEMORY_ONLY_2和MEMORY_AND_DISK_2等:与上面的级别功能相同,只不过每个分区在集群中两个节点上建立副本。
|
||||
|
||||
|
||||
这就是我们在前文提到过的,Spark相比于Hadoop在性能上的提升。我们可以随时把计算好的RDD缓存在内存中,以便下次计算时使用,这大幅度减小了硬盘读写的开销。
|
||||
|
||||
迭代函数(Iterator)和计算函数(Compute)是用来表示RDD怎样通过父RDD计算得到的。
|
||||
|
||||
迭代函数会首先判断缓存中是否有想要计算的RDD,如果有就直接读取,如果没有,就查找想要计算的RDD是否被检查点处理过。如果有,就直接读取,如果没有,就调用计算函数向上递归,查找父RDD进行计算。
|
||||
|
||||
到现在,相信你已经对弹性分布式数据集的基本结构有了初步了解。但是光理解RDD的结构是远远不够的,我们的终极目标是使用RDD进行数据处理。
|
||||
|
||||
要使用RDD进行数据处理,你需要先了解一些RDD的数据操作。
|
||||
|
||||
在第12讲中,我曾经提过,相比起MapReduce只支持两种数据操作,Spark支持大量的基本操作,从而减轻了程序员的负担。
|
||||
|
||||
接下来,让我们进一步了解基于RDD的各种数据操作。
|
||||
|
||||
RDD的转换操作
|
||||
|
||||
RDD的数据操作分为两种:转换(Transformation)和动作(Action)。
|
||||
|
||||
顾名思义,转换是用来把一个RDD转换成另一个RDD,而动作则是通过计算返回一个结果。
|
||||
|
||||
不难想到,之前举例的map、filter、groupByKey等都属于转换操作。
|
||||
|
||||
Map
|
||||
|
||||
map是最基本的转换操作。
|
||||
|
||||
与MapReduce中的map一样,它把一个RDD中的所有数据通过一个函数,映射成一个新的RDD,任何原RDD中的元素在新RDD中都有且只有一个元素与之对应。
|
||||
|
||||
在这一讲中提到的所有的操作,我都会使用代码举例,帮助你更好地理解。
|
||||
|
||||
rdd = sc.parallelize(["b", "a", "c"])
|
||||
rdd2 = rdd.map(lambda x: (x, 1)) // [('b', 1), ('a', 1), ('c', 1)]
|
||||
|
||||
|
||||
Filter
|
||||
|
||||
filter这个操作,是选择原RDD里所有数据中满足某个特定条件的数据,去返回一个新的RDD。如下例所示,通过filter,只选出了所有的偶数。
|
||||
|
||||
rdd = sc.parallelize([1, 2, 3, 4, 5])
|
||||
rdd2 = rdd.filter(lambda x: x % 2 == 0) // [2, 4]
|
||||
|
||||
|
||||
mapPartitions
|
||||
|
||||
mapPartitions是map的变种。不同于map的输入函数是应用于RDD中每个元素,mapPartitions的输入函数是应用于RDD的每个分区,也就是把每个分区中的内容作为整体来处理的,所以输入函数的类型是Iterator[T] => Iterator[U]。
|
||||
|
||||
rdd = sc.parallelize([1, 2, 3, 4], 2)
|
||||
def f(iterator): yield sum(iterator)
|
||||
rdd2 = rdd.mapPartitions(f) // [3, 7]
|
||||
|
||||
|
||||
在mapPartitions的例子中,我们首先创建了一个有两个分区的RDD。mapPartitions的输入函数是对每个分区内的元素求和,所以返回的RDD包含两个元素:1+2=3 和3+4=7。
|
||||
|
||||
groupByKey
|
||||
|
||||
groupByKey和SQL中的groupBy类似,是把对象的集合按某个Key来归类,返回的RDD中每个Key对应一个序列。
|
||||
|
||||
rdd = sc.parallelize([("a", 1), ("b", 1), ("a", 2)])
|
||||
rdd.groupByKey().collect()
|
||||
//"a" [1, 2]
|
||||
//"b" [1]
|
||||
|
||||
|
||||
在此,我们只列举这几个常用的、有代表性的操作,对其他转换操作感兴趣的同学可以去自行查阅官方的API文档。
|
||||
|
||||
RDD的动作操作
|
||||
|
||||
让我们再来看几个常用的动作操作。
|
||||
|
||||
Collect
|
||||
|
||||
RDD中的动作操作collect与函数式编程中的collect类似,它会以数组的形式,返回RDD的所有元素。需要注意的是,collect操作只有在输出数组所含的数据数量较小时使用,因为所有的数据都会载入到程序的内存中,如果数据量较大,会占用大量JVM内存,导致内存溢出。
|
||||
|
||||
rdd = sc.parallelize(["b", "a", "c"])
|
||||
rdd.map(lambda x: (x, 1)).collect() // [('b', 1), ('a', 1), ('c', 1)]
|
||||
|
||||
|
||||
实际上,上述转换操作中所有的例子,最后都需要将RDD的元素collect成数组才能得到标记好的输出。
|
||||
|
||||
Reduce
|
||||
|
||||
与MapReduce中的reduce类似,它会把RDD中的元素根据一个输入函数聚合起来。
|
||||
|
||||
from operator import add
|
||||
sc.parallelize([1, 2, 3, 4, 5]).reduce(add) // 15
|
||||
|
||||
|
||||
Count
|
||||
|
||||
Count会返回RDD中元素的个数。
|
||||
|
||||
sc.parallelize([2, 3, 4]).count() // 3
|
||||
|
||||
CountByKey
|
||||
|
||||
仅适用于Key-Value pair类型的 RDD,返回具有每个 key 的计数的的map。
|
||||
|
||||
rdd = sc.parallelize([("a", 1), ("b", 1), ("a", 1)])
|
||||
sorted(rdd.countByKey().items()) // [('a', 2), ('b', 1)]
|
||||
|
||||
|
||||
讲到这,你可能会问了,为什么要区分转换和动作呢?虽然转换是生成新的RDD,动作是把RDD进行计算生成一个结果,它们本质上不都是计算吗?
|
||||
|
||||
这是因为,所有转换操作都很懒,它只是生成新的RDD,并且记录依赖关系。
|
||||
|
||||
但是Spark并不会立刻计算出新RDD中各个分区的数值。直到遇到一个动作时,数据才会被计算,并且输出结果给Driver。
|
||||
|
||||
比如,在之前的例子中,你先对RDD进行map转换,再进行collect动作,这时map后生成的RDD不会立即被计算。只有当执行到collect操作时,map才会被计算。而且,map之后得到的较大的数据量并不会传给Driver,只有collect动作的结果才会传递给Driver。
|
||||
|
||||
这种惰性求值的设计优势是什么呢?让我们来看这样一个例子。
|
||||
|
||||
假设,你要从一个很大的文本文件中筛选出包含某个词语的行,然后返回第一个这样的文本行。你需要先读取文件textFile()生成rdd1,然后使用filter()方法生成rdd2,最后是行动操作first(),返回第一个元素。
|
||||
|
||||
读取文件的时候会把所有的行都存储起来,但我们马上就要筛选出只具有特定词组的行了,等筛选出来之后又要求只输出第一个。这样是不是太浪费存储空间了呢?确实。
|
||||
|
||||
所以实际上,Spark是在行动操作first()的时候开始真正的运算:只扫描第一个匹配的行,不需要读取整个文件。所以,惰性求值的设计可以让Spark的运算更加高效和快速。
|
||||
|
||||
让我们总结一下Spark执行操作的流程吧。
|
||||
|
||||
Spark在每次转换操作的时候,使用了新产生的 RDD 来记录计算逻辑,这样就把作用在 RDD 上的所有计算逻辑串起来,形成了一个链条。当对 RDD 进行动作时,Spark 会从计算链的最后一个RDD开始,依次从上一个RDD获取数据并执行计算逻辑,最后输出结果。
|
||||
|
||||
RDD的持久化(缓存)
|
||||
|
||||
每当我们对RDD调用一个新的action操作时,整个RDD都会从头开始运算。因此,如果某个RDD会被反复重用的话,每次都从头计算非常低效,我们应该对多次使用的RDD进行一个持久化操作。
|
||||
|
||||
Spark的persist()和cache()方法支持将RDD的数据缓存至内存或硬盘中,这样当下次对同一RDD进行Action操作时,可以直接读取RDD的结果,大幅提高了Spark的计算效率。
|
||||
|
||||
rdd = sc.parallelize([1, 2, 3, 4, 5])
|
||||
rdd1 = rdd.map(lambda x: x+5)
|
||||
rdd2 = rdd1.filter(lambda x: x % 2 == 0)
|
||||
rdd2.persist()
|
||||
count = rdd2.count() // 3
|
||||
first = rdd2.first() // 6
|
||||
rdd2.unpersist()
|
||||
|
||||
|
||||
在文中的代码例子中你可以看到,我们对RDD2进行了多个不同的action操作。由于在第四行我把RDD2的结果缓存在内存中,所以Spark无需从一开始的rdd开始算起了(持久化处理过的RDD只有第一次有action操作时才会从源头计算,之后就把结果存储下来,所以在这个例子中,count需要从源头开始计算,而first不需要)。
|
||||
|
||||
在缓存RDD的时候,它所有的依赖关系也会被一并存下来。所以持久化的RDD有自动的容错机制。如果RDD的任一分区丢失了,通过使用原先创建它的转换操作,它将会被自动重算。
|
||||
|
||||
持久化可以选择不同的存储级别。正如我们讲RDD的结构时提到的一样,有MEMORY_ONLY,MEMORY_AND_DISK,DISK_ONLY等。cache()方法会默认取MEMORY_ONLY这一级别。
|
||||
|
||||
小结
|
||||
|
||||
Spark在每次转换操作的时候使用了新产生的 RDD 来记录计算逻辑,这样就把作用在 RDD 上的所有计算逻辑串起来形成了一个链条,但是并不会真的去计算结果。当对 RDD 进行动作Action时,Spark 会从计算链的最后一个RDD开始,利用迭代函数(Iterator)和计算函数(Compute),依次从上一个RDD获取数据并执行计算逻辑,最后输出结果。
|
||||
|
||||
此外,我们可以通过将一些需要复杂计算和经常调用的RDD进行持久化处理,从而提升计算效率。
|
||||
|
||||
思考题
|
||||
|
||||
对RDD进行持久化操作和记录Checkpoint,有什么区别呢?
|
||||
|
||||
欢迎你把对弹性分布式数据集的疑问写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
169
专栏/大规模数据处理实战/15SparkSQL:Spark数据查询的利器.md
Normal file
169
专栏/大规模数据处理实战/15SparkSQL:Spark数据查询的利器.md
Normal file
@@ -0,0 +1,169 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
15 Spark SQL:Spark数据查询的利器
|
||||
你好,我是蔡元楠。
|
||||
|
||||
上一讲中,我介绍了弹性分布式数据集的特性和它支持的各种数据操作。
|
||||
|
||||
不过在实际的开发过程中,我们并不是总需要在RDD的层次进行编程。
|
||||
|
||||
就好比编程刚发明的年代,工程师只能用汇编语言,到后来才慢慢发展出高级语言,如Basic、C、Java等。使用高级语言大大提升了开发者的效率。
|
||||
|
||||
同样的,Spark生态系统也提供很多库,让我们在不同的场景中使用。
|
||||
|
||||
今天,让我们来一起探讨Spark最常用的数据查询模块——Spark SQL。
|
||||
|
||||
Spark SQL 发展历史
|
||||
|
||||
几年前,Hadoop/MapReduce在企业生产中的大量使用,HDFS上积累了大量数据。
|
||||
|
||||
由于MapReduce对于开发者而言使用难度较大,大部分开发人员最熟悉的还是传统的关系型数据库。
|
||||
|
||||
为了方便大多数开发人员使用Hadoop,Hive应运而生。
|
||||
|
||||
Hive提供类似SQL的编程接口,HQL语句经过语法解析、逻辑计划、物理计划转化成MapReduce程序执行,使得开发人员很容易对HDFS上存储的数据进行查询和分析。
|
||||
|
||||
在Spark刚问世的时候,Spark团队也开发了一个Shark来支持用SQL语言来查询Spark的数据。
|
||||
|
||||
Shark的本质就是Hive,它修改了Hive的内存管理模块,大幅优化了运行速度,是Hive的10倍到100倍之多。
|
||||
|
||||
但是,Shark对于Hive的依赖严重影响了Spark的发展。Spark想要定义的是一个统一的技术栈和完整的生态,不可能允许有这样的外在依赖。
|
||||
|
||||
试想,如果Spark想发布新的功能还需要等Hive的更新,那么势必会很难执行。此外,依赖于Hive还制约了Spark各个组件的相互集成,Shark也无法利用Spark的特性进行深度优化。
|
||||
|
||||
|
||||
|
||||
所以,2014年7月1日,Spark团队就将Shark交给Hive进行管理,转而开发了SparkSQL。
|
||||
|
||||
SparkSQL摒弃了Shark的(将SQL语句转化为Spark RDD的)执行引擎,换成自己团队重新开发的执行引擎。
|
||||
|
||||
Spark SQL不仅将关系型数据库的处理模式和Spark的函数式编程相结合,还兼容多种数据格式,包括Hive、RDD、JSON文件、CSV文件等。
|
||||
|
||||
可以说,Spark SQL的问世大大加快了Spark生态的发展。
|
||||
|
||||
Spark SQL的架构
|
||||
|
||||
Spark SQL本质上是一个库。它运行在Spark的核心执行引擎之上。
|
||||
|
||||
|
||||
|
||||
如上图所示,它提供类似于SQL的操作接口,允许数据仓库应用程序直接获取数据,允许使用者通过命令行操作来交互地查询数据,还提供两个API:DataFrame API和DataSet API。
|
||||
|
||||
Java、Python和Scala的应用程序可以通过这两个API来读取和写入RDD。
|
||||
|
||||
此外,正如我们在上一讲介绍的,应用程序还可以直接操作RDD。
|
||||
|
||||
使用Spark SQL会让开发者觉得好像是在操作一个关系型数据库一样,而不是在操作RDD。这是它优于原生的RDD API的地方。
|
||||
|
||||
与基本的Spark RDD API不同,Spark SQL提供的接口为Spark提供了关于数据结构和正在执行的计算的更多信息。
|
||||
|
||||
在内部,Spark SQL使用这些额外的信息来执行额外的优化。虽然Spark SQL支持多种交互方式,但是在计算结果时均使用相同的执行引擎。
|
||||
|
||||
这种统一意味着开发人员可以轻松地在不同的API之间来回切换,基于这些API提供了表达给定转换的最自然的方式。
|
||||
|
||||
接下来让我们进一步了解DataSet和DataFrame。
|
||||
|
||||
DataSet
|
||||
|
||||
DataSet,顾名思义,就是数据集的意思,它是Spark 1.6新引入的接口。
|
||||
|
||||
同弹性分布式数据集类似,DataSet也是不可变分布式的数据单元,它既有与RDD类似的各种转换和动作函数定义,而且还享受Spark SQL优化过的执行引擎,使得数据搜索效率更高。
|
||||
|
||||
DataSet支持的转换和动作也和RDD类似,比如map、filter、select、count、show及把数据写入文件系统中。
|
||||
|
||||
同样地,DataSet上的转换操作也不会被立刻执行,只是先生成新的DataSet,只有当遇到动作操作,才会把之前的转换操作一并执行,生成结果。
|
||||
|
||||
所以,DataSet的内部结构包含了逻辑计划,即生成该数据集所需要的运算。
|
||||
|
||||
当动作操作执行时,Spark SQL的查询优化器会优化这个逻辑计划,并生成一个可以分布式执行的、包含分区信息的物理计划。
|
||||
|
||||
那么,DataSet和RDD的区别是什么呢?
|
||||
|
||||
通过之前的叙述,我们知道DataSet API是Spark SQL的一个组件。那么,你应该能很容易地联想到,DataSet也具有关系型数据库中表的特性。
|
||||
|
||||
是的,DataSet所描述的数据都被组织到有名字的列中,就像关系型数据库中的表一样。
|
||||
|
||||
|
||||
|
||||
如上图所示,左侧的RDD虽然以People为类型参数,但Spark框架本身不了解People类的内部结构。所有的操作都以People为单位执行。
|
||||
|
||||
而右侧的DataSet却提供了详细的结构信息与每列的数据类型。
|
||||
|
||||
这让Spark SQL可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。也就是说,DataSet提供数据表的schema信息。这样的结构使得DataSet API的执行效率更高。
|
||||
|
||||
试想,如果我们要查询People的年龄信息,Spark SQL执行的时候可以依靠查询优化器仅仅把需要的那一列取出来,其他列的信息根本就不需要去读取了。所以,有了这些信息以后在编译的时候能够做更多的优化。
|
||||
|
||||
其次,由于DataSet存储了每列的数据类型。所以,在程序编译时可以执行类型检测。
|
||||
|
||||
DataFrame
|
||||
|
||||
DataFrame可以被看作是一种特殊的DataSet。它也是关系型数据库中表一样的结构化存储机制,也是分布式不可变的数据结构。
|
||||
|
||||
但是,它的每一列并不存储类型信息,所以在编译时并不能发现类型错误。DataFrame每一行的类型固定为Row,他可以被当作DataSet[Row]来处理,我们必须要通过解析才能获取各列的值。
|
||||
|
||||
所以,对于DataSet我们可以用类似people.name来访问一个人的名字,而对于DataFrame我们一定要用类似people.get As [String] (“name”)来访问。
|
||||
|
||||
RDD、DataFrame、DataSet对比
|
||||
|
||||
学习Spark到现在,我们已经接触了三种基本的数据结构:RDD、DataFrame和DataSet。接下来你的表格中,你可以看到它们的异同点,思考一下怎样在实际工程中选择。
|
||||
|
||||
|
||||
|
||||
发展历史
|
||||
|
||||
从发展历史上来看,RDD API在第一代Spark中就存在,是整个Spark框架的基石。
|
||||
|
||||
接下来,为了方便熟悉关系型数据库和SQL的开发人员使用,在RDD的基础上,Spark创建了DataFrame API。依靠它,我们可以方便地对数据的列进行操作。
|
||||
|
||||
DataSet最早被加入Spark SQL是在Spark 1.6,它在DataFrame的基础上添加了对数据的每一列的类型的限制。
|
||||
|
||||
在Spark 2.0中,DataFrame和DataSet被统一。DataFrame作为DataSet[Row]存在。在弱类型的语言,如Python中,DataFrame API依然存在,但是在Java中,DataFrame API已经不复存在了。
|
||||
|
||||
不变性与分区
|
||||
|
||||
由于DataSet和DataFrame都是基于RDD的,所以它们都拥有RDD的基本特性,在此不做赘述。而且我们可以通过简单的 API在 DataFrame或 Dataset与RDD之间进行无缝切换。
|
||||
|
||||
性能
|
||||
|
||||
DataFrame和DataSet的性能要比RDD更好。
|
||||
|
||||
Spark程序运行时,Spark SQL中的查询优化器会对语句进行分析,并生成优化过的RDD在底层执行。
|
||||
|
||||
举个例子,如果我们想先对一堆数据进行GroupBy再进行Filter操作,这无疑是低效的,因为我们并不需要对所有数据都GroupBy。
|
||||
|
||||
如果用RDD API实现这一语句,在执行时它只会机械地按顺序执行。而如果用DataFrame/DataSet API,Spark SQL的Catalyst优化器会将Filter操作和GroupBy操作调换顺序,从而提高执行效率。
|
||||
|
||||
下图反映了这一优化过程。
|
||||
|
||||
|
||||
|
||||
错误检测
|
||||
|
||||
RDD和DataSet都是类型安全的,而DataFrame并不是类型安全的。这是因为它不存储每一列的信息如名字和类型。
|
||||
|
||||
使用DataFrame API时,我们可以选择一个并不存在的列,这个错误只有在代码被执行时才会抛出。如果使用DataSet API,在编译时就会检测到这个错误。
|
||||
|
||||
小结
|
||||
|
||||
DataFrame和DataSet是Spark SQL提供的基于RDD的结构化数据抽象。
|
||||
|
||||
它既有RDD不可变、分区、存储依赖关系等特性,又拥有类似于关系型数据库的结构化信息。
|
||||
|
||||
所以,基于DataFrame和DataSet API开发出的程序会被自动优化,使得开发人员不需要操作底层的RDD API来进行手动优化,大大提升开发效率。
|
||||
|
||||
但是RDD API对于非结构化的数据处理有独特的优势,比如文本流数据,而且更方便我们做底层的操作。所以在开发中,我们还是需要根据实际情况来选择使用哪种API。
|
||||
|
||||
思考题
|
||||
|
||||
什么场景适合使用DataFrame API,什么场景适合使用DataSet API?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
141
专栏/大规模数据处理实战/16SparkStreaming:Spark的实时流计算API.md
Normal file
141
专栏/大规模数据处理实战/16SparkStreaming:Spark的实时流计算API.md
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
16 Spark Streaming:Spark的实时流计算API
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的内容是“Spark Streaming”。
|
||||
|
||||
通过上一讲的内容,我们深入了解了Spark SQL API。通过它,我们可以像查询关系型数据库一样查询Spark的数据,并且对原生数据做相应的转换和动作。
|
||||
|
||||
但是,无论是DataFrame API还是DataSet API,都是基于批处理模式对静态数据进行处理的。比如,在每天某个特定的时间对一天的日志进行处理分析。
|
||||
|
||||
在第二章中你已经知道了,批处理和流处理是大数据处理最常见的两个场景。那么作为当下最流行的大数据处理平台之一,Spark是否支持流处理呢?
|
||||
|
||||
答案是肯定的。
|
||||
|
||||
早在2013年,Spark的流处理组件Spark Streaming就发布了。之后经过好几年的迭代与改进,现在的Spark Streaming已经非常成熟,在业界应用十分广泛。
|
||||
|
||||
今天就让我们一起揭开Spark Streaming的神秘面纱,让它成为我们手中的利器。
|
||||
|
||||
Spark Streaming的原理
|
||||
|
||||
Spark Streaming的原理与微积分的思想很类似。
|
||||
|
||||
在大学的微积分课上,你的老师一定说过,微分就是无限细分,积分就是对无限细分的每一段进行求和。它本质上把一个连续的问题转换成了无限个离散的问题。
|
||||
|
||||
比如,用微积分思想求下图中阴影部分S的面积。
|
||||
|
||||
|
||||
|
||||
我们可以把S无限细分成无数个小矩形,因为矩形的宽足够短,所以它顶端的边近似是一个直线。这样,把容易计算的矩形面积相加,就得到不容易直接计算的不规则图形面积。
|
||||
|
||||
你知道,流处理的数据是一系列连续不断变化,且无边界的。我们永远无法预测下一秒的数据是什么样。Spark Streaming用时间片拆分了无限的数据流,然后对每一个数据片用类似于批处理的方法进行处理,输出的数据也是一块一块的。如下图所示。
|
||||
|
||||
|
||||
|
||||
Spark Streaming提供一个对于流数据的抽象DStream。DStream可以由来自Apache Kafka、Flume或者HDFS的流数据生成,也可以由别的DStream经过各种转换操作得来。讲到这里,你是不是觉得内容似曾相识?
|
||||
|
||||
没错,底层DStream也是由很多个序列化的RDD构成,按时间片(比如一秒)切分成的每个数据单位都是一个RDD。然后,Spark核心引擎将对DStream的Transformation操作变为针对Spark中对 RDD的Transformation操作,将RDD经过操作变成中间结果保存在内存中。
|
||||
|
||||
之前的DataFrame和DataSet也是同样基于RDD,所以说RDD是Spark最基本的数据抽象。就像Java里的基本数据类型(Primitive Type)一样,所有的数据都可以用基本数据类型描述。
|
||||
|
||||
也正是因为这样,无论是DataFrame,还是DStream,都具有RDD的不可变性、分区性和容错性等特质。
|
||||
|
||||
所以,Spark是一个高度统一的平台,所有的高级API都有相同的性质,它们之间可以很容易地相互转化。Spark的野心就是用这一套工具统一所有数据处理的场景。
|
||||
|
||||
由于Spark Streaming将底层的细节封装起来了,所以对于开发者来说,只需要操作DStream就行。接下来,让我们一起学习DStream的结构以及它支持的转换操作。
|
||||
|
||||
DStream
|
||||
|
||||
下图就是DStream的内部形式,即一个连续的RDD序列,每一个RDD代表一个时间窗口的输入数据流。
|
||||
|
||||
|
||||
|
||||
对DStream的转换操作,意味着对它包含的每一个RDD进行同样的转换操作。比如下边的例子。
|
||||
|
||||
sc = SparkContext(master, appName)
|
||||
ssc = StreamingContext(sc, 1)
|
||||
lines = sc.socketTextStream("localhost", 9999)
|
||||
words = lines.flatMap(lambda line: line.split(" "))
|
||||
|
||||
|
||||
|
||||
|
||||
首先,我们创建了一个lines的DStream,去监听来自本机9999端口的数据流,每一个数据代表一行文本。然后,对lines进行flatMap的转换操作,把每一个文本行拆分成词语。
|
||||
|
||||
本质上,对一个DStream进行flatMap操作,就是对它里边的每一个RDD进行flatMap操作,生成了一系列新的RDD,构成了一个新的代表词语的DStream。
|
||||
|
||||
正因为DStream和RDD的关系,RDD支持的所有转换操作,DStream都支持,比如map、flatMap、filter、union等。这些操作我们在前边学习RDD时都详细介绍过,在此不做赘述。
|
||||
|
||||
此外,DStream还有一些特有操作,如滑动窗口操作,我们可以一起探讨。
|
||||
|
||||
滑动窗口操作
|
||||
|
||||
任何Spark Streaming的程序都要首先创建一个StreamingContext的对象,它是所有Streaming操作的入口。
|
||||
|
||||
比如,我们可以通过StreamingContext来创建DStream。前边提到的例子中,lines这个DStream就是由名为sc的StreamingContext创建的。
|
||||
|
||||
StreamingContext中最重要的参数是批处理的时间间隔,即把流数据细分成数据块的粒度。
|
||||
|
||||
这个时间间隔决定了流处理的延迟性,所以,需要我们根据需求和资源来权衡间隔的长度。上边的例子中,我们把输入的数据流以秒为单位划分,每一秒的数据会生成一个RDD进行运算。
|
||||
|
||||
有些场景中,我们需要每隔一段时间,统计过去某个时间段内的数据。比如,对热点搜索词语进行统计,每隔10秒钟输出过去60秒内排名前十位的热点词。这是流处理的一个基本应用场景,很多流处理框架如Apache Flink都有原生的支持。所以,Spark也同样支持滑动窗口操作。
|
||||
|
||||
从统计热点词这个例子,你可以看出滑动窗口操作有两个基本参数:
|
||||
|
||||
|
||||
窗口长度(window length):每次统计的数据的时间跨度,在例子中是60秒;
|
||||
滑动间隔(sliding interval):每次统计的时间间隔,在例子中是10秒。
|
||||
|
||||
|
||||
显然,由于Spark Streaming流处理的最小时间单位就是StreamingContext的时间间隔,所以这两个参数一定是它的整数倍。
|
||||
|
||||
|
||||
|
||||
最基本的滑动窗口操作是window,它可以返回一个新的DStream,这个DStream中每个RDD代表一段时间窗口内的数据,如下例所示。
|
||||
|
||||
windowed_words = words.window(60, 10)
|
||||
|
||||
|
||||
windowed_words代表的就是热词统计例子中我们所需的DStream,即它里边每一个数据块都包含过去60秒内的词语,而且这样的块每10秒钟就会生成一个。
|
||||
|
||||
此外,Spark Streaming还支持一些“进阶”窗口操作。如countByWindow、reduceByWindow、reduceByKeyAndWindow和countByValueAndWindow,在此不做深入讨论。
|
||||
|
||||
Spark Streaming的优缺点
|
||||
|
||||
讲了这么多Spark Streaming,不管内部实现也好,支持的API也好,我们还并不明白它的优势是什么,相比起其他流处理框架的缺点是什么。只有明白了这些,才能帮助我们在实际工作中决定是否使用Spark Streaming。
|
||||
|
||||
首先,Spark Streaming的优点很明显,由于它的底层是基于RDD实现的,所以RDD的优良特性在它这里都有体现。
|
||||
|
||||
比如,数据容错性,如果RDD 的某些分区丢失了,可以通过依赖信息重新计算恢复。
|
||||
|
||||
再比如运行速度,DStream同样也能通过persist()方法将数据流存放在内存中。这样做的好处是遇到需要多次迭代计算的程序时,速度优势十分明显。
|
||||
|
||||
而且,Spark Streaming是Spark生态的一部分。所以,它可以和Spark的核心引擎、Spark SQL、MLlib等无缝衔接。换句话说,对实时处理出来的中间数据,我们可以立即在程序中无缝进行批处理、交互式查询等操作。这个特点大大增强了Spark Streaming的优势和功能,使得基于Spark Streaming的应用程序很容易扩展。
|
||||
|
||||
而Spark Streaming的主要缺点是实时计算延迟较高,一般在秒的级别。这是由于Spark Streaming不支持太小的批处理的时间间隔。
|
||||
|
||||
在第二章中,我们讲过准实时和实时系统,无疑Spark Streaming是一个准实时系统。别的流处理框架,如Storm的延迟性就好很多,可以做到毫秒级。
|
||||
|
||||
小结
|
||||
|
||||
Spark Streaming,作为Spark中的流处理组件,把连续的流数据按时间间隔划分为一个个数据块,然后对每个数据块分别进行批处理。
|
||||
|
||||
在内部,每个数据块就是一个RDD,所以Spark Streaming有RDD的所有优点,处理速度快,数据容错性好,支持高度并行计算。
|
||||
|
||||
但是,它的实时延迟相比起别的流处理框架比较高。在实际工作中,我们还是要具体情况具体分析,选择正确的处理框架。
|
||||
|
||||
思考题
|
||||
|
||||
如果想要优化一个Spark Streaming程序,你会从哪些角度入手?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
175
专栏/大规模数据处理实战/17StructuredStreaming:如何用DataFrameAPI进行实时数据分析_.md
Normal file
175
专栏/大规模数据处理实战/17StructuredStreaming:如何用DataFrameAPI进行实时数据分析_.md
Normal file
@@ -0,0 +1,175 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
17 Structured Streaming:如何用DataFrame API进行实时数据分析_
|
||||
你好,我是蔡元楠。
|
||||
|
||||
上一讲中,我们介绍了Spark中的流处理库Spark Streaming。它将无边界的流数据抽象成DStream,按特定的时间间隔,把数据流分割成一个个RDD进行批处理。所以,DStream API与RDD API高度相似,也拥有RDD的各种性质。
|
||||
|
||||
在第15讲中,我们比较过RDD和DataSet/DataFrame。你还记得DataSet/DataFrame的优点吗?你有没有想过,既然已经有了RDD API,我们为什么还要引入DataSet/DataFrame呢?
|
||||
|
||||
让我们来回顾一下DataSet/DataFrame的优点(为了方便描述,下文中我们统一用DataFrame来代指DataSet和DataFrame):
|
||||
|
||||
|
||||
DataFrame 是高级API,提供类似于SQL的query接口,方便熟悉关系型数据库的开发人员使用;
|
||||
Spark SQL执行引擎会自动优化DataFrame程序,而用RDD API开发的程序本质上需要工程师自己构造RDD的DAG执行图,所以依赖于工程师自己去优化。
|
||||
|
||||
|
||||
那么我们自然会想到,如果可以拥有一个基于DataFrame API的流处理模块,作为工程师的我们就不需要去用相对low level的DStream API去处理无边界数据,这样会大大提升我们的开发效率。
|
||||
|
||||
基于这个思想,2016年,Spark在其2.0版本中推出了结构化流数据处理的模块Structured Streaming。
|
||||
|
||||
Structured Streaming是基于Spark SQL引擎实现的,依靠Structured Streaming,在开发者眼里,流数据和静态数据没有区别。我们完全可以像批处理静态数据那样去处理流数据。随着流数据的持续输入,Spark SQL引擎会帮助我们持续地处理新数据,并且更新计算结果。
|
||||
|
||||
今天,就让我们来一起学习Structured Streaming的原理以及应用。
|
||||
|
||||
Structured Streaming模型
|
||||
|
||||
流数据处理最基本的问题就是如何对不断更新的无边界数据建模。
|
||||
|
||||
之前讲的Spark Streaming就是把流数据按一定的时间间隔分割成许多个小的数据块进行批处理。在Structured Streaming的模型中,我们要把数据看成一个无边界的关系型的数据表。每一个数据都是表中的一行,不断会有新的数据行被添加到表里来。我们可以对这个表做任何类似批处理的查询,Spark会帮我们不断对新加入的数据进行处理,并更新计算结果。
|
||||
|
||||
|
||||
|
||||
与Spark Streaming类似,Structured Streaming也是将输入的数据流按照时间间隔(以一秒为例)划分成数据段。每一秒都会把新输入的数据添加到表中,Spark也会每秒更新输出结果。输出结果也是表的形式,输出表可以写入硬盘或者HDFS。
|
||||
|
||||
这里我要介绍一下Structured Streaming的三种输出模式。
|
||||
|
||||
|
||||
完全模式(Complete Mode):整个更新过的输出表都被写入外部存储;
|
||||
附加模式(Append Mode):上一次触发之后新增加的行才会被写入外部存储。如果老数据有改动则不适合这个模式;
|
||||
更新模式(Update Mode):上一次触发之后被更新的行才会被写入外部存储。
|
||||
|
||||
|
||||
需要注意的是,Structured Streaming并不会完全存储输入数据。每个时间间隔它都会读取最新的输入,进行处理,更新输出表,然后把这次的输入删除。Structured Streaming只会存储更新输出表所需要的信息。
|
||||
|
||||
Structured Streaming的模型在根据事件时间(Event Time)处理数据时十分方便。
|
||||
|
||||
我们在第六讲中曾经讲过事件时间和处理时间(Processing Time)的区别。这里我再简单说一下。事件时间指的是事件发生的时间,是数据本身的属性;而处理时间指的是Spark接收到数据的时间。
|
||||
|
||||
很多情况下,我们需要基于事件时间来处理数据。比如说,统计每个小时接到的订单数量,一个订单很有可能在12:59被创建,但是到了13:01才被处理。
|
||||
|
||||
在Structured Streaming的模型中,由于每个数据都是输入数据表中的一行,那么事件时间就是行中的一列。依靠DataFrame API提供的类似于SQL的接口,我们可以很方便地执行基于时间窗口的查询。
|
||||
|
||||
Streaming DataFrame API
|
||||
|
||||
在Structured Streaming发布以后,DataFrame既可以代表静态的有边界数据,也可以代表无边界数据。之前对静态DataFrame的各种操作同样也适用于流式DataFrame。接下来,让我们看几个例子。
|
||||
|
||||
创建DataFrame
|
||||
|
||||
SparkSession.readStream()返回的DataStreamReader可以用于创建流DataFrame。它支持多种类型的数据流作为输入,比如文件、Kafka、socket等。
|
||||
|
||||
socketDataFrame = spark
|
||||
.readStream
|
||||
.format("socket")
|
||||
.option("host", "localhost")
|
||||
.option("port", 9999)
|
||||
.load()
|
||||
|
||||
|
||||
上边的代码例子创建了一个DataFrame,用来监听来自localhost:9999的数据流。
|
||||
|
||||
基本的查询操作
|
||||
|
||||
流DataFrame同静态DataFrame一样,不仅支持类似SQL的查询操作(如select和where等),还支持RDD的转换操作(如map和filter)。 让我们一起来看下面的例子。
|
||||
|
||||
假设我们已经有一个DataFrame代表一个学生的数据流,即每个数据是一个学生,每个学生有名字(name)、年龄(age)、身高(height)和年级(grade)四个属性,我们可以用DataFrame API去做类似于SQL的Query。
|
||||
|
||||
df = … // 这个DataFrame代表学校学生的数据流,schema是{name: string, age: number, height: number, grade: string}
|
||||
df.select("name").where("age > 10") // 返回年龄大于10岁的学生名字列表
|
||||
df.groupBy("grade").count() // 返回每个年级学生的人数
|
||||
df.sort_values([‘age’], ascending=False).head(100) //返回100个年龄最大的学生
|
||||
|
||||
|
||||
在这个例子中,通过第二行我们可以得到所有年龄在10岁以上的学生名字,第三行可以得到每个年级学生的人数,第四行得到100个年龄最大的学生信息。此外,DataFrame还支持很多基本的查询操作,在此不做赘述。
|
||||
|
||||
我们还可以通过isStreaming函数来判断一个DataFrame是否代表流数据。
|
||||
|
||||
df.isStreaming()
|
||||
|
||||
|
||||
基于事件时间的时间窗口操作
|
||||
|
||||
在学习Spark Streaming的时间窗口操作时,我们举过一个例子,是每隔10秒钟输出过去60秒的前十热点词。这个例子是基于处理时间而非事件时间的。
|
||||
|
||||
现在让我们设想一下,如果数据流中的每个词语都有一个时间戳代表词语产生的时间,那么要怎样实现,每隔10秒钟输出过去60秒内产生的前十热点词呢?你可以看看下边的代码。
|
||||
|
||||
words = ... #这个DataFrame代表词语的数据流,schema是 { timestamp: Timestamp, word: String}
|
||||
|
||||
|
||||
windowedCounts = words.groupBy(
|
||||
window(words.timestamp, "1 minute", "10 seconds"),
|
||||
words.word
|
||||
).count()
|
||||
.sort(desc("count"))
|
||||
.limit(10)
|
||||
|
||||
|
||||
基于词语的生成时间,我们创建了一个窗口长度为1分钟,滑动间隔为10秒的window。然后,把输入的词语表根据window和词语本身聚合起来,并统计每个window内每个词语的数量。之后,再根据词语的数量进行排序,只返回前10的词语。
|
||||
|
||||
在Structured Streaming基于时间窗口的聚合操作中,groupBy是非常常用的。
|
||||
|
||||
输出结果流
|
||||
|
||||
当经过各种SQL查询操作之后,我们创建好了代表最终结果的DataFrame。下一步就是开始对输入数据流的处理,并且持续输出结果。
|
||||
|
||||
我们可以用Dataset.writeStream()返回的DataStreamWriter对象去输出结果。它支持多种写入位置,如硬盘文件、Kafka、console和内存等。
|
||||
|
||||
query = wordCounts
|
||||
.writeStream
|
||||
.outputMode("complete")
|
||||
.format("csv")
|
||||
.option("path", "path/to/destination/dir")
|
||||
.start()
|
||||
|
||||
|
||||
query.awaitTermination()
|
||||
|
||||
|
||||
在上面这个代码例子中,我们选择了完全模式,把输出结果流写入了CSV文件。
|
||||
|
||||
Structured Streaming与Spark Streaming对比
|
||||
|
||||
接下来,让我们对比一下Structured Streaming和上一讲学过的Spark Streaming。看看同为流处理的组件的它们各有什么优缺点。
|
||||
|
||||
简易度和性能
|
||||
|
||||
Spark Streaming提供的DStream API与RDD API很类似,相对比较低level。
|
||||
|
||||
当我们编写 Spark Streaming 程序的时候,本质上就是要去构造RDD的DAG执行图,然后通过 Spark Engine 运行。这样开发者身上的担子就很重,很多时候要自己想办法去提高程序的处理效率。这不是Spark作为一个数据处理框架想看到的。对于好的框架来说,开发者只需要专注在业务逻辑上,而不用操心别的配置、优化等繁杂事项。
|
||||
|
||||
Structured Streaming提供的DataFrame API就是这么一个相对高level的API,大部分开发者都很熟悉关系型数据库和SQL。这样的数据抽象可以让他们用一套统一的方案去处理批处理和流处理,不用去关心具体的执行细节。
|
||||
|
||||
而且,DataFrame API是在Spark SQL的引擎上执行的,Spark SQL有非常多的优化功能,比如执行计划优化和内存管理等,所以Structured Streaming的应用程序性能很好。
|
||||
|
||||
实时性
|
||||
|
||||
在上一讲中我们了解到,Spark Streaming是准实时的,它能做到的最小延迟在一秒左右。
|
||||
|
||||
虽然Structured Streaming用的也是类似的微批处理思想,每过一个时间间隔就去拿来最新的数据加入到输入数据表中并更新结果,但是相比起Spark Streaming来说,它更像是实时处理,能做到用更小的时间间隔,最小延迟在100毫秒左右。
|
||||
|
||||
而且在Spark 2.3版本中,Structured Streaming引入了连续处理的模式,可以做到真正的毫秒级延迟,这无疑大大拓展了Structured Streaming的应用广度。不过现在连续处理模式还有很多限制,让我们期待它的未来吧。
|
||||
|
||||
对事件时间的支持
|
||||
|
||||
就像我们在前边讲过的,Structured Streaming对基于事件时间的处理有很好的支持。
|
||||
|
||||
由于Spark Streaming是把数据按接收到的时间切分成一个个RDD来进行批处理,所以它很难基于数据本身的产生时间来进行处理。如果某个数据的处理时间和事件时间不一致的话,就容易出问题。比如,统计每秒的词语数量,有的数据先产生,但是在下一个时间间隔才被处理,这样几乎不可能输出正确的结果。
|
||||
|
||||
Structured Streaming还有很多其他优点。比如,它有更好的容错性,保证了端到端exactly once的语义等等。所以综合来说,Structured Streaming是比Spark Streaming更好的流处理工具。
|
||||
|
||||
思考题
|
||||
|
||||
在基于事件时间的窗口操作中,Structured Streaming是怎样处理晚到达的数据,并且返回正确结果的呢?
|
||||
|
||||
比如,在每十分钟统计词频的例子中,一个词语在1:09被生成,在1:11被处理,程序在1:10和1:20都输出了对应的结果,在1:20输出时为什么可以把这个词语统计在内?这样的机制有没有限制?
|
||||
|
||||
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
217
专栏/大规模数据处理实战/18WordCount:从零开始运行你的第一个Spark应用.md
Normal file
217
专栏/大规模数据处理实战/18WordCount:从零开始运行你的第一个Spark应用.md
Normal file
@@ -0,0 +1,217 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
18 Word Count:从零开始运行你的第一个Spark应用
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我们来从零开始运行你的第一个Spark应用。
|
||||
|
||||
我们先来回顾一下模块三的学习路径。
|
||||
|
||||
首先,我们由浅入深地学习了Spark的基本数据结构RDD,了解了它这样设计的原因,以及它所支持的API。
|
||||
|
||||
之后,我们又学习了Spark SQL的DataSet/DataFrame API,了解到它不仅提供类似于SQL query的接口,大大提高了开发者的工作效率,还集成了Catalyst优化器,可以提升程序的性能。
|
||||
|
||||
这些API应对的都是批处理的场景。
|
||||
|
||||
再之后,我们学习了Spark的流处理模块:Spark Streaming和Structured Streaming。两者都是基于微批处理(Micro batch processing)的思想,将流数据按时间间隔分割成小的数据块进行批处理,实时更新计算结果。
|
||||
|
||||
其中Structured Streaming也是使用DataSet/DataFrame API,这套API在某种程度上统一了批处理和流处理,是当前Spark最流行的工具,我们必需要好好掌握。
|
||||
|
||||
虽然学习了这么多API以及它们的应用,但是大部分同学还没有从零开始写一个完整的Spark程序,可能更没有运行Spark程序的经历。纸上谈兵并不能帮助我们在工作生活中用Spark解决实际问题。所以,今天我就和你一起做个小练习,从在本地安装Spark、配置环境开始,为你示范怎样一步步解决之前提到数次的统计词频(Word Count)的问题。
|
||||
|
||||
通过今天的学习,你可以收获:
|
||||
|
||||
|
||||
怎样安装Spark以及其他相关的模块;
|
||||
知道什么是SparkContext、SparkSession;
|
||||
一个完整的Spark程序应该包含哪些东西;
|
||||
用RDD、DataFrame、Spark Streaming如何实现统计词频。
|
||||
|
||||
|
||||
这一讲中,我们使用的编程语言是Python,操作系统是Mac OS X。
|
||||
|
||||
在这一讲以及之前文章的例子中,我们都是用Python作为开发语言。虽然原生的Spark是用Scala实现,但是在大数据处理领域中,我个人最喜欢的语言是Python。因为它非常简单易用,应用非常广泛,有很多的库可以方便我们开发。
|
||||
|
||||
当然Scala也很棒,作为一个函数式编程语言,它很容易用链式表达对数据集进行各种处理,而且它的运行速度是最快的,感兴趣的同学可以去学习一下。
|
||||
|
||||
虽然Spark还支持Java和R,但是我个人不推荐你使用。用Java写程序实在有些冗长,而且速度上没有优势。
|
||||
|
||||
操作系统选Mac OS X是因为我个人喜欢使用Macbook,当然Linux/Ubuntu也很棒。
|
||||
|
||||
安装Spark
|
||||
|
||||
首先,我们来简单介绍一下如何在本地安装Spark,以及用Python实现的Spark库——PySpark。
|
||||
|
||||
在前面的文章中,我们了解过,Spark的job都是JVM(Java Virtual Machine)的进程,所以在安装运行Spark之前,我们需要确保已经安装Java Developer Kit(JDK)。在命令行终端中输入:
|
||||
|
||||
java -version
|
||||
|
||||
|
||||
如果命令行输出了某个Java的版本,那么说明你已经有JDK或者JRE在本地。如果显示无法识别这个命令,那么说明你还没有安装JDK。这时,你可以去Oracle的官网去下载安装JDK,然后配置好环境变量。
|
||||
|
||||
同样,我们需要确保Python也已经被安装在本地了。在命令行输入“Python”或者“Python3”,如果可以成功进入交互式的Python Shell,就说明已经安装了Python。否则,需要去Python官网下载安装Python。这里,我推荐你使用Python3而不是Python2。
|
||||
|
||||
我们同样可以在本地预装好Hadoop。Spark可以脱离Hadoop运行,不过有时我们也需要依赖于HDFS和YARN。所以,这一步并不是必须的,你可以自行选择。
|
||||
|
||||
接下来我们就可以安装Spark。首先去Spark官网的下载界面。在第一个下拉菜单里选择最新的发布,第二个菜单最好选择与Hadoop 2.7兼容的版本。因为有时我们的Spark程序会依赖于HDFS和YARN,所以选择最新的Hadoop版本比较好。
|
||||
|
||||
|
||||
|
||||
下载好之后,解压缩Spark安装包,并且把它移动到/usr/local目录下,在终端中输入下面的代码。
|
||||
|
||||
$ tar -xzf ~/Dowmloads/spark-2.4.3-bin-hadoop2.7.tg
|
||||
$ mv spark-2.4.3-bin-hadoop2.7.tgz /usr/local/spark
|
||||
|
||||
|
||||
经过上述步骤,从官网下载并安装Spark的文件,这样我们便完成了Spark的安装。但是,Spark也是要进行相应的环境变量配置的。你需要打开环境变量配置文件。
|
||||
|
||||
vim ~/.bash_profile
|
||||
|
||||
|
||||
并在最后添加一段代码。
|
||||
|
||||
export SPARK_HOME=/usr/local/spark
|
||||
export PATH=$PATH:$SPARK_HOME/bin
|
||||
|
||||
|
||||
这样,所需的步骤都做完之后,我们在命令行控制台输入PySpark,查看安装情况。如果出现下面的欢迎标志,就说明安装完毕了。
|
||||
|
||||
Welcome to
|
||||
____ __
|
||||
/ __/__ ___ _____/ /__
|
||||
_\ \/ _ \/ _ `/ __/ '_/
|
||||
/__ / .__/\_,_/_/ /_/\_\ version 2.4.3
|
||||
/_/
|
||||
|
||||
Using Python version 2.7.10 (default, Oct 6 2017 22:29:07)
|
||||
SparkSession available as 'spark'.
|
||||
>>>
|
||||
|
||||
|
||||
基于RDD API的Word Count程序
|
||||
|
||||
配置好所需的开发环境之后,下一步就是写一个Python程序去统计词语频率。我们都知道这个程序的逻辑应该是如下图所示的。
|
||||
|
||||
|
||||
|
||||
对于中间的先map再reduce的处理,我相信通过前面的学习,所有同学都可以用RDD或者DataFrame实现。
|
||||
|
||||
但是,我们对于Spark程序的入口是什么、如何用它读取和写入文件,可能并没有了解太多。所以,接下来让我们先接触一下Spark程序的入口。
|
||||
|
||||
在Spark 2.0之前,SparkContext是所有Spark任务的入口,它包含了Spark程序的基本设置,比如程序的名字、内存大小、并行处理的粒度等,Spark的驱动程序需要利用它来连接到集群。
|
||||
|
||||
无论Spark集群有多少个节点做并行处理,每个程序只可以有唯一的SparkContext,它可以被SparkConf对象初始化。
|
||||
|
||||
conf = SparkConf().setAppName(appName).setMaster(master)
|
||||
sc = SparkContext(conf=conf)
|
||||
|
||||
|
||||
这个appName参数是一个在集群UI上展示应用程序的名称,master参数是一个Spark、Mesos 或YARN的集群URL,对于本地运行,它可以被指定为“local”。
|
||||
|
||||
在统计词频的例子中,我们需要通过SparkContext对象来读取输入文件,创建一个RDD,如下面的代码所示。
|
||||
|
||||
text_file = sc.textFile("file://…...") //替换成实际的本地文件路径。
|
||||
|
||||
|
||||
这里的text_file是一个RDD,它里面的每一个数据代表原文本文件中的一行。
|
||||
|
||||
在这些版本中,如果要使用Spark提供的其他库,比如SQL或Streaming,我们就需要为它们分别创建相应的context对象,才能调用相应的API,比如的DataFrame和DStream。
|
||||
|
||||
hc = HiveContext(sc)
|
||||
ssc = StreamingContext(sc)
|
||||
|
||||
|
||||
在Spark 2.0之后,随着新的DataFrame/DataSet API的普及化,Spark引入了新的SparkSession对象作为所有Spark任务的入口。
|
||||
|
||||
SparkSession不仅有SparkContext的所有功能,它还集成了所有Spark提供的API,比如DataFrame、Spark Streaming和Structured Streaming,我们再也不用为不同的功能分别定义Context。
|
||||
|
||||
在统计词频的例子中,我们可以这样初始化SparkSession以及创建初始RDD。
|
||||
|
||||
spark = SparkSession
|
||||
.builder
|
||||
.appName(appName)
|
||||
.getOrCreate()
|
||||
text_file = spark.read.text("file://….").rdd.map(lambda r: r[0])
|
||||
|
||||
|
||||
由于SparkSession的普适性,我推荐你尽量使用它作为你们Spark程序的入口。随后的学习中,我们会逐渐了解怎样通过它调用DataFrame和Streaming API。
|
||||
|
||||
让我们回到统计词频的例子。在创建好代表每一行文本的RDD之后,接下来我们便需要两个步骤。
|
||||
|
||||
|
||||
把每行的文本拆分成一个个词语;
|
||||
统计每个词语的频率。
|
||||
|
||||
|
||||
对于第一步,我们可以用flatMap去把行转换成词语。对于第二步,我们可以先把每个词语转换成(word, 1)的形式,然后用reduceByKey去把相同词语的次数相加起来。这样,就很容易写出下面的代码了。
|
||||
|
||||
counts = lines.flatMap(lambda x: x.split(' '))
|
||||
.map(lambda x: (x, 1))
|
||||
.reduceByKey(add)
|
||||
|
||||
|
||||
这里counts就是一个包含每个词语的(word,count)pair的RDD。
|
||||
|
||||
相信你还记得,只有当碰到action操作后,这些转换动作才会被执行。所以,接下来我们可以用collect操作把结果按数组的形式返回并输出。
|
||||
|
||||
output = counts.collect()
|
||||
for (word, count) in output:
|
||||
print("%s: %i" % (word, count))
|
||||
spark.stop() // 停止SparkSession
|
||||
|
||||
|
||||
基于DataFrame API的Word Count程序
|
||||
|
||||
讲完基于RDD API的Word Count程序,接下来让我们学习下怎样用DataFrame API来实现相同的效果。
|
||||
|
||||
在DataFrame的世界中,我们可以把所有的词语放入一张表,表中的每一行代表一个词语,当然这个表只有一列。我们可以对这个表用一个groupBy()操作把所有相同的词语聚合起来,然后用count()来统计出每个group的数量。
|
||||
|
||||
但是问题来了,虽然Scala和Java支持对DataFrame进行flatMap操作,但是Python并不支持。那么要怎样把包含多个词语的句子进行分割和拆分呢?这就要用到两个新的操作——explode和split。split是pyspark.sql.functions库提供的一个函数,它作用于DataFrame的某一列,可以把列中的字符串按某个分隔符分割成一个字符串数组。
|
||||
|
||||
explode同样是pyspark.sql.functions库提供的一个函数,通俗点的翻译是“爆炸”,它也作用于DataFrame的某一列,可以为列中的数组或者map中每一个元素创建一个新的Row。
|
||||
|
||||
由于之前代码中创建的df_lines这个DataFrame中,每一行只有一列,每一列都是一个包含很多词语的句子,我们可以先对这一列做split,生成一个新的列,列中每个元素是一个词语的数组;再对这个列做explode,可以把数组中的每个元素都生成一个新的Row。这样,就实现了类似的flatMap功能。这个过程可以用下面的三个表格说明。
|
||||
|
||||
|
||||
|
||||
接下来我们只需要对Word这一列做groupBy,就可以统计出每个词语出现的频率,代码如下。
|
||||
|
||||
from pyspark.sql import SparkSession
|
||||
from pyspark.sql.functions import *
|
||||
|
||||
if __name__ == "__main__":
|
||||
spark = SparkSession
|
||||
.builder
|
||||
.appName(‘WordCount’)
|
||||
.getOrCreate()
|
||||
lines = spark.read.text("sample.txt")
|
||||
wordCounts = lines
|
||||
.select(explode(split(lines.value, " "))
|
||||
.alias("word"))
|
||||
.groupBy("word")
|
||||
.count()
|
||||
wordCounts.show()
|
||||
|
||||
spark.stop()
|
||||
|
||||
|
||||
从这个例子,你可以很容易看出使用DataSet/DataFrame API的便利性——我们不需要创建(word, count)的pair来作为中间值,可以直接对数据做类似SQL的查询。
|
||||
|
||||
小结
|
||||
|
||||
通过今天的学习,我们掌握了如何从零开始创建一个简单的Spark的应用程序,包括如何安装Spark、如何配置环境、Spark程序的基本结构等等。
|
||||
|
||||
实践题
|
||||
|
||||
希望你可以自己动手操作一下,这整个过程只需要跑通一次,以后就可以脱离纸上谈兵,真正去解决实际问题。
|
||||
|
||||
欢迎你在留言中反馈自己动手操作的效果。
|
||||
|
||||
如果你跑通了,可以在留言中打个卡。如果遇到了问题,也请你在文章中留言,与我和其他同学一起讨论。
|
||||
|
||||
|
||||
|
||||
|
269
专栏/大规模数据处理实战/19综合案例实战:处理加州房屋信息,构建线性回归模型.md
Normal file
269
专栏/大规模数据处理实战/19综合案例实战:处理加州房屋信息,构建线性回归模型.md
Normal file
@@ -0,0 +1,269 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
19 综合案例实战:处理加州房屋信息,构建线性回归模型
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“综合案例实战:处理加州房屋信息,构建线性回归模型”。
|
||||
|
||||
通过之前的学习,我们对Spark各种API的基本用法有了一定的了解,还通过统计词频的实例掌握了如何从零开始写一个Spark程序。那么现在,让我们从一个真实的数据集出发,看看如何用Spark解决实际问题。
|
||||
|
||||
数据集介绍
|
||||
|
||||
为了完成今天的综合案例实战,我使用的是美国加州1990年房屋普查的数据集。
|
||||
|
||||
|
||||
|
||||
数据集中的每一个数据都代表着一块区域内房屋和人口的基本信息,总共包括9项:
|
||||
|
||||
|
||||
该地区中心的纬度(latitude)
|
||||
该地区中心的经度(longitude)
|
||||
区域内所有房屋屋龄的中位数(housingMedianAge)
|
||||
区域内总房间数(totalRooms)
|
||||
区域内总卧室数(totalBedrooms)
|
||||
区域内总人口数(population)
|
||||
区域内总家庭数(households)
|
||||
区域内人均收入中位数(medianIncome)
|
||||
该区域房价的中位数(medianHouseValue)
|
||||
|
||||
|
||||
也就是说,我们可以把每一个数据看作一个地区,它含有9项我们关心的信息,也就是上面提到的9个指标。比如下面这个数据:
|
||||
|
||||
-122.230000,37.880000,41.000000,880.000000,129.000000,322.000000,126.000000,8.325200,452600.000000'
|
||||
|
||||
|
||||
这个数据代表该地区的经纬度是(-122.230000,37.880000),这个地区房屋历史的中位数是41年,所有房屋总共有880个房间,其中有129个卧室。这个地区内共有126个家庭和322位居民,人均收入中位数是8.3252万,房价中位数是45.26万。
|
||||
|
||||
这里的地域单位是美国做人口普查的最小地域单位,平均一个地域单位中有1400多人。在这个数据集中共有两万多个这样的数据。显然,这样小的数据量我们并“不需要”用Spark来处理,但是,它可以起到一个很好的示例作用。这个数据集可以从网上下载到。这个数据集是在1997年的一篇学术论文中创建的,感兴趣的同学可以去亲自下载,并加以实践。
|
||||
|
||||
那么我们今天的目标是什么呢?就是用已有的数据,构建一个线性回归模型,来预测房价。
|
||||
|
||||
我们可以看到,前8个属性都可能对房价有影响。这里,我们假设这种影响是线性的,我们就可以找到一个类似A=b_B+c_C+d_D+…+i_I的公式,A代表房价,B到I分别代表另外八个属性。这样,对于不在数据集中的房子,我们可以套用这个公式来计算出一个近似的房价。由于专栏的定位是大规模数据处理专栏,所以我们不会细讲统计学的知识。如果你对统计学知识感兴趣,或者还不理解什么是线性回归的话,可以去自行学习一下。
|
||||
|
||||
进一步了解数据集
|
||||
|
||||
每当我们需要对某个数据集进行处理时,不要急着写代码。你一定要先观察数据集,了解它的特性,并尝试对它做一些简单的预处理,让数据的可读性更好。这些工作我们最好在Spark的交互式Shell上完成,而不是创建python的源文件并执行。因为,在Shell上我们可以非常直观而简便地看到每一步的输出。
|
||||
|
||||
首先,让我们把数据集读入Spark。
|
||||
|
||||
from pyspark.sql import SparkSession
|
||||
|
||||
# 初始化SparkSession和SparkContext
|
||||
spark = SparkSession.builder
|
||||
.master("local")
|
||||
.appName("California Housing ")
|
||||
.config("spark.executor.memory", "1gb")
|
||||
.getOrCreate()
|
||||
sc = spark.sparkContext
|
||||
|
||||
# 读取数据并创建RDD
|
||||
rdd = sc.textFile('/Users/yourName/Downloads/CaliforniaHousing/cal_housing.data')
|
||||
|
||||
# 读取数据每个属性的定义并创建RDD
|
||||
header = sc.textFile('/Users/yourName/Downloads/CaliforniaHousing/cal_housing.domain')
|
||||
|
||||
|
||||
这样,我们就把房屋信息数据和每个属性的定义读入了Spark,并创建了两个相应的RDD。你还记得吧?RDD是有一个惰性求值的特性的,所以,我们可以用collect()函数来把数据输出在Shell上。
|
||||
|
||||
header.collect()
|
||||
|
||||
[u'longitude: continuous.', u'latitude: continuous.', u'housingMedianAge: continuous. ', u'totalRooms: continuous. ', u'totalBedrooms: continuous. ', u'population: continuous. ', u'households: continuous. ', u'medianIncome: continuous. ', u'medianHouseValue: continuous. ']
|
||||
|
||||
|
||||
这样,我们就得到了每个数据所包含的信息,这和我们前面提到的9个属性的顺序是一致的,而且它们都是连续的值,而不是离散的。你需要注意的是,collect()函数会把所有数据都加载到内存中,如果数据很大的话,有可能会造成内存泄漏,所以要小心使用。平时比较常见的方法是用take()函数去只读取RDD中的某几个元素。
|
||||
|
||||
由于RDD中的数据可能会比较大,所以接下来让我们读取它的前两个数据。
|
||||
|
||||
rdd.take(2)
|
||||
|
||||
[u'-122.230000,37.880000,41.000000,880.000000,129.000000,322.000000,126.000000,8.325200,452600.000000', u'-122.220000,37.860000,21.000000,7099.000000,1106.000000,2401.000000,1138.000000,8.301400,358500.000000']
|
||||
|
||||
|
||||
由于我们是用SparkContext的textFile函数去创建RDD,所以每个数据其实是一个大的字符串,各个属性之间用逗号分隔开来。这不利于我们之后的处理,因为我们可能会需要分别读取每个对象的各个属性。所以,让我们用map函数把大字符串分隔成数组,这会方便我们的后续操作。
|
||||
|
||||
rdd = rdd.map(lambda line: line.split(","))
|
||||
rdd.take(2)
|
||||
|
||||
[[u'-122.230000', u'37.880000', u'41.000000', u'880.000000', u'129.000000', u'322.000000', u'126.000000', u'8.325200', u'452600.000000'], [u'-122.220000', u'37.860000', u'21.000000', u'7099.000000', u'1106.000000', u'2401.000000', u'1138.000000', u'8.301400', u'358500.000000']]
|
||||
|
||||
|
||||
我们在前面学过,Spark SQL的DataFrame API在查询结构化数据时更方便使用,而且性能更好。在这个例子中你可以看到,数据的schema是定义好的,我们需要去查询各个列,所以DataFrame API显然更加适用。所以,我们需要先把RDD转换为DataFrame。
|
||||
|
||||
具体来说,就是需要把之前用数组代表的对象,转换成为Row对象,再用toDF()函数转换成DataFrame。
|
||||
|
||||
from pyspark.sql import Row
|
||||
|
||||
df = rdd.map(lambda line: Row(longitude=line[0],
|
||||
latitude=line[1],
|
||||
housingMedianAge=line[2],
|
||||
totalRooms=line[3],
|
||||
totalBedRooms=line[4],
|
||||
population=line[5],
|
||||
households=line[6],
|
||||
medianIncome=line[7],
|
||||
medianHouseValue=line[8])).toDF()
|
||||
|
||||
|
||||
现在我们可以用show()函数打印出这个DataFrame所含的数据表。
|
||||
|
||||
df.show()
|
||||
|
||||
|
||||
|
||||
|
||||
这里每一列的数据格式都是string,但是,它们其实都是数字,所以我们可以通过cast()函数把每一列的类型转换成float。
|
||||
|
||||
def convertColumn(df, names, newType)
|
||||
for name in names:
|
||||
df = df.withColumn(name, df[name].cast(newType))
|
||||
return df
|
||||
|
||||
columns = ['households', 'housingMedianAge', 'latitude', 'longitude', 'medianHouseValue', 'medianIncome', 'population', 'totalBedRooms', 'totalRooms']
|
||||
|
||||
df = convertColumn(df, columns, FloatType())
|
||||
|
||||
|
||||
转换成数字有很多优势。比如,我们可以按某一列,对所有对象进行排序,也可以计算平均值等。比如,下面这段代码就可以统计出所有建造年限各有多少个房子。
|
||||
|
||||
df.groupBy("housingMedianAge").count().sort("housingMedianAge",ascending=False).show()
|
||||
|
||||
|
||||
预处理
|
||||
|
||||
通过上面的数据分析,你可能会发现这些数据还是不够直观。具体的问题有:
|
||||
|
||||
|
||||
房价的值普遍都很大,我们可以把它调整成相对较小的数字;
|
||||
有的属性没什么意义,比如所有房子的总房间数和总卧室数,我们更加关心的是平均房间数;
|
||||
在我们想要构建的线性模型中,房价是结果,其他属性是输入参数。所以我们需要把它们分离处理;
|
||||
有的属性最小值和最大值范围很大,我们可以把它们标准化处理。
|
||||
|
||||
|
||||
对于第一点,我们观察到大多数房价都是十万起的,所以可以用withColumn()函数把所有房价都除以100000。
|
||||
|
||||
df = df.withColumn("medianHouseValue", col("medianHouseValue")/100000)
|
||||
|
||||
|
||||
对于第二点,我们可以添加如下三个新的列:
|
||||
|
||||
|
||||
每个家庭的平均房间数:roomsPerHousehold
|
||||
每个家庭的平均人数:populationPerHousehold
|
||||
卧室在总房间的占比:bedroomsPerRoom
|
||||
|
||||
|
||||
当然,你们可以自由添加你们觉得有意义的列,这里的三个是我觉得比较典型的。同样,用withColumn()函数可以容易地新建列。
|
||||
|
||||
df = df.withColumn("roomsPerHousehold", col("totalRooms")/col("households"))
|
||||
.withColumn("populationPerHousehold", col("population")/col("households"))
|
||||
.withColumn("bedroomsPerRoom", col("totalBedRooms")/col("totalRooms"))
|
||||
|
||||
|
||||
同样,有的列是我们并不关心的,比如经纬度,这个数值很难有线性的意义。所以我们可以只留下重要的信息列。
|
||||
|
||||
df = df.select("medianHouseValue",
|
||||
"totalBedRooms",
|
||||
"population",
|
||||
"households",
|
||||
"medianIncome",
|
||||
"roomsPerHousehold",
|
||||
"populationPerHousehold",
|
||||
"bedroomsPerRoom")
|
||||
|
||||
|
||||
对于第三点,最简单的办法就是把DataFrame转换成RDD,然后用map()函数把每个对象分成两部分:房价和一个包含其余属性的列表,然后在转换回DataFrame。
|
||||
|
||||
from pyspark.ml.linalg import DenseVector
|
||||
|
||||
input_data = df.rdd.map(lambda x: (x[0], DenseVector(x[1:])))
|
||||
df = spark.createDataFrame(input_data, ["label", "features"])
|
||||
|
||||
|
||||
我们重新把两部分重新标记为“label”和“features”,label代表的是房价,features代表包括其余参数的列表。
|
||||
|
||||
对于第四点,数据的标准化我们可以借助Spark的机器学习库Spark ML来完成。Spark ML也是基于DataFrame,它提供了大量机器学习的算法实现、数据流水线(pipeline)相关工具和很多常用功能。由于本专栏的重点是大数据处理,所以我们并没有介绍Spark ML,但是我强烈推荐同学们有空去了解一下它。
|
||||
|
||||
在这个AI和机器学习的时代,我们不能落伍。
|
||||
|
||||
from pyspark.ml.feature import StandardScaler
|
||||
|
||||
standardScaler = StandardScaler(inputCol="features", outputCol="features_scaled")
|
||||
scaler = standardScaler.fit(df)
|
||||
scaled_df = scaler.transform(df)
|
||||
|
||||
|
||||
在第二行,我们创建了一个StandardScaler,它的输入是features列,输出被我们命名为features_scaled。第三、第四行,我们把这个scaler对已有的DataFrame进行处理,让我们看下代码块里显示的输出结果。
|
||||
|
||||
scaled_df.take(1)
|
||||
|
||||
[Row(label=4.526, features=DenseVector([129.0, 322.0, 126.0, 8.3252, 6.9841, 2.5556, 0.1466]), features_scaled=DenseVector([0.3062, 0.2843, 0.3296, 4.3821, 2.8228, 0.2461, 2.5264]))]
|
||||
|
||||
|
||||
我们可以清楚地看到,这一行新增了一个features_scaled的列,它里面每个数据都是标准化过的,我们应该用它,而非features来训练模型。
|
||||
|
||||
创建模型
|
||||
|
||||
上面的预处理都做完后,我们终于可以开始构建线性回归模型了。
|
||||
|
||||
首先,我们需要把数据集分为训练集和测试集,训练集用来训练模型,测试集用来评估模型的正确性。DataFrame的randomSplit()函数可以很容易的随机分割数据,这里我们将80%的数据用于训练,剩下20%作为测试集。
|
||||
|
||||
train_data, test_data = scaled_df.randomSplit([.8,.2],seed=123)
|
||||
|
||||
|
||||
用Spark ML提供的LinearRegression功能,我们可以很容易得构建一个线性回归模型,如下所示。
|
||||
|
||||
from pyspark.ml.regression import LinearRegression
|
||||
|
||||
lr = LinearRegression(featuresCol='features_scaled', labelCol="label", maxIter=10, regParam=0.3, elasticNetParam=0.8)
|
||||
linearModel = lr.fit(train_data)
|
||||
|
||||
|
||||
LinearRegression可以调节的参数还有很多,你可以去官方API文档查阅,这里我们只是示范一下。
|
||||
|
||||
模型评估
|
||||
|
||||
现在有了模型,我们终于可以用linearModel的transform()函数来预测测试集中的房价,并与真实情况进行对比。代码如下所示。
|
||||
|
||||
predicted = linearModel.transform(test_data)
|
||||
predictions = predicted.select("prediction").rdd.map(lambda x: x[0])
|
||||
labels = predicted.select("label").rdd.map(lambda x: x[0])
|
||||
predictionAndLabel = predictions.zip(labels).collect()
|
||||
|
||||
|
||||
我们用RDD的zip()函数把预测值和真实值放在一起,这样可以方便地进行比较。比如让我们看一下前两个对比结果。
|
||||
|
||||
predictionAndLabel.take(2)
|
||||
|
||||
[(1.4491508524918457, 1.14999), (1.5831547768979277, 0.964)]
|
||||
|
||||
|
||||
这里可以看出,我们的模型预测的结果有些偏小,这可能有多个因素造成。最直接的原因就是房价与我们挑选的列并没有强线性关系,而且我们使用的参数也可能不够准确。
|
||||
|
||||
这一讲我只是想带着你一起体验下处理真实数据集和解决实际问题的感觉,想要告诉你的是这种通用的思想,并帮助你继续熟悉Spark各种库的用法,并不是说房价一定就是由这些参数线性决定了。感兴趣的同学可以去继续优化,或者尝试别的模型。
|
||||
|
||||
小结
|
||||
|
||||
这一讲我们通过一个真实的数据集,通过以下步骤解决了一个实际的数据处理问题:
|
||||
|
||||
|
||||
观察并了解数据集
|
||||
数据清洗
|
||||
数据的预处理
|
||||
训练模型
|
||||
评估模型
|
||||
|
||||
|
||||
其实这里还可以有与“优化与改进”相关的内容,这里没有去阐述是因为我们的首要目的依然是熟悉与使用Spark各类API。相信通过今天的学习,你初步了解了数据处理问题的一般思路,并强化了对RDD、DataFrame和机器学习API的使用。
|
||||
|
||||
实践与思考题
|
||||
|
||||
今天请你下载这个数据集,按文章的介绍去动手实践一次。如果有时间的话,还可以对这个过程的优化和改进提出问题并加以解决。
|
||||
|
||||
欢迎你在留言板贴出自己的idea。如果你觉得有所收获,也欢迎你把文章分享给朋友。
|
||||
|
||||
|
||||
|
||||
|
220
专栏/大规模数据处理实战/20流处理案例实战:分析纽约市出租车载客信息.md
Normal file
220
专栏/大规模数据处理实战/20流处理案例实战:分析纽约市出租车载客信息.md
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
20 流处理案例实战:分析纽约市出租车载客信息
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“流处理案例实战:分析纽约市出租车载客信息”。
|
||||
|
||||
在上一讲中,我们结合加州房屋信息的真实数据集,构建了一个基本的预测房价的线性回归模型。通过这个实例,我们不仅学习了处理大数据问题的基本流程,而且还进一步熟练了对RDD和DataFrame API的使用。
|
||||
|
||||
你应该已经发现,上一讲的实例是一个典型的批处理问题,因为处理的数据是静态而有边界的。今天让我们来一起通过实例,更加深入地学习用Spark去解决实际的流处理问题。
|
||||
|
||||
相信你还记得,在前面的章节中我们介绍过Spark两个用于流处理的组件——Spark Streaming和Structured Streaming。其中Spark Streaming是Spark 2.0版本前的的流处理库,在Spark 2.0之后,集成了DataFrame/DataSet API的Structured Streaming成为Spark流处理的主力。
|
||||
|
||||
今天就让我们一起用Structured Streaming对纽约市出租车的载客信息进行处理,建立一个实时流处理的pipeline,实时输出各个区域内乘客小费的平均数来帮助司机决定要去哪里接单。
|
||||
|
||||
数据集介绍
|
||||
|
||||
今天的数据集是纽约市2009~2015年出租车载客的信息。每一次出行包含了两个事件,一个事件代表出发,另一个事件代表到达。每个事件都有11个属性,它的schema如下所示:
|
||||
|
||||
|
||||
|
||||
这部分数据有个不太直观的地方,那就是同一次出行会有两个记录,而且代表出发的事件没有任何意义,因为到达事件已经涵盖了所有必要的信息。现实世界中的数据都是这样复杂,不可能像学校的测试数据一样简单直观,所以处理之前,我们要先对数据进行清洗,只留下必要的信息。
|
||||
|
||||
这个数据还包含有另外一部分信息,就是所有出租车的付费信息,它有8个属性,schema如下所示。
|
||||
|
||||
|
||||
|
||||
这个数据集可以从网上下载到,数据集的规模在100MB左右,它只是节选了一部分出租车的载客信息,所以在本机运行就可以了。详细的纽约出租车数据集超过了500GB,同样在网上可以下载,感兴趣的同学可以下载来实践一下。
|
||||
|
||||
流数据输入
|
||||
|
||||
你可能要问,这个数据同样是静态、有边界的,为什么要用流处理?
|
||||
|
||||
因为我们手里没有实时更新的流数据源。我也没有权限去公开世界上任何一个上线产品的数据流。所以,这里只能将有限的数据经过Kafka处理,输出为一个伪流数据,作为我们要构建的pipeline的输入。
|
||||
|
||||
在模块二中,我们曾经初步了解过Apache Kafka,知道它是基于Pub/Sub模式的流数据处理平台。由于我们的专栏并不涉及Apache Kafka的具体内容,所以我在这里就不讲如何把这个数据输入到Kafka并输出的细节了。你只要知道,在这个例子中,Consumer是之后要写的Spark流处理程序,这个消息队列有两个Topic,一个包含出行的地理位置信息,一个包含出行的收费信息。Kafka会按照时间顺序,向这两个Topic中发布事件,从而模拟一个实时的流数据源。
|
||||
|
||||
相信你还记得,写Spark程序的第一步就是创建SparkSession对象,并根据输入数据创建对应的RDD或者DataFrame。你可以看下面的代码。
|
||||
|
||||
from pyspark.sql import SparkSession
|
||||
|
||||
spark = SparkSession.builder
|
||||
.appName("Spark Structured Streaming for taxi ride info")
|
||||
.getOrCreate()
|
||||
|
||||
rides = spark
|
||||
.readStream
|
||||
.format("kafka")
|
||||
.option("kafka.bootstrap.servers", "localhost:xxxx") //取决于Kafka的配置
|
||||
.option("subscribe", "taxirides")
|
||||
.option("startingOffsets", "latest")
|
||||
.load()
|
||||
.selectExpr("CAST(value AS STRING)")
|
||||
|
||||
fares = spark
|
||||
.readStream
|
||||
.format("kafka")
|
||||
.option("kafka.bootstrap.servers", "localhost:xxxx")
|
||||
.option("subscribe", "taxifares")
|
||||
.option("startingOffsets", "latest")
|
||||
.load()
|
||||
.selectExpr("CAST(value AS STRING)
|
||||
|
||||
|
||||
在这段代码里,我们创建了两个Streaming DataFrame,并订阅了对应的Kafka topic,一个代表出行位置信息,另一个代表收费信息。Kafka对数据没有做任何修改,所以流中的每一个数据都是一个长String,属性之间是用逗号分割的。
|
||||
|
||||
417986,END,2013-01-02 00:43:52,2013-01-02 00:39:56,-73.984528,40.745377,-73.975967,40.765533,1,2013007646,2013007642
|
||||
|
||||
|
||||
数据清洗
|
||||
|
||||
现在,我们要开始做数据清洗了。要想分离出我们需要的位置和付费信息,我们首先要把数据分割成一个个属性,并创建对应的DataFrame中的列。为此,我们首先要根据数据类型创建对应的schema。
|
||||
|
||||
ridesSchema = StructType([
|
||||
StructField("rideId", LongType()), StructField("isStart", StringType()),
|
||||
StructField("endTime", TimestampType()), StructField("startTime", TimestampType()),
|
||||
StructField("startLon", FloatType()), StructField("startLat", FloatType()),
|
||||
StructField("endLon", FloatType()), StructField("endLat", FloatType()),
|
||||
StructField("passengerCnt", ShortType()), StructField("taxiId", LongType()),
|
||||
StructField("driverId", LongType())])
|
||||
|
||||
faresSchema = StructType([
|
||||
StructField("rideId", LongType()), StructField("taxiId", LongType()),
|
||||
StructField("driverId", LongType()), StructField("startTime", TimestampType()),
|
||||
StructField("paymentType", StringType()), StructField("tip", FloatType()),
|
||||
StructField("tolls", FloatType()), StructField("totalFare", FloatType())])
|
||||
|
||||
|
||||
接下来,我们将每个数据都用逗号分割,并加入相应的列。
|
||||
|
||||
def parse_data_from_kafka_message(sdf, schema):
|
||||
from pyspark.sql.functions import split
|
||||
assert sdf.isStreaming == True, "DataFrame doesn't receive streaming data"
|
||||
col = split(sdf['value'], ',')
|
||||
for idx, field in enumerate(schema):
|
||||
sdf = sdf.withColumn(field.name, col.getItem(idx).cast(field.dataType))
|
||||
return sdf.select([field.name for field in schema])
|
||||
|
||||
rides = parse_data_from_kafka_message(rides, ridesSchema)
|
||||
fares = parse_data_from_kafka_message(fares, faresSchema)
|
||||
|
||||
|
||||
在上面的代码中,我们定义了函数parse_data_from_kafka_message,用来把Kafka发来的message根据schema拆成对应的属性,转换类型,并加入到DataFrame的表中。
|
||||
|
||||
正如我们之前提到的,读入的数据包含了一些无用信息。
|
||||
|
||||
首先,所有代表出发的事件都已被删除,因为到达事件已经包含了出发事件的所有信息,而且只有到达之后才会付费。
|
||||
|
||||
其次,出发地点和目的地在纽约范围外的数据,也可以被删除。因为我们的目标是找出纽约市内小费较高的地点。DataFrame的filter函数可以很容易地做到这些。
|
||||
|
||||
MIN_LON, MAX_LON, MIN_LAT, MAX_LAT = -73.7, -74.05, 41.0, 40.5
|
||||
rides = rides.filter(
|
||||
rides["startLon"].between(MIN_LON, MAX_LON) &
|
||||
rides["startLat"].between(MIN_LAT, MAX_LAT) &
|
||||
rides["endLon"].between(MIN_LON, MAX_LON) &
|
||||
rides["endLat"].between(MIN_LAT, MAX_LAT))
|
||||
rides = rides.filter(rides["isStart"] == "END")
|
||||
|
||||
|
||||
上面的代码中首先定义了纽约市的经纬度范围,然后把所有起点和终点在这个范围之外的数据都过滤掉了。最后,把所有代表出发事件的数据也移除掉。
|
||||
|
||||
当然,除了前面提到的清洗方案,可能还会有别的可以改进的地方,比如把不重要的信息去掉,例如乘客数量、过路费等,你可以自己思考一下。
|
||||
|
||||
Stream-stream Join
|
||||
|
||||
我们的目标是找出小费较高的地理区域,而现在收费信息和地理位置信息还在两个DataFrame中,无法放在一起分析。那么要用怎样的方式把它们联合起来呢?
|
||||
|
||||
你应该还记得,DataFrame本质上是把数据当成一张关系型的表。在我们这个例子中,rides所对应的表的键值(Key)是rideId,其他列里我们关心的就是起点和终点的位置;fares所对应的表键值也是rideId,其他列里我们关心的就是小费信息(tips)。
|
||||
|
||||
说到这里,你可能会自然而然地想到,如果可以像关系型数据表一样,根据共同的键值rideId把两个表inner join起来,就可以同时分析这两部分信息了。但是这里的DataFrame其实是两个数据流,Spark可以把两个流Join起来吗?
|
||||
|
||||
答案是肯定的。在Spark 2.3中,流与流的Join(Stream-stream join)被正式支持。这样的Join难点就在于,在任意一个时刻,流数据都不是完整的,流A中后面还没到的数据有可能要和流B中已经有的数据Join起来再输出。为了解决这个问题,我们就要引入数据水印(Watermark)的概念。
|
||||
|
||||
数据水印定义了我们可以对数据延迟的最大容忍限度。
|
||||
|
||||
比如说,如果定义水印是10分钟,数据A的事件时间是1:00,数据B的事件时间是1:10,由于数据传输发生了延迟,我们在1:15才收到了A和B,那么我们将只处理数据B并更新结果,A会被无视。在Join操作中,好好利用水印,我们就知道什么时候可以不用再考虑旧数据,什么时候必须把旧数据保留在内存中。不然,我们就必须把所有旧数据一直存在内存里,导致数据不断增大,最终可能会内存泄漏。
|
||||
|
||||
在这个例子中,为什么我们做这样的Join操作需要水印呢?
|
||||
|
||||
这是因为两个数据流并不保证会同时收到同一次出行的数据,因为收费系统需要额外的时间去处理,而且这两个数据流是独立的,每个都有可能产生数据延迟。所以要对时间加水印,以免出现内存中数据无限增长的情况。
|
||||
|
||||
那么下一个问题就是,究竟要对哪个时间加水印,出发时间还是到达时间?
|
||||
|
||||
前面说过了,我们其实只关心到达时间,所以对rides而言,我们只需要对到达时间加水印。但是,在fares这个DataFrame里并没有到达时间的任何信息,所以我们没法选择,只能对出发时间加水印。因此,我们还需要额外定义一个时间间隔的限制,出发时间和到达时间的间隔要在一定的范围内。具体内容你可以看下面的代码。
|
||||
|
||||
faresWithWatermark = fares
|
||||
.selectExpr("rideId AS rideId_fares", "startTime", "totalFare", "tip")
|
||||
.withWatermark("startTime", "30 minutes")
|
||||
|
||||
ridesWithWatermark = rides
|
||||
.selectExpr("rideId", "endTime", "driverId", "taxiId", "startLon", "startLat", "endLon", "endLat")
|
||||
.withWatermark("endTime", "30 minutes")
|
||||
|
||||
joinDF = faresWithWatermark
|
||||
.join(ridesWithWatermark,
|
||||
expr("""
|
||||
rideId_fares = rideId AND
|
||||
endTime > startTime AND
|
||||
endTime <= startTime + interval 2 hours
|
||||
""")
|
||||
|
||||
|
||||
在这段代码中,我们对fares和rides分别加了半小时的水印,然后把两个DataFrame根据rideId和时间间隔的限制Join起来。这样,joinDF就同时包含了地理位置和付费信息。
|
||||
|
||||
接下来,就让我们开始计算实时的小费最高区域。
|
||||
|
||||
计算结果并输出
|
||||
|
||||
到现在为止,我们还没有处理地点信息。原生的经纬度信息显然并没有很大用处。我们需要做的是把纽约市分割成几个区域,把数据中所有地点的经纬度信息转化成区域信息,这样司机们才可以知道大概哪个地区的乘客比较可能给高点的小费。
|
||||
|
||||
纽约市的区域信息以及坐标可以从网上找到,这部分处理比较容易。每个接收到的数据我们都可以判定它在哪个区域内,然后对joinDF增加一个列“area”来代表终点的区域。现在,让我们假设area已经加到现有的DataFrame里。接下来我们需要把得到的信息告诉司机了。
|
||||
|
||||
还记得第16讲和第17讲中提到的滑动窗口操作吗?这是流处理中常见的输出形式,即输出每隔一段时间内,特定时间窗口的特征值。在这个例子中,我们可以每隔10分钟,输出过去半小时内每个区域内的平均小费。这样的话,司机可以每隔10分钟查看一下数据,决定下一步去哪里接单。这个查询(Query)可以由以下代码产生。
|
||||
|
||||
tips = joinDF
|
||||
.groupBy(
|
||||
window("endTime", "30 minutes", "10 minutes"),
|
||||
"area")
|
||||
.agg(avg("tip"))
|
||||
|
||||
|
||||
最后,我们把tips这个流式DataFrame输出。
|
||||
|
||||
query.writeStream
|
||||
.outputMode("append")
|
||||
.format("console")
|
||||
.option("truncate", False
|
||||
.start()
|
||||
.awaitTermination()
|
||||
|
||||
|
||||
你可能会问,为什么我们不可以把输出结果按小费多少进行排序呢?
|
||||
|
||||
这是因为两个流的inner-join只支持附加输出模式(Append Mode),而现在Structured Streaming不支持在附加模式下进行排序操作。希望将来Structured Streaming可以提供这个功能,但是现在,司机们只能扫一眼所有的输出数据来大概判断哪个地方的小费最高了。
|
||||
|
||||
小结
|
||||
|
||||
流处理和批处理都是非常常见的应用场景,而且相较而言流处理更加复杂,对延迟性要求更高。今天我们再次通过一个实例帮助你了解要如何利用Structured Streaming对真实数据集进行流处理。Spark最大的好处之一就是它拥有统一的批流处理框架和API,希望你在课下要进一步加深对DataSet/DataFrame的熟练程度。
|
||||
|
||||
思考题
|
||||
|
||||
今天的主题是“案例实战”,不过我留的是思考题,而不是实践题。因为我不确定你是否会使用Kafka。如果你的工作中会接触到流数据,那么你可以参考今天这个案例的思路和步骤来解决问题,多加练习以便熟悉Spark的使用。如果你还没有接触过流数据,但却想往这方面发展的话,我就真的建议你去学习一下Kafka,这是个能帮助我们更好地做流处理应用开发和部署的利器。
|
||||
|
||||
现在,来说一下今天的思考题吧。
|
||||
|
||||
|
||||
为什么流的Inner-Join不支持完全输出模式?
|
||||
对于Inner-Join而言,加水印是否是必须的? Outer-Join呢?
|
||||
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
179
专栏/大规模数据处理实战/21深入对比Spark与Flink:帮你系统设计两开花.md
Normal file
179
专栏/大规模数据处理实战/21深入对比Spark与Flink:帮你系统设计两开花.md
Normal file
@@ -0,0 +1,179 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
21 深入对比Spark与Flink:帮你系统设计两开花
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“深入对比Spark与Flink”。
|
||||
|
||||
相信通过这一模块前9讲的学习,你对Spark已经有了基本的认识。现在,我们先来回顾整个模块,理清一下思路。
|
||||
|
||||
首先,从MapReduce框架存在的问题入手,我们知道了Spark的主要优点,比如用内存运算来提高性能;提供很多High-level API;开发者无需用map和reduce两个操作实现复杂逻辑;支持流处理等等。
|
||||
|
||||
接下来,我们学习了Spark的数据抽象——RDD。RDD是整个Spark的核心概念,所有的新API在底层都是基于RDD实现的。但是RDD是否就是完美无缺的呢?显然不是,它还是很底层,不方便开发者使用,而且用RDD API写的应用程序需要大量的人工调优来提高性能。
|
||||
|
||||
Spark SQL提供的DataFrame/DataSet API就解决了这个问题,它提供类似SQL的查询接口,把数据看成关系型数据库的表,提升了熟悉关系型数据库的开发者的工作效率。这部分内容都是专注于数据的批处理,那么我们很自然地就过渡到下一个问题:Spark是怎样支持流处理的呢?
|
||||
|
||||
那就讲到了Spark Streaming和新的Structured Streaming,这是Spark的流处理组件,其中Structured Streaming也可以使用DataSet/DataFrame API,这就实现了Spark批流处理的统一。
|
||||
|
||||
通过这个简单的回顾我们发现,Spark的发布,和之后各个版本新功能的发布,并不是开发人员拍脑袋的决定,每个新版本发布的功能都是在解决旧功能的问题。在如此多的开源工作者的努力下,Spark生态系统才有今天的规模,成为了当前最流行的大数据处理框架之一。
|
||||
|
||||
在开篇词中我就提到过,我希望你能通过这个专栏建立自己的批判性思维,遇到一个新的技术,多问为什么,而不是盲目的接受和学习。只有这样我们才能不随波逐流,成为这个百花齐放的技术时代的弄潮儿。
|
||||
|
||||
所以,这里我想问你一个问题,Spark有什么缺点?
|
||||
|
||||
这个缺点我们之前已经提到过一个——无论是Spark Streaming还是Structured Streaming,Spark流处理的实时性还不够,所以无法用在一些对实时性要求很高的流处理场景中。
|
||||
|
||||
这是因为Spark的流处理是基于所谓微批处理(Micro-batch processing)的思想,即它把流处理看作是批处理的一种特殊形式,每次接收到一个时间间隔的数据才会去处理,所以天生很难在实时性上有所提升。
|
||||
|
||||
虽然在Spark 2.3中提出了连续处理模型(Continuous Processing Model),但是现在只支持很有限的功能,并不能在大的项目中使用。Spark还需要做出很大的努力才能改进现有的流处理模型。
|
||||
|
||||
想要在流处理的实时性上提升,就不能继续用微批处理的模式,而要想办法实现真正的流处理,即每当有一条数据输入就立刻处理,不做等待。那么当今时代有没有这样的流处理框架呢?
|
||||
|
||||
Apache Flink就是其中的翘楚。它采用了基于操作符(Operator)的连续流模型,可以做到微秒级别的延迟。今天我就带你一起了解一下这个流行的数据处理平台,并将Flink与Spark做深入对比,方便你在今后的实际项目中做出选择。
|
||||
|
||||
Flink核心模型简介
|
||||
|
||||
Flink中最核心的数据结构是Stream,它代表一个运行在多个分区上的并行流。
|
||||
|
||||
在Stream上同样可以进行各种转换操作(Transformation)。与Spark的RDD不同的是,Stream代表一个数据流而不是静态数据的集合。所以,它包含的数据是随着时间增长而变化的。而且Stream上的转换操作都是逐条进行的,即每当有新的数据进来,整个流程都会被执行并更新结果。这样的基本处理模式决定了Flink会比Spark Streaming有更低的流处理延迟性。
|
||||
|
||||
当一个Flink程序被执行的时候,它会被映射为Streaming Dataflow,下图就是一个Streaming Dataflow的示意图。
|
||||
|
||||
|
||||
|
||||
在图中,你可以看出Streaming Dataflow包括Stream和Operator(操作符)。转换操作符把一个或多个Stream转换成多个Stream。每个Dataflow都有一个输入数据源(Source)和输出数据源(Sink)。与Spark的RDD转换图类似,Streaming Dataflow也会被组合成一个有向无环图去执行。
|
||||
|
||||
在Flink中,程序天生是并行和分布式的。一个Stream可以包含多个分区(Stream Partitions),一个操作符可以被分成多个操作符子任务,每一个子任务是在不同的线程或者不同的机器节点中独立执行的。如下图所示:
|
||||
|
||||
|
||||
|
||||
从上图你可以看出,Stream在操作符之间传输数据的形式有两种:一对一和重新分布。
|
||||
|
||||
|
||||
一对一(One-to-one):Stream维护着分区以及元素的顺序,比如上图从输入数据源到map间。这意味着map操作符的子任务处理的数据和输入数据源的子任务生产的元素的数据相同。你有没有发现,它与RDD的窄依赖类似。
|
||||
重新分布(Redistributing):Stream中数据的分区会发生改变,比如上图中map与keyBy之间。操作符的每一个子任务把数据发送到不同的目标子任务。
|
||||
|
||||
|
||||
Flink的架构
|
||||
|
||||
当前版本Flink的架构如下图所示。
|
||||
|
||||
|
||||
|
||||
我们可以看到,这个架构和第12讲中介绍的Spark架构比较类似,都分为四层:存储层、部署层、核心处理引擎、high-level的API和库。
|
||||
|
||||
从存储层来看,Flink同样兼容多种主流文件系统如HDFS、Amazon S3,多种数据库如HBase和多种数据流如Kafka和Flume。
|
||||
|
||||
从部署层来看,Flink不仅支持本地运行,还能在独立集群或者在被 YARN 或 Mesos 管理的集群上运行,也能部署在云端。
|
||||
|
||||
核心处理引擎就是我们刚才提到的分布式Streaming Dataflow,所有的高级API及应用库都会被翻译成包含Stream和Operator的Dataflow来执行。
|
||||
|
||||
Flink提供的两个核心API就是DataSet API和DataStream API。你没看错,名字和Spark的DataSet、DataFrame非常相似。顾名思义,DataSet代表有界的数据集,而DataStream代表流数据。所以,DataSet API是用来做批处理的,而DataStream API是做流处理的。
|
||||
|
||||
也许你会问,Flink这样基于流的模型是怎样支持批处理的?在内部,DataSet其实也用Stream表示,静态的有界数据也可以被看作是特殊的流数据,而且DataSet与DataStream可以无缝切换。所以,Flink的核心是DataStream。
|
||||
|
||||
DataSet和DataStream都支持各种基本的转换操作如map、filter、count、groupBy等,让我们来看一个用DataStream实现的统计词频例子。
|
||||
|
||||
public class WindowWordCount {
|
||||
public static void main(String[] args) throws Exception {
|
||||
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
|
||||
|
||||
|
||||
DataStream<Tuple2<String, Integer>> dataStream = env
|
||||
.socketTextStream("localhost", 9999)
|
||||
.flatMap(new Splitter())
|
||||
.keyBy(0)
|
||||
.timeWindow(Time.seconds(5))
|
||||
.sum(1);
|
||||
|
||||
|
||||
dataStream.print();
|
||||
env.execute("Window WordCount");
|
||||
}
|
||||
|
||||
|
||||
public static class Splitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
|
||||
@Override
|
||||
public void flatMap(String sentence, Collector<Tuple2<String, Integer>> out) {
|
||||
for (String word: sentence.split(" ")) {
|
||||
out.collect(new Tuple2<String, Integer>(word, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
这里我是用Java来示范的,因为Flink就是用Java开发的,所以它对Java有原生的支持。此外,也可以用Scala来开发Flink程序,在1.0版本后更是支持了Python。
|
||||
|
||||
在这个例子中,我们首先创建了一个Splitter类,来把输入的句子拆分成(词语,1)的对。在主程序中用StreamExecutionEnvironment创建DataStream,来接收本地Web Socket的文本流,并进行了4步操作。
|
||||
|
||||
|
||||
用flatMap把输入文本拆分成(词语,1)的对;
|
||||
用keyBy把相同的词语分配到相同的分区;
|
||||
设好5秒的时间窗口;
|
||||
对词语的出现频率用sum求和。
|
||||
|
||||
|
||||
可以看出,DataStream的使用方法和RDD比较相似,都是把程序拆分成一系列的转换操作并分布式地执行。
|
||||
|
||||
在DataSet和DataStream之上,有更高层次的Table API。Table API和Spark SQL的思想类似,是关系型的API,用户可以像操作SQL数据库表一样的操作数据,而不需要通过写Java代码、操作DataStream/DataSet的方式进行数据处理,更不需要手动优化代码的执行逻辑。
|
||||
|
||||
此外,Table API同样统一了Flink的批处理和流处理。
|
||||
|
||||
Flink和Spark对比
|
||||
|
||||
通过前面的学习,我们了解到,Spark和Flink都支持批处理和流处理,接下来让我们对这两种流行的数据处理框架在各方面进行对比。
|
||||
|
||||
首先,这两个数据处理框架有很多相同点。
|
||||
|
||||
|
||||
都基于内存计算;
|
||||
都有统一的批处理和流处理API,都支持类似SQL的编程接口;
|
||||
都支持很多相同的转换操作,编程都是用类似于Scala Collection API的函数式编程模式;
|
||||
都有完善的错误恢复机制;
|
||||
都支持Exactly once的语义一致性。
|
||||
|
||||
|
||||
当然,它们的不同点也是相当明显,我们可以从4个不同的角度来看。
|
||||
|
||||
从流处理的角度来讲,Spark基于微批量处理,把流数据看成是一个个小的批处理数据块分别处理,所以延迟性只能做到秒级。而Flink基于每个事件处理,每当有新的数据输入都会立刻处理,是真正的流式计算,支持毫秒级计算。由于相同的原因,Spark只支持基于时间的窗口操作(处理时间或者事件时间),而Flink支持的窗口操作则非常灵活,不仅支持时间窗口,还支持基于数据本身的窗口,开发者可以自由定义想要的窗口操作。
|
||||
|
||||
从SQL功能的角度来讲,Spark和Flink分别提供SparkSQL和Table API提供SQL交互支持。两者相比较,Spark对SQL支持更好,相应的优化、扩展和性能更好,而 Flink 在 SQL 支持方面还有很大提升空间。
|
||||
|
||||
从迭代计算的角度来讲,Spark对机器学习的支持很好,因为可以在内存中缓存中间计算结果来加速机器学习算法的运行。但是大部分机器学习算法其实是一个有环的数据流,在Spark中,却是用无环图来表示。而Flink支持在运行时间中的有环数据流,从而可以更有效的对机器学习算法进行运算。
|
||||
|
||||
从相应的生态系统角度来讲,Spark的社区无疑更加活跃。Spark可以说有着Apache旗下最多的开源贡献者,而且有很多不同的库来用在不同场景。而Flink由于较新,现阶段的开源社区不如Spark活跃,各种库的功能也不如Spark全面。但是Flink还在不断发展,各种功能也在逐渐完善。
|
||||
|
||||
小结
|
||||
|
||||
今天我们从Spark存在的一个缺点——无法高效应对低延迟的流处理场景入手,一起学习了另一个主流流数据处理框架Flink,还对比了这两个框架的异同,相信现在你对两个框架肯定有了更多的认识。
|
||||
|
||||
我经常被问到的一个问题是:Spark和Flink到底该选哪一个?对于这个问题,我们还是要分一下场景。
|
||||
|
||||
对于以下场景,你可以选择Spark。
|
||||
|
||||
|
||||
数据量非常大而且逻辑复杂的批数据处理,并且对计算效率有较高要求(比如用大数据分析来构建推荐系统进行个性化推荐、广告定点投放等);
|
||||
基于历史数据的交互式查询,要求响应较快;
|
||||
基于实时数据流的数据处理,延迟性要求在在数百毫秒到数秒之间。
|
||||
|
||||
|
||||
Spark完美满足这些场景的需求, 而且它可以一站式解决这些问题,无需用别的数据处理平台。
|
||||
|
||||
由于Flink是为了提升流处理而创建的平台,所以它适用于各种需要非常低延迟(微秒到毫秒级)的实时数据处理场景,比如实时日志报表分析。
|
||||
|
||||
而且Flink用流处理去模拟批处理的思想,比Spark用批处理去模拟流处理的思想扩展性更好,所以我相信将来Flink会发展的越来越好,生态和社区各方面追上Spark。比如,阿里巴巴就基于Flink构建了公司范围内全平台使用的数据处理平台Blink,美团、饿了么等公司也都接受Flink作为数据处理解决方案。
|
||||
|
||||
可以说,Spark和Flink都在某种程度上统一了批处理和流处理,但也都有一些不足。下一模块中,让我们来一起学习一个全新的、完全统一批流处理的数据处理平台——Apache Beam,到时候我们会对Spark的优缺点有更加深入的认识。
|
||||
|
||||
思考题
|
||||
|
||||
除了高延迟的流处理这一缺点外,你认为Spark还有什么不足?可以怎样改进?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
137
专栏/大规模数据处理实战/22ApacheBeam的前世今生.md
Normal file
137
专栏/大规模数据处理实战/22ApacheBeam的前世今生.md
Normal file
@@ -0,0 +1,137 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
22 Apache Beam的前世今生
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“ Apache Beam的前世今生”。
|
||||
|
||||
从这一讲开始,我们将进入一个全新的篇章。在这一讲中,我将会带领你了解Apache Beam的完整诞生历程。
|
||||
|
||||
让我们一起来感受一下,Google是如何从处理框架上的一无所有,一直发展到推动、制定批流统一的标准的。除此之外,我还会告诉你,在2004年发布了MapReduce论文之后,Google在大规模数据处理实战中到底经历了哪些技术难题和技术变迁。我相信通过这一讲,你将会完整地认识到为什么Google会强力推崇Apache Beam。
|
||||
|
||||
在2003年以前,Google内部其实还没有一个成熟的处理框架来处理大规模数据。而当时Google的搜索业务又让工程师们不得不面临着处理大规模数据的应用场景,像计算网站URL访问量、计算网页的倒排索引(Inverted Index)等等。
|
||||
|
||||
那该怎么办呢?这个答案既简单又复杂:自己写一个。
|
||||
|
||||
没错,当时的工程师们需要自己写一个自定义的逻辑处理架构来处理这些数据。因为需要处理的数据量非常庞大,业务逻辑不太可能只放在一台机器上面运行。很多情况下,我们都必须把业务逻辑部署在分布式环境中。所以,这个自定义的逻辑处理架构还必须包括容错系统(Fault Tolerant System)的设计。
|
||||
|
||||
久而久之,Google内部不同组之间都会开发出一套自己组内的逻辑处理架构。因为工程师们遇到的问题很多都是相似的,开发出来的这些逻辑处理架构很多时候也都是大同小异,只是有一些数据处理上的逻辑差别而已。这无疑就变成了大家一起重复造轮子的情况。
|
||||
|
||||
这时候,就有工程师想到,能不能改善这一种状况。MapReduce的架构思想也就由此应运而生。
|
||||
|
||||
MapReduce
|
||||
|
||||
其实MapReduce的架构思想可以从两个方面来看。
|
||||
|
||||
一方面,它希望能提供一套简洁的API来表达工程师数据处理的逻辑。另一方面,要在这一套API底层嵌套一套扩展性很强的容错系统,使得工程师能够将心思放在逻辑处理上,而不用过于分心去设计分布式的容错系统。
|
||||
|
||||
这个架构思想的结果你早就已经知道了。MapReduce这一套系统在Google获得了巨大成功。在2004年的时候,Google发布的一篇名为“MapReduce: Simplified Data Processing on Large Clusters”的论文就是这份成果的总结。
|
||||
|
||||
在MapReduce的计算模型里,它将数据的处理抽象成了以下这样的计算步骤
|
||||
|
||||
|
||||
Map:计算模型从输入源(Input Source)中读取数据集合,这些数据在经过了用户所写的逻辑后生成出一个临时的键值对数据集(Key/Value Set)。MapReduce计算模型会将拥有相同键(Key)的数据集集中起来然后发送到下一阶段。这一步也被称为Shuffle阶段。
|
||||
Reduce:接收从Shuffle阶段发送过来的数据集,在经过了用户所写的逻辑后生成出零个或多个结果。
|
||||
|
||||
|
||||
很多人都说,这篇MapReduce论文是具有划时代意义的。可你知道为什么都这么说吗?
|
||||
|
||||
这是因为Map和Reduce这两种抽象其实可以适用于非常多的应用场景,而MapReduce论文里面所阐述的容错系统,可以让我们所写出来的数据处理逻辑在分布式环境下有着很好的可扩展性(Scalability)。
|
||||
|
||||
MapReduce在内部的成功使得越来越多的工程师希望使用MapReduce来解决自己项目的难题。
|
||||
|
||||
但是,就如我在模块一中所说的那样,使用MapReduce来解决一个工程难题往往会涉及到非常多的步骤,而每次使用MapReduce的时候我们都需要在分布式环境中启动机器来完成Map和Reduce步骤,以及启动Master机器来协调这两个步骤的中间结果(Intermediate Result),消耗不少硬件上的资源。
|
||||
|
||||
这样就给工程师们带来了以下一些疑问:
|
||||
|
||||
|
||||
我们的项目数据规模是否真的需要运用MapReduce来解决呢?是否可以在一台机器上的内存中解决呢?
|
||||
我们所写的MapReduce项目是否已经是最优的呢?因为每一个Map和Reduce步骤这些中间结果都需要写在磁盘上,会十分耗时。是否有些步骤可以省略或者合并呢?我们是否需要让工程师投入时间去手动调试这些MapReduce项目的性能呢?
|
||||
|
||||
|
||||
问题既然已经提出来了,Google的工程师们便开始考虑是否能够解决上述这些问题。最好能够让工程师(无论是新手工程师亦或是经验老到的工程师)都能专注于数据逻辑上的处理,而不用花更多时间在测试调优上。
|
||||
|
||||
FlumeJava就是在这样的背景下诞生的。
|
||||
|
||||
FlumeJava
|
||||
|
||||
这里,我先将FlumeJava的成果告诉你。因为FlumeJava的思想又在Google内容获得了巨大成功,Google也希望将这个思想分享给业界。所以在2010年的时候,Google公开了FlumeJava架构思想的论文。
|
||||
|
||||
|
||||
|
||||
FlumeJava的思想是将所有的数据都抽象成名为PCollection的数据结构,无论是从内存中读取的数据,还是在分布式环境下所读取的文件。
|
||||
|
||||
这样的抽象对于测试代码中的逻辑是十分有好处的。要知道,想测试MapReduce的话,你可能需要读取测试数据集,然后在分布式环境下运行,来测试代码逻辑。但如果你有了PCollection这一层抽象的话,你的测试代码可以在内存中读取数据然后跑测试文件,也就是同样的逻辑既可以在分布式环境下运行也可以在单机内存中运行。
|
||||
|
||||
而FlumeJava在MapReduce框架中Map和Reduce思想上,抽象出4个了原始操作(Primitive Operation),分别是parallelDo、groupByKey、 combineValues和flatten,让工程师可以利用这4种原始操作来表达任意Map或者Reduce的逻辑。
|
||||
|
||||
同时,FlumeJava的架构运用了一种Deferred Evaluation的技术,来优化我们所写的代码。
|
||||
|
||||
对于Deferred Evaluation,你可以理解为FlumeJava框架会首先会将我们所写的逻辑代码静态遍历一次,然后构造出一个执行计划的有向无环图。这在FlumeJava框架里被称为Execution Plan Dataflow Graph。
|
||||
|
||||
有了这个图之后,FlumeJava框架就会自动帮我们优化代码。例如,合并一些本来可以通过一个Map和Reduce来表达,却被新手工程师分成多个Map和Reduce的代码。
|
||||
|
||||
FlumeJava框架还可以通过我们的输入数据集规模,来预测输出结果的规模,从而自行决定代码是放在内存中跑还是在分布式环境中跑。
|
||||
|
||||
总的来说,FlumeJava是非常成功的。但是,FlumeJava也有一个弊端,那就是FlumeJava基本上只支持批处理(Batch Execution)的任务,对于无边界数据(Unbounded Data)是不支持的。所以,Google内部有着另外一个被称为Millwheel的项目来支持处理无边界数据,也就是流处理框架。
|
||||
|
||||
在2013年的时候,Google也公开了Millwheel思想的论文。
|
||||
|
||||
|
||||
|
||||
这时Google的工程师们回过头看,感叹了一下成果,并觉得自己可以再优秀一些:既然我们已经创造出好几个优秀的大规模数据处理框架了,那我们能不能集合这几个框架的优点,推出一个统一的框架呢?
|
||||
|
||||
这也成为了Dataflow Model诞生的契机。
|
||||
|
||||
Dataflow Model
|
||||
|
||||
在2015年时候,Google公布了Dataflow Model的论文,同时也推出了基于Dataflow Model思想的平台Cloud Dataflow,让Google以外的工程师们也能够利用这些SDK来编写大规模数据处理的逻辑。
|
||||
|
||||
|
||||
|
||||
讲到这么多,你可能会有个疑问了,怎么Apache Beam还没有出场呢?别着急,Apache Beam的登场契机马上就到了。
|
||||
|
||||
Apache Beam
|
||||
|
||||
前面我说了,Google基于Dataflow Model的思想推出了Cloud Dataflow云平台,但那毕竟也需要工程师在Google的云平台上面运行程序才可以。如果有的工程师希望在别的平台上面跑该如何解决呢?
|
||||
|
||||
所以,为了解决这个问题,Google在2016年的时候联合了Talend、Data Artisans、Cloudera这些大数据公司,基于Dataflow Model的思想开发出了一套SDK,并贡献给了Apache Software Foundation。而它Apache Beam的名字是怎么来的呢?就如下图所示,Beam的含义就是统一了批处理和流处理的一个框架。
|
||||
|
||||
|
||||
|
||||
这就是Apache Beam的发展历史,从中你可以看到它拥有很多优点,而这也是我们需要Beam的原因。
|
||||
|
||||
在现实世界中,很多时候我们不可避免地需要对数据同时进行批处理和流处理。Beam提供了一套统一的API来处理这两种数据处理模式,让我们只需要将注意力专注于在数据处理的算法上,而不用再花时间去对两种数据处理模式上的差异进行维护。
|
||||
|
||||
它能够将工程师写好的算法逻辑很好地与底层的运行环境分隔开。也就是说,当我们通过Beam提供的API写好数据处理逻辑后,这个逻辑可以不作任何修改,直接放到任何支持Beam API的底层系统上运行。
|
||||
|
||||
关于怎么理解这个优点,其实我们可以借鉴一下SQL(Structure Query Language)的运行模式。
|
||||
|
||||
我们在学习SQL语言的时候,基本上都是独立于底层数据库系统来学习的。而在我们写完一个分析数据的Query之后,只要底层数据库的Schema不变,这个Query是可以放在任何数据库系统上运行的,例如放在MySql上或者Oracle DB上。
|
||||
|
||||
同样的,我们用Beam API写好的数据处理逻辑无需改变,可以根据自身的需求,将逻辑放在Google Cloud Dataflow上跑,也可以放在Apache Flink上跑。在Beam上,这些底层运行的系统被称为Runner。现阶段Apache Beam支持的Runner有近十种,包括了我们很熟悉的Apache Spark和Apache Flink。
|
||||
|
||||
当然最后Apache Beam也是希望对自身的SDK能够支持任意多的语言来编写。现阶段Beam支持Java、Python和Golang。
|
||||
|
||||
|
||||
|
||||
也就是说,通过Apache Beam,最终我们可以用自己喜欢的编程语言,通过一套Beam Model统一地数据处理API,编写好符合自己应用场景的数据处理逻辑,放在自己喜欢的Runner上运行。
|
||||
|
||||
小结
|
||||
|
||||
今天,我与你一起回顾了Apache Beam的完整诞生历程。
|
||||
|
||||
通过这一讲,我希望你知道每一项技术都不会毫无理由地诞生,而每一项技术诞生的背后都是为了解决某些特定问题的。了解前人一步步解决问题的过程,有助于我们更有层次地理解一项技术产生的根本原因。在学习一项技术之前,先了解了它的历史源流,可以让我们做到知其然,并知其所以然。
|
||||
|
||||
思考题
|
||||
|
||||
你也能分享一些你所经历过的技术变迁或是技术诞生的故事吗?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
150
专栏/大规模数据处理实战/23站在Google的肩膀上学习Beam编程模型.md
Normal file
150
专栏/大规模数据处理实战/23站在Google的肩膀上学习Beam编程模型.md
Normal file
@@ -0,0 +1,150 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
23 站在Google的肩膀上学习Beam编程模型
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的话题是“站在Google的肩膀上学习Beam编程模型”。
|
||||
|
||||
在上一讲中,我带你一起领略了Apache Beam的完整诞生历史。通过上一讲,你应该对于Apache Beam在大规模数据处理中能够带来的便利有了一定的了解。
|
||||
|
||||
而在这一讲中,让我们一起来学习Apache Beam的编程模型,帮助你打下良好的基础以便应对接下来的Beam实战篇。希望你在以后遇到不同的数据处理问题时,可以有着Beam所提倡的思考模式。
|
||||
|
||||
现在让我们一起进入Beam的世界吧。
|
||||
|
||||
为什么要先学习Beam的编程模型?
|
||||
|
||||
可能你会有疑问,很多人学习一项新技术的时候,都是从学习SDK的使用入手,为什么我们不同样的从SDK入手,而是要先学习Beam的编程模型呢?
|
||||
|
||||
我的答案有两点。
|
||||
|
||||
第一,Apache Beam和其他开源项目不太一样,它并不是一个数据处理平台,本身也无法对数据进行处理。Beam所提供的是一个统一的编程模型思想,而我们可以通过这个统一出来的接口来编写符合自己需求的处理逻辑,这个处理逻辑将会被转化成为底层运行引擎相应的API去运行。
|
||||
|
||||
第二,学习Apache Beam的时候,如果只学习SDK的使用,可能你不一定能明白这些统一出来的SDK设计背后的含义,而这些设计的思想又恰恰是涵盖了解决数据处理世界中我们所能遇见的问题。我认为将所有的SDK都介绍一遍是不现实的。SDK会变,但它背后的原理却却不会改变,只有当我们深入了解了整个设计原理后,遇到各种应用场景时,才能处理得更加得心应手。
|
||||
|
||||
Beam的编程模型
|
||||
|
||||
那事不宜迟,我们来看看Beam的编程模型到底指的是什么?
|
||||
|
||||
简单来说,Beam的编程模型需要让我们根据“WWWH”这四个问题来进行数据处理逻辑的编写。“WWWH”是哪四个问题呢?这里我先卖个关子,在进入四个具体问题之前,我需要先介绍一下根据Beam编程模型所建立起来的Beam生态圈,帮助你理解Beam的编程模型会涉及到的几个概念。整个Apache Beam的生态圈构成就如下图所示。
|
||||
|
||||
|
||||
|
||||
为了帮助你理解,我为这几层加了编号,数字编号顺序是自下而上的,你可以对照查找。
|
||||
|
||||
第一层,是现在已有的各种大数据处理平台(例如Apache Spark或者Apache Flink),在Beam中它们也被称为Runner。
|
||||
|
||||
第二层,是可移植的统一模型层,各个Runners将会依据中间抽象出来的这个模型思想,提供一套符合这个模型的APIs出来,以供上层转换。
|
||||
|
||||
第三层,是SDK层。SDK层将会给工程师提供不同语言版本的API来编写数据处理逻辑,这些逻辑就会被转化成Runner中相应的API来运行。
|
||||
|
||||
第四层,是可扩展库层。工程师可以根据已有的Beam SDK,贡献分享出更多的新开发者SDK、IO连接器、转换操作库等等。
|
||||
|
||||
第五层,我们可以看作是应用层,各种应用将会通过下层的Beam SDK或工程师贡献的开发者SDK来实现。
|
||||
|
||||
最上面的第六层,也就是社区一层。在这里,全世界的工程师可以提出问题,解决问题,实现解决问题的思路。
|
||||
|
||||
通过第6讲的内容,我们已经知道,这个世界中的数据可以分成有边界数据和无边界数据,而有边界数据又是无边界数据的一种特例。所以,我们都可以将所有的数据抽象看作是无边界数据。
|
||||
|
||||
同时,每一个数据都是有两种时域的,分别是事件时间和处理时间。我们在处理无边界数据的时候,因为在现实世界中,数据会有延时、丢失等等的状况发生,我们无法保证现在到底是否接收完了所有发生在某一时刻之前的数据。所以现实中,流处理必须在数据的完整性和数据处理的延时性上作出取舍。Beam编程模型就是在这样的基础上提出的。
|
||||
|
||||
Beam编程模型会涉及到的4个概念,窗口、水位线、触发器和累加模式,我来为你介绍一下。
|
||||
|
||||
|
||||
窗口(Window)
|
||||
|
||||
|
||||
窗口将无边界数据根据事件时间分成了一个个有限的数据集。我们可以看看批处理这个特例。在批处理中,我们其实是把一个无穷小到无穷大的时间窗口赋予了数据集。我会在第32讲中,对窗口这个概念进行详细地介绍。
|
||||
|
||||
|
||||
水位线(Watermark)
|
||||
|
||||
|
||||
水位线是用来表示与数据事件时间相关联的输入完整性的概念。对于事件时间为X的水位线是指:数据处理逻辑已经得到了所有事件时间小于X的无边界数据。在数据处理中,水位线是用来测量数据进度的。
|
||||
|
||||
|
||||
触发器(Triggers)
|
||||
|
||||
|
||||
触发器指的是表示在具体什么时候,数据处理逻辑会真正地触发窗口中的数据被计算。触发器能让我们可以在有需要时对数据进行多次运算,例如某时间窗口内的数据有更新,这一窗口内的数据结果需要重算。
|
||||
|
||||
|
||||
累加模式(Accumulation)
|
||||
|
||||
|
||||
累加模式指的是如果我们在同一窗口中得到多个运算结果,我们应该如何处理这些运算结果。这些结果之间可能完全不相关,例如与时间先后无关的结果,直接覆盖以前的运算结果即可。这些结果也可能会重叠在一起。
|
||||
|
||||
懂得了这几个概念之后,我来告诉你究竟Beam编程模型中的“WWWH”是什么。它们分别是:What、Where、When、How。
|
||||
|
||||
What results are being calculated?
|
||||
|
||||
|
||||
|
||||
我们要做什么计算?得到什么样的结果?Beam SDK中各种transform操作就是用来回答这个问题的。这包括我们经常使用到批处理逻辑,训练机器学习模型的逻辑等等。
|
||||
|
||||
举个例子,我们每次学习大规模数据处理时所用到的经典例子WordCount里,我们想要得到在一篇文章里每个单词出现的次数,那我们所要做的计算就是通过Transform操作将一个单词集合转换成以单词为Key,单词出现次数为Value的集合。
|
||||
|
||||
Where in event time they are being computed?
|
||||
|
||||
|
||||
|
||||
计算什么时间范围的数据?这里的“时间”指的是数据的事件时间。我们可以通过窗口这个概念来回答这个问题。
|
||||
|
||||
例如,我们有三个不同的数据,它们的事件时间分别是12:01、12:59和14:00。如果我们的时间窗口设定为[12:00 , 13:00),那我们所需要处理的数据就是前两个数据了。
|
||||
|
||||
When in processing time they are materialized?
|
||||
|
||||
|
||||
|
||||
何时将计算结果输出?我们可以通过使用水位线和触发器配合触发计算。
|
||||
|
||||
在之前的概念中,我们知道触发器指的就是何时触发一个窗口中的数据被计算出最终结果。在Beam中,我们可以有多种多样的触发器信号,例如根据处理时间的信号来触发,也就是说每隔了一段时间Beam就会重新计算一遍窗口中的数据;也可以根据元素的计数来触发,意思就是在一个窗口中的数据只要达到一定的数据,这个窗口的数据就会被拿来计算结果。
|
||||
|
||||
现在我来举一个以元素计数来触发的例子。我们现在定义好的固定窗口(Fixed Window)时间范围为1个小时,从每天的凌晨00:00开始计算,元素计数定为2。我们需要处理的无边界数据是商品交易数据,我们需要计算在一个时间窗口中的交易总量。
|
||||
|
||||
为了方便说明,我们假设只接收到了4个数据点,它们按照以下顺序进入我们的处理逻辑。
|
||||
|
||||
|
||||
于6月11号23:59产生的10元交易;
|
||||
于6月12号00:01产生的15元交易;
|
||||
于6月11号23:57产生的20元交易;
|
||||
于6月11号23:57产生的30元交易。
|
||||
|
||||
|
||||
接收到第三个数据的时候,6月11号这个24小时窗口的数据已经达到了两个,所以触发了这个窗口内的数据计算,也就是6月11号的窗口内交易总量现在为10+20=30元。
|
||||
|
||||
当第四个数据(6月11号23:57产生的30元交易)进入处理逻辑时,6月11号这个24小时窗口的数据又超过了两个元素,这个窗口的计算再次被触发,交易总量被更新为30+30=60元。你可以看到,由于6月12号这个窗口的数据一直没有达到我们预先设定好的2,所以就一直没有被触发计算。
|
||||
|
||||
How earlier results relate to later refinements?
|
||||
|
||||
|
||||
|
||||
后续数据的处理结果如何影响之前的处理结果呢?这个问题可以通过累加模式来解决,常见的累加模式有:丢弃(结果之间是独立且不同的)、累积(后来的结果建立在先前的结果上)等等。
|
||||
|
||||
还是以刚刚上面所讲述的4个交易数据点为例子,你可能会认为这里我们采取的累加模式是累积,其实我们采取的是丢弃。因为我们从始至终只保存着一个计算结果。这里要再引入一个概念,每一次通过计算一个窗口中的数据而得到的结果,我们可以称之为窗格(Pane)。
|
||||
|
||||
我们可以看到,当数据处理逻辑第一次产生6月11号这个窗口结果的时候,两次交易相加产生的30元成为了一个窗格。而第二次产生窗口结果是60元,这个结果又是一个窗格。因为我们只需要计算在一个窗口时间中的交易总量,所以第一个窗格随之被丢弃,只保留了最新的窗格。如果我们采用的是累积的累加模式呢,那这两个交易总量30元和60元都会被保存下来,成为历史记录。
|
||||
|
||||
Beam的编程模型将所有的数据处理逻辑都分割成了这四个纬度,统一成了Beam SDK。我们在基于Beam SDK构建数据处理业务逻辑时,只需要根据业务需求,按照这四个维度调用具体的API,即可生成符合自己要求的数据处理逻辑。Beam会自动转化数据处理逻辑,并提交到具体的Runner上去执行。我们可以看到,无论是Runner自身的API还是Beam的SDK设计,都需要有能力解决上述四个问题。Beam的编程模型是贯穿了Beam生态圈中的每一层的。
|
||||
|
||||
在模块四的后续的内容中,我们会围绕着这四个问题展开具体的分析,看看在Beam的实战中,这每一步是如何被解答的。
|
||||
|
||||
小结
|
||||
|
||||
Google如此地推崇Apache Beam开源项目,除了借此能够推广自己的云计算平台之外,更是借鉴了Apache Hadoop在开源社区中所取得的巨大成功。Google希望为外界贡献一个容易使用而又功能强大的大数据处理模型,可以同时适用于流处理和批处理,并且还可以移植于各种不同数据处理平台上。
|
||||
|
||||
在Beam的生态圈中,我们可以看到,每一层的设计都是根据Beam的编程模型来搭建的。懂得了Beam编程模型之后,我们可以为生态圈中的任意一层做出贡献。
|
||||
|
||||
思考题
|
||||
|
||||
在现实应用中,你能否根据Beam的编程模型来分享你会怎么设计自己的数据处理逻辑呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
161
专栏/大规模数据处理实战/24PCollection:为什么Beam要如此抽象封装数据?.md
Normal file
161
专栏/大规模数据处理实战/24PCollection:为什么Beam要如此抽象封装数据?.md
Normal file
@@ -0,0 +1,161 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
24 PCollection:为什么Beam要如此抽象封装数据?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“为什么Beam要如此抽象封装数据”。
|
||||
|
||||
很多人在刚开始接触Apache Beam的时候,都会觉得这里面的概念太抽象了。什么PCollection、PValue、Transform……这都是些什么?尤其是PCollection,完全和先前的技术知识找不到对应。
|
||||
|
||||
确实如此。同样作为数据的容器,PCollection却并不像Python/Java的List或者C++的vector。PCollection是无序的,Beam对于PCollection中元素的处理顺序不作任何保证。所以,你不可能说“我想处理PCollection中的第二个元素”,因为它就没有“第几个”这种概念。
|
||||
|
||||
PCollection也不像Python/Java的Set,或者C++的unordered_set,PCollection不一定有固定的边界。所以,你也不能指望去查找一个PCollection的大小。在PCollection的世界里,也没有“固定大小”这样的概念。
|
||||
|
||||
作为程序员,我很讨厌重复造轮子,尤其是新瓶装旧酒。的确,有很多开发者为了体现自己项目的复杂度,故意强行引进了很多概念,让大家都似懂非懂的。这就像是为了体现自己知道茴香豆的“茴”有几种写法一样,故意用另一种写法来体现自己“有文化”。
|
||||
|
||||
那么Apache Beam引进这么多概念,是不是也是故意强行造轮子呢?答案是否定的。这一讲,我们就要分析PCollection为什么会如此设计。
|
||||
|
||||
在之前的章节中,我们已经讲到了Apache Beam的“爷爷”——FlumeJava产生的背景。
|
||||
|
||||
当时Google的工程师们发现MapReduce使用起来并没有很方便。如果计算过程都能够分解成map、shuffle、reduce步骤的话,MapReduce是非常能胜任的。
|
||||
|
||||
但是,很多现实世界中的问题,往往需要一系列的map或reduce步骤。这样的数据流水线就需要额外的代码用来协调这些map或reduce步骤。也需要一些额外的代码去管理分步的map/reduce步骤产生的一些中间结果。以至于新的开发者很难理解复杂的流水线。
|
||||
|
||||
清楚这样的背景对于理解Apache Beam的PCollection起源很有帮助。因为,这个项目起源只是为了提供比MapReduce更好的开发体验,最终的运算引擎仍然是MapReduce。
|
||||
|
||||
为什么需要PCollection?
|
||||
|
||||
那么,为什么Beam需要PCollection这样一个全新的抽象数据结构呢?
|
||||
|
||||
我们知道,不同的技术系统有不同的数据结构。比如,C++里有vector、unordered_map,安卓有ListView。相比它们而言,其实Beam的数据结构体系是很单调的,几乎所有数据都能表达为PCollection。
|
||||
|
||||
PCollection,就是Parallel Collection,意思是可并行计算的数据集。如果你之前学习了Spark的章节,就会发现PCollection和RDD十分相似。
|
||||
|
||||
在一个分布式计算系统中,我们作为架构设计者需要为用户隐藏的实现细节有很多,其中就包括了数据是怎样表达和存储的。
|
||||
|
||||
这个数据可能是来自于内存的数据(内部可能只是由一个C++ array存储);也有可能是来自外部文件(由几个文件存储);或者是来自于MySQL数据库(由数据库的行来表达)。
|
||||
|
||||
如果没有一个统一的数据抽象的话,开发者就需要不停地更改代码。比如,在小规模测试的时候用C++ vector,等到了真实的生产环境,我再换MySQL row。沉溺于这样的实现细节会让开发者无法集中注意力在真正重要的事情上,那就是“你想怎样处理数据”。
|
||||
|
||||
清楚了这些,你就能明白我们需要一层抽象来表达数据,而这层抽象就是PCollection。
|
||||
|
||||
PCollection的创建完全取决于你的需求。比如,在测试中PCollection往往来自于代码生成的伪造数据,或者从文件中读取。
|
||||
|
||||
Python
|
||||
|
||||
lines = (p
|
||||
| beam.Create(['To be, or not to be: that is the question. ']))
|
||||
|
||||
|
||||
|
||||
|
||||
lines = p | 'ReadMyFile' >> beam.io.ReadFromText('gs://some/inputData.txt'
|
||||
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> lines = p.apply(
|
||||
"ReadMyFile", TextIO.read().from("protocol://path/to/some/inputData.txt"));
|
||||
|
||||
|
||||
为什么PCollection需要Coders?
|
||||
|
||||
与普通编程相比,PCollection的另一个不同之处是,你需要为PCollection的元素编写Coder。例如,你有一个自己的类MyClass,那么PCollection 一定需要实现Coder 。
|
||||
|
||||
刚开始使用Beam时,你可能会感觉这很不方便。例如,你只是要去读取MySQL的一个表,也得为此实现Coder。
|
||||
|
||||
Coder的作用和Beam的本质紧密相关。因为你的计算流程最终会运行在一个分布式系统。所以,所有的数据都有可能在网络上的计算机之间相互传递。而Coder就是在告诉Beam,怎样把你的数据类型序列化和逆序列化,以方便在网络上传输。
|
||||
|
||||
Coder需要注册进全局的CoderRegistry中,简单来说,是为你自己的数据类型建立与Coder的对应关系。不然每次你都需要手动指定Coder。
|
||||
|
||||
Python
|
||||
|
||||
apache_beam.coders.registry.register_coder(int, BigEndianIntegerCoder)
|
||||
|
||||
|
||||
Java
|
||||
|
||||
PipelineOptions options = PipelineOptionsFactory.create();
|
||||
Pipeline p = Pipeline.create(options);
|
||||
|
||||
|
||||
CoderRegistry cr = p.getCoderRegistry();
|
||||
cr.registerCoder(Integer.class, BigEndianIntegerCoder.class);
|
||||
|
||||
|
||||
为什么PCollection是无序的?
|
||||
|
||||
讲完为什么PCollection需要Coder之后,我们再来看下,为什么PCollection是无序的。
|
||||
|
||||
PCollection的无序特性其实也和它的分布式本质有关。一旦一个PCollection被分配到不同的机器上执行,那么为了保证最大的处理输出,不同机器都是独立运行的。所以,它的执行顺序就无从得知了。可能是第一个元素先被运行,也可能是第二个元素先被运行。所以,肯定不会有PCollection[2]这样的运算符。
|
||||
|
||||
为什么PCollection没有固定大小?
|
||||
|
||||
无序也就算了,为什么PCollection还没有固定大小呢?
|
||||
|
||||
前面的章节中讲到过,Beam想要统一批处理和流处理,所以它要统一表达有界数据和无界数据。正因为如此,PCollection并没有限制它的容量。如前面所说,它可能表达内存上的一个数组,也可能表达整个数据库的所有数据。
|
||||
|
||||
一个PCollection可以是有界的,也可以是无界的。一个有界的PCollection表达了一个已知大小的固定的数据集。一个无界的PCollection表达了一个无限大小的数据集。事实上一个PCollection是否有界,往往取决于它是如何产生的。
|
||||
|
||||
从批处理的数据源中读取,比如一个文件或者是一个数据库,就会产生有界的PColleciton。如果从流式的或者是持续更新的数据库中读取,比如pub/sub或者kafka,会产生一个无界的PCollection。
|
||||
|
||||
但是,PCollection的有界和无界特性会影响到Beam的数据处理方式。一个批处理作业往往处理有界数据。而无界的PCollection需要流式的作业来连续处理。
|
||||
|
||||
在实现中,Beam也是用window来分割持续更新的无界数据。所以,一个流数据可以被持续地拆分成不同的小块。这样的处理方式我们会在实战部分展开。
|
||||
|
||||
如何理解PCollection的不可变性?
|
||||
|
||||
在普通编程语言中,大部分数据结构都是可变的。
|
||||
|
||||
Python
|
||||
|
||||
Alist = []
|
||||
alist.append(1)
|
||||
|
||||
|
||||
C++
|
||||
|
||||
Std::vector<int> list;
|
||||
list.push_back(1);
|
||||
|
||||
|
||||
但是PCollection不提供任何修改它所承载数据的方式。修改一个PCollection的唯一方式就是去转化(Transform)它,下一讲会展开讲Transformation。
|
||||
|
||||
但是在这一讲,我们需要理解的是,Beam的PCollection都是延迟执行(deferred execution)的模式。也就是说,当你下面这样的语句的时候,什么也不会发生。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<T1> p1 = ...;
|
||||
PCollection<T2> p2 = doSomeWork(p1);
|
||||
|
||||
|
||||
这样的语句执行完,p2这个PCollection仅仅会记录下自己是由doSomeWork这个操作计算而来的,和计算自己所需要的数据p1。当你执行写完100行的beam的运算操作,最终的结果仅仅是生成了一个有向无环图(DAG),也就是执行计划(execution plan)。
|
||||
|
||||
为什么这么设计呢?如果你记得我们在专栏第一部分讲到的大规模数据框架设计,可能会有印象。这样的有向无环图是框架能够自动优化执行计划的核心。
|
||||
|
||||
|
||||
|
||||
类似图中这样的数据处理流程,在Beam获知了整个数据处理流程后,就会被优化成下图所示。
|
||||
|
||||
|
||||
|
||||
这样的优化,在Beam中被称为sibling fusion。类似的操作优化我们后面会继续介绍。在这个小标题下,我想用这个优化的例子说明,PCollection下的数据不可变是因为改变本身毫无意义。
|
||||
|
||||
例如,在刚才这个例子中,你会发现,优化后的执行计划里已经没有了数据A0。因为,Beam发现数据A0作为中间结果并不影响最终输出。另外,由于Beam的分布式本质,即使你想要去修改一个PCollection的底层表达数据,也需要在多个机器上查找,毫无实现的价值。
|
||||
|
||||
小结
|
||||
|
||||
这一讲我们介绍了整个Beam核心数据结构PCollection的设计思想。尤其是分析了PCollection的几大特性为什么是这么设计的。它的特性包括Coders、无序性、没有固定大小、不可变性等。在每个特性下面我们也介绍了几个例子或者代码示例,希望能帮助你加深印象。
|
||||
|
||||
思考题
|
||||
|
||||
PCollection的设计是否能表达你的大规模数据处理场景呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
224
专栏/大规模数据处理实战/25Transform:Beam数据转换操作的抽象方法.md
Normal file
224
专栏/大规模数据处理实战/25Transform:Beam数据转换操作的抽象方法.md
Normal file
@@ -0,0 +1,224 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
25 Transform:Beam数据转换操作的抽象方法
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Beam数据转换操作的抽象方法”。
|
||||
|
||||
在上一讲中,我们一起学习了Beam中数据的抽象表达——PCollection。但是仅仅有数据的表达肯定是无法构建一个数据处理框架的。那么今天,我们就来看看Beam中数据处理的最基本单元——Transform。
|
||||
|
||||
下图就是单个Transform的图示。
|
||||
|
||||
|
||||
|
||||
之前我们已经讲过,Beam把数据转换抽象成了有向图。PCollection是有向图中的边,而Transform是有向图里的节点。
|
||||
|
||||
不少人在理解PCollection的时候都觉得这不那么符合他们的直觉。许多人都会自然地觉得PCollection才应该是节点,而Transform是边。因为数据给人的感觉是一个实体,应该用一个方框表达;而边是有方向的,更像是一种转换操作。事实上,这种想法很容易让人走入误区。
|
||||
|
||||
其实,区分节点和边的关键是看一个Transform是不是会有一个多余的输入和输出。
|
||||
|
||||
每个Transform都可能有大于一个的输入PCollection,它也可能输出大于一个的输出PCollection。所以,我们只能把Transform放在节点的位置。因为一个节点可以连接多条边,而同一条边却只能有头和尾两端。
|
||||
|
||||
Transform的基本使用方法
|
||||
|
||||
在了解了Transform和PCollection的关系之后,我们来看一下Transform的基本使用方法。
|
||||
|
||||
Beam中的PCollection有一个抽象的成员函数Apply。使用任何一个Transform时候,你都需要调用这个apply方法。
|
||||
|
||||
Java
|
||||
|
||||
pcollection1 = pcollection2.apply(Transform)
|
||||
|
||||
|
||||
Python
|
||||
|
||||
Pcollection1 = pcollection2 | Transform
|
||||
|
||||
|
||||
当然,你也可以把Transform级连起来。
|
||||
|
||||
final_collection = input_collection.apply(Transform1)
|
||||
.apply(Transform2)
|
||||
.apply(Transform3)
|
||||
|
||||
|
||||
|
||||
|
||||
所以说,Transform的调用方法是要通过apply()的,但是Transform有很多种。
|
||||
|
||||
常见的Transform
|
||||
|
||||
Beam也提供了常见的Transform接口,比如ParDo、GroupByKey。最常使用的Transform就是ParDo了。
|
||||
|
||||
ParDo就是 Parallel Do的意思,顾名思义,表达的是很通用的并行处理数据操作。GroupByKey的意思是把一个Key/Value的数据集按Key归并,就如下面这个例子。
|
||||
|
||||
cat, 1
|
||||
dog, 5
|
||||
and, 1
|
||||
jump, 3
|
||||
tree, 2
|
||||
cat, 5
|
||||
dog, 2
|
||||
and, 2
|
||||
cat, 9
|
||||
and, 6
|
||||
|
||||
=>
|
||||
|
||||
cat, [1,5,9]
|
||||
dog, [5,2]
|
||||
and, [1,2,6]
|
||||
jump, [3]
|
||||
tree, [2]
|
||||
|
||||
|
||||
当然,你也可以用ParDo来实现GroupByKey,一种简单的实现方法就是放一个全局的哈希表,然后在ParDo里把一个一个元素插进这个哈希表里。但这样的实现方法并不能用,因为你的数据量可能完全无法放进一个内存哈希表。而且,你还要考虑到PCollection会把计算分发到不同机器上的情况。
|
||||
|
||||
当你在编写ParDo时,你的输入是一个PCollection中的单个元素,输出可以是0个、1个,或者是多个元素。你只要考虑好怎样处理一个元素。剩下的事情,Beam会在框架层面帮你做优化和并行。
|
||||
|
||||
使用ParDo时,你需要继承它提供的DoFn类,你可以把DoFn看作是ParDo的一部分。因为ParDo和DoFn单独拿出来都没有意义。
|
||||
|
||||
java
|
||||
|
||||
static class UpperCaseFn extends DoFn<String, String> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element String word, OutputReceiver<String> out) {
|
||||
out.output(word.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
PCollection<String> upperCaseWords = words.apply(
|
||||
ParDo
|
||||
.of(new UpperCaseFn()));
|
||||
|
||||
|
||||
在上面的代码中你可以看出,每个DoFn的@ProcessElement标注的函数processElement,就是这个DoFn真正的功能模块。在上面这个DoFn中,我们把输入的一个词转化成了它的大写形式。之后在调用apply(ParDo.of(new UpperCaseFn()))的时候,Beam就会把输入的PCollection中的每个元素都使用刚才的processElement处理一遍。
|
||||
|
||||
看到这里,你可能会比较迷惑,transform、apply、DoFn、ParDo之间到底是什么关系啊?怎么突然冒出来一堆名词?其实,Transform是一种概念层面的说法。具体在编程上面,Transform用代码来表达的话就是这样的:
|
||||
|
||||
pcollection.apply(ParDo.of(new DoFn()))
|
||||
|
||||
|
||||
这里的apply(ParDo)就是一个Transform。
|
||||
|
||||
我们在第7讲中讲过数据处理流程的常见设计模式。事实上很多应用场景都可以用ParDo来实现。比如过滤一个数据集、格式转化一个数据集、提取一个数据集的特定值等等。
|
||||
|
||||
1.过滤一个数据集
|
||||
|
||||
当我们只想要挑出符合我们需求的元素的时候,我们需要做的,就是在processElement中实现。一般来说会有一个过滤函数,如果满足我们的过滤条件,我们就把这个输入元素输出。
|
||||
|
||||
Java
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(@Element T input, OutputReceiver<T> out) {
|
||||
if (IsNeeded(input)) {
|
||||
out.output(input);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2.格式转化一个数据集
|
||||
|
||||
给数据集转化格式的场景非常常见。比如,我们想把一个来自csv文件的数据,转化成TensorFlow的输入数据tf.Example的时候,就可以用到ParDo。
|
||||
|
||||
Java
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(@Element String csvLine, OutputReceiver<tf.Example> out) {
|
||||
out.output(ConvertToTfExample(csvLine));
|
||||
}
|
||||
|
||||
|
||||
3.提取一个数据集的特定值
|
||||
|
||||
ParDo还可以提取一个数据集中的特定值。比如,当我们想要从一个商品的数据集中提取它们的价格的时候,也可以使用ParDo。
|
||||
|
||||
Java
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(@Element Item item, OutputReceiver<Integer> out) {
|
||||
out.output(item.price());
|
||||
}
|
||||
|
||||
|
||||
通过前面的几个例子你可以看到,ParDo和DoFn这样的抽象已经能处理非常多的应用场景问题。事实正是如此,在实际应用中,80%的数据处理流水线都是使用基本的ParDo和DoFn。
|
||||
|
||||
Stateful Transform和side input/side output
|
||||
|
||||
当然,还有一些Transform其实也是很有用的,比如GroupByKey,不过它远没有ParDo那么常见。所以,这一模块中暂时不会介绍别的数据转换操作,需要的话我们可以在后面用到的时候再介绍。我想先在这里介绍和ParDo同样是必用的,却在大部分教程中被人忽略的技术点——Statefullness和side input/side output。
|
||||
|
||||
上面我们所介绍的一些简单场景都是无状态的,也就是说,在每一个DoFn的processElement函数中,输出只依赖于输入。它们的DoFn类不需要维持一个成员变量。无状态的DoFn能保证最大的并行运算能力。因为DoFn的processElement可以分发到不同的机器,或者不同的进程也能有多个DoFn的实例。但假如我们的processElement的运行需要另外的信息,我们就不得不转而编写有状态的DoFn了。
|
||||
|
||||
试想这样一个场景,你的数据处理流水线需要从一个数据库中根据用户的id找到用户的名字。你可能会想到用“在DoFn中增加一个数据库的成员变量”的方法来解决。的确,实际的应用情况中我们就会写成下面这个代码的样子。
|
||||
|
||||
java
|
||||
|
||||
static class FindUserNameFn extends DoFn<String, String> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element String userId, OutputReceiver<String> out) {
|
||||
out.output(database.FindUserName(userId));
|
||||
}
|
||||
|
||||
Database database;
|
||||
}
|
||||
|
||||
|
||||
但是因为有了共享的状态,这里是一个共享的数据库连接。在使用有状态的DoFn时,我们需要格外注意Beam的并行特性。
|
||||
|
||||
如上面讲到的,Beam不仅会把我们的处理函数分发到不同线程、进程,也会分发到不同的机器上执行。当你共享这样一个数据库的读取操作时,很可能引发服务器的QPS过高。
|
||||
|
||||
例如,你在处理一个1万个用户id,如果beam很有效地将你的DoFn并行化了,你就可能观察到数据库的QPS增加了几千。如果你不仅是读取,还做了修改的话,就需要注意是不是有竞争风险了。这里你可以联想在操作系统中有关线程安全的相关知识。
|
||||
|
||||
除了这种简单的增加一个成员变量的方法。如果我们需要共享的状态来自于另外一些Beam的数据处理的中间结果呢?这时候为了实现有状态DoFn我们需要应用Beam的Side input/side output计数。
|
||||
|
||||
java
|
||||
|
||||
PCollectionView<Integer> mediumSpending = ...;
|
||||
|
||||
PCollection<String> usersBelowMediumSpending =
|
||||
userIds.apply(ParDo
|
||||
.of(new DoFn<String, String>() {
|
||||
@ProcessElement
|
||||
public void processElement(@Element String userId, OutputReceiver<String> out, ProcessContext c) {
|
||||
int medium = c.sideInput(mediumSpending);
|
||||
if (findSpending(userId) <= medium) {
|
||||
out.output(userId);
|
||||
}
|
||||
}
|
||||
}).withSideInputs(mediumSpending)
|
||||
);
|
||||
|
||||
|
||||
比如,在这个处理流程中,我们需要根据之前处理得到的结果,也就是用户的中位数消费数据,找到消费低于这个中位数的用户。那么,我们可以通过side input把这个中位数传递进DoFn中。然后你可以在ProcessElement的参数ProcessContext中拿出来这个side input。
|
||||
|
||||
Transform的优化
|
||||
|
||||
之前我们也提到过,Beam中的数据操作都是lazy execution的。这使得Transform和普通的函数运算很不一样。当你写下面这样一个代码的时候,真正的计算完全没有被执行。
|
||||
|
||||
Pcollection1 = pcollection2.apply(Transform)
|
||||
|
||||
|
||||
这样的代码仅仅是让Beam知道了“你想对数据进行哪些操作”,需要让它来构建你的数据处理有向图。之后Beam的处理优化器会对你的处理操作进行优化。所以,千万不要觉得你写了10个Transform就会有10个Transform马上被执行了。
|
||||
|
||||
理解Transform的lazy execution非常重要。很多人会过度地优化自己的DoFn代码,想要在一个DoFn中把所有运算全都做了。其实完全没这个必要。
|
||||
|
||||
|
||||
|
||||
你可以用分步的DoFn把自己想要的操作表达出来,然后交给Beam的优化器去合并你的操作。比如,在FlumeJava论文中提到的MSCR Fusion,它会把几个相关的GroupByKey的Transform合并。
|
||||
|
||||
小结
|
||||
|
||||
在这一讲中,我们学习了Transform的概念和基本的使用方法。通过文章中的几个简单的例子,你要做到的是了解怎样编写Transform的编程模型DoFn类。有状态DoFn在实际应用中尤其常见,你可以多加关注。
|
||||
|
||||
思考题
|
||||
|
||||
你可能会发现Beam的ParDo类似于Spark的map()或者是MapReduce的map。它们确实有很多相似之处。那你认为它们有什么不一样之处呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
141
专栏/大规模数据处理实战/26Pipeline:Beam如何抽象多步骤的数据流水线?.md
Normal file
141
专栏/大规模数据处理实战/26Pipeline:Beam如何抽象多步骤的数据流水线?.md
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
26 Pipeline:Beam如何抽象多步骤的数据流水线?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Pipeline:Beam如何抽象多步骤的数据流水线”。
|
||||
|
||||
在上两讲中,我们一起学习了Beam是如何抽象封装数据,以及如何抽象对于数据集的转换操作的。在掌握了这两个基本概念后,我们就可以很好地回答Beam编程模型里的4个维度What、Where、When、How中的第一个问题——What了。也就是,我们要做什么计算?想得到什么样的结果?
|
||||
|
||||
|
||||
|
||||
这个时候你可能已经跃跃欲试,开始想用PCollection和Transform解决我们平常经常会使用到的批处理任务了。没有问题,那我们就先抛开Where、When和How这三个问题,由简至繁地讲起。
|
||||
|
||||
现在假设我们的数据处理逻辑只需要处理有边界数据集,在这个情况下,让我们一起来看看Beam是如何运行一套批处理任务的。
|
||||
|
||||
数据流水线
|
||||
|
||||
在Beam的世界里,所有的数据处理逻辑都会被抽象成数据流水线(Pipeline)来运行。那么什么是数据流水线呢?
|
||||
|
||||
Beam的数据流水线是对于数据处理逻辑的一个封装,它包括了从读取数据集,将数据集转换成想要的结果和输出结果数据集这样的一整套流程。
|
||||
|
||||
所以,如果我们想要跑自己的数据处理逻辑,就必须在程序中创建一个Beam数据流水线出来,比较常见的做法是在main()函数中直接创建。
|
||||
|
||||
Java
|
||||
|
||||
PipelineOptions options = PipelineOptionsFactory.create();
|
||||
Pipeline p = Pipeline.create(options);
|
||||
|
||||
|
||||
在创建Beam数据流水线的同时,我们必须给这个流水线定义一个选项(Options)。这个选项会告诉Beam,用户的Pipeline应该如何运行。例如,是在本地的内存上运行,还是在Apache Flink上运行?关于具体Beam选项的解释,我会在第30讲中展开讲解。
|
||||
|
||||
Beam数据流水线的应用
|
||||
|
||||
有了数据流水线这个抽象概念之后,我们就可以将PCollection和Transform应用在这个流水线里面了。
|
||||
|
||||
|
||||
|
||||
上图就是一个Beam的数据流水线,整个数据流水线包括了从读取数据,到经过了N个Transform之后输出数据的整个过程。
|
||||
|
||||
在第24讲中我们学习过PCollection的不可变性。也就是说,一个PCollection一经生成,我们就不能够再增加或者删除它里面的元素了。所以,在Beam的数据流水线中,每次PCollection经过一个Transform之后,流水线都会新创建一个PCollection出来。而这个新的PCollection又将成为下一个Transform的新输入。
|
||||
|
||||
|
||||
|
||||
在上图的示例中,Beam数据流水线在经过Transform1读取了输入数据集之后,会创建出一个新的PCollection1,而经过了Transform2之后,数据流水线又会创建出新的PCollection2出来,同时PCollection1不会有任何改变。也就是说,在上面的例子中,除去最终的输出结果,数据流水线一共创建了3个不同的PCollection出来。
|
||||
|
||||
这种特性可以让我们在编写数据处理逻辑的时候,对同一个PCollection应用多种不同的Transfrom。
|
||||
|
||||
例如下图所示,对于PCollection1,我们可以使三个不同的Transform应用在它之上,从而再产生出三个不同的PCollection2、PCollection3和PCollection4出来。
|
||||
|
||||
|
||||
|
||||
Beam数据流水线的处理模型
|
||||
|
||||
在了解完Beam数据流水线高度抽象的概念后,紧接着,我想和你介绍一下Beam数据流水线的处理模型,也就是数据流水线在运行起来之后,会发生些什么,它是如何处理我们定义好的PCollection和Transform的。
|
||||
|
||||
Beam数据流水线的底层思想其实还是动用了MapReduce的原理,在分布式环境下,整个数据流水线会启动N个Workers来同时处理PCollection。而在具体处理某一个特定Transform的时候,数据流水线会将这个Transform的输入数据集PCollection里面的元素分割成不同的Bundle,将这些Bundle分发给不同的Worker来处理。
|
||||
|
||||
Beam数据流水线具体会分配多少个Worker,以及将一个PCollection分割成多少个Bundle都是随机的。但Beam数据流水线会尽可能地让整个处理流程达到完美并行(Embarrassingly Parallel)。
|
||||
|
||||
我想举个几个例子让你更好地来理解这个概念。
|
||||
|
||||
假设在数据流水线的一个Transform里面,它的输入数据集PCollection是1、2、3、4、5、6这个6个元素。数据流水线可能会将这个PCollection按下图的方式将它分割成两个Bundles。
|
||||
|
||||
|
||||
|
||||
当然,PCollection也有可能会被分割成三个Bundles。
|
||||
|
||||
|
||||
|
||||
那数据流水线会启用多少个Worker来处理这些Bundle呢?这也是任意的。还是以刚刚的PCollection输入数据集作为例子,如果PCollection被分割成了两个Bundles,数据流水线有可能会分配两个Worker来处理这两个Bundles。
|
||||
|
||||
|
||||
|
||||
甚至有可能只分配一个Worker来处理这两个Bundles。
|
||||
|
||||
|
||||
|
||||
在多步骤的Transforms中,一个Bundle通过一个Transform产生出来的结果会作为下一个Transform的输入。
|
||||
|
||||
之前刚刚讲过,在Beam数据流水线中,抽象出来的PCollection经过一个Transform之后,流水线都会新创建一个PCollection出来。同样的,Beam在真正运行的时候,每一个Bundle在一个Worker机器里经过Transform逻辑后,也会产生出来一个新的Bundle,它们也是具有不可变性的。像这种具有关联性的Bundle,必须在同一个Worker上面处理。
|
||||
|
||||
我现在来举例说明一下上面的概念。现在假设输入数据集如下图所示,它被分成了两个Bundles。
|
||||
|
||||
|
||||
|
||||
我们现在需要做两个Transforms。第一个Transform会将元素的数值减一;第二个Transform会对元素的数值求平方。整个过程被分配到了两个Workers上完成。
|
||||
|
||||
|
||||
|
||||
过程就如上图所示,总共产生了6个不可变的Bundle出来,从Bundle1到Bundle3的整个过程都必须放在Worker1上完成,因为它们都具有关联性。同样的,从Bundle4到Bundle6的整个过程也都必须放在Worker2上完成。
|
||||
|
||||
Beam数据流水线的错误处理
|
||||
|
||||
在学习完Beam数据流水线底层的处理模型之后,你可能会有个疑问:既然Bundle都是放在分布式环境下处理的,要是其中一个步骤出错了,那数据流水线会做什么样的处理?接下来我会给你讲解一下Beam数据流水线的错误处理机制。
|
||||
|
||||
单个Transform上的错误处理
|
||||
|
||||
我们还是以单个Transform开始讲解。在一个Transform里面,如果某一个Bundle里面的元素因为任意原因导致处理失败了,则这整个Bundle里的元素都必须重新处理。
|
||||
|
||||
还是假设输入数据集如下图所示,被分成了两个Bundles。
|
||||
|
||||
|
||||
|
||||
Beam数据流水线分配了两个Worker来处理这两个Bundles。我们看到下图中,在Worker2处理Bundle2的时候,最后一个元素6处理失败了。
|
||||
|
||||
|
||||
|
||||
这个时候,即便Bundle2的元素5已经完成了处理,但是因为同一个Bundle里面的元素处理失败,所以整个Bundle2都必须拿来重新处理。
|
||||
|
||||
|
||||
|
||||
重新处理的Bundle也不一定要在原来的Worker里面被处理,有可能会被转移到另外的Worker里面处理。如上图所示,需要重新被处理的Bundle2就被转移到Worker1上面处理了。
|
||||
|
||||
多步骤Transform上的错误处理
|
||||
|
||||
学习完单个Transform上的错误处理机制,我们再来看看在多步骤的Transform上发生错误时是如何处理的。
|
||||
|
||||
在多步骤的Transform上,如果处理的一个Bundle元素发生错误了,则这个元素所在的整个Bundle以及与这个Bundle有关联的所有Bundle都必须重新处理。
|
||||
|
||||
我们还是用上面的多步骤Transform来讲解这个例子。
|
||||
|
||||
|
||||
|
||||
你可以看到,在Worker2中,处理Transform2逻辑的时候生成Bundle6里面的第一个元素失败了。因为Bundle4、Bundle5和Bundle6都是相关联的,所以这三个Bundle都会被重新处理。
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起学习了Beam里对于数据处理逻辑的高度抽象数据流水线,以及它的底层处理模型。数据流水线是构建数据处理的基础,掌握了它,我们就可以根据自身的应用需求,构建出一套数据流水线来处理数据。
|
||||
|
||||
思考题
|
||||
|
||||
你能根据自己的理解重述一下在Beam的数据流水线中,当处理的元素发生错误时流水线的错误处理机制吗?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
170
专栏/大规模数据处理实战/27PipelineI_O_Beam数据中转的设计模式.md
Normal file
170
专栏/大规模数据处理实战/27PipelineI_O_Beam数据中转的设计模式.md
Normal file
@@ -0,0 +1,170 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
27 Pipeline I_O_ Beam数据中转的设计模式
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Pipeline I/O: Beam数据中转的设计模式”。
|
||||
|
||||
在前面的章节中,我们一起学习了如何使用PCollection来抽象封装数据,如何使用Transform来封装我们的数据处理逻辑,以及Beam是如何将数据处理高度抽象成为Pipeline来表达的,就如下图所示。
|
||||
|
||||
|
||||
|
||||
讲到现在,你有没有发现我们还缺少了两样东西没有讲?没错,那就是最初的输入数据集和结果数据集。那么我们最初的输入数据集是如何得到的?在经过了多步骤的Transforms之后得到的结果数据集又是如何输出到目的地址的呢?
|
||||
|
||||
事实上在Beam里,我们可以用Beam的Pipeline I/O来实现这两个操作。今天我就来具体讲讲Beam的Pipeline I/O。
|
||||
|
||||
读取数据集
|
||||
|
||||
一个输入数据集的读取通常是通过Read Transform来完成的。Read Transform从外部源(External Source)中读取数据,这个外部源可以是本地机器上的文件,可以是数据库中的数据,也可以是云存储上面的文件对象,甚至可以是数据流上的消息数据。
|
||||
|
||||
Read Transform的返回值是一个PCollection,这个PCollection就可以作为输入数据集,应用在各种Transform上。Beam数据流水线对于用户什么时候去调用Read Transform是没有限制的,我们可以在数据流水线的最开始调用它,当然也可以在经过了N个步骤的Transforms后再调用它来读取另外的输入数据集。
|
||||
|
||||
以下的代码实例就是从filepath中读取文本。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> inputs = p.apply(TextIO.read().from(filepath));
|
||||
|
||||
|
||||
当然了,Beam还支持从多个文件路径中读取数据集的功能,它的文件名匹配规则和Linux系统底下的glob文件路径匹配模式是一样的,使用的是“*”和“?”这样的匹配符。
|
||||
|
||||
我来为你举个例子解释一下,假设我们正运行着一个商品交易平台,这个平台会将每天的交易数据保存在一个一个特定的文件路径下,文件的命名格式为YYYY-MM-DD.csv。每一个CSV文件都存储着这一天的交易数据。
|
||||
|
||||
现在我们想要读取某一个月份的数据来做数据处理,那我们就可以按照下面的代码实例来读取文件数据了。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> inputs = p.apply(TextIO.read().from("filepath/.../YYYY-MM-*.csv");
|
||||
|
||||
|
||||
这样做后,所有满足YYYY-MM-前缀和.csv后缀的文件都会被匹配上。
|
||||
|
||||
当然了,glob操作符的匹配规则最终还是要和你所要使用的底层文件系统挂钩的。所以,在使用的时候,最好要先查询好你所使用的文件系统的通配符规则。
|
||||
|
||||
我来举个Google Cloud Storage的例子吧。我们保存的数据还是上面讲到的商品交易平台数据,我们的数据是保存在Google Cloud Storage上面,并且文件路径是按照“filepath/…/YYYY/MM/DD/HH.csv”这样的格式来存放的。如果是这种情况,下面这样的代码写法就无法读取到一整个月的数据了。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> inputs = p.apply(TextIO.read().from("filepath/.../YYYY/MM/*.csv");
|
||||
|
||||
|
||||
因为在Google Cloud Storage的通配符规则里面,“_”只能匹配到“_”自己所在的那一层子目录而已。所以”filepath/…/YYYY/MM/*.csv”这个文件路径并不能找到“filepath/…/YYYY/MM/DD/…”这一层目录了。如果要达到我们的目标,我们就需要用到“**”的通配符,也就是如以下的写法。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> inputs = p.apply(TextIO.read().from("filepath/.../YYYY/MM/**.csv");
|
||||
|
||||
|
||||
如果你想要从不同的外部源中读取同一类型的数据来统一作为输入数据集,那我们可以多次调用Read Transform来读取不同源的数据,然后利用flatten操作将数据集合并,示例如下。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> input1 = p.apply(TextIO.read().from(filepath1);
|
||||
PCollection<String> input2 = p.apply(TextIO.read().from(filepath2);
|
||||
PCollection<String> input3 = p.apply(TextIO.read().from(filepath3);
|
||||
PCollectionList<String> collections = PCollectionList.of(input1).and(input2).and(input3);
|
||||
PCollection<String> inputs = collections.apply(Flatten.<String>pCollections());
|
||||
|
||||
|
||||
输出数据集
|
||||
|
||||
将结果数据集输出到目的地址的操作是通过Write Transform来完成的。Write Transform会将结果数据集输出到外部源中。
|
||||
|
||||
与Read Transform相对应,只要Read Transform能够支持的外部源,Write Transform都是支持的。在Beam数据流水线中,Write Transform可以在任意的一个步骤上将结果数据集输出。所以,用户能够将多步骤的Transforms中产生的任何中间结果输出。示例代码如下。
|
||||
|
||||
Java
|
||||
|
||||
output.apply(TextIO.write().to(filepath));
|
||||
|
||||
|
||||
需要注意的是,如果你的输出是写入到文件中的话,Beam默认是会写入到多个文件路径中的,而用户所指定的文件名会作为实际输出文件名的前缀。
|
||||
|
||||
Java
|
||||
|
||||
output.apply(TextIO.write().to(filepath/output));
|
||||
|
||||
|
||||
当输出结果超过一定大小的时候,Beam会将输出的结果分块,并写入到以“output00”“output01”等等为文件名的文件当中。如果你想将结果数据集保存成为特定的一种文件格式的话,可以使用“withSuffix”这个API来指定这个文件格式。
|
||||
|
||||
例如,如果你想将结果数据集保存成csv格式的话,代码就可以这样写:
|
||||
|
||||
Java
|
||||
|
||||
output.apply(TextIO.write().to(filepath/output).withSuffix(".csv"));
|
||||
|
||||
|
||||
在Beam里面,Read和Write的Transform都是在名为I/O连接器(I/O connector)的类里面实现的。而Beam原生所支持的I/O连接器也是涵盖了大部分应用场景,例如有基于文件读取输出的FileIO、TFRecordIO,基于流处理的KafkaIO、PubsubIO,基于数据库的JdbcIO、RedisIO等等。
|
||||
|
||||
当然了,Beam原生的I/O连接器并不可能支持所有的外部源。比如,如果我们想从Memcached中读取数据,那原生的I/O连接器就不支持了。说到这里你可能会有一个疑问,当我们想要从一些Beam不能原生支持的外部源中读取数据时,那该怎么办呢?答案很简单,可以自己实现一个自定义的I/O连接器出来。
|
||||
|
||||
自定义I/O连接器
|
||||
|
||||
自定义的I/O连接器并不是说一定要设计得非常通用,而是只要能够满足自身的应用需求就可以了。实现自定义的I/O连接器,通常指的就是实现Read Transform和Write Transform这两种操作,这两种操作都有各自的实现方法,下面我以Java为编程语言来一一为你解释。
|
||||
|
||||
自定义读取操作
|
||||
|
||||
我们知道Beam可以读取无界数据集也可以读取有界数据集,而读取这两种不同的数据集是有不同的实现方法的。
|
||||
|
||||
如果读取的是有界数据集,那我们可以有以下两种选项:
|
||||
|
||||
|
||||
使用在第25讲中介绍的两个Transform接口,ParDo和GroupByKey来模拟读取数据的逻辑。
|
||||
继承BoundedSource抽象类来实现一个子类去实现读取逻辑。
|
||||
|
||||
|
||||
如果读取的是无界数据集的话,那我们就必须继承UnboundedSource抽象类来实现一个子类去实现读取逻辑。
|
||||
|
||||
无论是BoundedSource抽象类还是UnboundedSource抽象类,其实它们都是继承了Source抽象类。为了能够在分布式环境下处理数据,这个Source抽象类也必须是可序列化的,也就是说Source抽象类必须实现Serializable这个接口。
|
||||
|
||||
如果我们是要读取有界数据集的话,Beam官方推荐的是使用第一种方式来实现自定义读取操作,也就是将读取操作看作是ParDo和GroupByKey这种多步骤Transforms。
|
||||
|
||||
好了,下面我来带你分别看看在不同的外部源中读取数据集是如何模拟成ParDo和GroupByKey操作的。
|
||||
|
||||
从多文件路径中读取数据集
|
||||
|
||||
从多文件路径中读取数据集相当于用户转入一个glob文件路径,我们从相应的存储系统中读取数据出来。比如说读取“filepath/**”中的所有文件数据,我们可以将这个读取转换成以下的Transforms:
|
||||
|
||||
|
||||
获取文件路径的ParDo:从用户传入的glob文件路径中生成一个PCollection 的中间结果,里面每个字符串都保存着具体的一个文件路径。
|
||||
读取数据集ParDo:有了具体PCollection 的文件路径数据集,从每个路径中读取文件内容,生成一个总的PCollection保存所有数据。
|
||||
|
||||
|
||||
从NoSQL数据库中读取数据集
|
||||
|
||||
NoSQL这种外部源通常允许按照键值范围(Key Range)来并行读取数据集。我们可以将这个读取转换成以下的Transforms:
|
||||
|
||||
|
||||
确定键值范围ParDo:从用户传入的要读取数据的键值生成一个PCollection保存可以有效并行读取的键值范围。
|
||||
读取数据集ParDo:从给定PCollection的键值范围,读取相应的数据,并生成一个总的PCollection保存所有数据。
|
||||
|
||||
|
||||
从关系型数据库读取数据集
|
||||
|
||||
从传统的关系型数据库查询结果通常都是通过一个SQL Query来读取数据的。所以,这个时候只需要一个ParDo,在ParDo里面建立与数据库的连接并执行Query,将返回的结果保存在一个PCollection里。
|
||||
|
||||
自定义输出操作
|
||||
|
||||
相比于读取操作,输出操作会简单很多,只需要在一个ParDo里面调用相应文件系统的写操作API来完成数据集的输出。
|
||||
|
||||
如果我们的输出数据集是需要写入到文件去的话,Beam也同时提供了基于文件操作的FileBasedSink抽象类给我们,来实现基于文件类型的输出操作。像很常见的TextSink类就是实现了FileBasedSink抽象类,并且运用在了TextIO中的。
|
||||
|
||||
如果我们要自己写一个自定义的类来实现FileBasedSink的话,也必须实现Serializable这个接口,从而保证输出操作可以在分布式环境下运行。
|
||||
|
||||
同时,自定义的类必须具有不可变性(Immutability)。怎么理解这个不可变性呢?其实它指的是在这个自定义类里面,如果有定义私有字段(Private Field)的话,那它必须被声明为final。如果类里面有变量需要被修改的话,那每次做的修改操作都必须先复制一份完全一样的数据出来,然后再在这个新的变量上做修改。这和我们在第27讲中学习到的Bundle机制一样,每次的操作都需要产生一份新的数据,而原来的数据是不可变的。
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起学习了在Beam中的一个重要概念Pipeline I/O,它使得我们可以在Beam数据流水线上读取和输出数据集。同时,我们还学习到了如何自定义一个I/O连接器,当Beam自身提供的原生I/O连接器不能满足我们需要的特定存储系统时,我们就可以自定义I/O逻辑来完成数据集的读取和输出。
|
||||
|
||||
思考题
|
||||
|
||||
你觉得Beam的Pipeline I/O设计能够满足我们所有的应用需求了吗?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
200
专栏/大规模数据处理实战/28如何设计创建好一个BeamPipeline?.md
Normal file
200
专栏/大规模数据处理实战/28如何设计创建好一个BeamPipeline?.md
Normal file
@@ -0,0 +1,200 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
28 如何设计创建好一个Beam Pipeline?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“如何设计创建好一个Beam Pipeline”。
|
||||
|
||||
这一讲我们会用到第7讲中介绍过的四种常见设计模式——复制模式、过滤模式、分离模式和合并模式。这些设计模式就像是武功的基本套路一样,在实战中无处不在。今天,我们就一起来看看我们怎么用Beam的Pipeline来实现这些设计模式。
|
||||
|
||||
设计Pipeline的基本考虑因素
|
||||
|
||||
在设计Pipeline时,你需要注意4条基本的考虑因素。
|
||||
|
||||
1.输入数据存储在哪里?
|
||||
|
||||
输入数据是存储在云存储文件系统,还是存储在一个关系型数据库里?有多大的数据量?这些都会影响你的pipeline设计是如何读入数据的。上一讲已经讲到过,Pipeline的数据读入是使用Read这个特殊的Transform。而数据读入往往是一个Pipeline的第一个数据操作。
|
||||
|
||||
2.输入数据是什么格式?
|
||||
|
||||
输入数据是纯文本文件?还是读取自关系型数据库的行?还是结构化好的特殊数据结构?这些都会影响你对于PCollection的选择。比如,如果输入数据是自带key/value的结构,那你用Beam的key/value为元素的PCollection能更好的表示数据。
|
||||
|
||||
3.这个pipeline你打算对数据进行哪些操作?
|
||||
|
||||
提前想好要做哪些数据操作,可以帮助你设计好Transform。可能你也会使用一些Beam提供的Transform或者是你的团队共用的一些Transform。
|
||||
|
||||
4.输出数据需要是什么样的格式,需要存储到哪里?
|
||||
|
||||
和输入数据相同,对于输出数据,我们也要提前判断好业务的需求。看看需要的数据格式是什么样的,是要存储到本地文本文件?还是存储到另一个数据库?
|
||||
|
||||
比如,你在跑一个本地批处理作业,就会需要先存到本地看一看。如果你在生成环境有永久性数据库,或者你有结构化的数据,可能更想存储到你的数据库里。
|
||||
|
||||
复制模式的Pipeline设计
|
||||
|
||||
现在,我们就来看看在第7讲中提到的复制模式(Copier Pattern)的例子是怎么用Beam实现的。这里需要用到第7讲的YouTube视频平台的复制模式案例。这里就简单介绍一下,以便唤醒你的记忆。如果你完全忘记了,我建议你先去做个回顾。
|
||||
|
||||
如今的视频平台会提供不同分辨率的视频给不同网络带宽的用户。在YouTube视频平台中,将鼠标放在视频缩略图上时,它会自动播放一段已经生成好的动画缩略图。平台的自然语言理解(NLP)的数据处理模块可以分析视频数据,自动生成视频字幕。视频分析的数据处理模块也可以通过分析视频数据产生更好的内容推荐系统。这使用的就是复制模式。
|
||||
|
||||
要想在在Beam中采用复制模式,我们可以用一个PCollection来表示输入的Video data set。将每一种视频处理编写成Transform。最后,多重输出各自为一个PCollection。整个过程就如同下图所示。
|
||||
|
||||
|
||||
|
||||
你可以从图片中看到,在这个工作流系统中,每个数据处理模块的输入都是相同的,而下面的5个数据处理模块都可以单独并且同步地运行处理。
|
||||
|
||||
复制模式通常是将单个数据处理模块中的数据完整地复制到两个或更多的数据处理模块中,然后再由不同的数据处理模块进行处理。当我们在处理大规模数据时,需要对同一个数据集采取多种不同的数据处理转换,我们就可以优先考虑采用复制模式。
|
||||
|
||||
比如下面的代码,我们用5个不同的pipeline来表示,它们的作用分别是生成高画质视频、生成低画质视频、生成GIF动画、生成视频字幕、分析视频。
|
||||
|
||||
PCollection<Video> videoDataCollection = ...;
|
||||
|
||||
// 生成高画质视频
|
||||
PCollection<Video> highResolutionVideoCollection = videoDataCollection.apply("highResolutionTransform", ParDo.of(new DoFn<Video, Video>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
c.output(generateHighResolution(c.element()));
|
||||
}
|
||||
}));
|
||||
|
||||
// 生成低画质视频
|
||||
PCollection<Video> lowResolutionVideoCollection = videoDataCollection.apply("lowResolutionTransform", ParDo.of(new DoFn<Video, Video>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
c.output(generateLowResolution(c.element()));
|
||||
}
|
||||
}));
|
||||
|
||||
// 生成GIF动画
|
||||
PCollection<Image> gifCollection = videoDataCollection.apply("gifTransform", ParDo.of(new DoFn<Video, Image>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
c.output(generateGIF(c.element()));
|
||||
}
|
||||
}));
|
||||
|
||||
// 生成视频字幕
|
||||
PCollection<Caption> captionCollection = videoDataCollection.apply("captionTransform", ParDo.of(new DoFn<Video, Caption>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
c.output(generateCaption(c.element()));
|
||||
}
|
||||
}));
|
||||
|
||||
// 分析视频
|
||||
PCollection<Report> videoAnalysisCollection = videoDataCollection.apply("videoAnalysisTransform", ParDo.of(new DoFn<Video, Report>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
c.output(analyzeVideo(c.element()));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
过滤模式的Pipeline设计
|
||||
|
||||
过滤模式(Filter Pattern)也可以用Beam来实现。这里我们先简单回顾一下第7讲的例子。在商城会员系统中,系统根据用户的消费次数、消费金额、注册时间划分用户等级。假设现在商城有五星、金牌和钻石这三种会员。而系统现在打算通过邮件对钻石会员发出钻石会员活动的邀请。
|
||||
|
||||
|
||||
|
||||
在过滤模式中,一个数据处理模块会将输入的数据集过滤,留下符合条件的数据,然后传输到下一个数据处理模块进行单独处理。
|
||||
|
||||
在用Beam实现时,我们把输入的用户群组表达成一个PCollection。输出的钻石会员用户群组也表示成一个PCollection。那么中间的过滤步骤就能编写成一个Transform。如下面代码所示,我们在一个Beam Pipeline里调用isDiamondUser()方法,从所有的用户中过滤出钻石会员。
|
||||
|
||||
PCollection<User> userCollection = ...;
|
||||
|
||||
PCollection<User> diamondUserCollection = userCollection.apply("filterDiamondUserTransform", ParDo.of(new DoFn<User, User>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
if (isDiamondUser(c.element()) {
|
||||
c.output(c.element());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
PCollection<User> notifiedUserCollection = userCollection.apply("notifyUserTransform", ParDo.of(new DoFn<User, User>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
if (notifyUser(c.element()) {
|
||||
c.output(c.element());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
分离模式的Pipeline设计
|
||||
|
||||
分离模式(Splitter Pattern)与过滤模式不同,并不会丢弃里面的任何数据,而是将数据分组处理。还是以商城会员系统为例。系统打算通过邮件对不同会员发出与他们身份相应的活动邀请。需要通过分离模式将用户按照会员等级分组,然后发送相应的活动内容。
|
||||
|
||||
|
||||
|
||||
用Beam应该怎么实现呢?我们可以应用第25讲中讲到的side input/output技术。同样的还是把用户群组都定义成不同的PCollection。最终的输出会是三个PCollection。
|
||||
|
||||
// 首先定义每一个output的tag
|
||||
final TupleTag<User> fiveStarMembershipTag = new TupleTag<User>(){};
|
||||
final TupleTag<User> goldenMembershipTag = new TupleTag<User>(){};
|
||||
final TupleTag<User> diamondMembershipTag = new TupleTag<User>(){};
|
||||
|
||||
PCollection<User> userCollection = ...;
|
||||
|
||||
PCollectionTuple mixedCollection =
|
||||
userCollection.apply(ParDo
|
||||
.of(new DoFn<User, User>() {
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
if (isFiveStartMember(c.element())) {
|
||||
c.output(c.element());
|
||||
} else if (isGoldenMember(c.element())) {
|
||||
c.output(goldenMembershipTag, c.element());
|
||||
} else if (isDiamondMember(c.element())) {
|
||||
c.output(diamondMembershipTag, c.element());
|
||||
}
|
||||
}
|
||||
})
|
||||
.withOutputTags(fiveStarMembershipTag,
|
||||
TupleTagList.of(goldenMembershipTag).and(diamondMembershipTag)));
|
||||
|
||||
// 分离出不同的用户群组
|
||||
mixedCollection.get(fiveStarMembershipTag).apply(...);
|
||||
|
||||
mixedCollection.get(goldenMembershipTag).apply(...);
|
||||
|
||||
mixedCollection.get(diamondMembershipTag).apply(...);
|
||||
|
||||
|
||||
比如在上面的代码中,我们在processElement()方法中,根据过滤函数,分拆出五星会员,金牌会员和钻石会员。并且把不同的会员等级输出到不同的side output tag中。之后可以在返回的PCollection中用这个side output tag得到想要的输出。
|
||||
|
||||
合并模式的Pipeline设计
|
||||
|
||||
合并模式(Joiner Pattern)会将多个不同的数据集合成一个总数据集,一并进行处理。之前介绍的合并模式案例是用街头美团外卖电动车的数量来预测美团的股价。
|
||||
|
||||
数据接入这一处理模块里,我们的输入数据有自己团队在街道上拍摄到的美团外卖电动车图片和第三方公司提供的美团外卖电动车图片。我们需要先整合所有数据然后进行其它数据处理。
|
||||
|
||||
|
||||
|
||||
使用Beam合并多个PCollection时,需要用到Beam自带的Flatten这个Transform函数,它的作用是把来自多个PCollection类型一致的元素融合到一个PCollection中去。下面的代码用元素类型为Image的PCollection来表达输入数据和输出数据。
|
||||
|
||||
PCollectionList<Image> collectionList = PCollectionList.of(internalImages).and(thirdPartyImages);
|
||||
PCollection<Image> mergedCollectionWithFlatten = collectionList
|
||||
.apply(Flatten.<Image>pCollections());
|
||||
|
||||
mergedCollectionWithFlatten.apply(...);
|
||||
|
||||
|
||||
例如,在上面的代码示例中,我们把internalImages和thirdPartyImages两个PCollection融合到一起。使用apply(Flatten)这样一个Transform实现多个PCollection的平展。
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起学习了怎样在Beam中设计实现第7讲介绍的经典数据处理模式,分别是4种设计模式,分别是复制模式、过滤模式、分离模式和合并模式。
|
||||
|
||||
在实现这四种数据处理模式的过程中,我们学到了两种Beam Transform的两个重要技术,分别是分离模式中用到的side output,和在合并模式中用到的Flatten。正如前文所说,第7讲的经典数据处理模式就像是武功的基本套路,实战项目中可能80%都是这些基本套路的组合。有了这些小型的模块实现,对我们未来实现大型系统是有很大帮助的。
|
||||
|
||||
思考题
|
||||
|
||||
在你的项目中有没有这四种设计模式的影子呢?如果有的话你觉得可以怎样用Beam Pipeline实现呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
242
专栏/大规模数据处理实战/29如何测试BeamPipeline?.md
Normal file
242
专栏/大规模数据处理实战/29如何测试BeamPipeline?.md
Normal file
@@ -0,0 +1,242 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
29 如何测试Beam Pipeline?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“如何测试Beam Pipeline”。
|
||||
|
||||
在上一讲中,我们结合了第7讲的内容,一起学习了在Beam的世界中我们该怎么设计好对应的设计模式。而在今天这一讲中,我想要讲讲在日常开发中经常会被忽略的,但是又非常重要的一个开发环节——测试。
|
||||
|
||||
你知道,我们设计好的Beam数据流水线通常都会被放在分布式环境下执行,具体每一步的Transform都会被分配到任意的机器上面执行。如果我们在运行数据流水线时发现结果出错了,那么想要定位到具体的机器,再到上面去做调试是不现实的。
|
||||
|
||||
当然还有另一种方法,读取一些样本数据集,再运行整个数据流水线去验证哪一步逻辑出错了。但这是一项非常耗时耗力的工作。即便我们可以把样本数据集定义得非常小,从而缩短运行数据流水线运行所需的时间。但是万一我们所写的是多步骤数据流水线的话,就不知道到底在哪一步出错了,我们必须把每一步的中间结果输出出来进行调试。
|
||||
|
||||
基于以上种种的原因,在我们正式将数据流水线放在分布式环境上面运行之前,先完整地测试好整个数据流水线逻辑,就变得尤为重要了。
|
||||
|
||||
为了解决这些问题,Beam提供了一套完整的测试SDK。让我们可以在开发数据流水线的同时,能够实现对一个Transform逻辑的单元测试,也可以对整个数据流水线端到端(End-to-End)地测试。
|
||||
|
||||
在Beam所支持的各种Runners当中,有一个Runner叫作DirectRunner。DirectRunner其实就是我们的本地机器。也就是说,如果我们指定Beam的Runner为DirectRunner的话,整个Beam数据流水线都会放在本地机器上面运行。我们在运行测试程序的时候可以利用这个DirectRunner来跑测试逻辑。
|
||||
|
||||
在正式讲解之前,有一点是我需要提醒你的。如果你喜欢自行阅读Beam的相关技术文章或者是示例代码的话,可能你会看见一些测试代码使用了在Beam SDK中的一个测试类,叫作DoFnTester来进行单元测试。这个DoFnTester类可以让我们传入一个用户自定义的函数(User Defined Function/UDF)来进行测试。
|
||||
|
||||
通过第25讲的内容我们已经知道,一个最简单的Transform可以用一个ParDo来表示,在使用它的时候,我们需要继承DoFn这个抽象类。这个DoFnTester接收的对象就是我们继承实现的DoFn。在这里,我们把一个DoFn看作是一个单元来进行测试了。但这并不是Beam所提倡的。
|
||||
|
||||
因为在Beam中,数据转换的逻辑都是被抽象成Transform,而不是Transform里面的ParDo这些具体的实现。每个Runner具体怎么运行这些ParDo,对于用户来说应该都是透明的。所以,在Beam的2.4.0版本之后,Beam SDK将这个类标记成了Deprecated,转而推荐使用Beam SDK中的TestPipeline。
|
||||
|
||||
所以,我在这里也建议你,在写测试代码的时候,不要使用任何和DoFnTester有关的SDK。
|
||||
|
||||
Beam的Transform单元测试
|
||||
|
||||
说完了注意事项,那事不宜迟,我们就先从一个Transform的单元测试开始,看看在Beam是如何做测试的(以下所有的测试示例代码都是以Java为编程语言来讲解)。
|
||||
|
||||
一般来说,Transform的单元测试可以通过以下五步来完成:
|
||||
|
||||
|
||||
创建一个Beam测试SDK中所提供的TestPipeline实例。
|
||||
创建一个静态(Static)的、用于测试的输入数据集。
|
||||
使用Create Transform来创建一个PCollection作为输入数据集。
|
||||
在测试数据集上调用我们需要测试的Transform上并将结果保存在一个PCollection上。
|
||||
使用PAssert类的相关函数来验证输出的PCollection是否是我所期望的结果。
|
||||
|
||||
|
||||
假设我们要处理的数据集是一个整数集合,处理逻辑是过滤掉数据集中的奇数,将输入数据集中的偶数输出。为此,我们通过继承DoFn类来实现一个产生偶数的Transform,它的输入和输出数据类型都是Integer。
|
||||
|
||||
Java
|
||||
|
||||
static class EvenNumberFn extends DoFn<Integer, Integer> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element Integer in, OutputReceiver<Integer> out) {
|
||||
if (in % 2 == 0) {
|
||||
out.output(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
那我们接下来就根据上面所讲的测试流程,测试这个EvenNumerFn Transform,来一步步创建我们的单元测试。
|
||||
|
||||
创建TestPipeline
|
||||
|
||||
第一步,创建TestPipeline。创建一个TestPipeline实例的代码非常简单,示例如下:
|
||||
|
||||
Java
|
||||
|
||||
...
|
||||
Pipeline p = TestPipeline.create();
|
||||
...
|
||||
|
||||
|
||||
如果你还记得在第26讲中如何创建数据流水线的话,可以发现,TestPipeline实例的创建其实不用给这个TestPipeline定义选项(Options)。因为TestPipeline中create函数已经在内部帮我们创建好一个测试用的Options了。
|
||||
|
||||
创建静态输入数据集
|
||||
|
||||
Java
|
||||
|
||||
...
|
||||
static final List<Integer> INPUTS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
|
||||
...
|
||||
|
||||
|
||||
第二步,创建静态的输入数据集。创建静态的输入数据集的操作就和我们平时所写的普通Java代码一样,在示例中,我调用了Arrays类的asList接口来创建一个拥有10个整数的数据集。
|
||||
|
||||
使用Create Transform创建PCollection
|
||||
|
||||
在创建完静态数据集后,我们进入第三步,创建一个PCollection作为输入数据集。在Beam原生支持的Transform里面,有一种叫作Create Transform,我们可以利用这个Create Transform将Java Collection的数据转换成为Beam的数据抽象PCollection,具体的做法如下:
|
||||
|
||||
Java
|
||||
|
||||
...
|
||||
PCollection<Integer> input = p.apply(Create.of(INPUTS)).setCoder(VarIntCoder.of());
|
||||
...
|
||||
|
||||
|
||||
调用Transform处理逻辑
|
||||
|
||||
第四步,调用Transform处理逻辑。有了数据抽象PCollection,我们就需要在测试数据集上调用我们需要测试的Transform处理逻辑,并将结果保存在一个PCollection上。
|
||||
|
||||
Java
|
||||
|
||||
...
|
||||
PCollection<String> output = input.apply(ParDo.of(new EvenNumberFn()));
|
||||
...
|
||||
|
||||
|
||||
根据第25讲的内容,我们只需要在这个输入数据集上调用apply抽象函数,生成一个需要测试的Transform,并且传入apply函数中就可以了。
|
||||
|
||||
验证输出结果
|
||||
|
||||
第五步,验证输出结果。在验证结果的阶段,我们需要调用PAssert类中的函数来验证输出结果是否和我们期望的一致,示例如下。
|
||||
|
||||
Java
|
||||
|
||||
...
|
||||
PAssert.that(output).containsInAnyOrder(2, 4, 6, 8, 10);
|
||||
...
|
||||
|
||||
|
||||
完成了所有的步骤,我们就差运行这个测试的数据流水线了。很简单,就是调用TestPipeline的run函数,整个Transform的单元测试示例如下:
|
||||
|
||||
Java
|
||||
|
||||
final class TestClass {
|
||||
static final List<Integer> INPUTS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
|
||||
|
||||
public void testFn() {
|
||||
Pipeline p = TestPipeline.create();
|
||||
PCollection<Integer> input = p.apply(Create.of(INPUTS)).setCoder(VarIntCoder.of());
|
||||
PCollection<String> output = input.apply(ParDo.of(new EvenNumberFn()));
|
||||
PAssert.that(output).containsInAnyOrder(2, 4, 6, 8, 10);
|
||||
p.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
有一点需要注意的是,TestPipeline的run函数是在单元测试的结尾处调用的,PAssert的调用必须在TestPipeliner调用run函数之前调用。
|
||||
|
||||
Beam的端到端测试
|
||||
|
||||
在一般的现实应用中,我们设计的都是多步骤数据流水线,就拿我在第一讲中举到的处理美团外卖电动车的图片为例子,其中就涉及到了多个输入数据集,而结果也有可能会根据实际情况有多个输出。
|
||||
|
||||
所以,我们在做测试的时候,往往希望能有一个端到端的测试。在Beam中,端到端的测试和Transform的单元测试非常相似。唯一的不同点在于,我们要为所有的输入数据集创建测试数据集,而不是只针对某一个Transform来创建。对于在数据流水线的每一个应用到Write Transfrom的地方,我们都需要用到PAssert类来验证输出数据集。
|
||||
|
||||
所以,端到端测试的步骤也分五步,具体内容如下:
|
||||
|
||||
|
||||
创建一个Beam测试SDK中所提供的TestPipeline实例。
|
||||
对于多步骤数据流水线中的每个输入数据源,创建相对应的静态(Static)测试数据集。
|
||||
使用Create Transform,将所有的这些静态测试数据集转换成PCollection作为输入数据集。
|
||||
按照真实数据流水线逻辑,调用所有的Transforms操作。
|
||||
在数据流水线中所有应用到Write Transform的地方,都使用PAssert来替换这个Write Transform,并且验证输出的结果是否我们期望的结果相匹配。
|
||||
|
||||
|
||||
为了方便说明,我们就在之前的例子中多加一步Transform和一个输出操作来解释如何写端到端测试。假设,我们要处理数据集是一个整数集合,处理逻辑是过滤掉奇数,将输入数据集中的偶数转换成字符串输出。同时,我们也希望对这些偶数求和并将结果输出,示例如下:
|
||||
|
||||
Java
|
||||
|
||||
final class Foo {
|
||||
static class EvenNumberFn extends DoFn<Integer, Integer> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element Integer in, OutputReceiver<Integer> out) {
|
||||
if (in % 2 == 0) {
|
||||
out.output(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ParseIntFn extends DoFn<String, Integer> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element String in, OutputReceiver<Integer> out) {
|
||||
out.output(Integer.parseInt(in));
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
PipelineOptions options = PipelineOptionsFactory.create();
|
||||
Pipeline p = Pipeline.create(options);
|
||||
PCollection<Integer> input = p.apply(TextIO.read().from("filepath/input")).apply(ParDo.of(new ParseIntFn()));
|
||||
PCollection<Integer> output1 = input.apply(ParDo.of(new EvenNumberFn()));
|
||||
output1.apply(ToString.elements()).apply(TextIO.write().to("filepath/evenNumbers"));
|
||||
PCollection<Integer> sum = output1.apply(Combine.globally(new SumInts()));
|
||||
sum.apply(ToString.elements()).apply(TextIO.write().to("filepath/sum"));
|
||||
p.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
从上面的示例代码中你可以看到,我们从一个外部源读取了一系列输入数据进来,将它转换成了整数集合。同时,将我们自己编写的EvenNumberFn Transform应用在了这个输入数据集上。得到了所有偶数集合之后,我们先将这个中间结果输出,然后再针对这个偶数集合求和,最后将这个结果输出。
|
||||
|
||||
整个数据流水线总共有一次对外部数据源的读取和两次的输出,我们按照端到端测试的步骤,为所有的输入数据集创建静态数据,然后将所有有输出的地方都使用PAssert类来进行验证。整个测试程序如下所示:
|
||||
|
||||
Java
|
||||
|
||||
final class TestClass {
|
||||
|
||||
static final List<String> INPUTS =
|
||||
Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
|
||||
|
||||
static class EvenNumberFn extends DoFn<Integer, Integer> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element Integer in, OutputReceiver<Integer> out) {
|
||||
if (in % 2 == 0) {
|
||||
out.output(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ParseIntFn extends DoFn<String, Integer> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element String in, OutputReceiver<Integer> out) {
|
||||
out.output(Integer.parseInt(in));
|
||||
}
|
||||
}
|
||||
|
||||
public void testFn() {
|
||||
Pipeline p = TestPipeline.create();
|
||||
PCollection<String> input = p.apply(Create.of(INPUTS)).setCoder(StringUtf8Coder.of());
|
||||
PCollection<Integer> output1 = input.apply(ParDo.of(new ParseIntFn())).apply(ParDo.of(new EvenNumberFn()));
|
||||
PAssert.that(output1).containsInAnyOrder(2, 4, 6, 8, 10);
|
||||
PCollection<Integer> sum = output1.apply(Combine.globally(new SumInts()));
|
||||
PAssert.that(sum).is(30);
|
||||
p.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
在上面的示例代码中,我们用TestPipeline替换了原来的Pipeline,创建了一个静态输入数据集并用Create Transform转换成了PCollection,最后将所有用到Write Transform的地方都用PAssert替换掉,来验证输出结果是否是我们期望的结果。
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起学习了在Beam中写编写测试逻辑的两种方式,分别是针对一个Transform的单元测试和针对整个数据流水线的端到端测试。Beam提供的SDK能够让我们不需要在分布式环境下运行程序而是本地机器上运行。测试在整个开发环节中是非常的一环,我强烈建议你在正式上线自己的业务逻辑之前先对此有一个完整的测试。
|
||||
|
||||
思考题
|
||||
|
||||
如果让你来利用Beam SDK来测试你日常处理的数据逻辑,你会如何编写测试呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
260
专栏/大规模数据处理实战/30ApacheBeam实战冲刺:Beam如何runeverywhere_.md
Normal file
260
专栏/大规模数据处理实战/30ApacheBeam实战冲刺:Beam如何runeverywhere_.md
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
30 Apache Beam实战冲刺:Beam如何run everywhere_
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Apache Beam实战冲刺:Beam如何run everywhere”。
|
||||
|
||||
你可能已经注意到,自第26讲到第29讲,从Pipeline的输入输出,到Pipeline的设计,再到Pipeline的测试,Beam Pipeline的概念一直贯穿着文章脉络。那么这一讲,我们一起来看看一个完整的Beam Pipeline究竟是如何编写的。
|
||||
|
||||
Beam Pipeline
|
||||
|
||||
一个Pipeline,或者说是一个数据处理任务,基本上都会包含以下三个步骤:
|
||||
|
||||
|
||||
读取输入数据到PCollection。
|
||||
对读进来的PCollection做某些操作(也就是Transform),得到另一个PCollection。
|
||||
输出你的结果PCollection。
|
||||
|
||||
|
||||
这么说,看起来很简单,但你可能会有些迷惑:这些步骤具体该怎么做呢?其实这些步骤具体到Pipeline的实际编程中,就会包含以下这些代码模块:
|
||||
|
||||
Java
|
||||
|
||||
// Start by defining the options for the pipeline.
|
||||
PipelineOptions options = PipelineOptionsFactory.create();
|
||||
|
||||
// Then create the pipeline.
|
||||
Pipeline pipeline = Pipeline.create(options);
|
||||
|
||||
PCollection<String> lines = pipeline.apply(
|
||||
"ReadLines", TextIO.read().from("gs://some/inputData.txt"));
|
||||
|
||||
PCollection<String> filteredLines = lines.apply(new FilterLines());
|
||||
|
||||
filteredLines.apply("WriteMyFile", TextIO.write().to("gs://some/outputData.txt"));
|
||||
|
||||
pipeline.run().waitUntilFinish();
|
||||
|
||||
|
||||
从上面的代码例子中你可以看到,第一行和第二行代码是创建Pipeline实例。任何一个Beam程序都需要先创建一个Pipeline的实例。Pipeline实例就是用来表达Pipeline类型的对象。这里你需要注意,一个二进制程序可以动态包含多个Pipeline实例。
|
||||
|
||||
还是以之前的美团外卖电动车处理的例子来做说明吧。
|
||||
|
||||
比如,我们的程序可以动态判断是否存在第三方的电动车图片,只有当有需要处理图片时,我们才去创建一个Pipeline实例处理。我们也可以动态判断是否存在需要转换图片格式,有需要时,我们再去创建第二个Pipeline实例。这时候你的二进制程序,可能包含0个、1个,或者是2个Pipeline实例。每一个实例都是独立的,它封装了你要进行操作的数据,和你要进行的操作Transform。
|
||||
|
||||
Pipeline实例的创建是使用Pipeline.create(options)这个方法。其中options是传递进去的参数,options是一个PipelineOptions这个类的实例。我们会在后半部分展开PipelineOptions的丰富变化。
|
||||
|
||||
第三行代码,我们用TextIO.read()这个Transform读取了来自外部文本文件的内容,把所有的行表示为一个PCollection。
|
||||
|
||||
第四行代码,用 lines.apply(new FilterLines()) 对读进来的PCollection进行了过滤操作。
|
||||
|
||||
第五行代码 filteredLines.apply(“WriteMyFile”, TextIO.write().to(“gs://some/outputData.txt”)),表示把最终的PCollection结果输出到另一个文本文件。
|
||||
|
||||
程序运行到第五行的时候,是不是我们的数据处理任务就完成了呢?并不是。
|
||||
|
||||
记得我们在第24讲、第25讲中提过,Beam是延迟运行的。程序跑到第五行的时候,只是构建了Beam所需要的数据处理DAG用来优化和分配计算资源,真正的运算完全没有发生。
|
||||
|
||||
所以,我们需要最后一行pipeline.run().waitUntilFinish(),这才是数据真正开始被处理的语句。
|
||||
|
||||
这时候运行我们的代码,是不是就大功告成呢?别急,我们还没有处理好程序在哪里运行的问题。你一定会好奇,我们的程序究竟在哪里运行,不是说好了分布式数据处理吗?
|
||||
|
||||
在上一讲《如何测试Beam Pipeline》中我们学会了在单元测试环境中运行Beam Pipeline。就如同下面的代码。和上文的代码类似,我们把Pipeline.create(options)替换成了TestPipeline.create()。
|
||||
|
||||
Java
|
||||
|
||||
Pipeline p = TestPipeline.create();
|
||||
|
||||
PCollection<String> input = p.apply(Create.of(WORDS)).setCoder(StringUtf8Coder.of());
|
||||
|
||||
PCollection<String> output = input.apply(new CountWords());
|
||||
|
||||
PAssert.that(output).containsInAnyOrder(COUNTS_ARRAY);
|
||||
|
||||
p.run();
|
||||
|
||||
|
||||
TestPipeline是Beam Pipeline中特殊的一种,让你能够在单机上运行小规模的数据集。之前我们在分析Beam的设计理念时提到过,Beam想要把应用层的数据处理业务逻辑和底层的运算引擎分离开来。
|
||||
|
||||
现如今Beam可以做到让你的Pipeline代码无需修改,就可以在本地、Spark、Flink,或者在Google Cloud DataFlow上运行。这些都是通过Pipeline.create(options) 这行代码中传递的PipelineOptions实现的。
|
||||
|
||||
在实战中,我们应用到的所有option其实都是实现了PipelineOptions这个接口。
|
||||
|
||||
举个例子,如果我们希望将数据流水线放在Spark这个底层数据引擎运行的时候,我们便可以使用SparkPipelineOptions。如果我们想把数据流水线放在Flink上运行,就可以使用FlinkPipelineOptions。而这些都是extends了PipelineOptions的接口,示例如下:
|
||||
|
||||
Java
|
||||
|
||||
options = PipelineOptionsFactory.as(SparkPipelineOptions.class);
|
||||
Pipeline pipeline = Pipeline.create(options);
|
||||
|
||||
|
||||
通常一个PipelineOption是用PipelineOptionsFactory这个工厂类来创建的,它提供了两个静态工厂方法给我们去创建,分别是PipelineOptionsFactory.as(Class)和PipelineOptionsFactory.create()。像上面的示例代码就是用PipelineOptionsFactory.as(Class)这个静态工厂方法来创建的。
|
||||
|
||||
当然了,更加常见的创建方法是从命令行中读取参数来创建PipelineOption,使用的是PipelineOptionsFactory#fromArgs(String[])这个方法,例如:
|
||||
|
||||
Java
|
||||
|
||||
public static void main(String[] args) {
|
||||
PipelineOptions options = PipelineOptionsFactory.fromArgs(args).create();
|
||||
Pipeline p = Pipeline.create(options);
|
||||
}
|
||||
|
||||
|
||||
下面我们来看看不同的运行模式的具体使用方法。
|
||||
|
||||
直接运行模式
|
||||
|
||||
我们先从直接运行模式开始讲。这是我们在本地进行测试,或者调试时倾向使用的模式。在直接运行模式的时候,Beam会在单机上用多线程来模拟分布式的并行处理。
|
||||
|
||||
使用Java Beam SDK时,我们要给程序添加Direct Runner的依赖关系。在下面这个maven依赖关系定义文件中,我们指定了beam-runners-direct-java这样一个依赖关系。
|
||||
|
||||
pom.xml
|
||||
<dependency>
|
||||
<groupId>org.apache.beam</groupId>
|
||||
<artifactId>beam-runners-direct-java</artifactId>
|
||||
<version>2.13.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
一般我们会把runner通过命令行指令传递进程序。就需要使用PipelineOptionsFactory.fromArgs(args)来创建PipelineOptions。PipelineOptionsFactory.fromArgs()是一个工厂方法,能够根据命令行参数选择生成不同的PipelineOptions子类。
|
||||
|
||||
PipelineOptions options =
|
||||
PipelineOptionsFactory.fromArgs(args).create();
|
||||
|
||||
|
||||
在实验程序中也可以强行使用Direct Runner。比如:
|
||||
|
||||
PipelineOptions options = PipelineOptionsFactory.create();
|
||||
options.setRunner(DirectRunner.class);
|
||||
// 或者这样
|
||||
options = PipelineOptionsFactory.as(DirectRunner.class);
|
||||
Pipeline pipeline = Pipeline.create(options);
|
||||
|
||||
|
||||
如果是在命令行中指定Runner的话,那么在调用这个程序时候,需要指定这样一个参数–runner=DirectRunner。比如:
|
||||
|
||||
mvn compile exec:java -Dexec.mainClass=YourMainClass \
|
||||
-Dexec.args="--runner=DirectRunner" -Pdirect-runner
|
||||
|
||||
|
||||
Spark运行模式
|
||||
|
||||
如果我们希望将数据流水线放在Spark这个底层数据引擎运行的时候,我们便可以使用Spark Runner。Spark Runner执行Beam程序时,能够像原生的Spark程序一样。比如,在Spark本地模式部署应用,跑在Spark的RM上,或者用YARN。
|
||||
|
||||
Spark Runner为在Apache Spark上运行Beam Pipeline提供了以下功能:
|
||||
|
||||
|
||||
Batch 和streaming的数据流水线;
|
||||
和原生RDD和DStream一样的容错保证;
|
||||
和原生Spark同样的安全性能;
|
||||
可以用Spark的数据回报系统;
|
||||
使用Spark Broadcast实现的Beam side-input。
|
||||
|
||||
|
||||
目前使用Spark Runner必须使用Spark 2.2版本以上。
|
||||
|
||||
这里,我们先添加beam-runners-spark的依赖关系。
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.beam</groupId>
|
||||
<artifactId>beam-runners-spark</artifactId>
|
||||
<version>2.13.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.spark</groupId>
|
||||
<artifactId>spark-core_2.10</artifactId>
|
||||
<version>${spark.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.spark</groupId>
|
||||
<artifactId>spark-streaming_2.10</artifactId>
|
||||
<version>${spark.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
然后,要使用SparkPipelineOptions传递进Pipeline.create()方法。常见的创建方法是从命令行中读取参数来创建PipelineOption,使用的是PipelineOptionsFactory.fromArgs(String[])这个方法。在命令行中,你需要指定runner=SparkRunner:
|
||||
|
||||
mvn exec:java -Dexec.mainClass=YourMainClass \
|
||||
-Pspark-runner \
|
||||
-Dexec.args="--runner=SparkRunner \
|
||||
--sparkMaster=<spark master url>"
|
||||
|
||||
|
||||
也可以在Spark的独立集群上运行,这时候spark的提交命令,spark-submit。
|
||||
|
||||
spark-submit --class YourMainClass --master spark://HOST:PORT target/...jar --runner=SparkRunner
|
||||
|
||||
|
||||
当Beam程序在Spark上运行时,你也可以同样用Spark的网页监控数据流水线进度。
|
||||
|
||||
Flink运行模式
|
||||
|
||||
Flink Runner是Beam提供的用来在Flink上运行Beam Pipeline的模式。你可以选择在计算集群上比如 Yarn/Kubernetes/Mesos 或者本地Flink上运行。Flink Runner适合大规模,连续的数据处理任务,包含了以下功能:
|
||||
|
||||
|
||||
以Streaming为中心,支持streaming处理和batch处理;
|
||||
和flink一样的容错性,和exactly-once的处理语义;
|
||||
可以自定义内存管理模型;
|
||||
和其他(例如YARN)的Apache Hadoop生态整合比较好。
|
||||
|
||||
|
||||
其实看到这里,你可能已经掌握了这里面的诀窍。就是通过PipelineOptions来指定runner,而你的数据处理代码不需要修改。PipelineOptions可以通过命令行参数指定。那么类似Spark Runner,你也可以使用Flink来运行Beam程序。
|
||||
|
||||
同样的,首先你需要在pom.xml中添加Flink Runner的依赖。
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.beam</groupId>
|
||||
<artifactId>beam-runners-flink-1.6</artifactId>
|
||||
<version>2.13.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
然后在命令行中指定flink runner:
|
||||
|
||||
mvn exec:java -Dexec.mainClass=YourMainClass \
|
||||
-Pflink-runner \
|
||||
-Dexec.args="--runner=FlinkRunner \
|
||||
--flinkMaster=<flink master url>"
|
||||
|
||||
|
||||
Google Dataflow 运行模式
|
||||
|
||||
Beam Pipeline也能直接在云端运行。Google Cloud Dataflow就是完全托管的Beam Runner。当你使用Google Cloud Dataflow服务来运行Beam Pipeline时,它会先上传你的二进制程序到Google Cloud,随后自动分配计算资源创建Cloud Dataflow任务。
|
||||
|
||||
同前面讲到的Direct Runner和Spark Runner类似,你还是需要为Cloud Dataflow添加beam-runners-google-cloud-dataflow-java依赖关系:
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.beam</groupId>
|
||||
<artifactId>beam-runners-google-cloud-dataflow-java</artifactId>
|
||||
<version>2.13.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
我们假设你已经在Google Cloud上创建了project,那么就可以用类似的命令行提交任务:
|
||||
|
||||
mvn -Pdataflow-runner compile exec:java \
|
||||
-Dexec.mainClass=<YourMainClass> \
|
||||
-Dexec.args="--project=<PROJECT_ID> \
|
||||
--stagingLocation=gs://<STORAGE_BUCKET>/staging/ \
|
||||
--output=gs://<STORAGE_BUCKET>/output \
|
||||
--runner=DataflowRunner"
|
||||
|
||||
|
||||
小结
|
||||
|
||||
这一讲我们先总结了前面几讲Pipeline的完整使用方法。之后一起探索了Beam的重要特性,就是Pipeline可以通过PipelineOption动态选择同样的数据处理流水线在哪里运行。并且,分别展开讲解了直接运行模式、Spark运行模式、Flink运行模式和Google Cloud Dataflow运行模式。在实践中,你可以根据自身需要,去选择不同的运行模式。
|
||||
|
||||
思考题
|
||||
|
||||
Beam的设计模式是对计算引擎动态选择,它为什么要这么设计?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
307
专栏/大规模数据处理实战/31WordCountBeamPipeline实战.md
Normal file
307
专栏/大规模数据处理实战/31WordCountBeamPipeline实战.md
Normal file
@@ -0,0 +1,307 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
31 WordCount Beam Pipeline实战
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“WordCount Beam Pipeline实战”。
|
||||
|
||||
前面我们已经学习了Beam的基础数据结构PCollection,基本数据转换操作Transform,还有Pipeline等技术。你一定跃跃欲试,想要在实际项目中使用了。这一讲我们就一起学习一下怎样用Beam解决数据处理领域的教科书级案例——WordCount。
|
||||
|
||||
WordCount你一定不陌生,在第18讲中,我们就已经接触过了。WordCount问题是起源于MapReduce时代就广泛使用的案例。顾名思义,WordCount想要解决的问题是统计一个文本库中的词频。
|
||||
|
||||
比如,你可以用WordCount找出莎士比亚最喜欢使用的单词,那么你的输入是莎士比亚全集,输出就是每个单词出现的次数。举个例子,比如这一段:
|
||||
|
||||
HAMLET
|
||||
|
||||
ACT I
|
||||
|
||||
SCENE I Elsinore. A platform before the castle.
|
||||
|
||||
[FRANCISCO at his post. Enter to him BERNARDO]
|
||||
|
||||
BERNARDO Who's there?
|
||||
|
||||
FRANCISCO Nay, answer me: stand, and unfold yourself.
|
||||
|
||||
BERNARDO Long live the king!
|
||||
|
||||
FRANCISCO Bernardo?
|
||||
|
||||
BERNARDO He.
|
||||
|
||||
FRANCISCO You come most carefully upon your hour.
|
||||
|
||||
BERNARDO 'Tis now struck twelve; get thee to bed, Francisco.
|
||||
|
||||
FRANCISCO For this relief much thanks: 'tis bitter cold,
|
||||
And I am sick at heart.
|
||||
|
||||
BERNARDO Have you had quiet guard?
|
||||
|
||||
FRANCISCO Not a mouse stirring.
|
||||
|
||||
BERNARDO Well, good night.
|
||||
If you do meet Horatio and Marcellus,
|
||||
The rivals of my watch, bid them make haste.
|
||||
|
||||
FRANCISCO I think I hear them. Stand, ho! Who's there?
|
||||
|
||||
|
||||
在这个文本库中,我们用“the: 数字”表示the出现了几次,数字就是单词出现的次数。
|
||||
|
||||
The: 3
|
||||
And: 3
|
||||
Him: 1
|
||||
...
|
||||
|
||||
|
||||
那么我们怎样在Beam中处理这个问题呢?结合前面所学的知识,我们可以把Pipeline分为这样几步:
|
||||
|
||||
|
||||
用Pipeline IO读取文本库(参考第27讲);
|
||||
用Transform对文本进行分词和词频统计操作(参考第25讲);
|
||||
用Pipeline IO输出结果(参考第27讲);
|
||||
所有的步骤会被打包进一个Beam Pipeline(参考第26讲)。
|
||||
|
||||
|
||||
整个过程就如同下图所示。
|
||||
|
||||
|
||||
|
||||
创建Pipeline
|
||||
|
||||
首先,我们先用代码创建一个PipelineOptions的实例。PipelineOptions能够让我们对Pipeline进行必要的配置,比如配置执行程序的Runner,和Runner所需要的参数。我们在这里先采用默认配置。
|
||||
|
||||
记得第30讲中我们讲过,Beam Pipeline可以配置在不同的Runner上跑,比如SparkRunner,FlinkRunner。如果PipelineOptions不配置的情况下,默认的就是DirectRunner,也就是说会在本机执行。
|
||||
|
||||
Java
|
||||
|
||||
PipelineOptions options = PipelineOptionsFactory.create();
|
||||
|
||||
|
||||
接下来,我们就可以用这个PipelineOptions去创建一个Pipeline了。一个Pipeline实例会去构建一个数据处理流水线所需要的数据处理DAG,以及这个DAG所需要进行的Transform。
|
||||
|
||||
Java
|
||||
|
||||
Pipeline p = Pipeline.create(options);
|
||||
|
||||
|
||||
应用Transform
|
||||
|
||||
在上面的设计框图中,我们可以看到,我们需要进行好几种Transform。比如TextIO.Read、ParDo、Count去读取数据,操纵数据,以及存储数据。
|
||||
|
||||
每一种Transform都需要一些参数,并且会输出特定的数据。输入和输出往往会用PCollection的数据结构表示。简单回顾一下,PCollection是Beam对于数据集的抽象,表示任意大小、无序的数据,甚至可以是无边界的Streaming数据。
|
||||
|
||||
在我们这个WordCount例子中,我们的Transform依次是这样几个。
|
||||
|
||||
第一个Transform,是先要用TextIO.Read来读取一个外部的莎士比亚文集,生成一个PCollection,包含这个文集里的所有文本行。这个PCollection中的每个元素都是文本中的一行。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> lines = p.apply(TextIO.read().from("gs://apache-beam-samples/shakespeare/*"));
|
||||
|
||||
|
||||
第二个Transform,我们要把文本行中的单词提取出来,也就是做分词(tokenization)。
|
||||
|
||||
这一步的输入PCollection中的每个元素都表示了一行。那么输出呢?输出还是一个PCollection,但是每个元素变成了单词。
|
||||
|
||||
你可以留意一下,我们这里做分词时,用的正则表达式[^\p{L}]+,意思是非Unicode Letters所以它会按空格或者标点符号等把词分开。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> words = lines.apply("ExtractWords", FlatMapElements
|
||||
.into(TypeDescriptors.strings())
|
||||
.via((String word) -> Arrays.asList(word.split("[^\\p{L}]+"))));
|
||||
|
||||
|
||||
第三个Transform,我们就会使用Beam SDK提供的Count Transform。Count Transform会把任意一个PCollection转换成有key/value的组合,每一个key是原来PCollection中的非重复的元素,value则是元素出现的次数。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<KV<String, Long>> counts = words.apply(Count.<String>perElement());
|
||||
|
||||
|
||||
第四个Transform会把刚才的key/value组成的PCollection转换成我们想要的输出格式,方便我们输出词频。因为大部分的时候,我们都是想要把输出存储到另一个文件里的。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> formatted = counts.apply("FormatResults", MapElements
|
||||
.into(TypeDescriptors.strings())
|
||||
.via((KV<String, Long> wordCount) -> wordCount.getKey() + ": " + wordCount.getValue()));
|
||||
|
||||
|
||||
最后一个Transform就是TextIO.Write用来把最终的PCollection写进文本文档。PCollection中的每一个元素都会被写为文本文件中的独立一行。
|
||||
|
||||
运行Pipeline
|
||||
|
||||
调用Pipeline的run()方法会把这个Pipeline所包含的Transform优化并放到你指定的Runner上执行。这里你需要注意,run()方法是异步的,如果你想要同步等待Pipeline的执行结果,需要调用waitUntilFinish()方法。
|
||||
|
||||
Java
|
||||
|
||||
p.run().waitUntilFinish();
|
||||
|
||||
|
||||
改进代码的建议
|
||||
|
||||
代码看起来都完成了,不过,我们还可以对代码再做些改进。
|
||||
|
||||
编写独立的DoFn
|
||||
|
||||
在上面的示例代码中,我们把Transform都inline地写在了apply()方法里。
|
||||
|
||||
Java
|
||||
|
||||
lines.apply("ExtractWords", FlatMapElements
|
||||
.into(TypeDescriptors.strings())
|
||||
.via((String word) -> Arrays.asList(word.split("[^\\p{L}]+"))));
|
||||
|
||||
|
||||
但是这样的写法在实际工作中很难维护。
|
||||
|
||||
一是因为真实的业务逻辑往往比较复杂,很难用一两行的代码写清楚,强行写成inline的话可读性非常糟糕。
|
||||
|
||||
二是因为这样inline的Transform几乎不可复用和测试。
|
||||
|
||||
所以,实际工作中,我们更多地会去继承DoFn来实现我们的数据操作。这样每个DoFn我们都可以单独复用和测试。
|
||||
|
||||
接下来,我们看看怎样用用DoFn来实现刚才的分词Transform?
|
||||
|
||||
其实很简单,我们继承DoFn作为我们的子类ExtracrtWordsFn,然后把单词的拆分放在DoFn的processElement成员函数里。
|
||||
|
||||
Java
|
||||
|
||||
static class ExtractWordsFn extends DoFn<String, String> {
|
||||
private final Counter emptyLines = Metrics.counter(ExtractWordsFn.class, "emptyLines");
|
||||
private final Distribution lineLenDist =
|
||||
Metrics.distribution(ExtractWordsFn.class, "lineLenDistro");
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(@Element String element, OutputReceiver<String> receiver) {
|
||||
lineLenDist.update(element.length());
|
||||
if (element.trim().isEmpty()) {
|
||||
emptyLines.inc();
|
||||
|
||||
|
||||
// Split the line into words.
|
||||
String[] words = element.split(“[^\\p{L}]+”, -1);
|
||||
|
||||
// Output each word encountered into the output PCollection.
|
||||
for (String word : words) {
|
||||
if (!word.isEmpty()) {
|
||||
receiver.output(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
创建PTransform合并相关联的Transform
|
||||
|
||||
PTransform类可以用来整合一些相关联的Transform。
|
||||
|
||||
比如你有一些数据处理的操作包含几个Transform或者ParDo,你可以把他们封装在一个PTransform里。
|
||||
|
||||
我们这里试着把上面的ExtractWordsFn和Count两个Transform封装起来。这样可以对这样一整套数据处理操作复用和测试。当定义PTransform的子类时,它的输入输出类型就是一连串Transform的最初输入和最终输出。那么在这里,输入类型是String,输出类型是KV。就如同下面的代码一样。
|
||||
|
||||
Java
|
||||
|
||||
/**
|
||||
* A PTransform that converts a PCollection containing lines of text into a PCollection of
|
||||
* formatted word counts.
|
||||
*
|
||||
* <p>This is a custom composite transform that bundles two transforms (ParDo and
|
||||
* Count) as a reusable PTransform subclass. Using composite transforms allows for easy reuse,
|
||||
* modular testing, and an improved monitoring experience.
|
||||
*/
|
||||
|
||||
public static class CountWords
|
||||
extends PTransform<PCollection<String>, PCollection<KV<String, Long>>> {
|
||||
@Override
|
||||
public PCollection<KV<String, Long>> expand(PCollection<String> lines) {
|
||||
|
||||
// Convert lines of text into individual words.
|
||||
PCollection<String> words = lines.apply(ParDo.of(new ExtractWordsFn()));
|
||||
|
||||
// Count the number of times each word occurs.
|
||||
PCollection<KV<String, Long>> wordCounts = words.apply(Count.perElement());
|
||||
|
||||
return wordCounts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
参数化PipelineOptions
|
||||
|
||||
刚才我们把输入文件的路径和输出文件的路径都写在了代码中。但实际工作中我们很少会这样做。
|
||||
|
||||
因为这些文件的路径往往是运行时才会决定,比如测试环境和生产环境会去操作不同的文件。在真正的实际工作中,我们往往把它们作为命令行参数放在PipelineOptions里面。这就需要去继承PipelineOptions。
|
||||
|
||||
比如,我们创建一个WordCountOptions,把输出文件作为参数output。
|
||||
|
||||
Java
|
||||
|
||||
public static interface WordCountOptions extends PipelineOptions {
|
||||
@Description("Path of the file to write to")
|
||||
@Required
|
||||
String getOutput();
|
||||
|
||||
void setOutput(String value);
|
||||
}
|
||||
|
||||
|
||||
完成上面两个方面的改进后,我们最终的数据处理代码会是这个样子:
|
||||
|
||||
Java
|
||||
|
||||
public static void main(String[] args) {
|
||||
WordCountOptions options =
|
||||
PipelineOptionsFactory.fromArgs(args).withValidation().as(WordCountOptions.class);
|
||||
|
||||
Pipeline p = Pipeline.create(options);
|
||||
|
||||
p.apply("ReadLines", TextIO.read().from(options.getInputFile()))
|
||||
.apply(new CountWords())
|
||||
.apply(ParDo.of(new FormatAsTextFn()))
|
||||
.apply("WriteCounts", TextIO.write().to(options.getOutput()));
|
||||
|
||||
p.run().waitUntilFinish();
|
||||
}
|
||||
|
||||
|
||||
DoFn和PTransform的单元测试
|
||||
|
||||
如同第29讲“如何测试Pipeline”中所讲的那样,我们用PAssert测试Beam Pipeline。具体在我们这个例子中,我一再强调要把数据处理操作封装成DoFn和PTransform,因为它们可以独立地进行测试。
|
||||
|
||||
什么意思呢?比如,ExtractWordsFn我们想要测试它能把一个句子分拆出单词,比如“” some input words “,我们期待的输出是[“some”, “input”, “words”]。在测试中,我们可以这样表达:
|
||||
|
||||
/** Example test that tests a specific {@link DoFn}. */
|
||||
@Test
|
||||
public void testExtractWordsFn() throws Exception {
|
||||
DoFnTester<String, String> extractWordsFn = DoFnTester.of(new ExtractWordsFn());
|
||||
|
||||
Assert.assertThat(
|
||||
extractWordsFn.processBundle(" some input words "),
|
||||
CoreMatchers.hasItems("some", "input", "words"));
|
||||
Assert.assertThat(extractWordsFn.processBundle(" "), CoreMatchers.hasItems());
|
||||
Assert.assertThat(
|
||||
extractWordsFn.processBundle(" some ", " input", " words"),
|
||||
CoreMatchers.hasItems("some", "input", "words"));
|
||||
}
|
||||
|
||||
|
||||
小结
|
||||
|
||||
这一讲我们应用前面学习的PCollection,Pipeline,Pipeline IO,Transform知识去解决了一个数据处理领域经典的WordCount问题。并且学会了一些在实际工作中改进数据处理代码质量的贴士,比如写成单独可测试的DoFn,和把程序参数封装进PipelineOptions。
|
||||
|
||||
思考题
|
||||
|
||||
文中提供了分词的DoFn——ExtractWordsFn,你能利用相似的思路把输出文本的格式化写成一个DoFn吗?也就是文中的FormatAsTextFn,把PCollection> 转化成PCollection ,每一个元素都是 : 的格式。
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
155
专栏/大规模数据处理实战/32BeamWindow:打通流处理的任督二脉.md
Normal file
155
专栏/大规模数据处理实战/32BeamWindow:打通流处理的任督二脉.md
Normal file
@@ -0,0 +1,155 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
32 Beam Window:打通流处理的任督二脉
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Beam Window:打通流处理的任督二脉”。
|
||||
|
||||
在上一讲中,我们一起用Beam编写了第一个完整的WordCount项目,我们所用的例子是统计莎士比亚的文集中最常使用到的一些单词。
|
||||
|
||||
这里我们所用到的“莎士比亚文集”这种类型的数据集是一个静态的数据集。也就是说,我们在生成输入数据集的时候,就已经知道了这个数据集是完整的,并不需要再等待新的数据进来。
|
||||
|
||||
根据前面的内容,我们可以把这种数据集归类为有界数据集(Bounded Dataset)。这里我们的数据流水线就是一个批处理的数据流水线。
|
||||
|
||||
这个时候你可能会有一个疑问,如果我们想要统计的内容是一个正在连载的小说,我们在编写数据流水线的时候,这个小说还并没有完结,也就是说,未来还会不断有新的内容作为输入数据流入我们的数据流水线,那我们需要怎么做呢?
|
||||
|
||||
这个时候我们就需要用到窗口(Window)这个概念了。
|
||||
|
||||
窗口
|
||||
|
||||
在Beam的世界中,窗口这个概念将PCollection里的每个元素根据时间戳(Timestamp)划分成为了不同的有限数据集合。
|
||||
|
||||
当我们要将一些聚合操作(Aggregation)应用在PCollection上面的时候,或者我们想要将不同的PCollections连接(Join)在一起的时候,其实Beam是将这些操作应用在了这些被窗口划分好的不同数据集合上的。
|
||||
|
||||
无论是有界数据集还是无界数据集,Beam都会一视同仁,按照上面所说的规则进行处理。
|
||||
|
||||
你可能又会有一个疑问,我们在上一讲的例子当中,根本就没有提到过窗口这个概念,但是我刚刚又说,Beam同样会将有界数据集根据窗口划分成不同的有限数据集合来处理,那这些窗口、PCollection中每个元素的时间戳又是从哪里来的呢?
|
||||
|
||||
其实,我们在用I/O连接器读取有界数据集的过程中,Read Transform会默认为每个元素分配一个相同的时间戳。在一般情况下,这个时间戳就是你运行数据流水线的时间,也就是处理时间(Processing Time)。而Beam也会为这个数据流水线默认地分配一个全局窗口(Global Window),你可以把它理解为是一个从无限小到无限大的时间窗口。
|
||||
|
||||
如果你想要显式地将一个全局窗口赋予一个有界数据集的话,可以使用如下代码来完成:
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> input = p.apply(TextIO.read().from(filepath));
|
||||
|
||||
PCollection<String> batchInputs = input.apply(Window.<String>into(new GlobalWindows()));
|
||||
|
||||
|
||||
需要注意的是,你在处理有界数据集的时候,可以不用显式地将一个窗口分配给一个PCollection数据集。但是,在处理无边界数据集的时候,你必须要显式地分配一个窗口给这个无边界数据集。而这个窗口不可以是前面提到的全局窗口,否则在运行数据流水线的时候会直接抛出异常错误。
|
||||
|
||||
在了解过窗口的基本概念之后,接下来我来给你讲讲在Beam中存在的不同窗口类型。
|
||||
|
||||
固定窗口(Fixed Window)
|
||||
|
||||
固定窗口在有的数据处理框架中又被称为滚动窗口(Tumbling Window)。固定窗口通常是由一个静态的窗口大小定义而来的。
|
||||
|
||||
例如,要是我们定义一个每小时的窗口,那这个窗口大小就是固定的一个小时,如果我们按照2019年7月8号的0时作为时间的起始点,那么这个固定窗口就可以分为类似下面这样的形式:
|
||||
|
||||
[July 8, 2019 0:00:00 AM, July 8, 2019 1:00:00 AM),
|
||||
[July 8, 2019 1:00:00 AM, July 8, 2019 2:00:00 AM)
|
||||
[July 8, 2019 2:00:00 AM, July 8, 2019 3:00:00 AM)
|
||||
……
|
||||
|
||||
|
||||
而一个PCollection中的所有元素,就会根据它们各自自身的时间戳被分配给相应的固定窗口中。
|
||||
|
||||
这里你需要注意一点,因为固定窗口本质上并不可能会重叠在一起,如果我们定义的窗口是固定窗口的话,PCollection中的每一个元素只会落入一个,且是唯一一个窗口中。
|
||||
|
||||
在Beam中,如果要定义一个上述所说的按照每小时分割的窗口,我们可以使用一个Window Transform来完成,如下所示:
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> input = p.apply(KafkaIO.<Long, String>read()).apply(Values.<String>create());
|
||||
|
||||
PCollection<String> fixedWindowedInputs = input.apply(Window.<String>into(FixedWindows.of(Duration.standardHours(1))));
|
||||
|
||||
|
||||
滑动窗口(Sliding Window)
|
||||
|
||||
滑动窗口通常是由一个静态的窗口大小和一个滑动周期(Sliding Period)定义而来的。
|
||||
|
||||
例如,我们可以定义一个窗口大小为一个小时,滑动周期为30分钟的一个滑动窗口。我们还是以2019年7月8号的0时作为时间的起始点,那这个滑动窗口可以分为下面这样的形式:
|
||||
|
||||
[July 8, 2019 0:00:00 AM, July 8, 2019 1:00:00 AM)
|
||||
[July 8, 2019 0:30:00 AM, July 8, 2019 1:30:00 AM)
|
||||
[July 8, 2019 1:00:00 AM, July 8, 2019 2:00:00 AM)
|
||||
[July 8, 2019 1:30:00 AM, July 8, 2019 2:30:00 AM)
|
||||
……
|
||||
|
||||
|
||||
因为Beam对于滑动周期的大小并没有做任何限制,所以你可以看到,滑动窗口和固定窗口不同的是,当滑动周期小于窗口大小的时候,滑动窗口会有部分重叠。也就是说,在一个PCollection里面,同一个元素是可以被分配到不同的滑动窗口中的。
|
||||
|
||||
可能你也会发现到,当滑动窗口的窗口大小和滑动周期一样的时候,这个滑动窗口的性质其实就和固定窗口一样了。
|
||||
|
||||
在Beam中,如果要定义一个上述所说,窗口大小为一个小时而滑动周期为30分钟的一个滑动窗口,我们同样可以使用一个Window Transform来完成,如下所示:
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> input = p.apply(KafkaIO.<Long, String>read()).apply(Values.<String>create());
|
||||
|
||||
PCollection<String> slidingWindowedInputs = input.apply(Window.<String>into(SlidingWindows.of(Duration.standardHours(1)).every(Duration.standardMinutes(30))));
|
||||
|
||||
|
||||
会话窗口(Sessions Window)
|
||||
|
||||
会话窗口和上面所说的两个窗口有点不一样,它并没有一个固定的窗口长度。
|
||||
|
||||
会话窗口主要是用于记录持续了一段时间的活动数据集。在一个会话窗口中的数据集,如果将它里面所有的元素按照时间戳来排序的话,那么任意相邻的两个元素它们的时间戳相差不会超过一个定义好的静态间隔时间段(Gap Duration)。
|
||||
|
||||
怎么理解这个定义呢?我想用一个例子来解释会比较清晰。
|
||||
|
||||
假设,我们现在正在一个视频流的平台中处理无界数据集,我们想要分析在这个平台中的一些用户行为习惯。
|
||||
|
||||
为了方便说明,我们想要分析的问题非常简单,就是一个用户在线看视频一般会在每天的什么时候开始看多长时间的视频。同时,我们假设只会有一个用户的数据流入我们的输入数据集中,这个数据会带有用户对视频平台发送视频流请求的时间戳。
|
||||
|
||||
我们希望定义一个会话窗口来处理这些数据,而这个会话窗口的间隔时间段为5分钟。
|
||||
|
||||
所有的数据假设都是发生在2019年7月8号中的,流入的数据集如下:
|
||||
|
||||
(key1, value1, [7:44:00 AM,7:44:00 AM))
|
||||
(key1, value2, [7:45:00 AM,7:45:00 AM))
|
||||
(key1, value3, [7:49:00 AM,7:49:00 AM))
|
||||
(key1, value4, [8:01:00 AM,8:01:00 AM))
|
||||
(key1, value5, [8:02:00 AM,8:02:00 AM))
|
||||
|
||||
|
||||
那么,这5个数据会形成两个会话窗口,分别是:
|
||||
|
||||
(key1,[(value1, [7:44:00 AM,7:44:00 AM)), (value2, [7:45:00 AM,7:45:00 AM)), (value3, [7:49:00 AM,7:49:00 AM))])
|
||||
|
||||
|
||||
(key1,[(value4, [8:01:00 AM,8:01:00 AM)), (value5, [8:02:00 AM,8:02:00 AM))])
|
||||
|
||||
|
||||
你可以看到,在第一个会话窗口中,数据的时间戳分别是7:44:00AM,7:45:00AM和7:49:00AM,这个窗口的总长度有5分钟。任意相邻的两个元素之间的时间间隔不会超过我们之前定义好的5分钟。
|
||||
|
||||
而在第二个会话窗口中,数据的时间戳分别是8:01:00AM和8:02:00AM,这个窗口的总长度有1分钟,同样任意相邻的两个元素之间的时间间隔不会超过5分钟。每一个会话窗口都记录了一个用户的在线活跃点和时长。
|
||||
|
||||
在Beam中,如果要定义一个上述所说会话窗口的话,你可以使用以下代码来完成:
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> input = p.apply(KafkaIO.<Long, String>read()).apply(Values.<String>create());
|
||||
|
||||
PCollection<String> sessionWindowedInputs = input.apply(Window.<String>into(Sessions.withGapDuration(Duration.standardMinutes(5))));
|
||||
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起学习了在处理无界数据集的时候,我们需要显示定义到的一个概念——窗口。
|
||||
|
||||
窗口的定义是后面我们编写流处理数据流水线的一个基石。而窗口这个概念其实就是用来回答我们在第23讲中“WWWH”问题里“Where in event time they are being computed”这个问题的。除去全局窗口,Beam里面总共让我们定义三种不同的窗口类型,分别是固定窗口,滑动窗口和会话窗口。
|
||||
|
||||
思考题
|
||||
|
||||
在今天介绍的三种时间窗口类型中,你觉得这三种时间窗口分别适合使用在什么样的应用场景中呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
199
专栏/大规模数据处理实战/33横看成岭侧成峰:再战StreamingWordCount.md
Normal file
199
专栏/大规模数据处理实战/33横看成岭侧成峰:再战StreamingWordCount.md
Normal file
@@ -0,0 +1,199 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
33 横看成岭侧成峰:再战Streaming WordCount
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“横看成岭侧成峰:再战Streaming WordCount”。
|
||||
|
||||
在上一讲中,我们学习了Beam窗口(Window)的概念。当时,我们提到窗口技术的产生是因为我们想要根据时间戳去分组处理一个PCollection中的元素。
|
||||
|
||||
我们也提到了在“统计莎士比亚文集词频”这个例子中,如果莎士比亚穿越到了现代,成了一名极客时间的专栏作家,我们就可能需要根据他文章的写作时间来统计词频了。
|
||||
|
||||
举个具体的例子的话,就是我们能不能灵活地得到莎士比亚在2017年9月使用的高频词汇?或者是他在2018年第7个周五偏爱使用的高频词汇呢?
|
||||
|
||||
时效性是数据处理很重要的一部分,类似上面这样的问题还有很多。
|
||||
|
||||
比如,能不能根据实时交通数据,得到最近24小时之内拥堵的道路?能不能根据所有微信分享文章的点击数据,得到过去一周最热门的文章?这些问题都是可以用窗口技术来解决。
|
||||
|
||||
所以今天这一讲,我们就来看看怎样在WordCount这个例子中使用窗口技术。我会介绍怎样在Beam中实现以下六个问题:
|
||||
|
||||
|
||||
怎样区分有界数据还是无界数据?
|
||||
怎样读取无边界数据?
|
||||
怎样给PCollection数据添加时间戳?
|
||||
怎样在PCollection应用窗口?
|
||||
怎样复用之前的DoFn和PTransform?
|
||||
怎样存储无边界数据?
|
||||
|
||||
|
||||
怎样区分有界数据还是无界数据?
|
||||
|
||||
我们知道,在Beam中你可以用同一个Pipeline处理有边界数据或者无边界数据。但我们在处理两者时的思考方式和操作方法还是有细微的不同的。
|
||||
|
||||
比如,有界数据之所以有界,是因为你在处理数据时,所有的数据就已经准备就绪了。
|
||||
|
||||
在第31讲的WordCount例子中,莎士比亚文集早已成为历史,不会有新的作品了。所以,你可以用有界数据的处理方式进行实现。当你的数据输入是有界的时候,下游的数据一般也是有界的。因为你的数据元素是有限的,在数据处理的过程中,不会凭空造出无限多的数据。
|
||||
|
||||
而无边界数据的到来是时刻不停的。在你处理处理流水线的任意时刻,数据都没有完全结束。
|
||||
|
||||
比如,在我们第1讲中提到的处理美团外卖电动车例子中,美团外卖电动车的图片就是一直在不停地更新。你不可能说“我已经有了所有的外卖电动车图片了”。在无界数据的处理流水线中,因为输入数据是无界的,所以下游的处理结果一般也是无界的。
|
||||
|
||||
相信你已经掌握了区分区分有界和无界数据方法。在接下来的内容中,我们会看到针对两种数据的不同处理方式。
|
||||
|
||||
但是,不论是有界数据还是无界数据,在Beam中我们都可以用窗口把数据按时间分割成一些有限大小的集合。只是对于无界数据,你必须使用窗口对数据进行分割,然后对每个窗口内的数据集进行处理。
|
||||
|
||||
怎样读取无边界数据?
|
||||
|
||||
在第31讲WordCount的案例中,我们从一个外部文本文件读取了莎士比亚全集的文本内容。当时,我们使用的是Beam的TextIO:
|
||||
|
||||
Java
|
||||
|
||||
Pipeline p = Pipeline.create(options);
|
||||
|
||||
p.apply("ReadLines", TextIO.read().from(options.getInputFile()))
|
||||
|
||||
|
||||
这是因为我们当时面对的是有边界的数据,在我们的数据处理流水线运行之前,所有的莎士比亚全集文本早已准备就绪,所以我们可以一股脑儿全部读进来。但是当输入数据是无界的时候,我们就没法这样读取数据了。常见的无界数据读取自logging系统或者Pub/Sub系统。
|
||||
|
||||
由于logging系统一直在不断地运行,新的log在不停地产生,并且每条log都自带时间戳。比如,我们想要根据用户对于微信文章的点击log分析不同时刻的热门文章,我们就可以去读取微信文章的log。而在Pub/Sub系统中,我们订阅的消息也会永无止境地到来,类似的一般Pub/Sub订阅的每条消息也会自带原生的时间戳。
|
||||
|
||||
这一讲中,我们已经假设莎士比亚穿越到现代在极客时间开了个专栏。我们不妨把他的专栏文章更新设计在一个Kafka消息系统中。
|
||||
|
||||
如下图所示,即使你并没有使用过Kafka也没有关系。你只需要知道在我们的数据处理系统中能够不定时地收到来自莎士比亚的文章更新,每一次的文章更新包含了更新的文章标题和更新内容。
|
||||
|
||||
|
||||
|
||||
这时,我们可以使用Beam的Kafka IO来读取来自Kafka的订阅消息。
|
||||
|
||||
在下面的示例代码中,我们指定了需要读取的Kafka消息主题“shakespeare”,以及Kafka消息的key/value类型都是String。你需要注意这里的读取选项withLogAppendTime(),它的意思是我们用Kafka的log append time作为我们beam PCollection数据集的时间戳。
|
||||
|
||||
Java
|
||||
|
||||
pipeline
|
||||
.apply(KafkaIO.<String, String>read()
|
||||
.withBootstrapServers("broker_1:9092,broker_2:9092")
|
||||
.withTopic("shakespeare") // use withTopics(List<String>) to read from multiple topics.
|
||||
.withKeyDeserializer(StringDeserializer.class)
|
||||
.withValueDeserializer(StringDeserializer.class)
|
||||
.withLogAppendTime()
|
||||
)
|
||||
|
||||
|
||||
怎样给PCollection数据添加时间戳?
|
||||
|
||||
一般情况下,窗口的使用场景中,时间戳都是原生的。就如同我们从Kafka中读取消息记录一样,时间戳是自带在每一条Kafka消息中的。
|
||||
|
||||
但Beam也允许我们手动给PCollection的元素添加时间戳。例如第31讲的WordCount例子本身就是一个有界数据集,你还记得吗?那么我们怎么给这些有界数据集添加时间戳呢?
|
||||
|
||||
第31讲的输入数据格式就是简单的文本文件:
|
||||
|
||||
HAMLET
|
||||
|
||||
ACT I
|
||||
|
||||
SCENE I Elsinore. A platform before the castle.
|
||||
|
||||
[FRANCISCO at his post. Enter to him BERNARDO]
|
||||
|
||||
BERNARDO Who's there?
|
||||
|
||||
FRANCISCO Nay, answer me: stand, and unfold yourself.
|
||||
|
||||
|
||||
为了方便阐述概念,我们不妨假设一下,现在我们的输入文件变成了如下的格式,每一行的开头都会带有一个时间戳,在冒号分隔符号之后才是我们需要处理的文本:
|
||||
|
||||
2019-07-05: HAMLET
|
||||
|
||||
2019-07-06: ACT I
|
||||
|
||||
2019-07-06: SCENE I Elsinore. A platform before the castle.
|
||||
|
||||
2019-07-07: [FRANCISCO at his post. Enter to him BERNARDO]
|
||||
|
||||
2019-07-07: BERNARDO Who's there?
|
||||
|
||||
2019-07-07: FRANCISCO Nay, answer me: stand, and unfold yourself.
|
||||
|
||||
|
||||
当时我们是直接对每一行的文本提取了所有的单词。但在现在这样的输入格式下,我们就可以先把每一行开头的时间戳提取出来。在DoFn的processElement实现中,我们用outputWithTimestamp()方法,可以对于每一个元素附上它所对应的时间戳。
|
||||
|
||||
Java
|
||||
|
||||
static class ExtractTimestampFn extends DoFn<String, String> {
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
String extractedLine = extractLine(c.element());
|
||||
Instant timestamp =
|
||||
new Instant(extractTimestamp(c.element());
|
||||
|
||||
c.outputWithTimestamp(extractedLine, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
怎样在PCollection应用窗口?
|
||||
|
||||
通过前面的内容,我们已经解决了“PCollection的时间戳来自于哪里”的问题。在无界数据的应用场景中,时间戳往往是数据记录自带的,比如来自Kafka消息。在有界数据的应用场景中,时间戳往往需要自己指定,比如我们读取的自定义的莎士比亚文集格式。
|
||||
|
||||
PCollection元素有了时间戳,我们就能根据时间戳应用窗口对数据进行划分。第32讲中,我们已经介绍了常见的窗口种类,有固定窗口、滑动窗口和会话窗口。
|
||||
|
||||
要把特定的窗口应用到PCollection上,我们同样使用PCollection的apply()方法。如果是固定窗口,我们就用FixedWindows类型,如果是滑动窗口就用SlidingWindows类型,相应的如果是会话窗口我们就用Sessions窗口类型。下面的代码示例就是使用FixedWindows的情况:
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> windowedWords = input
|
||||
.apply(Window.<String>into(
|
||||
FixedWindows.of(Duration.standardMinutes(options.getWindowSize()))));
|
||||
|
||||
|
||||
怎样复用之前的DoFn和PTransform?
|
||||
|
||||
有了窗口,我们下一步就是把之前的DoFn和PTransform应用到数据集上。
|
||||
|
||||
这一步其实是最简单的。因为Beam的Transform不区分有界数据还是无界数据。我们可以一行代码不改,和第31讲用到的例子一样,直接使用之前的CountWords这个PTransform就可以了。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<KV<String, Long>> wordCounts = windowedWords.apply(new WordCount.CountWords());
|
||||
|
||||
|
||||
值得注意的是,在应用了窗口之后,Beam的transform是在每一个窗口中间进行数据处理的。在我们的例子中,词频统计的是每一个窗口里的词频,而不再是全局的词频。
|
||||
|
||||
怎样输出无边界数据?
|
||||
|
||||
同数据读取对应,无边界数据的输出也是与有界数据大相径庭。在第31讲中,我们把数据处理结果写进了一个外部文件中,使用了TextIO:
|
||||
|
||||
Java
|
||||
|
||||
pipeline.apply("WriteCounts", TextIO.write().to(options.getOutput()));
|
||||
|
||||
|
||||
但是在无边界的应用场景中,数据在持续不断地进来。最常见的输出模式是把处理结果还是以Pub/Sub的模式发布出去。
|
||||
|
||||
假设我们用Google Pub/Sub输出我们的处理结果的话,我们可以用PubsubIO.writeStrings()方法。同样,这里的输出结果是针对每一个窗口的,每一个窗口都会输出自己的词频统计结果。
|
||||
|
||||
Java
|
||||
|
||||
pipeline.apply("Write to PubSub", PubsubIO.writeStrings().to(options.getOutputTopic()));
|
||||
|
||||
|
||||
小结
|
||||
|
||||
今天我们深入探索了Beam窗口在流处理的场景中的应用。
|
||||
|
||||
我们巩固了区分有界数据还是无界数据的方法,掌握了在Beam中怎样读取无边界数据,怎样给PCollection数据添加时间戳,怎样在PCollection应用窗口,怎样复用之前的DoFn和PTransform和怎样输出无边界数据。
|
||||
|
||||
将这些方法融会贯通后,相信类似的时间性数据处理或者是流处理问题在你手中都能迎刃而解了。
|
||||
|
||||
思考题
|
||||
|
||||
你的工作中有哪些应用场景不适合一般的数据批处理呢?能否利用这里介绍窗口方式处理?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
263
专栏/大规模数据处理实战/34Amazon热销榜BeamPipeline实战.md
Normal file
263
专栏/大规模数据处理实战/34Amazon热销榜BeamPipeline实战.md
Normal file
@@ -0,0 +1,263 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
34 Amazon热销榜Beam Pipeline实战
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Amazon热销榜Beam Pipeline实战”。
|
||||
|
||||
两个月前,亚马逊(Amazon)宣布将关闭中国国内电商业务的消息你一定还记忆犹新。虽然亚马逊遗憾离场,但它依然是目前全球市值最高的电商公司。
|
||||
|
||||
作为美国最大的一家网络电子商务公司,亚马逊的总部位于华盛顿州的西雅图。类似于BAT在国内的地位,亚马逊也是北美互联网FAANG五大巨头之一,其他四个分别是Facebook、Apple、Netflix和Google。
|
||||
|
||||
亚马逊的热销商品系统就如下图所示。
|
||||
|
||||
|
||||
|
||||
当我搜索“攀岩鞋”时,搜索结果的第三个被打上了“热销商品”的标签,这样能帮助消费者快速做出购买决策。
|
||||
|
||||
当我点击这个“Best Seller”的标签时,我可以浏览“攀岩鞋”这个商品分类中浏览销量最高的前100个商品。
|
||||
|
||||
|
||||
|
||||
这些贴心的功能都是由热销商品系统实现的。
|
||||
|
||||
这一讲我们就来看看在这样的热销商品系统中,怎样应用之前所学的Beam数据处理技术吧。今天,我们主要会解决一个热销商品系统数据处理架构中的这几个问题:
|
||||
|
||||
|
||||
怎样用批处理计算基础的热销商品列表、热销商品的存储和serving设计?
|
||||
怎样设计每小时更新的热销榜单?
|
||||
怎样设计商品去重处理流水线和怎样根据商品在售状态过滤热销商品?
|
||||
怎样按不同的商品门类生成榜单?
|
||||
|
||||
|
||||
1.怎样用批处理计算基础的热销商品列表、热销商品的存储和serving设计?
|
||||
|
||||
我们先来看最简单的问题设置,怎样用批处理计算基础的热销商品列表。
|
||||
|
||||
假设你的电商网站销售着10亿件商品,并且已经跟踪了网站的销售记录:商品id和购买时间 {product_id, timestamp},整个交易记录是1000亿行数据,TB级。举个例子,假设我们的数据是这样的:
|
||||
|
||||
|
||||
|
||||
我们可以把热销榜按 product_id 排名为:1,2,3。
|
||||
|
||||
你现在有没有觉得这个问题似曾相识呢?的确,我们在第3讲“大规模数据初体验”中用这个例子引出了数据处理框架设计的基本需求。
|
||||
|
||||
这一讲中,我们会从这个基本问题设置开始,逐步深入探索。
|
||||
|
||||
在第3讲中,我们把我们的数据处理流程分成了两个步骤,分别是:
|
||||
|
||||
|
||||
统计每个商品的销量
|
||||
找出销量前K
|
||||
|
||||
|
||||
我们先来看第一个步骤的统计商品销量应该如何在Beam中实现。我们在第3讲中是画了这样的计算集群的示意图:
|
||||
|
||||
|
||||
|
||||
如果你暂时没有思路的话,我们不妨试试换一个角度来思考这个问题。
|
||||
|
||||
统计商品的销量,换句话说,其实就是计算同样的商品id在我们的销售记录数据库中出现了多少次。这有没有让你联想到什么呢?没错,就是我们在第31讲中讲到的WordCount例子。WordCount是统计同样的单词出现的次数,而商品销量就是统计同样的商品id出现的次数。
|
||||
|
||||
所以,我们完全可以用WordCount中的部分代码解决商品销量统计的这部分数据处理逻辑。
|
||||
|
||||
在WordCount中,我们用words.apply(Count.perElement())把一个分词后的PCollection转换成了“单词为key,count为value”的一个key/value组合。
|
||||
|
||||
在这里呢,我们同样使用salesRecords.apply(Count.perElement())把一个商品id的PCollection转换成“key为商品id,value为count”的key/value组合。
|
||||
|
||||
Java
|
||||
|
||||
// WordCount的统计词频步骤
|
||||
wordCount = words.apply(Count.perElement())
|
||||
|
||||
// 我们这里的统计销量步骤
|
||||
salesCount = salesRecords.apply(Count.perElement())
|
||||
|
||||
|
||||
解决了统计每个商品的销量步骤,我们再来看看怎样统计销量前K的商品。在第3讲中,我们是画了一个计算集群来解决这个问题。
|
||||
|
||||
|
||||
|
||||
但是Beam提供了很好的API供我们使用。我们可以使用Top() 这个Transform。
|
||||
|
||||
Top接受的第一个参数就是我们这里的K,也就是我们最终要输出的前几个元素。我们需要实现的是一个Java Comparator interface。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<KV<String, Long>> topK =
|
||||
salesCount.apply(Top.of(K, new Comparator<KV<String, Long>>() {
|
||||
@Override
|
||||
public int compare(KV<String, Long> a, KV<String, Long> b) {
|
||||
return b.getValue.compareTo(a.getValue());
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
到这里,销量前K的产品就已经被计算出来了。
|
||||
|
||||
和所有数据处理流水线一样,我们需要的是一个完整的系统。那么你就不能仅仅满足于计算出结果,必须要考虑你的数据处理结果将怎样被使用。在本文开头的截图中,你能看到,热销商品都被打上了“Best Seller”的标签,点击“Best Seller”标签我们还能看到完整的热销榜单。
|
||||
|
||||
那么你可以考虑两种serving的方案。
|
||||
|
||||
一种是把热销商品的结果存储在一个单独的数据库中。但是在serving时候,你需要把商品搜索结果和热销商品结果进行交叉查询。如果搜索结果存在于热销商品数据库中,你就在返回的搜索结果元素中把它标注成“Best Seller”。
|
||||
|
||||
另一个可能不太灵活,就是把热销商品的结果写回原来的商品数据库中。如果是热销商品,你就在“是热销商品”这一列做标记。这种方案的缺点是每次更新热销结果后,都要去原来的数据库进行大量更新,不仅要把新成为热销的商品进行标记,还要将落选商品的标记去除。
|
||||
|
||||
两种serving方案的选择影响了你对于数据处理输出的业务需求。相应的,你可以把输出的前K销量产品使用Pipeline output输出到一个单独数据库,也可以统一更新所有数据库中的商品。
|
||||
|
||||
2.怎样设计每小时更新的热销榜单?
|
||||
|
||||
在设计完基础的热销商品处理系统后,我们注意到在Amazon的热销榜上有一行小字 “Updated hourly”,也就是“每小时更新”。的确,热销商品往往是有时效性的。去年热销的iPhone X今年就变成了iPhone XS。Amazon选择了以小时为单位更新热销榜单确实是合理的产品设计。
|
||||
|
||||
那么怎样用Beam实现这种定时更新的数据处理系统呢?
|
||||
|
||||
可能你在看到“时间”这个关键词的时候,就马上联想到了第32讲介绍的Beam Window。确实,用基于Window的流处理模式是一个值得考虑的方案。我在这里故意把问题设置得比较模糊。其实是因为这需要取决于具体的业务需求,实际上你也可以选择批处理或者流处理的方式。
|
||||
|
||||
我们先从简单的批处理模式开始。
|
||||
|
||||
在处理工程问题时,我们都是先看最简单的方案能否支撑起来业务需求,避免为了体现工程难度而故意将系统复杂化。采用批处理的方式的话,我们可以每隔一个小时运行一遍上一个小标题中的基础版热销商品系统,也就是部署成cron job的模式。
|
||||
|
||||
但你要注意,如果我们不修改代码的话,每一次运行都会计算目前为止所有销售的商品。如果这不是你的业务需求,你可以在批处理的数据输入步骤中,根据你的销售记录表的时间戳来筛选想要计算的时间段。比如,你可以设置成每一次运行都只计算从运行时间前6个月到该运行时间为止。
|
||||
|
||||
其实,批处理的模式已经能解决我们目前为止的大部分业务需求。但有时候我们不得不去使用流处理。比如,如果存储销售记录的团队和你属于不同的部门,你没有权限去直接读取他们的数据库,他们部门只对外分享一个Pub/Sub的消息队列。这时候就是流处理应用的绝佳场景。
|
||||
|
||||
不知道你还记不记得第33讲中我提到过,在Streaming版本的WordCount中监听一个Kafka消息队列的例子。同样的,这时候你可以订阅来自这个部门的销售消息。
|
||||
|
||||
Java
|
||||
|
||||
pipeline
|
||||
.apply(KafkaIO.<String, Long>read()
|
||||
.withBootstrapServers("broker_1:9092,broker_2:9092")
|
||||
.withTopic("sales_record") // use withTopics(List<String>) to read from multiple topics.
|
||||
.withKeyDeserializer(StringDeserializer.class)
|
||||
.withValueDeserializer(StringDeserializer.class)
|
||||
.withLogAppendTime()
|
||||
)
|
||||
|
||||
|
||||
之后你可以为你的输入PCollection添加窗口,和WordCount一样。不过这时候你很有可能需要滑动窗口,因为你的窗口是每小时移动一次。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<String> windowedProductIds = input
|
||||
.apply(Window.<String>into(
|
||||
SlidingWindows.of(Duration.standardMonths(options.getWindowSize()))));
|
||||
|
||||
|
||||
3.怎样设计商品去重处理流水线和怎样根据商品在售状态过滤热销商品?
|
||||
|
||||
通过前面的内容,我们已经设计出了能够每小时更新的热销榜单。但不知道你有没有注意到,其实我们之前的问题设置是过于简化了,忽略了很多现实而重要的问题,比如:
|
||||
|
||||
|
||||
怎样处理退货的商品?
|
||||
怎样处理店家因为收到差评故意把商品下架换个马甲重新上架?
|
||||
怎样处理那些虽然曾经热销但是现在已经不再出售的商品?
|
||||
|
||||
|
||||
这些问题都需要使用第28讲中介绍的Pipeline的基本设计模式:过滤模式。
|
||||
|
||||
|
||||
|
||||
我们这个案例中所需要的过滤条件是:
|
||||
|
||||
|
||||
把退货的商品销量减去
|
||||
把重复的商品销量进行叠加
|
||||
将在售商品过滤出来
|
||||
|
||||
|
||||
一起来想想这些过滤条件应该怎么实现吧。
|
||||
|
||||
对于退货商品,我们可以把所有退货的记录挑出来进行统计。同样对于每一个商品id,如果我们把出售的计数减去退货的计数就可以得到成功销售的计数。
|
||||
|
||||
而事实上,实际交易系统对于商品状态的跟踪会详细得多,每一个订单最终落在退货状态还是成功销售状态都可以在交易数据库中查询得到。我们可以把这个封装在isSuccessfulSale()方法中。
|
||||
|
||||
重复的商品在一个成熟的交易系统中一般会有另外一个去重的数据处理流水线。它能根据商品描述、商品图片等推测重复的商品。我们假设在我们系统中已经有了product_unique_id这样一个记录,那么我们只要把之前进行统计的product_id替换成统计product_unique_id就行了。
|
||||
|
||||
过滤在售的商品可能有多种实现方式,取决于你的小组有没有权限读取所需的数据库。
|
||||
|
||||
假如你可以读取一个商品状态的数据库列,那你可以直接根据 [商品状态=在售] 这样的判断条件进行过滤。假如你不能读取商品状态,那你可能需要查询在售的商品中是否有你的这个商品id来进行判断。但在这一讲中,我们先忽略实现细节,把这个过滤逻辑封装在isInStock()方法中。
|
||||
|
||||
最终我们的过滤处理步骤会是类似下面这样的。只有同时满足isSuccessfulSale()和isInStock()的product_unique_id,才会被我们后续的销量统计步骤所计算。
|
||||
|
||||
Java
|
||||
|
||||
PCollection<Product> productCollection = ...;
|
||||
|
||||
PCollection<Product> qualifiedProductCollection = productCollection
|
||||
.apply(“uniqueProductTransform”, Distinct.withRepresentativeValueFn(
|
||||
new SerializableFunction<Product, Long>() {
|
||||
@Override
|
||||
public Long apply(Product input) {
|
||||
return input.productUniqueId();
|
||||
}
|
||||
}).withRepresentativeType(TypeDescriptor.of(Long.class))
|
||||
)
|
||||
.apply("filterProductTransform", ParDo.of(new DoFn<Product, Product>(){
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
if (isSuccessfulSale(c.element()) && isInStockc.element())) {
|
||||
c.output(c.element());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
4.怎样按不同的商品门类生成榜单?
|
||||
|
||||
我们还注意到亚马逊的热销榜是按照不同的商品种类的。也就说每一个商品分类都有自己的榜单。这是很合理的业务设计,因为你不可能会去把飞机的销量和手机的销量相比,手机可能人手一个,飞机无法人手一个。
|
||||
|
||||
这时候我们在第28讲所学的分离模式设计就能大显身手了。分离模式把一个PCollection按照类别分离成了几个小的子PCollection。
|
||||
|
||||
|
||||
|
||||
在这个案例里,我们也需要对商品进行分离。
|
||||
|
||||
与经典的分离模式不同,我们这里每一个商品可能都属于多个类别。比如一双鞋子,它可能既归类为“户外”,也归类为“潮鞋”。还记得分离模式在Beam中怎么实现吗?没错就是使用output tag。我们先要为每一种分类定义tag,比如刚才说的outdoorTag和fashionTag。再把相应的商品输出进对应的tag中。示例如下:
|
||||
|
||||
Java
|
||||
|
||||
// 首先定义每一个output的tag
|
||||
final TupleTag<Product> outdoorTag = new TupleTag<Product>(){};
|
||||
final TupleTag<Product> fashionTag = new TupleTag<Product>(){};
|
||||
|
||||
PCollection<Product> salesCollection = ...;
|
||||
|
||||
PCollectionTuple mixedCollection =
|
||||
userCollection.apply(ParDo
|
||||
.of(new DoFn<Product, Product>() {
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) {
|
||||
if (isOutdoorProduct(c.element())) {
|
||||
c.output(c.element());
|
||||
} else if (isFashionProduct(c.element())) {
|
||||
c.output(fashionTag, c.element());
|
||||
}
|
||||
}
|
||||
})
|
||||
.withOutputTags(outdoorTag, TupleTagList.of(fashionTag)));
|
||||
|
||||
// 分离出不同的商品分类
|
||||
mixedCollection.get(outdoorTag).apply(...);
|
||||
|
||||
mixedCollection.get(fashionTag).apply(...);
|
||||
|
||||
|
||||
小结
|
||||
|
||||
这一讲我们从基础商品排行榜系统出发,利用到了之前学的数据处理设计模式和Beam编程方法。
|
||||
|
||||
同时,探索了以批处理计算为基础的热销商品列表。我们设计了每小时更新的热销榜单、商品去重处理流水线,根据商品在售状态过滤出热销商品,并按不同的商品门类生成榜单。
|
||||
|
||||
思考题
|
||||
|
||||
一个商品排名系统中还有太多需要解决的工程问题,你觉得哪些也可以利用大规模数据处理技术设计解决?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
227
专栏/大规模数据处理实战/35Facebook游戏实时流处理BeamPipeline实战(上).md
Normal file
227
专栏/大规模数据处理实战/35Facebook游戏实时流处理BeamPipeline实战(上).md
Normal file
@@ -0,0 +1,227 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
35 Facebook游戏实时流处理Beam Pipeline实战(上)
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“Facebook游戏实时流处理Beam Pipeline实战”。
|
||||
|
||||
Facebook这个社交平台我相信你一定早有耳闻。它除了能够让用户发送消息给好友,分享自己的动态图片和视频之外,还通过自身的App Center管理着各式各样的小游戏。许多游戏开发商借助Facebook的好友邀请机制让自己的App火了一把。
|
||||
|
||||
曾经有一段时间,在Facebook上有一款名为糖果传奇(Candy Crush Saga)的游戏风靡了整个北美。各个年龄层的玩家都会在空闲的时间拿出手机,过五关斩六将,希望尽快突破更多的关卡,并且获得高分。
|
||||
|
||||
|
||||
|
||||
当然了,除了消除游戏本身带来的乐趣以外,可以在Facebook里和自己的好友进行积分排名比拼也是另外一个能吸引用户的地方。
|
||||
|
||||
|
||||
|
||||
想要一个类似Facebook这样的好友间积分排行榜,你可以有很多种实现方式以及各种优化方法。那么,如果我们要利用Apache Beam的话,该怎样实现一个类似的游戏积分排行榜呢?
|
||||
|
||||
今天我就来和你一起研究,要如何利用Apache Beam的数据流水线来实现一个我们自定义的简单游戏积分排行榜。
|
||||
|
||||
为了简化整个游戏积分排行榜案例的说明,我们先来做几个方面的假设:
|
||||
|
||||
|
||||
面向的群体:游戏积分排行榜针对的是全局每位用户以及每一个关卡,我们不需要担心如何在Beam的数据流水线中优化每个用户自身的好友积分列表。
|
||||
更新时间:为了保持用户的粘性,我们设定这个游戏积分排行榜的数据每隔一周就会更新一次。也就是说如果一位用户在2019年7月15日成功通关了一次游戏并且分数是这周内自身的最高分,那么这次的最高分数将一直留在2019年7月15日至2019年7月21日这周的排行榜中。但是到了2019年7月22日后,这个分数将不会再出现,需要用户重新通关这个关卡后分数才会重新出现在新的一周游戏积分排行榜中。
|
||||
积分排位:游戏积分排行榜需要显示出这个关卡中得分最高的前100位用户。
|
||||
输入数据:每次用户通关后,这个App都会将用户自身的ID,通关游戏的时间(也就是事件时间)还有分数以CSV格式上传到服务器中,每个用户的游戏积分数据都可以从Google Cloud Bigtable中读取出来。
|
||||
输出数据:最终这个游戏积分排行榜结果可以从一个文件中获得。也就是说,我们的Beam数据流水线需要将最终结果写入文件中。
|
||||
|
||||
|
||||
有了这些假设,我们就一起来由浅入深地看看有哪些执行方案。
|
||||
|
||||
正如上一讲中所说,如果可以用简单的方法解决战斗,我们当然要避免将问题复杂化了。一种比较直观的做法就是使用crontab定时执行一个Beam数据流水线,将每周需要进行计算排名的开始时间点和结束时间点传入数据流水线中,过滤掉所有事件时间不在这个时间范围内的数据。
|
||||
|
||||
那么,具体要怎么做呢?
|
||||
|
||||
首先,我们先要定义一个类,来保存我们之前假设好用户上传的信息。
|
||||
|
||||
Java
|
||||
|
||||
class UserScoreInfo {
|
||||
String userId;
|
||||
Double score;
|
||||
Long eventTimestamp;
|
||||
|
||||
public UserScoreInfo(String userId, Double score, Long eventTimestamp) {
|
||||
this.userId = userId;
|
||||
this.score = score;
|
||||
this.eventTimestamp = eventTimestamp;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public Double getScore() {
|
||||
return this.score;
|
||||
}
|
||||
|
||||
public Long getEventTimestamp() {
|
||||
return this.eventTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
这个类十分简单,构造函数需要传入的是用户ID、游戏通关时的积分还有通关时间。
|
||||
|
||||
有了这个类之后,整个数据流水线的逻辑就可以围绕着这个类来处理,步骤大致如下:
|
||||
|
||||
|
||||
从Google Cloud Bigtable中读取保存用户通关积分等信息的所有Bigtable Row出来,得到PCollection 。
|
||||
将PCollection 转换成我们定义好的类,成为PCollection 。
|
||||
根据我们传入的开始边界时间和结束边界时间过滤掉不属于这一周里数据,得到有效时间内的PCollection 。
|
||||
将PCollection转换成PCollection>,KV里面的Key就用户ID。
|
||||
自定义一个Composite Transform,其中包括三个步骤:利用Top Transform将每一个用户的最高分率选出来,得到PCollection>;将PCollection>转换成为PCollection>;再次利用Top Transform将PCollection>中前100名高分用户筛选出来。
|
||||
将结果写入CSV格式的文件中,格式为“用户ID,分数”。
|
||||
|
||||
|
||||
在上面所描述的步骤中,第5步出现了一个叫Composite Transform的概念。
|
||||
|
||||
那么,什么是Composite Transform呢?其实Composite Transform并不是指一个具体的Transform,而是指我们可以将多个不同的Transforms嵌套进一个类中,使得数据流水线更加模块化。具体做法是继承PTransform这个类,并且实现expand抽象方法来实现的。
|
||||
|
||||
用我们实现过的WordsCount来举例,我们可以将整个WordsCount数据流水线模块化成一个Composite Transform,示例如下:
|
||||
|
||||
Java
|
||||
|
||||
public static class WordsCount extends PTransform<PCollection<String>,
|
||||
PCollection<KV<String, Long>>> {
|
||||
@Override
|
||||
public PCollection<KV<String, Long>> expand(PCollection<String> lines) {
|
||||
|
||||
PCollection<String> words = lines.apply(
|
||||
ParDo.of(new ExtractWordsFn()));
|
||||
|
||||
PCollection<KV<String, Long>> wordsCount =
|
||||
words.apply(Count.<String>perElement());
|
||||
|
||||
return wordsCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
在上面这个例子中,输入的参数是每一行字符串PCollection ,输出结果是每一个单词对应出现的次数PCollection。在实现expand这个抽象方法的时候,里面总共嵌套了两个不同的Transform,分别是一个ParDo用来提取每一行的单词,还有一个Count Transform统计单词出现的次数。
|
||||
|
||||
所以在第5步中,我们也可以自己定义一个ExtractUserAndScore的Composite Transform来实现上面所描述的多个不同的Transforms。
|
||||
|
||||
好了,为了事先知道游戏积分排行榜中开始的边界时间和结束的边界时间,我们还需要自己实现一个Options接口。方法是继承PipelineOptions这个接口,具体如下所示:
|
||||
|
||||
Java
|
||||
|
||||
public interface Options extends PipelineOptions {
|
||||
@Default.String("1970-01-01-00-00")
|
||||
String getStartBoundary();
|
||||
|
||||
void setStartBoundary(String value);
|
||||
|
||||
@Default.String("2100-01-01-00-00")
|
||||
String getEndBoundary();
|
||||
|
||||
void setEndBoundary(String value);
|
||||
}
|
||||
|
||||
|
||||
这样开始的边界时间和结束的边界时间就都可以通过Pipeline option的参数传入。
|
||||
|
||||
例如,我们想要得到2019年7月15日至2019年7月21日这周的排行榜,那在运行数据流水线的时候,参数就可以按照“–startBoundary=2019-07-15-00-00 –etartBoundary=2019-07-21-00-00”传入了。
|
||||
|
||||
整个数据流水线的大致逻辑如下:
|
||||
|
||||
Java
|
||||
|
||||
final class LeaderBoard {
|
||||
static class UserScoreInfo {
|
||||
String userId;
|
||||
Double score;
|
||||
Long eventTimestamp;
|
||||
|
||||
public UserScoreInfo(String userId, Double score, Long eventTimestamp) {
|
||||
this.userId = userId;
|
||||
this.score = score;
|
||||
this.eventTimestamp = eventTimestamp;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public Double getScore() {
|
||||
return this.score;
|
||||
}
|
||||
|
||||
public Long getEventTimestamp() {
|
||||
return this.eventTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTimeFormatter formatter =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd-HH-mm")
|
||||
.withZone(DateTimeZone.forTimeZone(TimeZone.getTimeZone("Asia/Shanghai")));
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
|
||||
Pipeline pipeline = Pipeline.create(options);
|
||||
|
||||
final Instant startBoundary = new Instant(formatter.parseMillis(options.getStartBoundary()));
|
||||
final Instant endBoundary = new Instant(formatter.parseMillis(options.getEndBoundary()));
|
||||
|
||||
pipeline
|
||||
.apply(
|
||||
BigtableIO.read()
|
||||
.withProjectId(projectId)
|
||||
.withInstanceId(instanceId)
|
||||
.withTableId("ScoreTable"))
|
||||
.apply("ConvertUserScoreInfo", ParDo.of(new ConvertUserScoreInfoFn()))
|
||||
.apply(
|
||||
"FilterStartTime",
|
||||
Filter.by((UserScoreInfo info) -> info.getTimestamp() > startBoundary.getMillis()))
|
||||
.apply(
|
||||
"FilterEndTime",
|
||||
Filter.by((UserScoreInfo info) -> info.getTimestamp() < endBoundary.getMillis()))
|
||||
.apply("RetrieveTop100Players", new ExtractUserAndScore())
|
||||
.apply(
|
||||
FileIO.<List<String>>write()
|
||||
.via(
|
||||
new CSVSink(Arrays.asList("userId", "score"))
|
||||
.to("filepath")
|
||||
.withPrefix("scoreboard")
|
||||
.withSuffix(".csv")));
|
||||
|
||||
pipeline.run().waitUntilFinish();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
其中,ConvertUserScoreInfoFn这个Transform代表着第2步转换操作,数据流水线中两个Filter Transform分别代表着第3和第4步。第5步“获得最高分的前100位用户”是由ExtractUserAndScore这个Composite Transform来完成的。
|
||||
|
||||
你可以看到,不算上各种具体Transform的实现,整个数据流水线的逻辑框架大概用60行代码就可以表示出来。
|
||||
|
||||
虽然这个批处理的方法可以用简单的逻辑得到最后我们想要的结果,不过其实它还存在着不少的不足之处。
|
||||
|
||||
因为我们的批处理数据流水线使用crontab来定时运行,所以“运行数据流水线的时间间隔”以及“完成数据流水线”这之间的时间之和会给最终结果带来延迟。
|
||||
|
||||
比如,我们定义crontab每隔30分钟来运行一次数据流水线,这个数据流水线大概需要5分钟完成,那在这35分钟期间用户上传到服务器的分数是无法反应到积分排行榜中的。
|
||||
|
||||
那么,有没有能够缩小延时的办法呢?
|
||||
|
||||
当然有,答案就是将输入数据作为无边界数据集读取进来,进行实时的数据处理。在这里面我们会运用的到第23讲所讲述到的窗口(Window)、触发器(Trigger)和累加模式(Accumulation)的概念。
|
||||
|
||||
我将在下一讲中,与你具体分析怎样运用Beam的数据流水线实现一个实时输出的游戏积分排行榜。
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起展开讨论了自己实现一个简易游戏积分排行榜的过程。可以知道的是,我们可以使用Beam的数据流水线来完成这一任务。而在Beam数据流水线的实现方式中,我们又可以分成批处理的实现方式和即将在下一讲中展开讨论的实时流处理的方式。批处理虽然简单,但是存在着延时性高、无法快速更新积分排行榜的缺点。
|
||||
|
||||
思考题
|
||||
|
||||
在今天这一讲的最后,我提示了你在实时流处理中需要用到窗口、触发器和累加模式。那我们就先来做个预热,思考一下,在流处理中你会对这三种概念赋予什么值呢?
|
||||
|
||||
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
158
专栏/大规模数据处理实战/36Facebook游戏实时流处理BeamPipeline实战(下).md
Normal file
158
专栏/大规模数据处理实战/36Facebook游戏实时流处理BeamPipeline实战(下).md
Normal file
@@ -0,0 +1,158 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
36 Facebook游戏实时流处理Beam Pipeline实战(下)
|
||||
你好,我是蔡元楠。
|
||||
|
||||
在上一讲中,我们一起对怎样实现一个简易的游戏积分排行榜展开了讨论,也一起研究了如何使用批处理计算的方式在Beam中构建出一个数据流水线来得出排行榜结果。
|
||||
|
||||
我们知道,虽然批处理计算可以得到一个完整的结果,但是它也存在着自身的不足,比如会有一定的延时,需要额外的crontab来管理定时任务,增加了维护成本等等。
|
||||
|
||||
所以在上一讲的末尾,我们提出了使用实时流处理来改进这些不足,而其中就需要用到窗口、触发器和累加模式这几个概念。
|
||||
|
||||
相信学习了第32讲的内容后,你对于窗口在Beam中是如何运作的,已经比较了解了。对于有效时间为一周的积分排行榜来说,我们可以赋予一个“窗口时长为一周的固定窗口”给数据流水线。也就是说,我们最终的结果会按照每一周的时长来得出。
|
||||
|
||||
那接下来的问题就剩下我们怎么定义触发器和累加模式了。
|
||||
|
||||
首先,我想先讲讲触发器在Beam中是怎么运作的。在第23讲中,我们已经了解了触发器在Beam中的作用。它是用于告诉数据流水线,什么时候需要计算一遍落在窗口中的所有数据的。这在实时流处理中尤为重要。
|
||||
|
||||
在实时流处理当中,我们总是需要在数据结果的完整性和延迟性上做出一些取舍。
|
||||
|
||||
如果我们设置的触发器比较频繁,例如说每隔几分钟甚至是几秒钟,或者是在时间上很早发生的话,那就表示我们更倾向于数据流水线的延时比较小,但是不一定能够获得完整的数据。
|
||||
|
||||
如果我们设置的触发器是比较长时间的,像每隔一个小时才会触发一次窗口中的计算的话,那就表示我们更希望获得完整的数据集来得到最终结果。
|
||||
|
||||
为什么这么说呢?
|
||||
|
||||
因为在现实世界中,我们是没有办法保证数据流水线可以在某一刻能够得到在这一刻之前所产生的所有数据的。
|
||||
|
||||
就拿获得这个游戏积分排行榜的数据为例子来说明一下。
|
||||
|
||||
在现实生活中,可能很多用户是用手机来通关游戏,并且上传通关时间和积分的。有的地方可能因为信号差,上传数据会有很大的延迟。甚至可能有这样的情况:有一些用户是在坐飞机的时候玩的游戏(在飞行模式之下完成各种通关),等到飞机降落,手机重新有了信号之后,数据才被上传到服务器,这时候可能已经有了好几个小时的延时了。
|
||||
|
||||
如果提早触发窗口中的数据计算,可能会有很多“迟到”的数据未被纳入最终结果中,而这些“迟到”的数据有可能又会影响到游戏积分排行榜。
|
||||
|
||||
所以,在很多复杂的场景下,我们希望尽可能地将所有数据归入正确的时间窗口中,而且还要能够得到正确的结果。因此,除了要对触发器进行设置之外,我们还需要设置到底应不应该在一些“迟到”的数据来到的时候,重新计算一下整个结果。
|
||||
|
||||
在Beam中,我们可以为PCollection设置的触发器有4种模式:
|
||||
|
||||
1.基于事件时间的触发器(Event-Time Trigger)
|
||||
|
||||
如果设置了基于事件时间的触发器,那所有的计算都是基于PCollection中所有元素的事件时间的。
|
||||
|
||||
如果我们不显式地设置触发器的话,Beam的默认触发器就是基于事件时间的,如果要显式地设置基于事件时间的触发器,可以使用AfterWatermark类进行设置。
|
||||
|
||||
2.基于处理时间触发器(Process-Time Trigger)
|
||||
|
||||
如果设置了基于处理时间的触发器,那一个PCollection中的所有元素都会在数据流水线中的某一个时刻被处理。如果要显式地设置基于处理时间的触发器,可以使AfterProcessingTime类进行设置。
|
||||
|
||||
3.数据驱动的触发器(Data-Driven Trigger)
|
||||
|
||||
数据驱动的触发器一般是在每个元素到达每个窗口时,通过检查这个元素是否满足某个属性来触发的。
|
||||
|
||||
就好像我在第23讲所举的例子一样,检查元素是否在窗口中到达一定的数量,然后触发计算就是数据驱动的触发器的一种,在Beam中,可以使用AfterPane.elementCountAtLeast()函数来配置。
|
||||
|
||||
4.复合触发器(Composite Trigger)
|
||||
|
||||
复合触发器其实就是由上面所说三种基本触发器组合而成的。在第23讲中,我举过一个触发器的例子,例子中至少要等到有两个交易数据到达后才能触发计算。
|
||||
|
||||
有同学在留言中问我,如果现实中只有一个数据到达窗口,那岂不是永远都触发不了计算了?其实,这个时候就可以定义一个复合触发器,可以定义成累积有超过两个元素落入窗口中或者是每隔一分钟触发一次计算的复合触发器。
|
||||
|
||||
而像我之前提到的,如果我们需要处理“迟到”的数据,那在Beam中又是怎么操作呢?我们可以使用withAllowedLateness这个在Window类里定义好的函数,方法签名如下:
|
||||
|
||||
Java
|
||||
|
||||
public Window<T> withAllowedLateness(Duration allowedLateness);
|
||||
|
||||
|
||||
这个函数接收的参数就是我们希望允许多久的“迟到”数据可以被纳入计算中。
|
||||
|
||||
最后需要说明的是累加模式。
|
||||
|
||||
在Beam中,我们可以设置两种累加模式,分别是丢弃模式和累积模式。它们可以分别通过Window类里的函数discardingFiredPanes()和accumulatingFiredPanes()来设置。
|
||||
|
||||
好了,那现在回到我们的积分排行榜问题当中。
|
||||
|
||||
虽然我们对输入数据集设定的窗口是一个窗口时长为1周的固定窗口,但是我们也需要尽可能地在近乎实时的状态下更新排行榜。所以,我们可以设置数据流水线在每5分钟更新一次。
|
||||
|
||||
那我们接受“迟到”多久的数据呢?
|
||||
|
||||
我在网上查询了一下,现在飞机航班直飞耗时最长的是新加坡飞往纽约的航班,大概需要19个小时。如果玩游戏的用户恰好也在这趟航班上,那么可能数据的延时可能就会超过19个小时了。那我们就设定允许“迟到”20个小时的数据也纳入我们的窗口计算当中。
|
||||
|
||||
一般情况下,我们可以从Pub/Sub数据流中读取实时流数据。为了简化数据流水线的逻辑,不在数据流水线中保存中间状态,我们现在假设在实际操作的时候,服务器已经判断好某一用户的分数是否是最高分,如果是最高分的话,再通过Pub/Sub将数据传入流水线。
|
||||
|
||||
这时,我们的累加模式可以定义为丢弃模式,也就是只保留最新的结果。
|
||||
|
||||
为此,我们可以写出一个Transform来设置所有上述描述的概念,分别是:
|
||||
|
||||
|
||||
设置窗口时长为1周的固定窗口。
|
||||
每隔5分钟就会计算一次窗口内数据的结果。
|
||||
允许“迟到”了20个小时的数据被重新纳入窗口中计算。
|
||||
采用丢弃模式来保存最新的用户积分。
|
||||
|
||||
|
||||
Java
|
||||
|
||||
static class ConfigUserScores extends PTransform<PCollection<UserScoreInfo>, PCollection<UserScoreInfo>> {
|
||||
private final Duration FIXED_WINDOW_SIZE = Duration.standardDays(7);
|
||||
private final Duration FIVE_MINUTES = Duration.standardMinutes(5);
|
||||
private final Duration TWENTY_HOURS = Duration.standardHours(20);
|
||||
|
||||
@Override
|
||||
public PCollection<UserScoreInfo> expand(PCollection<UserScoreInfo> infos) {
|
||||
return infos.apply(
|
||||
Window.<UserScoreInfo>into(FixedWindows.of(FIXED_WINDOW_SIZE))
|
||||
.triggering(
|
||||
AfterWatermark.pastEndOfWindow()
|
||||
.withEarlyFirings(
|
||||
AfterProcessingTime.pastFirstElementInPane().plusDelayOf(FIVE_MINUTES))
|
||||
.withLateFirings(
|
||||
AfterProcessingTime.pastFirstElementInPane().plusDelayOf(FIVE_MINUTES)))
|
||||
.withAllowedLateness(TWENTY_HOURS)
|
||||
.discardingFiredPanes());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
有了这个Transform去设定好我们在实时流处理中如何处理数据之后,我们其实只需要修改之前批处理数据流水线中很小的一部分,就可以达到我们想要的结果了。
|
||||
|
||||
Java
|
||||
|
||||
...
|
||||
pipeline.apply(
|
||||
KafkaIO.<String>read()
|
||||
.withBootstrapServers("broker_1:9092,broker_2:9092")
|
||||
.withTopic("user_scores")
|
||||
.withKeyDeserializer(StringDeserializer.class)
|
||||
.withValueDeserializer(StringDeserializer.class)
|
||||
.withLogAppendTime())
|
||||
.apply("ConvertUserScoreInfo", ParDo.of(new ConvertUserScoreInfoFn()))
|
||||
.apply("ConfigUserScores", new ConfigUserScores())
|
||||
.apply("RetrieveTop100Players", new ExtractUserAndScore())
|
||||
...
|
||||
|
||||
|
||||
如代码所示,真正做出修改的地方就是将读取输入数据集的BigTableIO改成使用KafkaIO来读取。将批处理的两个Filter Transform替换成我们自定义的ConfigUserScores Transform。
|
||||
|
||||
到此为止,我们就可以“一劳永逸”,运行一个实时流处理的数据流水线来得到游戏积分排行榜结果了。
|
||||
|
||||
小结
|
||||
|
||||
今天我们一起设计了一个实时流处理的数据流水线,来完成之前自定义的一个简单游戏积分排行榜。
|
||||
|
||||
这里面有不少经验是值得我们在处理现实的应用场景中借鉴的。比如,我们应该考虑数据结果的完整性有多重要、我们能接受多大的延迟、我们是否接受晚来的数据集等等。
|
||||
|
||||
这些问题其实又回到了第23讲中提到过的——我们在解决现实问题时,应该回答好的“WWWH”这四个问题。
|
||||
|
||||
思考题
|
||||
|
||||
今天我们一起探讨了如何利用实时流处理的方式来解决游戏积分排行榜的问题,里面涉及了配置窗口,触发器和累加模式。这些配置可能还不是最优的,你觉得我们还有什么地方可以进行优化的呢?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
115
专栏/大规模数据处理实战/375G时代,如何处理超大规模物联网数据.md
Normal file
115
专栏/大规模数据处理实战/375G时代,如何处理超大规模物联网数据.md
Normal file
@@ -0,0 +1,115 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
37 5G时代,如何处理超大规模物联网数据
|
||||
你好,我是蔡元楠。
|
||||
|
||||
时间过的真快,转眼间我们已经结束了前五个模块的学习,来到了最后一个模块“大规模数据的挑战和未来”。
|
||||
|
||||
一门技术类课程的常见学习路线就是“过去→现在→未来”。这个专栏也是如此,我们首先研究了大数据处理技术的发展历程,从MapReduce出发,深入剖析了它的设计思路和优缺点。接下来结合具体的例子,一起学习了当下最流行的数据处理框架Spark和Apache Beam。
|
||||
|
||||
在这个过程中,你不难发现,任何一门技术的出现都是为了解决实际问题,改进之前的技术所存在的缺陷,而贯穿整个课程的两大场景就是批处理和流处理。
|
||||
|
||||
Spark在MapReduce的基础上不断改进,在批处理这方面有良好的性能,在流处理上也在不断提高。Apache Beam更是一个统一批处理和流处理的框架。
|
||||
|
||||
正如我在开篇词中写到的,我理想中的专栏是一份与你一同成长的计划。虽然我们已经对当下流行的技术有了深入的了解,但是作为一名架构师,你的目光一定要放长远,要时刻对未来5~10年,乃至20年的新问题和技术发展方向保持了解,不能固步自封,只满足于现状。毕竟,我们的征途是星辰大海。
|
||||
|
||||
在模块六中,我将列举三个大数据处理技术未来的方向,带你了解这些问题的挑战和难度,并学习现有的解决方案。希望通过这一模块的学习,你可以对大数据处理的未来有一些初步的认识,并强化自己学习新知识的能力。
|
||||
|
||||
什么是物联网?
|
||||
|
||||
物联网(Internet of Things)应该是一个你经常听说的名词,不过,你真的了解它吗?让我先来简要介绍一下什么是物联网吧。
|
||||
|
||||
你可以将物联网的功能看作“使用嵌入在物理环境中的网络连接设备,来改进现有流程,或启用以前无法实现的新场景”。这些设备或事物连接到网络后,可以提供它们使用传感器从环境中收集的信息,或允许其他系统通过执行器连接,并作用于现实世界。
|
||||
|
||||
|
||||
|
||||
它们可以是我们个人拥有并随身携带的设备(比如手表、眼镜),或留在家中的设备(比如电视、空调、音响等智能硬件),也可能是联网的工厂设备或机器,还可能是城市中的公共设施(比如停车场、公交车)。
|
||||
|
||||
想象一下,未来我们身边的所有物体,都有可能连入互联网,我们的生活将变得无比便捷。每个设备都能将来自现实世界的有价值信息转换为数字数据,从而有效改善人类与各类产品的交互方式。
|
||||
|
||||
物联网可以被广泛应用在生活的各方各面,比如智能家居、智能交通、智能工厂、智能医院、智能物流等。
|
||||
|
||||
|
||||
智能家居:这可能是你对物联网应用了解最多的方面。家里的各种电器,乃至防盗门窗,都可以连入物联网,我们可以通过手机或者电脑远程操控所有电器。如果有异常情况,比如着火或者小偷进入,都可以及时发现并采取措施。
|
||||
智能交通:在公路和铁路的关键点设置传感器,可以监控交通基础设施的运作状况,以及监控特殊事件,比如交通流量的变化和道路拥堵的发生。这些物联网传感器发回到总部的信息,可以用来向每辆汽车通知拥堵点,并提供备用路线。在停车场设置传感器和摄像头,也可以向每个人提供车位信息。
|
||||
智能工厂:工厂的所有机器设备都可以连入网络,我们可以通过各类传感器来获得实时的机器设备数据与性能,并把它传入控制中心。通过对这些数据进行实时处理,我们可以自动预测设备何时需要维护、实时优化设备性能、预测停机时间、检测异常、跟踪设备状况和位置。工厂的自动化程度将大大提高。
|
||||
智能医院:病人可以在身上佩戴检测身体基本指标的手环,每时每刻把身体信息发回数据处理中心,医院就可以实时了解病人的身体情况。一旦有异常情况发生,还可以自动呼叫救护车。
|
||||
智能物流:卡车配备传感器之后,可以追踪一路上的运送情况,选择最佳运送路线,追踪时间等。在有些情况下,传感器还用于追踪驾驶员的速度、刹车习惯等,数据处理终端可以选择最安全、最环保的驾驶路线。
|
||||
|
||||
|
||||
物联网的世界充分体现了大规模数据的四个特点——多样性、大规模、高速率和真实性。
|
||||
|
||||
1.多样性
|
||||
|
||||
说数据是具备多样性的,你很容易理解。这是因为物联网涉及的应用范围很广,就如我刚才提到的智能家居、智能交通、智能工厂、智能医院等。
|
||||
|
||||
从广义上讲,生活中的各方各面都可以应用物联网。而且,在不同的领域和行业,需要面对的应用数据的类型、格式也不尽相同,这些都是物联网多样性的体现。
|
||||
|
||||
2.大规模
|
||||
|
||||
之所以说物联网数据规模庞大,是因为它的节点是海量的,它不像互联网,局限于手机或者电脑。
|
||||
|
||||
想象一下,你的眼镜、手表、音响、空调、冰箱、电视……这些全部都成为了物联网的节点。而且,这些设备是24小时不间断地提供数据的,数据的生成频率远高于互联网。所以,物联网的实时数据规模是非常大的。
|
||||
|
||||
3.高速率
|
||||
|
||||
物联网中的数据速率比常见的大数据处理场景要更高。由于前面数据“大规模”的特点,物联网要求数据处理中心能处理更多的数据。同时,为了满足物联网的实时响应,数据的传输速率也要更高才行。
|
||||
|
||||
举个例子,如果速率不够高、不够实时,那么汽车的自动驾驶就会危险重重。因为它与真实物理世界直接相关,需要能实时访问、控制相应的节点和设备才能完成安全的驾驶。只有高数据传输速率才能支持它的实时性。
|
||||
|
||||
这也是为什么物联网是最近十年才发展起来的原因,十几年前的通信和网速很难达到这样的要求。
|
||||
|
||||
4.真实性
|
||||
|
||||
我们都知道,物联网的数据来源于真实世界,而且要根据数据分析处理后的结果,对真实世界中的设备发送指令采取相应的操作,最终会作用于真实世界。所以,物联网对数据真实性要求很高。
|
||||
|
||||
由此可见,在物联网的世界中,构建一个可靠的、处理速度快的大规模数据处理方案尤其重要。
|
||||
|
||||
处理物联网数据的架构
|
||||
|
||||
一个基本的物联网数据处理pipeline就如下图所示:
|
||||
|
||||
|
||||
|
||||
你可以看到,在这个pipeline中,各个设备终端不断地向数据接收层发送数据。在这一层,数据被清洗,并且转换为统一的格式,然后发送到数据分析层进行分析。在分析过后,处理过的数据可以被存储下来。基于存储的数据,我们可以创建各种dashboard来展示,这也方便管理人员直观地观察数据。
|
||||
|
||||
如果分析之后发现需要某些设备采取特定的操作,这些信息可以从数据分析层传送回设备控制层,从而向终端设备发送相应的指令。
|
||||
|
||||
各大云服务厂商都提供物联网数据处理的解决方案。
|
||||
|
||||
对于数据接收层,市场上有Google IoT Core、IoT Hub、Azure Event Hub等产品,它们可以接收各类设备发送的数据,并对它们进行管理。数据分析层就是我们进行数据处理的地方,可以用Spark、Hadoop、Azure DataBricks或者Google Cloud Dataflow等平台进行分析。数据存储层则是各类分布式存储系统如Google Cloud BigQuery、HBase、Amazon S3等。如果要基于数据创建dashboard,可以用Google Cloud Datalab等交互式分析工具。
|
||||
|
||||
以Google Cloud Platform为例,它提供的物联网数据处理基本架构如下图所示:
|
||||
|
||||
|
||||
|
||||
终端数据经过Cloud IoT Core的清洗并转换成统一的格式之后,被发送到Cloud Pub/Sub这个消息队列中,我们可以配置不同的数据分析工具来订阅Pub/Sub中的消息。
|
||||
|
||||
Cloud Functions是一个事件驱动的无服务器计算平台,利用它可以对数据进行实时处理,并无需配置服务器。Cloud DataFlow是Google Cloud提供的基于Apache Beam的批流数据统一处理平台,它可以将数据存入Big Query,还可以配置Google Cloud Machine Learning来对物联网数据进行训练,得到相应的数据模型。数据分析的结果可以传回Cloud IoT Core,通过它来对终端设备发送指令。
|
||||
|
||||
在实际应用中,物联网的数据处理场景分不同的类型。
|
||||
|
||||
有的场景数据量小、处理简单,但是对实时性要求高;有的场景数据量大,处理比较复杂,而且需要综合历史数据。
|
||||
|
||||
基于这两种分类,有人提出了“Device-Edge-Cloud”(设备-边缘-云)的架构,即把简单的、需要实时计算和分析的过程放到离终端设备更近的地方,如设备本身、网关或者服务器,以保证数据数据处理的实时性,同时也减少数据传输的风险,即我们常听说的边缘计算;把复杂的、需要存储的数据处理放在Cloud上。这样可以大大加快简单操作的分析和响应速度。
|
||||
|
||||
在上面的架构中,除了物联网设备以外的部分,都部署在Google Cloud上。结合边缘设备处理的特性之后,Google Cloud的物联网数据处理架构就如下图所示:
|
||||
|
||||
|
||||
|
||||
小结
|
||||
|
||||
物联网是当今大规模数据处理的一大热点。今天我们初步了解了物联网的应用场景,产生数据的特性,以及基本的物联网数据处理架构,并以Google Cloud Platform为例,带你一起了解了一个成熟的物联网云服务平台都有怎样的特性。你可以去看看其他的云服务厂商所提供的物联网数据处理平台,比如微软的Azure IoT Hub,比较一下它们的异同。
|
||||
|
||||
思考题
|
||||
|
||||
都说在5G时代,边缘计算是一个非常重要的技术。你能去了解一下边缘计算,然后告诉我为什么可以这么说吗?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
129
专栏/大规模数据处理实战/38大规模数据处理在深度学习中如何应用?.md
Normal file
129
专栏/大规模数据处理实战/38大规模数据处理在深度学习中如何应用?.md
Normal file
@@ -0,0 +1,129 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
38 大规模数据处理在深度学习中如何应用?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“大规模数据处理在深度学习中如何应用?”。
|
||||
|
||||
“深度学习”这个词,既是一个人工智能的研究领域,也概括了构建人工神经网络的技术方法。2012年的AlexNet,2015年的Google Inception V3级数式地打破ImageNet计算机视觉比赛的最高纪录,2017年亮相的AlphaGo更是掀起了全球的深度学习风暴。
|
||||
|
||||
在Google,深度学习系统被应用在预测广告的点击率、推荐用户可能喜爱的视频、生成更接近人类的机器发声、自动生成邮件回复等几乎所有产品线。
|
||||
|
||||
|
||||
|
||||
很多人并不理解深度学习技术,我经常见到这样几种误区:
|
||||
|
||||
|
||||
觉得深度学习是最近几年才兴起的技术;
|
||||
觉得深度学习只是一个技术时髦(就像今年流行Python,明年流行Go语言一样);
|
||||
觉得深度学习只是算法模型。
|
||||
|
||||
|
||||
要打破这些误区,我们必须深刻地理解超大规模数据在深度学习的发展中到底扮演了一个怎样的角色。
|
||||
|
||||
大规模数据在深度学习发展中扮演的角色
|
||||
|
||||
事实上,类似于模拟神经网络的计算机方法早在20世纪60年代就被提出来了。
|
||||
|
||||
当时通信领域大神香农也在神经网络领域有所涉猎。但是在60年代到90年代的几十年间,深度学习虽然想法新颖、听起来很好,但是在实际上,人们发现以当时的计算能力根本没法训练神经网络。反而是像决策树,SVM等非神经网络的方法大放异彩。
|
||||
|
||||
所以,从20世纪下半叶到2010年代究竟是什么让深度学习成了世界的焦点呢?一根火柴是点不着的,只有把一根火柴扔进汽油罐里里才会爆炸。想要知道这个答案,我们需要结合技术发展的背景来看最近的十年有哪些改变?
|
||||
|
||||
芯片技术处在摩尔定律的末期,几乎每两年翻一番,云计算服务的兴起使得强大的计算能力变得容易获得。互联网的快速发展和以2007年发布iPhone为标志的移动互联网时代的到来,使得互联网用户的数量和使用时长都翻了好几倍,科技公司因此积累了比以往多得多的数据。
|
||||
|
||||
讲到这里,你肯定明白了,正是因为强大的计算能力和大规模数据突然变得可获得,人们一下子发现曾经遥不可及的神经网络方法真的可以被计算了,才引发了深度学习的爆发性发展。
|
||||
|
||||
即使是现在也是如此,在数据量并不充足的人工智能任务上,人们会发现还是传统方法表现更好,然而一旦数据量上来了,深度学习就会碾压式地击败所有传统方法。
|
||||
|
||||
理解了大规模数据在深度学习发展中扮演的主要角色,我们再来看看为什么说,以大规模数据驱动的深度学习将是一次不可逆的影响深远的技术变革。
|
||||
|
||||
这就得从最近十年哪些“没有变”说起。
|
||||
|
||||
我们看到,计算机的发展从有一个房子那么大的巨型机,到个人电脑,再到智能手机。表面上看,计算机体积变得更小了,然而实际上是人们想要计算更个性化、更私密的需求没有改变。而只有深度学习才能满足更个性的计算需求,无论是给你推荐你喜欢的音乐,还是分析你的健康记录。
|
||||
|
||||
工业生产的发展从人力,到蒸汽机,再到电能和计算机。人们想要解放繁重的重复劳动的需求从来没有改变。而只有深度学习才能满足下一波劳动力的解放,也就是重复的脑力劳动。这些影响力都是别的技术时髦远远不能相比的(比如今年流行这个前端框架,明年流行那个前端框架,今年流行这个语言,明年流行那个语言)。我们看一个技术的影响力,就是看这个技术能够解决哪些曾经不能解决的问题。而深度学习技术所能解决的新问题,几乎涵盖了人类社会发展的各个方面。
|
||||
|
||||
在了解了深度学习的巨大影响力和大规模数据在深度学习技术中的重要角色后,我们结合案例来具体看看,在一个由深度学习驱动的技术产品或者技术系统周期中,大规模数据处理技术是怎样被应用的呢?
|
||||
|
||||
最后你会发现,大规模数据处理技术几乎无处不在。你甚至可能会感叹,深度学习系统实际上就是一个复杂的大规模数据处理系统。事不宜迟,我们现在就来看看。
|
||||
|
||||
一个深度学习驱动的产品周期一般按时间顺序分为这样几个阶段:
|
||||
|
||||
|
||||
数据搜集整理;
|
||||
深度学习模型开发;
|
||||
部署和测试深度学习模型;
|
||||
形成数据闭环反馈不断优化深度学习模型。
|
||||
|
||||
|
||||
数据搜集整理 (Data Curation)
|
||||
|
||||
数据搜集整理就是针对你需要训练的深度学习问题收集所需要的数据。
|
||||
|
||||
比如,你要研发在线广告点击率的预测模型,你可能需要搜集用户的网页点击行为历史,网页链接的属性等数据。
|
||||
|
||||
如果你要研发之前提到的美团股价预测模型,你可能需要去搜集街上的美团外卖电动车图片。这种数据搜集整理的任务时间跨度可能很大,也可能涉及很多非技术的因素,比如需要去和合作公司谈判数据授权等等。
|
||||
|
||||
数据的搜集整理是任何AI系统开发的第一步,可以说没有数据就没有AI。
|
||||
|
||||
要注意,并不是只有监督学习需要高质量的数据,实际上无监督学习也需要高质量的数据。比如,在自然语言理解的无监督预训练步骤,你也需要根据训练任务选择高质量的文本库,比如中文文本库,或者医学文本库(如果你要针对医学病例训练模型)。
|
||||
|
||||
抛开这些非技术因素不谈,数据搜集整理的技术复杂度也是非常高。我们往往用data massage——给数据按摩,来形容数据搜集整理技术工作是一份并不容易的,十分需要技巧和力量的工作。
|
||||
|
||||
因为你的数据来源会非常多,每个数据源的格式可能都不一样,不同数据源提供的数据种类也会有不同,数据源直接甚至可能会相互矛盾。在实际应用中,数据搜集整理的技术部分经常是由很多个大规模数据处理流水线组成。
|
||||
|
||||
|
||||
|
||||
在我们在第1讲中介绍的美团外卖电动车搜集案例中,我们就介绍了数据搜集系统的复杂度。的确,一般来讲,你至少需要多个数据处理流水线去完成以下几项任务:
|
||||
|
||||
|
||||
消化外面的数据;
|
||||
对数据进行各种转换,变成你想要的结构和格式;
|
||||
清理数据,比如不准确的数据要找出来,送给人工单独审核和处理;
|
||||
如果由人工审核,你还需要数据处理流水线能够处理人工审核结果。
|
||||
|
||||
|
||||
这些数据处理的流水线可能用一整个专栏的篇幅都无法罗列完。你看在计算机视觉领域鼎鼎有名的ImageNet,到现在已经花了10多年整理收集,才取得现在这样非常干净、丰富、准确的状态。你就能明白,数据搜集整理本质上就是大规模的训练样本数据处理。
|
||||
|
||||
深度学习模型开发 (Modeling)
|
||||
|
||||
看到这,你可能会觉得:深度学习的模型开发阶段是不是总算没有数据处理什么事了?看起来都是算法啊,数学啊?完全不是的。
|
||||
|
||||
当我们在实验深度学习模型时候,许多时间都花在了数据处理上。经常要做的事情是,先去分析一下拿到手上的样本数据。
|
||||
|
||||
比如,在使用皮肤的照片分类良性的痣还是黑色素瘤时,肯定会先去看一下样本中良性和恶性的分布比例。如果分布很不均匀,比如黑色素瘤的样本只有1%时,很容易影响我们模型的表现。那就需要考虑别的方法,比如使用不同的损耗函数,过采样较少的分类样本等等。
|
||||
|
||||
除了数据分析以外,许多深度学习模型的架构设计都需要大规模的数据处理能力。比如,在hard negative mining技术中,我们需要使用数据处理技术把检查点的深度模型在数据集上都跑一遍,来找出值得加入训练集的样本。
|
||||
|
||||
部署和测试深度学习模型 (Deployment)
|
||||
|
||||
部署和测试深度学习模型,其实就是把模型工程化为一个提供结果预测的服务。这样的服务,本质上就是一个数据处理的流水线,它可以是批处理流水线,也可以是流处理流水线。
|
||||
|
||||
比如,我们部署广告点击率预测模型时,最终我们肯定是根据用户的浏览场景选择点击率高的广告,这样才能去展示点击率高和用户的网页浏览最相关的广告。然而可供选择的候选广告可能有几千万个。如何快速地把所有这样几千万个候选广告的点击率全部预测一遍,这就是一个大规模数据的批处理问题。
|
||||
|
||||
具体提供深度学习模型推理服务的可能是TensorFlow Servo。但是真正用在生产环境时,我们都需要把Servo封装成一个大规模数据处理系统。
|
||||
|
||||
形成数据闭环反馈不断优化深度学习模型(Feedback and improvement)
|
||||
|
||||
你的深度学习产品上线后,依然需要大规模数据处理技术。比如你提供自动聊天回复功能,能够根据一个微信对话的上下文自动推荐一些合适的回复。如果别人说“明天一起吃饭好不好?”,你可以推荐回复“好”。
|
||||
|
||||
那么,像这样的功能上线后,你怎样评估这个深度学习模型的效果呢?你需要去跟踪用户与这个功能的交互。
|
||||
|
||||
比如,有多少用户会去选择你推荐的回复,又有多少用户会不选择你推荐的回复而自己打字呢?通过这些追踪的用户行为,你就能利用大规模的数据处理技术,不断地为你的深度学习模型提供更多现实的数据,去进一步训练改进模型。也能利用用户行为去评估当前模型的表现。
|
||||
|
||||
小结
|
||||
|
||||
这一讲,我们首先学习了大规模数据在深度学习几十年的发展中所扮演的至关重要的角色。然后我们一起看了在一个深度学习驱动的产品周期里的各个阶段,有哪些大规模数据处理的任务。相信通过这一讲,你一定能感受到大规模的数据处理技术就是深度学习系统的奠基石。
|
||||
|
||||
思考题
|
||||
|
||||
除了这一讲中提到的这些应用,你还能想到哪些大规模数据处理技术在深度学习系统中的应用呢?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
137
专栏/大规模数据处理实战/39从SQL到StreamingSQL:突破静态数据查询的次元.md
Normal file
137
专栏/大规模数据处理实战/39从SQL到StreamingSQL:突破静态数据查询的次元.md
Normal file
@@ -0,0 +1,137 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
39 从SQL到Streaming SQL:突破静态数据查询的次元
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要与你分享的主题是“从SQL到Streaming SQL:突破静态数据查询的次元”。
|
||||
|
||||
在前面的章节中,我们介绍了一些流数据处理相关的知识和技术,比如Apache Spark的流处理模块——Spark Streaming和Structured Streaming,以及Apache Beam中的窗口处理。相信你对流处理的重要性和一些基本手段都有所了解了。
|
||||
|
||||
流处理之所以重要,是因为现在是个数据爆炸的时代,大部分数据源是每时每刻都在更新的,数据处理系统对时效性的要求都很高。作为当代和未来的数据处理架构师,我们势必要深刻掌握流数据处理的技能。
|
||||
|
||||
“批”“流”两手抓,两手都要硬。
|
||||
|
||||
你还记得,我在第15讲中介绍过的Spark SQL吗?它最大的优点就是DataFrame/DataSet是高级API,提供类似于SQL的query接口,方便熟悉关系型数据库的开发人员使用。
|
||||
|
||||
当说到批处理的时候,我们第一个想到的工具就是SQL,因为基本上每个数据从业者都懂,而且它的语法简单易懂,方便使用。那么,你也能很自然地联想到,如果在流处理的世界中也可以用SQL,或者相似的语言,那真是太棒了。
|
||||
|
||||
这样的思想在第17讲中我们曾经提到过。
|
||||
|
||||
Spark的Structured Streaming就是用支持类SQL的DataFrame API去做流处理的。支持用类似于SQL处理流数据的平台还有很多,比如Flink、Storm等,但它们是把SQL做成API和其他的处理逻辑结合在一起,并没有把它单独分离成一种语言,为它定义语法。
|
||||
|
||||
那么,到底有没有类似SQL语法来对流数据进行查询的语言呢?答案是肯定的。我们把这种语言统称为Streaming SQL。Siddhi Streaming SQL和Kafka KSQL就是两个典型的Streaming SQL语言,下文的例子我们主要用这两种语言来描述。
|
||||
|
||||
不同于SQL,Streaming SQL并没有统一的语法规范,不同平台提供的Streaming SQL语法都有所不同。而且Streaming SQL作用的数据对象也不是有界的数据表,而是无边界的数据流,你可以把它设想为一个底部一直在增加新数据的表。
|
||||
|
||||
SQL是一个强大的、对有结构数据进行查询的语言,它提供几个独立的操作,如数据映射(SELECT)、数据过滤(WHERE)、数据聚合(GROUP BY)和数据联结(JOIN)。将这些基本操作组合起来,可以实现很多复杂的查询。
|
||||
|
||||
在Streaming SQL中,数据映射和数据过滤显然都是必备而且很容易理解的。数据映射就是从流中取出数据的一部分属性,并作为一个新的流输出,它定义了输出流的格式。数据过滤就是根据数据的某些属性,挑选出符合条件的。
|
||||
|
||||
让我们来看一个简单的例子吧。假设,有一个锅炉温度的数据流BoilerStream,它包含的每个数据都有一个ID和一个摄氏温度(t),我们要拿出所有高于350摄氏度的数据,并且把温度转换为华氏度。
|
||||
|
||||
Select id, t*7/5 + 32 as tF from BoilerStream[t > 350]; //Siddhi Streaming SQL
|
||||
|
||||
Select id, t*7/5 + 32 as tF from BoilerStream Where t > 350; //Kafka KSQL
|
||||
|
||||
|
||||
你可以看出,这两种语言与SQL都极为类似,相信你都可以直接看懂它的意思。
|
||||
|
||||
Streaming SQL允许我们用类似于SQL的命令形式去处理无边界的流数据,它有如下几个优点:
|
||||
|
||||
|
||||
简单易学,使用方便:SQL可以说是流传最广泛的数据处理语言,对大部分人来说,Streaming SQL的学习成本很低。
|
||||
效率高,速度快:SQL问世这么久,它的执行引擎已经被优化过很多次,很多SQL的优化准则被直接借鉴到Streaming SQL的执行引擎上。
|
||||
代码简洁,而且涵盖了大部分常用的数据操作。
|
||||
|
||||
|
||||
除了上面提到过的数据映射和数据过滤,Streaming SQL的GROUP BY也和SQL中的用法类似。接下来,让我们一起了解Streaming SQL的其他重要操作:窗口(Window)、联结(Join)和模式(Pattern)。
|
||||
|
||||
窗口
|
||||
|
||||
在之前Spark和Beam的流处理章节中,我们都学习过窗口的概念。所谓窗口,就是把流中的数据按照时间戳划分成一个个有限的集合。在此之上,我们可以统计各种聚合属性如平均值等。
|
||||
|
||||
在现实世界中,大多数场景下我们只需要关心特定窗口,而不需要研究全局窗口内的所有数据,这是由数据的时效性决定的。
|
||||
|
||||
应用最广的窗口类型是以当前时间为结束的滑动窗口,比如“最近5小时内的车流量“,或“最近50个成交的商品”。
|
||||
|
||||
所有的Streaming SQL语法都支持声明窗口,让我们看下面的例子:
|
||||
|
||||
Select bid, avg(t) as T From BoilerStream#window.length(10) insert into BoilerStreamMovingAveage; // Siddhi Streaming SQL
|
||||
|
||||
Select bid, avg(t) as T From BoilerStream WINDOW HOPPING (SIZE 10, ADVANCE BY 1); // Kafka KSQL
|
||||
|
||||
|
||||
这个例子中,我们每接收到一个数据,就生成最近10个温度的平均值,插入到一个新的流中。
|
||||
|
||||
在Beam Window中,我们介绍过固定窗口和滑动窗口的区别,而每种窗口都可以是基于时间或数量的,所以就有4种组合:
|
||||
|
||||
|
||||
滑动时间窗口:统计最近时间段T内的所有数据,每当经过某个时间段都会被触发一次。
|
||||
固定时间窗口:统计最近时间段T内的所有数据,每当经过T都会被触发一次
|
||||
滑动长度窗口:统计最近N个数据,每当接收到一个(或多个)数据都会被触发一次。
|
||||
固定长度窗口:统计最近N个数据,每当接收到N个数据都会被触发一次。
|
||||
|
||||
|
||||
再度细化,基于时间的窗口都可以选择不同的时间特性,例如处理时间和事件时间等。此外,还有会话(Session)窗口等针对其他场景的窗口。
|
||||
|
||||
联结
|
||||
|
||||
当我们要把两个不同流中的数据通过某个属性连接起来时,就要用到Join。
|
||||
|
||||
由于在任一个时刻,流数据都不是完整的,第一个流中后面还没到的数据有可能要和第二个流中已经有的数据Join起来再输出。所以,对流的Join一般要对至少一个流附加窗口,这也和第20讲中提到的数据水印类似。
|
||||
|
||||
让我们来看一个例子,流TempStream里的数据代表传感器测量的每个房间的温度,每分钟更新一次;流RegulatorStream里的数据代表每个房间空调的开关状态。我们想要得到所有温度高于30度但是空调没打开的的房间,从而把它们的空调打开降温:
|
||||
|
||||
from TempStream[temp > 30.0]#window.time(1 min) as T
|
||||
join RegulatorStream[isOn == false]#window.length(1) as R
|
||||
on T.roomNo == R.roomNo
|
||||
select T.roomNo, R.deviceID, 'start' as action
|
||||
insert into RegulatorActionStream; // Siddhi Streaming SQL
|
||||
|
||||
|
||||
在上面的代码中,我们首先对TempStream流施加了一个长度为1分钟的时间窗口,这是因为温度每分钟会更新一次,上一分钟的数据已然失效;然后把它和流RegulatorStream中的数据根据房间ID连接起来,并且只选出了大于30度的房间和关闭的空调,插入新的流RegulatorActionStream中,告诉你这些空调需要被打开。
|
||||
|
||||
模式
|
||||
|
||||
通过上面的介绍我们可以看出,Streaming SQL的数据模型继承自SQL的关系数据模型,唯一的不同就是每个数据都有一个时间戳,并且每个数据都是假设在这个特定的时间戳才被接收。
|
||||
|
||||
那么我们很自然地就会想研究这些数据的顺序,比如,事件A是不是发生在事件B之后?
|
||||
|
||||
这类先后顺序的问题在日常生活中很普遍。比如,炒股时,我们会计算某只股票有没有在过去20分钟内涨/跌超过20%;规划路线时,我们会看过去1小时内某段路的车流量有没有在下降。
|
||||
|
||||
这里你不难看出,我们其实是在检测某个模式有没有在特定的时间段内发生。
|
||||
|
||||
股票价格涨20%是一个模式,车流量下降也是一个模式。在流数据处理中,检测模式是一类重要的问题,我们会经常需要通过对最近数据的研究去总结发展的趋势,从而“预测”未来。
|
||||
|
||||
在Siddhi Streaming SQL中,“->”这个操作符用于声明发生的先后关系。一起来看下面这个简单例子:
|
||||
|
||||
from every( e1=TempStream ) -> e2=TempStream[ e1.roomNo == roomNo and (e1.temp + 5) <= temp ]
|
||||
within 10 min
|
||||
select e1.roomNo, e1.temp as initialTemp, e2.temp as finalTemp
|
||||
insert into AlertStream;
|
||||
|
||||
|
||||
这个query检测的模式是10分钟内房间温度上升超过5度。对于每一个接收到的温度信号,把它和之前10分钟内收到的温度信号进行匹配。如果房间号码相同,并且温度上升超过5度,就插入到输出流。
|
||||
|
||||
很多流处理平台同样支持模式匹配,比如Apache Flink就有专门的Pattern API,我建议你去了解一下。
|
||||
|
||||
小结
|
||||
|
||||
今天我们初步了解了Streaming SQL语言的基本功能。
|
||||
|
||||
虽然没有统一的语法规范,但是各个Streaming SQL语言都支持相似的操作符,如数据映射、数据过滤、联结、窗口和模式等,大部分操作符都是继承自SQL,只有模式是独有的,这是由于流数据天生具有的时间性所导致。
|
||||
|
||||
Streaming SQL大大降低了开发人员实现流处理的难度,让流处理变得就像写SQL查询语句一样简单。它现在还在高速发展,相信未来会变得越来越普遍。
|
||||
|
||||
思考题
|
||||
|
||||
你觉得Streaming SQL的发展前景如何?欢迎留言与我一起讨论。
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
97
专栏/大规模数据处理实战/40大规模数据处理未来之路.md
Normal file
97
专栏/大规模数据处理实战/40大规模数据处理未来之路.md
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
40 大规模数据处理未来之路
|
||||
你好,我是蔡元楠。
|
||||
|
||||
今天我要分享的内容是“大规模数据处理实战”专栏的最后一讲。
|
||||
|
||||
我相信通过整个专栏的系统学习,你已经掌握了大规模数据处理的基础概念与设计模式。同时,我也相信,专栏中对现实世界中常见的大规模数据处理架构的深入探讨,可以在解决现实难题时为你提供一些思路。
|
||||
|
||||
但我更希望的是,通过模块六中对大规模数据处理在未来的应用与展望讲解,让你吃下一颗定心丸,那就是,大规模数据处理技术是在放眼未来的几十年中都依然会是炙手可热的一项技术,不会被淘汰。
|
||||
|
||||
你不难发现,我在专栏的后半部分,花了不少的篇幅来专门介绍Apache Beam的各种概念、底层思想以及实际应用的。我个人是十分认同Google所推崇的Dataflow Model的计算模型,也相信未来Apache Beam的发展前景是很好的。
|
||||
|
||||
所以在专栏的最后一讲,我想和你讲讲我对数据处理框架和对Beam的一些看法和展望。
|
||||
|
||||
技术迭代带来的烦恼
|
||||
|
||||
在专栏的后半部分,我们不断深入探讨了Apache Beam。有同学曾经在留言中提过一个问题:“我已经掌握好Spark了,也觉得Spark的语法更简练,为什么还需要学习Beam呢?”
|
||||
|
||||
对于这个问题,我相信在你刚刚接触Beam的时候,多多少少都会有相同的疑问。
|
||||
|
||||
我来给你举个例子,带你穿越时间,去看看一个常见的大规模数据处理框架在面临迁移时会遇到的烦恼。
|
||||
|
||||
在2006年,Hadoop刚刚发布,在选择数据处理框架的时候,你的首选肯定是Apache Hadoop。当时Hadoop在开源社区横空出世,让所有工程师们都可以利用这个框架来处理自己的数据,尤其是利用Hadoop自带的MapReduce计算模型,可以让整个数据处理的效率有质的飞跃。
|
||||
|
||||
而在2009年,Spark这个计算框架在加州伯克利大学的AMPLab实验室中诞生。2010年,Spark正式开源了。“Spark的数据处理效率远在Hadoop之上”的结论经过了业界的验证。2014年,Spark更是成为了Apache的顶级项目。
|
||||
|
||||
如果你是在这个时候进入IT行业的工程师,更加可能选择直接学习Spark,而只会把少量时间放在Hadoop的学习上。在2014年进行创业的小公司同样可能也会直接使用Spark来进行底层数据处理基础设施的搭建。
|
||||
|
||||
那之前已经花了很多时间在搭建Hadoop MapReduce作为公司基础设施的公司,或者是已经很深入学习了Hadoop的工程师这个时候该怎么办呢?
|
||||
|
||||
一种做法是放弃现有的技术框架,重新花费大量时间去学习新的数据处理框架。这太累了。对于工程师来说,平时本来就有着做不完的任务和业绩压力,还需要抽空学习新的技术和进行代码的迁移,这无疑让工程师们有着非常大的压力和负担。
|
||||
|
||||
当然,还有一种做法是保持现有的技术框架,不断优化现有基础设施,并且寄希望于老框架可以有新的功能发布让其性能得以提高。
|
||||
|
||||
那我们再把时间往后推一点,到了2014年,也就是Flink项目刚刚面世的时候。这时候的互联网应用场景已经有了极大的变化,批处理加上流处理同时存在的状况常常遇见。而Flink除了在处理大规模数据有着极高效率之外,它的批流统一功能也恰恰是满足了这种批流处理同时存在的场景。
|
||||
|
||||
2018年初,阿里巴巴更是看好Flink的前景,收购了Apache Flink背后的商业母公司——德国的Data Artisans。
|
||||
|
||||
讲到这里,你不难发现,当有新的技术框架出现的时候,工程师就会陷入一个选择的困难,纠结到底是抛弃原有的技术架构,还是花大量时间去做技术迁移。
|
||||
|
||||
其实,如果一开始就有Beam模型存在的话,你可能就不必有这个烦恼了。
|
||||
|
||||
因为我们完全不需要担心某一个Runner,也就是具体的数据处理技术框架过时之后所带来的技术迁移成本。如果你想要完成底层处理框架的迁移,只需要更改一些Runner的接口就可以了。
|
||||
|
||||
Apache Beam能带来什么?
|
||||
|
||||
那么,我们来看看对应用它的工程师来说,Apache Beam能带来什么?
|
||||
|
||||
因为Apache Beam是根据Dataflow Model倡导API的实现的,所以它完全能够胜任批流统一的任务。同时,因为Apache Beam有着中间的抽象转换层,工程师可以从API中解放出来,不需要学习新Runner的API语法。这也就是我说过多次的“编写一套程序就可以放在不同的Runner上面跑”。
|
||||
|
||||
除了对工程师来说能够极大地减少新技术学习的时间成本,Apache Beam还能够推动大规模数据处理领域的最新技术发展。
|
||||
|
||||
从上面我所举的例子中你可以看到,在一项新技术从诞生到流行的过程中,可能会有很多公司因为技术迁移成本太大而选择放弃采用最新的技术,这其实还影响了新技术的传播使用。因为当一部分工程师选择留守原本技术框架的时候,新技术框架也就相应地缺少了这部分的用户群体。
|
||||
|
||||
那我们来想象一下,如果所有的工程师都在使用Beam来进行项目编写的话会有什么样的效果。
|
||||
|
||||
因为有了Beam的抽象层,你可以非常轻松地更换不同的底层Runner。这就意味着我们可以先选择一个处理数据效率最高的一个RunnerA。如果有其他的Runner优化了自身,比如RunnerB,从而拥有更高效率的时候,工程师们又可以将底层Runner换成RunnerB。
|
||||
|
||||
这些Runner其实就是大数据处理框架的本身,像Spark、Apex、Storm、Flink这些数据处理架构。这对整个技术行业都可以起到一种良性的竞争。如果Runner要想争取更多用户的话,就必须努力提升自身数据处理的效率来让用户选择自己。
|
||||
|
||||
做底层数据处理框架的工程师则可以专心优化自身的效率和增加自身的功能,而不必担心迁移。
|
||||
|
||||
且Apache Beam还有着自己的社区。所以,在实际工程中如果遇到一些特别的、没有在官方文档中解释的问题,你就可以到社区去求助了。有了社区交流之后,全世界的工程师们都可以对自己的工程提出问题,解决问题,实现解决问题的思路。
|
||||
|
||||
Beam Runner功能的迭代速度
|
||||
|
||||
最后你可能会问,Apache Beam的一些功能现在还没有,那还是先观望观望吧。那我来以Flink支持整个Dataflow的功能来告诉你Beam Runner功能的迭代速度有多快。
|
||||
|
||||
Kostas Tzoumas(Data Artisans的联合创始人以及Flink的作者)在Beam出现的时候就表达过自己的看法,他坚信Beam Model是批流数据处理的正确编程模型。所以,在Dataflow Model论文发布之初,他们就根据Dataflow Model的思想重写了Flink的0.10版本的DataStream API。
|
||||
|
||||
这整个过程花费了多少时间呢?
|
||||
|
||||
在2014年的12月,Google正式发布Google Cloud Dataflow SDK。在2015年的1月,Data Artisans紧随Google的脚步,根据Dataflow Model的概念,开始了DataStream API的重写工作。
|
||||
|
||||
在2015年3月,Flink已经可以对批处理进行支持了。到了2015年的12月,Flink可以支持设置窗口和水印的流处理。其实这个时候DataStream API已经很接近Dataflow Model的概念了。所以在2016年3月,Data Artisans正式开始在Apache Beam的Runner代码库中贡献Flink的Runner代码。
|
||||
|
||||
在2016年5月的时候,Flink对批处理也支持了窗口的概念,同时也对整个批处理完成了集成测试(Integration Test)。在2016年8月,Flink完成了对流处理的集成测试。自此,Flink DataStream API宣告完成了对Dataflow Model的支持。
|
||||
|
||||
整个过程从无到有,一共只花费了近一年半的时间。所以你完全可以不必对功能不完整抱有太多的担心。
|
||||
|
||||
小结
|
||||
|
||||
今天,我给你阐述了我自己对于Beam的一些看法,同时也希望借助我所举出的一些例子,能够让你明白Beam对于未来数据处理发展的重要性。
|
||||
|
||||
思考题
|
||||
|
||||
你对Apache Beam还有什么疑问吗?欢迎提问与我探讨。
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
126
专栏/大规模数据处理实战/FAQ第一期学习大规模数据处理需要什么基础?.md
Normal file
126
专栏/大规模数据处理实战/FAQ第一期学习大规模数据处理需要什么基础?.md
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
FAQ第一期 学习大规模数据处理需要什么基础?
|
||||
你好,我是蔡元楠。
|
||||
|
||||
专栏上线已经一个月了,在这里我要先感谢大家的留言,留言的对答可以使我们互有补益。
|
||||
|
||||
这段时间,我发现留言中的很多问题都很有价值,希望你也可以看到。所以,我根据已发布的文章中的思考题,从留言中摘录了一些典型的、常见的问题做出答疑集锦,最终成为了今天你看到的“特别福利篇”。
|
||||
|
||||
“开篇词”问题精选
|
||||
|
||||
问题一:学习大规模数据处理需要有什么基础?
|
||||
|
||||
|
||||
|
||||
这是一个很好的问题,虽然专栏已经更新了一个月,我还是要把这个开篇词中的提问放进来。就像你看到的那样,有好几位读者都问了类似的问题。
|
||||
|
||||
其实在最开始做专栏的内容设计时,我并没有对读者的知识背景作任何假设。
|
||||
|
||||
所以,即使是一些基础的技术概念,我也会举例解释一下(如果你已经会了可能会觉得啰嗦,这时候就需要你照顾一下其他同学了)。如果你有一些语言的编程经验(任何语言都可以)的话,看文章的理解速度会快一点。文章中会有一些示例代码,是用Python编写的。
|
||||
|
||||
但是在设计类型的案例中,我不觉得它对读者有特别的技术要求。
|
||||
|
||||
希望你在后面的阅读中提出建议,告诉我有哪些地方我讲得不够清楚,或者解释的过多,我会适当调整内容。
|
||||
|
||||
问题二:小型公司程序员学习大规模数据处理的意义?
|
||||
|
||||
-
|
||||
这个问题问得很好。以客观条件来看,韩程的说法没有问题。
|
||||
|
||||
大规模的互联网公司天生数据量是要大一些的。但是,这并不意味着大数据处理只在大公司才能发挥价值。你也要考虑其他方面。
|
||||
|
||||
第一,对于公司来讲,小型互联网公司或者传统企业,并不是不需要数据处理技能,而是他们还没有从数据中挖掘business insight的意识,没有数据驱动决策的意识,甚至没有收集数据的意识。
|
||||
|
||||
举个我工作中见到的例子。比如,有些饲养奶牛的农户,他们几十年来根本不知道什么是数据。但是,当我们帮他们细致地搜集奶牛每天的活动数据,比如饮食、运动、作息、产奶,他们就能从中找到最经济(最优)的饲料投放方式。
|
||||
|
||||
第二,对于个人来讲,你就一定要看长期的职业发展,公司会从小变大,职位会从低变高。当你需要影响决策的时候,当你面临的数据量变多的时候,当你准备跳槽的时候,数据的处理能力都是至关重要的。
|
||||
|
||||
“第一讲”问题精选
|
||||
|
||||
思考题:如果你在Facebook负责处理用户数据,你会选择什么样的分片函数来保证均匀分布的数据分片?
|
||||
|
||||
我发现有很多精彩的回答。比如下图中的CountingStars同学,他的思路非常有意思。是把年龄的数值前后颠倒进行分片。
|
||||
|
||||
|
||||
|
||||
还有这位Mark Lee,他认为可以使用身份证后面的随机数来进行分片,纯技术上看起来似乎可行。但要使用用户的身份ID的话,你还需要考虑是否符合法律、道德、隐私方面的问题。
|
||||
|
||||
|
||||
|
||||
而Freud的想法是引用随机标记来保证数据分片的随机性。但这里要保证数据的均匀可重复才行。如果你在shard2上的任务失败,你需要能够还原出错的任务并进行重试。
|
||||
|
||||
|
||||
|
||||
榣山樵客把这几个回答可能出现的问题做了个总结。他的回复是一切有效降低十位数权重的哈希算法都是可行的。
|
||||
|
||||
|
||||
|
||||
倒置年龄可以明显改善分布不均的问题,但是也可能对某些单一热点无解,比如25岁的用户特别多的话还是会出问题。
|
||||
|
||||
随机分区可以做到均衡,但对combine、io等优化不够友好。还有一个缺点,是当分区任务失败,需要重新分区的时候,分区结果不再是deterministic的。如果某一台机器宕机了,你要如何重新分配原本属于这台机器上的用户数据?
|
||||
|
||||
先采样,再动态合并和拆分的实现过于复杂,效果可能不够稳定。
|
||||
|
||||
像他一样,在每个答案里都分别给出这个答案所存在的不足,这一点是我非常赞赏的。在开发设计中没有哪个答案是特别完美的,我们能做的是分析哪一个才是最符合自身应用需求,进而改善。
|
||||
|
||||
“第二讲”问题精选
|
||||
|
||||
第二讲中,我留下的思考题是“你现在正在使用的数据处理技术有什么问题?你有怎样的改进设计?”。
|
||||
|
||||
mjl在回答中阐述了他比较了解的Spark和Flink,总结得很好。
|
||||
|
||||
|
||||
|
||||
虽然原生Spark Streaming Model和Dataflow Model不一样,但是Cloudera Labs也有根据Dataflow Model的原理实现了Spark Dataflow,使得Beam也可以跑Spark runner。
|
||||
|
||||
而对于Flink来说的话,在0.10版本以后,它的DataStream API就已经是根据Dataflow Model的思想来重写了。
|
||||
|
||||
现在Flink也支持两套API,分别是DataStream版本的和Beam版本的。其实data Artisans一直都有和Google保持交流,希望未来两套Beam和Flink的API能达到统一。
|
||||
|
||||
最后赞一点,批处理是流处理的子集,这个观点我在第一讲的留言中也提到过。
|
||||
|
||||
第三讲和第四讲中问题较为开放,与读者自身的工作内容强相关,很多都是大家在分享自己的经验,内容很丰富,这里篇幅不足,建议大家去原文的留言中看一看。
|
||||
|
||||
“第五讲”问题精选
|
||||
|
||||
第五讲中讲的主要是分布式处理系统的三个重要指标:扩展性,一致性和持久性。根据这个内容,3SKarl同学提问弱一致性和最终一致性的区别是什么。
|
||||
|
||||
|
||||
|
||||
这是个很棒的问题。简而言之,弱一致性是个很宽泛的概念,它是区别于强一致性而定义的。广义上讲,任何不是强一致的,而又有某种同步性的分布式系统,我们都可以说它是弱一致的。
|
||||
|
||||
而最终一致性是弱一致性的一个特例,而且是最常被各种分布式系统用到的一个特例。
|
||||
|
||||
其他的比如因果一致性、FIFO一致性等都可以看作是弱一致性的特例,不同弱一致性只是对数据不同步的容忍程度不同,但是经过一段时间,所有节点的数据都要求要一致。
|
||||
|
||||
学习专栏时,重要的是理解它们的区别。这部分知识是为了后边讲CAP理论服务的,实际的工作中也不会像考试考概念题一样,让你背写这些一致性的定义。
|
||||
|
||||
|
||||
|
||||
hua168同学问的是强一致性的误差范围。这个问题非常有趣,强一致性并没有误差可言的,强一致性简单地说指的就是如果更新一条数据,那所有用户读取数据的时候必须都看到这条更新了的数据。
|
||||
|
||||
在这里我也想借着FAQ分享一个自己当年在面试Bloomberg的面试经历。
|
||||
|
||||
面试官给我出的题目是这样的:如果要设计Bloomberg的股票信息系统中的数据库系统,系统需要实时更新股票价格,而数据更新的写入量非常大,用户也需要读取最新的股票资讯,你会如何设计这套系统。
|
||||
|
||||
这个问题其实有很多的未知区域需要我们去和面试官去阐明,例如用户的Use Cases是什么?在此我就不一一展开了,在这里我只想分享一个和一致性相关的内容。
|
||||
|
||||
在和Bloomberg的Tech Lead讨论时我发现,原来他们的股票系统显示的股价并不是强一致性的,延迟范围是1分钟左右。
|
||||
|
||||
因为应用场景上,普通股民并不会需要实时关心每秒钟股票价格的动态,更多的是关心大盘走势。而金融巨头在操作股票的时候,更多只关心特定的几只股票,所以这些股票的价格通常对于他们来说会更新快一点。
|
||||
|
||||
所以说,很多现实生活上的实际应用和我们本来想象的并不太一样。
|
||||
|
||||
到这里,我们的第一期答疑就结束了。
|
||||
|
||||
就像我在专栏一开始的时候与你说的一样,我希望你能够积极与我互动。其实很多同样的问题会在不同的人身上重复出现,你不表达出来的话,可能永远也不知道,原来有那么多人曾经和你遇到过同样的困境。
|
||||
|
||||
如果你觉得有所收获,欢迎你把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
135
专栏/大规模数据处理实战/FAQ第三期ApacheBeam基础答疑.md
Normal file
135
专栏/大规模数据处理实战/FAQ第三期ApacheBeam基础答疑.md
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
FAQ第三期 Apache Beam基础答疑
|
||||
你好,我是蔡元楠。
|
||||
|
||||
这里是“FAQ第三期:Apache Beam基础答疑”。这一期主要是针对上周结束的模块四——Apache Beam的基础知识部分进行答疑,并且做了一些补充。
|
||||
|
||||
如果你对文章的印象不深了,可以先点击题目返回文章复习。当然,你也可以继续在留言中提出疑问。希望我的解答对你有所帮助。
|
||||
|
||||
22 | Apache Beam的前世今生
|
||||
|
||||
在第22讲中,我分享了Apache Beam的诞生历程。留言中渡码、coder和Milittle都分享了自己了解的技术变迁、技术诞生历史。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
而JohnT3e则是分享了我在文章中提到的几个论文的具体内容。他分享的论文是非常好的补充材料,也希望你有时间的话可以下载来看一看。我把链接贴在了文章里,你可以直接点击下载浏览。
|
||||
|
||||
MapReduce论文-
|
||||
Flumejava论文-
|
||||
MillWheel论文-
|
||||
Data flow Model论文
|
||||
|
||||
Morgan在第22讲中提问:Beam和Spark是什么关系?
|
||||
|
||||
|
||||
|
||||
我的回答是,Spark可以作为Beam的一个底层Runner来运行通过Beam SDK所编写的数据处理逻辑。相信在读完第23讲的内容后,Morgan会对这个概念有一个更好的认识。
|
||||
|
||||
23 | 站在Google的肩膀上学习Beam编程模型
|
||||
|
||||
在第23讲中,明翼提出的问题如下:
|
||||
|
||||
|
||||
|
||||
其实明翼的这些问题本质上还是在问:Beam在整个数据处理框架中扮演着一个什么样的角色?
|
||||
|
||||
首先,为什么不是所有的大数据处理引擎都可以作为底层Runner呢?原因是,并不是所有的数据处理引擎都按照Beam的编程模型去实现了相应的原生API。
|
||||
|
||||
我以现在国内很火的Flink作为底层Runner为例子来说一下。
|
||||
|
||||
在Flink 0.10版本以前,Flink的原生API并不是按照Beam所提出的编程模型来写的,所以那个时候,Flink并不能作为Beam的底层Runner。而在Flink 0.10版本以后,Flink按照Beam编程模型的思想重写了DataStream API。这个时候,如果我们用Beam SDK编写完数据处理逻辑就可以直接转换成相应的Flink原生支持代码。
|
||||
|
||||
当然,明翼说的没错,因为不是直接在原生Runner上编写程序,在参数调整上肯定会有所限制。但是,Beam所提倡的是一个生态圈系统,自然是希望不同的底层数据处理引擎都能有相应的API来支持Beam的编程模型。
|
||||
|
||||
这种做法有它的好处,那就是对于专注于应用层的工程师来说,它解放了我们需要学习不同引擎中原生API的限制,也改善了我们需要花时间了解不同处理引擎的弊端。对于专注于开发数据处理引擎的工程师来说,他们可以根据Beam编程模型不断优化自身产品。这样会导致更多产品之间的竞争,从而最终对整个行业起到良性的促进作用。
|
||||
|
||||
在第23讲中,JohnT3e也给出了他对Beam的理解。
|
||||
|
||||
|
||||
|
||||
我是很赞成JohnT3e的说法的。这其实就好比SQL,我们学习SQL是学习它的语法,从而根据实际应用场景来写出相应的SQL语句去解决问题。
|
||||
|
||||
而相对的,如果觉得底层使用MySQL很好,那就是另外的决定了。写出来的SQL语句是不会因此改变的。
|
||||
|
||||
24 | 为什么Beam要如此抽象封装数据?
|
||||
|
||||
在第24讲中,人唯优的提问如下:
|
||||
|
||||
|
||||
|
||||
确实,Beam的Register机制和Spark里面的kryo Register是类似的机制。Beam也的确为常见的数据格式提供了默认的输入方式的。
|
||||
|
||||
但这是不需要重复工作的。基本的数据结构的coder在GitHub上可以看到。比如String,List之类。
|
||||
|
||||
25 | Beam数据转换操作的抽象方法
|
||||
|
||||
在第25讲中,我们学习了Transform的概念和基本的使用方法,了解了怎样编写Transform的编程模型DoFn类。不过,sxpujs认为通用的DoFn很别扭。
|
||||
|
||||
|
||||
|
||||
这个问题我需要说明一下,Spark的数据转换操作API是类似的设计,Spark的数据操作可以写成这样:
|
||||
|
||||
JavaRDD<Integer> lineLengths = lines.map(new Function<String, Integer>() {
|
||||
public Integer call(String s) { return s.length(); }
|
||||
});
|
||||
|
||||
|
||||
我不建议你用自己的使用习惯去评判自己不熟悉的、不一样的API。当你看到这些API的设计时,你更应该去想的,是这种设计的目标是什么,又有哪些局限。
|
||||
|
||||
比如,在数据处理框架中,Beam和Spark之所以都把数据操作提取出来让用户自定义,是因为它们都要去根据用户的数据操作构建DAG,用户定义的DoFn就成了DAG的节点。
|
||||
|
||||
实际使用中,往往出现单个数据操作的业务逻辑也非常复杂的情况,它也需要单独的单元测试。这也是为什么DoFn类在实际工作中更常用,而inline的写法相对少一点的原因。因为每一个DoFn你都可以单独拿出来测试,或者在别的Pipeline中复用。
|
||||
|
||||
26 | Pipeline:Beam如何抽象多步骤的数据流水线?
|
||||
|
||||
在第26讲中,espzest提问如下:
|
||||
|
||||
|
||||
|
||||
其实我们通过第24讲的内容可以知道,PCollection是具有无序性的,所以最简单的做法Bundle在处理完成之后可以直接append到结果PCollection中。
|
||||
|
||||
至于为什么需要重做前面的Bundle,这其实也是错误处理机制的一个trade-off了。Beam希望尽可能减少persistence cost,也就是不希望将中间结果保持在某一个worker上。
|
||||
|
||||
你可以这么想,如果我们想要不重新处理前面的Bundle,我们必须要将很多中间结果转换成硬盘数据,这样一方面增加很大的时间开销,另一方面因为数据持久化了在具体一台机器上,我们也没有办法再重新动态分配Bundle到不同的机器上去了。
|
||||
|
||||
接下来,是cricket1981的提问:
|
||||
|
||||
|
||||
|
||||
其实文章中所讲到的随机分配并不是说像分配随机数那样将Bundle随机分配出去给workers,只是说根据runner的不同,Bundle的分配方式也会不一样了,但最终还是还是希望能使并行度最大化。
|
||||
|
||||
至于完美并行的背后机制,Beam会在真正处理数据前先计算优化出执行的一个有向无环图,希望保持并行处理数据的同时,能够减少每个worker之间的联系。
|
||||
|
||||
就如cricket1981所问的那样,Beam也有类似Spark的persist方法,BEAM-7131 issue就有反应这个问题。
|
||||
|
||||
28 | 如何设计创建好一个Beam Pipeline?
|
||||
|
||||
在第28讲中,Ming的提问如下:
|
||||
|
||||
|
||||
|
||||
对此,我的回答是,一个集群有可能同时执行两个pipeline的。在实践中,如果你的四个pipeline之间如果有逻辑依赖关系,比如一个pipeline需要用到另一个pipeline的结果的话,我建议你把这些有依赖关系的pipeline合并。
|
||||
|
||||
如果你的pipeline之间是互相独立,你可以有四个独立的二进制程序。这个提问里,Ming说的集群应该是物理上的机器,这和pipeline完全是两个概念。好的集群设计应该能够让你可以自由地提交pipeline任务,你不需要去管什么具体集群适合去安排跑你的任务。
|
||||
|
||||
JohnT3e的问题如下:
|
||||
|
||||
|
||||
|
||||
对于这个问题,我觉得JohnT3e可以先退一步,看看这个需求场景到底适不适用于分布式数据处理。
|
||||
|
||||
分布式的核心就是并行,也就是说同一批数据集合元素和元素之间是无依赖关系的。如果你的场景对于元素的先后顺序有业务需求,可能可以看看PubSub,RPC等是不是更适合。而不是Beam的PCollection。
|
||||
|
||||
好了,第三期答疑到这里就结束了。最后,感谢在Apache Beam的基础知识模块里积极进行提问的同学们,谢谢你们的提问互动。
|
||||
|
||||
@JohnT3e、@渡码、@coder、@morgan、@Milittle、@linuxfans、@常超、@明翼、@ditiki、@朱同学、@Bin滨、@A_F、@人唯优、@张凯江、@胡墨、@cricket1981、@sxpujs、@W.T、@cricket1981、@espzest、@沈洪彬、@onepieceJT2018、@fy、@Alpha、@TJ、@dancer、@YZJ、@Ming、@蒙开强
|
||||
|
||||
|
||||
|
||||
|
103
专栏/大规模数据处理实战/FAQ第二期Spark案例实战答疑.md
Normal file
103
专栏/大规模数据处理实战/FAQ第二期Spark案例实战答疑.md
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
FAQ第二期 Spark案例实战答疑
|
||||
你好,我是蔡元楠。
|
||||
|
||||
这里是第二期答疑,上周我们结束了Spark部分的内容,时隔一周,我们的Spark案例实战答疑终于上线了。
|
||||
|
||||
通过10讲的Spark学习,相信你已经对Spark的基本特性有了深入的了解,也基本掌握了如何使用各类常用API,如RDD、DataSet/DataFrame、Spark Streaming和Structured Streaming。今天我将针对模块三中提出的一些共性留言做一个集中答疑。
|
||||
|
||||
我首先要为积极留言的同学们点个赞,感谢同学们亲自动手实践,有的同学还通过查阅官方API文档的形式找出了正确的实现方式,这非常值得鼓励。
|
||||
|
||||
第18讲
|
||||
|
||||
|
||||
|
||||
在第18讲中,kylin同学留言问到,为什么用我们通篇用的是DataFrame API而不是DataSet。这是因为PySpark的SQL库只有DataFrame,并没有DataSet。不过在Scala和Java中,DataSet已经成为了统一的SQL入口。
|
||||
|
||||
|
||||
|
||||
斯盖丸同学问道,第18讲代码中groupBy(‘value’)中value是什么意思?
|
||||
|
||||
这里我说一下,SparkSession.read.text()读取文件后生成的DataFrame只有一列,它的默认名字就是“value”。
|
||||
|
||||
在第18讲的代码中,我们用lines.value去读取这一列,是同样的道理。之后我们给新的列重命名为”word”,所以groupBy的参数变成了”word”。如果你印象不深了,可以返回去查看一下。
|
||||
|
||||
讲到这里,我要为Jerry同学点个赞。在开篇词中我就提到过,我希望你可以建立自己的批判性思维,不要盲目听从任何人的意见。除了认真实践,像Jerry一样通过查阅官方文档找到了正确的实现方式,做的真的很棒,希望可以在工作中也把这份批判性思维和独立学习的能力保持下去。
|
||||
|
||||
你可以发现,在第18讲中,我介绍的explode和split方法在官方文档中都有详细的讲解,这些内容并没有很大的难点,通过自己阅读官方文档都可以学会。官方文档中还有很多我没有提到的用法,在仅仅10讲的篇幅中我不能把Spark的每一个用法都教给你。我能做的,只是从源头出发,分析新的技术、新的API产生的原因,教会你思考的方式,并结合例子,让你体会一下如何在真实场景中利用这些技术,而不是照本宣科地把官方文档复述一遍。
|
||||
|
||||
学习新的技术跟上学时背单词不一样,我们要做的是在最短时间内掌握核心内容和设计的理念,至于具体的用法,完全可以在用到的时候查阅官方文档。
|
||||
|
||||
第19讲
|
||||
|
||||
|
||||
|
||||
第19讲中,gotojeff提出的这个语言选择问题,其实我之前就提到过,PySpark现在不支持DataSet,只有Scala和Java支持。这是由语言特性决定的,Python是动态类型的语言,而DataSet是强类型的,要求在编译时检测类型安全。所以,在所有用Python的代码例子中,我用的都是DataFrame。
|
||||
|
||||
大部分人都同意在Spark中,Scala和Python是优于Java和R的。至于在Spark生态中,Scala和Python孰优孰劣,这是个很主观的问题,我们不能只因为不支持DataSet这一点就说Python比Scala差。
|
||||
|
||||
Scala确实很优秀,Spark原生的实现就是用Scala写的,所以任何新发布的功能肯定支持Scala,官方文档也都是用Scala来举例子。而且Scala的性能要优于Python。但是Python也有很多优点,比如容易学习、应用场景广。这两种语言在Spark的世界中都可以满足我们绝大多数的需求,选择任何一个都不是错误的。
|
||||
|
||||
第20讲
|
||||
|
||||
第20讲的思考题是,为什么流的Inner-Join不支持完全输出模式?对于Inner-Join而言,加水印是否是必须的? Outer-Join呢?
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
这里,我节选了never leave、jon、Ming的留言,里面是他们对这个思考题的回答,不知道你是不是也进行了深入的思考?那么现在,让我也来分享一下我的看法吧。
|
||||
|
||||
首先,现阶段不仅Inner-join不支持完全输出模式,任何类型的Join都不支持完全输出模式。
|
||||
|
||||
这是因为完全输出模式要求每当有新数据输入时,输出完整的结果表。而对于无边界数据,我们很难把所有历史数据存在内存中。所以,一般Join的都是在某个时间窗口内的流数据,这就是引入watermarking的原因。希望将来Spark可以引入新的机制来支持这一点。
|
||||
|
||||
其次,我们都知道Outer join是要在Inner Join的基础上,把没有匹配的行的对应列设为NULL。但是由于流数据的无边界性,Spark永远无法知道在未来会不会找到匹配的数据。所以,为了保证Outer Join的正确性,加水印是必须的。这样Spark的执行引擎只要在水印的有效期内没找到与之匹配的数据,就可以把对应的列设为NULL并输出。
|
||||
|
||||
那么Inner Join呢?由于Inner Join不需要连接两个表中所有的行,所以在Spark官网的叙述中,水印和事件时间的限制不是必须的。但是如果不加任何限制,流数据会不断被读入内存,这样无疑是不安全的。所以,我推荐你即便是Inner Join也要加水印和事件时间的限制。
|
||||
|
||||
|
||||
|
||||
Feng.X同学不是很理解实例中两个Streaming DataFrame Join时,为什么要加上事件时间的限制“endTime > startTime AND endTime <= startTime + interval 2 hours”。
|
||||
|
||||
事实上,这个限制会抛弃任何长于2个小时的出租车出行数据。确实,对于这个例子来说,这样一个对事件时间的限制并不是必须的。加入它其实是为了告诉你,在基于事件时间来join两个流时,我们一般不考虑时间跨度过大的情况,因为它们没有普遍意义,还会影响数据分析的结果。
|
||||
|
||||
举个例子吧,对于一个网页广告来说,我们需要知道用户看到一个广告后要多长时间才会去点击它,从而评估广告的效果。
|
||||
|
||||
这里显然有两个流:一个代表用户看到广告的事件,另一个代表用户点击广告的事件。尽管我们可以通过用户的ID来Join这两个流,但是我们需要加一个限制,就是点击广告的时间不能比看到广告的时间晚太久,否则Join的结果很可能是不准确的。比如,用户可能在1:00和2:00都看到了广告,但是只在2:01点击了它,我们应该把2:00和2:01Join起来,而不应该Join1:00和2:01,因为1:00看到的广告并没有促使他点击。
|
||||
|
||||
第21讲
|
||||
|
||||
第21讲的思考题是,除了高延迟的流处理这一缺点外,你认为Spark还有什么不足?可以怎样改进?
|
||||
|
||||
我们都知道,Spark并不是一个完美的数据处理框架,它的优点明显,也同样有不少不足之处。
|
||||
|
||||
|
||||
在数据规模很大的场景中,靠内存处理的开销很大。如果内存不够把中间结果写入硬盘的话,又会影响处理速度;
|
||||
Spark没有自己的文件管理系统,它对HDFS或者其他的文件系统依赖很强;
|
||||
在流处理中,只支持基于时间的窗口,而不支持其他种类的窗口,比如基于数据个数的窗口。
|
||||
|
||||
|
||||
正是由于Spark还有诸多不足,所以众多开源的贡献者才会持续对Spark做改进,Spark也在不断发布新版本。此外,很多新的数据处理框架的发明也是为了从根本上解决Spark存在的问题,比如Flink,还有我们正在学习的Apache Beam。
|
||||
|
||||
|
||||
|
||||
这位飞哥grapefruit不太明白Flink支持基于数据本身窗口是什么意思,我来回答一下。
|
||||
|
||||
窗口是流数据处理中最重要的概念之一,窗口定义了如何把无边界数据划分为一个个有限的数据集。基于事件时间的窗口只是窗口的一种,它是按照事件时间的先后顺序来划分数据,比如说1:00-1:10是一个集合,1:10-1:20又是一个集合。
|
||||
|
||||
但是窗口并不都是基于时间的。比如说我们可以按数据的个数来划分,每接受到10个数据就是一个集合,这就是Count-based Window(基于数量的窗口)。Flink对于窗口的支持远比Spark要好,这是它相比Spark最大的优点之一。它不仅支持基于时间的窗口(处理时间、事件时间和摄入时间),还支持基于数据数量的窗口。
|
||||
|
||||
此外,在窗口的形式上,Flink支持滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)、全局窗口(Global Window)和会话窗口(Session Windows)。
|
||||
|
||||
到这里,我们Spark案例实战篇的答疑就结束了。欢迎继续留言,与我分享你的问题与答案。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
88
专栏/大规模数据处理实战/加油站Practicemakesperfect!.md
Normal file
88
专栏/大规模数据处理实战/加油站Practicemakesperfect!.md
Normal file
@@ -0,0 +1,88 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
加油站 Practice makes perfect!
|
||||
你好,我是蔡元楠。
|
||||
|
||||
端午节了,先祝你节日快乐。不知道正在读文章的你这几天是不是有什么娱乐活动。我最近去参加了湾区一年一度的“三俗”活动——摘樱桃。而且,还看了一个有意思的电影《疾速备战》(John Wick: Chapter 3-Parabellum),推荐给你。
|
||||
|
||||
说归正题,在专栏的更新过程中,对于“留言的处理”这一点我还是要先道个歉,前段时间因为一些个人原因比较忙,所以对于问题的回复没有做好。我会努力把答疑这件事重新做起来。既然有问题了,就是要解决掉才行。
|
||||
|
||||
专栏进程也已经过半了,我好像感觉到同学们的阅读热情都在降低,排除“养肥了再读”的情况,可能是因为很多人进入了学习的“倦怠期”。我想了下,正好又到了假期,也许是应该专门写一篇文章,来跟你“聊聊天”。毕竟对于绝大部分人来说,“学习”这件事情真的是“逆人性”的,总需要一些建议和鼓励。
|
||||
|
||||
学习的目的就是解决问题
|
||||
|
||||
|
||||
|
||||
说实话,我们都不是天生以学习为乐趣的“学神”,所以现在我们的学习目的往往就是集中在“解决问题”上。而事实上,“解决问题”这件事其实并不是我们从工作的时候才开始面对的。
|
||||
|
||||
还记得我们在校园时期受的最多的训练吗?那就是“做题”。枯燥无味的“做题”过程就是我们接受的最初的对于“解决问题”的训练。在这个“做题”过程中学会的解决问题的方法一定是你至今都在受用的。
|
||||
|
||||
只不过是不同阶段里,你需要解决的问题的主题会变化而已。小时候应付数学题,生活中要应对和男/女朋友吵架,工作中要应对定义设计技术系统,等等。
|
||||
|
||||
当然,我们要面对的这些问题的规模也一直在变化。就像我们小时候做的题:根据a和b,求c是多少?已知条件都有,正确解法只有一个。而成人世界的问题:怎样改善北京的交通?已知条件你不是很了解,解法也不止一个,而且很难验证。
|
||||
|
||||
我十分同意Amazon创始人贝索斯的一句话,“与其看20年后什么会变,不如看什么不会变”。在我们的人生中,不会变的就是“人要解决问题”这件事。
|
||||
|
||||
工作需要你解决沟通问题
|
||||
|
||||
|
||||
|
||||
其实学习方法有很多,如果套用机器学习的术语,我们在学生时期常用的解决问题方法是Reinforcement Learning的Imitation Learning,也就是模仿学习。孔子所说的举一反三也是在说这个道理,通过做一个题,学会做相似的其他题。
|
||||
|
||||
当然,不只是具体的技术问题,与他人的沟通交流中也可以用上这样简单的道理。
|
||||
|
||||
举个例子吧,之前有一段时间里,一直困扰我的问题是,我主持的会议议程总是被人打断。
|
||||
|
||||
|
||||
Let’s take a step back, …-
|
||||
Before we talk about this, let’s talk about…-
|
||||
……
|
||||
|
||||
|
||||
而解决这个问题的方法和学生时期一样,就是模仿。模仿那些把会议组织好的人,学习他们做了什么,看他们是如何沟通的,再套用到自己身上加以练习。这里我自己总结了很多细节,比如提前一天再把agenda发给参加会的人,会议开始前把agenda投影上去,被人打断了再强势打断回来,等等。
|
||||
|
||||
|
||||
That’s an important topic, let’s have a separate meeting.-
|
||||
Let’s put it offline.-
|
||||
We only have 20min left, let’s…-
|
||||
……
|
||||
|
||||
|
||||
前面说过,学习的目的是解决问题。知道学习的方法只是第一步,更重要的是你要把它用到自己的工作、生活中。
|
||||
|
||||
工作需要你解决技术问题
|
||||
|
||||
|
||||
|
||||
说到这儿,我不知道你是不是了解我的工作,我给你简单介绍一下吧。其实这方面挺简单的,你随意Google一下,都能找到有关Google Brain的介绍。
|
||||
|
||||
Google Brain的雏形起源于一项斯坦福大学与Google的联合研究项目。
|
||||
|
||||
2011年,Google资深专家杰夫·迪恩(Jeff Dean)、研究员格雷·科拉多(Greg Corrado)和吴恩达(Andrew Ng)教授是这个小团队的最初三名成员。团队有多个主要研究方向,包括机器学习算法和技术、医疗健康、机器学习支撑计算机系统、机器人、自然语言理解、音乐艺术创作和知觉仿真等。
|
||||
|
||||
我现在的工作方向就是医疗健康,主要负责的是癌症相关的AI应用。
|
||||
|
||||
比如,能否把所有癌症相关的数据更有效地整理起来,让医生和研究者更方便地查询。这些数据非常多,包括诊断过程中的CT、活检切片、基因序列、治疗过程中患者对于不同疗法的反应,等等。
|
||||
|
||||
再举个细点的例子吧。有研究者发现,免疫疗法对于癌症患者的有效程度更多的是取决于癌变组织周围的免疫细胞环境。在这一前提下,我们要解决的问题就是:能否用AI来分析癌变组织的免疫细胞环境?
|
||||
|
||||
看到这,你大概能理解了,拥有大规模数据的整理、搜集、存储、分析能力,是我们完成工作内容的先决条件。而我们日常要锻炼的、加强的也就是这些解决技术问题的能力。
|
||||
|
||||
当然,可能大公司和小团队侧重的方面并不相同。如果你在一个小团队里,这个小团队里更看重的是你是否对这个事业有兴趣。但是,大公司主要看重的还是员工解决沟通问题和解决技术问题的能力。
|
||||
|
||||
这一点如果体现在面试流程的情境中,你可以这样理解:站在面试流程制定者的角度,他们设计一些面试流程的目的只是为了判定这个人进公司后是不是能成为一个好的同事,帮助公司成功。
|
||||
|
||||
所以,在面试的过程中,你可以先给自己一个人设,就是把自己当作是“面试官的同事”。把面试问题的内容定义成一个需要解决的问题,一个需要你们去共同解决的问题。比如,面试题条件没给全的时候,完全不需要惊慌,他很有可能是故意的,你需要做的是去和面试官沟通我们这个项目需求。
|
||||
|
||||
这与真实的工作中一样,拿到一个项目后,你要马上开始死命地工作吗?不是的。你应该是要多去问为什么。为什么做这个项目?团队想要达到什么目标?想要短期的方案还是长期的方案?这样的心态摆好之后,剩下的就是你的技术问题了。
|
||||
|
||||
从看到学,是一件事;从学到用,是一件事;从用到会,又是另外一件事了。而解决技术问题方法只有一个:Practice makes perfect!
|
||||
|
||||
今天说了这么多,其实都是我自己的一些经验。那么对于“如何解决问题”这一点,你可以分享一下你的经历吗?
|
||||
|
||||
|
||||
|
||||
|
69
专栏/大规模数据处理实战/结束语世间所有的相遇,都是久别重逢.md
Normal file
69
专栏/大规模数据处理实战/结束语世间所有的相遇,都是久别重逢.md
Normal file
@@ -0,0 +1,69 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
结束语 世间所有的相遇,都是久别重逢
|
||||
今天和往常一样。我开车沿着硅谷的101公路下班,101还是一样的堵。
|
||||
|
||||
今天和往常又不一样。比如,今天午饭的话题不一样。往日我会利用午饭或者喝咖啡的时间和同事一起讨论一些极客时间专栏读者提出的问题。而且,今天回到家里也不需要赶着写稿。如果是往日,编辑催稿的微信早就已经堆积成了99+未读消息。
|
||||
|
||||
专栏成功的指标=所有读者收获之和
|
||||
|
||||
我还清楚地记得,4月15日(北京时间),因为时差原因,我在凌晨2、3点等待着专栏上线,守着手机屏幕,看新的读者加入。我在极客时间的留言管理页面一遍一遍下拉刷新,期待着去回复包括你在内的读者留言。说不定我一个人就给极客时间的服务器QPS增加了1或者2(笑)。
|
||||
|
||||
写作的过程中,我也碰到过很多困难。
|
||||
|
||||
专栏占用了很多时间,有时候为了查证一个技术点我会研读Apache Beam代码到深夜,女朋友甚至因此和我吵过架。不仅是写作的时间需要去平衡,我的全职工作和生活也需要。在专栏更新过半的时候,因为家人身体原因,我必须一直往返于湾区和德州,没有完整的时间录音,不得不更换了主播来录音。
|
||||
|
||||
为此,我也在这里,再次和你道歉。
|
||||
|
||||
当然,也有写作时间之外的阻力。我在开篇词中对专栏的内容要求是,每一篇专栏都是原汁原味的硅谷技术分享。这也带来了一个意外的问题。
|
||||
|
||||
第1讲“为什么MapReduce会被硅谷一线大厂淘汰”一经在知乎分享,就一跃成为MapReduce话题点赞排名第一的文章。它的影响力超过了Google内容发行部门的预计,以至于负责Google内容审查的部门找到我,要求一起审查专栏中还未发布的内容。
|
||||
|
||||
这样的小插曲其实我也没有预料到。在经历了14天详细的审查之后,我的专栏终于通过审核,被认定为是开源技术分享,而并不涉及商业机密。
|
||||
|
||||
专栏写作期间,有时候和朋友吃个饭我都不得不提前告辞:“今晚需要回家写稿”。朋友都问我:“为什么你要花这么多时间去写专栏?”
|
||||
|
||||
是啊,为什么?
|
||||
|
||||
因为你。专栏上线的十几天的时候,在LinkedIn、微信等各种社交渠道里,有来自Apple、Uber、思科等各种公司的读者联系到我,和我说: “蔡老师,谢谢你,我看了你的专栏,真的收获很大。”
|
||||
|
||||
你的收获,你的成长,才是我如此认真写作的动力。即使订阅量只有1,我也会为了这1份订阅背后的信任,100%交付每一篇内容。
|
||||
|
||||
万物皆数据
|
||||
|
||||
记得在刚入职Google的时候,组里希望我能在了解大组业务的同时,熟悉Google内部对于大规模数据处理的整个流程和做法。而组里布置给我的第一个启动项目就是修改一个大型MapReduce任务中的一小部分逻辑。
|
||||
|
||||
当时我着了迷似地研究组内数据处理的整套流程。
|
||||
|
||||
我逐渐发现,Google内部所使用的API和我在上学时自学的Apache Spark非常不同。即便需要新加或者修改的逻辑非常多,再加上数据异常的检测(Monitoring)和执行流程日志(Logging)的输出,整个代码加起来也很难超过100行。
|
||||
|
||||
而我组里的技术领导(Tech Lead)更是告诉我,在查看自己所修改的Pipeline的时候,可以利用DAG来查看一整套执行流程图,我更是觉得十分神奇。
|
||||
|
||||
后来我才慢慢知道,其实在我加入Google的时候,Google已经完全淘汰了以前的MapReduce Framework,而将Google Dataflow的整套思想都运用到了这个内部称为Flume的大规模数据处理Framework中去。在新入职员工的内部培训中,讲师就说过——Flume是每个Noogler的必修课。
|
||||
|
||||
在经历了几年的技术历练之后,我开始成为Google的面试官之一。
|
||||
|
||||
一般来说,面试题目除了基本的数据结构和算法外,还会涉及到系统设计。其实这些题目或多或少都在考察着我们如何处理转换数据,如何保存数据。在和Google不同的组之间进行交流后,我也更加清楚地了解到,其实每个组都在做着数据处理的任务。
|
||||
|
||||
我们自己的生活难道不也是完完全全被大规模数据处理充斥着的吗?
|
||||
|
||||
例子太多了,日常生活购物网站(淘宝、Amazon)在处理着所有客户的交易商品数据,平时使用的支付软件(支付宝、Venmo)在处理着我们的支付数据,社交软件(微博、Facebook和Twitter)在处理着大规模社交信息流数据。
|
||||
|
||||
其实我们一直都生活在一个被大规模数据处理所包围着环境中,我相信等到5G全面到来,这种现象会更加明显。而我也越来越觉得,学习好大规模数据处理真的是一门必修课。
|
||||
|
||||
我的下一步
|
||||
|
||||
虽然我们对于大规模数据处理的学习已经告一段落,但是我坚信“万物皆数据”,坚信数据会改变人类生活。
|
||||
|
||||
接下来,我会回收更多的时间精力,继续从事人工智能医疗健康的研发工作。除此之外,我还会参与录制Google官方的TensorFlow视频课程。
|
||||
|
||||
感谢你在这三个月里的努力学习,希望你能学有所得,也希望有机会能与你再次重逢!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user