diff --git a/doc/_toc.yml b/doc/_toc.yml index c1472e2..e493f19 100644 --- a/doc/_toc.yml +++ b/doc/_toc.yml @@ -2,6 +2,21 @@ root: index subtrees: - numbered: 2 entries: + - file: ch-big-data-intro/index + entries: + - file: ch-big-data-intro/bigdata + - file: ch-big-data-intro/batch-processing-and-stream-processing + - file: ch-big-data-intro/representative-big-data-technologies + - file: ch-big-data-intro/evolution-of-big-data-processing-platforms + - file: ch-big-data-intro/stream-processing-basics + - file: ch-big-data-intro/bigdata-programming-languages + - file: ch-big-data-intro/exercise-building-data-stream-using-Kafka + - file: ch-programming-basics/index + entries: + - file: ch-programming-basics/inheritance-and-polymorphism + - file: ch-programming-basics/generics + - file: ch-programming-basics/functional-programming + - file: ch-programming-basics/exercise-Flink-development-environment - file: ch-system-design/index entries: - file: ch-system-design/dataflow diff --git a/doc/ch-big-data-intro/batch-processing-and-stream-processing.md b/doc/ch-big-data-intro/batch-processing-and-stream-processing.md new file mode 100644 index 0000000..76e9414 --- /dev/null +++ b/doc/ch-big-data-intro/batch-processing-and-stream-processing.md @@ -0,0 +1,64 @@ +(batch-processing-and-stream-processing)= +# 从批处理到流处理 + +## 1.2.1 数据与数据流 + +在大数据的5个“V”中我们已经提到,数据量大且产生速度快。从时间维度来讲,数据源源不断地产生,形成一个无界的数据流(Unbounded Data Stream)。如图1-5所示,单条数据被称为事件(Event),事件按照时序排列会形成一个数据流。例如,我们每时每刻的运动数据都会累积到手机传感器上,金融交易随时随地都在发生,物联网(Internet of Things,IoT)传感器会持续监控并生成数据。 + +![图1-5 数据和数据流](./img/) + +数据流中的某段有界数据流(Bounded Data Stream)可以组成一个数据集。我们通常所说的对某份数据进行分析,指的是对某个数据集进行分析。随着数据的产生速度越来越快,数据源越来越多,人们对时效性的重视程度越来越高,如何处理数据流成了大家更为关注的问题。 + +在本书以及其他官方资料中,也会将单条事件称为一条数据或一个元素(Element)。在本书后文的描述中,事件、数据、元素这3个概念均可以用来表示数据流中的某个元素。 + +## 1.2.2 批处理与流处理 + +### 1. 批处理 + +批处理(Batch Processing)是指对一批数据进行处理。我们身边的批处理比比皆是,最常见的批处理例子有:微信运动每天晚上有一个批处理任务,把用户好友一天所走的步数统计一遍,生成排序结果后推送给用户;银行信用卡中心每月账单日有一个批处理任务,把一个月的消费总额统计一次,生成用户月度账单;国家统计局每季度对经济数据做一次统计,公布季度国内生产总值(GDP)。可见,批处理任务一般是对一段时间的数据聚合后进行处理的。对于数据量庞大的应用,如微信运动、银行信用卡中心等情景,一段时间内积累的数据总量非常大,计算非常耗时。 + +### 2. 流处理 + +如前文所述,数据其实是以流(Stream)的方式持续不断地产生着的,流处理(Stream Processing)就是对数据流进行处理。时间就是金钱,对数据流进行分析和处理,获取实时数据价值越发重要。如“双十一电商大促销”,管理者要以秒级的响应时间查看实时销售业绩、库存信息以及与竞品的对比结果,以争取更多的决策时间;股票交易要以毫秒级的速度来对新信息做出响应;风险控制要对每一份欺诈交易迅速做出处理,以减少不必要的损失;网络运营商要以极快速度发现网络和数据中心的故障;等等。以上这些场景,一旦出现故障,造成服务的延迟,损失都难以估量。因此,响应速度越快,越能减少损失、增加收入。而IoT和5G的兴起将为数据生成提供更完美的底层技术基础,海量的数据在IoT设备上采集,并通过高速的5G通道传输到服务器,庞大的实时数据流将汹涌而至,流处理的需求肯定会爆炸式增长。 + +## 1.2.3 为什么需要一个优秀的流处理框架 + +处理实时流的系统通常被称为流计算框架、实时计算框架或流处理框架。下面就来解释为何需要一个可靠的流处理框架。 + +### 1. 股票交易的业务场景 + +我们都知道股票交易非常依赖各类信息,一些有可能影响股票市场价格的信息经常首发于财经网站、微博、微信等社交媒体平台上。作为人类的我们不可能24小时一直监控各类媒体,如果有一个自动化的系统来做一些分析和预警,将为决策者争取到更多时间。 + +假设我们有数只股票的交易数据流,我们可以通过这个数据流来计算以10秒为一个时间窗口的股票价格波动,选出那些超过5%变化幅度的股票,并将这些股票与媒体的实时文本数据做相关分析,以判断媒体上的哪些实时信息会影响股票价格。当相关分析的结果足够有说服力时,可以将这个系统部署到生产环境,实时处理股票与媒体数据,产生分析报表,并发送给交易人员。那么,如何构建一个可靠的程序来解决上述业务场景问题呢? + +### 2. 生产者-消费者模型 + +处理流数据一般使用“生产者-消费者”(Producer-Consumer)模型来解决问题。如图1-6所示,生产者生成数据,将数据发送到一个缓存区域(Buffer),消费者从缓存区域中消费数据。这里我们暂且不关心生产者如何生产数据,以及数据如何缓存,我们只关心如何实现消费者。 + +![图1-6 生产者-消费者模型](./img/) + +在股票交易的场景中,我们可以启动一个进程来实现消费者,该进程以10秒为一个时间窗口,统计时间窗口内的交易情况,找到波动最大的那些股票。同时,该进程也对新流入的媒体文本进行分析。这个逻辑看起来很容易实现,但深挖之后会发现问题繁多。 + +### 3. 流处理框架要解决的诸多问题 + +#### (1) 可扩展性 + +股票交易和媒体文本的数据量都非常大,仅以微博为例,平均每秒有上千条、每天有上亿条微博数据。一般情况下,单个节点无法处理这样规模的数据,这时候需要使用分布式计算。假如我们使用类似MPI的框架,需要手动设计分治算法,这对很多程序员来说有一定的挑战性。 + +随着数据不断增多,我们能否保证我们的程序能够快速扩展到更多的节点上,以应对更多的计算需求?具体而言,当计算需求增多时,计算资源能否线性增加而不是耗费大量的资源,程序的代码逻辑能否保持简单而不会变得极其复杂?一个具有可扩展性的系统必须能够优雅地解决这些问题。 + +#### (2) 数据倾斜 + +在分布式计算中,数据需要按照某种规则分布到各个节点上。假如数据路由规则设计得不够完善,当数据本身分布不均匀时,会发生数据倾斜,这很可能导致部分节点数据量远大于其他节点。这样的后果是:轻则负载重的节点延迟过高,重则引发整个系统的崩溃。假如一条突发新闻在网络媒体平台引发激烈的讨论和分析,数据突增,程序很可能会崩溃。数据倾斜是分布式计算中经常面临的一个问题。 + +#### (3) 容错性 + +整个系统崩溃重启后,之前的那些计算如何恢复?或者部分节点发生故障,如何将该节点上的计算迁移到其他的节点上?我们需要一个机制来做故障恢复,以增强系统的容错性。 + +#### (4) 时序错乱 + +限于网络条件和其他各种潜在影响因素,流处理引擎处理某个事件的时间并不是事件本来发生的时间。比如,你想统计上午11:00:00到11:00:10的交易情况,然而发生在11:00:05的某项交易因网络延迟没能抵达,这时候要直接放弃这项交易吗?绝大多数情况下我们会让程序等待,比如我们会假设数据最晚不会延迟超过10分钟,因此程序会等待10分钟。等待一次也还能接受,但是如果有多个节点在并行处理呢?每个节点等待一段时间,最后做数据聚合时就要等待更长时间。 + +批处理框架一般处理一个较长时间段内的数据,数据的时序性对其影响较小。批处理框架用更长的时间来换取更好的准确性。流处理框架对时序错乱更为敏感,框架的复杂程度也因此大大增加。 + +Flink是解决上述问题的最佳选择之一。如果用Flink去解决前文提到的股票建模问题,只需要设置时间窗口,并在这个时间窗口下做一些数据处理的操作,还可以根据数据量来设置由多少节点并行处理。 diff --git a/doc/ch-big-data-intro/bigdata-programming-languages.md b/doc/ch-big-data-intro/bigdata-programming-languages.md new file mode 100644 index 0000000..6980644 --- /dev/null +++ b/doc/ch-big-data-intro/bigdata-programming-languages.md @@ -0,0 +1,31 @@ +(bigdata-programming-languages)= +# 编程语言的选择 + +大数据编程一般会使用Java、Scala和Python等编程语言,Flink目前也支持上述3种语言,本节从大数据编程的角度来分析几种编程语言的优劣。 + +## 1.6.1 Java和Scala + +Java是“老牌”企业级编程语言。Java相比C/C++更易上手,支持多线程,其生态圈中可用的第三方库众多。Java虚拟机(Java Virtual Machine,JVM)保证了程序的可移植性,可以快速部署到不同计算机上,是很多分布式系统首选的编程语言,比如Hadoop和Flink的绝大多数代码都是用Java编写的,这些框架提供了丰富的文档,网络社区的支持好。因此,进行大数据编程,Java编程是必备的技能。相比一些新型编程语言,Java的缺点是代码有点冗长。 + +Scala是一门基于JVM的编程语言。相比Java,Scala的特色是函数式编程。函数式编程非常适合大数据处理,我们将在第2章进一步介绍函数式编程思想。在并行计算方面,Scala支持Actor模型,Actor模型是一种更为先进的并行计算编程模型,很多大数据框架都基于Actor模型。Spark、Flink和Kafka都是基于Actor模型的大数据框架。Scala可以直接调用Java的代码,相比Java,Scala代码更为简洁和紧凑。凡事都有两面性,Scala虽然灵活简洁,但是不容易掌握,即使是有一定Java基础的开发者,也需要花一定时间系统了解Scala。 + +另外,Java和Scala在互相学习和借鉴。Java 8开始引入了Lambda表达式和链式调用,能够支持函数式编程,部分语法与Scala越来越接近,代码也更加简洁。 + +**注意** + +这里的Lambda表达式与1.4.1小节介绍的Lambda架构不同。Lambda表达式基于函数式编程,是一种编写代码的方式。Lambda架构主要指如何同时处理流批数据,是一种大数据架构。 + +Flink的核心代码由Java和Scala编写,为这两种语言提供丰富强大的API,程序员可根据自己和团队的习惯从Java和Scala中选择。本书基于以下两点考虑,决定主要以Java来演示Flink的编程。 + +- Flink目前绝大多数代码和功能均由Java实现,考虑到本书会展示一些Flink中基于Java的源码和接口,为了避免读者在Java和Scala两种语言间混淆,将主要使用Java展示一些Flink的核心概念。 +- 不同读者的编程语言基础不一样,Scala用户往往有一定的Java编程基础,而Java用户可能对Scala并不熟悉。而且Scala的语法非常灵活,一不小心可能出现莫名其妙的错误,初学者难以自行解决,而Scala相对应的书籍和教程不多。或者说Scala用户一般能够兼容Java,而Java用户学习Scala的成本较高。 + +此外,由于大多数Spark作业基于Scala,很多大数据工程师要同时负责Spark和Flink两套业务逻辑,加上Flink的Scala API与Spark比较接近,本书也会在一些地方提示Scala用户在使用Flink时的必要注意事项,并在随书附赠的工程中提供Java和Scala两个版本的代码,方便读者学习。 + +## 1.6.2 Python + +Python无疑是近几年来编程语言界的“明星”。Python简单易用,有大量第三方库,支持Web、科学计算和机器学习,被广泛应用到人工智能领域。大数据生态圈的各项技术对Python支持力度也很大,Hadoop、Spark、Kafka、HBase等技术都有Python版本的API。鉴于Python在机器学习和大数据领域的流行程度,Flink社区非常重视对Python API的支持,正在积极完善Flink的Python接口。相比Java和Scala,Python API还处于完善阶段,迭代速度非常快。Flink的Python API名为PyFlink,是在1.9版本之后逐渐完善的,但相比Java和Scala还不够完善。考虑到Python和Java/Scala有较大区别,本书的绝大多数内容均基于Java相关知识,且PyFlink也在不断迭代、完善,本书暂时不探讨PyFlink。 + +## 1.6.3 SQL + +严格来说,SQL并不是一种全能的编程语言,而是一种在数据库上对数据进行操作的语言,相比Java、Scala和Python,SQL的上手门槛更低,它在结构化数据的查询上有绝对的优势。一些非计算机相关专业出身的读者可以在短期内掌握SQL,并进行数据分析。随着数据科学的兴起,越来越多的岗位开始要求候选人有SQL技能,包括数据分析师、数据产品经理和数据运营等岗位。Flink把这种面向结构化查询的需求封装成了表(Table),对外提供Table API 和 SQL的调用接口,提供了非常成熟的SQL支持。SQL的学习和编写成本很低,利用它能够处理相对简单的业务逻辑,其非常适合在企业内被大规模推广。本书第8章将重点介绍Table API 和SQL的使用方法。 diff --git a/doc/ch-big-data-intro/bigdata.md b/doc/ch-big-data-intro/bigdata.md new file mode 100644 index 0000000..fdbd3bc --- /dev/null +++ b/doc/ch-big-data-intro/bigdata.md @@ -0,0 +1,64 @@ +(bigdata)= +# 什么是大数据 + +## 1.1.1 大数据的5个“V” + +大数据,顾名思义,就是拥有庞大体量的数据。关于什么是大数据、如何定义大数据、如何使用大数据等一系列问题,拥有不同领域背景的读者的理解各不相同。通常,业界将大数据的特点归纳为图1-1所示的5个“V”。 + +![图1-1 大数据的5个"V"](./img/) + +- **Volume**:指数据量大。数据量单位从TB(1 024 GB)、PB(1 024 TB)、EB(1 024 PB)、ZB(1 024 EB)甚至到YB(1 024 ZB)。纽约证券交易所每天产生的交易数据大约在TB级,瑞士日内瓦附近的大型强子对撞机每年产生的数据约为PB级,而目前全球数据总量已经在ZB级,相当于1 000 000 PB。基于更大规模的数据,我们可以对某个研究对象的历史、现状和未来有更加全面的了解。 +- **Velocity**:指数据产生速度快。数据要求的处理速度更快和时效性更强,因为时间就是金钱。金融市场的交易数据必须以秒级的速度进行处理,搜索和推荐引擎需要以分钟级速度将实时新闻推送给用户。更快的数据处理速度可让我们基于最新的数据做出更加实时的决策。 +- **Variety**:指数据类型繁多。数据可以是数字、文字、图片、视频等不同的形式,数据源可能是社交网络、视频网站、可穿戴设备以及各类传感器。数据可能是Excel表格等高度结构化的数据,也可能是图片和视频等非结构化的数据。 +- **Veracity**:指数据真实性。一方面,数据并非天然具有高价值,一些异常值会被掺杂进来,例如,统计偏差、人的情感因素、天气因素、经济因素甚至谎报数据等导致的异常值。另一方面,数据源类型不同,如何将来自多样的数据源的多元异构数据连接、匹配、清洗和转化,最终形成具有真实性的数据是一项非常有挑战性的工作。 +- **Value**:指数据价值。大数据已经推动了世界的方方面面的发展,从商业、科技到医疗、教育、经济、人文等社会的各个领域,我们研究和利用大数据的最终目的是挖掘数据背后的深层价值。 + +在数据分析领域,全部研究对象被称为总体(Population),总体包含大量的数据,数据甚至可能是无限的。很多情况下,我们无法保证能收集和分析总体的所有数据,因此研究者一般基于全部研究对象的一个子集进行数据分析。样本(Sample)是从总体中抽取的个体,是全部研究对象的子集。通过对样本的调查和分析,研究者可以推测总体的情况。比如调查某个群体的金融诚信情况,群体内所有人是总体,我们可以抽取一部分个体作为样本,以此推测群体的金融诚信水平。 + +在大数据技术成熟之前,受限于数据收集、存储和分析能力,样本数量相对较小。大数据技术的成熟让数据存储和计算能力不再是瓶颈,研究者可以在更大规模的数据上,以更快的速度进行数据分析。但数据并非天然有价值,如何对数据“点石成金”非常有挑战性。在金融诚信调查中,如果我们直接询问样本对象,“你是否谎报了家庭资产以获取更大的金融借贷额度?”十之八九,我们得不到真实的答案,但我们可以结合多种渠道的数据来分析该问题,比如结合样本对象的工作经历、征信记录等数据。 + +大数据具有更大的数据量、更快的速度、更多的数据类型等特点。在一定的数据真实性基础上,大数据技术最终要为数据背后的价值服务。 + +随着大数据技术的发展,数据的复杂性越来越高,有人在这5个“V”的基础上,又提出了一些补充内容,比如增加了动态性(Vitality),强调整个数据体系的动态性;增加了可视性(Visualization),强调数据的显性化展现;增加了合法性(Validity),强调数据采集和应用的合法性,特别是对于个人隐私数据的合理使用等;增加了数据在线(Online),强调数据永远在线,能随时被调用和计算。 + +## 1.1.2 大数据分而治之 + +计算机诞生之后,一般是在单台计算机上处理数据。大数据时代到来后,一些传统的数据处理方法无法满足大数据的处理需求。将一组计算机组织到一起形成一个集群,利用集群的力量来处理大数据的工程实践逐渐成为主流。这种使用集群进行计算的方式被称为分布式计算,当前几乎所有的大数据系统都在使用集群进行分布式计算。 + +分布式计算的概念听起来很高深,其背后的思想却十分朴素,即分而治之,又称为分治法(Divide and Conquer)。如图1-2所示,分治法是指将一个原始问题分解为多个子问题,多个子问题分别在多台计算机上求解,借助必要的数据交换和合并策略,将子结果汇总即可求出最终结果的方法。具体而言,不同的分布式系统使用的算法和策略根据所要解决的问题各有不同,但基本上都是将计算拆分,把子问题放到多台计算机上,分而治之地计算求解。分布式计算的每台计算机(物理机或虚拟机)又被称为一个节点。 + +![图1-2 分治法](./img/divide-conquer.png) + +分布式计算已经有很多比较成熟的方案,其中比较有名的有消息传递接口(Message Passing Interface,MPI)和映射归约模型(MapReduce)。 + +### 1. MPI + +MPI是一个“老牌”分布式计算框架,从MPI这个名字也可以看出,MPI主要解决节点间数据通信的问题。在前MapReduce时代,MPI是分布式计算的业界标准。MPI现在依然广泛运用于全球各大超级计算中心、大学、研究机构中,许多物理、生物、化学、能源等基础学科的大规模分布式计算都依赖MPI。图1-3所示为使用MPI在4台服务器上并行计算的示意图。 + +![图1-3 在4台服务器上使用MPI进行并行计算](./img/mpi.png) + +使用MPI编程,需要使用分治法将问题分解成子问题,在不同节点上分而治之地求解。MPI提供了一个在多进程、多节点间进行数据通信的方案,因为绝大多数情况下,在中间求解和最终汇总的过程中,需要对多个节点上的数据进行交换和同步。 + +MPI中最重要的两个操作为数据发送和数据接收,数据发送表示将本进程中某些数据发送给其他进程,数据接收表示接收其他进程的数据。在实际的代码开发过程中,程序员需要自行设计分治算法,将复杂问题分解为子问题,手动调用MPI库,将数据发送给指定的进程。 + +MPI能够以很细的粒度控制数据的通信,这是它的优势,从某些方面而言这也是它的劣势,因为细粒度的控制意味着从分治算法设计、数据通信到结果汇总都需要程序员手动控制。有经验的程序员可以对程序进行底层优化,取得成倍的速度提升。但如果程序员对计算机分布式系统没有太多经验,编码、调试和运行MPI程序的时间成本极高,加上数据在不同节点上分布不均衡和通信延迟等问题,一个节点进程失败将会导致整个程序失败。因此,MPI对大部分程序员来说简直就是“噩梦”。 + +并非所有的程序员都能熟练掌握MPI编程,衡量一个程序的时间成本,不仅要考虑程序运行的时间,也要考虑程序员学习、开发和调试的时间。就像C语言运算速度极快,但是Python却更受欢迎一样,MPI虽然能提供极快的分布式计算速度,但不太接地气。 + +### 2. MapReduce + +为了解决分布式计算学习和使用成本高的问题,研究人员开发出了更简单易用的MapReduce编程模型。MapReduce是Google于2004年推出的一种编程模型,与MPI将所有事情交给程序员控制不同,MapReduce编程模型只需要程序员定义两个操作:Map和Reduce。 + +比起MPI,MapReduce编程模型将更多的中间过程做了封装,程序员只需要将原始问题转化为更高层次的应用程序接口(Application Programming Interface,API),至于原始问题如何分解为更小的子问题、中间数据如何传输和交换、如何将计算扩展到多个节点等一系列细节问题可以交给大数据编程模型来解决。因此,MapReduce相对来说学习门槛更低,使用更方便,编程开发速度更快。 + +图1-4所示为使用MapReduce思想制作三明治的过程,读者可以通过这幅图更好的理解MapReduce。 + +假设我们需要大批量地制作三明治,三明治的每种食材可以分别单独处理,Map阶段将原材料在不同的节点上分别进行处理,生成一些中间食材,Shuffle/Group阶段将不同的中间食材进行组合,Reduce阶段最终将一组中间食材组合成三明治成品。可以看到,这种Map + Shuffle/Group + Reduce的方式就是分治法的一种实现。 + +![图1-4 使用MapReduce制作三明治的过程](./img/mapreduce-sandwichs.jpeg) + +基于MapReduce编程模型,不同的团队分别实现了自己的大数据框架:Hadoop是较早的一种开源实现,如今已经成为大数据领域的业界标杆,之后又出现了Spark和Flink。这些框架提供了编程接口,辅助程序员存储、处理和分析大数据。 + +1.1.1 小节介绍了大数据的5个“V”特点,1.1.2 小节介绍了大数据的分治法。面对海量数据和各不相同的业务逻辑,我们很难使用一种技术或一套方案来解决各类大数据问题。比如,电商平台和视频网站的大数据架构会略有不同。实际上,大数据技术是一整套方案,包括存储、计算和提供在线服务等多个重要部分,而且与数据形态、业务逻辑、提供何种价值等多方面的因素有关。 + +与大数据有关联的组件众多、技术各有不同,限于本书主题和编者能力,无法一一阐述,本书主要从计算层面来介绍大数据的分析和处理方法。 diff --git a/doc/ch-big-data-intro/evolution-of-big-data-processing-platforms.md b/doc/ch-big-data-intro/evolution-of-big-data-processing-platforms.md new file mode 100644 index 0000000..5886aef --- /dev/null +++ b/doc/ch-big-data-intro/evolution-of-big-data-processing-platforms.md @@ -0,0 +1,54 @@ +(evolution-of-big-data-processing-platforms)= +# 从Lambda到Kappa:大数据处理平台的演进 + +前文已经提到,流处理框架经历了3代的更新迭代,大数据处理也随之经历了从Lambda架构到Kappa架构的演进。本节以电商平台的数据分析为例,来解释大数据处理平台如何支持企业在线服务。电商平台会将用户在App或网页的搜索、点击和购买行为以日志的形式记录下来,用户的各类行为形成了一个实时数据流,我们称之为用户行为日志。 + +## 1.4.1 Lambda架构 + +当以 Storm 为代表的第一代流处理框架成熟后,一些互联网公司为了兼顾数据的实时性和准确性,采用图1-12所示的Lambda架构来处理数据并提供在线服务。Lambda架构主要分为3部分:批处理层、流处理层和在线服务层。其中数据流来自Kafka这样的消息队列。 + +![图1-12 Lambda架构](./img/lambda.png) + +1. **批处理层** + 在批处理层,数据流首先会被持久化保存到批处理数据仓库中,积累一段时间后,再使用批处理引擎来进行计算。这个积累时间可以是一小时、一天,也可以是一个月。处理结果最后导入一个可供在线应用系统查询的数据库上。批处理层中的批处理数据仓库可以是HDFS、Amazon S3或其他数据仓库,批处理引擎可以是MapReduce或Spark。 + + 假如电商平台的数据分析部门想查看全网某天哪些商品购买次数最多,可使用批处理引擎对该天数据进行计算。像淘宝、京东这种级别的电商平台,用户行为日志数据量非常大,在这份日志上进行一个非常简单的计算都可能需要几个小时。批处理引擎一般会定时启动,对前一天或前几个小时的数据进行处理,将结果输出到一个数据库中。与动辄几个小时的批处理的处理时间相比,直接查询一个在线数据库中的数据只需要几毫秒。使用批处理生成一个预处理结果,将结果输出到在线服务层的数据库中,是很多企业仍在采用的办法。 + + 这里计算购买次数的例子相对比较简单,在实际的业务场景中,一般需要做更为复杂的统计分析或机器学习计算,比如构建用户画像时,根据用户年龄和性别等人口统计学信息,分析某类用户最有可能购买的是哪类商品,这类计算耗时更长。 + + 批处理层能保证某份数据的结果的准确性,而且即使程序运行失败,直接重启即可。此外,批处理引擎一般扩展性好,即使数据量增多,也可以通过增加节点数量来横向扩展。 + +2. **流处理层** + 很明显,假如整个系统只有一个批处理层,会导致用户必须等待很久才能获取计算结果,一般有几个小时的延迟。电商数据分析部门只能查看前一天的统计分析结果,无法获取当前的结果,这对实时决策来说是一个巨大的时间鸿沟,很可能导致管理者错过最佳决策时机。因此,在批处理层的基础上,Lambda架构增加了一个流处理层,用户行为日志会实时流入流处理层,流处理引擎生成预处理结果,并导入一个数据库。分析人员可以查看前一小时或前几分钟内的数据结果,这大大增强了整个系统的实时性。但数据流会有事件乱序等问题,使用早期的流处理引擎,只能得到一个近似准确的计算结果,相当于牺牲了一定的准确性来换取实时性。 + + 1.3.4小节曾提到,早期的流处理引擎有一些缺点,由于准确性、扩展性和容错性的不足,流处理层无法直接取代批处理层,只能给用户提供一个近似结果,还不能为用户提供一个一致准确的结果。因此Lambda架构中,出现了批处理和流处理并存的现象。 + +3. **在线服务层** + 在线服务层直接面向用户的特定请求,需要将来自批处理层准确但有延迟的预处理结果和流处理层实时但不够准确的预处理结果做融合。在融合过程中,需要不断将流处理层的实时数据覆盖批处理层的旧数据。很多数据分析工具在数据合并上下了不少功夫,如Apache Druid,它可以融合流处理与批处理结果。当然,我们也可以在应用程序中人为控制预处理结果的融合。存储预处理结果的数据库可能是关系型数据库MySQL,也可能是Key-Value键值数据库Redis或HBase。 + +4. **Lambda架构的优缺点** + Lambda架构在实时性和准确性之间做了一个平衡,能够解决很多大数据处理的问题,它的优点如下: + - 批处理的准确度较高,而且在数据探索阶段可以对某份数据试用不同的方法,反复对数据进行实验。另外,批处理的容错性和扩展性较强。 + - 流处理的实时性较强,可以提供一个近似准确的结果。 + + Lambda架构的缺点也比较明显,如下: + - 使用两套大数据处理引擎,如果两套大数据处理引擎的API不同,有任何逻辑上的改动,就需要在两边同步更新,维护成本高,后期迭代的时间周期长。 + - 早期流处理层的结果只是近似准确。 + +## 1.4.2 Kappa架构 + +Kafka的创始人杰•克雷普斯认为在很多场景下,维护一套Lambda架构的大数据处理平台耗时耗力,于是提出在某些场景下,没有必要维护一个批处理层,直接使用一个流处理层即可满足需求,即图1-13所示的Kappa架构。 + +![图1-13 Kappa架构](./img/kappa.png) + + +Kappa架构的兴起主要有如下两个原因: +- Kafka可以保存更长时间的历史数据,它不仅起到消息队列的作用,也可以存储数据,替代数据库。 +- Flink流处理引擎解决了事件乱序下计算结果的准确性问题。 + +Kappa架构相对更简单,实时性更好,所需的计算资源远小于Lambda架构,随着实时处理的需求的不断增长,更多的企业开始使用Kappa架构。 + +Kappa架构的流行并不意味着不再需要批处理,批处理在一些特定场景上仍然有自己的优势。比如,进行一些数据探索、机器学习实验,需要使用批处理来反复验证不同的算法。Kappa架构适用于一些逻辑固定的数据预处理流程,比如统计一个时间段内商品的曝光和购买次数、某些关键词的搜索次数等,这类数据处理需求已经固定,无须反复试验迭代。 + +Flink以流处理见长,但也实现了批处理的API,是一个集流处理与批处理于一体的大数据处理引擎,为Kappa架构提供更可靠的数据处理性能,未来Kappa架构将在更多场景下逐渐替换Lambda架构。 + diff --git a/doc/ch-big-data-intro/exercise-building-data-stream-using-Kafka.md b/doc/ch-big-data-intro/exercise-building-data-stream-using-Kafka.md new file mode 100644 index 0000000..e7c88ad --- /dev/null +++ b/doc/ch-big-data-intro/exercise-building-data-stream-using-Kafka.md @@ -0,0 +1,109 @@ +(exercise-building-data-stream-using-Kafka)= +# 案例实战:使用Kafka构建文本数据流 + +尽管本书主题是Flink,但是对数据流的整个生命周期有一个更全面的认识有助于我们理解大数据和流处理。1.3.3小节简单介绍了Kafka这项技术,本节将介绍如何使用Kafka构建实时文本数据流,读者可以通过本节了解数据流管道的大致结构:数据生产者源源不断地生成数据流,数据流通过消息队列投递,数据消费者异步地对数据流进行处理。 + +## 1.7.1 Kafka和消息队列相关背景知识 + +### 1. 消息队列的功能 + +消息队列一般使用图1-6所示的“生产者-消费者”模型来解决问题:生产者生成数据,将数据发送到一个缓存区域,消费者从缓存区域中消费数据。消息队列可以解决以下问题: + +- **系统解耦**:很多企业内部有众多系统,一个App也包含众多模块,如果将所有的系统和模块都放在一起作为一个庞大的系统来开发,未来则会很难维护和扩展。如果将各个模块独立出来,模块之间通过消息队列来通信,未来可以轻松扩展每个独立模块。另外,假设没有消息队列,M个生产者和N个消费者通信,会产生M×N个数据管道,消息队列将这个复杂度降到了M+N。 +- **异步处理**:同步是指如果模块A向模块B发送消息,必须等待返回结果后才能执行接下来的业务逻辑。异步是消息发送方模块A无须等待返回结果即可继续执行,只需要向消息队列中发送消息,至于谁去处理这些消息、消息等待多长时间才能被处理等一系列问题,都由消费者负责。异步处理更像是发布通知,发送方不用关心谁去接收通知、如何对通知做出响应等问题。 +- **流量削峰**:电商促销、抢票等场景会对系统造成巨大的压力,瞬时请求暴涨,消息队列的缓存就像一个蓄水池,以很低的成本将上游的洪峰缓存起来,下游的数据处理模块按照自身处理能力从缓存中拉取数据,避免数据处理模块崩溃。 +- **数据冗余**:很多情况下,下游的数据处理模块可能发生故障,消息队列将数据缓存起来,直到数据被处理,一定程度上避免了数据丢失风险。 + +Kafka作为一个消息队列,主要提供如下3种核心能力: + +- 为数据的生产者提供发布功能,为数据的消费者提供订阅功能,即传统的消息队列的能力。 +- 将数据流缓存在缓存区域,为数据提供容错性,有一定的数据存储能力。 +- 提供了一些轻量级流处理能力。 + +可见Kafka不仅是一个消息队列,也有数据存储和流处理的功能,确切地说,Kafka是一个流处理系统。 + +### 2. Kafka的一些核心概念 + +Kafka涉及不少概念,包括Topic、Producer、Consumer等,这里从Flink流处理的角度出发,只对与流处理关系密切的核心概念做简单介绍。 + +- **Topic**:Kafka按照Topic来区分不同的数据。以淘宝这样的电商平台为例,某个Topic发布买家用户在电商平台的行为日志,比如搜索、点击、聊天、购买等行为;另外一个Topic发布卖家用户在电商平台上的行为日志,比如上新、发货、退货等行为。 +- **Producer**:多个Producer将某份数据发布到某个Topic下。比如电商平台的多台线上服务器将买家行为日志发送到名为user_behavior的Topic下。 +- **Consumer**:多个Consumer被分为一组,名为Consumer Group,一组Consumer Group订阅一个Topic下的数据。通常我们可以使用Flink编写的程序作为Kafka的Consumer来对一个数据流做处理。 + +## 1.7.2 使用Kafka构建一个文本数据流 + +### 1. 下载和安装 + +如前文所述,绝大多数的大数据框架基于Java,因此在进行开发之前要先搭建Java编程环境,主要是下载和配置Java开发工具包(Java Development Kit,JDK)。网络上针对不同操作系统的相关教程已经很多,这里不赘述。 + +从Kafka官网下载二进制文件形式的软件包,软件包扩展名为 .tgz。Windows用户可以使用7Zip或WinRAR软件解压 .tgz文件,Linux和macOS用户需要使用命令行工具,进入该下载目录。 + +```bash +$ tar -xzf kafka_2.12-2.3.0.tgz +$ cd kafka_2.12-2.3.0 +``` + +**注意**: + +`$`符号表示该行命令在类UNIX操作系统(macOS和Linux)命令行中执行,而不是在Python交互命令界面或其他任何交互界面中。Windows的命令行提示符是大于号`>`。 + +解压之后的文件中,`bin`目录默认为Linux和macOS设计。Windows用户要进入`bin\windows\`来启动相应脚本,且脚本文件扩展名要改为`.bat`。 + +### 2. 启动服务 + +Kafka使用ZooKeeper来管理集群,因此需要先启动ZooKeeper。刚刚下载的Kafka包里已经包含了ZooKeeper的启动脚本,可以使用这个脚本快速启动一个ZooKeeper服务。 + +```bash +$ bin/zookeeper-server-start.sh config/zookeeper.properties +``` + +启动成功后,对应日志将被输出到屏幕上。 + +接下来再开启一个命令行会话,启动Kafka: + +```bash +$ bin/kafka-server-start.sh config/server.properties +``` + +以上两个操作均使用`config`文件夹下的默认配置文件,需要注意配置文件的路径是否写错。生产环境中的配置文件比默认配置文件复杂得多。 + +### 3. 创建Topic + +开启一个命令行会话,创建一个名为`Shakespeare`的Topic: + +```bash +$ bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic Shakespeare +``` + +也可以使用命令查看已有的Topic: + +```bash +$ bin/kafka-topics.sh --list --bootstrap-server localhost:9092 +Shakespeare +``` + +### 4. 发送消息 + +接下来我们模拟 Producer,假设这个 Producer 是莎士比亚(Shakespeare)本人,它不断向“Shakespeare”这个Topic发送自己的最新作品: + +```bash +$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic Shakespeare +>To be, or not to be, that is the question: +``` + +每一行作为一条消息事件,被发送到了Kafka集群上,虽然这个集群只有本机这一台服务器。 + +### 5. 消费数据 + +另外一些人想了解莎士比亚向Kafka发送过哪些新作,所以需要使用一个Consumer来消费刚刚发送的数据。我们开启一个命令行会话来模拟Consumer: + +```bash +$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic Shakespeare --from-beginning +To be, or not to be, that is the question: +``` + +Producer端和Consumer端在不同的命令行会话中,我们可以在Producer端的命令行会话里不断输入一些文本。切换到Consumer端后,可以看到相应的文本被发送了过来。 + +至此,我们模拟了一个实时数据流数据管道:不同人可以创建Topic,发布属于自己的内容;其他人可以订阅一个或多个Topic,根据需求设计后续处理逻辑。 + +使用Flink做流处理时,我们很可能以消息队列作为输入数据源,进行一定处理后,再输出到消息队列、数据库或其他组件上。 diff --git a/doc/ch-big-data-intro/index.md b/doc/ch-big-data-intro/index.md new file mode 100644 index 0000000..1eefb9b --- /dev/null +++ b/doc/ch-big-data-intro/index.md @@ -0,0 +1,13 @@ +# 大数据技术概述 + +牛津大学教授维克托·迈尔-舍恩伯格(Viktor Mayer-Schönberger)指出,大数据带来的信息风暴正在改变我们的生活、工作和思维。理解大数据并对这些数据进行有效的处理和分析是企业和政府的机遇,更是一种挑战。数据流的处理必须满足高吞吐和低延迟的特性,Apache Flink(以下简称Flink)是一种针对数据流的大数据处理框架。开源领域比较知名的大数据处理框架Apache Hadoop(以下简称Hadoop)和Apache Spark(以下简称Spark),主要专注于批处理。 +读完本章之后,读者可以了解以下内容。 +- 大数据的特点、大数据分而治之的处理思想。 +- 批处理和流处理的区别。 +- 流处理的基础概念。 +- 流处理框架的技术更迭和架构演进。 +- Flink开发的常用编程语言。 + + +```{tableofcontents} +``` \ No newline at end of file diff --git a/doc/ch-big-data-intro/representative-big-data-technologies.md b/doc/ch-big-data-intro/representative-big-data-technologies.md new file mode 100644 index 0000000..f7c6408 --- /dev/null +++ b/doc/ch-big-data-intro/representative-big-data-technologies.md @@ -0,0 +1,77 @@ +(representative-big-data-technologies)= +# 代表性大数据技术 + +MapReduce编程模型的提出为大数据分析和处理开创了一条先河,其后涌现出一批知名的开源大数据技术,本节主要对一些流行的技术和框架进行简单介绍。 + +## 1.3.1 Hadoop + +2004年,Hadoop的创始人道格·卡廷(Doug Cutting)和麦克·卡法雷拉(Mike Cafarella)受MapReduce编程模型和Google File System等技术的启发,对其中提及的思想进行了编程实现,Hadoop的名字来源于道格·卡廷儿子的玩具大象。由于道格·卡廷后来加入了雅虎,并在雅虎工作期间做了大量Hadoop的研发工作,因此Hadoop也经常被认为是雅虎开源的一款大数据框架。时至今日,Hadoop不仅是整个大数据领域的先行者和领航者,更形成了一套围绕Hadoop的生态圈,Hadoop和它的生态圈是绝大多数企业首选的大数据解决方案。图1-7展示了Hadoop生态圈一些流行组件。 + +Hadoop生态圈的核心组件主要有如下3个。 + +- **Hadoop MapReduce**:Hadoop版本的MapReduce编程模型,可以处理海量数据,主要面向批处理。 +- **HDFS**:HDFS(Hadoop Distributed File System)是Hadoop提供的分布式文件系统,有很好的扩展性和容错性,为海量数据提供存储支持。 +- **YARN**:YARN(Yet Another Resource Negotiator)是Hadoop生态圈中的资源调度器,可以管理一个Hadoop集群,并为各种类型的大数据任务分配计算资源。 + +这三大组件中,数据存储在HDFS上,由MapReduce负责计算,YARN负责集群的资源管理。除了三大核心组件,Hadoop生态圈还有很多其他著名的组件,部分如下。 + +- **Hive**:借助Hive,用户可以编写结构化查询语言(Structured Query Language,SQL)语句来查询HDFS上的结构化数据,SQL语句会被转化成MapReduce运行。 +- **HBase**:HDFS可以存储海量数据,但访问和查询速度比较慢,HBase可以提供给用户毫秒级的实时查询服务,它是一个基于HDFS的分布式数据库。HBase最初受Google Bigtable技术的启发。 +- **Kafka**:Kafka是一款流处理框架,主要用作消息队列。 +- **ZooKeeper**:Hadoop生态圈中很多组件使用动物来命名,形成了一个大型“动物园”,ZooKeeper是这个动物园的管理者,主要负责分布式环境的协调。 + +![图1-7 Hadoop生态圈](./img/) + +## 1.3.2 Spark + +2009年,Spark诞生于加州大学伯克利分校,2013年被捐献给Apache基金会。实际上,Spark的创始团队本来是为了开发集群管理框架Apache Mesos(以下简称Mesos)的,其功能类似YARN,Mesos开发完成后,需要一个基于Mesos的产品运行在上面以验证Mesos的各种功能,于是他们接着开发了Spark。Spark有火花、鼓舞之意,创始团队希望用Spark来证明在Mesos上从零开始创造一个项目非常简单。 + +Spark是一款大数据处理框架,其开发初衷是改良Hadoop MapReduce的编程模型和提高运行速度,尤其是提升大数据在机器学习方向上的性能。与Hadoop相比,Spark的改进主要有如下两点。 + +- **易用性**:MapReduce模型比MPI更友好,但仍然不够方便。因为并不是所有计算任务都可以被简单拆分成Map和Reduce,有可能为了解决一个问题,要设计多个MapReduce任务,任务之间相互依赖,整个程序非常复杂,导致代码的可读性和可维护性差。Spark提供更加方便易用的接口,提供Java、Scala、Python和R语言等的API,支持SQL、机器学习和图计算,覆盖了绝大多数计算场景。 +- **速度快**:Hadoop的Map和Reduce的中间结果都需要存储到磁盘上,而Spark尽量将大部分计算放在内存中。加上Spark有向无环图的优化,在官方的基准测试中,Spark比Hadoop快一百倍以上。 + +Spark的核心在于计算,主要目的在于优化Hadoop MapReduce计算部分,在计算层面提供更细致的服务。 + +Spark并不能完全取代Hadoop,实际上,从图1-7可以看出,Spark融入了Hadoop生态圈,成为其中的重要一员。一个Spark任务很可能依赖HDFS上的数据,向YARN申请计算资源,将结果输出到HBase上。当然,Spark也可以不用依赖这些组件,独立地完成计算。 + +![图1-8 Spark生态圈](./img/) + +Spark主要面向批处理需求,因其优异的性能和易用的接口,Spark已经是批处理界绝对的“王者”。Spark的子模块Spark Streaming提供了流处理的功能,它的流处理主要基于mini-batch的思想。如图1-9所示,Spark Streaming将输入数据流切分成多个批次,每个批次使用批处理的方式进行计算。因此,Spark是一款集批处理和流处理于一体的处理框架。 + +![图1-9 Spark Streaming mini-batch处理](./img/) + +## 1.3.3 Apache Kafka + +2010年,LinkedIn开始了其内部流处理框架的开发,2011年将该框架捐献给了Apache基金会,取名Apache Kafka(以下简称Kafka)。Kafka的创始人杰·克雷普斯(Jay Kreps)觉得这个框架主要用于优化读写,应该用一个作家的名字来命名,加上他很喜欢作家卡夫卡的文学作品,觉得这个名字对一个开源项目来说很酷,因此取名Kafka。 + +Kafka也是一种面向大数据领域的消息队列框架。在大数据生态圈中,Hadoop的HDFS或Amazon S3提供数据存储服务,Hadoop MapReduce、Spark和Flink负责计算,Kafka常常用来连接不同的应用系统。 + +如图1-10所示,企业中不同的应用系统作为数据生产者会产生大量数据流,这些数据流还需要进入不同的数据消费者,Kafka起到数据集成和系统解耦的作用。系统解耦是让某个应用系统专注于一个目标,以降低整个系统的维护难度。在实践上,一个企业经常拆分出很多不同的应用系统,系统之间需要建立数据流管道(Stream Pipeline)。假如没有Kafka的消息队列,M个生产者和N个消费者之间要建立M×N个点对点的数据流管道,Kafka就像一个中介,让数据管道的个数变为M+N,大大减小了数据流管道的复杂程度。 + +![图1-10 Kafka可以连接多个应用系统](./img/) + +从批处理和流处理的角度来讲,数据流经Kafka后会持续不断地写入HDFS,积累一段时间后可提供给后续的批处理任务,同时数据流也可以直接流入Flink,被用于流处理。 + +随着流处理的兴起,Kafka不甘心只做一个数据流管道,开始向轻量级流处理方向努力,但相比Spark和Flink这样的计算框架,Kafka的主要功能侧重在消息队列上。 + +## 1.3.4 Flink + +Flink是由德国3所大学发起的的学术项目,后来不断发展壮大,并于2014年年末成为Apache顶级项目之一。在德语中,“flink”表示快速、敏捷,以此来表征这款计算框架的特点。 + +Flink主要面向流处理,如果说Spark是批处理界的“王者”,那么Flink就是流处理领域冉冉升起的“新星”。流处理并不是一项全新的技术,在Flink之前,不乏流处理引擎,比较著名的有Storm、Spark Streaming,图1-11展示了流处理框架经历的三代演进。 + +2011年成熟的Apache Strom(以下简称Storm)是第一代被广泛采用的流处理引擎。它是以数据流中的事件为最小单位来进行计算的。以事件为单位的框架的优势是延迟非常低,可以提供毫秒级的延迟。流处理结果依赖事件到达的时序准确性,Storm并不能保障处理结果的一致性和准确性。Storm只支持至少一次(At-Least-Once)和至多一次(At-Most-Once),即数据流里的事件投递只能保证至少一次或至多一次,不能保证只有一次(Exactly-Once)。在多项基准测试中,Storm的数据吞吐量和延迟都远逊于Flink。对于很多对数据准确性要求较高的应用,Storm有一定劣势。此外,Storm不支持SQL,不支持中间状态(State)。 + +图1-11 流处理框架演进 + +2013年成熟的Spark Streaming是第二代被广泛采用的流处理框架。1.3.2小节中提到,Spark是“一统江湖”的大数据处理框架,Spark Streaming采用微批次(mini-batch)的思想,将数据流切分成一个个小批次,一个小批次里包含多个事件,以接近实时处理的效果。这种做法保证了“Exactly-Once”的事件投递效果,因为假如某次计算出现故障,重新进行该次计算即可。Spark Streaming的API相比第一代流处理框架更加方便易用,与Spark批处理集成度较高,因此Spark可以给用户提供一个流处理与批处理一体的体验。但因为Spark Streaming以批次为单位,每次计算一小批数据,比起以事件为单位的框架来说,延迟从毫秒级变为秒级。 + +与前两代引擎不同,在2015年前后逐渐成熟的Flink是一个支持在有界和无界数据流上做有状态计算的大数据处理框架。它以事件为单位,支持SQL、状态、水位线(Watermark)等特性,支持“Exactly-Once”。比起Storm,它的吞吐量更高,延迟更低,准确性能得到保障;比起Spark Streaming,它以事件为单位,达到真正意义上的实时计算,且所需计算资源相对更少。具体而言,Flink的优点如下。 + +- 支持事件时间(Event Time)和处理时间(Processing Time)多种时间语义。即使事件乱序到达,Event Time也能提供准确和一致的计算结果。Procerssing Time适用于对延迟敏感的应用。 +- Exactly-Once投递保障。 +- 毫秒级延迟。 +- 可以扩展到上千台节点、在阿里巴巴等大公司的生产环境中进行过验证。 +- 易用且多样的API,包括核心的DataStream API和DataSet API以及Table API和SQL。 +- 可以连接大数据生态圈各类组件,包括Kafka、Elasticsearch、JDBC、HDFS和Amazon S3。可以运行在Kubernetes、YARN、Mesos和独立(Standalone)集群上。 \ No newline at end of file diff --git a/doc/ch-big-data-intro/stream-processing-basics.md b/doc/ch-big-data-intro/stream-processing-basics.md new file mode 100644 index 0000000..f3a6090 --- /dev/null +++ b/doc/ch-big-data-intro/stream-processing-basics.md @@ -0,0 +1,87 @@ +(stream-processing-basics)= +# 流处理基础概念 + +前文已经多次提到,在某些场景下,流处理打破了批处理的一些局限。Flink作为一款以流处理见长的大数据引擎,相比其他流处理引擎具有众多优势。本节将对流处理的一些基本概念进行细化,这些概念是入门流处理的必备基础,至此你将正式进入数据流的世界。 + +## 1.5.1 延迟和吞吐 + +在批处理场景中,我们主要通过一次计算的总耗时来评价性能。在流处理场景,数据源源不断地流入系统,大数据框架对每个数据的处理越快越好,大数据框架能处理的数据量越大越好。例如1.2.3小节中提到的股票交易案例,如果系统只能处理一两只股票或处理时间长达一天,那说明这个系统非常不靠谱。衡量流处理的“快”和“量”两方面的性能,一般用延迟(Latency)和吞吐(Throughput)这两个指标。 + +### 1. 延迟 + +延迟表示一个事件被系统处理的总时间,一般以毫秒为单位。根据业务不同,我们一般关心平均延迟(Average Latency)和分位延迟(Percentile Latency)。假设一个食堂的自助取餐流水线是一个流处理系统,每个就餐者前来就餐是它需要处理的事件,从就餐者到达食堂到他拿到所需菜品并付费离开的总耗时,就是这个就餐者的延迟。如果正赶上午餐高峰期,就餐者极有可能排队,这个排队时间也要算在延迟中。例如,99 分位延迟表示对所有就餐者的延迟进行统计和排名,取排名第99%位的就餐者延迟。一般商业系统更关注分位延迟,因为分位延迟比平均延迟更能反映这个系统的一些潜在问题。还是以食堂的自助餐流水线为例,该流水线的平均延迟可能不高,但是在就餐高峰期,延迟一般会比较高。如果延迟过高,部分就餐者会因为等待时间过长而放弃排队,用户体验较差。通过检查各模块分位延迟,能够快速定位到哪个模块正在“拖累”整个系统的性能。 + +延迟对于很多流处理系统非常重要,比如欺诈检测系统、告警监控系统等。Flink可以将延迟降到毫秒级别。如果用mini-batch的思想处理同样的数据流,很可能有分钟级到小时级的延迟,因为批处理引擎必须等待一批数据达到才开始进行计算。 + +### 2. 吞吐 + +吞吐表示一个系统最多能处理多少事件,一般以单位时间处理的事件数量为标准。需要注意的是,吞吐除了与引擎自身设计有关,也与数据源发送过来的事件数据量有关,有可能流处理引擎的最大吞吐量远小于数据源的数据量。比如,自助取餐流水线可能在午餐时间的需求最高,很可能出现大量排队的情况,但另外的时间几乎不需要排队等待。假设一天能为1 000个人提供就餐服务,共计10小时,那它的平均吞吐量为100人/小时;仅午间2小时的高峰期就提供了600人,它的峰值吞吐量是300人/小时。比起平均吞吐量,峰值吞吐量更影响用户体验,如果峰值吞吐量低,会导致就餐者等待时间过长而放弃排队。排队的过程被称作缓存(Buffering)。如果排队期间仍然有大量数据进入缓存,很可能超出系统的极限,就会出现反压(Backpressure)问题,这时候就需要一些优雅的策略来处理类似问题,否则会造成系统崩溃,用户体验较差。 + +### 3. 延迟与吞吐 + +延迟与吞吐其实并不是相互孤立的,它们相互影响。如果延迟高,那么很可能吞吐较低,系统处理不了太多数据。为了优化这两个指标,首先提高自助取餐流水线的行进速度,加快取餐各个环节的进程。当用户量大到超过流水线的瓶颈时,需要再增加一个自助取餐流水线。这就是当前大数据系统都在采用的两种加速方式,第一是优化单节点内的计算速度,第二是使用并行策略,分而治之地处理数据。如果一台计算机做不了或做得不够快,那就用更多的计算机一起来做。 + +综上,延迟和吞吐是衡量流处理引擎的重要指标。如何保证流处理系统保持高吞吐和低延迟是一项非常有挑战性的工作。 + +## 1.5.2 窗口与时间 + +### 1. 不同窗口模式 + +比起批处理,流处理对窗口(Window)和时间概念更为敏感。在批处理场景下,数据已经按照某个时间维度被分批次地存储了。一些公司经常将用户行为日志按天存储,一些开放数据集都会说明数据采集的时间始末。因此,对于批处理任务,处理一个数据集,其实就是对该数据集对应的时间窗口内的数据进行处理。在流处理场景下,数据以源源不断的流的形式存在,数据一直在产生,没有始末。我们要对数据进行处理时,往往需要明确一个时间窗口,比如,数据在“每秒”“每小时”“每天”的维度下的一些特性。窗口将数据流切分成多个数据块,很多数据分析都是在窗口上进行操作,比如连接、聚合以及其他时间相关的操作。 + +图1-14展示了3种常见的窗口形式:滚动窗口、滑动窗口、会话窗口。 + +![图1-14 3种常见的窗口形式](./img/) + +- **滚动窗口(Tumbling Window)**:模式一般定义一个固定的窗口长度,长度是一个时间间隔,比如小时级的窗口或分钟级的窗口。窗口像车轮一样,滚动向前,任意两个窗口之间不会包含同样的数据。 +- **滑动窗口(Sliding Window)**:模式也设有一个固定的窗口长度。假如我们想每分钟开启一个窗口,统计10分钟内的股票价格波动,就使用滑动窗口模式。当窗口的长度大于滑动的间隔,可能会导致两个窗口之间包含同样的事件。其实,滚动窗口模式是滑动窗口模式的一个特例,滚动窗口模式中滑动的间隔正好等于窗口的大小。 +- **会话窗口(Session Window)**:模式的窗口长度不固定,而是通过一个间隔来确定窗口,这个间隔被称为会话间隔(Session Gap)。当两个事件之间的间隔大于会话间隔,则两个事件被划分到不同的窗口中;当事件之间的间隔小于会话间隔,则两个事件被划分到同一窗口。 + +### 2. 时间语义 + +#### (1) Event Time和Processing Time + +“时间”是平时生活中最常用的概念之一,在流处理中需要额外注意它,因为时间的语义不仅与窗口有关,也与事件乱序、触发计算等各类流处理问题有关。常见的时间语义如下。 + +- **Event Time**:事件实际发生的时间。 +- **Processing Time**:事件被流处理引擎处理的时间。 + +对于一个事件,自其发生起,Event Time就已经确定不会改变。因各类延迟、流处理引擎各个模块先后处理顺序等因素,不同节点、系统内不同模块、同一数据不同次处理都会产生不同的Processing Time。 + +#### (2) “一分钟”真的是一分钟吗? + +在很多应用场景中,时间有着不同的语义,“一分钟”真的是一分钟吗?很多手机游戏中多玩家在线实时竞技,假设我们在玩某款手机游戏,该游戏将数据实时发送给游戏服务器,服务器计算一分钟内玩家的一些操作,这些计算影响用户该局游戏的最终得分。当游戏正酣,我们进入了电梯,手机信号丢失,一分钟后才恢复信号;幸好手机在电梯期间缓存了掉线时的数据,并在信号恢复后将缓存数据传回了服务器,图1-15展示了这个场景的流处理过程。在丢失信号的这段时间,你的数据没有被计算进去,显然这样的计算不公平。当信号恢复时,数据重传到服务器,再根据Event Time重新计算一次,那就非常公平了。我们可以根据Event Time复现一个事件序列的实际顺序。因此,使用Event Time是最准确的。 + +![图1-15 数据传输过程恰好遇到信号丢失](./img/signal.png) + +### 3. Watermark + +虽然使用Event Time更准确,但问题在于,因为各种不可控因素,事件上报会有延迟,那么最多要等待多长时间呢?从服务器的角度来看,在事件到达之前,我们也无法确定是否有事件发生了延迟,如何设置等待时间是一个很难的问题。比如刚才的例子,我们要统计一分钟内的实时数据,考虑到事件的延迟,如何设置合理的等待时间,以等待一分钟内所有事件都到达服务器?也正因为这个问题,流处理与批处理在准确性上有差距,因为批处理一般以更长的一段时间为一个批次,一个批次内延迟上报的数据比一个流处理时间窗口内延迟上报的数据相对更少。比如电商平台上,对于计算一件商品每分钟点击次数,使用一天的总数除以分钟数,比使用一分钟时间窗口实时的点击次数更准确。可以看到,数据的实时性和准确性二者不可得兼,必须取一个平衡。 + +Watermark是一种折中解决方案,它假设某个时间点上,不会有比这个时间点更晚的上报数据。当流处理引擎接收到一个Watermark后,它会假定之后不会再接收到这个时间窗口的内容,然后会触发对当前时间窗口的计算。比如,一种Watermark 策略等待延迟上报的时间非常短,这样能保证低延迟,但是会导致错误率上升。在实际应用中,Watermark设计为多长非常有挑战性。还是以手机游戏为例,系统不知道玩家这次掉线的原因是什么,可能是在穿越隧道,可能是有事退出了该游戏,还有可能是坐飞机进入飞行模式。 + +那既然Event Time似乎可以解决一切问题,为什么还要使用Processing Time?前文也提到了,为了处理延迟上报或事件乱序,需要使用一些机制来等待,这样会导致延迟提高。某些场景可能对准确性要求不高,但是对实时性要求更高,在这些场景下使用Processing Time就更合适一些。 + +## 1.5.3 状态与检查点 + +状态是流处理区别于批处理的特有概念。如果我们对一个文本数据流进行处理,把英文大写字母都改成英文小写字母,这种处理是无状态的,即系统不需要记录额外的信息。如果我们想统计这个数据流一分钟内的单词出现次数,一方面要处理每一瞬间新流入的数据,另一方面要保存之前一分钟内已经进入系统的数据,额外保存的数据就是状态。图1-16展示了无状态和有状态两种不同类型的计算。 + +![图1-16 无状态计算和有状态计算](./img/state-stateless.png) + +状态在流处理中经常被用到。再举一个温度报警的例子,当系统在监听到“高温”事件后10分钟内又监听到“冒烟”的事件,系统必须及时报警。在这个场景下,流处理引擎把“高温”的事件作为状态记录下来,并判断这个状态接下来十分钟内是否有“冒烟”事件。 + +流处理引擎在数据流上做有状态计算主要有以下挑战。 + +- 设计能够管理状态的并行算法极具挑战。前文已经多次提到,大数据需要在多节点上分布式计算,一般将数据按照某个Key进行切分,将相同的Key切分到相同的节点上,系统按照Key维护对应的状态。 +- 如果状态数据不断增长,最后就会造成数据爆炸。因此可使用一些机制来限制状态的数据总量,或者将状态数据从内存输出到磁盘或文件系统上,持久化保存起来。 +- 系统可能因各种错误而出现故障,重启后,必须能够保证之前保存的状态数据也能恢复,否则重启后很多计算结果有可能是错误的。 + +检查点(Checkpoint)机制其实并不是一个新鲜事物,它广泛存在于各类计算任务上,主要作用是将中间数据保存下来。当计算任务出现问题,重启后可以根据Checkpoint中保存的数据重新恢复任务。在流处理中,Checkpoint主要保存状态数据。 + +## 1.5.4 数据一致性保障 + +流处理任务可能因为各种原因出现故障,比如数据量暴涨导致内存溢出、输入数据发生变化而无法解析、网络故障、集群维护等。事件进入流处理引擎,如果遇到故障并重启,该事件是否被成功处理了呢?一般有如下3种结果。 + +- **At-Most-Once**:每个事件最多被处理一次,也就是说,有可能某些事件直接被丢弃,不进行任何处理。这种投递保障最不安全,因为一个流处理系统完全可以把接收到的所有事件都丢弃。 +- **At-Least-Once**:无论遇到何种状况,流处理引擎能够保证接收到的事件至少被处理一次,有些事件可能被处理多次。例如,我们统计文本数据流中的单词出现次数,事件被处理多次会导致统计结果并不准确。 +- **Exactly-Once**:无论是否有故障重启,每个事件只被处理一次。Exactly-Once意味着事件不能有任何丢失,也不能被多次处理。比起前两种保障,Exactly-Once的实现难度非常高。如遇故障重启,Exactly-Once就必须确认哪些事件已经被处理、哪些还未被处理。Flink在某些情况下能提供Exactly-Once的保障。 diff --git a/doc/ch-programming-basics/exercise-Flink-development-environment.md b/doc/ch-programming-basics/exercise-Flink-development-environment.md new file mode 100644 index 0000000..feeee0e --- /dev/null +++ b/doc/ch-programming-basics/exercise-Flink-development-environment.md @@ -0,0 +1,262 @@ +(exercise-Flink-development-environment)= +# 2.4 案例实战 Flink开发环境搭建 + +本案例实战主要带领读者完成对Flink开发环境的搭建。 + +## 2.4.1 准备所需软件 + +在1.7节中我们简单提到了Kafka的安装部署所需的软件环境,这里我们再次梳理一下Flink开发所需的软件环境。 + +1. **操作系统** + - 目前,我们可以在Linux、macOS和Windows操作系统上开发和运行Flink。类UNIX操作系统(Linux或macOS)是大数据首选的操作系统,它们对Flink的支持更好,适合进行Flink学习和开发。后文会假设读者已经拥有了一个类UNIX操作系统。Windows用户为了构建一个类UNIX环境,可以使用专门为Linux操作系统打造的子系统(Windows subsystem for Linux,即WSL)或者是Cygwin,又或者创建一个虚拟机,在虚拟机中安装Linux操作系统。 + +2. **JDK** + - 和Kafka一样,Flink开发基于JDK,因此也需要提前安装好JDK 1.8+ (Java 8或更高的版本),配置好Java环境变量。 + +3. **其他工具** + - 其他的工具因开发者习惯不同来安装,不是Flink开发所必需的,但这里仍然建议提前安装好以下工具。 + - **Apache Maven 3.0+** + - Apache Maven是一个项目管理工具,可以对Java或Scala项目进行构建及依赖管理,是进行大数据开发必备的工具。这里推荐使用Maven是因为Flink源码工程和本书的示例代码工程均使用Maven进行管理。 + - **IntelliJ IDEA** + - IntelliJ IDEA是一个非常强大的编辑器和开发工具,内置了Maven等一系列工具,是大数据开发必不可少的利器。Intellij IDEA本来是一个商业软件,它提供了社区免费版本,免费版本已经基本能满足绝大多数的开发需求。 + - 除IntelliJ IDEA之外,还有Eclipse IDE或NetBeans IDE等开发工具,读者可以根据自己的使用习惯选择。由于IntelliJ IDEA对Scala的支持更好,本书建议读者使用IntelliJ IDEA。 + +## 2.4.2 下载并安装Flink + +从Flink官网下载编译好的Flink程序,把下载的.tgz压缩包放在你想放置的目录。在下载时,Flink提供了不同的选项,包括Scala 2.11、Scala 2.12、源码版等。其中,前两个版本是Flink官方提供的可执行版,解压后可直接使用,无须从源码开始编译打包。Scala不同版本间兼容性较差,对于Scala开发者来说,需要选择自己常用的版本,对于Java开发者来说,选择哪个Scala版本区别不大。本书写作时,使用的是Flink 1.11和Scala 2.11,读者可以根据自身情况下载相应版本。 + +按照下面的方式,解压该压缩包,进入解压目录,并启动Flink集群。 + +```bash +$ tar -zxvf flink-1.11.2-bin-scala_2.11.tgz # 解压 +$ cd flink-1.11.2-bin-scala_2.11 # 进入解压目录 +$ ./bin/start-cluster.sh # 启动 Flink 集群 +``` + +成功启动后,打开浏览器,输入`http://localhost:8081`,可以进入Flink集群的仪表盘(WebUI),如图2-4所示。Flink WebUI可以对Flink集群进行管理和监控。 + +## 2.4.3 创建Flink工程 + +我们使用Maven从零开始创建一个Flink工程。 + +```bash +$ mvn archetype:generate \ + -DarchetypeGroupId=org.apache.flink \ + -DarchetypeArtifactId=flink-quickstart-java \ + -DarchetypeVersion=1.11.2 \ + -DgroupId=com.myflink \ + -DartifactId=flink-study-scala \ + -Dversion=0.1 \ + -Dpackage=quickstart \ + -DinteractiveMode=false +``` + +archetype是Maven提供的一种项目模板,是别人提前准备好了的项目的结构框架,用户只需要使用Maven工具下载这个模板,在这个模板的基础上丰富并完善代码逻辑。主流框架一般都准备好了archetype,如Spring、Hadoop等。 + +不熟悉Maven的读者可以先使用IntelliJ IDEA内置的Maven工具,熟悉Maven的读者可直接跳过这部分。 + +如图2-5所示,在IntelliJ IDEA里依次单击“File”→“New”→“Project”,创建一个新工程。 + +如图2-6所示,选择左侧的“Maven”,并勾选“Create from archetype”,并单击右侧的“Add Archetype”按钮。 + +如图2-7所示,在弹出的窗口中填写archetype信息。其中GroupId为org.apache.flink,ArtifactId为flink-quickstart-java,Version为1.11.2,然后单击“OK”。这里主要是告诉Maven去资源库中下载哪个版本的模板。随着Flink的迭代开发,Version也在不断更新,读者可以在Flink的Maven资源库中查看最新的版本。GroupId、ArtifactId、Version可以唯一表示一个发布出来的Java程序包。配置好后,单击Next按钮进入下一步。 + +如图2-8所示,这一步是建立你自己的Maven工程,以区别其他Maven工程,GroupId是你的公司或部门名称(可以随意填写),ArtifactId是工程发布时的Java归档(Java Archive,JAR)包名,Version是工程的版本。这些配置主要用于区别不同公司所发布的不同包,这与Maven和版本控制相关,Maven的教程中都会介绍这些概念,这里不赘述。 + +接下来可以继续单击“Next”按钮,注意最后一步选择你的工程所在的磁盘位置,单击“Finish”按钮,如图2-9所示。至此,一个Flink模板就下载好了。 + +工程结构如图2-10所示。左侧的“Project”栏是工程结构,其中src/main/java文件夹是Java代码文件存放位置,src/main/scala是Scala代码文件存放位置。我们可以在StreamingJob这个文件上继续修改,也可以重新创建一个新文件。 + +注意,开发前要单击右下角的“Import Changes”,让Maven导入所依赖的包。 + +## 2.4.4 调试和运行Flink程序 + +我们创建一个新的文件,名为WordCountKafkaInStdOut.java,开始编写第一个Flink程序—流式词频统计(WordCount)程序。这个程序接收一个Kafka文本数据流,进行词频统计,然后输出到标准输出上。这里先不对程序做深入分析,后文中将会做更详细的解释。 + +首先要设置Flink的运行环境。 + +```java +// 设置Flink运行环境 +StreamExecutionEnvironment env = +StreamExecutionEnvironment.getExecutionEnvironment(); +``` + +设置Kafka相关参数,连接对应的服务器和端口号,读取名为Shakespeare的Topic中的数据源,将数据源命名为stream。 + +```java +// Kafka参数 +Properties properties = new Properties(); +properties.setProperty("bootstrap.servers", "localhost:9092"); +properties.setProperty("group.id", "flink-group"); +String inputTopic = "Shakespeare"; +// Source +FlinkKafkaConsumer consumer = + new FlinkKafkaConsumer(inputTopic, new SimpleStringSchema(), properties); +DataStream stream = env.addSource(consumer); +``` + +使用Flink API处理这个数据流。 + +```java +// Transformation +// 使用Flink API对输入流的文本进行操作 +// 切词转换、分组、设置时间窗口、聚合 +DataStream> wordCount = stream + .flatMap((String line, Collector> collector) -> { + String[] tokens = line.split("\\s"); + // 输出结果 + for (String token : tokens) { + if (token.length() > 0) { + collector.collect(new Tuple2<>(token, 1)); + } + } + }) + .returns(Types.TUPLE(Types.STRING, Types.INT)) + .keyBy(0) + .timeWindow(Time.seconds(5)) + .sum(1); +``` + +将数据流输出。 + +```java +// Sink +wordCount.print(); +``` + +最后运行该程序。 + +```java +// execute +env.execute("kafka streaming word count"); +``` + +env.execute() 是启动Flink作业所必需的,只有在execute()方法被调用时,之前调用的各个操作才会被提交到集群上或本地计算机上运行。 + +该程序的完整代码如代码清单2-9所示。 + +```java +import org.apache.flink.api.common.serialization.SimpleStringSchema; +import org.apache.flink.api.common.typeinfo.Types; +import org.apache.flink.api.java.tuple.Tuple2; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.streaming.api.windowing.time.Time; +import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer; +import org.apache.flink.util.Collector; + +import java.util.Properties; + +public class WordCountKafkaInStdOut { + + public static void main(String[] args) throws Exception { + + // 设置Flink执行环境 + StreamExecutionEnvironment env = +StreamExecutionEnvironment.getExecutionEnvironment(); + + // Kafka参数 + Properties properties = new Properties(); + properties.setProperty("bootstrap.servers", "localhost:9092"); + properties.setProperty("group.id", "flink-group"); + String inputTopic = "Shakespeare"; + String outputTopic = "WordCount"; + + // Source + FlinkKafkaConsumer consumer = + new FlinkKafkaConsumer(inputTopic, new SimpleStringSchema(), properties); + DataStream stream = env.addSource(consumer); + + // Transformation + // 使用Flink API对输入流的文本进行操作 + // 按空格切词、计数、分区、设置时间窗口、聚合 + DataStream> wordCount = stream + .flatMap((String line, Collector> collector) -> { + String[] tokens = line.split("\\s"); + // 输出结果 + for (String token : tokens) { + if (token.length() > 0) { + collector.collect(new Tuple2<>(token, 1)); + } + } + }) + .returns(Types.TUPLE(Types.STRING, Types.INT)) + .keyBy(0) + .timeWindow(Time.seconds(5)) + .sum(1); + + // Sink + wordCount.print(); + + // execute + env.execute("kafka streaming word count"); + + } +} +``` + +代码写完后,我们还要在Maven的项目对象模型(Project Object Model,POM)文件中引入下面的依赖,让Maven可以引用Kafka。 + +```xml + + org.apache.flink + flink-connector-kafka_${scala.binary.version} + ${flink.version} + +``` + +其中,`${scala.binary.version}`是所用的Scala版本号,可以是2.11或2.12,`${flink.version}`是所用的Flink的版本号,比如1.11.2。 + +## 2.4.5 运行程序 + +我们在1.7节中展示过如何启动一个Kafka集群,并向某个Topic内发送数据流。在本次Flink作业启动之前,我们还要按照1.7节提到的方式启动一个Kafka集群、创建对应的Topic,并向Topic中写入数据。 + +1. **在IntelliJ IDEA中运行程序** + - 在IntelliJ IDEA中,单击绿色运行按钮,运行这个程序。图2-11所示的两个绿色运行按钮中的任意一个都可以运行这个程序。 + - IntelliJ IDEA下方的“Run”栏会显示程序的输出,包括本次需要输出的结果,如图2-12所示。 + +恭喜你,你的第一个Flink程序运行成功! + +**提示** + +如果在Intellij IDEA中运行程序时遇到`java.lang.NoClassDefFoundError` 报错,这是因为没有把依赖的类都加载进来。在Intellij IDEA中单击“Run”->“Edit configurations...”,在“Use classpath of module”选项上选择当前工程,并且勾选“Include dependencies with‘Provided’ Scope” + +2. **向集群提交作业** + - 目前,我们学会了先下载并启动本地集群,接着在模板的基础上添加代码,并在IntelliJ IDEA中运行程序。而在生产环境中,我们一般需要将代码编译打包,提交到集群上。我们将在第9章详细介绍如何向Flink集群提交作业。 + - 注意,这里涉及两个目录:一个是我们存放刚刚编写代码的工程目录,简称工程目录;另一个是从Flink官网下载解压的Flink主目录,主目录下的bin目录中有Flink提供的命令行工具。 + - 进入工程目录,使用Maven命令行将代码编译打包。 + +```bash +# 使用Maven命令行将代码编译打包 +# 打好的包一般放在工程目录的target目录下 +$ mvn clean package +``` + +回到Flink主目录,使用Flink提供的命令行工具flink,将打包好的作业提交到集群上。命令行的参数--class用来指定哪个主类作为入口。我们之后会介绍命令行的具体使用方法。 + +```bash +$ bin/flink run --class +com.flink.tutorials.java.api.projects.wordcount.WordCountKafkaInStdOut +/Users/luweizheng/Projects/big-data/flink-tutorials/target/flink-tutorials-0.1.jar +``` + +如图2-13所示,这时,Flink WebUI上就多了一个Flink作业。 + +程序的输出会保存到Flink主目录下面的log目录下的.out文件中,可以使用下面的命令查看结果。 + +```bash +$ tail -f log/flink-*-taskexecutor-*.out +``` + +必要时,可以使用下面的命令关停本地集群。 + +```bash +$ ./bin/stop-cluster.sh +``` + +Flink开发和调试过程中,一般有如下几种方式运行程序。 +- 使用IntelliJ IDEA内置的绿色运行按钮。这种方式主要在本地调试时使用。 +- 使用Flink提供的命令行工具向集群提交作业,包括Java和Scala程序。这种方式更适合生产环境。 +- 使用Flink提供的其他命令行工具,比如针对Scala、Python和SQL的交互式环境。 + +对于新手,可以先使用IntelliJ IDEA提供的内置运行按钮,熟练后再使用命令行工具。 diff --git a/doc/ch-programming-basics/functional-programming.md b/doc/ch-programming-basics/functional-programming.md new file mode 100644 index 0000000..3123302 --- /dev/null +++ b/doc/ch-programming-basics/functional-programming.md @@ -0,0 +1,190 @@ +(functional-programming)= +# 函数式编程 + +函数式编程(Functional Programming)是一种编程范式,因其更适合做并行计算,近年来开始受到大数据开发者的广泛关注。Python、JavaScript等语言对函数式编程的支持都不错;Scala更是以函数式编程的优势在大数据领域“攻城略地”;即使是Java,也为了适应函数式编程,加强对函数式编程的支持。未来的程序员或多或少都要了解一些函数式编程思想。这里抛开一些数学推理等各类复杂的概念,仅从Flink开发的角度带领读者熟悉函数式编程。 + +## 2.3.1 函数式编程思想简介 + +在介绍函数式编程前,我们可以先回顾传统的编程范式如何解决一个数学问题。假设我们想求解一个数学表达式: + +```plaintext +addResult = x + y +result = addResult * z +``` + +在这个例子中,我们要先求解中间结果,将其存储到中间变量,再进一步求得最终结果。这仅仅是一个简单的例子,在更多的编程实践中,程序员必须告诉计算机每一步执行什么命令、需要声明哪些中间变量等。因为计算机无法理解复杂的概念,只能听从程序员的指挥。 + +中学时代,我们的老师在数学课上曾花费大量时间讲解函数,函数指对于自变量的映射。函数式编程的思想正是基于数学中对函数的定义。其基本思想是,在使用计算机求解问题时,我们可以把整个计算过程定义为不同的函数。比如,将这个问题转化为: + +```plaintext +result = multiply(add(x, y), z) +``` + +我们再对其做进一步的转换: + +```plaintext +result = add(x, y).multiply(z) +``` + +传统思路中要创建中间变量,要分步执行,而函数式编程的形式与数学表达式形式更为相似。人们普遍认为,这种函数式的描述更接近人类自然语言。 + +如果要实现这样一个函数式程序,主要需要如下两步。 +1. 实现单个函数,将零到多个输入转换成零到多个输出。比如`add()`这种带有映射关系的函数,它将两个输入转化为一个输出。 +2. 将多个函数连接起来,实现所需业务逻辑。比如,将`add()`、`multiply()`连接到一起。 + +接下来我们通过Java代码来展示如何实践函数式编程思想。 + +## 2.3.2 Lambda表达式的内部结构 + +数理逻辑领域有一个名为λ演算的形式系统,主要研究如何使用函数来表达计算。一些编程语言将这个概念应用到自己的平台上,期望能实现函数式编程,取名为Lambda表达式(λ的英文拼写为Lambda)。 + +我们先看一下Java的Lambda表达式的语法规则。 + +```java +(parameters) -> { + body +} +``` + +Lambda表达式主要包括一个箭头符号`->`,其两边连接着输入参数和函数体。我们再看看代码清单 2-5中的几个Java Lambda表达式。 + +```java +// 1. 无参数,返回值为5 +() -> 5 + +// 2. 接收1个int类型参数,将其乘以2,返回一个int类型值 +x -> 2 * x + +// 3. 接收2个int类型参数,返回它们的差 +(x, y) -> x – y + +// 4. 接收2个int类型参数,返回它们的和 +(int x, int y) -> x + y + +// 5. 接收1个String类型参数,将其输出到控制台,不返回任何值 +(String s) -> { System.out.print(s); } + +// 6. 参数为圆半径,返回圆面积,返回值为double类型 +(double r) -> { + double pi = 3.1415; + return r * r * pi; +} +``` + +代码清单 2-5 Java Lambda表达式 + +可以看到,这几个例子都有一个`->`,表示这是一个函数式的映射,相对比较灵活的是左侧的输入参数和右侧的函数体。图2-2所示为Java Lambda表达式的拆解,这很符合数学中对一个函数做映射的思维方式。 + +![图2-2 Java Lambda表达式拆解](./img/lambda.png) + +## 2.3.3 函数式接口 + +通过前文的几个例子,我们大概知道Lambda表达式的内部结构了,那么Lambda表达式到底是什么类型呢?在Java中,Lambda表达式是有类型的,它是一种接口。确切地说,Lambda表达式实现了一个函数式接口(Functional Interface),或者说,前文提到的一些Lambda表达式都是函数式接口的具体实现。 + +函数式接口是一种接口,并且它只有一个虚函数。因为这种接口只有一个虚函数,因此其对应英文为Single Abstract Method(SAM)。SAM表示这个接口对外只提供这一个函数的功能。如果我们想自己设计一个函数式接口,我们应该给这个接口添加`@FunctionalInterface`注解。编译器会根据这个注解确保该接口是函数式接口,当我们尝试往该接口中添加超过一个虚函数时,编译器会报错。在代码清单 2-6中,我们设计一个加法的函数式接口`AddInterface`,然后实现这个接口。 + +```java +@FunctionalInterface +interface AddInterface { + T add(T a, T b); +} + +public class FunctionalInterfaceExample { + + public static void main( String[] args ) { + + AddInterface addInt = (Integer a, Integer b) -> a + b; + AddInterface addDouble = (Double a, Double b) -> a + b; + + int intResult; + double doubleResult; + + intResult = addInt.add(1, 2); + doubleResult = addDouble.add(1.1d, 2.2d); + } +} +``` + +代码清单 2-6 一个能够实现加法功能的函数式接口 + +Lambda表达式实际上是在实现函数式接口中的虚函数,Lambda表达式的输入类型和返回类型要与虚函数定义的类型相匹配。 + +假如没有Lambda表达式,我们仍然可以实现这个函数式接口,只不过代码比较“臃肿”。首先,我们需要声明一个类来实现这个接口,可以是下面的类。 + +```java +public static class MyAdd implements AddInterface { + @Override + public Double add(Double a, Double b) { + return a + b; + } +} +``` + +然后,在业务逻辑中这样调用:`doubleResult = new MyAdd().add(1.1d, 2.2d);`。或者是使用匿名类,省去`MyAdd`这个名字,直接实现`AddInterface`并调用: + +```java +doubleResult = new AddInterface(){ + @Override + public Double add(Double a, Double b) { + return a + b; + } +}.add(1d, 2d); +``` + +声明类并实现接口和使用匿名类这两种方法是Lambda表达式出现之前,Java开发者经常使用的两种方法。实际上我们想实现的逻辑仅仅是一个`a + b`,其他代码其实都是冗余的,都是为了给编译器看的,并不是为了给程序员看的。有了比较我们就会发现,Lambda表达式的简洁优雅的优势就凸显出来了。 + +为了方便大家使用,Java内置了一些的函数式接口,放在`java.util.function`包中,比如`Predicate`、`Function`等,开发者可以根据自己需求实现这些接口。这里简单展示一下这两个接口。 + +`Predicate`对输入进行判断,符合给定逻辑则返回`true`,否则返回`false`。 + +```java +@FunctionalInterface +public interface Predicate { + // 判断输入的真假,返回boolean类型值 + boolean test(T t); +} +``` + +`Function`接收一个类型`T`的输入,返回一个类型`R`的输出。 + +```java +@FunctionalInterface +public interface Function { + // 接收一个类型T的输入,返回一个类型R的输出 + R apply(T t); +} +``` + +部分底层代码提供了一些函数式接口供开发者调用,很多框架的API就是类似上面的函数式接口,开发者通过实现接口来完成自己的业务逻辑。Spark和Flink对外提供的Java API其实就是这种函数式接口。 + +## 2.3.4 Java Stream API + +Stream API是Java 8 的一大亮点,它与`java.io`包里的`InputStream`和`OutputStream`是完全不同的概念,也不是Flink、Kafka等大数据流处理框架中的数据流。它专注于对集合(Collection)对象的操作,是借助Lambda表达式的一种应用。通过Java Stream API,我们可以体验到Lambda表达式带来的编程效率的提升。 + +我们看一个简单的例子,代码清单 2-7首先过滤出非空字符串,然后求得每个字符串的长度,最终返回为一个`List`类型值。代码使用了Lambda表达式来完成对应的逻辑。 + +```java +List strings = Arrays.asList( + "abc", "", "bc", "12345", + "efg", "abcd","", "jkl"); + +List lengths = strings + .stream() + .filter(string -> !string.isEmpty()) + .map(s -> s.length()) + .collect(Collectors.toList()); + +lengths.forEach((s) -> System.out.println(s)); +``` + +代码清单 2-7 使用Lambda表达式来完成对String类型列表的操作 + +这段代码中,数据先经过`stream()`方法被转换为一个`Stream`类型,后经过`filter()`、`map()`、`collect()`等处理逻辑,生成我们所需的输出。各个操作之间使用英文点号`.`来连接,这种方式被称作方法链(Method Chaining)或者链式调用。链式调用可以被抽象成一个管道(Pipeline),将代码清单2-7进行抽象,可以形成图2-3所示的Stream管道。 + +![图2-3 Stream管道](./img/stream.png) + +## 2.3.5 函数式编程小结 + +函数式编程更符合数学上函数映射的思想。具体到编程语言层面,我们可以使用Lambda表达式来快速编写函数映射,函数之间通过链式调用连接到一起,完成所需业务逻辑。Java的Lambda表达式是后来才引入的,而Scala天生就是为函数式编程所设计。由于在并行处理方面的优势,函数式编程正在被大量应用于大数据处理领域。 + +对Lambda表达式、Java Stream API以及Flink API有了基本了解后,我们也应该注意不要将Java Stream API与Flink API混淆。 diff --git a/doc/ch-programming-basics/generics.md b/doc/ch-programming-basics/generics.md new file mode 100644 index 0000000..d0851ba --- /dev/null +++ b/doc/ch-programming-basics/generics.md @@ -0,0 +1,191 @@ +(generics)= +# 泛型 + +泛型(Generic)是强类型编程语言中经常使用的一种技术。很多框架的代码中都会大量使用泛型,比如在Java中我们经常看到如下的代码。 + +```java +List strList = new ArrayList(); +List doubleList = new LinkedList(); +``` + +在这段代码中,`ArrayList`是一个泛型类,`List`是一个泛型接口,它们提供给开发者一个放置不同类型的集合容器,我们可以向这个集合容器中添加`String`、`Double`以及其他各类数据类型。无论内部存储的是什么类型,集合容器提供给开发者的功能都是相同的,比如`add()`,`get()`等方法。有了泛型,我们就没必要创建`StringArrayList`、`DoubleArrayList`等类了,否则代码量太大,维护起来成本极高。 + +## 2.2.1 Java中的泛型 + +在Java中,泛型一般有3种使用方式:泛型类、泛型接口和泛型方法。一般使用尖括号`<>`来接收泛型参数。 + +### 1. Java泛型类 + +如代码清单 2-3所示,我们定义一个泛型类`MyArrayList`,这个类可以简单支持初始化和数据写入。只要在类名后面加上``就可以让这个类支持泛型,类内部的一些属性和方法都可以使用泛型`T`。或者说,类的泛型会作用到整个类。 + +```java +public class MyArrayList { + + private int size; + T[] elements; + + public MyArrayList(int capacity) { + this.size = capacity; + this.elements = (T[]) new Object[capacity]; + } + + public void set(T element, int position) { + elements[position] = element; + } + + @Override + public String toString() { + String result = ""; + for (int i = 0; i < size; i++) { + result += elements[i].toString(); + } + return result; + } + + public static void main(String[] args){ + MyArrayList strList = new MyArrayList(2); + strList.set("first", 0); + strList.set("second", 1); + + System.out.println(strList.toString()); + } +} +``` + +代码清单 2-3 一个名为`MyArrayList`的泛型类,它可以支持简单的数据写入 + +当然我们也可以给这个类添加多个泛型参数,比如``, ``等。泛型一般使用大写字母表示,Java为此提供了一些大写字母使用规范,如下。 + +- `T` 代表一般的任何类。 +- `E` 代表元素(Element)或异常(Exception)。 +- `K` 或`KEY`代表键(Key)。 +- `V` 代表值(Value),通常与`K`一起配合使用。 + +我们也可以从父类中继承并扩展泛型,比如Flink源码中有这样一个类定义,子类继承了父类的`T`,同时自己增加了泛型`K`: + +```java +public class KeyedStream extends DataStream { + ... +} +``` + +### 2. Java泛型接口 + +Java泛型接口的定义和Java泛型类基本相同。下面的代码展示了在`List`接口中定义`subList()`方法,该方法对数据做截取。 + +```java +public interface List { + ... + public List subList(int fromIndex, int toIndex); +} +``` + +继承并实现这个接口的代码如下。 + +```java +public class ArrayList implements List { + ... + public List subList(int fromIndex, int toIndex) { + ... + // 返回一个List类型值 + } +} +``` + +这个例子中,要实现的`ArrayList`依然是泛型的。需要注意的是,`class ArrayList implements List`这句声明中,`ArrayList`和`List`后面都要加上``,表明要实现的子类是泛型的。还有另外一种情况,要实现的子类不是泛型的,而是有确定类型的,如下面的代码。 + +```java +public class DoubleList implements List { + ... + public List subList(int fromIndex, int toIndex) { + ... + // 返回一个List类型值 + } +} +``` + +### 3. Java泛型方法 + +泛型方法可以存在于泛型类中,也可以存在于普通的类中。 + +```java +public class MyArrayList { + ... + // public关键字后的表明该方法是一个泛型方法 + // 泛型方法中的类型E和泛型类中的类型T可以不一样 + public E processElement(E element) { + ... + return E; + } +} +``` + +从上面的代码可以看出,`public`或`private`关键字后的``表示该方法一个泛型方法。泛型方法的类型`E`和泛型类中的类型`T`可以不一样。或者说,如果泛型方法是泛型类的一个成员,泛型方法既可以继续使用类的类型`T`,也可以自己定义新的类型`E`。 + +### 4. 通配符 + +除了用 ``表示泛型,还可用 ``这种形式。`` 被称为通配符,用来适应各种不同的泛型。此外,一些代码中还会涉及通配符的边界问题,主要是为了对泛型做一些安全性方面的限制。有兴趣的读者可以自行了解泛型的通配符和边界。 + +### 5. 类型擦除 + +Java的泛型有一个遗留问题,那就是类型擦除(Type Erasure)。我们先看一下下面的代码。 + +```java +Class strListClass = new ArrayList().getClass(); +Class intListClass = new ArrayList().getClass(); +// 输出:class java.util.ArrayList +System.out.println(strListClass); +// 输出:class java.util.ArrayList +System.out.println(intListClass); +// 输出:true +System.out.println(strListClass.equals(intListClass)); +``` + +虽然声明时我们分别使用了`String`和`Integer`,但运行时关于泛型的信息被擦除了,我们无法区别`strListClass`和`intListClass`这两个类型。这是因为,泛型信息只存在于代码编译阶段,当程序运行到JVM上时,与泛型相关的信息会被擦除。类型擦除对于绝大多数应用系统开发者来说影响不太大,但是对于一些框架开发者来说,必须要注意。比如,Spark和Flink的开发者都使用了一些办法来解决类型擦除问题,对于API调用者来说,受到的影响不大。 + +## 2.2.2 Scala中的泛型 + +对Java的泛型有了基本了解后,我们接着来了解一下Scala中的泛型。相比而言,Scala的类型系统更复杂,这里只介绍一些简单语法,使读者能够读懂一些源码。 + +Scala中,泛型放在了方括号`[]`中。或者我们可以简单地理解为,原来Java的泛型类``,现在改为`[T]`即可。 + +在代码清单 2-4中,我们创建了一个名为`Stack`的泛型类,并实现了两个简单的方法,类中各成员和方法都可以使用泛型`T`。我们也定义了一个泛型方法,形如`isStackPeekEquals[T]()`,方法中可以使用泛型`T`。 + +```scala +object MyStackDemo { + + // Stack泛型类 + class Stack[T] { + private var elements: List[T] = Nil + def push(x: T) { elements = x :: elements } + def peek: T = elements.head + } + + // 泛型方法,检查两个Stack顶部是否相同 + def isStackPeekEquals[T](p: Stack[T], q: Stack[T]): Boolean = { + p.peek == q.peek + } + + def main(args: Array[String]): Unit = { + val stack = new Stack[Int] + stack.push(1) + stack.push(2) + println(stack.peek) + + val stack2 = new Stack[Int] + stack2.push(2) + val stack3 = new Stack[Int] + stack3.push(3) + println(isStackPeekEquals(stack, stack2)) + println(isStackPeekEquals(stack, stack3)) + } +} +``` + +代码清单 2-4 使用Scala实现一个简易的`Stack`泛型类 + +## 2.2.3 泛型小结 + +本节简单总结了Java和Scala的泛型知识。对于初学者来说,泛型的语法有时候让人有些眼花缭乱,但其目的是接受不同的数据类型,增强代码的复用性。 + +泛型给开发者提供了不少便利,尤其是保证了底层代码简洁性。因为这些底层代码通常被封装为一个框架,会有各种各样的上层应用调用这些底层代码,进行特定的业务处理,每次调用都可能涉及泛型问题。包括Spark和Flink在内的很多框架都需要开发者基于泛型进行API调用。开发者非常有必要了解泛型的基本用法。 diff --git a/doc/ch-programming-basics/index.md b/doc/ch-programming-basics/index.md new file mode 100644 index 0000000..cb1793c --- /dev/null +++ b/doc/ch-programming-basics/index.md @@ -0,0 +1,12 @@ +# 大数据必备编程知识 + +在正式介绍Flink编程之前,我们先回顾和复习一下必备的编程知识,了解这些编程知识有助于我们快速读懂各类源码,深刻理解Flink API及其背后的原理。本章所涉及的主要内容如下。 +- 继承和多态。 +- 泛型。 +- 函数式编程。 +本章的案例实践将带领读者从零开始搭建Flink开发环境。 +如第1章所述,本书主要基于Java的相关知识,也会在必要的地方兼顾Scala的相关知识。像Java和Scala这样的编程语言经过多年的发展,可谓博大精深,本书无法覆盖编程语言的所有特性,只选取了一些与Flink开发密切相关的知识点,目的是帮助读者熟悉相关的接口,便于在阅读后文的过程中能够快速上手。或者当读者在后文遇到一些编程语言上的问题时,可以回过头来翻阅本章内容。 + + +```{tableofcontents} +``` \ No newline at end of file diff --git a/doc/ch-programming-basics/inheritance-and-polymorphism.md b/doc/ch-programming-basics/inheritance-and-polymorphism.md new file mode 100644 index 0000000..b8a354a --- /dev/null +++ b/doc/ch-programming-basics/inheritance-and-polymorphism.md @@ -0,0 +1,157 @@ +(inheritance-and-polymorphism)= +# 继承和多态 + +继承和多态是现代编程语言中最为重要的概念。继承和多态允许用户将一些代码进行抽象,以达到复用的目的。Flink开发过程中会涉及大量的继承和多态相关问题。 + +## 2.1.1 继承、类和接口 + +继承在现实世界中无处不在。比如我们想描述动物和它们的行为,可以先创建一个动物类别,动物类别又可以分为狗和鱼,这样的一种层次结构其实就是编程语言中的继承关系。动物类涵盖了每种动物都有的属性,比如名字、描述信息等。从动物类衍生出的众多子类,比如鱼类、狗类等都具备动物的基本属性。不同类型的动物又有自己的特点,比如鱼会游泳、狗会吼叫。继承关系保证所有动物都具有动物的基本属性,这样就不必在创建一个新的子类的时候,将它们的基本属性(名字、描述信息)再复制一遍。同时,子类更加关注自己区别于其他类的特点,比如鱼所特有的游泳动作。 + +图2-1所示为对动物进行的简单的建模。其中,每个动物都有一些基本属性,即名字(name)和描述(description);有一些基本方法,即getName()和eat(),这些基本功能共同组成了Animal类。在Animal类的基础上,可以衍生出各种各样的子类、子类的子类等。比如,Dog类有自己的dogData属性和bark()方法,同时也可以使用父类的name等属性和eat()方法。 + +我们将图2-1所示的Animal类继承关系转化为代码,一个Animal公共父类可以抽象如代码清单 2-1所示。 + +```java +public class Animal { + + private String name; + private String description; + + public Animal(String myName, String myDescription) { + this.name = myName; + this.description = myDescription; + } + + public String getName() { + return this.name; + } + + public void eat(){ + System.out.println(name + "正在吃"); + } +} +``` + +代码清单 2-1 一个简单的Animal类 + +![图2-1 Animal类继承关系](./img/extend.png) + +子类可以拥有父类非private的属性和方法,同时可以扩展属于自己的属性和方法。比如Dog类或Fish类可以继承Animal类,可以直接复用Animal类里定义的属性和方法。这样就不存在代码的重复问题,整个工程的可维护性更好。在Java和Scala中,子类继承父类时都要使用extends关键字。代码清单 2-2实现了一个Dog类,并在里面添加了Dog类的一些特有成员。 + +```java +public class Dog extends Animal implements Move { + + private String dogData; + + public Dog(String myName, String myDescription, String myDogData) { + this.name = myName; + this.description = myDescription; + this.dogData = myDogData; + } + + @Override + public void move(){ + System.out.println(name + "正在奔跑"); + } + + public void bark(){ + System.out.println(name + "正在叫"); + } +} +``` + +代码清单 2-2 Dog类继承Animal类,并实现了一些特有的成员 + +不过,Java只允许子类继承一个父类,或者说Java不支持多继承。`class A extends B, C`这样的语法在Java中是不允许的。另外,有一些方法具有更普遍的意义,比如move()方法,不仅动物会移动,一些机器(比如Machine类和Car类)也会移动。因此让Animal类和Machine类都继承一个Mover类在逻辑上没有太大意义。对于这种场景,Java提供了接口,以关键字`interface`标注,可以将一些方法进一步抽象出来,对外提供一种功能。不同的子类可以继承相同的接口,实现自己的业务逻辑,也解决了Java不允许多继承的问题。代码清单 2-2的Dog类也实现了这样一个名为Move的接口。 + +Move接口的定义如下。 + +```java +public interface Move { + public void move(); +} +``` + +注意 + +在Java中,一个类可以实现多个接口,并使用`implements`关键字。 + +```java +class ClassA implements Move, InterfaceA, InterfaceB { + ... +} +``` + +在Scala中,一个类实现第一个接口时使用关键字`extends`,后面则使用关键字`with`。 + +```scala +class ClassA extends Move with InterfaceA, InterfaceB { + ... +} +``` + +接口与类的主要区别在于,从功能上来说,接口强调特定功能,类强调所属关系;从技术实现上来说,接口里提供的都是抽象方法,类中只有用`abstract`关键字定义的方法才是抽象方法。抽象方法是指只定义了方法签名,没有定义具体实现的方法。实现一个子类时,遇到抽象方法必须去做自己的实现。继承并实现接口时,要实现里面所有的方法,否则会报错。 + +在Flink API调用过程中,绝大多数情况下都继承一个父类或接口。对于Java用户来说,如果继承一个接口,就要使用`implements`关键字;如果继承一个类,要使用`extends`关键字。对于Scala用户来说,绝大多数情况使用`extends`关键字就足够了。 + +## 2.1.2 重写与重载 + +### 1. 重写 + +子类可以用自己的方式实现父类和接口的方法,比如前文提到的move()方法。子类的方法会覆盖父类中已有的方法,实际执行时,Java会调用子类方法,而不是使用父类方法,这个过程被称为重写(Override)。在实现重写时,需要使用`@Override`注解(Annotation)。重写可以概括为,外壳不变,核心重写;或者说方法签名等都不能与父类有变化,只修改花括号内的逻辑。 + +虽然Java没有强制开发者使用这个注解,但是`@Override`会检查该方法是否正确重写了父类方法,如果发现其父类或接口中并没有该方法时,会报编译错误。像IntelliJ IDEA之类的集成开发环境也会有相应的提示,帮助我们检查方法是否正确重写。这里强烈建议开发者在继承并实现方法时养成使用`@Override`的习惯。 + +```java +public class ClassA implements Move { + @Override + public void move(){ + ... + } +} +``` + +在Scala中,在方法前添加一个`override`关键字可以起到重写提示的作用。 + +```scala +class ClassA extends Move { + override def move(): Unit = { + ... + } +} +``` + +### 2. 重载 + +一个很容易和重写混淆的概念是重载(Overload)。重载是指,在一个类里有多个同名方法,这些方法名字相同、参数不同、返回类型不同。 + +```java +public class Overloading { + + // 无参数,返回值为int类型 + public int test(){ + System.out.println("test"); + return 1; + } + + // 有一个参数 + public void test(int a){ + System.out.println("test " + a); + } + + // 有两个参数和一个返回值 + public String test(int a, String s){ + System.out.println("test " + a + " " + s); + return a + " " + s; + } +} +``` + +这段代码演示了名为test()的方法的多种不同的具体实现,每种实现在参数和返回值类型上都有区别。包括Flink在内,很多框架的源码和API应用了大量的重载,目的是给开发者提供多种不同的调用接口。 + +## 2.1.3 继承和多态小结 + +本节简单总结了Java/Scala的继承和多态基本原理和使用方法,包括数据建模、关键字的使用、方法的重写等。从Flink开发的角度来说,需要注意以下两点。 + +- 对于Java的一个子类,可以用`extends`关键字继承一个类,用`implements`关键字实现一个接口。如果需要覆盖父类的方法,则需要使用`@Override`注解。 +- 对于Scala的一个子类,可以用`extends`关键字继承一个类或接口。如果需要覆盖父类的方法,则需要在方法前添加一个`override`关键字。 diff --git a/pom.xml b/pom.xml index 6b041d0..e32cfa4 100644 --- a/pom.xml +++ b/pom.xml @@ -44,12 +44,12 @@ under the License. UTF-8 - 1.12.4 - 2.11 - 2.11.12 + 1.19.0 + 22 + 2.12 + ${target.java.version} + ${target.java.version} 2.17.1 - 1.8 - 1.8