2008年8月30日

何かを見つけた、その後

おっとっと、忘れてた。

仮に、問題のポイントと相関が高いカウンターが見つかったとしよう。それで終わってくれるならば特にどうと言うことは無いだろうが、普通は
「で、それは何をどうしているって??」
と言うことを確認しなくてはいけない。もし障害分析を仕事でやっている場合ならばお客様には
「r2が 0.43 なのでこいつが問題点です」
という1行よりはもう少し親切な…そう例えばグラフ化するとか…してあげなくてはいけない。

そこで、自分が「これ」と決めたカウンターに関して、Excelシートを作る事を考えよう。

もちろん、対象となるカウンターがもう決まっているのだから、relog.exe に対象カウンター一覧を食わせて TSV ファイルを作り、それをそのまま Excel に飲ませるのだってOKだ。
ただ、このやり方だと、例えば対象となる Performance Monitor のログが30個あると、30回「新しくシートを作って、そこにTSVファイルを読み込ませて」を繰り返さなくてはいけない。はっきり言おう。私は嫌だ。

と言うわけで。Excelシートを作ってくれるスクリプトだ。

toxls.pl

#!/usr/bin/perl
# Output xls file name is described as ARGV[0];
# ARGV[1] and on should contian
# ( ARGV[odd], ARGV[odd+1] ) = ( TSV filename, sheet name ).
# And at least ARGV[1] and ARGV[2] should be given in order to have something meaningful.
# Or, you'll get empty Excel file.


use Spreadsheet::WriteExcel;
use Spreadsheet::WriteExcel::Utility;

$workbookname = shift @ARGV;
if ( !defined( $workbookname ) ) {
die "Excel WorkBook name not given\n";
}

my $workbook = Spreadsheet::WriteExcel->new($workbookname);


@sheetnames = @ARGV;

while( $sheetfilename = shift @ARGV ) {
$sheetname = shift@ARGV;
if ( !defined( $sheetname )) {
die "Excel sheetname not given for TSV file name: $sheetfilename\n";
}

readsheet( $sheetname, $sheetfilename );
}

$workbook->close();
exit 0;


sub readsheet {
my ( $sheetname, $sheetfilename ) = @_;

my $worksheet = $workbook->add_worksheet( $sheetname );
open SHEETFILE, $sheetfilename or die "unable to open filename: $sheetfilename\n";


my $linenumber = 0;
my $offset = 5;
my $maxcells = 0;

while () {
chomp;

my @cells = split /\t/;

if ( $maxcells < $#cells ) {
$maxcells = $#cells;
}

if ( $linenumber == 0 ) {
$column = $linenumber;
} else {
$column = $linenumber + $offset;
}

for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ /^\"(.+)\"/;
my $tmp = $1;
$worksheet->write( $column, $i, $tmp );
}
$linenumber++;
}

$maxlinenum = $linenumber + $offset;

for ( $row = 0; $row <= $maxcells; $row++ ) {

# Since we need to skip first element, headofdata requires to have +1.
my $headofdata = $offset+1 +1;
my $tailofdata = $maxlinenum - 1;

my $ydatatop = xl_rowcol_to_cell( $headofdata, $row );
my $ydatatail = xl_rowcol_to_cell( $tailofdata, $row );

my $xdatatop = xl_rowcol_to_cell( $headofdata, 1 );
my $xdatatail = xl_rowcol_to_cell( $tailofdata, 1 );


my $formulacol = 1;
my $formularow = $row;
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'R2' );
} elsif ( $row == 1 ) {
$worksheet->write_formula( $formulacol, $formularow, "=RSQ( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
} else {
$worksheet->write_formula( $formulacol, $formularow, "=RSQ( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
}


$formulacol = 2;
$formularow = $row;
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'a' );
} elsif ( $row == 1 ) {
# do nothing
} else {
$worksheet->write_formula( $formulacol, $formularow, "=SLOPE( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
}


$formulacol = 3;
$formularow = $row;
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'b' );
} elsif ( $row == 1 ) {
# do nothing
} else {
$worksheet->write_formula( $formulacol, $formularow, "=INTERCEPT( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
}


$formulacol = 4;
$formularow = $row;
my $r2 = xl_rowcol_to_cell( 1, $formularow );
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'High Relation' );
} elsif ( $row == 1 ) {
# do nothing
} else {
$worksheet->write_formula( $formulacol, $formularow, "=IF( $r2>=0.25,\"*\",\"\")" );
}
}

close SHEETFILE;
}


ヘルプも何もありゃしない :p 使い方としてはこんな感じ:


% toxls.pl AFO.xls A.TSV A-dayo B.TSV B-desu


するってーと AFO.xls が作られる。開くと見えるのはこんな感じだ。



タブを見ると「A-dayo」「B-desu」というのがあるのが判るだろう。それぞれ A.TSV, B.TSVの中身がシートになった状態になっている。

こいつにはもう一つおまけの機能がある。2行目から4行目がそれぞれ「r2」「y=ax+bのa」「y=ax+bのb」になっている。これらは Perlで算出したものではなく、Excelの「式」が書いてあってあなたがこのExcelシートを開いたときに値を再計算してくれた状態なのだ。

