白话说流——什么是流,从批认识流(二)
二. 流与批,从批到流
我们用点击数据作为例子。每条点击数据,由点击的用户、时间和链接构成。
批处理的数据是已结束的、有界的数据,总共 6 条,不再随时间变化,我们可以视作为有限的“表”。
流处理的数据是未结束的、无界的数据,当前 6 条,会随着时间增长,我们可以视作为无限的“流”。
1.无状态查询
首先我们来看一个简单查询,逻辑如下面的 SQL 所示,要做的是过滤掉 12:00:00 前的数据并给点击时间拼接上日期。
SELECT user, CONCAT(‘2019-07-11 ’, cTime) AS t_when
FROM info
WHERE cTime > ’12:00:00’
对于批的处理,输入数据是有限的。处理方法是获取完整的输入数据,过滤掉 12:00:00 前的点击数据,然后给余下的数据做时间字段的拼接。数据处理完成,输出结果仍然是有限的。
对于流的处理,情况有所不同。流式计算的前置条件是输入无限,永不停止,无法获取完整的输入数据后再进行处理。
然而,这个查询显然是一个无状态的查询,每行数据的处理和上下文没有关系。流可以逐行处理,来一个数据就处理一个数据。
每来一条数据,判断其是否是 12:00:00 前的数据,若是就过滤掉不进行处理,若不是则进行时间字段的拼接,然后输出。数据处理完成,输出结果是无限的,与输入数据是无限地为对偶关系。
同样的用标准 SQL 描述的逻辑,批和流用不同的模式,分别实现对有限“表”和无限“流”的数据处理。
2.窗口查询
前面的查询没有状态,比较直观和简单。下面我们再来看一个更复杂查询:带状态的查询。
逻辑同下面 SQL 所示,查询截止到该用户的当前点击,该用户一共点击了多少次,同时过滤掉黑名单用户("Liz")。这是个典型的窗口查询。
SELECT user, COUNT(1) OVER (PARTITION BY user ) AS cnt
FROM info
WHERE user != ’Liz’
对于批的处理,同样是先获取所有的输入数据,过滤掉黑名单用户,然后在余下的数据里,利用缓存,按用户进行分组计数,逐行加 1。数据处理完成,输出结果仍然是有限的。
对于流的处理,同样输入数据是永不停止的,无法像批一样能难道所有的输入数据再进行处理。尽管是有上下文关联的有状态查询,但是对于每一行数据的处理,只和“过去”的数据有关,统计范围是从最开始到当前行,本质上还是有限数据集的计算。
具体处理方法也很直观:逐行处理输入数据,先判断是否黑名单,若是就丢掉,若不是,利用缓存按用户分组计数,每来一条就加 1,输出计数值。数据处理完成,输出结果仍然是无限的。
3.统计查询
最后,我们来看聚合统计,流式计算的核心本质,体现在聚合统计中。
逻辑如下面 SQL ,统计每一个用户的点击次数,过滤掉黑名单。
SELECT user, COUNT(1) AS cnt
FROM info WHERE user != ’Liz’
GROUP BY user
对于批的处理,过程很简单,这里不再累赘,过程就是过滤黑名单然后分组统计。
对于流的处理,显然这里有个矛盾!要得到最终的统计结果,显然需要先得到所有的输入数据,但是输入数据是无限的永不停止!
我们可以想一想一个有趣的关系。无限的输入对应无限的输出,对于无限输入的聚合统计,输出的自然也应该是无限的聚合结果。
联想前面的窗口统计,我们在流里可以这么处理:计算截止到当前输入行的统计值并输出,其本质是窗口统计;当新的输入数据到达,改变了聚合统计值,那就对输出结果进行“修正”:先指出之前的输出旧值,即“回撤”,然后更新输出值,即“更新”。
这就是流式计算的聚合统计的回撤流,其通过“回撤”,实现聚合统计。
Flink,就是利用回撤流实现聚合统计。
4.回撤和流表对偶
有人会疑惑,回撤是必须的吗? 可以输出更新后的统计值吗?
如果没有回撤,聚合统计就等价于窗口统计。在一些情形下,例如下游是幂等的写入操作,输入数据没有维度变更,那么结果是正确的。
但是,在大部分情况,没有回撤会导致计算错误。如下所示,下游有另外一个聚合操作,没有回撤,统计结果显然是错误的。
SELECT
SUM(cnt) AS total_cnt
FROM
(
SELECT
user, COUNT(1) AS cnt
FROM info WHERE user != ’Liz’
GROUP BY user
)t
流/表对偶
同样以前面的聚合统计为例,观察流的输出,我们可以发现:
回撤流的规约结果,我们得到的是一个动态变化的表
动态表的变化记录,实际上就是回撤流
这就是所谓的“流/表对偶”。Flink 提出“流批合一”,没有为流实现专门的 SQL,而是基于 ANSI SQL 实现对流的查询,就是以此为其理论依据。
批,对应的是有限的输入数据集,有限表,静态表。
流,对应的是无限的输入数据集,无限流,动态表。
小结
总结一下,流的关键是:输入永不结束,查询永不结束,输出也永不结束。
流式计算,如果是不受未来值影响的操作,例如无状态查询,窗口查询,并不产生回撤数据。如果是关注未来值的操作,例如聚合统计,联合操作,必须要有回撤数据以保证计算正确。
定义在批上的操作语义,可同样应用在流上,只是两者的输入输出不一样,但两者是对偶关系,一定程度上等价。
另外,我们在应用时,也要明白流的代价。例如,聚合统计,每个输入数据,都会生成生回撤和更新两个输出结果。联合操作,需要的缓存空间更是会不断增长。我们要理解流的方式,才能对这些开销和代价有认知。
同时,在业务场景允许的情况,可以改为代价更小的做法,例如用窗口统计代替聚合统计,例如引入内部的微批次机制减少回撤数据的生成的。实际上,Flink 的 Blink Planer ,就是这么做的。
版权声明: 本文为 InfoQ 作者【KAMI】的原创文章。
原文链接:【http://xie.infoq.cn/article/c8db7a38015be8f3e1188421f】。文章转载请联系作者。
评论