2012-03-11 93 views
7

我爲少數java應用程序執行性能測試。在測試過程中,應用程序會產生非常大的日誌文件(可能爲7-10 GB)。我需要在特定日期和時間之間修剪這些日誌文件。目前,我使用python腳本,它解析日期時間python對象中的日誌時間戳,並只打印匹配的字符串。但是這個解決方案非常緩慢。 5 GB日誌分析大約25分鐘 顯然,日誌文件中的條目是按順序排列的,我不需要逐行讀取所有文件。 我曾經考慮過從頭到尾閱讀文件,直到條件匹配並在匹配的行數之間打印文件。但我不知道如何從後面讀取文件,而無需將其下載到內存中。修剪大日誌文件

請問,你能否爲我推薦任何適合本案例的解決方案。

這裏是Python腳本的一部分:

 lfmt = '%Y-%m-%d %H:%M:%S' 
     file = open(filename, 'rU') 
     normal_line = '' 
     for line in file: 
     if line[0] == '[': 
      ltimestamp = datetime.strptime(line[1:20], lfmt) 

      if ltimestamp >= str and ltimestamp <= end: 
      normal_line = 'True' 
     else: 
      normal_line = '' 

     if normal_line: 
     print line, 
+3

你可以發佈你的日誌文件? – kev 2012-03-11 08:27:50

+2

忽略kev的笑話,你應該看看你的日誌文件旋轉策略。讓任何日誌文件變得這麼大是不好的做法。 – TerryE 2012-03-11 11:00:16

+1

'logrotate'工具是去這裏的方式 - 它存在以防止發生這種事情。 – Daenyth 2012-03-11 17:16:38

回答

5

由於數據是連續的,如果開始和感興趣的區域的結束是接近文件的開頭,然後閱讀從文件的末尾(找到匹配的終點)仍然是一個不好的解決方案!

我寫了一些代碼,可以根據需要快速找到開始點和結束點,這種方法被稱爲binary search,類似於經典兒童「更高或更低」的猜謎遊戲!

該腳本讀取lower_boundsupper_bounds(最初是SOF和EOF)之間的試行線,並檢查匹配標準。如果所尋找的線路較早,則通過讀取lower_bound與之前的讀取試驗之間的中間線(如果其更高,則其在猜測和上限之間分開),再次猜測。因此,您不斷在上下限之間進行迭代 - 這會產生最快的「平均」解決方案。

這應該是一個真正的快速解決方案(登錄到第2行的基數!!)。例如,在最壞的情況下(查找1000行中的999行),使用二分查找只需要9行讀取! (從十億線將只需30 ...)

爲下面的代碼

假設:

  • 每一行與時間信息開始。
  • 時間是唯一的 - 如果沒有,當發現匹配時,您必須檢查向後或向前以適當(如果需要)包含或排除具有匹配時間的所有條目。
  • 有趣的是,這是一個遞歸函數,因此文件的行數限制爲2 ** 1000(幸運的是,這允許相當大的文件...)

此外:

  • 這可適於在任意塊來讀取,而不是通過線,如果優選的。正如J.F.塞巴斯蒂安所建議的那樣。
  • 在我原來的答案中,我建議這種方法,但使用linecache.getline,雖然這可能是不適合大文件,因爲它將整個文件讀入內存(因此file.seek()是優越的),這要感謝TerryE和J.F. Sebastian指出的。

進口日期時間

def match(line): 
    lfmt = '%Y-%m-%d %H:%M:%S' 
    if line[0] == '[': 
     return datetime.datetime.strptime(line[1:20], lfmt) 

def retrieve_test_line(position): 
    file.seek(position,0) 
    file.readline() # avoids reading partial line, which will mess up match attempt 
    new_position = file.tell() # gets start of line position 
    return file.readline(), new_position 

def check_lower_bound(position): 
    file.seek(position,0) 
    new_position = file.tell() # gets start of line position 
    return file.readline(), new_position 

def find_line(target, lower_bound, upper_bound): 
    trial = int((lower_bound + upper_bound) /2) 
    inspection_text, position = retrieve_test_line(trial) 
    if position == upper_bound: 
     text, position = check_lower_bound(lower_bound) 
     if match(text) == target: 
      return position 
     return # no match for target within range 
    matched_position = match(inspection_text) 
    if matched_position == target: 
     return position 
    elif matched_position < target: 
     return find_line(target, position, upper_bound) 
    elif matched_position > target: 
     return find_line(target, lower_bound, position) 
    else: 
     return # no match for target within range 

