解析純真IP地址庫


一周以來,一直在做 IP地址庫的解析。從調研到編碼到優化,大概花了有七八天的時間。感覺很好玩。總結一下整個做的過程。

1、關於IP 地址庫的解析方式

目前主要的解析方式有兩種:通過API,或通過IP數據庫。

API方式很簡單,目前國內大廠不少提供API接口,只要發送請求的IP,就能獲得相應的地理位置。像BAT等等公司都提供IP查詢接口。這種解析方式的好處在於,編碼簡單,一個請求獲得數據,然后解析一下就好了(通常只是個json數據),而且不用維護數據庫,對本地沒有負擔。但是缺點也挺明顯的,首先是慢,發送網絡請求一秒鍾發不了幾條,其次是有限制,比如百度限制每秒鍾 250條請求,防止並發量太大造成網絡阻塞,再次要受制於人,什么都要聽人家的,萬一今天地址換了,明天接口數據格式改了,后天要收費了……哦賣糕的。

IP數據庫方式相對來講復雜一點,需要有完善的數據庫,還要建立相應的查詢服務。優缺點則跟API方式正好相反:優點是查詢快,不受網絡和網站的限制,缺點是編碼相對復雜,而且要一直維護數據庫。數據庫國內最著名的是純真網絡,ipip,國外更加著名的GeoIP等等。

我們在權衡利弊之后,決定采取數據庫方式。聽說GeoIP對國外ip 數據很完善,但是對於國內的ip還是不太全的。因此,我們初步選用純真IP數據庫來解析。

2、存儲
下載下來純真數據庫的過程就不介紹了,我也沒有閑心去解析dat,就直接解壓成txt來做了。數據一共不到45萬條。

先普及個常識,那就是IP地址實際上是一個unsigned int值。在群里詢問做法的時候我發現很多人居然都不知道這一點。我們看到的IP地址,是4個0~255之間的數,而實際上在計算機中IP地址的表示是32位二進制。 01010101.10101010.00110011.11001100醬嬸的。32位二進制,當然就是一個unsigned int的取值范圍。IP解析也是一樣,把IP轉化成int 進行存儲和查詢,是最節省空間、最效率的方法。

書歸正文,解壓出來的IP地址庫是醬嬸的:

