Zeek SumStats HLL_UNIQUE 插件深度解析:基于 HyperLogLog 的高效唯一值基数统计
网络安全网络IDS【免费下载链接】zeekZeek 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。该插件借助 HyperLogLogHLL概率算法以固定内存代价统计海量数据流中的唯一值数量是构建扫描检测、访问量审计、去重计数等安全分析场景的高效工具。读完本文你将掌握 HLL_UNIQUE 的加载方式、两个精度控制参数hll_error_margin、hll_confidence的语义与取值技巧、完整的 Zeek 脚本实战写法以及集群环境下 HLL 计数器如何被无缝合并聚合的底层原理。一、插件在 SumStats 框架中的定位SumStatsSummary Statistics是 Zeek 内置的通用汇总统计框架定位是把大流量的数据流归纳为简单的缩减度量见 scripts/base/frameworks/sumstats/main.zeek 第 1-3 行注释。它的核心抽象包括SumStats::Key被统计的对象标识由str: string与host: addr两个可选字段组成例如客户端 IP或HTTP Host 头中的域名SumStats::Observation单次观测数据点numcount、dbldouble、strstring三字段一次只填一个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.rstaverage、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_UNIQUESumStats::Calculation枚举增加一个成员成员语义SumStats::HLL_UNIQUE计算观测流中唯一值的数量基于 HyperLogLog 算法它需要出现在 Reducer 的apply集合中才会被触发执行。2.2 Reducer 新增的两个精度控制字段字段类型默认值说明hll_error_margindouble0.01default可选HLL 的误差率error marginhll_confidencedouble0.95default可选HLL 的置信度confidence这两个参数共同决定了 HLL 内部桶bucket的数量误差率越小、置信度越高需要的桶越多、内存开销越大但估算越精确。更多参数细节见 scripts/base/frameworks/sumstats/plugins/hll_unique.zeek 第 8-14 行。2.3 ResultVal 新增的结果字段字段类型默认值说明hll_uniquecount0default可选追踪基数时的唯一值数量估算结果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()向该流注入数据时回调做三件事惰性初始化若rv$card尚未建立则以 Reducer 上配置的hll_error_margin/hll_confidence调用hll_cardinality_init()创建计数器并把参数快照存入 ResultValrv$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 独立维护各自的 HLLmanager 汇总时只需合并无需传输全量原始数据。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 typehll_cardinality_estimate(handle) : double估算当前基数计数器为空时返回 -1.0hll_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 defaultF log; }; event HTTP::log(rec: HTTP::Info) { if ( ! rec?$host || ! rec?$uri ) return; local obs SumStats::Observation($strcat(rec$host, rec$uri)); SumStats::observe(url_observations, [$hostrec$id$orig_h], obs); } event zeek_init() { local r1 SumStats::Reducer($streamurl_observations, $applyset(SumStats::HLL_UNIQUE), $hll_error_margin0.01, $hll_confidence0.95); SumStats::create([$nameunique-urls-per-source, $epoch10min, $reducersset(r1), $epoch_resultfunction(ts: time, key: SumStats::Key, result: SumStats::Result) { print fmt(%s: %s - %s unique URLs, ts, key$host, result[url_observations]$hll_unique); }]); }要点拆解Reducer 通过$applyset(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_requestworker 把自己的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_UNIQUEUNIQUE数据结构HyperLogLog 概率计数器opaque of cardinality精确的set[Observation]内存固定由误差率/置信度决定桶数与元素规模无关随唯一值数量线性增长结果精度估算值受hll_error_margin/hll_confidence约束精确值上限控制无靠内存换精度支持unique_max限制存储上限适用场景超大基数、集群聚合、长期统计基数可控、需要精确去重结果一个值得注意的细节是unique插件的源码注释中留有 TODOunique.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.rstHLL 底层实现src/probabilistic/CardinalityCounter.cc、src/probabilistic/CardinalityCounter.hHLL BIF 定义src/probabilistic/cardinality-counter.bif集群支持scripts/base/frameworks/sumstats/cluster.zeek精确去重对照插件scripts/base/frameworks/sumstats/plugins/unique.zeek赞分享网络安全网络IDS【免费下载链接】zeekZeek 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 框架 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后端数据库客户端缓存上一篇Data Engineering Zoomcamp在 Docker 中以官方方式搭建 Apache Airflow编排 NYC Taxi 数据摄入 GCS/BigQuery 管线下一篇Dapr Docker 镜像体系全解发布镜像、调试镜像与开发容器构建指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考