嗯……前阵子接了个活儿,需要做一个基于IP地址黑名单的分流网关。刚接到的时候心想iptables不就行了么,没想到一看客户给的IP黑名单规模……我擦……上亿个……
黑名单到了这个规模,就不得不考虑下优化的问题了。要知道从0.0.0.0到255.255.255.255,IP地址总共也只有232个,约43亿,除去不可能用于实际的地址以及内网地址之后就更少了,只有几分之一。上亿个IP地址的黑名单已经达到实际可用IP地址的几分之一了,而且还要实现大流量高性能查询,实在是……
最初打算是用redis来实现,而第一版程序也是这么实现的,不过可能是不熟悉redis,做出来的东西性能不够达标。
然后我想到一个点子……
技术需求如下:
IP地址由4个字节构成,从0.0.0.0到255.255.255.255,每个IP地址在网关只有两种状态:在/不在黑名单里。上面已经说过,IP地址由4个字节构成。那么我只需要构建一个232规模的字节数组,完成全IP地址到本地内存空间的映射。每个字节只存0或者1,表示对应的IP地址是不是黑名单IP即可。这么做需要4GB的内存。
进一步考虑,一个字节只存一个bool实在是太浪费,对这种映射关系稍做修改,每个IP地址对应到字节数组里一个字节的某个二进制位即可,这样就能把数据压缩到原先的八分之一,即512MB。这样32位的系统也可以胜任了(虽然提供的是64位系统)。
使用数据库技术,绕不过去的是查询和排序之类耗时的工作,对于如此庞大的黑名单库(其规模已经达到全IP地址数的几分之一),这是主要的性能消耗。但如果将IP地址映射到本地内存空间,那么直接就省掉了这个最消耗性能的操作,直接就能查到这个IP在不在黑名单里了。
好了,想法有了,实现起来也就是五分钟的事儿,我编写的版本如下(C实现,省略了由IP地址字符串转换成无符号32位整数的过程):
#include "stdio.h"
#include "stdlib.h"
#include "string.h" #define BLACKLIST_FILE "d:\\dummy_ip.bin"//一个512MB大小的随机内容的二进制文件 char *test=new char[**];//IP地址映射到本地内存数据的数组
int init();
bool checkBlackList(unsigned long inputIP);
void setValue(unsigned long inputIP, bool inputValue); //全IP段IP黑名单快速查询 //原理:IP从0.0.0.0到255.255.255.255,总共2^32个IP地址。每个IP地址只有两个状态:在黑名单,或者不在
//因此最初设计是申请0x00000000到0xFFFFFFFF个字节的内存空间(4GB),建立全IP地址到内存的映射,每个字节存放一个二进位,存储该地址对应IP是不是黑名单IP
//经过压缩之后,每个字节存放8个二进制位,因此总空间可压缩到原先的八分之一,即512MB
//查询时,将IP地址的四个字节合组成一个int32并右移3位,得到该IP对应的字节,然后用这个int32的低三位确定字节里的二进制位,即是否是黑名单IP
//将IP转换成int32之后,单次查询仅需要1次内存直接访问和3次位操作
//适用于IP黑名单很大的情况 int main(int argc, char* argv[])
{
init();
for(int i=;i<;++i)
{ //生成随机的IP地址进行查询
unsigned char a=rand()%,b=rand()%,c=rand()%,d=rand()%;
unsigned long ip=a*b*c*d;
printf("IP:%u.%u.%u.%u is hit:%d\n",a,b,c,d,checkBlackList(ip));
}
return ;
} //数据初始化,将保存在本地文件的数据读取到内存里
int init()
{
FILE* fp = fopen(BLACKLIST_FILE,"r");
if (fp==NULL)
return ;
fgets(test,strlen(test),fp);
fclose(fp);
return ;
} //查询IP是否在黑名单里,仅仅需要三次位运算和一次内存访问
bool checkBlackList(unsigned long inputIP)
{
return test[inputIP>>] &(<<(inputIP & (unsigned long)));
} //设置黑名单IP的值。找到IP对应的字节,然后使用掩码和位运算设置对应的二进制位的值
void setValue(unsigned long inputIP, bool inputValue)
{
unsigned long byteIndex = inputIP >> ;
char maskByte = (char)(<<(inputIP & (unsigned long)));
test[byteIndex] = (inputValue?(test[byteIndex] | maskByte):(test[byteIndex] & (!maskByte)));
/*if(inputValue)//喜欢简洁,改成(:?)形式了,见上行
test[byteIndex] = test[byteIndex] | maskByte;
else
test[byteIndex] = test[byteIndex] & (!maskByte);*/
}
嗯,跑起来可比之前的redis实现快了不是一星半点。