標籤:ror 沒有 資料 記錄 inpu 串處理 導致 工具 層級
一個用Java寫的GUI程式,作用是分析日誌, 它會將一定數量的格式相同的文本記錄檔讀入記憶體分析處理,然後將結果合并輸出。
檔案數量幾十個,檔案大小幾KB, 日誌記錄幾千條左右, 此工具可以流暢處理, 輕鬆滿足需求。
然而, 因為記錄日誌的方案調整,記錄日誌類型範圍從warn、error層級擴大到了連info、debug層級的日誌也要記錄,從而導致了日誌量激增, 固定時間範圍內產生的記錄檔增加到了幾百個, 單個大小也增加到幾M,日誌記錄一下子達到幾十萬條,然後工具就跑的跟蝸牛一樣慢, 幾十秒才能出結果。
要繼續使用這個工具,自然需要最佳化,要最佳化則需要定位效能瓶頸在哪裡。 這個工具不複雜,沒有複雜的計算, 也沒有無謂的運行損耗, 最可能導致問題的地方是檔案內容讀取的I/O開銷,可是以Java的能耐,從幾百個檔案中讀取幾百M內容, 沒有可能需要幾十秒時間。 而且為了提升速度我還使用了多執行緒,使用線程池為每個記錄檔的讀取和處理分配一個線程,於是我以此處為中心開始分析問題所在,直覺上, 線程的使用總是容易和效能問題聯絡在一起。
因為使用
Executors.newCachedThreadPool()
建立我所使用的線程池,而使用這個線程池太過於潦草, 它沒有大小限制,為每個記錄檔的處理分配一個線程,假如有100個檔案需要處理, 那麼線程池就會建立100個線程, 而我的機器CPU只有4核心,顯然這100個線程無法被同時執行,而且還增加了線程之間切換的開銷,所以懷疑這是導致效能問題的原因之一。
從相關網路資料中得知,最佳線程數目可通過以下公式確定
最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目
顯然,公式中所有變數除了CPU數目, 其它一概不知,所以我採用了另外一個簡單粗暴的公式
線程資料 = 2 * CPU核心數 + 1
這是I/O密集型程式的計算公式。因為這個工具需要頻繁讀取磁碟檔案,所以我將他定位為I/O密集型
ExecutorServiceexecutorService=Executors.newFixedThreadPool(9);
通過使用有數量限制的線程池對程式進行改進後,運行速度似乎有所改善, 可是對於讓程式以可以接受的速度運行而言卻是杯水車薪。當然,我本來也對此改進沒有抱多大希望,因為上百個線程的切換不可能要花幾十秒。
我知道程式存在問題, 可對於一個沒有問題的程式在正常情況下處理這些記錄檔需要花多少時間沒有一個明確的概念,所以並沒有充足的信心確定程式出問題的模組所在,只能沒有證據的猜測, 因此不能迅速的定位問題所在。
可能是直覺的將效能與並發和線程掛鈎,因此接下來的最佳化仍舊以這個主題進行。
我決定將記錄檔的讀取與處理分開,讀取檔案使用一個線程, 日誌資料處理使用一個線程, 讀取檔案的線程將讀取的內容存入一個阻塞隊列,資料處理響線程從阻塞隊列中讀取資料進行處理,這是典型的生產消費模式。
費了很大力氣實現這個模式,信心滿滿的以為程式的運行速度會有所改善,然而,出乎意料的程式啟動並執行速度非但沒有改善,而且比以前更加緩慢了,並且伴隨卡死的現象, 我很沮喪, 可不得不承認,效能瓶頸不在並發處理上,朝這個方向最佳化永遠不能解決問題,高大上的並發技能在這裡毫無用武之地。
我意識必須不斷縮小問題範圍,精確定位到問題代碼所在,才能啃掉這塊骨頭。
我雖然懷疑效能瓶頸在檔案讀取上,可並沒有明確證據, 必須需要一個明確的資料指標才行。
我在程式的關鍵部位加上調試代碼,計算執行各個環節所需要的時間,從資料中明確得知,大部分時間的確被檔案讀取佔用了。
我定位到最接近檔案的代碼,一個從其它項目中拷貝過來的方法
publicstaticStringreadStringFromFile(Filefile){
try{
if(file.isFile()file.exists()){
InputStreamReaderread=newInputStreamReader(newFileInputStream(file),UTF-8);
BufferedReaderbufferedReader=newBufferedReader(read);
StringlineTxt=bufferedReader.readLine();
Stringresult=;
while(lineTxt!=null){
result+=lineTxt;
lineTxt=bufferedReader.readLine();
}
returnresult;
}
}catch(IOExceptione){
e.printStackTrace();
}
returnnull;
}
這個方法似乎沒什麼毛病, 還使用 BufferedReader 來提升效能,我百思不得其解,並開始推測問題所在。
程式是逐行讀去內容的,問題會不會出現在這裡?
如果一次性讀取整個檔案的內容速度會不會快一點?
正思考間,目光被一行代碼吸引
result+=lineTxt;
然後靈光一現,一個編程概念再我腦海中閃現:String類型是安全執行緒的,狀態不可變, 每次操作都會複製一個自身的副本,因此效能很低,在頻繁拼接字串的情況下, 應該以StringBuffer或者StringBuilder代替String。
幾百個記錄檔, 幾十萬條日誌記錄,即使一行一條記錄, Java虛擬機器也執行了幾十萬次的字串的拷貝,速度慢是必然的。
確定問題所在,改進就很容易了,我用StringBuilder類替代String進行字串處理
publicstaticStringreadStringFromFile(Filefile){
try{
if(file.isFile()file.exists()){
InputStreamReaderread=newInputStreamReader(newFileInputStream(file),UTF-8);
BufferedReaderbufferedReader=newBufferedReader(read);
StringlineTxt=bufferedReader.readLine();
StringBuilderresult=newStringBuilder();
while(lineTxt!=null){
result.append(lineTxt);
lineTxt=bufferedReader.readLine();
}
returnresult.toString();
}
}catch(IOExceptione){
e.printStackTrace();
}
returnnull;
}
程式運行速度提升驚人,之前幾十秒才能出的結果,現在不到一秒就完成,問題迎刃而解。 至於為什麼不用StringBuffer是因為StringBuffer是StringBuffer的並發安全版本,效能略微不及StringBuider,而這裡不存在並發處理問題,因此自然選著效能更優的StringBuilder。
只改三行代碼就能解決的問題卻令我折騰了半天, 究其原因在於以下原因
①:思路先入為主, 總是把效能問題和並發掛上鉤,利用多執行緒最佳化
②:憑直覺判斷問題形成的原因,沒有確切的依據, 致使最佳化方向錯誤
很多情況下, 程式效能問題跟並發處理無關, 當一個程式運行緩慢時, 並不會因為多加幾個線程處理就會變的很快,真正引起問題的原因往往出人意料,而且極有可能是非常低級的問題, 排除掉這些問題所在,保證代碼健康,如果效能問題依舊存在, 再使用並發手段去解決不遲。
再者,最佳化程式效能必須確定造成效能問題的代碼塊所在,不能評直覺判斷, 否者不但解決不了問題,還浪費時間。
?
Java效能調優