lfmt = '%Y-%m-%d %H:%M:%S' 
# start_target = # first line you are trying to find: 
start_target = datetime.datetime.strptime("2012-02-01 13:10:00", lfmt) 
# end_target = # last line you are trying to find: 
end_target = datetime.datetime.strptime("2012-02-01 13:39:00", lfmt) 
file = open("log_file.txt","r") 
lower_bound = 0 
file.seek(0,2) # find upper bound 
upper_bound = file.tell() 

sequence_start = find_line(start_target, lower_bound, upper_bound) 

if sequence_start or sequence_start == 0: #allow for starting at zero - corner case 
    sequence_end = find_line(end_target, sequence_start, upper_bound) 
    if not sequence_end: 
     print "start_target match: ", sequence_start 
     print "end match is not present in the current file" 
else: 
    print "start match is not present in the current file" 

if (sequence_start or sequence_start == 0) and sequence_end: 
    print "start_target match: ", sequence_start 
    print "end_target match: ", sequence_end 
    print 
    print start_target, 'target' 
    file.seek(sequence_start,0) 
    print file.readline() 
    print end_target, 'target' 
    file.seek(sequence_end,0) 
    print file.readline() 
+1

fraxel幾乎是正確的。您*不能*使用linecache,因爲任何支持可變行長度文件中的精確行號偏移的方法都必須在內部執行(懶惰)全面掃描。對1024字節塊進行線性插值讀取,然後掃描\ n + DTS模式以查找當前時間。正如fraxel所說,這是文件大小的O(log N)。 – TerryE 2012-03-11 11:06:42

+0

@TerryE你能否稍微解釋一下你的「線性插值」建議,或者提供一個鏈接? – urschrei 2012-03-11 14:15:40

+1

是一個簡單的迭代方法,它工作得很好*如果記錄速率是統一的。如果不是,則使用二進制搜索。維基百科上都有文章。您正在使用事實,即文件在升序DTS中以通過探測找到正確的開始和結束字節。對於8Gb的日誌,這需要約66塊讀取,例如〜6秒。當然,你仍然需要從字節M複製到N和/或動態壓縮,例如, 'head -c -1234567890 somelog.log |頭-c 999999 | gzip -c3> someextract.log.gz'使用適度的壓縮因子,如果你壓縮。你真的只是保存I/O。 – TerryE 2012-03-11 14:28:19

1

7〜10 GB是一個大的數據量。如果我必須分析這類數據,我會將應用程序記錄到數據庫或將日誌文件上傳到數據庫。然後在數據庫上可以高效地進行大量的分析。如果你正在使用像Log4J這樣的標準日誌記錄工具,那麼記錄數據庫應該很簡單。只是建議一個替代解決方案

更多關於數據庫日誌,你可以參考這篇文章:

A good database log appender for Java?

2

5 GB的日誌解析約25分鐘

正是〜3MB /秒。即使在Python can do much better (~500MB/s for wc-l.py)中依次掃描O(n)掃描,即性能應該僅受I/O限制。

要執行你能適應FileSearcher使用固定的記錄用線而不是一個文件的二進制搜索,使用類似 tail -n implementation in Python(這是O(n)掃描'\n')的方法。

要避免O(n)(如果日期範圍只選擇日誌的一小部分),您可以使用大量的固定塊的近似搜索,並允許某些記錄由於它們位於塊邊界而被遺漏,例如,使用未修改FileSearcherrecord_size=1MB和一個自定義Query類:

class Query(object): 

    def __init__(self, query): 
     self.query = query # e.g., '2012-01-01' 

    def __lt__(self, chunk): 
     # assume line starts with a date; find the start of line 
     i = chunk.find('\n') 
     # assert '\n' in chunk and len(chunk) > (len(self.query) + i) 
     # e.g., '2012-01-01' < '2012-03-01' 
     return self.query < chunk[i+1:i+1+len(self.query)] 

要考慮到該日期範圍可以跨越多塊,你可以修改FileSearcher.__getitem__返回(filepos, chunk)和搜索兩次(bisect_left()bisect_right())找到近似filepos_mindatefilepos_maxdate。之後,您可以在給定文件位置周圍執行線性搜索(例如,使用tail -n方法)以查找確切的第一個和最後一個日誌記錄。

0

如果您有權訪問Windows環境,則可以使用MS LogParser來讀取文件並收集您可能需要的任何信息。它使用一種SQL語法,使得使用這個工具成爲一種樂趣。它也支持大量的輸入類型。

作爲額外的好處,它還支持iCheckPoint開關,它可以在使用順序日誌文件時創建檢查點文件。欲瞭解更多信息,請查看日誌解析幫助下的 「高級功能 - >解析輸入增量」

參見: