first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View 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 VenturesGoogle的风险投资基金会寻找并帮助优秀初创公司在投的初创公司做导师那时候经常参加一些对方公司的技术架构评审。
一次评审中对方的技术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年后人类社会的技术挑战是你拉开与别人差距的重要一站。
在刚开始动笔写专栏的时候,我就在设想,什么样的人会是这个专栏的目标读者呢?
直到专栏快上线,我写下这篇开篇词,我才真正定义读者的标签——应该就是跟我一样渴望成长的人。是的,我和你一样,都渴望成长——渴望知识的成长,渴望经验的成长,渴望财富的成长。
所以我想把这个专栏设计成一份共同的成长规划,而不是一本死板的教材。
正如我在开头的小故事里所说的,这个世界没有谁是绝对的权威。
我希望你每一期都能在留言栏里质疑、提问和讨论。这些互动能帮助我和别的同学一起提高。
最后,我期待和你一起开始学习,共同成长!

View 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 BeamFlumeJava的开源版本揭开MapReduce继任者的神秘面纱。
思考题
如果你在Facebook负责处理例子中的用户数据你会选择什么分片函数来保证均匀分布的数据分片?
欢迎你把答案写在留言区,与我和其他同学一起探讨。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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。
思考题
你现在在使用的数据处理技术有什么问题,你有怎样的改进设计?
欢迎你把自己的想法写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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个应用场景我们探索了大规模数据处理框架的设计。
这些都是为了帮助你更好地理解后面所要讲的所有知识。比如,为什么传统算法不再奏效?为什么要去借助抽象的数据处理描述语言?希望在后面的学习过程中,你能一直带着这些问题出发。
思考题
在你的工作中,有没有随着数据规模变大,系统出问题的情况,你又是怎么解决的?
欢迎你把自己的想法写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 分布式系统学会用服务等级协议SLA来评估你的系统
你好,我是蔡元楠。
从今天开始,我们进入专栏的第二模块。通过这一模块的学习,带你一起夯实大规模数据处理的基础。
首先,我将结合硅谷顶尖科技公司的最佳实践 (Best Practice) ,和你一起分享在设计分布式系统架构时,我们有可能会碰到哪些雷区?又有哪些必备的基础知识?
在硅谷一线大厂所维护的系统服务中我们经常可以看见SLA这样的承诺。
例如在谷歌的云计算服务平台Google Cloud Platform中他们会写着“99.9% Availability”这样的承诺。那什么是“99.9% Availability”呢
要理解这个承诺是什么意思首先你需要了解到底什么是SLA
SLAService-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是多少又或者RPSRequests 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是什么呢又有什么方面可以优化的呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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定理和你进一步讨论。
思考题
对于微信朋友圈的评论功能,你觉得哪种一致性模型更适用?为什么?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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等都是流行的流处理架构平台。
在介绍完这两种处理模式后,你会发现,无论是批处理模式还是流处理模式,在现实生活中都有着很广泛的应用。你应该根据自己所面临的实际场景来决定到底采用哪种数据处理模式。
小结
批处理模式在不需要实时分析结果的情况下是一种很好的选择。尤其当业务逻辑需要处理大量的数据以挖掘更为深层次数据信息的时候。
而在应用需求需要对数据进行实时分析处理时,或者说当有些数据是永无止境的事件流时(例如传感器发送回来的数据时),我们就可以选择用流处理模式。
思考题
相信在学习完这一讲后,你会对批处理模式和流处理模式有着清晰的认识。今天的思考题是,在你的日常开发中,所面临的数据处理模式又是哪一种模式呢?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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小时的提醒等功能。在这整个过程中系统的数据处理运用了哪几个设计模式呢
欢迎你把文章分享给你的朋友。