5行目には「もしr2の値が0.25より大きかったら * を、そうでなければ何も表示しない(エラーの場合を除く)」という式が埋めてある。単なる目印のようなものだが、たまに便利だったりする。

2008年8月26日

15個に分類するには

sort.pl が出してくる3つのファイルを、マシン2台分用意してAとBの邂逅の図1のように分類するのに必要なツールは、実は簡単なものでいい。

今回はcommon.plとonlyFirst.plという2つのプログラムを作り、それを使うことにしよう。
common.plは2つのファイルを受け取り、その両方に存在するカウンター名を出力する。
onlyFirst.plは同じく2つのファイルを受け取るが、最初のファイルにあって2つ目のファイルには存在しないカウンター名を出力する。

「diffコマンドじゃ駄目なのか?」
という声が聞こえてきそうだが、実は駄目だ。diffは厳密な差分を作るわけではない。あまりにも複雑な差分が出てくると、その近辺をまとめて「えいやっ」と全部違うことにしてしまう。パッチを作るのには便利だが、本当の意味で差分を知りたいときには役に立たないのだ。




おっとっと。sort.pl の出力例を示そう。

"\System\Threads"
# "-0.22995779821056035196" "0.05288058895784879381" "-0.07730117409966560384" "143.68969576542572045539"
# "-0.51157343991699204758" "0.26170738442850427247" "-0.28398973957007304341" "268.04879224402808287292"
# "-0.54019071369820630440" "0.29180600716577749228" "-0.44072471855335868898" "364.09672304650726120687"
# "-0.55452955075971981685" "0.30750302266577667691" "-0.35677693634035859957" "315.68199698065233504727"
# "-0.56238435856552132237" "0.31627616675915285595" "-0.25723384667083141975" "251.10319887283403001070"
"\System\System Up Time"
# "-0.01694272285973713476" "0.00028705585790185927" "-0.00002142521429336236" "319.75744872321046887814"
# "-0.10027028487321032493" "0.01005413002855475131" "-0.00006496869460687431" "814.02214841113016417319"
# "-0.15561543326229096847" "0.02421616306941053433" "-0.00009270518792943096" "1140.66520449120347153620"
# "0.02950515281233018273" "0.00087055404247895569" "0.00002478604557354800" "-190.27570068419384733538"
# "0.64118189188208415717" "0.41111421847748865761" "0.00006133456719050028" "-615.26230486597317302122"
"\System\System Calls/sec"
# "-0.18729246211596788708" "0.03507846636546126618" "-0.00055434531116103602" "100.21166379708057100480"
# "-0.29232961498316379384" "0.08545660379620478167" "-0.00111680437443534113" "93.05619900302319881495"
# "-0.34131479803199158732" "0.11649579135561920833" "-0.00090462856546019161" "90.75926531940870958778"
# "-0.34750948339695522578" "0.12076284105081869973" "-0.00149413414952154013" "87.55393220941927354234"
# "-0.51114894911397353620" "0.26127324818031950750" "-0.00296996323726849211" "104.08031976970305210654"


# で始まっている行はコメントだ。この行はデバッグ用に各カウンターから算出されるr, r2, a, b の4つの値を記録しているだけで、この段階では何の意味も無い。これは処理の前に grep -v で消してしまおう。


% cd $TOP
% cd $TOP/06rmComment
% cat filter.sh
#!/bin/sh

