惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
腾讯CDC
WordPress大学
WordPress大学
爱范儿
爱范儿
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
Latest news
Latest news
Last Week in AI
Last Week in AI
V2EX - 技术
V2EX - 技术
I
InfoQ
N
News | PayPal Newsroom
SecWiki News
SecWiki News
Microsoft Azure Blog
Microsoft Azure Blog
美团技术团队
T
Troy Hunt's Blog
H
Hacker News: Front Page
S
SegmentFault 最新的问题
TaoSecurity Blog
TaoSecurity Blog
V
Visual Studio Blog
Martin Fowler
Martin Fowler
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园_首页
S
Security @ Cisco Blogs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
小众软件
小众软件
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tor Project blog
L
LINUX DO - 热门话题
月光博客
月光博客
S
Schneier on Security
Y
Y Combinator Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Proofpoint News Feed
Forbes - Security
Forbes - Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
A
Arctic Wolf
Webroot Blog
Webroot Blog
博客园 - 叶小钗
F
Fortinet All Blogs
S
Securelist
AI
AI
B
Blog RSS Feed
Security Latest
Security Latest

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
布隆过滤器
2023-06-20 · via 蛮荆

布隆过滤器

2023-06-20 算法

基本概念

布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出,是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 – 维基百科

查找问题建模

如何判断某个元素是否存在于一个集合中

比较直观的做法是将所有元素保存起来,然后逐个比较确认结果,建模转换为具体的数据结构:

  • 线性结构: 数组、链表等,时间复杂度为 O(N), 空间复杂度为 O(N)
  • 散列表结构: Map、HashTable 等, 时间复杂度为 O(1), 空间复杂度为 O(N)
  • 树形结构: AVL、RBTree 等, 时间复杂度为 O(logN), 空间复杂度为 O(N)

常见的数据结构随着元素的增加,存储空间也会越来越大,同时查询速度也会越来越慢 (散列表结构不受影响),如果我们希望在保持时间复杂度为 O(N) 的前提下, 尽可能地降低内存占用,就需要转换一下思路:保存特征数据,而非所有数据。什么是特征数据? 简单来说,就是能够唯一标识出其对应的源数据, 比如常见的 用户 ID游戏中奖 ID管理后台审批流 ID 等。

原理和应用场景

布隆过滤器的原理: 当一个元素被加入集合时,通过 K 个哈希函数 将元素映射到一个位图数据结构 (例如 bitmap) 中的 K 个位置上面,并将这些位置全部置为 1。 判断元素是否存在于集合时,只需查看 K 个哈希函数 映射后的 K 个位置上面的值即可。

如果这些位置任意一个为 0,则表示被检测元素一定不存在于集合中,如果都是 1,则表示被检测元素有较大可能存在于集合中

为什么不能检测 “一定存在” ?

检测元素时可以返回 “一定不存在” 和 “可能存在”,因为可能有多个元素映射到相同的 bit 上面,导致该位置为 1, 那么一个不存在的元素也可能会被误判为存在, 所以无法提供 “一定存在” 的语义保证。

为什么元素不允许被 “删除” ?

如果删除了某个元素,导致对应的 bit 位置为 0, 但是可能有多个元素映射到相同的 bit 上面,那么一个存在的元素会被误判为不存在 (这是不可接收的), 所以为了 “一定不存在” 的语义保证,元素不允许被删除。

PS: 虽然可以引入一个 删除计数器 来解决上述问题,不过这需要引入额外的空间,失去了使用过滤器的意义。

两个语义操作

  • Add : 添加一个元素到集合中
  • Test : 检查给定元素是否存在于集合中

示例

URL 去重

假设下面的 URL 列表为采集队列 (一共 100 条数据),我们希望采集完成的 URL 可以记录起来,避免多次采集,如果使用正常的字符串存储的话 (以 Go 语言为例,string 占用 16 bytes), 大概需要 16 bytes * 100 = 1600 bytes 内存空间。

https://www.example.com/u/101/profile
https://www.example.com/u/102/profile

...

https://www.example.com/u/200/profile

