news 2026/10/8 19:29:16

Zeek SumStats HLL_UNIQUE 插件深度解析:基于 HyperLogLog 的高效唯一值基数统计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zeek SumStats HLL_UNIQUE 插件深度解析:基于 HyperLogLog 的高效唯一值基数统计
  • 网络安全
  • 网络
  • IDS

【免费下载链接】zeek

Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.

项目地址:https://gitcode.com/gh_mirrors/ze/zeek
点击查看免费下载

导读

本文深入解析 Zeek 网络分析框架中 SumStats 汇总统计框架的核心插件HLL_UNIQUE(源码位于 scripts/base/frameworks/sumstats/plugins/hll_unique.zeek,官方文档见 doc/scripts/base/frameworks/sumstats/plugins/hll_unique.zeek.rst)。该插件借助 HyperLogLog(HLL)概率算法,以固定内存代价统计海量数据流中的唯一值数量,是构建扫描检测、访问量审计、去重计数等安全分析场景的高效工具。读完本文,你将掌握 HLL_UNIQUE 的加载方式、两个精度控制参数(hll_error_margin、hll_confidence)的语义与取值技巧、完整的 Zeek 脚本实战写法,以及集群环境下 HLL 计数器如何被无缝合并聚合的底层原理。

一、插件在 SumStats 框架中的定位

SumStats(Summary Statistics)是 Zeek 内置的通用汇总统计框架,定位是"把大流量的数据流归纳为简单的缩减度量"(见 scripts/base/frameworks/sumstats/main.zeek 第 1-3 行注释)。它的核心抽象包括:

  • SumStats::Key:被统计的对象标识,由str: string与host: addr两个可选字段组成,例如"客户端 IP"或"HTTP Host 头中的域名";
  • SumStats::Observation:单次观测数据点,num(count)、dbl(double)、str(string)三字段一次只填一个;
  • SumStats::Reducer:绑定到某条观测流(stream)并声明要做哪些计算(apply)的归约器;
  • SumStats::SumStat:一组 Reducer 的聚合体,按epoch时间窗切分结果,并通过epoch_result、threshold_crossed等回调输出。

框架本身只定义了PLACEHOLDER这一占位计算类型,全部具体计算均由插件以"注册"方式扩展(见 scripts/base/frameworks/sumstats/main.zeek 第 8-12 行与第 236-261 行的钩子定义)。官方随框架提供的插件共有 12 个(见 doc/scripts/base/frameworks/sumstats/plugins/index.rst):average、hll_unique、last、max、min、sample、std-dev、variance、sum、topk、unique。其中hll_unique与unique都负责"统计唯一值数量",但实现思路截然不同,本文的主角hll_unique走的是概率型 HyperLogLog 路线。

二、插件加载与对外接口

hll_unique插件通过 Zeek 的模块加载指令引入:

@load base/frameworks/sumstats/plugins/hll_unique

插件内部以@load base/frameworks/sumstats保证主框架先行加载,并在module SumStats;命名空间内通过redef record与redef enum向三个核心类型注入新成员(源码见 scripts/base/frameworks/sumstats/plugins/hll_unique.zeek 第 7-37 行)。

2.1 新增计算类型HLL_UNIQUE

SumStats::Calculation枚举增加一个成员:

成员语义
SumStats::HLL_UNIQUE计算观测流中唯一值的数量(基于 HyperLogLog 算法)

它需要出现在 Reducer 的apply集合中才会被触发执行。

2.2 Reducer 新增的两个精度控制字段

字段类型默认值说明
hll_error_margindouble0.01(&default,可选)HLL 的误差率(error margin)
hll_confidencedouble0.95(&default,可选)HLL 的置信度(confidence)

这两个参数共同决定了 HLL 内部桶(bucket)的数量:误差率越小、置信度越高,需要的桶越多、内存开销越大,但估算越精确。更多参数细节见 scripts/base/frameworks/sumstats/plugins/hll_unique.zeek 第 8-14 行。

2.3 ResultVal 新增的结果字段

字段类型默认值说明
hll_uniquecount0(&default,可选)追踪基数时的唯一值数量估算结果
cardopaqueof cardinality无(可选)内部使用:HLL 计数器句柄,仅能通过专用 BIF 函数操作
hll_error_margindouble无(可选)内部使用:供结果合并(compose)钩子读取
hll_confidencedouble无(可选)内部使用:供结果合并(compose)钩子读取

其中card在源码注释中被明确标注为 "Internal use only",因为概率型数据结构必须经由专用 BIF 检查,脚本层不应直接触碰(见 scripts/base/frameworks/sumstats/plugins/hll_unique.zeek 第 28-37 行)。脚本开发者在epoch_result回调中直接读取result$hll_unique即可。

三、HyperLogLog 原理与参数背后的内存权衡

要理解hll_error_margin与hll_confidence的真实含义,需要看底层实现。Zeek 的 HLL 实现在 src/probabilistic/CardinalityCounter.cc 与 src/probabilistic/CardinalityCounter.h 中,类名为zeek::probabilistic::detail::CardinalityCounter。

3.1 桶数由误差与置信度共同推导

构造函数CardinalityCounter(double error_margin, double confidence)(CardinalityCounter.cc 第 86-91 行)调用OptimalB()计算出桶数参数b,再以2^b作为桶数量m。OptimalB()(第 16-31 行)的推导逻辑是:

  • 先按公式2 * (log(1.04) - log(error)) / ln2给出b的初始估算;
  • 再以"置信度必须落在k个标准差之内"为约束逐步增大b,直到erf(k / sqrt(2)) >= confidence为止。

也就是说,hll_error_margin = 0.01意味着估算值的相对误差约在 ±1% 量级,而hll_confidence = 0.95意味着 95% 的把握该误差区间成立。头文件中的示例说明更直观(见 CardinalityCounter.h 第 27-31 行):假设误差率为 2%、置信度 95%,若Size()返回估算值 100,则可以 95% 确信真实基数落在 98 到 102 之间。

3.2 桶数组与内存占用

Init()(CardinalityCounter.cc 第 33-67 行)要求桶数量必须是 16、32、64 或大于等于 128 的 2 的幂,否则直接触发InternalError。每个桶只是一个uint8_t(记录该桶内元素哈希的首个置位比特位置,见 CardinalityCounter.h 第 172-178 行),因此内存占用极小——例如默认参数(误差 1%、置信度 95%)只需数千个桶,总计几 KB 级别。这正是 HLL 相比精确计数的核心优势:无论观测了几百万还是几十亿个元素,内存恒定不变。误差率与桶数的标准关系是1.04/sqrt(m)(68% 概率下的标准误差,见 CardinalityCounter.h 第 165-169 行注释),开发者可按此公式反向估算所需桶规模。

四、源码级实现解析:观察、估算与合并

4.1 观察钩子:注册HLL_UNIQUE计算

插件通过register_observe_plugin(HLL_UNIQUE, ...)注册观察回调(见 scripts/base/frameworks/sumstats/plugins/hll_unique.zeek 第 39-53 行),主框架在zeek_init时统一执行register_observe_plugins()钩子(见 scripts/base/frameworks/sumstats/main.zeek 第 307-311 行)。每次SumStats::observe()向该流注入数据时,回调做三件事:

  1. 惰性初始化:若rv$card尚未建立,则以 Reducer 上配置的hll_error_margin/hll_confidence调用hll_cardinality_init()创建计数器,并把参数快照存入 ResultVal(rv$hll_error_margin、rv$hll_confidence);
  2. 添加元素:调用hll_cardinality_add(rv$card, obs)把本次观测值喂入计数器;
  3. 实时估算:调用hll_cardinality_estimate(rv$card) as count更新rv$hll_unique。

需要留意的是,SumStats::observe()在 main.zeek 第 493-497 行有一个取值约定:当观测值是字符串时统一回退为数值1.0。这意味着字符串型观测(如 URL、域名、用户名)会被 HLL 插件当作"元素本身"去重计数,而数值型观测的数值本身才是去重对象——请按你的统计目标选择合适的Observation字段。

4.2 合并钩子:集群/多流的基数无损合并

SumStats 框架在合并两个ResultVal时会执行compose_resultvals_hook(见 main.zeek 第 320-331 行)。hll_unique插件的合并逻辑(见 hll_unique.zeek 第 55-78 行)是 HLL 算法最精彩的部分:

  • 若两侧都没有card,直接返回;
  • 否则用存在一侧的hll_error_margin/hll_confidence新建计数器,并调用hll_cardinality_merge_into()把两个计数器逐桶取最大值合并;
  • 合并后立即用hll_cardinality_estimate()刷新result$hll_unique。

由于 HyperLogLog 的可合并性(两个同参数计数器可以无损合并为等价于"各自数据直接观测"的计数器),多路数据流的唯一值去重不会因合并而重复计数。这一点在集群场景中价值极大:每个 worker 独立维护各自的 HLL,manager 汇总时只需合并,无需传输全量原始数据。

