2014年1月31日金曜日

サーバーの監視を楽しむ

結構前から、サーバー監視は自分で作ったものでやってたんです。好きなように表示したいし、欲しい項目も業務に依存したものもあるし。既存のツールを調べて使いこなす(そういうの得意じゃないっていうのもありますが)のも、どうかなー、と思って。


今は黒背景にしてるけど、気分で白にしたり、クリスマスバージョンにしたり。これをオフィスに大きなテレビに出してるんですよね。

で、もとになるデータをどうやって取得するのか、っていうのがいろいろなんですよ。
DBから業務データを集計したものだったり、単純にサーバーのCPUとかメモリとかだったり、サイトごとのリクエスト数、コネクション数とか。データセンターのスイッチのトラフィックとかもね。

エージェントベースのものでいけるのもあるし、それだと面倒だからWMIでとったり、SQLだったりさー。NewRelicも最近使いつつ(Server Monitor)。でも.NET Agentでのアプリケーションプロファイルは極悪なほどパフォーマンスが悪くなるので、テストの時だけね。あれ、常時運用で仕掛けてるひといるのかな。お金がたんまりあってはサーバーたくさんでハニーポット的に一部のサーバーだけロードバランさの割り当て率下げて、とかならやれなくはないかもだけどー。マルチテナントきりきりだとちょっとなー、と思うんす。そーでもないの?

で、いろんなところから取得したデータをMongoDBに入れて、画面に出すときには、MongoDBからとってきて表示。MVCアプリでjqPlotでレンダリング。
取得と保持と表示を分けてるのは、まぁ、ね。いろいろとりたいし、いろいろ出したいのと、取得したものをそのままの精度で保持しときたいから。MongoDBはしょぼいサーバーで動かしてるけど、がんばってくれるんす。
ちなみにこれとは別のMongoDBクラスタでトレース情報はとってるけど、そっちはアプリケーションログなんで、また別のモニターに出たりしてて。そっちはそっちで重要だけど、なんか、あんまり楽しくない。

ひょっとして、監視っていうのが需要の少ないものなのかもしれないけど、どうなんでしょうね。
まぁ、いいか。

WMIで対象データを抽出する場合は、WMIで取得したい項目もクラス名調べたりいろいろ面倒だけど、抽出自体はとても簡単で、コンソールからwmicっていうの取得するか、コードでSystem.Managementのクラス使ってちょこちょこ取得。
これのいい点は対象サーバーのCPUをほとんど使わないところ。必要なものだけしかとらないしね。辛い点は外部からの監視だから対象サーバーに到達しないとか、忙しくて無視されるとちゃんととれないところ。でも、そのときは前後の状態から人が目で見て判断できることがほとんどだから、そんなに気にしない。気になる人には向いてないですね。

CPU利用率を取得するならwmicの場合、コンソールに

wmic /node:"localhost" path Win32_PerfFormattedData_PerfOS_Processor where (Name="_Total")

って、書くと取れますね(localhostならnodeは指定しなくてもいいです)。ほかの項目もこの容量でなんでもとれるけど、プロセス起動になるので、対象サーバーが多いとちょっと辛いかも。

結果は↓。


テキストでとれるので適当にパースしましょう。とりたいフィールド名は指定できるので、必要ない項目は無視で。

System.Managementなら、↓こんな感じで。

ManagementObjectCollection response = null;
var query = "select * from Win32_PerfFormattedData_PerfOS_Processor where (Name=\"_Total\")";
Console.WriteLine(node.Path + " : " + query);

var serverExists = true;
try
{
var ping = new Ping();
var result = ping.Send(node.Path);
serverExists = result.Status == IPStatus.Success;
}
catch 
{
serverExists = false;
}

if (serverExists)
{
try
{
var scope = new ManagementScope("\\\\" + node.Path + "\\root\\cimv2");
var enumerationOptions = new EnumerationOptions
{
Timeout = TimeSpan.FromMilliseconds(_settings.ProcessTimeout)
};
var searcher = new ManagementObjectSearcher(scope, new ObjectQuery(query), enumerationOptions);
response = searcher.Get();
}
catch (Exception ex)
{
// ダメなら無視
Debug.WriteLine(ex.Message);
}
}

雰囲気ね。一度PING飛ばしてちゃんとレスポンスがあるのかどうか確認してるのはManagementObjectSearchはTimeoutを検出しないからデス。どんまい。
これだと、Taskで複数一度に回せて意外とさらさらー、って取り出せます。

続いて、ネットワーク機器の監視にみんな使ってるであろうMRTG。SNMP到達しない機器だろうと、MRTGは見れる、なんていう構成、ありますよね。ないですか。そうですか。あるんです!

