1. 程式人生 > >榨乾伺服器:一次慘無人道的效能優化

榨乾伺服器:一次慘無人道的效能優化

# 背景 做過2B類系統的同學都知道,2B系統最噁心的操作就是什麼都喜歡批量,這不,我最近就遇到了一個噁心的需求——50個使用者同時每人匯入1萬條單據,每個單據七八十個欄位,請給我優化。 # Excel匯入技術選型 說起Excel匯入的需求,很多同學都做過,也很熟悉,這裡面用到的技術就是POI系列了。 但是,原生的POI很難用,需要自己去呼叫POI的API解析Excel,每換一個模板,你都要寫一堆重複而又無意義的程式碼。 所以,後面出現了EasyPOI,它基於原生POI做了一層封裝,使用註解即可幫助你自動解析Excel到你的Java物件。 EasyPOI雖然好用,但是資料量特別大之後呢,會時不時的來個記憶體溢位,甚是煩惱。 所以,後面某裡又做了一些封裝,搞出來個EasyExcel,它可以配置成不會記憶體溢位,但是解析速度會有所下降。 如果要扣技術細節的話,就是DOM解析和SAX解析的區別,DOM解析是把整個Excel載入到記憶體一次性解析出所有資料,針對大Excel記憶體不夠用就OOM了,而SAX解析可以支援逐行解析,所以SAX解析操作得當的話是不會出現記憶體溢位的。 因此,經過評估,我們系統的目標是每天500萬單量,這裡面匯入的需求非常大,為了穩定性考慮,我們最後選擇使用EasyExcel來作為Excel匯入的技術選型。 # 匯入設計 我們以前也做過一些系統,它們都是把匯入的需求跟正常的業務需求耦合在一起的,這樣就會出現一個非常嚴重的問題:一損俱損,當大匯入來臨的時候,往往系統特別卡。 匯入請求同其它的請求一樣只能打到一臺機器上處理,這個匯入請求打到哪臺機器哪臺機器倒黴,其它同樣打到這臺機器的請求就會受到影響,因為匯入佔用了大量的資源,不管是CPU還是記憶體,通常是記憶體。 還有一個很操蛋的問題,一旦業務受到影響,往往只能通過加記憶體來解決,4G不行上8G,8G不行上16G,而且,是所有的機器都要同步調大記憶體,而實際上匯入請求可能也就幾個請求,導致浪費了大量的資源,大量的機器成本。 另外,我們匯入的每條資料有七八十個欄位,且在處理的過程中需要寫資料庫、寫ES、寫日誌等多項操作,所以每條資料的處理速度是比較慢的,我們按50ms算(實際比50ms還長),那1萬條資料光處理耗時就需要 10000 * 50 / 1000 = 500秒,接近10分鐘的樣子,這個速度是無論如何都接受不了的。 所以,我一直在思考,有沒有什麼方法既可以縮減成本,又可以加快匯入請求的處理速度,同時,還能營造良好的使用者體驗? 經過苦思冥想,還真被我想出來一種方案:獨立出來一個匯入服務,把它做成通用服務。 匯入服務只負責接收請求,接收完請求直接告訴前端收到了請求,結果後面再通知。 然後,解析Excel,解析完一條不做其它處理直接就把它扔到Kafka中,下游的服務去消費,消費完了,再發一條訊息給Kafka告訴匯入服務這條資料的處理結果,匯入服務檢測到所有行數都收到了反饋,再通知前端這次匯入完成了。(前端輪詢) ![](https://img2020.cnblogs.com/blog/1648938/202103/1648938-20210312143219459-673503475.png) 如上圖所示,我們以匯入XXX為例描述下整個流程: 1. 前端發起匯入XXX的請求; 2. 後端匯入服務接收到請求之後立即返回,告訴前端收到了請求; 3. 匯入服務每解析一條資料就寫入一行資料到資料庫,同時傳送該資料到Kafka的XXX_IMPORT分割槽; 4. 處理服務的多個例項從XXX_IMPORT的不同分割槽拉取資料並處理,這裡的處理可能涉及資料合規性檢查,呼叫其他服務補齊資料,寫資料庫,寫ES,寫日誌等; 5. 待一條資料處理完成後給Kafka的IMPORT_RESULT傳送訊息說這條資料處理完了,或成功或失敗,失敗需要有失敗原因; 6. 匯入服務的多個例項從IMPORT_RESULT中拉取資料,更新資料庫中每條資料的處理結果; 7. 前端輪詢的介面在某一次請求的時候發現這次匯入全部完成了,告訴使用者匯入成功; 8. 使用者可以在頁面上檢視匯入失敗的記錄並下載; 這就是整個匯入的過程,下面就開始了踩坑之旅,你準備好了嗎? > 聰明的同學會發現,(關注公號彤哥讀原始碼一起學習一起浪)其實大批量匯入跟電商中的秒殺是有些類似的,所以,整個過程引入Kafka來在削峰和非同步。 # 初步測試 經過上面的設計,我們測試匯入1萬條資料只需要20秒,比之前預估的10分鐘快了不止一星半點。 但是,我們發現一個很嚴重的問題,當我們匯入資料的時候,查詢介面卡到爆,需要等待10秒的樣子查詢介面才能刷出來,從表象來看,是匯入影響了查詢。 # 初步懷疑 因為我們查詢只走了ES,所以,初步懷疑是ES的資源不夠。 但是,當我們檢視ES的監控時發現,ES的CPU和記憶體都還很充足,並沒有什麼問題。 然後,我們又仔細檢查了程式碼,也沒有發現明顯的問題,而且服務本身的CPU、記憶體、頻寬也沒有發現明顯的問題。 真的神奇了,完全沒有了任何思路。 而且,我們的日誌也是寫ES的,日誌的量比匯入的量還更大,查日誌的時候也沒有發現卡過。 所以,我想,直接通過Kibana查詢資料試試。 說幹就幹,在匯入的同時,在Kibana上查詢資料,並沒有發現卡,結果顯示只需要幾毫秒資料就查出來了,更多的耗時是在網路傳輸上,但是整體也就1秒左右資料就刷出來了。 因此,可以排除是ES本身的問題,肯定還是我們的程式碼問題。 此時,我做了個簡單的測試,我把查詢和匯入的處理服務分開,發現也不卡,秒級返回。 答案已經快要浮出水面了,一定是匯入處理的時候把ES的連線池資源佔用完了,導致查詢的時候拿不到連線,所以,需要等待。 通過檢視原始碼,最終發現ES的連線數是在RestClientBuilder類中寫死的,DEFAULT_MAX_CONN_PER_ROUTE=10,DEFAULT_MAX_CONN_TOTAL=30,每個路由最大10,總連線數最大30,而且更操蛋的是,這兩個配置是寫死在程式碼裡面的,沒有引數可以配置,只能通過修改程式碼來實現了。 這裡也可以做個簡單的估算,我們的處理服務部署了4臺機器,每臺機器一共可以建立30條連線,4臺機器就是120條連線,匯入一萬單如果平均分配,每條連線需要處理 10000 / 120 = 83條資料,每條資料處理100ms(上面用的50ms,都是估值)就是8.3秒,所以,查詢的時候需要等待10秒左右,比較合理。 直接把這兩個引數調大10倍到100和300,(關注公號彤哥讀原始碼一起學習一起浪)再部署服務,測試發現匯入的同時,查詢也正常了。 接下來,我們又測試了50個使用者同時匯入1萬單,也就是併發匯入50萬單,按1萬單20秒來算,總共耗時應該在 50*20=1000秒/60=16分鐘,但是,測試發現需要耗時30分鐘以上,這次瓶頸又在哪裡呢? # 再次懷疑 我們之前的壓測都是基於單使用者1萬單來測試的,當時的伺服器配置是匯入服務4臺機器,處理服務4臺機器,根據上面我們的架構圖,按理說匯入服務和處理服務都是可以無限擴充套件的,只要加機器,效能就能上去。 所以,首先,我們把處理服務的機器加到了25臺(我們基於k8s,擴容非常方便,改個數字的事),跑一下50萬單,發現沒有任何效果,還是30分鐘以上。 然後,我們把匯入服務的機器也加到25臺,跑了一下50萬單,同樣地,發現也沒有任何效果,此時,有點懷疑人生了。 通過檢視各元件的監控,發現,此時匯入服務的資料庫有個指標叫做IOPS,已經達到了5000,並且持續的在5000左右,IOPS是什麼呢? 它表示一秒讀寫IO多少次,跟TPS/QPS差不多,說明MySQL一秒與磁碟的互動次數,一般來說,5000已經是非常高的了。 目前來看,瓶頸可能在這裡,再次檢視這個MySQL例項的配置,發現它使用的是超高IO,實際上還是普通的硬碟,想著如果換成SSD會不會好點呢。 說幹就幹,聯絡運維重新購買一個磁碟是SSD的MySQL例項。 切換配置,重新跑50萬單,這次的時間果然降下來了,只需要16分鐘了,接近降了一半。 所以,SSD還是要快不少的,檢視監控,當我們匯入50萬單的時候,SSD的MySQL的IOPS能夠達到12000左右,快了一倍多。 後面,我們把處理服務的MySQL磁碟也換成SSD,時間再次下降到了8分鐘左右。 你以為到這裡就結束了嘛(關注公號彤哥讀原始碼一起學習一起浪)? # 思考 上面我們說了,根據之前的架構圖,匯入服務和處理服務是可以無限擴充套件的,而且我們已經分別加到了25臺機器,但是效能並沒有達到理想的情況,讓我們來計算一下。 假設瓶頸全部在MySQL,對於匯入服務,我們一條資料大概要跟MySQL互動4次,整個Excel分成頭表和行表,第一條資料是插入頭表,後面的資料是更新頭表、插入行表,等處理完了會更新頭表、更新行表,所以按12000的IOPS來算的話,MySQL會消耗我們 500000 * 4 / 12000 / 60= 2.7分鐘,同樣地,處理服務也差不多,處理服務還會去寫ES,但處理服務沒有頭表,所以時間也按2.7分鐘算,但是這兩個服務本質上是並行的,沒有任何關係,所以總的時間應該可以控制在4分鐘以內,因此,我們還有4分鐘的優化空間。 # 再優化 經過一系列排查,我們發現Kafka有個引數叫做kafka.listener.concurrency,處理服務設定的是20,而這個Topic的分割槽是50,也就是說實際上我們25臺機器只使用了2.5臺機器來處理Kafka中的訊息(猜測)。 找到了問題點,就很好辦了,先把這個引數調整成2,保持分割槽數不變,再次測試,果然時間降下來了,到5分鐘了,後面經過一系列調整測試,發現分割槽數是100,concurrency是4的時候效率是最高的,最快可以達到4分半的樣子。 至此,整個優化過程告一段落。 # 總結 現在我們來總結一下一共優化了哪些地方: 1. 匯入Excel技術選型為EasyExcel,確實非常不錯,從來沒出現過OOM; 2. 匯入架構設計修改為非同步方式處理,參考秒殺架構; 3. Elasticsearch連線數調整為每個路由100,最大連線數300; 4. MySQL磁碟更換為SSD; 5. Kafka優化分割槽數和kafka.listener.concurrency引數; 另外,還有很多其它小問題,限於篇幅和記憶,無法一一講出來。 # 後期規劃 通過這次優化,我們也發現了當資料量足夠大的時候,瓶頸還是在儲存這塊,所以,是不是優化儲存這塊,效能還可以進一步提升呢? 答案是肯定的,比如,有以下的一些思路: 1. 匯入服務和處理服務都修改為分庫分表,不同的Excel落入不同的庫中,減輕單庫壓力; 2. 寫MySQL修改為批量操作,減少IO次數; 3. 匯入服務使用Redis來記錄,而不是MySQL; 但是,這次要不要把這些都試一遍呢,其實沒有必要,通過這次壓測,我們至少能做到心裡有數就可以了,真的等到量達到了那個級別,再去優化也不遲。 好了,今天的文章就到這