使用快取記憶體Serde加速Kafka反序列化效能 - Kaszuba
Kafka內部世界是在位元組級別上儲存狀態的,Serde負責在外部領域語言和Kafka世界之間進行翻譯,但會造成一定的效能損失,因為讀寫需要“始終”通過Serde,尤其是在使用諸如Avro之類的重型Serdes時。
在Kafka峰會上,彭博社的Lei Chen很好地解釋了反序列化的效能問題。有幾種解決此問題的方法。彭博社採取的方法是建立自定義狀態儲存。這是一個非常好的解決方案,並且可以在所有情況下使用,但實際上我很困惑為什麼預設情況下框架沒有這樣的狀態儲存。如何執行此操作以及與此實現相關的問題可以在此處找到。但是還有一種更簡單的方法可能足以滿足您的用例,即快取Serde。
如果您的資料在對狀態儲存的讀取或寫入時都不經常更改,則在Serde級別上進行快取可能就足夠了。它實現起來很簡單,並且不需要對流api發生任何干擾。
我將只關注反序列化,但是相同的概念也適用於序列化。快取反序列化器通過維護記憶體中快取來工作。讀取時,首先搜尋快取以檢視該值是否已反序列化,如果已經返回,則返回該值;如果尚未返回,則呼叫內部反序列化器,並將反序列化的值儲存在快取中。如果達到快取記憶體的最大大小,它將開始清除最早的條目。任何型別的快取都可以用於此目的,下面介紹的一種使用LinkedHashMap,因為它的功能類似於堆疊,因此當達到一定大小的快取時,可以刪除最舊的條目。
package tkaszuba; import org.apache.kafka.common.serialization.Deserializer; import org.apache.kafka.common.utils.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.LinkedHashMap; import java.util.Map; public class CachingDeserializer<T> implements Deserializer<T> { private static final Logger logger = LoggerFactory.getLogger(CachingDeserializer.class); private final LinkedHashMap<Bytes, T> cache = new LinkedHashMap<>(); private final Deserializer<T> inner; private final int cacheSize; public CachingDeserializer(Deserializer<T> inner) { this(500, inner); } public CachingDeserializer(int cacheSize, Deserializer<T> inner) { this.cacheSize = cacheSize; this.inner = inner; } @Override public T deserialize(String s, byte[] bytes) { Bytes key = Bytes.wrap(bytes); if (cache.containsKey(key)) { logger.debug("Taking deserialized value from cache"); return cache.get(key); } if (cache.size() == cacheSize) removeOldestEntryFromCache(); T value = inner.deserialize(s, bytes); logger.debug("Adding deserialized value to cache"); cache.put(key, value); return value; } public Map<Bytes, T> getCache() { return cache; } private void removeOldestEntryFromCache() { logger.debug("Removing oldest deserialized value from cache"); cache.remove(cache.entrySet().iterator().next().getKey()); } @Override public void configure(Map<String, ?> configs, boolean isKey) { inner.configure(configs, isKey); } @Override public void close() { inner.close(); } } |
CachingDeserializer使用Byte物件包裝器作為鍵,而不是位元組陣列,因為它更易於使用。當達到快取大小時,將使用迭代器刪除列表中的第一項,因此無需進行昂貴的遍歷。
使用反序列化器的方法如下:
@Test void testCachingSerde() { KeyValue<Integer, String> keyValue1 = new KeyValue<>(1, "test"); KeyValue<Integer, String> keyValue2 = new KeyValue<>(2, "test"); KeyValue<Integer, String> keyValue3 = new KeyValue<>(3, "test"); KeyValueSerializer<Integer, String> innerSerializer = new KeyValueSerializer<>(Serdes.Integer(), Serdes.String()); KeyValueDeserializer<Integer, String> innerDeserializer = new KeyValueDeserializer<>(Serdes.Integer(), Serdes.String()); try(CachingDeserializer<KeyValue<Integer, String>> deserializer = new CachingDeserializer<>(2, innerDeserializer)) { assertDoesNotThrow(() -> deserializer.configure(Collections.emptyMap(), false)); byte[] value1 = innerSerializer.serialize(TOPIC, keyValue1); byte[] value2= innerSerializer.serialize(TOPIC, keyValue2); byte[] value3 = innerSerializer.serialize(TOPIC, keyValue3); assertEquals(keyValue1, deserializer.deserialize(TOPIC, value1)); assertEquals(1, deserializer.getCache().size(), "Should contain one item in the cache"); assertEquals(keyValue1, deserializer.deserialize(TOPIC, value1)); assertEquals(1, deserializer.getCache().size(), "Should contain one item in the cache"); assertEquals(keyValue1, deserializer.getCache().entrySet().iterator().next().getValue(), "Should contain keyValue1"); assertEquals(keyValue2, deserializer.deserialize(TOPIC, value2)); assertEquals(2, deserializer.getCache().size(), "Should contain two items in the cache"); Iterator<Map.Entry<Bytes, KeyValue<Integer, String>>> iterator = deserializer.getCache().entrySet().iterator(); assertEquals(keyValue1, iterator.next().getValue(), "Should contain keyValue1"); assertEquals(keyValue2, iterator.next().getValue(), "Should contain keyValue2"); assertEquals(keyValue3, deserializer.deserialize(TOPIC, value3)); assertEquals(2, deserializer.getCache().size(), "Should contain two items in the cache"); iterator = deserializer.getCache().entrySet().iterator(); assertEquals(keyValue2, iterator.next().getValue(), "Should contain keyValue2"); assertEquals(keyValue3, iterator.next().getValue(), "Should contain keyValue3"); } } |
注意:在快取中儲存複雜的可變物件(例如Avro)時,應克隆返回的物件,否則將修改快取中的原始物件。直接從狀態儲存讀取資料時,您無需擔心,因為在反序列化過程中會克隆物件。您可能已將其包含在Serde中,具體取決於如何使用Serde。
因此,自然而然的問題是,為什麼您不將所有內容都跳過而不直接將快取記憶體保留在記憶體中,而無需在後臺使用任何狀態儲存呢?我想這全都取決於您的用例,狀態儲存提供了容錯能力並很好地處理了多個分割槽,但是確實引入了很多複雜性,這些複雜性有時很難處理。希望在新版本中,API將會增長,並且與它們一起使用將變得更加容易。
相關文章
- 高效能記憶體快取 ristretto記憶體快取
- CPU快取記憶體快取記憶體
- 快取及使用 Circuit Breaker 限制記憶體使用快取UI記憶體
- Glide - 記憶體快取與磁碟快取IDE記憶體快取
- DDD 和 記憶體快取記憶體快取
- 記憶體快取選型記憶體快取
- 多核cpu、cpu快取記憶體、快取一致性協議、快取行、記憶體快取記憶體協議
- Android記憶體優化之記憶體快取Android記憶體優化快取
- docker部署redis快取記憶體DockerRedis快取記憶體
- 談談CPU快取記憶體快取記憶體
- CPU快取和記憶體屏障快取記憶體
- django 快取表格到記憶體Django快取記憶體
- CQengine高效能記憶體資料快取查詢框架記憶體快取框架
- 鐵威馬NAS如何使用SSD快取記憶體?快取記憶體
- Java記憶體快取-通過Google Guava建立快取Java記憶體快取GoGuava
- 調整緩衝區快取記憶體(Buffer Cache)的效能(轉)快取記憶體
- GO語言————6.12 通過記憶體快取來提升效能Go記憶體快取
- 調整緩衝區快取記憶體(Buffer Cache)的效能(1)快取記憶體
- 調整緩衝區快取記憶體(Buffer Cache)的效能(2)快取記憶體
- 調整緩衝區快取記憶體(Buffer Cache)的效能(3)快取記憶體
- MRAM快取記憶體的組成快取記憶體
- 使用Go實現健壯的記憶體型快取Go記憶體快取
- 記憶體資料庫快取使用者手冊記憶體資料庫快取
- ASP.NET Core - 快取之記憶體快取(上)ASP.NET快取記憶體
- ASP.NET Core - 快取之記憶體快取(下)ASP.NET快取記憶體
- Oracle資料庫高效能秘密之資料快取記憶體Oracle資料庫快取記憶體
- Java記憶體快取-通過Map定製簡單快取Java記憶體快取
- iOS開發之記憶體與快取iOS記憶體快取
- 建立快取記憶體機制-java版快取記憶體Java
- 記憶體資料庫快取介紹記憶體資料庫快取
- 【Linux】Linux 的快取記憶體Linux快取記憶體
- Oracle資料庫高效能祕密之資料快取記憶體Oracle資料庫快取記憶體
- Oracle效能最佳化調整--調整緩衝區快取記憶體Oracle快取記憶體
- 淺談快取寫法(三):記憶體快取該如何設計快取記憶體
- 快取記憶體一致性協議MESI與記憶體屏障快取記憶體協議
- 記憶體資料庫快取使用者手冊總結記憶體資料庫快取
- TMCache原始碼分析(一)—TMMemoryCache記憶體快取原始碼記憶體快取
- iOS 除SDWebImage之外清理記憶體中快取iOSWeb記憶體快取