前情
前幾天做了一個工況統計的功能,查詢最近7天的數據然後分析數據的分佈。最終的效果是這樣的:
從一開始接到這個需求就感覺哪裏有點不對勁,上線一週後終於迎來了一次爆發:
頁面響應慢、多次查詢後服務不可用。
分析
從線上環境拉取日誌後發現兩個異常表現
² 在進行查詢分析的時候,後臺會出現超時異常;
² 經歷了兩到三次的超時異常後,出現了OOM導致系統服務不可用
從日誌的表現我們可以大致還原一下事故現場。
1, 用戶點擊查詢分析功能,服務器開始吭哧吭哧的查詢,由於數據量多導致查詢的響應速度成爲瓶頸。
2, 用戶在等了好久之後覺得頁面是不是掛了,於是刷新頁面並重新發起一次查詢。
3, 服務器重新啓動一個線程繼續開始漫長的吭哧吭哧之旅。
4, 某一次的查詢終於達到了熔斷點,返回了超時異常,但是內存依然佔用無法及時回收,當新的操作需要使用內存的時候,出現了OOM異常,系統服務不可用
驗證
我們可以從兩方面進行驗證,首先是通過問題重現驗證我們關於事故現場的假設,其次通過分析代碼來找出支持這種假設的依據。
問題重現
問題重現很容易,我們從客戶現場拿回了全量的數據文件在本地就可以克隆一個現場環境。然後按照我們設想的步驟進行操作,在第2步的時候等了將近1分鐘,頁面返回了一個空結果集,然後我們再點擊一次查詢,沒過多久頁面出現500錯誤服務已經不可用。
查看後臺日誌的表現和線上環境的表現一致,所以我們可以認爲我們基本還原了現場。
唯一的不同是我們這邊等了近1分鐘後頁面是有返回一個空的結果集,並不是設想中的一直等待。
代碼驗證
查看了該功能的相關代碼後,我們定位出幾個可能導致問題的代碼片段。
Ø 使用了線程池充分利用多線程的優勢加快響應速度,這個沒問題,但是當線程池被不恰當使用的時候很有可能造成系統資源得不到合理分配,最終導致OOM。
Ø 在這裏通過兩個線程分別查詢前一天的數據和前七天的數據,事實上這兩部分數據是有重複的,多餘的查詢動作造成了資源的浪費。
Ø 在線程阻塞的時候設置了15秒和30秒的查詢,這裏應該是造成日誌中超時異常的根源。
交叉驗證
結合問題重現和代碼驗證的結果,我們通過交叉驗證進一步的確認癥結所在。
² 超時問題
在代碼驗證的過程中我們定位了兩個代碼片段,接下來我們就修改這兩個片段,去掉超時時間的指定,一直等待到執行完成。
原來代碼:dailyFuture.get(15, TimeUnit.SECONDS);
修改後代碼:dailyFuture.get();
通過這一步的驗證,我們發現整個操作是可以返回結果的,只不過需要等將近2分鐘,那麼超時問題就轉移爲慢查詢問題了。
接下來我們比較測試數據和線上數據後發現,線上數據比測試數據翻了一倍,原因是現場加大了採集頻率,這樣導致了我們預先實驗出來的超時時間設定不符合數據要求。
由於數據量是我們不可控的,最終針對這個問題我們的解決思路是優化查詢,減少查詢所需要的時間,優化起點是數據量每天1728000點查詢時間30秒,對象轉換時間5-10秒。
² OOM問題
針對OOM問題我們定位的原因是多線程,於是我們將整個查詢過程查詢7天的數據都串行化後,發現一次查詢的內存佔用都在1G以下,由於串行後資源得到及時釋放,多次查詢也不會造成OOM問題。
對比我們目前的設置,線程池設置的最大大小是30,每天的查詢作爲一個任務提交,理論上最多會在內存中保留30天的數據,OOM簡直是一定的了。這樣分析下來我們OOM的問題轉移爲線程池大小的合理設置問題,當然慢查詢問題也是導致OOM問題的一個因素,因爲查詢一直沒有返回導致資源無法被垃圾回收。
總結下來,解決目前問題(超時和服務不可用)的突破口是:
1, 優化慢查詢
2, 合理設置線程池
解決
通過上面的分析和驗證過程我們已經找到了問題的兩個突破口,其中設置線程池主要通過不斷的優化調整完成,所以解決問題的重點我們放在了優化慢查詢上。
我們將整個分佈分析過程分解如下:
其中Influx查詢和客戶端對象轉換屬於我們慢查詢的優化範疇,開始優化之前我們明確一下我們優化的基線
數據量 | Influx查詢用時 | 對象轉換用時 |
1728000(20Hz) | 30s | 5-10s |
Influx查詢
對於Influx查詢,我們首先分析執行過程:
客戶端通過Okhttp發送請求到Influx服務端,服務端執行查詢語句,返回結果到客戶端。
分析下來影響查詢響應時間的因素主要有
1, 傳輸因素,數據量對於網絡傳輸的時間消耗
2, 服務器因素,服務器負載性能對於influx的查詢時間影響
3, 查詢語句,查詢語句的不合理書寫影響了influx的查詢性能,如未使用時間和標籤等索引
首先我們看看我們的查詢語句:
SELECT * FROM history WHERE item_code='%s' and time>=%s and time<%s
這個語句使用了time進行篩選,使用了item_code這個標籤進行索引查詢,但是在返回的結果集中使用了*返回了所有的字段。實際上我們這個需求只需要用到value字段,根據influx的查詢接口我們針對每條記錄只需要返回長度爲2的數組,一個記錄時間一個記錄value。現在使用了*返回了很多無用的tag字段,結果集相當於翻倍了,改進後的查詢語句:
SELECT value FROM history WHERE item_code='%s' and time>=%s and time<%s
僅僅通過這個小小的改動,每次查詢的平均時間減少了8-10s。
順着這個思路我們進一步縮減結果集,將原來寫在代碼中的一個過濾條件加入查詢語句,最終的查詢語句如下:
SELECT value FROM history WHERE ABS(value)> %s and item_code='%s' and time>=%s and time<%s
優化後的查詢時間穩定在15-20s。
對象轉換
對象轉換階段主要發生在influx客戶端,通過InfluxDBResultMapper將服務端的Response轉換成Pojo對象。
在這一步中我們定位了幾個可能的優化點
1, 將Response轉換成Pojo相比於直接操作Response,多了一倍的內存佔用
InfluxDB這樣做可以確保通用性,而對於我們來說沒有必要。
2, 轉換過程基於反射
爲了找到Pojo和字段的對應關係,需要通過反射來保證通用性,我們在已經知道順序的情況下直接賦值即可。
3, 在遍歷結果集的使用的stream而沒有使用parallelstream
InfluxDB爲了保證結果集解析的順序性,而在我們的需求中對於順序沒有要求,所以可以利用並行流來提高處理效率。
最終我們的方案是直接解析Response進行分析計算,代碼如下:
if(!CommonUtil.isNullResponse(res)) { for(Series s : res.getResults().get(0).getSeries()) { if(s.getName().equals("history")) { Map<Double, Long> resultMap = s.getValues().parallelStream().map(row -> (double)row.get(1)/divisor*10+BUCKET_SIZE/2 ) .collect(Collectors.groupingBy(Math::floor,Collectors.counting())); resultMap.keySet().forEach(index ->{ long count0 = + resultMap.get(index); int index0 = (int)index.doubleValue(); if(index0 > BUCKET_SIZE -1) { count[BUCKET_SIZE -1] = count[BUCKET_SIZE -1] + count0; }else if(index0 < 0) { count[0] = count[0] + count0; }else { count[index0] = count[index0] + count0; } }); } } }
通過這一步優化,我們每一次查詢後的轉換可以控制在2s之內,而且大大降低了內存使用。反觀InfluxDB客戶端的實現,這也體現了代碼中通用性和運行效率的博弈,我們可以合理利用tradeoff最終實現我們的目的。
總結
我們通過一起線上的事故作爲切入點,首先通過經驗進行合理的假設分析,得到一個可以模擬的事故現場。然後通過問題重現、代碼定位和交叉驗證的方式定位到最終需要解決的問題。在解決問題階段我們根據我們在解決一般性能問題的經驗,遷移到時序數據庫性能問題的解決上,而最終的解決方案也證實了性能問題都是相通的。
回顧一下我們性能優化的起點,我們的優化還是比較有效的。
數據量 | Influx查詢用時 | 對象轉換用時 | |
優化前 | 1728000(20Hz) | 30s | 5-10s |
優化後 | 1728000(20Hz) | 20s | 2s |
也許有人會覺得查詢花費20s還是有點慢,下面就分享一個這個過程中的趣事。
針對這個問題,我和另一個同事分別進行優化,在我已經優化到7次查詢用時80s左右的時候我覺得已經沒有多大的餘地了,結果他給出了他的優化結果是60s,出於好奇心我又花了一天時間在考慮資源利用最大化的情況下也還是在80左右徘徊。
最後我只能拿着他的方案過來研究一下,結果發現一跑要100多,於是檢查一下機器配置後發現他的測試機是DDR4,而我的是DDR3.
同樣把我的優化方案放到他的機器上跑了一下,7天的查詢時間爲40s,相當於內存效率差了一倍。
最終整合一下優化效果如圖:
DDR3 | 數據量 | Influx查詢用時 | 對象轉換用時 |
優化前 | 1728000(20Hz) | 30s | 5-10s |
優化後 | 1728000(20Hz) | 20s | 2s |
DDR4 | |||
優化後 | 1728000(20Hz) | 10s | 1s |
花絮
此外分享在InfluxDB調優中的兩個花絮
1, 使用chunk的方式並不能提升查詢效率
Chunk的方式其實是一種數據分頁查詢,通過串行的方式查詢一個個子結果集。優點是可以快速返回並處理,減少資源鎖時間。但是在百萬級數據上的切分並沒有到達查詢引擎的臨界點,相反增加了建立連接的次數和結果集處理的複雜度。
2, 不使用綁定變量的方式查詢速度更快
使用關係型數據庫如MySql的經驗告訴我們,在查詢的時候使用綁定變量可以更加安全並且可以獲得較好的查詢性能。安全是毋庸置疑的,但是Influx對於更快的查詢性能方面似乎沒有實現,經過實測使用綁定變量的查詢反而會慢一點。至於這點目前還沒有很好的佐證(挖個坑,等讀懂了Influxdb的源代碼再來填)