基於內存映射的千萬級數據處理框架
在計算機的世界裏,將大問題切分爲多個小問題予以解決是很是優秀的思想。
許多優秀的數據存儲框架都採用分佈式架構解決海量數據的存儲問題,在典型的數據庫中間件架構中,
每每抽象出邏輯的數據表概念,一個邏輯表對應多個物理表,寫入的數據會根據規則路由到指定的物理表,
這不只解決了海量數據的存儲問題,還附帶解決單點故障問題,在以前依靠昂貴服務器的架構中,一旦我
們這個昂貴的傢伙罷工,那麼網站幾乎就會陷入癱瘓,而在分佈式架構中,對服務器的要求是比較低的,
並且即便某個節點壞了,每每也隻影響部分業務。分佈式架構給咱們帶來了'銀彈',但同時也給咱們帶來
了挑戰,咱們不得不面對數據切分後的合併問題.在某些場景下,咱們不得不從各個節點加載大量數據,從
中篩選出知足業務需求的幾條,如執行如下SQL:
select * from xxx limit 10000000,10
咱們不得不去每一個節點加載1000w+10條數據,合併後在其中篩選知足條件的10條,這將會對咱們架構的
中樞節點形成巨大的壓力,簡單的解決辦法是加大中樞節點的內存和CPU,在內中合併這些數據,但這不現
實,相比於大量的業務數據,現今的內存容量小的太多太多,第二種方案是將這些數據先寫到硬盤,再根據
需求加載數據進行處理,相比內存而言,硬盤的容量能支持很是大的數據,可是,若是有時間要求呢?若是
每條與上述SQL相似的語句都須要幾小時或數十小時的時間來運行,哪恐怕業務又會不斷的抱怨了。咱們先
擱置這個問題,看一個比較常見的現象,在咱們2G內存的PC中,能夠同時打開大型遊戲,文本編輯器,瀏
覽器,圖片查看程序。。。,這些程序使用的內存每每遠超咱們的物理內存,操做系統是如何調度這些程序
的呢,技巧之一就是使用虛擬內存,每一個進程都有4G左右的虛擬內存空間(32bit),這些空間並非真實
的物理內存地址,他和物理內存或文件之間存在映射關係,這個映射關係是在系統加載程序時建立好的,當內
存不足時,OS會將最近使用較少的內存塊寫到文件,須要時又讀回內存,這部分操做雖然涉及I/O,可是與
普通的I/O不一樣,一方面他老是按塊順序地讀寫,另外一方面他不會有二次讀寫問題(數據老是讀寫到系統的緩
衝區中轉),因此即便在2G的PC上運行多個程序使用內存大於2G,在局部性原理的做用下,你感受不到系統
內存調度帶來的影響,固然,這種感受是相對的。既然OS的虛擬內存機制利用文件實現也十分高效,咱們爲什
麼不用來解決上述問題呢,windows和*ux都提供了相應的接口,咱們能夠用來實現內存映射,幸運的是
java已經支持這種內存映射特性,採用java能夠快速實現這樣一套模擬OS的虛擬內存管理框架,Moni
就是這樣一套框架,初衷是做爲我設計的mysql中間件VirtualDB的跨節點數據合併組件,後來在研究
Mycat時發現Mycat也遇到這個問題,因而我把這部份抽出來做爲一個獨立的框架,並取名Moni。我爲
Moni設計了一套自動擴容和地址數據分離的數據結構,Moni的數據存取很是快,自己幾乎沒有內存消耗。
理論上容量只受限於硬盤大小,目前版本數據量建議在千萬級之內。Moni是一個很是輕巧的框架,總共5個類,
Moni源碼:
https://git.oschina.net/coder-c/Moni.git 類結構以下:
org.virtualdb.mpp
MemMapBytesItor:用於存取byte[]的數據結構只支持順序添加和遍歷,是最高效的
MemMapBytesArray:用於存取byte[]的數據結構,支持隨機讀寫和排序
MemMapLongArray :用於存取long的數據結構
MemMapSorter :上述數據結構的排序工具,實現了原地歸併和堆排序以及位圖排序
MemMapUtil :工具類
org.virtualdb.mpp.test
....
java
一般只需關注 MemMapBytesArray,MemMapLongArray是爲實現地址和數據分離而實現的,地址數據
分離是實現快速排序的基礎,目前全部的排序算法都須要移動數據自己,這在數據量大時會形成巨大的時間消耗,有
的算法還會產生巨大的空間消耗。有以下數組:
數據: [12,96,8,-1,45]
索引: 0, 1,2, 3, 4
常規的排序須要將-1移到0號索引,將8移到1號索引,依次類推。在數據長度不固定的數組中,每次數據的移動涉
及空間的擴容和壓縮,所以難以用常規的排序算法進行排序,即便實現了排序也很是耗時。
咱們換個思惟進行排序,不移動數據,改成移動索引,以下:
數據: [12,96,8,-1,45]
索引: 0, 1,2, 3, 4
將-1的索引改成0,8的索引改成1,依此類推,實現對數據的排序。常規排序中所使用的索引是數組自然具備的,
但在基於地址移動實現的排序中,咱們必須本身維護索引,MemMapBytesArray內部記錄每條數據的地址從而在
巨大的‘內存’中區分不一樣的數據項以便排序,MemMapBytesArray初始化時會映射400M的內存,若是添加數據
大於400M,MemMapBytesArray會自動擴容,這點與JDK的ArrayList十分相似,但不一樣的是,JDK的
ArrayList在擴容時必須copy數據,這在數據量大時會帶來巨大的時間效耗,MemMapBytesArray擴容後無
需copy原數據,而是記錄當前的擴容數,獲取數據時按page+offset的方式定位數據的具體地址,實現快速的存取
數據。簡單說,ArrayList是採用單一數組存儲數據,而MemMapBytesArray採用多數組存儲數據,他至關於
一本字典,咱們須要某條數據時先找到該數據所在的page,再根據offset定位到數據。MemMapBytesArray支
持不定長的數據項,但建議單條數據項儘可能小,而且不能超過65535個字節。對於不須要排序的數據而言,MemMapBytesItor
是最佳選擇,他不須要記錄數據的地址,佔用的空間較少,順序遍歷也使得其速度很快。
MemMapSorter中的原地歸併排序,在我電腦上測試,與JDK自帶的快排至關,對1000w隨機數排序,MemMapSorter
原地歸併耗時1.3s左右,JDK耗時1.15s左右。固然,對於不重複的數字排序,最快的仍是MemMapSorter裏的位圖排序,
1000w隨機數排序耗時僅0.3s,實現也是最簡單的,內存消耗也很是少,1000w個5000w之內的數據排序,僅消耗5000w/8
也就是大約6M內存。