4.3 底层的五个 BIF 函数

插件依赖的 BIF 定义在 src/probabilistic/cardinality-counter.bif,全部面向opaque of cardinality句柄:

BIF签名作用
hll_cardinality_init(err: double, confidence: double) : opaque of cardinality以误差率与置信度创建计数器
hll_cardinality_add(handle, elem: any) : bool添加元素;元素类型需与计数器已定型类型一致,否则报 "incompatible HLL data type"
hll_cardinality_estimate(handle) : double估算当前基数;计数器为空时返回 -1.0
hll_cardinality_merge_into(handle1, handle2) : bool把 handle2 并入 handle1;参数不同的计数器无法合并
hll_cardinality_copy(handle) : opaque of cardinality复制计数器

值得注意的类型约束:hll_cardinality_add()在首个元素加入时会为计数器"定型"(Typify,见 cardinality-counter.bif 第 43-61 行),后续元素类型必须一致。这也是 SumStats 插件把每次观测的obs直接传给hll_cardinality_add时,需要保持观测字段类型稳定的原因。

五、实战示例:统计每个源 IP 访问的独特 URL 数

下面是一个完整的可运行脚本,演示如何在连接(connection)层面收集 HTTP 请求数据,并按源 IP 统计其访问的独特 URL 数量:

@load base/frameworks/sumstats/plugins/hll_unique redef record HTTP::Info += { ## 在每个 HTTP 请求日志记录时触发 SumStats 观测 sumstats_url_obs: bool &default=F &log; }; event HTTP::log(rec: HTTP::Info) { if ( ! rec?$host || ! rec?$uri ) return; local obs = SumStats::Observation($str=cat(rec$host, rec$uri)); SumStats::observe("url_observations", [$host=rec$id$orig_h], obs); } event zeek_init() { local r1 = SumStats::Reducer($stream="url_observations", $apply=set(SumStats::HLL_UNIQUE), $hll_error_margin=0.01, $hll_confidence=0.95); SumStats::create([$name="unique-urls-per-source", $epoch=10min, $reducers=set(r1), $epoch_result=function(ts: time, key: SumStats::Key, result: SumStats::Result) { print fmt("%s: %s -> %s unique URLs", ts, key$host, result["url_observations"]$hll_unique); }]); }

要点拆解:

  • Reducer 通过$apply=set(SumStats::HLL_UNIQUE)声明计算类型,通过$hll_error_margin/$hll_confidence覆盖默认精度;
  • 观测流标识"url_observations"必须与SumStats::observe()的第一个参数一致,框架据此把数据路由到对应 Reducer(见 main.zeek 第 439-503 行的observe()实现);
  • epoch_result回调在每个 10 分钟窗口结束时,按 Key 逐个输出结果;这里读取result["url_observations"]$hll_unique即为该源 IP 访问的独特 URL 估算数量;
  • 若要叠加"突发扫描"检测,可在此基础上补充$threshold_val/$threshold_crossed,让唯一 URL 数跨过阈值时立刻告警,而不是等窗口结束(阈值语义见 main.zeek 第 114-133 行)。

六、集群部署:worker 采集、manager 合并的完整链路

SumStats 对集群是"透明"支持的:脚本加载路径 scripts/base/frameworks/sumstats/load.zeek 会根据Cluster::is_enabled()自动选择加载 cluster.zeek 或 non-cluster.zeek。对于 HLL_UNIQUE,集群链路的关键环节是:

  1. worker 侧:每个 worker 独立维护本地 HLL 计数器,每来一条观测就地更新card与hll_unique;窗口到期时 manager 广播cluster_ss_request,worker 把自己的ResultTable(含各 Key 的 HLL 句柄)打包发回;
  2. manager 侧:收到各 worker 的同一 Key 结果后调用compose_results()(main.zeek 第 333-351 行),逐流触发compose_resultvals_hook,即上一节描述的 HLL 桶级合并,最终得到覆盖全集群观测的唯一值估算;
  3. 阈值中间更新:worker 可在本地值达到阈值的cluster_request_global_view_percent(默认 0.2)比例时主动上报(见 cluster.zeek 第 12-18 行与 70-86 行),manager 随即请求全集群视图并合并判定,从而在窗口结束前就触发threshold_crossed。

正因 HLL 计数器体积极小且可无损合并,这条链路上传输的只是紧凑的桶数组而非原始观测流,集群通信开销被压到最低。这是hll_unique在分布式大规模流量下优于精确去重的决定性因素。

七、HLL_UNIQUE 与 UNIQUE 的选型对比

SumStats 同时提供了精确去重插件unique(见 scripts/base/frameworks/sumstats/plugins/unique.zeek),二者对比如下:

维度HLL_UNIQUEUNIQUE
数据结构HyperLogLog 概率计数器(opaque of cardinality)精确的set[Observation]
内存固定(由误差率/置信度决定桶数),与元素规模无关随唯一值数量线性增长
结果精度估算值,受hll_error_margin/hll_confidence约束精确值
上限控制无(靠内存换精度)支持unique_max限制存储上限
适用场景超大基数、集群聚合、长期统计基数可控、需要精确去重结果

一个值得注意的细节是:unique插件的源码注释中留有 TODO(unique.zeek 第 31-35 行),提到未来可能把内部实现迁移到 HyperLogLog 结构。这从侧面印证了概率去重是 Zeek 在该场景下的演进方向。对于单机、基数明确可控且要求精确的场景选UNIQUE;对于海量流、集群合并或需要极低内存的场景,HLL_UNIQUE是更合适的选择。

八、使用注意事项

  1. 参数平衡:hll_error_margin越小、hll_confidence越高,桶数越多。默认值 0.01/0.95 已覆盖绝大多数检测场景;追求更省内存可放宽误差到 0.02,追求更高精度可收紧到 0.005,但需自行评估内存代价。
  2. 观测字段类型稳定:HLL 计数器会按首个元素定型,混用$str与$num/$dbl观测会触发 BIF 层的类型不兼容报错(见 cardinality-counter.bif 第 53-57 行)。
  3. 结果取值:hll_unique是估算值(hll_cardinality_estimate(...) as count强转,见 hll_unique.zeek 第 51 行),做阈值判定时建议预留误差余量;空计数器时底层Size()返回 -1.0,强转后需留意边界行为。
  4. 不要直接操作card字段:它是内部句柄,正确姿势是只读hll_unique;如需在脚本中手工处理 HLL,请使用第二节列出的五个 BIF 函数。

参考路径速查

  • 插件源码:scripts/base/frameworks/sumstats/plugins/hll_unique.zeek
  • 插件文档:doc/scripts/base/frameworks/sumstats/plugins/hll_unique.zeek.rst
  • 框架主实现:scripts/base/frameworks/sumstats/main.zeek
  • 框架文档:doc/scripts/base/frameworks/sumstats/main.zeek.rst
  • HLL 底层实现:src/probabilistic/CardinalityCounter.cc、src/probabilistic/CardinalityCounter.h
  • HLL BIF 定义:src/probabilistic/cardinality-counter.bif
  • 集群支持:scripts/base/frameworks/sumstats/cluster.zeek
  • 精确去重对照插件:scripts/base/frameworks/sumstats/plugins/unique.zeek
  • 网络安全
  • 网络
  • IDS

【免费下载链接】zeek

Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.

项目地址:https://gitcode.com/gh_mirrors/ze/zeek
点击查看免费下载
上一篇:Data Engineering Zoomcamp:在 Docker 中以官方方式搭建 Apache Airflow,编排 NYC Taxi 数据摄入 GCS/BigQuery 管线
下一篇:Dapr Docker 镜像体系全解:发布镜像、调试镜像与开发容器构建指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 19:25:26

开源AI技能集Superpowers:让Claude稳定遵循你的工作习惯

1. 这个项目到底解决什么问题如果你经常用 Claude 这类 AI 助手处理日常任务,一定遇到过同一个问题——它很强,但它记不住你的习惯。每次让它写日志、做周报、给变量命名、按你的风格检查代码,它都像第一次见你一样,从头开始“猜”…

作者头像 李华
网站建设 2026/10/8 19:25:18

AI编程助手如何重塑代码审查:人机协同的三种模式与落地实践

1. 从“人肉审查”到“人机协同”:AI编程助手到底改了什么代码审查这件事,干了十年开发的人都有体会:它从来不是“看代码”这么简单。一个中等规模的团队,每天可能产生几十个合并请求,每个请求涉及几百行改动。审查者要…

作者头像 李华
网站建设 2026/10/8 19:12:39

题解:洛谷 P1098 [NOIP 2007 提高组] 字符串的展开

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/8 19:09:55

数据采集教学闭环:从HTML解析到EXIF元数据提取

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华