40行中的持久性KeyValue Server和一个可悲的事实
再次出現(xiàn)。. 回顧 Peters關(guān)于Unsafe用法的書面概述 ,我將簡要介紹一下Java中的低級技術(shù)如何通過啟用更高級別的抽象或允許Java性能級別來節(jié)省開發(fā)工作可能很多人都不知道。
我的主要觀點是表明,將對象轉(zhuǎn)換為字節(jié),反之亦然是一個重要的基礎(chǔ),實際上影響了任何現(xiàn)代Java應(yīng)用程序。
硬件喜歡處理字節(jié)流,而不是處理通過指針連接的對象圖,因為“所有內(nèi)存都是磁帶” (如果我沒記錯的話,M.Thompson ..)。
因此,許多基本技術(shù)很難與原始Java堆對象一起使用:
- 內(nèi)存映射文件 –一種出色而簡單的技術(shù),可安全,快速,輕松地保存應(yīng)用程序數(shù)據(jù)。
- 網(wǎng)絡(luò)通信基于發(fā)送字節(jié)數(shù)據(jù)包
- 進(jìn)程間通信 (共享內(nèi)存)
- 當(dāng)今服務(wù)器的大主內(nèi)存 (64GB至256GB)。 (GC問題)
- CPU高速緩存最適合在內(nèi)存中以連續(xù)字節(jié)流形式存儲的數(shù)據(jù)
因此在大多數(shù)情況下使用Unsafe類有助于將Java對象圖轉(zhuǎn)換為連續(xù)內(nèi)存區(qū)域,反之亦然
- [性能增強(qiáng)] 對象序列化或
- 包裝器類,以簡化對存儲在連續(xù)內(nèi)存區(qū)域中的數(shù)據(jù)的訪問。
(本文的代碼和示例可在此處找到)
基于序列化的堆外
考慮一個零售Web應(yīng)用程序,其中可能有數(shù)百萬個注冊用戶。 實際上,我們不希望在關(guān)系數(shù)據(jù)庫中表示數(shù)據(jù),因為所有必要的操作是在用戶登錄后快速檢索與用戶相關(guān)的數(shù)據(jù)。此外,我們希望快速遍歷社交圖。
讓我們看一個簡單的用戶類,其中包含一些屬性和構(gòu)成社交圖的“朋友”列表。
將其存儲在堆上的最簡單方法是簡單的大型HashMap。
或者,可以使用堆外映射來存儲大量數(shù)據(jù)。 堆外映射將其鍵和值存儲在本機(jī)堆中,因此垃圾回收不需要跟蹤此內(nèi)存。 此外,可以告知本機(jī)堆自動與磁盤(內(nèi)存映射文件)同步。 甚至在您的應(yīng)用程序崩潰時也可以使用,因為操作系統(tǒng)可以管理對更改的內(nèi)存區(qū)域的回寫。
有一些具有各種功能集的開源堆外地圖實現(xiàn)(例如ChronicleMap ),在此示例中,我將使用一種簡單且簡單的實現(xiàn),該實現(xiàn)具有快速迭代(可選的全掃描搜索)和易用性的特點。
序列化用于存儲對象,反序列化用于將它們再次拉到Java堆。 令人愉快的是,我寫了這個星球上最快的,完全符合JDK的對象序列化 (afaik),因此我將利用它。
完成:
- 通過內(nèi)存映射文件實現(xiàn)持久性(映射將在創(chuàng)建時重新加載)。
- Java Heap仍然是空的,無法使用Full GC <100ms進(jìn)行真實的應(yīng)用程序處理。
- 整體內(nèi)存消耗明顯減少。 序列化的用戶記錄約為60個字節(jié),因此理論上3億條記錄可容納180GB的服務(wù)器內(nèi)存。 無需引發(fā)大數(shù)據(jù)標(biāo)志并在AWS上運(yùn)行4096個hadoop節(jié)點。
比較常規(guī)內(nèi)存中的Java HashMap和基于快速序列化的持久性堆外映射(擁有1500萬條用戶記錄),將顯示以下結(jié)果(在3Ghz較舊的XEON 2×6上):
| 消耗的Java堆(MB) | 完整GC | 本機(jī)堆(MB) | 每秒鐘獲取/輸入操作 | 所需的VM大小(MB) | |
| 哈希圖 | 6.865,00 | 26,039 | 0 | 3.800.000,00 | 12.000,00 |
| OffheapMap(基于序列化) | 63,00 | 0,026 | 3.050 | 750.000,00 | 500,00 |
[ 測試源/博客項目 ]注意:您至少需要16GB的RAM才能執(zhí)行它們。
如人們所見,無論如何,即使進(jìn)行快速序列化,訪問性能也要付出沉重的代價(約5倍):與其他持久性替代方案相比,其性能仍然更好(每個“ get”操作“ put()”為1-3微秒)非常相似)。
使用JDK序列化的速度至少要慢5到10倍(下面直接比較),因此使該方法無用。
相對于更高的抽象水平,交易性能有所提高:“使我服務(wù)器化”
單個服務(wù)器將無法為成千上萬的用戶提供服務(wù),因此我們需要以某種方式在進(jìn)程之間甚至跨機(jī)器共享數(shù)據(jù),甚至更好。
使用快速實現(xiàn),可以為網(wǎng)絡(luò)消息傳遞慷慨地使用(快速)序列化。 再說一次:如果運(yùn)行速度要慢5至10倍,那將是不可行的。 替代方法需要更多數(shù)量級的工作才能獲得相似的結(jié)果。
通過使用Actor實現(xiàn)(異步ftw!)包裝持久性堆外哈希映射,一些代碼行構(gòu)成了具有基于TCP和HTTP接口的持久性KeyValue服務(wù)器(使用kontraktor actors )。 當(dāng)然,如果稍后決定,仍可以在過程中使用Actor。
現(xiàn)在,這是一個微服務(wù)。 鑒于它沒有進(jìn)行任何優(yōu)化的嘗試并且是單線程的 ,因此它的速度相當(dāng)快[與上述XEON機(jī)器相同]:
- 每秒280_000次成功的遠(yuǎn)程查找
- 如果失敗查找,則為800_000(找不到密鑰)
- 基于序列化的TCP接口(1個內(nèi)襯)
- REST-of-us(1個班輪)的嚴(yán)格Web服務(wù)。
[ 來源:KVServer,KVClient ]注意:您至少需要16GB的RAM才能執(zhí)行測試。
現(xiàn)實世界中的實現(xiàn)可能希望通過將接收到的序列化對象byte []直接放入映射中而不是對其進(jìn)行兩次編碼(一次編碼/解碼以便通過網(wǎng)絡(luò)傳輸,然后解碼/編碼以用于拆分映射)來使性能提高一倍。
“ RestActorServer.Publish(..);” 除了原始tcp之外,還可以將KVActor作為Web服務(wù)公開:
使用flyweight包裝器/結(jié)構(gòu)獲得類似C的性能
通過序列化,常規(guī)Java對象將轉(zhuǎn)換為字節(jié)序列。 一個可以做相反的事情:創(chuàng)建包裝器類,該包裝器類從基礎(chǔ)字節(jié)數(shù)組或本機(jī)內(nèi)存地址的固定或計算位置讀取數(shù)據(jù)。 (例如,請參閱此博客文章 )。
通過移動基本指針,僅通過移動包裝器的偏移量就可以訪問不同的記錄。 復(fù)制這樣的“打包對象”歸結(jié)為內(nèi)存副本。 此外,以這種方式編寫分配免費(fèi)的代碼非常容易。 缺點是,與常規(guī)Java對象相比,讀/寫單個字段會降低性能。 這可以通過使用Unsafe類來彌補(bǔ)。
如引用的博客文章中所示,“ flyweight”包裝器類可以手動實現(xiàn),但是隨著代碼的增長,這種情況變得難以維護(hù)。
快速序列化提供了一個副產(chǎn)品“結(jié)構(gòu)仿真”,支持在運(yùn)行時從常規(guī)Java類創(chuàng)建flyweight包裝器類。 這樣可以在很大程度上避免應(yīng)用程序代碼中的低級字節(jié)擺弄。
常規(guī)Java類如何映射到平面內(nèi)存(fst-struct):
當(dāng)然,有一些更簡單的工具可以幫助減少編碼的手動編程(例如Slab ),這可能更適合許多情況并且使用較少的“魔術(shù)”。
使用不同的方法(悲傷事實傳入)可以期待什么樣的性能?
讓我們采用以下結(jié)構(gòu)類,包括價格更新和表示可交易工具(例如股票)的嵌入式結(jié)構(gòu),并使用各種方法對其進(jìn)行編碼:
代碼中的“結(jié)構(gòu)”
純編碼性能:
| 結(jié)構(gòu) | fast-Ser(無共享裁判) | 快速服務(wù) | JDK Ser(未共享) | JDK系列 |
| 26.315.000,00 | 7.757.000,00 | 5.102.000,00 | 649.000,00 | 644.000,00 |
具有消息傳遞吞吐量的真實世界測試:
為了對實際應(yīng)用中的差異進(jìn)行基本估算,我做了一個實驗,當(dāng)通過可靠的UDP消息以高速率發(fā)送和接收消息時,不同的編碼如何執(zhí)行:
考試:
發(fā)送方盡可能快地對消息進(jìn)行編碼,然后使用可靠的多播將其發(fā)布,訂戶接收并對其進(jìn)行解碼。
| 結(jié)構(gòu) | fast-Ser(無共享裁判) | 快速服務(wù) | JDK Ser(未共享) | JDK系列 |
| 6.644.107,00 | 4.385.118,00 | 3.615.584,00 | 81.582,00 | 79.073,00 |
(在I7 / Win8,XEON / Linux上進(jìn)行的測試得分略高,結(jié)構(gòu)的msg大小約為70字節(jié),序列化約為60字節(jié))。
與最低速度相比,最慢速度:82。測試突出顯示了微基準(zhǔn)測試未涵蓋的問題:編碼和解碼應(yīng)執(zhí)行類似的操作,因為實際吞吐量由Min(編碼性能,解碼性能)確定。 出于未知原因,JDK序列化設(shè)法對每秒測試的消息進(jìn)行編碼,例如每秒500_000次,解碼性能僅為每秒80_000次,因此在測試中,接收器Swift下降:
”
…
*****接收速率統(tǒng)計:每秒80351 **********
*****接收速率統(tǒng)計:每秒78769 **********
SUB-ud4q已被服務(wù)1的PUB-9afs丟棄
致命的,無法跟上。 退出
“
(在此處創(chuàng)建背壓可能不是解決此問題的正確方法!)
結(jié)論
- 快速序列化允許在分布式應(yīng)用程序中實現(xiàn)某種程度的抽象,如果序列化實現(xiàn)是
- 太慢了
–不完整。 例如無法處理任何可序列化的對象圖 –需要手動編碼/修改。 (會對演員消息類型,期貨,孢子,維護(hù)噩夢施加許多限制) - 諸如“不安全”之類的低級實用程序可實現(xiàn)數(shù)據(jù)的不同表示,從而為特殊工作負(fù)載提供超常的吞吐量或有保證的等待時間邊界(無分配主路徑)。 使用JDK的公共工具集不可能實現(xiàn)這些目標(biāo)。
- 在分布式系統(tǒng)中,通信性能至關(guān)重要。 查看上面的數(shù)字,刪除不安全并不是最大的麻煩。.JSON或XML不能解決此問題。
- 盡管HotSpot VM已達(dá)到非凡的性能和可靠性水平,但JDK的某些部分卻浪費(fèi)了CPU,就像沒有明天一樣。 考慮到我們生活在分布式應(yīng)用程序和數(shù)據(jù)時代,應(yīng)該很容易實現(xiàn)(而不是手動編碼)通過網(wǎng)絡(luò)移動內(nèi)容。
附錄:有限的延遲
快速的Ping Pong RTT延遲基準(zhǔn)測試表明Java可以輕松地與C解決方案競爭,只要主要路徑是無分配的,并且采用了上述技術(shù)即可:
[學(xué)分:圖表和使用HdrHistogram進(jìn)行的測量]
這是一個“實驗”,而不是一個基準(zhǔn)測試(因此,請不要閱讀:“ 證明:Java比C更快” ),它表明低級Java至少可以在此低級領(lǐng)域與C競爭。
當(dāng)然,它不是完全慣用的 Java代碼,但是與JNI或純C(++)解決方案相比,它仍然更易于處理,移植和維護(hù)。 低延遲的C(++)代碼也不是慣用的!
翻譯自: https://www.javacodegeeks.com/2015/01/a-persistent-keyvalue-server-in-40-lines-and-a-sad-fact.html
總結(jié)
以上是生活随笔為你收集整理的40行中的持久性KeyValue Server和一个可悲的事实的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 太糟糕了,Java 8没有Iterabl
- 下一篇: 3cm多长 3cm是多长