その場合、MTRGが吐き出してるログファイルから値を取り出すと楽ちんですね。


あとは、たまに(設定間隔で)集計値が出力されるのでそれは無視するようにしましょう。そうすると、単なるHTTPでログの収集が簡単ポン。

public class MrtgLog
{
public DateTime DateTime { get; set; }
public long InBytesAverage { get; set; }
public long OutBytesAverage { get; set; }
public long InBytesMax { get; set; }
public long OutBytesMax { get; set; }
public long InBytesTotal { get; set; }
public long OutBytesTotal { get; set; }
}
public class MrtgLoader
{
public async Task GetLogFile(Uri uri)
{
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Range = new RangeHeaderValue(0, 1024);
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();

return content;
}

private static DateTime unixEpocTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public DateTime? UnixTimeToDateTime(string unixTime)
{
long longTime;
if (!long.TryParse(unixTime, out longTime))
return null;

return unixEpocTime.AddSeconds(long.Parse(unixTime));
}

private MrtgLog ParseLine(string line)
{
DateTime? dateTime = null;
var cols = line.Split(' ');

if (cols.Length > 0)
{
dateTime = UnixTimeToDateTime(cols[0]);
}
if (!dateTime.HasValue)
return null;

if (cols.Length == 3)
{
return new MrtgLog
{
DateTime = dateTime.Value,
InBytesTotal= long.Parse(cols[1]),
OutBytesTotal = long.Parse(cols[2]),
};
if (cols.Length == 5)
{
return new MrtgLog
{
DateTime = dateTime.Value,
InBytesAverage = long.Parse(cols[1]),
OutBytesAverage = long.Parse(cols[2]),
InBytesMax = long.Parse(cols[3]),
OutBytesMax = long.Parse(cols[4])
};
}

return null;
}

public IEnumerable ParseLog(string log)
{
if (string.IsNullOrWhiteSpace(log))
return Enumerable.Empty();

var logLines = log.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
return logLines.Select(ParseLine).Where(record=> record != null);
}
}

こんな感じで。雰囲気ね。
これはあれです、MRTGでとってるからと言って、MRTGじゃないと見れないのはなんかやだ、って人向けです。

後はNewRelicで収集してるMetricデータの取り出し。REST APIです。


ここに書いてるとおり!
だと、あれなので、curlで取得する場合にどう書きましょうか、っていうのをCPUを例に。これがなんかちょっとわかりにくかったんす。


Applicationじゃなくて、Serversに入ってる値が欲しいんだけど、その場合もagent_idを指定。で、agent_idっていうのは、あれで、"https://api.newrelic.com/api/v1/accounts/:account_id/servers.xml"からとれるidのことだってさ。

そしたら今度は対象Agentで取得できる項目一覧を確認。"https://api.newrelic.com/api/v1/agents/:agent_id/metrics.xml"ね。

まぁ、みんな同じOSなら、内容もたぶん同じだし、項目は最初に確認するときに見る感じで。そうするとCPUの利用率は"System/CPU/User/percent"と"System/CPU/System/percent"っていうのがわかりましたね。

じゃぁ、agent_idを指定してmetricsの値をとります。

"https://api.newrelic.com/api/v1/accounts/:account_id/agents/:agent_id/data.xml?begin=2014-01-29T03:51:00Z&end=2014-01-29T03:52:00Z&metrics[]=System/CPU/User/percent&metrics[]=System/CPU/System/percent&metrics[]=System/Memory/Used/bytes&metrics[]=System/PhysicalMemory/Installed[bytes]&field=average_value"

対象期間を指定して、とりたいmetrics名指定して、fieldはaverage_valueで。
これで、とれるんだけどー、これだとサーバーごとにリクエスト投げなきゃいけないんすよね。んー、いいんだけと、やりすぎは怒られそうじゃないすか。そもそもagent_id複数指定できる的なこと書いてるし(fieldは1個ずつです)。

結果的にAPIが違ったっていう話で。
"https://api.newrelic.com/api/v1/accounts/:account_id/metrics/data.xml?agent_id[]=:agent1&agent_id[]=3agent2&begin=2014-01-29T03:51:00Z&end=2014-01-29T03:52:00Z&metrics[]=System/CPU/User/percent&metrics[]=System/CPU/System/percent&metrics[]=System/Memory/Used/bytes&metrics[]=System/PhysicalMemory/Installed[bytes]&field=average_value"

これなら、一度に全部のサーバーの値が取れますね。
あとは、こうやって集めたデータを、好きなように加工して、好きなように表示するだけで、なんかいい感じに監視できて楽しいです。

楽しいか...?