前幾天作了一個工況統計的功能,查詢最近7天的數據而後分析數據的分佈。最終的效果是這樣的:java
從一開始接到這個需求就感受哪裏有點不對勁,上線一週後終於迎來了一次爆發:sql
頁面響應慢、屢次查詢後服務不可用。數據庫
從線上環境拉取日誌後發現兩個異常表現數組
² 在進行查詢分析的時候,後臺會出現超時異常;安全
² 經歷了兩到三次的超時異常後,出現了OOM致使系統服務不可用性能優化
從日誌的表現咱們能夠大體還原一下事故現場。服務器
1, 用戶點擊查詢分析功能,服務器開始吭哧吭哧的查詢,因爲數據量多致使查詢的響應速度成爲瓶頸。網絡
2, 用戶在等了很久以後以爲頁面是否是掛了,因而刷新頁面並從新發起一次查詢。多線程
3, 服務器從新啓動一個線程繼續開始漫長的吭哧吭哧之旅。app
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查詢,咱們首先分析執行過程:
客戶端經過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的源代碼再來填)