嗯……前陣子接了個活兒,需要做一個基於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位整數的過程):
1 #include "stdio.h" 2 #include "stdlib.h" 3 #include "string.h" 4 5 #define BLACKLIST_FILE "d:\\dummy_ip.bin"//一個512MB大小的隨機內容的二進制文件 6 7 char *test=new char[512*1024*1024];//IP地址映射到本地內存數據的數組 8 int init(); 9 bool checkBlackList(unsigned long inputIP); 10 void setValue(unsigned long inputIP, bool inputValue); 11 12 //全IP段IP黑名單快速查詢 13 14 //原理:IP從0.0.0.0到255.255.255.255,總共2^32個IP地址。每個IP地址只有兩個狀態:在黑名單,或者不在 15 //因此最初設計是申請0x00000000到0xFFFFFFFF個字節的內存空間(4GB),建立全IP地址到內存的映射,每個字節存放一個二進位,存儲該地址對應IP是不是黑名單IP 16 //經過壓縮之后,每個字節存放8個二進制位,因此總空間可壓縮到原先的八分之一,即512MB 17 //查詢時,將IP地址的四個字節合組成一個int32並右移3位,得到該IP對應的字節,然后用這個int32的低三位確定字節里的二進制位,即是否是黑名單IP 18 //將IP轉換成int32之后,單次查詢僅需要1次內存直接訪問和3次位操作 19 //適用於IP黑名單很大的情況 20 21 int main(int argc, char* argv[]) 22 { 23 init(); 24 for(int i=0;i<100;++i) 25 { //生成隨機的IP地址進行查詢 26 unsigned char a=rand()%256,b=rand()%256,c=rand()%256,d=rand()%256; 27 unsigned long ip=a*b*c*d; 28 printf("IP:%u.%u.%u.%u is hit:%d\n",a,b,c,d,checkBlackList(ip)); 29 } 30 return 0; 31 } 32 33 //數據初始化,將保存在本地文件的數據讀取到內存里 34 int init() 35 { 36 FILE* fp = fopen(BLACKLIST_FILE,"r"); 37 if (fp==NULL) 38 return 0; 39 fgets(test,strlen(test),fp); 40 fclose(fp); 41 return 1; 42 } 43 44 //查詢IP是否在黑名單里,僅僅需要三次位運算和一次內存訪問 45 bool checkBlackList(unsigned long inputIP) 46 { 47 return test[inputIP>>3] &(1<<(inputIP & (unsigned long)7)); 48 } 49 50 //設置黑名單IP的值。找到IP對應的字節,然后使用掩碼和位運算設置對應的二進制位的值 51 void setValue(unsigned long inputIP, bool inputValue) 52 { 53 unsigned long byteIndex = inputIP >> 3; 54 char maskByte = (char)(1<<(inputIP & (unsigned long)7)); 55 test[byteIndex] = (inputValue?(test[byteIndex] | maskByte):(test[byteIndex] & (!maskByte))); 56 /*if(inputValue)//喜歡簡潔,改成(:?)形式了,見上行 57 test[byteIndex] = test[byteIndex] | maskByte; 58 else 59 test[byteIndex] = test[byteIndex] & (!maskByte);*/ 60 }
嗯,跑起來可比之前的redis實現快了不是一星半點。