- 网络安全
- 网络
- IDS
【免费下载链接】zeek
Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.
导读
本文深入解析 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_margin | double | 0.01(&default,可选) | HLL 的误差率(error margin) |
hll_confidence | double | 0.95(&default,可选) | HLL 的置信度(confidence) |
这两个参数共同决定了 HLL 内部桶(bucket)的数量:误差率越小、置信度越高,需要的桶越多、内存开销越大,但估算越精确。更多参数细节见 scripts/base/frameworks/sumstats/plugins/hll_unique.zeek 第 8-14 行。
2.3 ResultVal 新增的结果字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
hll_unique | count | 0(&default,可选) | 追踪基数时的唯一值数量估算结果 |
card | opaqueof cardinality | 无(可选) | 内部使用:HLL 计数器句柄,仅能通过专用 BIF 函数操作 |
hll_error_margin | double | 无(可选) | 内部使用:供结果合并(compose)钩子读取 |
hll_confidence | double | 无(可选) | 内部使用:供结果合并(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()向该流注入数据时,回调做三件事:
- 惰性初始化:若
rv$card尚未建立,则以 Reducer 上配置的hll_error_margin/hll_confidence调用hll_cardinality_init()创建计数器,并把参数快照存入 ResultVal(rv$hll_error_margin、rv$hll_confidence); - 添加元素:调用
hll_cardinality_add(rv$card, obs)把本次观测值喂入计数器; - 实时估算:调用
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,集群链路的关键环节是:
- worker 侧:每个 worker 独立维护本地 HLL 计数器,每来一条观测就地更新
card与hll_unique;窗口到期时 manager 广播cluster_ss_request,worker 把自己的ResultTable(含各 Key 的 HLL 句柄)打包发回; - manager 侧:收到各 worker 的同一 Key 结果后调用
compose_results()(main.zeek 第 333-351 行),逐流触发compose_resultvals_hook,即上一节描述的 HLL 桶级合并,最终得到覆盖全集群观测的唯一值估算; - 阈值中间更新: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_UNIQUE | UNIQUE |
|---|---|---|
| 数据结构 | HyperLogLog 概率计数器(opaque of cardinality) | 精确的set[Observation] |
| 内存 | 固定(由误差率/置信度决定桶数),与元素规模无关 | 随唯一值数量线性增长 |
| 结果精度 | 估算值,受hll_error_margin/hll_confidence约束 | 精确值 |
| 上限控制 | 无(靠内存换精度) | 支持unique_max限制存储上限 |
| 适用场景 | 超大基数、集群聚合、长期统计 | 基数可控、需要精确去重结果 |
一个值得注意的细节是:unique插件的源码注释中留有 TODO(unique.zeek 第 31-35 行),提到未来可能把内部实现迁移到 HyperLogLog 结构。这从侧面印证了概率去重是 Zeek 在该场景下的演进方向。对于单机、基数明确可控且要求精确的场景选UNIQUE;对于海量流、集群合并或需要极低内存的场景,HLL_UNIQUE是更合适的选择。
八、使用注意事项
- 参数平衡:
hll_error_margin越小、hll_confidence越高,桶数越多。默认值 0.01/0.95 已覆盖绝大多数检测场景;追求更省内存可放宽误差到 0.02,追求更高精度可收紧到 0.005,但需自行评估内存代价。 - 观测字段类型稳定:HLL 计数器会按首个元素定型,混用
$str与$num/$dbl观测会触发 BIF 层的类型不兼容报错(见 cardinality-counter.bif 第 53-57 行)。 - 结果取值:
hll_unique是估算值(hll_cardinality_estimate(...) as count强转,见 hll_unique.zeek 第 51 行),做阈值判定时建议预留误差余量;空计数器时底层Size()返回 -1.0,强转后需留意边界行为。 - 不要直接操作
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.
相关推荐
Zeek SumStats 框架 MIN 计算插件深入解析:用 `SumStats::MIN` 追踪数值流的最小值
Zeek SumStats 框架 MIN 计算插件深入解析:用 SumStats::MIN 追踪数值流的最小值 SumStats::MIN 是 Zeek 汇总统
网络安全网络IDSStarRocks approx_count_distinct 函数解析:基于 HyperLogLog 的高基数去重计数
StarRocks approx_count_distinct 函数解析:基于 HyperLogLog 的高基数去重计数 approx_count_distin
数据库OLAP数据仓库大数据湖仓一体数据分析node-redis HyperLogLog:基数统计的高效实现
node redis HyperLogLog:基数统计的高效实现 在当今大数据时代,如何高效统计海量数据的唯一元素数量成为了一个重要挑战。node redis
后端数据库客户端缓存
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考