首頁>科技>

在移動網際網路的應用中,經常需要根據使用者的位置資訊等做一些使用者側資訊的統計分析。而要拿到使用者的位置資訊,一般有兩個方法: GPS 定位的資訊和使用者 IP 地址。由於每個手機都不一定會開啟 GPS,而且有時並不太需要太精確的位置(到城市這個級別即可),所以根據 IP 地址入手來分析使用者位置是個不錯的選擇。 要做到這個功能得需要一個 IP 和地理位置的對映關係庫,並依賴這個庫啟動一個 IP 轉地理位置的服務。本文從需求入手,結合 Github 上擁有 8.4k 星的 ip2region 來分析對映關係庫的設計以及 IP 如何快速轉換成地理位置。

介紹

IP 定位服務很常見,而且很多公司都提供了類似的付費服務,比如阿里,高德,百度等,當然也有公開的免費服務,像 GeoIP,純真IP等。這些服務要麼透過 HTML 頁面解析,要麼透過介面請求,但不管怎樣都離不開一次 http 請求,更不用說大部分服務都對 QPS 作了限制。下表枚舉了一些常見的透過 IP 獲取地址的方式。

在日常工作通常需要將大量使用者請求日誌中 IP 轉換成使用者位置資訊,用作後續的分析。這其中的關鍵是資料量大,處理要快。顯然每次都透過請求 API 公共服務的方式無法滿足我們的日常需求。

暴力生成 IP 庫

對於日常的需求,一種簡單粗暴的做法就是提前透過 API 獲取所有公網 IP 對應的位置資訊,按照下面的 TIPS 中我們可以估算出如果透過訪問淘寶IP地址庫來遍歷 3.3 億的國內 IP 地址要 10 年。如果是高德的企業使用者遍歷國內 IP 地址大概要 11 天。感覺這個 11 天還是能夠接受的。

TIPS: IPv4目前所說的 IP 地址是指 IPv4,它是使用 32 位(4 位元組)地址,因此地址空間約有 42.9 億$2^32=4294967296$個, 不過一些地址是作為特殊用途所保留的,如專用網路(約 1800 萬個)和多播地址(約 2.7 億個),這些減少了可在網際網路上路由的地址數量。 據 wiki 上統計,中國的 IPv4 數量達到 3.3 億,而美國有 15.4 億個。

這裡我們約定一下位置資訊的資料格式: 國家|區域|省份|城市|ISP,如果介面中返回的欄位沒有對應的資訊,則對應的欄位填充0。那麼我們透過順序請求 API 服務可以獲取到如下檔案資料(地址依次遞增):

0.0.0.0|0|0|0|內網IP|內網IP0.0.0.1|0|0|0|內網IP|內網IP...1.0.15.255|中國|0|廣東省|廣州市|電信...255.255.255.255|0|0|0|內網IP|內網IP

只要有了這個檔案,可以將其讀到記憶體中,使用字典儲存,鍵為 IP 地址,值為位置資訊。程式可以在 O(1) 時間複雜度內返回位置資訊,不過該程式或檔案佔用的大小我們可以粗略的計算一下。 假定我們使用 utf-8 進行儲存,一條記錄最短的情況是 0.0.0.0|0|0|0|0|0,佔用17個位元組,IP 庫檔案的大小為 17*4294967296 = 73014444032 B = 71303MB = 71GB。這個大小是任意一個程式不能接受的。

空間最佳化IP 庫檔案最佳化

從上面的檔案資料發現大量的相鄰 IP 擁有相同的位置資訊(客戶在申請一段 IP 地址都會盡量連在一起),所以我們可以將這樣的記錄合成一條記錄。如下檔案資料(地址段依次遞增):

0.0.0.0|0.255.255.255|0|0|0|內網IP|內網IP...1.0.8.0|1.0.15.255|中國|0|廣東省|廣州市|電信...224.0.0.0|255.255.255.255|0|0|0|內網IP|內網IP