而如果我们改为 布隆过滤器 的方式来记录的话,假设哈希函数可以将每个链接转换为 1 个 0-255 的数字,通过将数据存入一个 bitmap, 那么理想情况下 (不发生任何碰撞) 最终只需要 1 个 byte 的内存空间即可,内存使用直接降低了 1600 倍

# 布隆过滤器记录 URL 只需要 1 个 byte

------------------------------------
| 0   | 1   | 2   | 3  | ... | 255 | 
------------------------------------
| URL | URL | URL |    |     | URL |
------------------------------------

网站用户性别

假设网站的用户数量为 1000万,每个用户性别存储需要 1 个 bytes, 如果正常存储的话,大概需要 1 byte * 1000 W ≈ 10 MB 内存空间。

而如果我们改为 布隆过滤器 的方式来记录的话,假设用户 ID 是一个数字,通过将数据存入一个 bitmap, 那么 1 byte 可以表示 256 个 用户性别, 理想情况下 (不发生任何碰撞) 最终只需要 1000 W / 256 * 1 byte ≈ 0.04 MB 内存空间,内存使用直接降低了 256 倍

# 布隆过滤器记录用户网站性别

--------------------------------------------------
| 0     | 1     | 2     | 3     | ...  | 9999999 | 
--------------------------------------------------
| User1 | User2 | User3 | User4 | ...  | User... |
--------------------------------------------------

其他应用场景

  • 垃圾邮件过滤
  • 防止缓存穿透
  • 业务记录计数器
  • Web 拦截

两个核心支撑点

通过上面两个简单的应用场景可以看到,要实现一个健壮的 布隆过滤器,需要规划设计好两个技术点:

  • 元素长度和用于存储哈希映射值的数据结构,一般使用 bitmap (具体的长度和存储规模有关)
  • 显著降低碰撞、性能优良的哈希函数

Murmur3FNV 系列和 Jenkins 等非密码学算法适合作为哈希函数,其中 Murmur3 因为易于实现,并且在速度和随机分布上有很好的权衡, 实践中多选用其作为 布隆过滤器 的哈希函数。

数学部分

一个 布隆过滤器 至少需要包含如下参数:

  • 哈希空间大小,记为 m
  • 元素集合大小,记为 n
  • 哈希函数个数,记为 k
  • 误判概率,记为 p (可能出现一个元素不在集合中,但是被误判为存在于集合中,这个误判的概率取值范围为 0 - 1)