(純真IP數據庫,可以到http://www.cz88.net去下載。這個是公開的。我相信我要不說的話一定有人不知道。)

三個字段,start, end, address,從start到end之間的IP都是address這個位置。然而仔細觀察可以發現,end其實並沒有什么卵用。因為end跟下一個start是連接的,中間沒有斷開的ip。所以只需要記錄start: address就好了,所以——我們得到了一個鍵值對。啊哈,關於鍵值對我們能用的武器就多了,最典型的可以用redis這樣的數據庫或者直接用字典。

那么查詢怎么查?最簡單的方法,提取keys,順序排列,二分查找。45萬條數據最多19次比較即可。注意這里我們要找的是“小於等於給定IP的最大值”。

什么?你說每一條ip對應一個address ,把所有ip的address寫成一個列表?唔……也不是不可以,不過首先你的服務器得有200G內存。沒錯,200G。內存。

3、算法演進

3.1、首先考慮redis

我需要保持程序一直運行,即需要一個server,里面是保存好的地址結構,當我需要查詢一條ip的時候,只需要發送一條請求即可。那么,如果使字典保持在內存里,就必須要程序一直運行,需要我寫一個server。tcp還是udp還是http的無所謂。然而,我懶。所以首先考慮redis。畢竟人家存儲結構都寫好了,我都不用動腦筋,往里存就好了。

然而事實證明我想錯了。

3.1.1、普通鍵值對

先用最簡單的方法,set ip addr,全部存進去;然后查詢的時候讀取keys,類型轉換,排序,二分,查到小於等於給定ip的最大值,行雲流水的一套下來——3.2秒。泥煤這速度還不如直接發api請求呢!仔細想想,自己確實是犯二了,40多萬數類型轉換再排序,能快就見鬼了。

3.1.2、有序集合

考慮下一方案,一要存整數,二要有序。存整數是不可能了,在網上查到redis中的數據類型,根本沒有數字相關的,只有字符串和各種序列類型。經人介紹選定有序集合。zadd ip2addr ip addr添加好。然而查詢時候始終有錯誤。莫名其妙了好久,終於查到原因了:假設一條ip1對應地址是addr ,過不久一條ip2對應的地址也是addr,那么ip2就會把ip1覆蓋掉。這不科學啊!唉,只能拋棄有序集合了。

(其實后來有大神查到還是可以用的,如果相同的addr會覆蓋,那就人為的讓它不同,例如可以存儲addr@ip這樣的形式。當時着急了,也沒多想想。)

3.1.3、列表

讓ip有序,最合適的還是列表。於是在redis里面我建立了兩個列表,一個是ip,另一個是對應位置的addr。查詢時候先獲得ip列表,查到給定ip,用這個ip的索引去查找對應位置的addr。願望是美好的,現實是殘酷的。由於redis中的列表采用的是雙向鏈表,要獲取全部40多萬數據也是夠慢,這就造成了結果查詢一條數據要360ms。而且有個看似奇怪的特性:ip值較小的,比如1.2.3.4,查詢結果就4ms,而ip值大到222.222.222.222這樣的就要接近400ms了。

這仍然是一個慢到不能忍的結果。

3.1.4、列表+分塊

列表做出來的結果大概在300ms多,還是太慢,我在redis里面大概掃了一遍,沒有什么更合適的數據結構了。那么就只能進行算法層面的優化了。觀察了一下ip地址的結構,前22萬條數據應該包含了前面一半ip,剩下的ip在后一半數據里,試了試從ip列表里只提取一半數據進行查詢,果然時間也縮到了一半,大概170ms。那么,能否更精確的定位ip所在的位置?

想象一下42億個ip,分散在44萬條數據中,每塊里面有多少個ip?肯定不是平均分布的,但是數量是可以統計的。我把int范圍切分,每10^7作為一個塊,那么42億多的int數可以切分出來430塊(例如ip值小於10^7放在第0區,小於2*10^7大於10^7放在第1區,等等),這樣就統計出來每個塊中ip的數量。下一步進行累加,算出前0塊共有多少個ip,前1塊有多少ip,前2塊共有多少ip……舉個栗子,統計ip數量的列表為[a1,a2,a3,a4...],那么累加的列表為[a1,a1+a2, a1+a2+a3, a1+a2+a3+a4...]。這個列表即ip索引。這樣可以精確的定位ip。查詢的時候,先計算ip屬於哪個塊,然后找到對應的索引,最后通過索引來找到對應的ip范圍。雖然多查詢了一次,但是極大地縮減了從redis中取數的數量。經測試,速度已經達到了65ms左右。

然而這個算法有兩個問題:首先是塊大小的設置,需要人為干預,塊大小涉及到每個塊里的ip數量,還涉及到塊的數量,也就是索引列表的大小。這個完全靠經驗,沒有什么理論。另一個問題是塊中ip數量為0的情況。還用剛才的栗子,有個ip列表[a1,0,0,0,a3,a4...],索引列表為[a1,a1,a1,a1,a1+a3, a1+a3+a4...] ,也就是一個ip在a1范圍內,而下一條ip已經在a3范圍內了。現在我查詢一條ip,本應查詢范圍是[a1, a1+a3],而現在查詢的范圍變成了[a1, a1],這樣必然結果錯誤。我也沒有太好的辦法解決,現在想到的只能是再記錄一下ip數量表,現查詢一下ip所在塊是不是0,如果是,就去找到在這之前第一個不為0的塊。這樣性能肯定是要下降的。

3.2、內存

3.2.1、有序字典

碰到問題后,詢問了一下q群里的大神們。幾個做過的人都是自己寫服務的。唉,本想偷懶,折騰了一圈反倒把自己坑了。於是自己寫socket來做。存儲結構為了保持整數和有序,使用OrderedDict來保存。像以前那樣拿到keys,二分,查詢,看眼時間,哭了,怎么還是50ms?

3.2.2、字典+列表

繼續請教大神們,怎么做的,得到的答案是用列表。恍然大悟。用dict.iteritems()這種形式的列表,既能保持字典的鍵值對形狀,又是有序的。OrderedDict內部使用雙向鏈表,當然怎么算都是列表更快。在原來的基礎上簡單的改了改,重新測一下,1ms。1ms?!對比了一下ip庫,似乎結果並沒有錯。

 

那好吧,就到這里了,這個問題就可以暫時告一段落了。從個人來講我覺得最有趣的是中間redis列表+分塊那個算法,不能應用實在可惜,因為在后面的算法中,主要瓶頸在於socket的傳輸速度,而不是列表數據的多少,單純查詢過程的速度已經達到了10^-5s的級別。遺留了幾個小問題吧,知道思路就好,反正也沒法用到最優解中去。

代碼:https://github.com/anpengapple/iplocate


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM