在過去,由於粗粒度緩存過期策略和外部緩存的缺乏,查詢緩存在 Kylin 中的使用效率不高。由於激進的緩存過期策略,有用的緩存經常被不必要地清理。因爲查詢緩存存儲在本地服務器中,它們因而不能在服務器之間共享。同時,由於本地緩存的大小限制,並不是所有有用的查詢結果都可以被緩存。
針對這些不足,我們使用簽名檢查來實現新的查詢緩存失效策略,並引入 memcached 作爲 Kylin 的分佈式緩存,使 Kylin 服務器能夠在服務器之間共享緩存。同時添加 memcached 服務器來擴展分佈式緩存也是很容易的。
這些功能由 eBay Kylin 團隊提出和開發,在此非常感謝他們的貢獻。
相關的 JIRA
- KYLIN-2895 Refine Query Cache: https://issues.apache.org/jira/browse/KYLIN-2895
- KYLIN-2899 Introduce segment level query cache:https://issues.apache.org/jira/browse/KYLIN-2899
- KYLIN-2898 Introduce memcached as a distributed cache for queries:https://issues.apache.org/jira/browse/KYLIN-2898
- KYLIN-2894 Change the query cache expiration strategy by signature checking:https://issues.apache.org/jira/browse/KYLIN-2894
- KYLIN-2897 Improve the query execution for a set of duplicate queries in a short period:https://issues.apache.org/jira/browse/KYLIN-2897
- KYLIN-2896 Refine query exception cache:https://issues.apache.org/jira/browse/KYLIN-2896
深度剖析
引入 memcached 作爲分佈式查詢緩存
memcached 是一種自由開放的開源、高性能、分佈式內存對象緩存系統。它適用於數據庫調用、API 調用或頁面渲染等場景,可以用於任意數據(字符串、對象)的內存內鍵值存儲。它簡單而有效。它的簡單設計便於快速部署,並令其易於開發,並解決了面臨大數據緩存的許多問題。它的 API 適用於大多數流行語言。
通過 Kylin-2898,Kylin 使用 memcached 作爲分佈式緩存服務,並使用EHCache作爲本地緩存服務。當在 applicationcontext.xml中配置 RemotelocalFailOvercacheManager 時,對於每個緩存 PUT/GET 操作,Kylin 將首先檢查分佈式緩存服務是否可用,只有當分佈式緩存服務不可用時,纔會使用本地緩存服務。
首先,多個查詢服務器可以共享查詢緩存。對於每個 Kylin 服務器而言,更少的 JVM 內存會被佔用,這有助於降低 GC 壓力。其次,由於 memcached 是集中式的,所以在多個 Kylin 進程中將避免重複的緩存條目。第三, memcached 具有較大的尺寸和易於擴展的特性,這將有助於減少由於內存容量有限而導致的不得不丟棄掉有用緩存條目的可能性。
爲了處理節點故障和擴展 memcached 集羣,作者引入了一種一致性散列策略以順利解決這類問題。Ketama 實現了一致的散列算法,這意味着您可以從 memcached 池中添加或刪除服務器,而不需要對所有鍵進行完全重新映射。詳細信息可以在 Ketama consistent hash strategy (https://www.last.fm/user/RJ/journal/2007/04/10/rz_libketama_-_a_consistent_hashing_algo_for_memcache_clients) 中查閱。
Segment 級別緩存
當前,Kylin 使用 SQL 作爲緩存鍵,當 Kylin 收到查詢請求時,如果緩存中存在結果,它將直接返回緩存的結果,且不需要查詢 HBASE。當有新的片段生成或現有片段刷新時,所有相關的緩存結果都需被清除。對於一些經常被建構的 Cube,如流式 Cube (NRT Streaming 或 Real-time OLAP),緩存未擊中的情況會急劇增加,這可能會降低查詢性能。
對於 Kylin cube 而言,大多數歷史 Segment 是不可更改的除非 segment 被更新,對歷史 Segment 的相同查詢出來的結果應該始終相同,所以歷史 Segment 的緩存也不應該被清除。爲此,我們決定實現 Segment 級別緩存,它是現有前端緩存的一個補充,其思路與操作系統中的 Level1/Level2 緩存相似。
基於簽名檢查的緩存失效策略
當前,對於無效的查詢緩存, CacheService 將調用cleanDataCache 或 cleanAllDataCache。這兩種方法都將清除所有查詢緩存,這非常低效且不必要。在生產環境中,每天有數百個 Cubing 作業,這意味着查詢緩存將被每幾分鐘全部清除一次。我們接着介紹了一種新的基於簽名檢查的查詢緩存失效策略。
基本思路如下:
將 SQLResponse(也就是查詢結果)放入緩存時,我們爲每個 SQLResponse 計算簽名。要計算 SQLResponse 的簽名,我們選擇 Cube 最後一次構建發生的時間及其 Segment 作爲 SignatureCalculator 的輸入。
當從緩存獲取 SQL 對應的 SQLResponse 時,首先檢查簽名是否一致。如果不一致,則此緩存值已過期並將被刪除。
關於簽名的計算,如下所示:
1. ComponentSignature 的 toString 將把成員變量連接到字符串中;如果 ComponentSignature具有其他 ComponentSignature作爲成員,則將遞歸地計算 toString。
2. toString的返回值將輸入 SignatureCalculator,SignatureCalculator 將經過MD5編碼後的字符串作爲查詢緩存簽名的標識符。
其他增強
短時間內重複查詢的優化
如果不同的客戶端同時向 Kylin 發送相同的請求, 在首條查詢結果返回前,對於任一查詢而言,就不能找到他們的查詢緩存,因此必須分別計算它們。更糟糕的是,如果這些查詢很複雜,它們通常會花費很長的時間,這樣 Kylin 能利用緩存查詢的機會就會更少;同時也會耗費大量的計算資源,使得查詢服務器性能變差並對 Hbase 集羣造成損害
爲了減少重複的複雜查詢的影響,我們可以阻塞隨後出現的查詢,等到首個查詢獲取到結果再統一返回。如果同時出現重複的複雜查詢,此延遲策略會尤其有用。要使其生效,您應該將 kylin.query.lazy-query-enabled 設置爲 true。另外,您也可以選擇將kylin.query.lazy-query-waiting-timeout-milliseconds 設置爲您認爲後來的重複查詢需要的等待時間,以匹配您的場景。
刪除異常緩存
過去,查詢緩存被分爲兩部分,一部分用於存儲成功的查詢結果,另一部分用於儲存失敗的查詢結果,它們會分別失效。這看起來不是一個很好的分類標準,因爲它不夠細粒度。在引入查詢緩存簽名後,我們沒有理由將它們分開,因此刪除了異常緩存。
如何使用
爲了做好準備,您需要安裝 memcached,可以參考 https://github.com/memcached/memcached/wiki/Install. 接着您需要修改kylin.properties 和 applicationContext.xml.
kylin.properties
kylin.cache.memcached.hosts=10.1.2.42:11211
kylin.query.cache-signature-enabled=true
kylin.query.lazy-query-enabled=true
kylin.metrics.memcached.enabled=true
kylin.query.segment-cache-enabled=true
applicationContext.xml
<cache:annotation-driven/>
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
p:configLocation="classpath:ehcache-test.xml" p:shared="true"/>
<bean id="remoteCacheManager" class="org.apache.kylin.cache.cachemanager.MemcachedCacheManager"/>
<bean id="localCacheManager" class="org.apache.kylin.cache.cachemanager.InstrumentedEhCacheCacheManager"
p:cacheManager-ref="ehcache"/>
<bean id="cacheManager" class="org.apache.kylin.cache.cachemanager.RemoteLocalFailOverCacheManager"/>
<bean id="memcachedCacheConfig" class="org.apache.kylin.cache.memcached.MemcachedCacheConfig">
<property name="timeout" value="500"/>
<property name="hosts" value="${kylin.cache.memcached.hosts}"/>
</bean>
查詢緩存配置
常規部分
配置鍵 | 配置值 | 說明 |
kylin.query.cache-enabled |
boolean,默認值爲真 |
是否啓用查詢緩存 |
kylin.query.cache-threshold-duration |
long, 以毫秒爲單位,默認值爲2000 |
需要被緩存的查詢的查詢時間閾值 |
kylin.query.cache-threshold-scan-count |
long,默認值爲10240 |
需要被緩存的查詢的掃描行計數閾值 |
kylin.query.cache-threshold-scan-bytes |
long,默認值爲1024 * 1024 (1MB) |
需要被緩存的查詢的查詢掃描字節閾值 |
Memcached部分
配置鍵 | 配置值 | 說明 |
kylin.cache.memcached.hosts | 主機1:端口1,主機2:端口2 | memcached主機的主機列表 |
kylin.query.segment-cache-enabled | 默認值 false | 是否啓用Segment級別緩存 |
kylin.query.segment-cache-timeout | 默認值2000 | memcached超時閾值 |
kylin.query.segment-cache-max-size | 200 (MB) | 置入memcached的最大字節 |
緩存簽名部分
配置鍵 | 配置值 | 說明 |
kylin.query.cache-signature-enabled | 默認值 false | 是否對查詢緩存使用簽名 |
kylin.query.signature-class | 默認值是org.apache.kylin.rest.signature.FactTableRealizationSetCalculator | 使用哪個類計算查詢緩存的簽名 |
其他優化部分
配置鍵 | 配置值 | 說明 |
kylin.query.lazy-query-enabled | 默認值 false | 是否阻止重複的SQL查詢 |
kylin.query.lazy-query-waiting-timeout-milliseconds | long, 以毫秒爲單位,默認值是60000 | 阻止重複SQL查詢的最長時段 |