連載
» 2009年03月25日 00時00分 公開

SystemTapで真犯人を捕まえろ!Linuxトラブルシューティング探偵団 番外編(3)(2/4 ページ)

[高田哲生, 前野真輝,NTT OSSセンタ]

「見えない」メモリの謎

 では、式(2)を基に積み上げグラフを作成し、メモリの使用量を解析します。

図2 meminfo解析グラフ 図2 meminfo解析グラフ

 図2はリブートする少し前の/proc/meminfoについて、前項の観点に基づいて積み上げグラフを作ったものです。

 ほぼすべてのメモリがActiveとなっており、MemFreeやInactiveはわずかです。これは確かにメモリ不足といえそうです。カーネルの物理メモリが怪しいと考えていたのですが、肝心のPageTablesやSlab、VmallocUsedなどはあまり増えていません。これは一体どういうことでしょうか?

 ……と、ここまできて、1つ不審な点に気が付きませんか?

 そう。すべてを足したメモリの総量が約12Gbytesしかありません。確かこのサーバはメモリを50Gbytes搭載していましたよね? それがいつの間にか12Gbytesになっている……何やら大変なことが起きていそうです。

 確認のために、リブート時前後のメモリ使用量のグラフを見てみましょう(図3)。リブート前は10Gbytes前後ですが、リブート後に50Gbytes程度に上がっています。これは明らかに式(2)が成り立っていないことを示しています。

図3 リブート前後のmeminfo解析グラフ 図3 リブート前後のmeminfo解析グラフ

把握できないメモリ領域の正体を追え

 図1の右下に白い空白を残したことにお気付きでしょうか? 式(2)で計算が合わないのはこの部分です。つまり、Linuxには/proc/meminfoには記録されないメモリ領域が存在するのです。これは一体何なのでしょうか。

 vmalloc()やkmalloc()を利用してメモリを確保した場合、これらAPIの延長上でカウンタを設けて統計情報を取得しています。/proc/meminfoは、その統計情報を出力するためのエントリです。

 しかしLinuxカーネルでは、それらのAPIよりもさらにローレベルの関数を利用することで、これらのカウントを回避することができます。ある意味「密輸」のような形でメモリを取得することができてしまうのです。この「何でもあり」が、カーネルプログラミングの恐ろしいところの1つといえます。

 このような場合、切り分けの手順としてはどうすればよいでしょうか? Linuxカーネルは1000万行を超える巨大なプログラムです。やみくもにソースコードを読んで問題点にぶつかる可能性は、それこそ宝くじに当たるよりも低いくらいです。やはり論理的に順を追って地道な絞り込みをやっていくしかありません。

 白い空白のメモリ領域を消費している原因を論理的に追っていくに当たって、注目すべきは以下の2点です。

  1. Slab、VmallocUsed、PageTablesなどにはカウントされない
  2. MemFreeはカウントダウンさせている

 つまり、通常のAPIは利用していないが、MemFreeのカウントダウンは行っているのです。MemFreeをカウントダウンさせるような処理は、各種メモリ確保APIよりもローレベルな部分で実装されています。Slabやvmalloc、PageTablesなどもすべて、このMemFreeをカウントダウンさせるような共通関数を利用しています。その関数がalloc_pages()(注3)です。

 基本的にこの関数はメモリの統計情報のカウント対象にならないため、直接使うことは望ましくないのですが、ソースコードをgrepしてみると、このalloc_pages()を直接呼び出している不届き者は結構います。特に、ドライバのような「一度メモリを確保するともうあまりメモリを必要としない」ルーチンに多く見られます。ほかにも、DMA領域用のメモリ確保など、多くのサブシステムから共通関数として呼び出されています。

 今回もこのalloc_pages()を直接呼び出している部分を中心的に調べるのがよさそうです。しかし、非常に多くの部分で利用されているこの関数の呼び出し元を逐一追っていくのもまた現実的ではありません。どうすれば問題の根源を見つけることができるのでしょうか……?

注3:alloc_pages()自体はマクロ定義されており、関数としては__alloc_pages()というものが本体です。ここでは簡単にするために、関連処理の総称としてalloc_pages()と呼んでいます


困ったときのカーネルプローブ

 カーネルに問題がある場合、ソースコードを見るのも1つの手ですが、ほかにも解析のための手法はいくつかあります。例えば以下の手法が代表的でしょう。

  1. Crash Dump
  2. OProfile
  3. カーネルにprintk文を入れるパッチを当てて地道に解析(カーネルプローブ)

 1のCrash Dumpはプロセスのコアダンプと同じで、死んだ瞬間のメモリイメージを保存し、解析する手法です。カーネルパニックなどを起こしてシステムが停止した場合に、どんな理由で停止したのかなどを解析するのに向いています(注4)。

 2のOProfileは性能解析ツールとしての意味合いが強く、ボトルネックの解析などに向いています。

 3の方法は説明するまでもありませんが、あまりにも地道な手段です。怪しい場所に「printk()」という、ユーザープログラムでいうprintf()に当たる関数を埋め込み、挙動を観察します。

 1と2の方法はメモリリークのような事象には向いていません。3は作業が大変なうえに、独自パッチを顧客の環境で導入してもらえるとは思えません。途方に暮れていたわれわれが望みを掛けたツールがあります。その名を「SystemTap」といいます。

 SystemTapとは、ちょうどこのシステムが動作しているRHEL 4 Update 2から、テクノロジープレビューとして同梱されるようになったツールです。一言でいうと、動作中のカーネルに独自のパッチを当て、好きな処理をさせることができるツールです。

図4 SystemTapの動作 図4 SystemTapの動作

 もっときちんと説明すると、以下のように動作します。

  1. 挿入させたい動作をするカーネルモジュールを作成する
  2. kprobesという機能を利用し、任意の場所へそのカーネルモジュールへのジャンプ命令を埋め込む
  3. カーネルの動作が変更される

 このツールは、動作中のカーネル、つまりメモリ上にしか変更を加えません。つまり、ディスク上にあるカーネルのバイナリにパッチを当てないため、リブートすればそれ以前にカーネルモジュールをアンロードし、元に戻ります。そのため、顧客から見ると、パッチを当てて解析をするよりもリスクやコストが数段安くて済みます。

 それでは、SystemTapをどのように利用したのか説明しましょう。

 alloc_pages()には、対になるメモリ解放用関数としてfree_pages()があります。通常は、とあるalloc_pages()で取得したメモリは、しばらくすればfree_pages()で解放されます。つまり、alloc_pages()には、それに対応するfree_pages()があるはずです。もし対になっていない場合はメモリリークが発生している可能性が高いということになります。

 われわれはこの2つの関数に、カーネル内のスタックトレースとプロセス名を取得して保存するような処理を差し込み、顧客側の検証環境で実施してもらうことにしました。

 そうして取得したログデータから、alloc_pages()に対応するfree_pages()を見つけてペアを作ります。そのペアのうち、alloc_pagesとfree_pagesの数が釣り合っていないものを見つけ出します。

 この作業、大変そうに聞こえますが、本当に大変です!

 5分程度データを取得しただけで、スタックトレースで分類すべきalloc_pagasの呼び出しパターン数は5000種類以上になっていました。いきなりalloc_pagesとfree_pagesの対応関係を見ていってもキリがないため、メモリ統計情報を取得するAPIから呼び出されているものを片っ端から削っていきます。そこから、類似のスタックトレースをまとめていき、free_pagesとの対応関係を取っていきます。

 そして、最終的にalloc_pagesとfree_pagesの対応関係が取れていないように見えたのが、NFSから呼び出されているものと、cciss(HP製SMARTアレイのドライバ)から呼び出されているものでした。

注4:この手法を利用して、稼働中のカーネルのメモリイメージを保存するLive Dumpという手法もありますが、その瞬間瞬間のイメージしか取得できませんので、SystemTapほどの柔軟性はありません。


Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。