View 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 ServiceSNS。被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系统中不同的组件许多实时的流处理架构就是利用这个数据处理的设计模式搭建起来的。因为发布/订阅模式同时具有很好的伸缩性。
如果你在开发的场景适合我所讲到的适应场景,可以优先考虑使用发布/订阅模式。
思考题
你认为微信的朋友圈功能适合使用发布/订阅模式吗?为什么?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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在他们的论文“Brewers 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的哪两个属性呢为什么呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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架构呢
欢迎在评论中留言,与我和其他同学一起讨论。
如果你觉得今天有些不一样的收获,也欢迎你把这篇文章分享给你的好友。

View 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架构中的速度层使它既能够进行实时数据处理同时也有能力在业务逻辑更新的情况下重新处理以前处理过的历史数据呢
他根据自身多年的架构经验发现,我们是可以做到这样的改进的。
在前面PublishSubscribe模式那一讲中我讲到过像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架构的优势吗
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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框架中依然存在的用什么思路可以解决
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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中若干个分区计算得来是否还算窄依赖
最后,欢迎你把对弹性分布式数据集的疑问写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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_ONLYMEMORY_AND_DISKDISK_ONLY等。cache()方法会默认取MEMORY_ONLY这一级别。
小结
Spark在每次转换操作的时候使用了新产生的 RDD 来记录计算逻辑,这样就把作用在 RDD 上的所有计算逻辑串起来形成了一个链条,但是并不会真的去计算结果。当对 RDD 进行动作Action时Spark 会从计算链的最后一个RDD开始利用迭代函数Iterator和计算函数Compute依次从上一个RDD获取数据并执行计算逻辑最后输出结果。
此外我们可以通过将一些需要复杂计算和经常调用的RDD进行持久化处理从而提升计算效率。
思考题
对RDD进行持久化操作和记录Checkpoint有什么区别呢
欢迎你把对弹性分布式数据集的疑问写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,169 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 Spark SQLSpark数据查询的利器
你好,我是蔡元楠。
上一讲中,我介绍了弹性分布式数据集的特性和它支持的各种数据操作。
不过在实际的开发过程中我们并不是总需要在RDD的层次进行编程。
就好比编程刚发明的年代工程师只能用汇编语言到后来才慢慢发展出高级语言如Basic、C、Java等。使用高级语言大大提升了开发者的效率。
同样的Spark生态系统也提供很多库让我们在不同的场景中使用。
今天让我们来一起探讨Spark最常用的数据查询模块——Spark SQL。
Spark SQL 发展历史
几年前Hadoop/MapReduce在企业生产中的大量使用HDFS上积累了大量数据。
由于MapReduce对于开发者而言使用难度较大大部分开发人员最熟悉的还是传统的关系型数据库。
为了方便大多数开发人员使用HadoopHive应运而生。
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的操作接口允许数据仓库应用程序直接获取数据允许使用者通过命令行操作来交互地查询数据还提供两个APIDataFrame 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 APISpark 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
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 Spark StreamingSpark的实时流计算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程序你会从哪些角度入手
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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输出时为什么可以把这个词语统计在内这样的机制有没有限制
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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都是JVMJava Virtual Machine的进程所以在安装运行Spark之前我们需要确保已经安装Java Developer KitJDK。在命令行终端中输入
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就是一个包含每个词语的wordcountpair的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程序的基本结构等等。
实践题
希望你可以自己动手操作一下,这整个过程只需要跑通一次,以后就可以脱离纸上谈兵,真正去解决实际问题。
欢迎你在留言中反馈自己动手操作的效果。
如果你跑通了,可以在留言中打个卡。如果遇到了问题,也请你在文章中留言,与我和其他同学一起讨论。

View 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。如果你觉得有所收获也欢迎你把文章分享给朋友。