for i in ../05sort/*.{HI,BORDER,LOW}; do
o=`echo $i | sed 's/\.\.\/05sort\///g'`
echo $o
egrep -v '^#' $i | sort | uniq > $o
done
% ./filter.sh

sort はデバッグしやすくするためだが(アルファベット順に並んでいると間違ってるかどうか探しやすい)、uniq は「ついカッとなってやった」の類だ。無くても構わない。




では。まず2つのファイルに共通しているカウンター名を引っ張り出すスクリプト。

common.pl

#! /usr/bin/perl

# common.pl
# read and , line by line.
# if common lines were found regardless of it's order, output it to STDOUT.

$inAfn = $ARGV[0];
$inBfn = $ARGV[1];

open( INA, $inAfn ) or die "can't open file $inAfn as read\n";
open( INB, $inBfn ) or die "can't open file $inBfn as read\n";

while () {
chomp;
push @inAline, ( $_ );
}

while () {
chomp;
push @inBline, ( $_ );
}

close INA;
close INB;


while ( $line = pop @inAline ) {
# find same line in @inBline;
for ( $i = 0; $i <= $#inBline; $i++ ) {
if ( $inBline[$i] eq $line ) {
print "$line\n";
}
}
}

えぇ、自分で言うのもなんだが、すげぇ馬鹿コードである。共通要素を見つけた後、next で次に行かないのもどうだかと思うし、inBline から見つかった要素を消さないのもどうだかと思う。

次は片一方にしかないカウンターを見つけるスクリプト。

onlyFirst.pl

#! /usr/bin/perl

# onlyFirst.pl
# read and , line by line.
# if line from does not exist in line from then print that to STDOUT.

$inAfn = $ARGV[0];
$inBfn = $ARGV[1];

open( INA, $inAfn ) or die "can't open file $inAfn as read\n";
open( INB, $inBfn ) or die "can't open file $inBfn as read\n";

while () {
chomp;
push @inAline, ( $_ );
}

while () {
chomp;
push @inBline, ( $_ );
}

close INA;
close INB;

A_line:

while ( $line = pop @inAline ) {
# find same line in @inBline;
for ( $i = 0; $i <= $#inBline; $i++ ) {
if ( $inBline[$i] eq $line ) {
next A_line;
}
}
print "$line\n";
}

ふ。負けず劣らずこちらも馬鹿コードである orz




% cd $TOP
% cd $TOP/07common
% cat filter.sh
##############################################
# re-classify into matrix
#
# B \ A | | | |
# \ | HIGH | BORDER | LOW |
#------------+------+--------+-----+
# HIGH | HH | HB | HL |
#------------+------+--------+-----+
# BORDER | BH | BB | BL |
#------------+------+--------+-----+
# LOW | LH | LB | LL |
#------------+------+--------+-----+

./common.pl ../06rmComment/A.HI ../06rmComment/B.HI > HH
./common.pl ../06rmComment/A.HI ../06rmComment/B.BORDER > BH
./common.pl ../06rmComment/A.HI ../06rmComment/B.LOW > LH

./common.pl ../06rmComment/A.BORDER ../06rmComment/B.HI > HB
./common.pl ../06rmComment/A.BORDER ../06rmComment/B.BORDER > BB
./common.pl ../06rmComment/A.BORDER ../06rmComment/B.LOW > LB

./common.pl ../06rmComment/A.LOW ../06rmComment/B.HI > HL
./common.pl ../06rmComment/A.LOW ../06rmComment/B.BORDER > BL
./common.pl ../06rmComment/A.LOW ../06rmComment/B.LOW > LL

for i in HI BORDER LOW; do
./onlyFirst.pl ../06rmComment/A.$i ../06rmComment/B.HI > ./tmp1
./onlyFirst.pl ./tmp1 ../06rmComment/B.BORDER > ./tmp2
./onlyFirst.pl ./tmp2 ../06rmComment/B.LOW > ./onlyA.$i
done

for i in HI BORDER LOW; do
./onlyFirst.pl ../06rmComment/B.$i ../06rmComment/A.HI > ./tmp1
./onlyFirst.pl ./tmp1 ../06rmComment/A.BORDER > ./tmp2
./onlyFirst.pl ./tmp2 ../06rmComment/A.LOW > ./onlyB.$i
done

% ./filter.sh


C,Dの処理は省略した。

はっきり言ってこれでもかと言うぐらいの力押しスクリプトだ。よい子はまねをしてはいけない(ぇ…)。



というわけで、これで機械的に出来る分類は終わった。
ココから先は、人間の推理力がモノを言う。

実際、ここにある perl のスクリプトはほぼそのまま私が使っているスクリプトだ。シェルスクリプトの方は時々に応じて変更しているのでこれこのままではないが。で、今まで発見できなかった要因をいろいろ見つけることが出来ている。

性能障害問題で悩んでいるなら、きっと役に立つと思う。少なくとも同じ考え方は使えるはずだ。

2008年8月24日

AとBの邂逅

ここまでは、各マシン毎の分類だった。ここからはマシンごとの特徴を見た上での分類が必要になる。要点としては図1のように分類する事になる。

 A only HIA only BORDERA only LOW
Aマシンのカウンター
B only HIBマシンのカウンター(A,B)=(HI,HI)(A,B)=(BORDER,HI)(A,B)=(LOW,HI)
B only BORDER(A,B)=(HI,BORDER)(A,B)=(BORDER,BORDER)(A,B)=(LOW,BORDER)
B only LOW(A,B)=(HI,LOW)(A,B)=(BORDER,LOW)(A,B)=(LOW,LOW)
図1. AとBの分類


まず最も判りやすいところから説明しよう。中央の紫色の3x3=9枡目は、AとBの両方で存在した(NaNによる除去を免れた)カウンターだ。A, B それぞれにおいて、どう分類されていたのか、に応じて3x3通りが存在する。

薄い黄色で塗られた3つの枡目、「A only xxxx」とあるのは『Aには存在したがBには存在しなかった』カウンターを現す。同様に「B only xxxx」とある水色の3つの升目は『Bには存在したが、Aには存在しなかった』カウンターを現す。

生き残ったカウンターは以上15個の升目のどこかに必ず入る。まずこの分類を行う。

さて、これらの升目の内、重要視するのは図2で示した6枡である。


 A only HIA only BORDERA only LOW
Aマシンのカウンター
B only HIBマシンのカウンター(A,B)=(HI,HI)(A,B)=(BORDER,HI)(A,B)=(LOW,HI)
B only BORDER(A,B)=(HI,BORDER)(A,B)=(BORDER,BORDER)(A,B)=(LOW,BORDER)
B only LOW(A,B)=(HI,LOW)(A,B)=(BORDER,LOW)(A,B)=(LOW,LOW)
図2. 重要視するポイント


症状はAでは発生しているが、Bでは発生していない。そして問題が生じているときにはプロセッサが忙しく働いている。ならば、「AのときはIdleと強い相関があり、Bの時にはIdleとの相関が弱い」所に探すべきネタがある。

Aにしか存在しないカウンターの内、相関がLOWのもの以外の2つは、当然検討対象にしなくてはいけない。これは判ると思う。逆にBにしか存在しないカウンターは無視して構わない。

A,Bともに存在したカウンターの内、Aにおいて相関がLOWだったものは、この場合検討対象から外してよい。

Bにおいて相関がHIだったものも、この場合検討から外してよいはずだ。AもHI,BもHIならそれは「いつでも相関が高いもの」…例えば、プロセッサ消費量の内の User の割合など…であることが考えられる。また、AがHIで、BがHIではないのでは明らかに今回探している対象から外れるのは自明だ。

残った6枡の内、A,Bともに BORDERなものは特別扱いが必要になる。BORDERにあるのは計測すると相関が高かったり高く無かったりしたものだ。つまり「AもBも、相関が高いんだか低いんだかよく判らん」ものだったりする。この場合、各カウンターの内容を調べて、個別に判断する必要が出る。しかし、逆に言うとここは手間がかかるところなので、それ以外の部分で何か見つかった場合は無視して構わない、ともいえる。

と言うわけで、残り5枡を最優先で調べるべし、と言うことになる。

2008年8月16日

sort.pl の出力

先に進む前に sort.pl の出力についてもう少し説明する。

sort.pl が入力として受け付ける(前提としている)のは、calcr2.pl の出力を複数 cat でつないだもの。今回は sort もかけることで同じカウンターが近接するようにしてあるが、本質的には sort の必要は無い。結果として、このような内容が延々続くファイルになる:


"\LogicalDisk(C:)\% Disk Read Time" "-0.04062578806352645403" "0.00165045465578256851" "-0.12770872885354865610" "90.45804378385737658023"
"\LogicalDisk(C:)\% Disk Read Time" "-0.04406606083496136481" "0.00194181771751051590" "-0.17957809895894711362" "90.36202077972433047033"
"\LogicalDisk(C:)\% Disk Read Time" "-0.10162240553099880448" "0.01032711330590677641" "-0.25960115664153218127" "91.91528872471089858070"
"\LogicalDisk(C:)\% Disk Read Time" "-0.10606592840069039460" "0.01124998116750038125" "-0.50117255489344281550" "89.78700245921672272973"
"\LogicalDisk(C:)\% Disk Read Time" "-0.63225077753384162361" "0.39974104569214729193" "-0.98409431769226710436" "99.62442055641675669955"
"\LogicalDisk(C:)\% Disk Time" "-0.04336533750022928025" "0.00188055249650879188" "-0.11305679326692022027" "90.53746144831214471477"
:


r2の値を使ってこれらをソートしていくわけだが、全ての計測結果においてr2が同じ性質を持つわけではない。実はここでは4種類に分類している。出力しているファイルは3種類だが、実際には分類は4種類あるのだ。

そこで、まず個々のr2の値はどのような値をとりえるのか、考えて見よう。

r2のとる値


すでに述べたが、r2は0.0から1.0までの間の値を取る。正確には計算できる場合は、それらの間の値をとる。そう。計算できない場合、と言うのがあるのだ。

指定したカウンターに値が全く記録されていない場合がありえる。この場合、データが全くないので、回帰直線も引けない。つまり y=ax+b という形のaとbも求められない。rの値は a と b がないと計算できないのでrも求められないし、その二乗値も求められない。
この場合、r, r2, a, b は全て Not a Number (NaN) となる。

rやr2が NaN になるケースがもう一つある。a が 0.0 になった場合だ。厳密には一定の誤差の範囲内であれば NaN になってしまう。rの計算式には a による除算が含まれているのだが、ここで 0割り(0を分母とする分数の値を計算しようとした時に生じるエラー)が起こるのだ。この場合、r, r2は NaN になるが、a は 0.0 になるし、bはどんな値になるかはデータ依存だ。

以上の2つのケース以外の場合、rの値は 0.0 から 1.0 の間の値を取る。厳密には、計算誤差のせいで 1.0 を超える数字が出たり、0.0を下回る数字が出たりすることもあるが、あくまでも誤差の範囲、と捉えてもらって大丈夫だ。

r2の値による分類


さて、今回、sort.pl でカウンターを分類するにあたって次のような規則を適用した。


1) 同じカウンターに属する、全てのr2の値が NaN ならば、そのカウンターは記録しない。

データが一切ないのであれば、そのカウンターは考慮に値しない。

データがあるが a が 0.0 になる、と言うことは Processor Idle の値に全く影響を受けてない/影響を与えていない、と言うことだ。今回は影響のあるものを探しているのだから、そのカウンターについて、以降考慮する必要は無い。
Process ID のように変わるはずの無いカウンターが該当する。

2) 同じカウンターの一部だけが NaN の場合、そのNaNは無視する。

取りあえず残りのデータだけで分析しよう、と言うことだ。

3) 全ての r2が境界値以上だったらHI

4) 全ての r2が境界値未満だったらLOW

5) r2が境界値以上、未満の両方存在したら、BORDER



なので、全てNaNの場合のカウンターがこの段階で消えている。カウンターの総数は分類前と分類後では、分類後の方が少ない可能性がある、ということだ。

2008年8月12日

相関係数によってカウンターを分類する

カウンターごとに相関が求まったので、それを元にカウンターを分類する。

その前に。カウンターにくっついているマシン名がうざったい。

"\\A\Processor(_Total)\% Idle Time" "0.99999999999999558689" ....


というこの「\\A\」がうざったいのだ。と言うのは、後で「マシン同士の」相関を取る必要があるからだ。カウンター名にマシン名が含まれていては「同じカウンター同士」を判別するのが面倒になる。だからこの場で外してしまおう。

そしてついでに、同じマシンに関して、別の日付のものをあわせて1つにしてしまおう。


次に。各TSVファイルを分類する。分類するスクリプトはこれ:

sort.pl
$r2_limitation = 0.25;

open( INFILE, "$ARGV[0]" ) or die "can't open file $ARGV[0] as read\n";
open( OHI, " > $ARGV[1]" ) or die "can't open file $ARGV[1] as write\n";
open( OBO, " > $ARGV[2]" ) or die "can't open file $ARGV[2] as write\n";
open( OLO, " > $ARGV[3]" ) or die "can't open file $ARGV[3] as write\n";

while () {
chomp;
my @cells = split /\t/;
my $i, $n;

for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ s/"(.*)".*$/\1/g;
}

$n = $cells[0];
if ( $n eq "name" ) {
next;
}
if ( $counter{$n} <= 0 ) {
push @name, $n;
}

$i = $counter{$n};
$counter{$n}++;
$r{$n}[$i] = $cells[1];
$r2{$n}[$i] = $cells[2];
$a{$n}[$i] = $cells[3];
$b{$n}[$i] = $cells[4];
}

while ( $n = pop @name ) {
my $allbig = 1;
my $allsmall = 1;
my $findNaN = 0;

for ( $i = 0; $i < $counter{$n}; $i++ ) {
if ( $r2{$n}[$i] < $r2_limitation ) {
$allbig = 0;
}
if ( $r2{$n}[$i] >= $r2_limitation ) {
$allsmall = 0;
}
if ( $r2{$n}[$i] eq "NaN" ) {
$findNaN = 1;
}
}

if (( $allbig )&&( !$allsmall )) {
print OHI "\"$n\"\n";
for ( $i = 0; $i < $counter{$n}; $i++ ) {
print OHI "# \"$r{$n}[$i]\"\t\"$r2{$n}[$i]\"\t\"$a{$n}[$i]\"\t\"$b{$n}[$i]\"\n";
}
}

if (( !$allbig )&&( $allsmall )) {
print OLO "\"$n\"\n";
for ( $i = 0; $i < $counter{$n}; $i++ ) {
print OLO "# \"$r{$n}[$i]\"\t\"$r2{$n}[$i]\"\t\"$a{$n}[$i]\"\t\"$b{$n}[$i]\"\n";
}
}

if (( !$allbig )&&( !$allsmall )) {
print OBO "\"$n\"\n";
for ( $i = 0; $i < $counter{$n}; $i++ ) {
print OBO "# \"$r{$n}[$i]\"\t\"$r2{$n}[$i]\"\t\"$a{$n}[$i]\"\t\"$b{$n}[$i]\"\n";
}
}
if (( $allbig )&&( $allsmall )) {
if ( $findNaN ) {
print STDERR "\"$n\" seems to have NaN as it's r2 value\n";
} else {
die "\"$n\" did not have any element in $ARGV[0]?!\n";
}
}
}


引数は4つ。
1つ目が入力TSVファイル名。
2つ目は $r2_limitation 以上のr2値を持っているカウンターだけを集めたTSVファイルの名前。
4つ目は $r2_limitation 未満のr2値を持っているカウンターだけを集めたTSVファイルの名前。
3つ目は r2の値が $r2_limitation 以上だったり以下だったりふらつくものを集めたTSVファイルの名前。

とりあえず $r2_limitation はSTATISTICS HACKの推奨にあわせて「強い相関」の値0.25に設定してあります。

% cd $TOP
% cd $TOP/05sort
% cat filter.sh
#!/bin/sh

onebefore="../04Colleration";
for i in A B C D; do
cat $onebefore/$i.*.tsv | sed -e 's/^"\\\\[[:upper:][:digit:]]\+\\/"\\/g' | sort > $i.lst
done

for i in A B C D; do
./sort.pl $i.lst $i.HI $i.BORDER $i.LOW
done
% bash filter.sh


これで A.HI, A.BORDER, A.LOW や B.HI, B.BORDER, B.LOW ができた。

一番計算機パワーを食うところはこれでおしまい。ここから先は人間が悩むしかない…。

2008年8月11日

じゃぁ、相関とかを求めるかね。

全部のマシン、全部の日付、全部のエントリについて、"Processor(_Total)\% Idle Time" との相関 r ならびに r2, 回帰式 y=ax+b にマップしたときの a, b を求めよう。あ、x 側が"Processor(_Total)\% Idle Time"で、yが「相関を求めるべき他のエントリ」な。

Perlにはこの手の統計処理用のモジュールが結構ある。どれを使うか迷う所だが、今回は深く考えずにMath::NumberCruncherを使ってみよう。

もし、cygwinを使っているならば、次の命令を実行し、尋ねてくる質問に真摯に答えればインストールは完了する。
% perl -MCPAN -e 'install Math::NumberCruncher'

Active Perlのときはどうするのか?とかそういう質問の答は判らないのでよろしく。

http://search.cpan.org/~sifukurt/Math-NumberCruncher-5.00/NumberCruncher.pod
に一応説明らしきものがある事になっているが、はっきり言って説明になっていない。2引数を取る場合、どちらかがxでどちらかがy…という問題を解かなきゃいけないはずだが、それもない。強引に超能力をかけることにする。多分、こういうこったろう。

$correlation = Math::NumberCruncher::Correlation(\@yarray,\@xarray);

($slope,$y_intercept ) = Math::NumberCruncher::BestFit(\@yarray,\@xarray);


ここで、$correlation は「相関係数」つまり、今まで言っていた r の事だ。$slope (傾き)と $y_intercept (y切片) は、それぞれ a と b になる。r2を直接求める関数はないので、$r2=$r*$r; で求める事になる。




入力TSVファイルは全部次のようなフォーマットになっている。
"(PDH-TSV 4.0) (Tokyo Standard Time)(-540)" "\\A\Processor(_Total)\% Idle Time" "\\A\LogicalDisk(C:)\% Disk Read Time" ........
"07/15/2008 07:59:59.810" "0.079295186091091777" "0.00050253600663232288" .......
"07/15/2008 08:00:59.809" "98.125628004019234" "0.10616734613768194" .......
........


第一行がラベルで、各ラベルは ダブルクォート(") で囲まれている。さらに、それらはタブで分割されている。第2行以降は測定結果だ。

各行の最初は「測定時刻」だ。従って、相関を求めるべき一方の「Idle」は2つ目の要素になる。

ダブルクォートの中にはタブはないので、とりあえずタブで要素を分割すればいいだろう。その後、ダブルクォートを各要素から取り除けばよい。これを2次元配列にぶちこんだあと、2つ目の要素列と3つ目以降の各要素列との相関を求める。




各要素の出力は次のような形とする。
"name" "r" "r2" "slope" "y intercept"
"\\A\Processor(_Total)\% Idle Time" "0.99999999999999558689" "0.99999999999999117378" "0.99999999999995984122" "0.00000000000341678664"
"\\A\LogicalDisk(C:)\% Disk Read Time" "-0.08658618963563029212" "0.00749716823561733062" "-0.36956562874045379176" "85.25898260145136281611"
.......

第1行は各列のラベルだ。で、2行目以降は「相関を取った列のラベル」、相関係数 "r"、rの二乗値 "r2"、傾き "a"、y切片 "b"の順になる。1行毎に相関を取る相手が変わる。

ある TSVファイルから、この出力を求める perl スクリプトは次の通りだ。

calcr2.pl

use Math::NumberCruncher;
$ref = Math::NumberCruncher->new();

my $linenumber = 0;
my $maxcells = 0;

# read data.
while () {
chomp;

my @cells = split /\t/;

if ( $maxcells < $#cells ) {
$maxcells = $#cells;
}

if ( $linenumber == 0 ) {
for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ /^\"(.+)\"/;

my $tmp = $1;
$countername[$i] = $tmp;
}
} elsif ( $linenumber == 1 ) {
# skip this one!
# first taken data is useless.
} else {
for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ /^\"(.+)\"/;
my $tmp = $1;

# $linenumber == 2 got to be $index == 0.
my $index = $linenumber - 2;

$num[$i][$index] = $tmp;
}
}

$linenumber++;
}

# output the result
print "\"name\"\t\"r\"\t\"r2\"\t\"slope\"\t\"y intercept\"\n";

for ( $i = 1; $i <= $maxcells; $i++ ) {

$xname = $countername[1];
$yname = $countername[$i];
@xarray = @{$num[1]};
@yarray = @{$num[$i]};

$r = Math::NumberCruncher::Correlation( \@yarray, \@xarray );
($a, $b)
= Math::NumberCruncher::BestFit( \@yarray, \@xarray );
$r2 = $r * $r;

print "\"$yname\"\t\"$r\"\t\"$r2\"\t\"$a\"\t\"$b\"\n";
}


いくつかポイントがある。

入力は標準入力。出力は標準出力だ。

データ行になって最初の行はわざと捨てている。これは、この手の記録の多くが「前回取得したデータとの差分」から求められる事が多いからだ。1つ前のデータがない、最初の一手は往々にして間違ったデータである事が多い。故にそれは取り除く。

@xarray, @yarray をコピーしているが、これは単に「配列の配列」から「配列(2つ目の方)」を持ってくる方法を完全に失念していたためだ。うまく動かない…と七転八倒するためにコピーをとったのに過ぎない。多分、ここには無駄があり、この無駄を省けば結構速くなるんじゃないかな、と思う。

こいつを、すでに作ってあるTSVファイルに対して適用する。ただし、出力結果もTSV。ややこしい事おびただしいが、作ったときは深く考えてなかった。カっとなってやった。今では反省している。

% cd $TOP
% cd $TOP/04Colleration
% cat filter.sh
for i in ../03TSV/*.tsv; do
of=`echo $i | sed 's/\.\.\/03TSV\///g'`;
echo $of
cat $i | ./calcr2.pl > $of
done


私の環境では12時間ほどかかりました。もっとメモリとプロセッサパワーが欲しいです。

2008年8月4日

一覧の出現順序をいじったら…TSV化だっ

一覧から
\\A\Processor(_Total)\% Idle Time

とかを切り出して、一番最初に出てくるようにする。

% cd $TOP
% cd $TOP/02LST
% cat filter.sh
for i in A B C D; do
cat ../01PQRlst/$i* | fgrep 'Processor(_Total)\% Idle Time' | sort | uniq > ./$i.lst
cat ../01PQRlst/$i* | fgrep -v 'Processor(_Total)\% Idle Time' | sort | uniq >> ./$i.other
done
% bash filter.sh

大事なのは Sort を掛けてある事と、各マシンについて日付をまたいでリストをマージしている事。

Sortをかけているのは、リストのエントリが「大雑把に何\細かく言うと何」という形式に従っているから、と言うのが一つ。似た項目が集まるように。もう一つは uniq をかけるため。




さて、とりあえずここまできたら、blg ファイルの中身を TSVファイルに変換しよう。
先ほど作った A.lst, B.lst を relog.exe に適用すれば、そこで指定した順番でカウンターが出力される。

% cd $TOP
% cd $TOP/03TSV
% cat filter.sh
for i in A B C D; do
for j in $TOP/native_blg/$i*.blg; do
outfile=`echo $j | sed -e 's/^.*\//g' -e 's/\.blg$/.tsv/g'`
relog.exe $j -f TSV -cf $TOP/02LST/$i.lst -o $outfile;
done
done
% bash filter.sh


これで $TOP/03TSV/ 下に欲しい順序で、各 blg ファイルの中身が TSV の形式で格納されたわけだ。ただし、3500列 480行もあるファイルが4個 x サーバ4種類の合計16個もあり、このTSVファイルを Excel に食わせようとすると、Excel側がギブアップするってぐらいのおチャメさんなファイルになったわけだが…

2008年8月3日

まずは一覧

どこか適当なディレクトリを $TOP と定義する。

$TOP/native_blg/

ディレクトリ下に、blgファイルが全部置いてある、としよう。そして、cygwin が「全部インストール」してあると仮定しよう。bash も使い放題だ。Perl もCPANモジュールが適宜インストールできる、とする。

% ls $TOP/native_blg/
A_Jul15.blg
A_Jul16.blg
A_Jul17.blg
A_Jul18.blg
B_Jul15.blg
B_Jul16.blg
B_Jul17.blg
B_Jul18.blg
:

[マシン名]_日付.blg というファイル名フォーマットらしい。

まず、最初にするべきなのは、各 blg ファイルに含まれているエントリを全部引き出す事だ。Printer情報だのProcess情報だのと、結構それぞれ異なっているはずなので、一覧を取り出して、不要なものをある程度削らなくてはいけないし、Excelに食わせられるように200個ぐらいづつに切り分けるための一覧表が必要になる。

relog.exe というプログラムが .blg ファイルを CSV や TSV (Tab Separated Values)フォーマットに変換してくれる。 これには -q というオプションがあって、このカウンタの一覧を引っ張り出してくれる:

% relog A_Jul15.blg -q -o A_Jul15.lst
% cat A_Jul15.lst
\\A\LogicalDisk(C:)\Avg. Disk Bytes/Write
\\A\LogicalDisk(E:)\Avg. Disk Bytes/Write
\\A\LogicalDisk(F:)\Avg. Disk Bytes/Write
\\A\LogicalDisk(_Total)\Avg. Disk Bytes/Write
\\A\LogicalDisk(C:)\% Idle Time
\\A\LogicalDisk(E:)\% Idle Time
\\A\LogicalDisk(F:)\% Idle Time
\\A\LogicalDisk(_Total)\% Idle Time
\\A\LogicalDisk(C:)\Disk Reads/sec
:


すでに後悔の念バリバリな分量が出てくる。うわ、必要なんだかなんだか判らないものだらけ…。中にこれが見つかるはずだ。

\\A\Processor(_Total)\% Idle Time


これがCPUがIdleになっている割合を示すカウンターだと思われる。\\A\の部分までは「マシンを指定する」ためのものなので、こいつの出力と、その他の出力とのr2を求めればいいわけだ。

とりあえず、全部のファイルについて、このカウンターリストを作る。

% cd $TOP
% mkdir 00nativelist
% pushd 00nativelist
% cat filter.sh
for i in ../native_blg/*.blg; do
j=`echo $i | sed 's/..\/native_blg\///g' | sed 's/\.blg$/.lst/g'`;
relog $i -q -o $j
done
% bash filter.sh
% popd

これで $TOP/00nativelist/ の下に、各 blg ファイルのカウンター一覧ファイルが出来た。今回は Printer Queue は関係なさそうなので、削り落とそう。

% cd $TOP
% mkdir 01PQRlst
% pushd 01PQRlst
% cat filter.sh
for i in ../00nativelst/*.lst; do
j=`echo $i | sed 's/..\/00nativelst\///g'`;
egrep -v '^\\\\\w+\\Print Queue.*\\' $i > $j;
done
% bash filter.sh
% popd


これで、不要な要素をカウンターリストから削る事ができた。

performance monitor の出力に辟易している話

Windowsには Performance Monitor と言うものがある。

システムの評価、できてますか?

身近な定量評価その2 - Windowsのperfmon.exeを使ってみる


なんかを見ると起動の仕方とか、出てくるグラフとかが判ると思う。Unixでいうtopと *stat シリーズを足して貧相なGUIを付け加えたような代物だ。基本的にシステムの異常を監視するときにはよいが、何が悪いのか知ろうというときにはあまり役に立たない

しかし、世界は広いというか物を知らない人は多いと言うか…
「システムの調子が悪いので見てください」
と言って、こいつが吐く .blg ファイルをゴッソリ送りつけてくるやからがいる。
「取りあえず何をとったらいいのか判らないので、全部とりましたから」
とか言いながら。そんな所にはヒントは無いと言うのに。

perfmonは「全部」取ったりすると 3000項目以上データを取得してくれる。1分に1回取得しても8時間分取得すると480回取得するわけだ。これだけのデータを perfmon の貧相なGUIで見て回るのははっきり言って不可能に近い。

このままではどうしようもないので、Excel にでも食わせて見るか…しかし MS-Office 2003 とかを使っていると、これもまた無理、と判る。Excel-2003は「1シート当たり、256列 65536行」しか受け付けないのだ。3000項目 × 480回のデータ取得ではどうあがいてもシートの中に納まりきらない。

そこで、だ。こういう場合にどうするか、と言うのが今回のお題。とは言っても、このままではノーヒント過ぎるのでヒントを1つもらう。

「調子が悪い、と言うのはどうやって判ったの?」
「あー、なんかCPUの利用率が天井に張り付いてるんですよ…」

なるほど。これで戦略は立った。CPUの利用率と相関が高い項目を見つけ出して、そいつらが何なのか調べればいい、と言うわけだ。

「あ、ちなみに。Aっていう場所では起こってるんですが、Bという場所では起こっていないんで…。両方とも perfmon の出力は取ってあります」

ふむ。ならば、AとBでそれぞれについてCPUの利用率との相関を取って、『Aでは相関が高いが、Bでは低いもの』を見つけ出せばいいって事になるな。



現実的には、『CPUの利用率』というのは「ユーザが利用している」場合と「kernelが利用している」場合の2通りがある。どちらが本命か判らない。こういう場合は CPUの非使用率 、つまり CPU Idle との相関を取ると良い。CPU Idle の値が低い場合に多く動くもの、あるいはリソースとして少なくなるものを見つければいい。

そういう時は R2 というものを取ればいい。あぁ、大丈夫。 R2というのは 0.0 から 1.0 の間の値を取り、『相関が高いほど値がでかくなる不思議な数字』とだけ覚えておけばいい。さらに言うと、注目するべきは 0.25 以上の値をとっているもので、なおかつ 1.0 ではないもの(1.0は直接的過ぎて、ちょっと怪しい)。

もっと詳しい事が知りたければこの本を見て欲しい。


R2 という値自体は、Excelの RSQ() という関数で求める事ができる。どうにかしてデータを Excel にぶち込み、各項目と CPU Idle 値との RSQ() 値を求め、その中から 0.25 以上の数字になっている項目を見つけ出せばいいのだ。

.....

そう。そうして問題が元に戻ってくる。perfmon の blg ファイルから、Excelに入る程度の小さなデータセットを作り出す方法、その中に必ず CPU Idle の値を入れておく方法、そしてできれば各項目との RSQ() 指定までは自動的にする方法…

なるべく苦労せず、ステップ バイ ステップで(つまりいつでも後戻りできる形で)実行するにはどうすればいいのか… その苦闘の歴史を綴ろう。

「tcpのいろは」は取りあえずあきらめますた

ちょっち出直しだ。

いや、描かなくちゃいけない図の多さに辟易しているうちに半年も経つとは…