哈希函数个数满足以下公式时,布隆过滤器 有最好的效果 (数学证明部分省略,数学渣暴露了 :( … )

k = [(ln2)(m/n)]

还是以刚才的 网站用户性别 为例,1000W 用户需要以不高于 0.00001% 误判率的的情况下被检测到,需要多少个哈希函数?需要耗费多少空间?

# 带入公式计算

n = ceil(m / (-k / log(1 - exp(log(p) / k))))
p = pow(1 - exp(-k / (m / n)), k)
m = ceil((n * log(p)) / log(1 / pow(2, log(2))))
k = round((m / n) * log(2))

记不住数学公式?没关系,已经有大佬做好了一个 在线工具,可以通过输入参数直接生成对应的哈希函数个数。

Bloom Filter Calculator

开源库

笔者选择了 bits-and-blooms/bloom 作为研究 布隆过滤器 代码实现,版本为 v3.3.1

安装组件

$ go get -u github.com/bits-and-blooms/bloom/v3

示例

初始化过滤器时,需要知道对应的业务场景有多少元素(期望的容量),以及期望的误判概率。常见的误判率为 1%, 误报率越低,需要的内存就越多, 同时容量越大,需要的内存就越多。

初始化过滤器时,应该尽可能明确需要的元素数量,因为 布隆过滤器 不是动态数据结构,如果指定的元素数量太少,则可能会超出误判概率范围

package main

import "github.com/bits-and-blooms/bloom"

func main() {
	// 初始化能够接收 100 万个元素且误判率为 1% 的布隆过滤器
	filter := bloom.NewWithEstimates(1000000, 0.01)

	hw := []byte(`hello world`)
	hg := []byte(`hello golang`)

	filter.Add(hw)

	println(filter.Test(hw)) // true
	println(filter.Test(hg)) // false
}

我们只添加了 hello world 字符串到过滤器中,所以正常的输出应该是 true false:

$ go run main.go

true
false

源码分析

接下来,我们来简单分析下 bits-and-blooms/bloom 的实现代码。

哈希算法选择

module github.com/bits-and-blooms/bloom/v3

go 1.14

require (
	github.com/bits-and-blooms/bitset v1.3.1
	github.com/twmb/murmur3 v1.1.6
)

bits-and-blooms/bloom 的依赖包中,可以看到其选择了 murmur3 算法作为哈希函数实现。

BloomFilter

BloomFilter 结构体表示一个 布隆过滤器,包含 3 个字段,分别对应上文提到的 布隆过滤器 的必要参数。

type BloomFilter struct {
	m uint              // 哈希空间大小
	k uint              // 哈希函数个数
	b *bitset.BitSet    // bitmap
}

初始化过滤器

NewWithEstimates 方法创建一个 布隆过滤器,内部调用了 EstimateParameters 方法来计算 mk 的值, 最后通过 mk 初始化一个 BloomFilter 结构体并返回。

func NewWithEstimates(n uint, fp float64) *BloomFilter {
	m, k := EstimateParameters(n, fp)
	return New(m, k)
}

func EstimateParameters(n uint, p float64) (m uint, k uint) {
    m = uint(math.Ceil(-1 * float64(n) * math.Log(p) / math.Pow(math.Log(2), 2)))
    k = uint(math.Ceil(math.Log(2) * float64(m) / float64(n)))
    return
}

添加元素

BloomFilter.Add 方法添加一个元素到 布隆过滤器,返回当前的 BloomFilter 对象来支持链式调用。

func (f *BloomFilter) Add(data []byte) *BloomFilter {
	h := baseHashes(data)
	for i := uint(0); i < f.k; i++ {
		f.b.Set(f.location(h, i))
	}
	return f
}

baseHashes 函数返回哈希函数计算出的哈希值,其内部调用了 murmur3 算法。

func baseHashes(data []byte) [4]uint64 {
    ...
}

location 函数返回哈希值对应的 bitmap 的位置索引,通过将 bitmap 对应的位置进行设置,就可以将元素添加到 布隆过滤器 了。

func location(h [4]uint64, i uint) uint64 {
	...
}

检测元素

BloomFilter.Test 方法检测一个元素是否存在于 布隆过滤器 中,可以看到,其内部实现就是 BloomFilter.Add 方法的逆过程。

func (f *BloomFilter) Test(data []byte) bool {
	h := baseHashes(data)
	for i := uint(0); i < f.k; i++ {
		if !f.b.Test(f.location(h, i)) {
			return false
		}
	}
	return true
}

调用流程图

调用流程图

小结

本文简述了 布隆过滤器 的基本概念和原理,并通过开源库 bits-and-blooms/bloom 的实现代码分析了实现 布隆过滤器 的核心技术点, 最后引用维基百科关于 布隆过滤器 的优缺点作为文章结尾。

优点

布隆过滤器 在算法的空间复杂度和时间复杂度方面都有巨大的优势,AddTest 操作的时间复杂度都是常数 O(K) (K: 哈希函数个数)。 由于哈希函数相互之间没有关联,因此可以通过硬件并行实现计算加速,此外,由于 布隆过滤器 不存储源数据,在某些对数据保密要求非常严格的场景 (数据可用,但不可见) 有明显优势。 最后,在哈希空间向量长度相同的情况下,使用同一组哈希函数的两个 布隆过滤器集合相关运算可以使用位操作进行。

缺点

布隆过滤器 最明显的的缺点就是 误判率,随着元素数量增加,误判率 也随之增加,所以如果元素数量很少的情况下,使用 map 就可以了。 此外,布隆过滤器 不支持删除元素, 因为 布隆过滤器 只能确定元素一定不存在于集合中,无法确定元素一定存在于集合中,如果一定要删除某个元素, 则必须重新构建整个 布隆过滤器

Reference