从bitmap到布隆过滤器,再到高并发缓存设计策略

从bitmap到布隆过滤器,再到高并发缓存设计策略

前言:怎么能把风马牛不相及的概念串在一块,就得看笔者的本事了。

 bitmap和布隆过滤器

海量整数中是否存在某个值--bitmap

在一个程序中,经常有让我们判断一个集合中是否存在某个数的case;大多数情况下,只需要用map或是list这样简单的数据结构,如果使用的是高级语言,还能乘上快车调用几个封装好的api,加几个if else,两三行代码就可以在控制台看自己“完美”而又“健壮”的代码跑起来了。

但是,事无完美,在高并发环境下,所有的case都会极端化,如果这是一个十分庞大的集合(给这个庞大一个具体的值吧,一个亿),简单的一个hash map,不考虑链表所需的指针内存空间,一亿个int类型的整数,就需要380多M(4byte × 10 ^8),十亿的话就是4个G,不考虑性能,光算算这内存开销,即使现在满地都是128G的服务器,也不好吃下这一壶。

bitmap则使用位数代表数的大小,bit中存储的0或者1来标识该整数是否存在,具体模型如下:

这是一个能标识0-9的“bitmap”,其中4321这四个数存在

计算一下bitmap的内存开销,如果是1亿以内的数据查找,我们只需要1亿个bit = 12MB左右的内存空间,就可以完成海量数据查找了,是不是极其诱人的一个内存缩减,以下为Java实现的bitmap代码:


public class MyBitMap {
 
    private byte[] bytes;
    private int initSize;
 
    public MyBitMap(int size) {
        if (size <= 0) {
            return;
        }
        initSize = size / (8) + 1;
        bytes = new byte[initSize];
    }
 
    public void set(int number) {
        //相当于对一个数字进行右移动3位,相当于除以8
        int index = number >> 3;
        //相当于 number % 8 获取到byte[index]的位置
        int position = number & 0x07;
        //进行|或运算  参加运算的两个对象只要有一个为1,其值为1。
        bytes[index] |= 1 << position;
    }
 
 
    public boolean contain(int number) {
        int index = number >> 3;
        int position = number & 0x07;
        return (bytes[index] & (1 << position)) != 0;
    }
 
    public static void main(String[] args) {
        MyBitMap myBitMap = new MyBitMap(32);
        myBitMap.set(30);
        myBitMap.set(13);
        myBitMap.set(24);
        System.out.println(myBitMap.contain(2));
    }
 
}

使用简单的byte数组和位运算,就能做到时间与空间的完美均衡,是不是美美哒,wrong!试想一下,如果我们明确这是一个一亿以内,但是数量级只有10的集合,我们使用bitmap,同样需要开销12M的数据,如果是10亿以内的数据,开销就会涨到120M,bitmap的空间开销永远是和他的数据取值范围挂钩的,只有在海量数据下,他才能够大显身手。

再说说刚刚提到的那个极端case,假设这个数据量在一千万,但是取值范围好死不死就在十个亿以内,那我们不可避免还是要面对120M的开销,有方法应对么?

布隆过滤器

如果面对笔者说的以上问题,我们结合一下常规的解决方案,譬如说hash一下,我将十亿以内的某个数据,hash成一亿内的某个值,再去bitmap中查怎么样,如下图,布隆过滤器就是这么干的:

利用多个hash算法得到的值,减小hash碰撞的概率

像上面的图注所说,我们可以利用多个hash算法减小碰撞概率,但只要存在碰撞,就一定会有错误判断,我们无法百分百确定一个值是否真的存在,但是hash算法的魅力在于,我不能确定你是否存在,但是我可以确定你是否真的不存在,这也就是以上的实现为什么称之“过滤器”的原因了。

高并发缓存设计策略

why cache??

如果读者是一个计算机专业的同学,cache这个词应该是能达到让耳朵起茧的出现频次。在计算机体系中,cache是介于cpu以及内存之间,用来缓和cpu和内存处理速度差距的那么一个和事佬;在OS中,page cache又是内存和IO之间的和事佬。

cache是个和事老??听着似乎怪怪的,但是也蛮形象的啦。

前面讲了大半截的算法理论,为了防止读者犯困,直接进入下半部分主题,高并发缓存设计。

即使是在软件层,我们同样需要这么一个和事老,从最简单的服务架构开始,通常我们在服务端发起请求,然后CURD某个关系型数据库例如Mysql。但是,类似这样的架构都需要有一个磁盘作为终端持久化,即使增加索引,使用B+树的这种数据结构进行优化查询,效率还是会卡在需要频繁寻道的IO上。这个时候,一个和事老的作用就十分明显了,我们会添加一些内存操作,来缓和IO处理速度慢带来的压力。cache is not a problem,how to use it is actually a problem。

缓存一致性问题

缓存处理的机制有以下几种:

  1. cache aside;

  2. read through;

  3. write through;

  4. write behind caching;

缓存穿透问题

所谓的缓存击穿,就是当请求发出,而无法在缓存中读到数据时,请求还是会作用到database,这样的话,缓存减压的效果就不复存在了。

设想这么一个场景,如果一个用户,使用大流量恶意频繁地去查询一条数据库中没有的记录,一直击穿缓存,势必会把database打死,如何避免缓存击穿,这就是一个问题了。

有两种方案,第一种,在缓存中添加空值,如果在database中查询无果,我们大可以把值设置为null,防止下次再次访问数据库,这样做简单便捷,但是多少有些浪费空间。

第二种方案,就是使用布隆过滤器(点题),在cache与web服务器中间加一层布隆过滤器,对访问的key做记录,如此以来,同样可以解决缓存击穿的问题。