View 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实时输出各个区域内乘客小费的平均数来帮助司机决定要去哪里接单。
数据集介绍
今天的数据集是纽约市20092015年出租车载客的信息。每一次出行包含了两个事件一个事件代表出发另一个事件代表到达。每个事件都有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中流与流的JoinStream-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呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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 StreamingSpark流处理的实时性还不够所以无法用在一些对实时性要求很高的流处理场景中。
这是因为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-oneStream维护着分区以及元素的顺序比如上图从输入数据源到map间。这意味着map操作符的子任务处理的数据和输入数据源的子任务生产的元素的数据相同。你有没有发现它与RDD的窄依赖类似。
重新分布RedistributingStream中数据的分区会发生改变比如上图中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还有什么不足可以怎样改进
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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的底层系统上运行。
关于怎么理解这个优点其实我们可以借鉴一下SQLStructure 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的完整诞生历程。
通过这一讲,我希望你知道每一项技术都不会毫无理由地诞生,而每一项技术诞生的背后都是为了解决某些特定问题的。了解前人一步步解决问题的过程,有助于我们更有层次地理解一项技术产生的根本原因。在学习一项技术之前,先了解了它的历史源流,可以让我们做到知其然,并知其所以然。
思考题
你也能分享一些你所经历过的技术变迁或是技术诞生的故事吗?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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的编程模型来分享你会怎么设计自己的数据处理逻辑呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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_setPCollection不一定有固定的边界。所以你也不能指望去查找一个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的设计是否能表达你的大规模数据处理场景呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,224 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 TransformBeam数据转换操作的抽象方法
你好,我是蔡元楠。
今天我要与你分享的主题是“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。它们确实有很多相似之处。那你认为它们有什么不一样之处呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 PipelineBeam如何抽象多步骤的数据流水线
你好,我是蔡元楠。
今天我要与你分享的主题是“PipelineBeam如何抽象多步骤的数据流水线”。
在上两讲中我们一起学习了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的数据流水线中当处理的元素发生错误时流水线的错误处理机制吗
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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设计能够满足我们所有的应用需求了吗
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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实现呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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来测试你日常处理的数据逻辑你会如何编写测试呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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的设计模式是对计算引擎动态选择它为什么要这么设计
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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上跑比如SparkRunnerFlinkRunner。如果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"));
}
小结
这一讲我们应用前面学习的PCollectionPipelinePipeline IOTransform知识去解决了一个数据处理领域经典的WordCount问题。并且学会了一些在实际工作中改进数据处理代码质量的贴士比如写成单独可测试的DoFn和把程序参数封装进PipelineOptions。
思考题
文中提供了分词的DoFn——ExtractWordsFn你能利用相似的思路把输出文本的格式化写成一个DoFn吗也就是文中的FormatAsTextFn把PCollection> 转化成PCollection ,每一个元素都是 : 的格式。
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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 AM7:44:00 AM))
(key1, value2, [7:45:00 AM7:45:00 AM))
(key1, value3, [7:49:00 AM7:49:00 AM))
(key1, value4, [8:01:00 AM8:01:00 AM))
(key1, value5, [8:02:00 AM8:02:00 AM))
那么这5个数据会形成两个会话窗口分别是
(key1[(value1, [7:44:00 AM7:44:00 AM)), (value2, [7:45:00 AM7:45:00 AM)), (value3, [7:49:00 AM7:49:00 AM))])
(key1[(value4, [8:01:00 AM8:01:00 AM)), (value5, [8:02:00 AM8:02:00 AM))])
你可以看到在第一个会话窗口中数据的时间戳分别是7:44:00AM7: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里面总共让我们定义三种不同的窗口类型分别是固定窗口滑动窗口和会话窗口。
思考题
在今天介绍的三种时间窗口类型中,你觉得这三种时间窗口分别适合使用在什么样的应用场景中呢?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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和怎样输出无边界数据。
将这些方法融会贯通后,相信类似的时间性数据处理或者是流处理问题在你手中都能迎刃而解了。
思考题
你的工作中有哪些应用场景不适合一般的数据批处理呢?能否利用这里介绍窗口方式处理?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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 排名为123。
你现在有没有觉得这个问题似曾相识呢的确我们在第3讲“大规模数据初体验”中用这个例子引出了数据处理框架设计的基本需求。
这一讲中,我们会从这个基本问题设置开始,逐步深入探索。
在第3讲中我们把我们的数据处理流程分成了两个步骤分别是
统计每个商品的销量
找出销量前K
我们先来看第一个步骤的统计商品销量应该如何在Beam中实现。我们在第3讲中是画了这样的计算集群的示意图
如果你暂时没有思路的话,我们不妨试试换一个角度来思考这个问题。
统计商品的销量换句话说其实就是计算同样的商品id在我们的销售记录数据库中出现了多少次。这有没有让你联想到什么呢没错就是我们在第31讲中讲到的WordCount例子。WordCount是统计同样的单词出现的次数而商品销量就是统计同样的商品id出现的次数。
所以我们完全可以用WordCount中的部分代码解决商品销量统计的这部分数据处理逻辑。
在WordCount中我们用words.apply(Count.perElement())把一个分词后的PCollection转换成了“单词为keycount为value”的一个key/value组合。
在这里呢我们同样使用salesRecords.apply(Count.perElement())把一个商品id的PCollection转换成“key为商品idvalue为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编程方法。
同时,探索了以批处理计算为基础的热销商品列表。我们设计了每小时更新的热销榜单、商品去重处理流水线,根据商品在售状态过滤出热销商品,并按不同的商品门类生成榜单。
思考题
一个商品排名系统中还有太多需要解决的工程问题,你觉得哪些也可以利用大规模数据处理技术设计解决?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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数据流水线的实现方式中我们又可以分成批处理的实现方式和即将在下一讲中展开讨论的实时流处理的方式。批处理虽然简单但是存在着延时性高、无法快速更新积分排行榜的缺点。
思考题
在今天这一讲的最后,我提示了你在实时流处理中需要用到窗口、触发器和累加模式。那我们就先来做个预热,思考一下,在流处理中你会对这三种概念赋予什么值呢?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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”这四个问题。
思考题
今天我们一起探讨了如何利用实时流处理的方式来解决游戏积分排行榜的问题,里面涉及了配置窗口,触发器和累加模式。这些配置可能还不是最优的,你觉得我们还有什么地方可以进行优化的呢?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,115 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
37 5G时代如何处理超大规模物联网数据
你好,我是蔡元楠。
时间过的真快,转眼间我们已经结束了前五个模块的学习,来到了最后一个模块“大规模数据的挑战和未来”。
一门技术类课程的常见学习路线就是“过去→现在→未来”。这个专栏也是如此我们首先研究了大数据处理技术的发展历程从MapReduce出发深入剖析了它的设计思路和优缺点。接下来结合具体的例子一起学习了当下最流行的数据处理框架Spark和Apache Beam。
在这个过程中,你不难发现,任何一门技术的出现都是为了解决实际问题,改进之前的技术所存在的缺陷,而贯穿整个课程的两大场景就是批处理和流处理。
Spark在MapReduce的基础上不断改进在批处理这方面有良好的性能在流处理上也在不断提高。Apache Beam更是一个统一批处理和流处理的框架。
正如我在开篇词中写到的我理想中的专栏是一份与你一同成长的计划。虽然我们已经对当下流行的技术有了深入的了解但是作为一名架构师你的目光一定要放长远要时刻对未来510年乃至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时代边缘计算是一个非常重要的技术。你能去了解一下边缘计算然后告诉我为什么可以这么说吗
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,129 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
38 大规模数据处理在深度学习中如何应用?
你好,我是蔡元楠。
今天我要与你分享的主题是“大规模数据处理在深度学习中如何应用?”。
“深度学习”这个词既是一个人工智能的研究领域也概括了构建人工神经网络的技术方法。2012年的AlexNet2015年的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)
你的深度学习产品上线后,依然需要大规模数据处理技术。比如你提供自动聊天回复功能,能够根据一个微信对话的上下文自动推荐一些合适的回复。如果别人说“明天一起吃饭好不好?”,你可以推荐回复“好”。
那么,像这样的功能上线后,你怎样评估这个深度学习模型的效果呢?你需要去跟踪用户与这个功能的交互。
比如,有多少用户会去选择你推荐的回复,又有多少用户会不选择你推荐的回复而自己打字呢?通过这些追踪的用户行为,你就能利用大规模的数据处理技术,不断地为你的深度学习模型提供更多现实的数据,去进一步训练改进模型。也能利用用户行为去评估当前模型的表现。
小结
这一讲,我们首先学习了大规模数据在深度学习几十年的发展中所扮演的至关重要的角色。然后我们一起看了在一个深度学习驱动的产品周期里的各个阶段,有哪些大规模数据处理的任务。相信通过这一讲,你一定能感受到大规模的数据处理技术就是深度学习系统的奠基石。
思考题
除了这一讲中提到的这些应用,你还能想到哪些大规模数据处理技术在深度学习系统中的应用呢?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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语言下文的例子我们主要用这两种语言来描述。
不同于SQLStreaming 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的发展前景如何欢迎留言与我一起讨论。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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 TzoumasData 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还有什么疑问吗欢迎提问与我探讨。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View 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分钟左右。
因为应用场景上,普通股民并不会需要实时关心每秒钟股票价格的动态,更多的是关心大盘走势。而金融巨头在操作股票的时候,更多只关心特定的几只股票,所以这些股票的价格通常对于他们来说会更新快一点。
所以说,很多现实生活上的实际应用和我们本来想象的并不太一样。
到这里,我们的第一期答疑就结束了。
就像我在专栏一开始的时候与你说的一样,我希望你能够积极与我互动。其实很多同样的问题会在不同的人身上重复出现,你不表达出来的话,可能永远也不知道,原来有那么多人曾经和你遇到过同样的困境。
如果你觉得有所收获,欢迎你把文章分享给你的朋友。

