2011年1月25日火曜日

topコマンドのログ解析(Python手習い) その2

その後OrderedDictというクラスがcollectionsモジュールにあることが判明したため、これを使ってもうちょっとマシなファイルハンドル操作を実装してみた。

ついでにファイルオープン時に致命的なバグがあったため、合わせて修正。

from collections import OrderedDict
from datetime import datetime, timedelta
from functools import reduce
from os.path import exists
from re import match, search, split
from sys import argv
from shutil import rmtree
from os import makedirs

MAX_FILE_HANDLE = 500
WORK_DIR = 'work'

if len(argv) != 3:
    print("\nUsage:\npython", argv[0], "logfile yyyy/mm/dd")
    exit()

rmtree(WORK_DIR, True)
if not exists(WORK_DIR):
    makedirs(WORK_DIR)

mydate = datetime.strptime(argv[2], '%Y/%m/%d')
mydateStr = mydate.strftime('%Y/%m/%d ')

try:
    f = open(argv[1])
    mem = open(WORK_DIR + "/mem.log", 'w')
    mem.write("time,mem av,mem used,mem free,mem shard,mem buff,mem actv, mem in_d,swap av,swap used,swap free,swap cached\n")
    processes = OrderedDict()
    maxtime = ""

    for line in f:
        line = line.strip()
        if match(r"^\d\d:\d\d:\d\d", line):
            if line[0:2] == "00" and timestamp[0:2] != "00":
                mydate = mydate + timedelta(1)
                mydateStr = mydate.strftime('%Y/%m/%d ')
            timestamp = line[0:8]
        elif match(r"^Mem", line):
            data = split(r"\s+", line)[1:9:2]
        elif match(r"^\d+k", line):
            data = data + split(r"\s+", line)[0:5:2]
        elif match(r"^Swap:", line):
            data = data + split(r"\s+", line)[1:8:2]
            prefix = mydateStr + timestamp
            result = reduce((lambda x,y: x + ',' + y.rstrip('k')), data, prefix)
            mem.write(result + "\n")
        elif search(r"java", line):
            data = split(r"\s+", line)
            pid = data[0]

            process = processes.get(pid)
            if process == None:
                while len(processes) >= MAX_FILE_HANDLE:
                    processes.popitem(last=False)[1].close()
                filename = WORK_DIR + "/Pid-" + pid + ".log"
                if exists(filename):
                    process = open(filename, 'a')
                else:
                    process = open(filename, 'w')
                    process.write("time,PID,USER,PRI,NI,SIZE,RSS,SHARE,STAT,%CPU,%MEM,TIME,CPU,COMMAND\n")
            else:
                del processes[pid]
            processes[pid] = process

            prefix = mydateStr + timestamp
            result = reduce((lambda x,y: x + ',' + y), data, prefix)
            process.write(result + "\n")
            if data[10] > maxtime:
                maxtime = data[10]
                maxpid = pid

    print("maxpid:", maxpid)

finally:
    if f != None:
        f.close()
    if mem != None:
        mem.close()
    map(lambda x: x.close(), processes)

キモは51~63行あたり。

OrderedDictは名が示すとおり、(key, value)セットの追加順序を記憶する辞書型である。ただし、一度追加した(key, value)は、valueの更新が行われても順序を入れ替えない。今回はLRUを実装しなければならないため、既に追加されている(key, value)にアクセスした場合は必ず一度削除してから再度追加するようにしている。

さらに、もし管理しているファイルハンドルがMAX_FILE_HANDLEを超えそうになったら、一番使われていないファイルハンドルを取得しクローズ処理を行なっている(54行目)。popitemメソッドは、引数がtrueの場合はLIFO、falseの場合はFIFOとして動作する。

前回バグっていたのを直したのは56~60行目あたり。前回はopen関数の第2引数を常に'w'と指定していたため、ファイルハンドルが増えたために一旦クローズしてしまったファイルについて、再度書きこみを行おうとした場合にクローズ前までのデータを消してしまっていた。オープン時にファイルが存在する場合は追記('a')するように修正。

150MB程度のログファイルの処理に、僕の非常に非力なマシン(PentiumM 1.2GHz 752MB RAM)で約2分。生成されるファイル数は4,000あまりというところ。

時間があったら、次はもう少しモジュール化してみたい。

2011年1月24日月曜日

topコマンドのログ解析(Python手習い)

topコマンドのログをExcelでグラフ化できるようにCSV形式に変換するPythonスクリプトを書いてみた。とりあえず動いたが、色々知らないまま書いているのでたぶんもっと良い書き方があるはず。

JavaのPID毎に別ファイルに出力するようにしたかったのだが、Windows上では500個ちょっとオープンしたところで"Too Many Open Files"エラーが出たため、500を超えたら一旦すべてクローズするように暫定対処した。できれば参照の少ないファイルから先にクローズしていくようにしたい。Javaで言うところのLinkedHashMapみたいなものは無いのだろうか。

スクリプト言語としてはずいぶん昔にPerlを触った以来だが、やっつけで書きやすい割にPerlよりは可読性が高いように感じる。

import sys
import re
from datetime import datetime, timedelta
from functools import reduce

if len(sys.argv) != 3 :
 print("\nUsage:\npython", sys.argv[0], "logfile yyyy/mm/dd")
 exit()

mydate = datetime.strptime(sys.argv[2], '%Y/%m/%d')
mydateStr = mydate.strftime('%Y/%m/%d ')

try :
 f = open(sys.argv[1])
 mem = open("mem.log", 'w')
 mem.write("time,mem av,mem used,mem free,mem shard,mem buff,mem actv, mem in_d,swap av,swap used,swap free,swap cached\n")
 processes = {}
 maxtime = ""

 for line in f :
  line = line.strip()
  if re.match(r"^\d\d:\d\d:\d\d", line) :
   if line[0:2] == "00" and timestamp[0:2] != "00" :
    mydate = mydate + timedelta(1)
    mydateStr = mydate.strftime('%Y/%m/%d ')
   timestamp = line[0:8]
  elif re.match(r"^Mem", line) :
   data = re.split(r"\s+", line)[1:9:2]
  elif re.match(r"^\d+k", line) :
   data = data + re.split(r"\s+", line)[0:5:2]
  elif re.match(r"^Swap:", line) :
   data = data + re.split(r"\s+", line)[1:8:2]
   prefix = mydateStr + timestamp
   result = reduce((lambda x,y: x + ',' + y.rstrip('k')), data, prefix)
   mem.write(result + "\n")
  elif re.search(r"java", line) :
   data = re.split(r"\s+", line)
   if data[0] not in processes :
    processes[data[0]] = open("Pid-" + data[0] + ".log", 'w')
    processes[data[0]].write("time,PID,USER,PRI,NI,SIZE,RSS,SHARE,STAT,%CPU,%MEM,TIME,CPU,COMMAND\n")
   prefix = mydateStr + timestamp
   result = reduce((lambda x,y: x + ',' + y), data, prefix)
   processes[data[0]].write(result + "\n")
   if data[10] > maxtime :
    maxtime = data[10]
    maxpid = data[0]

  if len(processes) > 500 :
   map(lambda x: x.close(), processes)
   processes = {}

 print("maxpid:", maxpid)

finally :
 if f != None :
  f.close()
 if mem != None :
  mem.close()
 map(lambda x: x.close(), processes)