用 Pulsar Functions 进行实时分析

原作者:David Kjerrumgaard
翻译:StreamNative-Sijia

对于许多事件驱动的应用程序来说,如何快速处理、理解和响应数据至关重要。在针对这些场景的分析和数据处理中,计算精确值可能会过于费时,或不合理占用资源。这种情况下,在给定时间内得到近似结果比等待准确结果更有意义。例如,要计算某个网页或网站上独立访客的确切数量,需要保留所有之前的独立访客记录以进行比较。唯一标识符的数量不可累加,因此并行性完全无所助益。

如果一个用例不需要精确结果,并且可使用近似值,那么在需要更少内存的条件下,我们可以提供多种技术和算法更快地计算精确的近似值。并且,有几个开源库实现了本文中涉及的每个模式,这相对简化了在 Apache Pulsar Function 中使用这些库的操作。

近似设计模式

此类模式指当事件流太大而无法存储,或因数据移动得太快而无法处理时,提供近似值、估计值和随机数据样本以进行统计分析的技术。

我们可以利用能够使用小型数据结构的算法,而无需保留大量数据。这些数据结构通常为千字节,又称为 sketch。Sketch 也是流算法,因为每个传入的项目只需要查看一次。由于具有这两个属性,这些算法成为了边缘设备部署上的理想选择。

模式 1:集合元素

有时,我们需要在不查询外部数据存储的条件下,确认是否在合理确定范围内看到过流元素。由于无法将整个流历史记录保留在内存中,我们需要借助一种能够利用数据结构的近似技术—— Bloom filter。Bloom filter 是一种节省空间的概率数据结构,可用于测试元素是否属于集合。

图 1 Bloom Filter 算法

如图 1 所示,所有 bloom filter 都使用两个关键元素。

  1. N 位数组,初始化为 0
  2. K 个独立哈希函数 h(x)的集合,输入一个值,生成一个小于 N 的数

向 filter 中添加新元素时,对该元素执行所有哈希函数。这些哈希值可视为位数组的索引,并将相应的数组元素设置为“1”。

当检查 filter 中是否已经存在某个元素时,我们会再次使用哈希函数,但是这次要对每个哈希索引进行数组查找。如果其中至少有一个为零,则 filter 中存在此元素。Bloom filter 的一个主要特点是保证不会返回假阴性。因此,可以确定一个元素在集合中,或可能在集合中,但需要其他逻辑才能最终确定。下面的示例利用 Twitter 中 Bloom Filter 算法的stream-lib 来实现,Bloom Filter 最简单的操作就是过滤。当 Pulsar Function 处理新事件时,首先检查确认我们是否见过这一事件。如果没有,则将其路由到“未见过” topic 以进行进一步处理。

import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import com.clearspring.analytics.stream.membership.BloomFilter;
 
public class BloomFilterFunction implements Function {
    BloomFilter filter = new BloomFilter(20, 20);
 
    Void process(String input, Context context) throws Exception {
      if (!filter.isPresent(input)) {
        filter.add(input);
        // Route to “not seen” topic
        context.publish(“notSeenTopic”, input);
      }
      return null;
   }
}

模式 2:事件频率

另一个常见的近似统计是一个特定元素在含有重复元素的无限数据流中出现的频率。这在回答诸如“元素 X 在数据流中出现了多少次?”的问题时非常有用。而这类结果在网络监控和分析中尤为实用。

那么,如果计算样本频率这一过程很简单,为什么还需要获取一个近似值呢?为什么不直接计算每个样本的观测值,然后除以总观测值来计算频率呢?

许多高频事件流中,用于此类计算的时间或内存都不充足。可以想象仅通过一个 40 Gbps 的连接进行分析和网络流量采样,该连接每秒可以处理 7800 万个 64 字节的数据包。仅对于单个 40 Gbps 的连接来说,进行计算的时间或存储数据的空间都不充足,更不用说由多个这样的网络段组成的网络了。

图 2 时间频率中的 Count-Min Sketch 算法

在这种情况下,因为无法及时计算出精确结果,所以可以选择使用准确度可接受的估计值。采样频率估计中最常用的算法是 Count-Min Sketch,此算法无需存储数据本身,即可提供数据的 sketch(近似值)。如图 2 所示,Count-Min Sketch 算法执行过程中使用两个元素:

  1. M x K 的计数器矩阵,每个矩阵都初始化为 0,每一行对应一个哈希函数
  2. K 个独立哈希函数 h(x)的集合

向 sketch 中添加新元素时,对该元素执行所有哈希函数。这些哈希值可视为位数组的索引,同时将相应的数组元素增加“1”。

现在,可以看到存储在 M-K 矩阵中每个元素的近似值,只要对每个元素执行哈希函数,并像插入时一样检索所有对应的数组元素,即可快速确定元素 X 在流中出现过多少次。但是,这次我们不增加数组元素的值,而是将列表中的最小值作为事件计数的估计值。

由于在计数器上只增不减,使用这一算法估计的事件频率只会偏高。这样,我们就可以知道见过的元素中,返回值即为出现次数最多的计数。Count-Min Sketch 算法的准确性由哈希函数 k 的数量决定。要计算 X 的概率误差,需要设置 k >= log 1/X,这样对于中等大小的 k 值(例如:k=5),产生的概率误差为 1%。

向 sketch 中添加输入后,可以立刻得到计数的估计值,可用于基于该计数的任何形式的逻辑,包括简单的 function,例如:过滤事件、在计数超过阈值时发出警报,或更复杂的 function,例如:将更新的计数发布到外部数据存储以在仪表板上显示等。

模式 3:最频繁 K 项估计

Count-Min 算法的另一个常见用法是维护频繁项列表,这些列表通常称为“Heavy Hitters”。这种设计模式保留了比某些预定义值(例如:前 K 项列表,数据流中 K 个最频繁项的列表,如 Twitter 上浏览量最高的 10 条推文,或 Amazon 上最受欢迎的 20 个产品)更频繁出现的项。

这种设计模式还有另外几种实际应用,例如:检测正在异常发送大量数据的 IP 地址(如在拒绝服务攻击期间,即攻击者向目标服务发起大量请求,消耗服务资源,使正常用户无法得到服务),识别交易量较大的股票等。

还可以通过 Count-Min Sketch 算法来解决最频繁 K 项估计的问题。更新计数的逻辑与事件频率(Event Frequency)用例中的逻辑完全相同。但是,还有一个长度为 K 的附加列表,用来保存更新后最频繁的 K 个元素,如图 3 所示。向 Top-K sketch 中添加元素时,执行如下逻辑:

  • 对该元素执行所有哈希函数。哈希值可视为位数组的索引,相应的数组元素加 1。
  • 像在事件频率用例中的操作一样,通过对元素执行所有哈希函数,并像插入时一样检索所有对应的数组元素来计算该元素的事件频率。但是,这次不给相应的数组元素加 1,而是将列表中的最小值作为近似事件的计数。
  • 将计算所得的该元素的事件频率与数组中前 K 个元素的最小值进行比较,如果事件频率较大,则删除数组中的最小值,并将其替换为新元素。

图 3 Top-K 算法数据流程图

图 3 显示了将该元素添加到 Top-K sketch 时的事件序列。首先,执行 k 个独立的哈希函数,并将每个对应的数组 entry 加 1(例如:98+1)。接下来,计算更新后数组 entry 中的最小值(99,108,312,681,282),示例中的最小值为 99,再与计数最大值对比。如果最小值更大,则用新元素替换原来的最小值。

在下面的 Pulsar Function 代码示例中,StreamSummary 类执行了所有与 Top-K 列表更新相关的操作,所以我们只需调用“offer”方法,先将元素添加到 sketch 中,如果元素也在 Top-K 列表中,则将其路由到优先级 topic 中以进行进一步处理。

import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import com.clearspring.analytics.stream.StreamSummary;
 
public class CountMinFunction implements Function {
     StreamSummary summary = new StreamSummary (256);
 
     Void process(String input, Context context) throws Exception {
        // Add the element to the sketch
        summary.offer(input, 1)
        // Grab the updated top 10,
        List> topK = summary.topK(10);
        return null;
     }
}

向 sketch 中添加输入后,Top-K 就会立刻更新,并且可用于执行任何形式的逻辑,例如:将更新后的 Top-K 发布到外部数据存储以在仪表板上显示等。

模式 4:不同元素计数

在一些用例中,数据流包含重复的元素,但我们想要计算其中不同元素的数量(例如:IP 地址、独立访客、广告曝光量等)。在计算机科学中,这是一个众所周知的问题,称为 Count-distinct 问题。在资源受限的环境中,我们不能将整个流存储在内存中,因而必须依靠概率算法,这种近似结果非常有效。目前,用于解决 Coun-distinct 问题的算法有以下两种:

  • 基于位模式算法(Bit-patten Based) :通过基于集合中每个数字二进制形式的计算来估算数据流中不重复元素数量的观测值。此类算法包括:LogLogHyperLogLogHyperLogLog++ 等。
  • 基于顺序统计算法(Order-statistics Based):基于顺序统计,例如:流中的最小值。此类算法包括 MinCountBar-Tossef 等。

HyperLogLog

HyperLogLog 算法的基本流程包括四个步骤,如图 4 所示,获取要计数的元素并对其执行哈希函数,获取哈希值并将其转换为二进制字符串。

二进制字符串的最低 p 个有效位用于确定待更新寄存器的位置。P 值决定估计值的精度,必须大于零。P 值越大,估计值越精确,但为了权衡空间,p 值每增长一次,空间就会指数增长一次,也就是说,空间需求为 2 的 p 次方。

一旦寄存器的位置已知,该算法就会利用“位模式可观察量”现象,即对剩余的位字符串从右边开始计算 0 的数量,每出现一个 0,计数增加 1。然后,用最终结果替换之前确定的寄存器位置。

图 4 HyperLogLog 算法数据流程图

幸运的是,有一个 HyperLogLog 算法的 Apache 许可实现,我们可以在其中使用 Apache Pulsar Function:

import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import io.airlift.stats.cardinality.HyperLogLog;
 
public class HyperLogLogFunction implements Function {
   HyperLogLog hll = HyperLogLog.newInstance(2048);
 
   Void process(Integer value, Context context) throws Exception {
       hll.add(value);
       Integer numDistinctElements = hll.cardinality();
       // Do something with the distinct elements
   }
}

先来看一下向 HyperLogLog 添加数据元素的过程,以深化理解。如图 5 所示,左侧为要添加的原始数据 10,529,222,右侧为由原始数据生成的哈希值 2,386,714,787。

哈希值的二进制字符串表示如下图所示,其中蓝色的为 6 个最低有效位,绿色的为其余数位。6 个最低有效位表示十进制值 35,用作寄存器的索引。

接下来,我们从剩余数字的最右端向左数出现 1 之前连续出现的 0 的次数,得到值为 1。用个数 1 加上数值 1 则得到 2,将该值存放于图中红色标注的第 35 个寄存器中。对每个新添加的元素重复此过程。

图 5 HyperLogLog 算法内元素处理

总结

本文介绍了几种近似算法,这些算法因其空间效率和恒定时间性能成为流数据集性能分析的最佳选择。

我们深入研究了基于概率算法的实时分析功能并给出了一些 Pulsar Function 示例,这些 Function 利用了上述算法的现有开源实现,使得入门使用更轻松便捷。

本文展示了将这些复杂算法的现有实现合并到 Apache Pulsar Functions 中的操作是简便可行的,可将其部署在资源受限的环境中。因此,可以利用这些近似算法,而不必了解代码和/或自己写代码。

你可能感兴趣的:(pulsar,function)