View 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上可以看到。比如StringList之类。
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 | PipelineBeam如何抽象多步骤的数据流水线
在第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可以先退一步看看这个需求场景到底适不适用于分布式数据处理。
分布式的核心就是并行也就是说同一批数据集合元素和元素之间是无依赖关系的。如果你的场景对于元素的先后顺序有业务需求可能可以看看PubSubRPC等是不是更适合。而不是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@蒙开强

View 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案例实战篇的答疑就结束了。欢迎继续留言与我分享你的问题与答案。如果你觉得有所收获也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,88 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
加油站 Practice makes perfect
你好,我是蔡元楠。
端午节了先祝你节日快乐。不知道正在读文章的你这几天是不是有什么娱乐活动。我最近去参加了湾区一年一度的“三俗”活动——摘樱桃。而且还看了一个有意思的电影《疾速备战》John Wick: Chapter 3-Parabellum推荐给你。
说归正题,在专栏的更新过程中,对于“留言的处理”这一点我还是要先道个歉,前段时间因为一些个人原因比较忙,所以对于问题的回复没有做好。我会努力把答疑这件事重新做起来。既然有问题了,就是要解决掉才行。
专栏进程也已经过半了,我好像感觉到同学们的阅读热情都在降低,排除“养肥了再读”的情况,可能是因为很多人进入了学习的“倦怠期”。我想了下,正好又到了假期,也许是应该专门写一篇文章,来跟你“聊聊天”。毕竟对于绝大部分人来说,“学习”这件事情真的是“逆人性”的,总需要一些建议和鼓励。
学习的目的就是解决问题
说实话,我们都不是天生以学习为乐趣的“学神”,所以现在我们的学习目的往往就是集中在“解决问题”上。而事实上,“解决问题”这件事其实并不是我们从工作的时候才开始面对的。
还记得我们在校园时期受的最多的训练吗?那就是“做题”。枯燥无味的“做题”过程就是我们接受的最初的对于“解决问题”的训练。在这个“做题”过程中学会的解决问题的方法一定是你至今都在受用的。
只不过是不同阶段里,你需要解决的问题的主题会变化而已。小时候应付数学题,生活中要应对和男/女朋友吵架,工作中要应对定义设计技术系统,等等。
当然我们要面对的这些问题的规模也一直在变化。就像我们小时候做的题根据a和b求c是多少已知条件都有正确解法只有一个。而成人世界的问题怎样改善北京的交通已知条件你不是很了解解法也不止一个而且很难验证。
我十分同意Amazon创始人贝索斯的一句话“与其看20年后什么会变不如看什么不会变”。在我们的人生中不会变的就是“人要解决问题”这件事。
工作需要你解决沟通问题
其实学习方法有很多如果套用机器学习的术语我们在学生时期常用的解决问题方法是Reinforcement Learning的Imitation Learning也就是模仿学习。孔子所说的举一反三也是在说这个道理通过做一个题学会做相似的其他题。
当然,不只是具体的技术问题,与他人的沟通交流中也可以用上这样简单的道理。
举个例子吧,之前有一段时间里,一直困扰我的问题是,我主持的会议议程总是被人打断。
Lets take a step back, …-
Before we talk about this, lets talk about…-
……
而解决这个问题的方法和学生时期一样就是模仿。模仿那些把会议组织好的人学习他们做了什么看他们是如何沟通的再套用到自己身上加以练习。这里我自己总结了很多细节比如提前一天再把agenda发给参加会的人会议开始前把agenda投影上去被人打断了再强势打断回来等等。
Thats an important topic, lets have a separate meeting.-
Lets put it offline.-
We only have 20min left, lets…-
……
前面说过,学习的目的是解决问题。知道学习的方法只是第一步,更重要的是你要把它用到自己的工作、生活中。
工作需要你解决技术问题
说到这儿我不知道你是不是了解我的工作我给你简单介绍一下吧。其实这方面挺简单的你随意Google一下都能找到有关Google Brain的介绍。
Google Brain的雏形起源于一项斯坦福大学与Google的联合研究项目。
2011年Google资深专家杰夫·迪恩Jeff Dean、研究员格雷·科拉多Greg Corrado和吴恩达Andrew Ng教授是这个小团队的最初三名成员。团队有多个主要研究方向包括机器学习算法和技术、医疗健康、机器学习支撑计算机系统、机器人、自然语言理解、音乐艺术创作和知觉仿真等。
我现在的工作方向就是医疗健康主要负责的是癌症相关的AI应用。
比如能否把所有癌症相关的数据更有效地整理起来让医生和研究者更方便地查询。这些数据非常多包括诊断过程中的CT、活检切片、基因序列、治疗过程中患者对于不同疗法的反应等等。
再举个细点的例子吧。有研究者发现免疫疗法对于癌症患者的有效程度更多的是取决于癌变组织周围的免疫细胞环境。在这一前提下我们要解决的问题就是能否用AI来分析癌变组织的免疫细胞环境
看到这,你大概能理解了,拥有大规模数据的整理、搜集、存储、分析能力,是我们完成工作内容的先决条件。而我们日常要锻炼的、加强的也就是这些解决技术问题的能力。
当然,可能大公司和小团队侧重的方面并不相同。如果你在一个小团队里,这个小团队里更看重的是你是否对这个事业有兴趣。但是,大公司主要看重的还是员工解决沟通问题和解决技术问题的能力。
这一点如果体现在面试流程的情境中,你可以这样理解:站在面试流程制定者的角度,他们设计一些面试流程的目的只是为了判定这个人进公司后是不是能成为一个好的同事,帮助公司成功。
所以,在面试的过程中,你可以先给自己一个人设,就是把自己当作是“面试官的同事”。把面试问题的内容定义成一个需要解决的问题,一个需要你们去共同解决的问题。比如,面试题条件没给全的时候,完全不需要惊慌,他很有可能是故意的,你需要做的是去和面试官沟通我们这个项目需求。
这与真实的工作中一样,拿到一个项目后,你要马上开始死命地工作吗?不是的。你应该是要多去问为什么。为什么做这个项目?团队想要达到什么目标?想要短期的方案还是长期的方案?这样的心态摆好之后,剩下的就是你的技术问题了。
从看到学是一件事从学到用是一件事从用到会又是另外一件事了。而解决技术问题方法只有一个Practice makes perfect
今天说了这么多,其实都是我自己的一些经验。那么对于“如何解决问题”这一点,你可以分享一下你的经历吗?

View 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视频课程。
感谢你在这三个月里的努力学习,希望你能学有所得,也希望有机会能与你再次重逢!