ip2region 庫中最新的 ip.merge.txt 共有 658207 記錄,檔案大小 39 M。

IP地址最佳化

從上面的檔案資料發現大量的IP地址以字串形式儲存,而 IPv4 是使用 32 位地址。所以將其轉換成整型進行儲存可以大大節省空間,比如像最短的字串 0.0.0.0 佔據 7 位元組,最長的字串 111.111.111.111 佔據 15 位元組,如果將其轉換成整型,他們都佔據 4 位元組。0.0.0.0 是 int(0), 111.111.111.111 是 int(1869573999)。

位置資訊最佳化

從上面的檔案資料發現相同的位置資訊會對應不同的 IP 段(客戶可能在不同的時間段去申請 IP 段),所以還是有大量的位置資訊在 IP 庫檔案中,在記憶體中我們可以只保留一份位置資訊,並使用指標或者檔案偏移量+資料長度來獲取對應的位置資訊。

最佳化後的IP庫

根據上面的最佳化,我們可以生成最終的IP庫:ip2region.db,該檔案只有8.1M。

IP庫的結構

IP 庫檔案ip2region.db的結構分為四個部分:super 塊, header索引區,資料區,索引區。具體如下圖所示:

super 塊用來儲存索引塊的起始地址和結束地址,第一個索引指標指向索引塊的開始位置,也就是第一個索引分割槽的第一個索引塊, 最後一個索引指標 指向索引塊的結束位置-12,也就是最後一個索引分割槽的最後一個索引塊的頭地址。這樣查詢的時候直接讀取super塊 8 個位元組,就能快速獲取索引塊的地址範圍。header 索引區header索引是對索引塊的二級索引,專門為b+tree搜尋服務的。索引區總長度除以索引分割槽長度12*(1024*8/12-1)就是 header 索引的實際索引數。該區域大小為 2048*8 bytes, 由 2048 個 8 bytes 的 header 索引塊組成。header索引塊前四個位元組儲存每個索引分割槽第一個索引塊的起始ip值,後四個位元組指向該索引塊的地址。header索引區之所以定義為接近16k,是因為可以透過四次磁碟讀取讀取整個header索引區,然後在記憶體中進行查詢,查詢的結果可以確定該ip在索引區的某個索引分割槽內,然後再根據地址兩次讀取8k 索引分割槽到記憶體,再在記憶體中查詢,從而減少磁碟讀取的次數。資料區儲存的資料,資料格式如下:中國|華南|廣東省|深圳市|鵬博士, 分別表示國家,區域,省份,城市,運營商索引區索引區是由索引塊構成, 每個索引塊佔 12 位元組,包括起始IP, 結束IP, 資料資訊。資料資訊中前三個位元組儲存資料地址,後一個位元組儲存資料長度。 每一條索引塊對應 ip.merge.txt 中的一條記錄,表示一個 IP 段的索引。在檢索中當指定 IP 在某個索引塊的起始IP和結束IP中間,即表示命中索引。再透過索引塊中的資料地址和資料長度,就能從 ip2region.db 讀取對應的位置資訊資料。IP庫的生成

ip2region 的 Github 倉庫中提供了 ip2region.db 的生成過程,是用 JAVA 寫的,其類圖如下所示:

透過熟悉生成 ip2region.db 的原始碼,簡述一下其生成過程如下:

透過 RandomAccessFile 在檔案中預留 8 bytes 的 super 塊和 2048*8 bytes 的 header 索引區掃描 ip.merge.txt 檔案,對每一條記錄作如下處理:依據每一條記錄的起始IP, 結束IP 和資料,生成一個索引塊, 前四個位元組儲存起始IP, 中間四個位元組儲存結束IP, 後四個位元組儲存已經計算出的資料地址(透過 RandomAccessFile 寫入,這裡維護一個位置資訊到檔案位置的字典,保證同一個位置資訊只寫入一次。),並將索引塊暫存在 indexPool 連結串列中。這一步會將資料區的所有位置資訊確定。掃描完 ip.merge.txt 中所有的記錄, 將 indexPool 中所有的索引塊寫到資料區後面。在此過程中將 int(1024*8/12-1)= 681 個索引塊組成一個索引分割槽,並記錄下每個索引分割槽第一個索引塊的起始IP和地址資訊(header塊),並暫存在 headerPool 連結串列中。此外還會將索引區的起始位置和結束位置記錄下來。調整 RandomAccessFile 指向檔案開頭,寫入索引區的起始位置儲存到 super 塊的前四個位元組,結束位置儲存到 super 塊的後四個位元組。繼續將 headerPool 中的 header 塊寫入到 header 區。調整 RandomAccessFile 指向檔案結尾,寫入時間戳和版權資訊。

TIPS: ip2region 倉庫中還使用了 global_region.csv 資料,該檔案有5列(行號,,區域,,郵政編碼),對應著區域的具體資訊,可以往資料區每個位置資訊中填充。

快速搜尋

ip2region 提供三種查詢演算法,最差的查詢耗時都是ms級別的。分別是記憶體二分搜尋,b+tree搜尋,二分查詢。耗時依次增加。其搜尋結構圖如下:

二分搜尋

透過 super 塊可以拿到索引區的起始位置和結束位置,而且每個索引塊都是 12 bytes,其中的 IP 地址都是遞增的,所以可以使用二分搜尋來快速獲取位置資訊。其步驟如下:

把 IP 值透過 ip2long 方法轉為整型讀取 super 塊獲取索引區的起始位置和結束位置,二者相減 +1 可得索引塊的總個數採用二分法直接求解,比較索引塊中起始IP,結尾IP 和當前 IP 的大小,即可找到該 IP 對應的索引塊,根據索引塊後面四個位元組得到資料地址和資料長度,從而拿到位置資訊。b+tree搜尋

b+tree 搜尋用到了 header 索引區,第一步先在 header 索引區中使用二分搜尋,定位到某個索引分割槽後,再在對應的索引分割槽中使用二分搜尋。相比較二分搜尋而言,它的速度更快,原因是讀磁碟的次數遠低於二分搜尋。其步驟如下:

把 IP 值透過 ip2long 轉為整型使用二分法在 header 索引區中搜索,比較得到對應的 header 索引塊以及其對應的索引分割槽。讀取對應索引分割槽,再透過二分法定位到對應的索引塊,從而獲得位置資訊。基於記憶體的二分搜尋

該方法和二分搜尋方法類似,區別就是前者將 ip2region.db 全部讀進記憶體中,後者則是透過不斷讀取 ip2region.db 檔案。

總結

ip2region 庫只解決了一個非常常見的 IP 定位問題,但將這個服務做到了又小又快(當然還提供了多語言的客戶端),從而在 Github 上獲得了 8.4k 的 star。

佔用記憶體小相鄰 IP 的位置資訊相同,透過 IP 段來解決相鄰 IP 對應相同位置資訊,避免位置資訊被重複儲存IP 轉換成 INT,像字串 111.111.111.111 被轉換成int(1869573999),從 15Byte 縮小到 4Byte不同的 IP 段也有相同的位置資訊,透過指標來指向特定的位置資訊,保證位置資訊只儲存一次(全量掃描儲存進字典中)搜尋速度快IP 有序,使用二分搜尋將時間複雜度降到 O(logN)二級索引 header 索引區的使用,降低磁碟讀寫頻率,先確定索引分割槽,再從索引分割槽確定索引位置,在確定位置資訊資料。多語言客戶端支援

支援 java、C#、php、c、python、nodejs、php擴充套件(php5和php7)、golang、rust、lua、lua_c, nginx。

16
最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • “殺豬”交易所MXEX已經病急亂投醫?