做者|Stephan Ewen
整理|秦江杰本文整理自 Flink 创始公司 Ververica 联合创始人兼 CTO - Stephan Ewen 在 Flink Forward China 2018 上的演讲《Stream Processing takes on Everything》。
这个演讲主题看似比较激进:流处理解决全部问题。不少人对于 Flink 可能还停留在最初的认知,以为 Flink 是一个流处理引擎,实际上 Flink 能够作不少其余的工做,好比批处理、应用程序。在这个演讲中,Stephan 首先会简单说明他对 Flink 功能的观点,而后深刻介绍一个特定领域的应用和事件处理场景。这个场景乍看起来不是一个流处理的使用场景,可是在 Stephan 看来,它实际上就是一个颇有趣的流处理使用场景。算法
上图对为何流处理能够处理一切做出诠释,将数据看作流是一个天然而又十分强大的想法。大部分数据的产生过程都是随时间生成的流,好比一个 Petabyte 的数据不会凭空产生。这些数据一般都是一些事件的积累,好比支付、将商品放入购物车,网页浏览,传感器采样输出。数据库
基于数据是流的想法,咱们对数据处理能够有相应的理解。好比将过去的历史数据看作是一个截止到某一时刻的有限的流,或是将一个实时处理应用当作是从某一个时刻开始处理将来到达的数据。可能在将来某个时刻它会中止,那么它就变成了处理从开始时刻到中止时刻的有限数据的批处理。固然,它也有可能一直运行下去,不断处理新到达的数据。这个对数据的重要理解方式很是强大,基于这一理解,Flink 能够支持整个数据处理范畴内的全部场景。apache
最广为人知的 Flink 使用场景是流分析、连续处理(或者说渐进式处理),这些场景中 Flink 实时或者近实时的处理数据,或者采集以前提到的历史数据而且连续的对这些事件进行计算。晓伟在以前的演讲中提到一个很是好的例子来讲明怎么样经过对 Flink 进行一些优化,进而能够针对有限数据集作一些特别的处理,这使得 Flink 可以很好的支持批处理的场景,从性能上来讲可以与最早进的批处理引擎相媲美。而在这根轴的另外一头,是我今天的演讲将要说明的场景 – 事件驱动的应用。这类应用广泛存在于任何服务或者微服务的架构中。这类应用接收各种事件(多是 RPC 调用、HTTP 请求),而且对这些事件做出一些响应,好比把商品放进购物车,或者加入社交网络中的某个群组。缓存
在我进一步展开今天的演讲以前,我想先对社区在 Flink 的传统领域(实时分析、连续处理)近期所作的工做作一个介绍。Flink 1.7 在 2018 年 11 月 30 日已经发布。在 Flink 1.7 中为典型的流处理场景加入了一些很是有趣的功能。好比我我的很是感兴趣的在流式 SQL 中带时间版本的 Join。一个基本想法是有两个不一样的流,其中一个流被定义为随时间变化的参照表,另外一个是与参照表进行 Join 的事件流。好比事件流是一个订单流,参照表是不断被更新的汇率,而每一个订单须要使用最新的汇率来进行换算,并将换算的结果输出到结果表。这个例子在标准的 SQL 当中实际上并不容易表达,但在咱们对 Streaming SQL 作了一点小的扩展之后,这个逻辑表达变得很是简单,咱们发现这样的表达有很是多的应用场景。网络
另外一个在流处理领域十分强大的新功能是将复琐事件处理(CEP)和 SQL 相结合。CEP 应用观察事件模式。好比某个 CEP 应用观察股市,当有两个上涨后紧跟一个下跌时,这个应用可能作些交易。再好比一个观察温度计的应用,当它发现有温度计在两个超过 90 摄氏度的读数以后的两分钟里没有任何操做,可能会进行一些操做。与 SQL 的结合使这类逻辑的表达也变得很是简单。架构
第三个 Flink 1.7 中作了不少工做的功能是 Schema 升级。这个功能和基于流的应用紧密相关。就像你能够对数据库进行数据 Schema 升级同样,你能够修改 Flink 表中列的类型或者从新写一个列。并发
另外我想简单介绍的是流处理技术不只仅是简单对数据进行计算,这还包括了不少与外部系统进行事务交互。流处理引擎须要在采用不一样协议的系统之间以事务的方式移动数据,并保证计算过程和数据的一致性。这一部分功能也是在 Flink 1.7 中获得了加强。异步
以上我对 Flink 1.7 的新功能向你们作了简单总结。下面让咱们来看看今天我演讲的主要部分,也就是利用 Flink 来搭建应用和服务。我将说明为何流处理是一个搭建应用和服务或者微服务的有趣技术。分布式
我将从左边这个高度简化的图提及,咱们一下子将聊一些其中的细节。首先咱们来看一个理解应用简单的视角。如左图所示,一个应用能够是一个 Container,一个 Spring 应用,或者 Java 应用、Ruby 应用,等等。这个应用从诸如 RPC,HTTP 等渠道接收请求,而后依据请求进行数据库变动。这个应用也可能调用另外一个微服务并进行下一步的处理。咱们能够很是天然的想到进入到应用的这些请求能够看作是个事件组成的序列,因此咱们能够把它们看作是事件流。可能这些事件被缓存在消息队列中,而应用会从消息队列中消费这些事件进行处理,当应用须要响应一个请求时,它将结果输出到另外一个消息队列,而请求发送方能够从这个消息队列中消费获得所发送请求的响应。在这张图中咱们已经能够看到一些有趣的不一样。函数
第一个不一样是在这张图中应用和数据库再也不是分开的两个实体,而是被一个有状态的流处理应用所代替。因此在流处理应用的架构中,再也不有应用和数据库的链接了,它们被放到了一块儿。这个作法有利有弊,但其中有些好处是很是重要的。首先是性能上的好处是明显的,由于应用再也不须要和数据库进行交互,处理能够基于内存中的变量进行。其次这种作法有很好而且很简单的一致性。
这张图被简化了不少,实际上咱们一般会有不少个应用,而不是一个被隔离的应用,不少状况下你的应用会更符合这张图。系统中有个接收请求的接口,而后请求被发送到第一个应用,可能会再被发到另外一个应用,而后获得相应。在图中有些应用会消费中间结果的流。这张图已经展现了为何流处理是更适合比较复杂的微服务场景的技术。由于不少时候系统中不会有一个直接接收用户请求并直接响应的服务,一般来讲一个微服务须要跟其余微服务通讯。这正如在流处理的架构中不一样应用在建立输出流,同时基于衍生出的流再建立并输出新的流。
到目前为止,咱们看到的内容多少还比较直观。而对基于流处理技术的微服务架构而言,人们最常问的一个问题是如何保证事务性?若是系统中使用的是数据库,一般来讲都会有很是成熟复杂的数据校验和事务模型。这也是数据库在过去许多年中十分红功的缘由。开始一个事务,对数据作一些操做,提交或者撤销一个事务。这个机制使得数据完整性获得了保证(一致性,持久性等等)。
那么在流处理中咱们怎么作到一样的事情呢?做为一个优秀的流处理引擎,Flink 支持了刚好一次语义,保证了每一个事件只会被处理一遍。可是这依然对某些操做有限制,这也成为了使用流处理应用的一个障碍。咱们经过一个很是简单流处理应用例子来看咱们能够作一些什么扩展来解决这个问题。咱们会看到,解决办法其实出奇的简单。
让咱们以这个教科书式的事务为例子来看一下事务性应用的过程。这个系统维护了帐户和其中存款余额的信息。这样的信息多是银行或者在线支付系统的场景中用到的。假设咱们想要处理相似下面的事务:若是帐户 A 中的余额大于 100,那么从帐户 A 中转帐 50 元到帐户 B。这是个很是简单的两个帐户之间进行转帐的例子。
数据库对于这样的事务已经有了一个核心的范式,也就是原子性,一致性,隔离性和持久性(ACID)。这是可以让用户放心使用事务的几个基本保证。有了他们,用户不用担忧钱在转帐过程当中会丢失或者其余问题。让咱们用这个例子来放到流处理应用中,来让流处理应用也能提供和数据相同的 ACID 支持:
原子性要求一个转帐要不就彻底完成,也就是说转帐金额从一个帐户减小,并增长到另外一个帐户,要不就两个帐户的余额都没有变化。而不会只有一个帐户余额改变。不然的话钱就会凭空减小或者凭空增长。
一致性和隔离性是说若是有不少用户同时想要进行转帐,那么这些转帐行为之间应该互不干扰,每一个转帐行为应该被独立的完成,而且完成后每一个帐户的余额应该是正确的。也就是说若是两个用户同时操做同一个帐户,系统不该该出错。
持久性指的是若是一个操做已经完成,那么这个操做的结果会被妥善的保存而不会丢失。
咱们假设持久性已经被知足。一个流处理器有状态,这个状态会被 checkpoint,因此流处理器的状态是可恢复的。也就是说只要咱们完成了一个修改,而且这个修改被 checkpoint 了,那么这个修改就是持久化的。
让咱们来看看另外三个例子。设想一下,若是咱们用流处理应用来实现这样一个转帐系统会发生什么。咱们先把问题简化一些,假设转帐不须要有条件,仅仅是将 50 元从帐户 A 转到帐户,也就是说帐户 A 的余额减小 50 元而帐户 B 的余额增长 50 元。咱们的系统是一个分布式的并行系统,而不是一个单机系统。简单起见咱们假设系统中只有两台机器,这两台机器能够是不一样的物理机或者是在 YARN 或者 Kubernetes 上不一样的容器。总之它们是两个不一样的流处理器实例,数据分布在这两个流处理器上。咱们假设帐户 A 的数据由其中一台机器维护,而帐户 B 的数据有另外一台机器维护。
如今咱们要作个转帐,将 50 元从帐户 A 转移到帐户 B,咱们把这个请求放进队列中,而后这个转帐请求被分解为对帐户 A 和 B 分别进行操做,而且根据键将这两个操做路由到维护帐户 A 和维护帐户 B 的这两台机器上,这两台机器分别根据要求对帐户 A 和帐户 B 的余额进行改动。这并非事务操做,而只是两个独立无心义的改动。一旦咱们将转帐的请求改的稍微复杂一些就会发现问题。
下面咱们假设转帐是有条件的,咱们只想在帐户 A 的余额足够的状况下才进行转帐,这样就已经有些不太对了。若是咱们仍是像以前那样操做,将这个转帐请求分别发送给维护帐户 A 和 B 的两台机器,若是 A 没有足够的余额,那么 A 的余额不会发生变化,而 B 的余额可能已经被改动了。咱们就违反了一致性的要求。
咱们看到咱们须要首先以某种方式统一作出是否须要更改余额的决定,若是这个统一的决定中余额须要被修改,咱们再进行修改余额的操做。因此咱们先给维护 A 的余额的机器发送一个请求,让它查看 A 的余额。咱们也能够对 B 作一样的事情,可是这个例子里面咱们不关心 B 的余额。而后咱们把全部这样的条件检查的请求汇总起来去检验条件是否知足。由于 Flink 这样的流处理器支持迭代,若是知足转帐条件,咱们能够把这个余额改动的操做放进迭代的反馈流当中来告诉对应的节点来进行余额修改。反之若是条件不知足,那么余额改动的操做将不会被放进反馈流。这个例子里面,经过这种方式咱们能够正确的进行转帐操做。从某种角度上来讲咱们实现了原子性,基于一个条件咱们能够进行所有的余额修改,或者不进行任何余额修改。这部分依然仍是比较直观的,更大的困难是在于如何作到并发请求的隔离性。
假设咱们的系统没有变,可是系统中有多个并发的请求。咱们在以前的演讲中已经知道,这样的并发可能达到每秒钟几十亿条。如图,咱们的系统可能从两个流中同时接受请求。若是这两个请求同时到达,咱们像以前那样将每一个请求拆分红多个请求,首先检查余额条件,而后进行余额操做。然而咱们发现这会带来问题。管理帐户 A 的机器会首先检查 A 的余额是否大于 50,而后又会检查 A 的余额是否大于 100,由于两个条件都知足,因此两笔转帐操做都会进行,但实际上帐户 A 上的余额可能没法同时完成两笔转帐,而只能完成 50 元或者 100 元的转帐中的一笔。这里咱们须要进一步思考怎么样来处理并发的请求,咱们不能只是简单地并发处理请求,这会违反事务的保证。从某种角度来讲,这是整个数据库事务的核心。数据库的专家们花了一些时间提供了不一样解决方案,有的方案比较简单,有的则很复杂。但全部的方案都不是那么容易,尤为是在分布式系统当中。
在流处理中怎么解决这个问题呢?直觉上讲,若是咱们可以让全部的事务都按照顺序依次发生,那么问题就解决了,这也被成为可序列化的特性。可是咱们固然不但愿全部的请求都被依次顺序处理,这与咱们使用分布式系统的初衷相违背。因此咱们须要保证这些请求最后的产生的影响看起来是按照顺序发生的,也就是一个请求产生的影响是基于前一个请求产生影响的基础之上的。换句话说也就是一个事务的修改须要在前一个事务的全部修改都完成后才能进行。这种但愿一件事在另外一件事以后发生的要求看起来很熟悉,这彷佛是咱们之前在流处理中曾经遇到过的问题。是的,这听上去像是事件时间。用高度简化的方式来解释,若是全部的请求都在不一样的事件时间产生,即便因为种种缘由他们到达处理器的时间是乱序的,流处理器依然会根据他们的事件时间来对他们进行处理。流处理器会使得全部的事件的影响看上去都是按顺序发生的。按事件时间处理是 Flink 已经支持的功能。
那么详细说来,咱们到底怎么解决这个一致性问题呢?假设咱们有并行的请求输入并行的事务请求,这些请求读取某些表中的记录,而后修改某些表中的记录。咱们首先须要作的是把这些事务请求根据事件时间顺序摆放。这些请求的事务时间不可以相同,可是他们之间的时间也须要足够接近,这是由于在事件时间的处理过程当中会引入必定的延迟,咱们须要保证所处理的事件时间在向前推动。所以第一步是定义事务执行的顺序,也就是说须要有一个聪明的算法来为每一个事务制定事件时间。
在图上,假设这三个事务的事件时间分别是 T+2, T 和 T+1。那么第二个事务的影响须要在第一和第三个事务以前。不一样的事务所作的修改是不一样的,每一个事务都会产生不一样的操做请求来修改状态。咱们如今须要将对访问每一个行和状态的事件进行排序,保证他们的访问是符合事件时间顺序的。这也意味着那些相互之间没有关系的事务之间天然也没有了任何影响。好比这里的第三个事务请求,它与前两个事务之间没有访问共同的状态,因此它的事件时间排序与前两个事务也相互独立。而当前两个事务之间的操做的到达顺序与事件时间不符时,Flink 则会依据它们的事件时间进行排序后再处理。
必须认可,这样说仍是进行了一些简化,咱们还须要作一些事情来保证高效执行,可是整体原则上来讲,这就是所有的设计。除此以外咱们并不须要更多其余东西。
为了实现这个设计,咱们引入了一种聪明的分布式事件时间分配机制。这里的事件时间是逻辑时间,它并不须要有什么现实意义,好比它不须要是真实的时钟。使用 Flink 的乱序处理能力,而且使用 Flink 迭代计算的功能来进行某些前提条件的检查。这些就是咱们构建一个支持事务的流处理器的要素。
咱们实际上已经完成了这个工做,称之为流式帐簿(Streaming Ledger),这是个在 Apache Flink 上很小的库。它基于流处理器作到了知足 ACID 的多键事务性操做。我相信这是个很是有趣的进化。流处理器一开始基本上没有任何保障,而后相似 Storm 的系统增长了至少一次的保证。但显然至少一次依然不够好。而后咱们看到了刚好一次的语义,这是一个大的进步,但这只是对于单行操做的刚好一次语义,这与键值库很相似。而支持多行刚好一次或者多行事务操做将流处理器提高到了一个能够解决传统意义上关系型数据库所应用场景的阶段。
Streaming Ledger 的实现方式是容许用户定义一些表和对这些表进行修改的函数。
Streaming Ledger 会运行这些函数和表,全部的这些一块儿编译成一个 Apache Flink 的有向无环图(DAG)。Streaming Ledger 会注入全部事务时间分配的逻辑,以此来保证全部事务的一致性。
搭建这样一个库并不难,难的是让它高性能的运行。让咱们来看看它的性能。这些性能测试是几个月以前的,咱们并无作什么特别的优化,咱们只是想看看一些最简单的方法可以有什么样的性能表现。而实际性能表现看起来至关不错。若是你看这些性能条造成的阶梯跨度,随着流处理器数量的增加,性能的增加至关线性。
在事务设计中,没有任何协同或者锁参与其中。这只是流处理,将事件流推入系统,缓存一小段时间来作一些乱序处理,而后作一些本地状态更新。在这个方案中,没有什么特别代价高昂的操做。在图中性能增加彷佛超过了线性,我想这主要是由于 JAVA 的 JVM 当中 GC 的工做缘由致使的。在 32 个节点的状况下咱们每秒能够处理大约两百万个事务。为了与数据库性能测试进行对比,一般当你看数据库的性能测试时,你会看到相似读写操做比的说明,好比 10% 的更新操做。而咱们的测试使用的是 100% 的更新操做,而每一个写操做至少更新在不一样分区上的 4 行数据,咱们的表的大小大约是两亿行。即使没有任何优化,这个方案的性能也很是不错。
另外一个在事务性能中有趣的问题是当更新的操做对象是一个比较小的集合时的性能。若是事务之间没有冲突,并发的事务处理是一个容易的事情。若是全部的事务都独立进行而互不干扰,那这个不是什么难题,任何系统应该都能很好的解决这样的问题。
当全部的事务都开始操做同一些行时,事情开始变得更有趣了,你须要隔离不一样的修改来保证一致性。因此咱们开始比较一个只读的程序、一个又读又写可是没有写冲突的程序和一个又读又写并有中等程度写冲突的程序这三者之间的性能。你能够看到性能表现至关稳定。这就像是一个乐观的并发冲突控制,表现很不错。那若是咱们真的想要针对这类系统的阿喀琉斯之踵进行考验,也就是反复的更新同一个小集合中的键。
在传统数据库中,这种状况下可能会出现反复重试,反复失败再重试,这是一种咱们总想避免的糟糕状况。是的,咱们的确须要付出性能代价,这很天然,由于若是你的表中有几行数据每一个人都想更新,那么你的系统就失去了并发性,这自己就是个问题。可是这种状况下,系统并没崩溃,它仍然在稳定的处理请求,虽然失去了一些并发性,可是请求依然可以被处理。这是由于咱们没有冲突重试的机制,你能够认为咱们有一个基于乱序处理自然的冲突避免的机制,这是一种很是稳定和强大的技术。
咱们还尝试了在跨地域分布的状况下的性能表现。好比咱们在美国、巴西,欧洲,日本和澳大利亚各设置了一个 Flink 集群。也就是说咱们有个全球分布的系统。若是你在使用一个关系型数据库,那么你会付出至关高昂的性能代价,由于通讯的延迟变得至关高。跨大洲的信息交互比在同一个数据中心甚至同一个机架上的信息交互要产生大得多的延迟。
可是有趣的是,流处理的方式对延迟并非十分敏感,延迟对性能有所影响,可是相比其它不少方案,延迟对流处理的影响要小得多。因此,在这样的全球分布式环境中执行分布式程序,的确会有更差的性能,部分缘由也是由于跨大洲的通讯带宽不如统一数据中内心的带宽,可是性能表现依然不差。
实际上,你能够拿它当作一个跨地域的数据库,同时仍然可以在一个大概 10 个节点的集群上得到每秒几十万条事务的处理能力。在这个测试中咱们只用了 10 个节点,每一个大洲两个节点。因此 10 个节点能够带来全球分布的每秒 20 万事务的处理能力。我认为这是颇有趣的结果,这是由于这个方案对延迟并不敏感。
我已经说了不少利用流处理来实现事务性的应用。可能听起来这是个很天然的想法,从某种角度上来讲的确是这样。可是它的确须要一些很复杂的机制来做为支撑。它须要一个连续处理而非微批处理的能力,须要可以作迭代,须要复杂的基于事件时间处理乱序处理。为了更好地性能,它须要灵活的状态抽象和异步 checkpoint 机制。这些是真正困难的事情。这些不是由 Ledger Streaming 库实现的,而是 Apache Flink 实现的,因此即便对这类事务性的应用而言,Apache Flink 也是真正的中流砥柱。
至此,咱们能够说流处理不只仅支持连续处理、流式分析、批处理或者事件驱动的处理,你也能够用它作事务性的处理。固然,前提是你有一个足够强大的流处理引擎。这就是我演讲的所有内容。
本文为云栖社区原创内容,未经容许不得转载。