缓存雪崩问题

缓存雪崩发生于在某个时间点,缓存同时失效,例如缓存设置了失效时间,这会联动的导致大量缓存击穿问题。

加分布式锁是一种解决方案,只有拿到锁的请求才能访问database。但是这样治标不治本,当请求量过多时,大量的线程阻塞,也会把内存撑坏的。

预热数据,分散地设置失效时间,这样可以减少缓存雪崩发生的概率。

提高缓存可用性,cache的单点一样是会是缓存雪崩的隐患,大部分缓存中间件都提供高可用架构,如redis的主从+哨兵架构。

 

<think>好的,我需要回答用户关于布隆过滤器在操作系统内核中的应用与实现的问题。首先,我要回忆布隆过滤器的基本概念,它是一个概率型数据结构,用于判断元素是否在集合中,可能会有误判但不会漏判。接下来,我需要查找操作系统内核中哪些地方可能用到布隆过滤器。根据用户提供的引用,特别是引用[5]提到I/O调度层管理块设备的请求队列,合并请求,可能用布隆过滤器来快速判断请求是否已存在,避免重复处理。 另外,网络子系统中的包过滤或连接跟踪也可能应用布隆过滤器,例如快速过滤恶意IP或端口。内存管理方面,如页面缓存或slab分配器中,判断某些页面是否被缓存,减少扫描时间。文件系统中,快速判断文件路径或inode是否存在,加速元数据查询。 实现方面,内核中的布隆过滤器需要考虑内存分配位操作的高效性。可能需要使用内核提供的位图API,如bitmap_setbitmap_test。同时,动态扩展的布隆过滤器可能需要多层结构,但内核资源有限,需权衡内存性能。误判率控制也很重要,特别是在关键路径上,高误判可能影响系统稳定性。 需要注意的是,虽然布隆过滤器在内核中有潜在应用,但实际实现可能因内核版本子系统而异。用户提供的引用中没有直接提到内核中的布隆过滤器,但可以推测其应用场景。例如引用[3]提到过滤器过短导致失效的问题,内核实现时需合理设计哈希函数数量位数组大小。引用[5]的I/O调度层合并请求可能与布隆过滤器结合,优化请求处理。 最后,要确保回答结构清晰,分点说明应用场景实现要点,并附上相关问题。引用相关文献时正确标注,如引用[5]关于I/O调度层的内容。可能还需要补充内核中实际使用布隆过滤器的例子,如某些文件系统或网络模块的具体实现,但若缺乏具体信息,需说明可能的应用方向而非具体实例。</think>### 布隆过滤器在操作系统内核中的应用与实现 #### 1. **应用场景** 布隆过滤器在内核中主要用于**快速判断数据是否存在**,以减少不必要的计算或I/O开销,具体应用包括: - **I/O请求合并与缓存管理** 在块设备I/O调度层(如Linux内核的`elevator`调度器),布隆过滤器可快速判断某个请求是否已被缓存或合并。例如,通过记录已处理的磁盘扇区范围,避免重复处理相同请求,从而优化I/O性能[^5]。 - **网络数据包过滤** 在网络子系统中(如Netfilter框架),布隆过滤器可用于快速过滤恶意IP地址或端口。例如,内核模块可能用其判断某个IP是否在黑名单中,减少遍历哈希表或链表的开销。 - **内存管理优化** 在内存页管理(如页面缓存或slab分配器)中,布隆过滤器可快速判断某个页是否已被缓存,减少无效的页表遍历操作。 - **文件系统元数据查询** 文件系统(如ext4或Btrfs)可能用布隆过滤器加速元数据查询,例如判断某个文件路径的inode是否已被加载到内存中。 --- #### 2. **实现要点** 在内核中实现布隆过滤器需考虑以下关键问题: - **内存分配与位操作** 内核中通常使用`bitmap`结构表示布隆过滤器的位数组,通过`kzalloc`等函数动态分配内存。位操作通过内核提供的`set_bit()``test_bit()`等原子操作实现。 - **哈希函数设计** 内核需选择高效且分布均匀的哈希函数。例如,可能组合使用`jhash`(Jenkins哈希)`crc32`等轻量级哈希函数。 - **动态扩展与误判率控制** 若需动态扩展容量,可参考**可扩展布隆过滤器**(Scalable Bloom Filter)的设计,但内核中更倾向静态预分配以降低复杂度。误判率需通过调整位数组大小哈希函数数量控制[^1]。 --- #### 3. **示例代码(伪代码)** 以下是一个简化的内核模块中布隆过滤器的实现框架: ```c #include <linux/bitmap.h> #define BLOOM_SIZE 8192 // 位数组大小 static unsigned long bloom_filter[BITS_TO_LONGS(BLOOM_SIZE)]; // 添加元素 void bloom_add(const void *data, size_t len) { u32 hash1 = jhash(data, len, 0) % BLOOM_SIZE; u32 hash2 = crc32_le(0, data, len) % BLOOM_SIZE; set_bit(hash1, bloom_filter); set_bit(hash2, bloom_filter); } // 判断元素是否存在 bool bloom_test(const void *data, size_t len) { u32 hash1 = jhash(data, len, 0) % BLOOM_SIZE; u32 hash2 = crc32_le(0, data, len) % BLOOM_SIZE; return test_bit(hash1, bloom_filter) && test_bit(hash2, bloom_filter); } ``` --- #### 4. **挑战与限制** - **内存限制**:内核内存有限,需权衡位数组大小与误判率[^3]。 - **并发安全**:需使用原子操作或锁机制保证多线程安全。 - **误判对系统的影响**:若用于关键路径(如安全检测),需结合其他机制(如二次确认)降低风险。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值