过滤(filter)
基本上可以说是最简单的流计算操作了,它用于在数据流上筛选出符合指定条件的元素,并将筛选出的元素作为新的流输出。流的过滤是一个容易理解且容易实现的操作。例如,我们现在需要监控仓库的环境温度,在火灾发生前提前预警以避免火灾,那么就可以采用过滤功能,从来自于传感器的记录环境温度的事件流中过滤出温度高于100℃的事件。我们使用Flink实现如下:
image.png
映射(map)
映射(map)用于将数据流中的每个元素转化为新元素,并将新元素输出为数据流。同样以仓库环境温度监控为例,但这次我们不是将高温事件过滤出来,而是采用数据工程师在做特征工程时常用的一种操作:二值化。我们在原始环境温度事件中,添加一个新的布尔(boolean)类型字段,用于表示该事件是否是高温事件。同样,我们使用Flink实现如下:
image.png
在上面示意代码的Lambda表达式中,通过原始事件的temperature字段判断是否为高温事件后附加到事件上,最后返回附加了高温信息的事件。图4-2展示了映射操作的作用,它将一个由圆形组成的数据流,转化为由五角星形状组成的数据流。同样在实际开发过程中,我们可以将“形状”具象为任何东西。对数据流中的数据做转化或信息增强,正是映射操作的重要作用。
image.png
展开映射(flatMap)
用于将数据流中的每个元素转化为N个新元素,其中N∈[0 +∞)。相比映射而言,展开映射是一个更加灵活的方法,因为映射只能一对一地对数据流元素进行转化,而展开映射能1对N地对数据流元素进行转化。下面举一个展开映射在社交活动分析中使用的例子。现在有一组代表用户信息的数据流,其中每个元素记录了用户(用user字段表示)及其好友列表(用friends数组字段表示)信息,现在我们要分析各个用户与其各个好友之间的亲密程度,以判断他们之间是否是“塑料兄弟”或“塑料姐妹”。我们要先将用户及其好友列表一一展开,展开后的每个元素代表了用户及其某一个好友之间的关系。下面是采用Flink实现的例子。
image.png
在上面代码的展开映射方法中,我们使用Java 8的流式API,将用户的好友列表friends展开,与用户形成一对对的好友关系记录(用“%s->%s”格式表示),最终由out::collect收集起来,写入输出数据流中。图4-3展示了展开映射操作的作用,它将一个由包含小圆形在内的正方形组成的数据流,展开转化为由小圆形组成的数据流。在实际开发过程中,我们还经常使用展开映射实现Map/Reduce或Fork/Join计算模式中的Map或Fork操作。更有甚者,由于展开映射的输出元素个数能够为0,我们有时候连Reduce或Join操作也可以使用展开映射操作实现。
image.png聚合(reduce)
用于将数据流中的元素按照指定方法进行聚合,并将聚合结果作为新的流输出。由于流数据具有时间序列的特征,所以聚合操作不能像诸如Hadoop等批处理计算框架那样作用在整个数据集上。流数据的聚合操作必然指定了窗口(或者说这样做才有更加实际的意义),这些窗口可以基于时间、事件或会话(session)等。同样以社交活动分析为例,这次我们需要每秒钟统计一次10秒内用户活跃事件数。使用Flink实现如下。
image.png
在上面的代码片段中,socialWebStream是用户活跃事件流,我们使用timeWindowAll指定每隔1秒,对10秒窗口内的数据进行一次计算。reduce方法的输入是一个用于求和的Lambda表达式。在实际执行时,这个求和Lambda表达式会依次将每条数据与前一次计算的结果相加,最终完成对窗口内全部流数据的求和计算。如果将求和操作换成其他“二合一”的计算,则可以实现相应功能的聚合运算。由于使用了窗口,所以聚合后流的输出不再像映射运算那样逐元素地输出,而是每隔一段时间才会输出窗口内的聚合运算结果。如前面的示例代码中,就是每隔1秒输出10秒窗口内的聚合计算结果。图4-4展示了聚合操作的作用,它将一个由带有数值的圆形组成数据流,以3个元素为窗口,进行求和聚合运算,并输出为新的数据流。在实际开发过程中,我们可选择不同的窗口实现、不同的窗口长度、不同的聚合内容、不同的聚合方法,从而在流数据上实现各种各样的聚合操作。
image.png关联(join)
用于将两个数据流中满足特定条件的元素对组合起来,按指定规则形成新元素,并将新元素输出为数据流。在关系型数据库中,关联操作是非常常用的查询手段,这是由关系型数据库的设计理念(即数据库的3种设计范式)决定的。而在流数据领域,由于数据来源的多样性和在时序上的差异性,数据流之间的关联也成为一种非常自然的需求。以常见场景为例,假设我们收集的事件流同时被输入两个功能不同的子系统以做处理,它们各自处理的结果同样以数据流的方式输出。现在需要将这两个子系统的输出流按照相同事件id合并起来,以汇总两个子系统对同一事件的处理结果。在这个合并过程中,两个数据流之间的元素是“一对一”的对应关系。这种情况实现起来相对简单。相比关系型数据库表间的关联操作,流数据的关联在语义和实现上都更加复杂些。由于流的无限性,只有在类似于前面“一对一”等非常受限的使用场景下,不限时间窗口的关联设计和实现才有意义,也相对简单。在大多数使用场景下,我们需要引入“窗口”来对关联的流数据进行时间同步,即只对两个流中处于相同时间窗口内的数据进行关联操作。
即使引入了窗口,流数据的关联依旧复杂。当窗口时间很长,窗口内的数据量很大(需要将部分数据存入磁盘),而关联的条件又比较宽泛(如关联条件不是等于而是大于)时,那么流之间的关联计算将非常慢(不是相对于关系型数据库慢,而是相对于实时计算的要求慢),基本上我们也别指望能够非常快速地获得两个流关联的结果了。反过来讲,如果关联的周期很短,数据量不大,而我们能够使用的内存又足够将这些数据都放入内存,那么关联操作就能够相对快速地实现。同样以社交网络分析为例子,这次我们需要将两个不同来源的事件流,按照用户id将它们关联起来,汇总为一条包含用户完整信息的数据流。以下就是在Flink中实现这个功能的示例代码。
image.png
在上面的代码片段中,socialWebStream和socialWebStream2分别是两个来源的用户事件流,我们使用where和equalTo指定关联的条件,即按照user字段的值相等关联起来。然后使用window指定每隔1秒,对10秒窗口内的数据进行关联计算。最后利用apply方法,指定了合并计算的方法。流的关联是一个我们经常想用但又容易让人头疼的操作。因为稍不注意,关联操作的性能就会惨不忍睹。关联操作需要保存大量的状态,尤其是窗口越长,需要保存的数据越多。因此,当使用流数据的关联功能时,应尽可能让窗口较短。图4-5展示了采用内联接(inne.join)的关联操作,它将两个各带id和部分字段的数据流分成相同的时间窗口后,按照id相等进行内联接关联,最后输出两个流内联接后的数据流。
image.png分组(KeyBy)
如果说各种流计算应用或流计算框架最终能够实现分布式计算,实现高并发和高吞吐,那么最大的功臣莫过于“分组”(key By)操作的实现了。分组操作是实现并行流计算的最主要手段,它将流划分为不相交的分区流,分组键相同的消息被划分到相同的分区流中,各个分区流在逻辑上相互独立,具有各自独立的运行时上下文。这就带来两个非常大的好处。1)流分组后,能够被分配到不同的计算节点上执行,从而实现了CPU、内存、磁盘等资源的分布式使用和扩展。2)分区流具有独立的运行时上下文,就像线程局部量一样,对于涉及运行时状态的流计算任务来说,这极大地简化了安全处理并发问题的难度。以电商场景为例,假设我们要在“双十一抢购”那天,实时统计各个商品的销量以展现在监控大屏上。使用Flink实现如下。
image.png在上面的代码中,transactionStream代表交易数据流,在取出了分别代表商品和销量的product字段和number字段后,我们使用keyBy方法根据商品对数据流进行分组,然后每10秒统计一次10秒内的各商品销售总量。
image.png图4-6展示了数据流的分组操作。通过分组操作,将原本包含多种形状的数据流划分为多个包含单一形状的数据流。当然,这里的“多个”是指逻辑上的多个,它们在物理上可以是多个流,也可以是一个流,这就与具体的并行度设置有关了。
网友评论