Update
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
---
|
||||
tags: [CS, NET, algorithm, data-stream, heavy-hitter]
|
||||
create time: 2026-04-17 13:45
|
||||
---
|
||||
|
||||
# HeavyKeeper
|
||||
|
||||
## 概述
|
||||
|
||||
HeavyKeeper 是一种用于高速数据流中检测 Heavy Hitters(频繁项)的高效算法。它在保持常数内存空间的同时,能够准确地识别出出现频率超过设定阈值的数据项,广泛应用于网络流量监测、热点检测等场景。
|
||||
|
||||
## 算法原理
|
||||
|
||||
### 核心思想
|
||||
|
||||
HeavyKeeper 结合了 Count-Min Sketch 和守桶策略,通过多层哈希守桶机制来提高准确性。其核心目标是区分"大象流"和"老鼠流"。
|
||||
|
||||
#### 什么是大象流和老鼠流?
|
||||
|
||||
在数据流分析中,通常将数据流按频率分为两类:
|
||||
|
||||
| 类型 | 特征 | 频率占比 | 典型例子 |
|
||||
|------|------|----------|---------|
|
||||
| **大象流**(Elephant Flow) | 高频出现 | 占总流量的大部分 | 热门IP、热门搜索词、DDoS攻击流量 |
|
||||
| **老鼠流**(Mouse Flow) | 低频偶发 | 数量众多但频率极低 | 少量用户访问、正常连接请求 |
|
||||
|
||||
**关键洞察**:在很多场景中,**80-90%的流量来自不到1%的源**,这就是大象流。HeavyKeeper 的目标就是高效识别这些大象流,过滤掉老鼠流。
|
||||
|
||||
### 守桶机制
|
||||
|
||||
#### 什么是"守桶"?
|
||||
|
||||
"守桶"(Keeper)是 HeavyKeeper 的核心创新。每个桶会"守护"一个特定的数据项:
|
||||
|
||||
- 当数据流中的一个项到来时,哈希到某个桶
|
||||
- 如果这个项正好是该桶"守护"的项,就直接计数
|
||||
- 如果不是,则根据概率决定是否"抢夺"守护权
|
||||
|
||||
**底层原理**:让大象流(高频项)能够长期占据守桶位置,而老鼠流(低频项)很难长期占用桶的资源。
|
||||
|
||||
#### 桶的结构
|
||||
|
||||
每个桶维护以下信息:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| **item** | 数据项 | 当前守护的数据项 |
|
||||
| **count** | 整数 | 守护项的精确计数 |
|
||||
| **error** | 整数 | 误差估计(记录非守护项经过的次数) |
|
||||
|
||||
#### 守桶策略:大象流如何压制老鼠流
|
||||
|
||||
**替换概率公式**:
|
||||
```python
|
||||
替换概率 = min(1, 新项估计频率 / 当前守护项计数)
|
||||
```
|
||||
|
||||
这个公式的直观含义:
|
||||
|
||||
| 情况 | 新项类型 | 替换概率 | 结果 |
|
||||
|------|---------|---------|------|
|
||||
| 大象流 vs 老鼠流 | 老鼠流(freq≈1) | 1/count | 极小,**老鼠流无法撼动大象流** |
|
||||
| 老鼠流 vs 老鼠流 | 老鼠流(freq≈2) | 2/count | 较小,随机性强 |
|
||||
| 大象流 vs 老鼠流 | 大象流(freq=50) | 50/5=1 | 必然替换,**新大象流抢占桶** |
|
||||
| 大象流 vs 大象流 | 大象流(freq=98) | 98/95≈1 | 可能替换,两个大象流竞争 |
|
||||
|
||||
**例子**:
|
||||
```
|
||||
桶#100 当前守护:IP=10.0.0.1 (count=100, error=5) ← 大象流
|
||||
新到来:IP=10.0.0.2 → 哈希到桶#100 ← 老鼠流
|
||||
|
||||
替换概率 = min(1, 1/100) = 0.01
|
||||
|
||||
结果:0.95(随机数)> 0.01 → 不替换
|
||||
解释:大象流继续守护,老鼠流只能默默增加error
|
||||
```
|
||||
|
||||
#### 多层哈希的作用
|
||||
|
||||
单层哈希可能发生冲突(多个项哈希到同一个桶),多层哈希通过冗余来解决:
|
||||
|
||||
- 同一个项会由L个不同的哈希函数映射到L层的不同桶
|
||||
- 查询时取所有层的最小值(保守估计)
|
||||
- 即使部分桶冲突,也能获得准确的下界
|
||||
|
||||
**最终计数** = min(所有层中该项的count值)
|
||||
|
||||
#### Heavy Hitters 检测流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[数据流新项 x] --> B[计算L个哈希]
|
||||
B --> C[访问L个桶]
|
||||
|
||||
C --> D{是否为守护项?}
|
||||
D -->|是| E[count++]
|
||||
D -->|否| F[计算替换概率]
|
||||
|
||||
F --> G{触发替换?}
|
||||
G -->|是| H[替换并重置count=1]
|
||||
G -->|否| I[error++]
|
||||
|
||||
E --> J[继续]
|
||||
H --> J
|
||||
I --> J
|
||||
```
|
||||
|
||||
查找Top K时,只需遍历所有桶,收集 `count ≥ 阈值` 的候选项。
|
||||
|
||||
### 衰减机制
|
||||
|
||||
#### 为什么需要衰减?
|
||||
|
||||
**问题场景**:
|
||||
```
|
||||
10:00-10:05 IP=10.0.0.1 出现 1000 次 → 成为大象流,占据桶
|
||||
10:06-12:00 IP=10.0.0.1 不再出现,但其count=1000依然存在
|
||||
12:01 IP=10.0.0.2 频繁出现,但无法抢占count=1000的桶
|
||||
```
|
||||
|
||||
如果不衰减,过时的大象流会持续占用资源,阻碍新大象流的检测。
|
||||
|
||||
#### 衰减机制的工作原理
|
||||
|
||||
HeavyKeeper 通过周期性衰减来解决这个问题:
|
||||
|
||||
**方法1:时间窗口衰减(推荐)**
|
||||
```python
|
||||
def periodic_decay():
|
||||
每经过 Δt 时间,所有桶的 count 和 error 乘以衰减因子 α
|
||||
count = count × α
|
||||
error = error × α
|
||||
其中 α ∈ (0, 1),通常 α = 0.9 或 0.99
|
||||
```
|
||||
|
||||
**方法2:基于老化(Aging)**
|
||||
```python
|
||||
def aging(bucket, current_time):
|
||||
elapsed = current_time - bucket.last_update_time
|
||||
decay = exp(-λ × elapsed) # λ 是衰减速率
|
||||
bucket.count = bucket.count × decay
|
||||
```
|
||||
|
||||
#### 衰减机制的数学效果
|
||||
|
||||
**时间窗口视角**:
|
||||
```
|
||||
衰减因子 α = 0.99,窗口大小 = N
|
||||
|
||||
N时刻前的权重:0.99^N ≈ 0.366 ← 仅保留36.6%
|
||||
2N时刻前的权重:0.99^2N ≈ 0.134 ← 只保留13.4%
|
||||
```
|
||||
|
||||
这意味着:越久远的计数对当前统计影响越小,让算法能够"遗忘"过时的流。
|
||||
|
||||
#### 衰减规则示例
|
||||
|
||||
| 场景 | 原 count | 衰减后 count | 解析 |
|
||||
|------|----------|-------------|------|
|
||||
| 持续活跃的大象流 | 1000 | 990 (×0.99) | 持续补充,衰减不影响地位 |
|
||||
| 最近消失的大象流 | 1000 | 366 (×0.99^100) | 100个周期后快速衰减,让出桶 |
|
||||
| 新大象流 | 0 → 10 | 10 (刚开始) | 有机会竞争已衰减的桶 |
|
||||
|
||||
#### 衰减带来的好处
|
||||
|
||||
| 优势 | 说明 |
|
||||
|------|------|
|
||||
| **自适应流行度漂移** | 热点变化时,旧热点会自动失去守桶权 |
|
||||
| **滑动窗口效果** | 只关注最近时间窗口内的频率,而非历史总和 |
|
||||
| **防止资源垄断** | 过时的大象流不会长期占用桶资源 |
|
||||
|
||||
#### 衰减与守桶的协同
|
||||
|
||||
衰减机制和守桶机制协同工作,形成一个动态平衡:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[大象流活跃] --> B[count快速累积]
|
||||
B --> C[占据守桶位置]
|
||||
|
||||
C --> D{时间流逝}
|
||||
D -->|持续活跃| E[保持守桶]
|
||||
D -->|停止活跃| F[衰减降低count]
|
||||
|
||||
F --> G{新竞争者?}
|
||||
G -->|有| H[被替换,让出桶]
|
||||
G -->|无| I[继续衰减直至清理]
|
||||
```
|
||||
|
||||
## 关键参数
|
||||
|
||||
| 参数 | 说明 | 典型值 |
|
||||
|------|------|--------|
|
||||
| **m** | 每层桶的数量 | 2^15 ~ 2^20 |
|
||||
| **L** | 哈希层数 | 3 ~ 5 |
|
||||
| **θ** | 频率阈值 | 0.001 ~ 0.01 |
|
||||
| **α** | 衰减因子 | 0.9 ~ 0.99 |
|
||||
| **Δt** | 衰减周期 | 根据应用场景 |
|
||||
|
||||
## 性能特征
|
||||
|
||||
### 空间复杂度
|
||||
- 空间复杂度: **O(m × L)**
|
||||
- 每桶存储: item (~8字节) + count (~4字节) + error (~4字节)
|
||||
|
||||
### 时间复杂度
|
||||
|
||||
| 操作 | 时间复杂度 | 说明 |
|
||||
|------|-----------|------|
|
||||
| **插入** | O(L) | 对每层进行哈希和更新 |
|
||||
| **查询** | O(L) | 取所有层最小值 |
|
||||
| **衰减** | O(m×L) | 批量处理所有桶 |
|
||||
|
||||
## 优势与局限
|
||||
|
||||
### 优势
|
||||
- **大象流识别准确**: 守桶机制确保高频项持续占据资源
|
||||
- **老鼠流过滤**: 低频项很难干扰大象流统计
|
||||
- **自适应流行度变化**: 衰减机制处理热点漂移
|
||||
- **内存高效**: 恒定空间,不受数据流规模影响
|
||||
|
||||
### 局限性
|
||||
- **参数敏感**: m、L、α 等参数需要根据数据特征调优
|
||||
- **哈希冲突**: 极端情况下可能产生误报或漏报
|
||||
- **衰减延迟**: 热点切换时需要一定时间生效
|
||||
|
||||
## 应用场景
|
||||
|
||||
### 典型用例
|
||||
|
||||
1. **网络流量分析**: 识别高频 IP 地址或端口(大象流)
|
||||
2. **DDoS 防护**: 检测异常高频流量,过滤老鼠流
|
||||
3. **实时推荐**: 发现用户偏好热点,利用衰减实现热点漂移
|
||||
4. **CDN 缓存**: 识别热门内容进行预加载
|
||||
5. **日志分析**: 快速定位高频错误或异常事件
|
||||
|
||||
### 实际部署考虑
|
||||
|
||||
| 场景 | 大象流示例 | 老鼠流示例 | 衰减建议 |
|
||||
|------|-----------|-----------|---------|
|
||||
| 网络带宽监控 | P2P下载、视频流 | 正常网页浏览 | 较慢衰减(α=0.99) |
|
||||
| DDoS检测 | 攻击源IP | 正常用户IP | 快速衰减(α=0.9) |
|
||||
| 搜索热门 | 热门关键词 | 长尾搜索 | 中等衰减(α=0.95) |
|
||||
|
||||
## 参考实现
|
||||
|
||||
### 伪代码
|
||||
|
||||
```python
|
||||
class HeavyKeeper:
|
||||
def __init__(self, m, L, threshold, decay_factor):
|
||||
self.m = m # 每层桶数
|
||||
self.L = L # 哈希层数
|
||||
self.threshold = threshold
|
||||
self.decay_factor = decay_factor # 衰减因子
|
||||
|
||||
# 初始化多层 sketch
|
||||
self.buckets = [[KeeperBucket() for _ in range(m)]
|
||||
for _ in range(L)]
|
||||
|
||||
# 初始化哈希函数
|
||||
self.hash_funcs = [get_hash_func(i) for i in range(L)]
|
||||
|
||||
def insert(self, item, timestamp):
|
||||
for layer in range(self.L):
|
||||
idx = self.hash_funcs[layer](item) % self.m
|
||||
bucket = self.buckets[layer][idx]
|
||||
|
||||
if bucket.item == item:
|
||||
# 守护项匹配,直接计数(大象流强化)
|
||||
bucket.count += 1
|
||||
bucket.last_seen = timestamp
|
||||
else:
|
||||
# 计算替换概率
|
||||
estimated_freq = self._estimate_freq(item)
|
||||
replace_prob = min(1, estimated_freq / bucket.count)
|
||||
|
||||
if random.random() < replace_prob:
|
||||
# 替换为新项(大象流夺权)
|
||||
bucket.item = item
|
||||
bucket.count = 1
|
||||
bucket.error = bucket.count
|
||||
bucket.last_seen = timestamp
|
||||
else:
|
||||
# 不替换,仅增加误差(老鼠流被阻拦)
|
||||
bucket.error += 1
|
||||
|
||||
def apply_decay(self, current_time):
|
||||
"""应用衰减机制"""
|
||||
for layer in range(self.L):
|
||||
for bucket in self.buckets[layer]:
|
||||
elapsed = current_time - bucket.last_seen
|
||||
if elapsed > DECAY_INTERVAL:
|
||||
bucket.count *= self.decay_factor
|
||||
bucket.error *= self.decay_factor
|
||||
|
||||
# 归零清理
|
||||
if bucket.count < 1:
|
||||
bucket.item = None
|
||||
bucket.count = 0
|
||||
bucket.error = 0
|
||||
|
||||
def query(self, item):
|
||||
"""查询Item的频率估计"""
|
||||
min_count = float('inf')
|
||||
for layer in range(self.L):
|
||||
idx = self.hash_funcs[layer](item) % self.m
|
||||
bucket = self.buckets[layer][idx]
|
||||
if bucket.item == item:
|
||||
min_count = min(min_count, bucket.count)
|
||||
|
||||
return min_count if min_count != float('inf') else 0
|
||||
|
||||
def get_top_k(self, k):
|
||||
"""获取Top K大象流"""
|
||||
candidates = {}
|
||||
for layer in range(self.L):
|
||||
for bucket in self.buckets[layer]:
|
||||
if bucket.count >= self.threshold and bucket.item:
|
||||
item = bucket.item
|
||||
candidates[item] = max(candidates.get(item, 0),
|
||||
bucket.count)
|
||||
|
||||
# 返回 Top-k
|
||||
return sorted(candidates.items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True)[:k]
|
||||
```
|
||||
|
||||
## 相关算法对比
|
||||
|
||||
| 算法 | 空间复杂度 | 大象流准确性 | 老鼠流过滤 | 衰减支持 | 适用场景 |
|
||||
|------|-----------|------------|-----------|---------|---------|
|
||||
| **HeavyKeeper** | O(m×L) | 高 | 优秀 | 原生支持 | 高速数据流,需检测热点漂移 |
|
||||
| **Count-Min** | O(m×L) | 中 | 无 | 需额外实现 | 通用频率统计 |
|
||||
| **SpaceSaving** | O(k) | 中 | 好 | 手动实现 | 固定数量Top-K |
|
||||
| **LossyCounter** | O(kε) | 高 | 一般 | 手动实现 | 离线精确统计 |
|
||||
|
||||
## 参考资料
|
||||
|
||||
- HeavyKeeper: Streaming Heavy Hitters Detection with Known Error Bounds (2020)
|
||||
- Count-Min Sketch: An Improved Data Stream Summary
|
||||
- Streaming Algorithms for Finding Heavy Hitters
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[CS/NET/Count-Min-Sketch.md]]
|
||||
- [[CS/DS/HashMap.md]]
|
||||
- [[CS/NET/Network-Flow-Analysis.md]]
|
||||
Reference in New Issue
Block a user