ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja nb ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja en tt ru en ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja pt en pt ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja en ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
それはどうしてでしょうか?今まではどのようにやっているのかを自分達でもうまく説明できなかったのですが、最近、少し説明できるようになってきた気がするのでその方法を紹介します。題材はruby-trunk - Bug #5688: Solaris10 で spawn を繰り返すとメモリリークする - Ruby Issue Tracking Systemです。この問題はメモリリークなので、プログラムを動かし続けてもメモリ使用量が増えなくなれば解決ということになります。
rb_pid_t rb_fork_err(...) { rb_pid_t pid; for (; before_fork(), (pid = fork()) < 0; prefork()) { ...; } if (!pid) { ...; /* 子プロセス用の処理 */ } after_fork(); return pid; }
このうち、「for (...) {...}」は「forkに失敗しても何回か試してみる」という処理なのでさらに簡略化して以下のように考えることができます。
rb_pid_t rb_fork_err(...) { rb_pid_t pid; before_fork(); pid = fork(); if (!pid) { ...; /* 子プロセス用の処理 */ } after_fork(); return pid; }
ここで、どこが原因の切り分けポイントになるかを考えます。まず、上記の処理の中を大きく分けると以下のようになります。
before_fork()
if (!pid) {...}
after_fork()
このうち、「if (!pid) {...}」は子プロセス専用の処理なので今回は考えなくてもよいことがわかります。なぜなら、今回の再現プログラムでは親プロセスのメモリ使用量が増えることを問題視していて、子プロセスに関しては気にしていないからです。すると以下が問題の切り分けポイントになることがわかります。
何も実行しなかったらどうなるか。
before_fork()のみ実行したらどうなるか。
before_fork()とfork()のみ実行したらどうなるか。
それぞれの結果で以下のようなことがわかります。
メモリリークしなかった場合
何もせずにすぐにreturn
rb_fork_err()の前に原因あり。(今回はこのケースはないはず。)
before_fork()かfork()かafter_fork()に原因あり。
before_fork()の後にすぐにreturn
before_fork()の中に原因あり。
fork()かafter_fork()に原因あり。
fork()の後にすぐにreturn
fork()の中に原因あり。(fork()はOSが提供しているので、今回はこのケースはないはず。)
after_fork()に原因あり。
それぞれを試すコードは以下のようになります。
何もせずにreturn:
rb_pid_t rb_fork_err(...) { return -1; }
before_fork()の後にすぐにreturn:
rb_pid_t rb_fork_err(...) { before_fork(); return -1; }
fork()の後にすぐにreturn:
rb_pid_t rb_fork_err(...) { rb_pid_t pid; before_fork(); pid = fork(); if (!pid) { _exit(EXIT_FAILURE); /* 子プロセスは気にしないのですぐ終了する。 */ } return pid; }
実際に試すと以下のことがわかりました。
何もせずにすぐにreturn
before_fork()の後にすぐにreturn
fork()の後にすぐにreturn
よって、after_fork()の中に問題があることがわかります。
続いて、上記の手順と同様にafter_fork()の中を調べ、原因を絞り込んでいくことを繰り返すと、rb_thread_create_timer_thread()の中のpthread_attr_tに原因があることがわかります。ここで大事なことは「上記の手順と同様に…繰り返す」という部分です。少し原因を絞り込めたからといって、一足飛びに手順を飛ばして原因を見つけようとすると迷子になってしまいます。十分に絞り込めるまではコツコツと一歩ずつ確実に原因を絞り込んでいくことが重要です。さくさくとデバッグを進めていく様子だけを見ると、手順を飛ばして一気に問題を解決しているように見えるかもしれませんが、そんなことはありません。一歩ずつ確実に進めています。ただ、その一歩ずつがテキパキと手早く進んでいるだけなのです。
原因を十分に絞り込めたら問題を修正します。
今回の例では、原因を絞り込むとpthread_attr_tを使った場合のみメモリ使用量が増えることがわかりました。簡略化すると以下のように使用していました。
static void rb_thread_create_timer_thread(void) { ...; if (!timer_thread_id) { pthread_attr_t attr; ...; pthread_attr_init(&attr); ...; err = pthread_create(&timer_thread_id, &attr, thread_timer, 0); ...; } ...; }
同じソースコード内のpthread_attr_tの他の使用例を見ると、以下のようになっています。
static int native_thread_create(rb_thread_t *th) { ...; else { pthread_attr_t attr; ...; CHECK_ERR(pthread_attr_init(&attr)); ...; err = pthread_create(&th->thread_id, &attr, thread_start_func_1, th); ...; CHECK_ERR(pthread_attr_destroy(&attr)); ...; } ...; }
違いはpthread_attr_destroy()を呼んでいるかいないかです。pthread_attr_destroy()は名前からして必要のなくなったリソースを解放する関数にみえるので、rb_thread_create_timer_thread()でpthread_attr_destroy()を呼び忘れていることが原因と考えてよさそうです。実際にpthread_attr_destroy()を追加して再現プログラムを実行するとメモリ使用量が増えなくなることが確認できました。これで修正できたと考えてよさそうです。ということで、パッチができあがることになります。
Rubyでのメモリリークの問題を例にして、私達がやっている「よく知らないプログラムの直し方」を説明しました。今回のような直し方では「それぞれのコードがどうしてそのようなコードになっているのか」という点はほとんど考慮にいれていません。単純に「その処理を実行したら問題が再現するかどうか」だけで絞り込んでいます。そのため、プログラム全体を把握する必要がなくなり、よく知らないプログラムに対しても使える技術になっています。もちろん、この技術はよく知っているプログラムに対しても使えます。
おさらいすると、プログラムの直し方は以下のステップになります。
問題を再現する小さなプログラムを作成する。
問題のソフトウェアを変更しながら原因となっている箇所を特定する。
このうち、「問題を再現する小さなプログラムを作成する」ことはよく聞くのではないでしょうか。バクレポートを報告する文脈でも、「再現プログラムをつけてくれるとうれしい」というのはよく言われます。しかし、「問題のソフトウェアを変更しながら原因となっている箇所を特定する」についてはあまり言及されていないようです。今回は処理を実行するかどうかで判断する方法を使いましたが、print文を埋め込んだり、デバッガを使ったりすることもあります。これらは場合によって使い分けます。それらのやり方よりも大事なことは、「この結果で何がわかるか」を意識しながら一歩一歩着実に原因を絞り込んでいくことです。多くの場合、原因がわかったら修正することはそれほど難しいことではありません。
たとえよく知らないプログラムでも、問題を見つけたときはその問題を直す人が増えることを願っています。華麗に回避することもできるでしょうが、次に同じ道を進んできた人が同じ問題にぶつからずにすんだらステキではないでしょうか。
Steps To Reproduce、略してSTRと言われる事もあります。 ↩
言葉で再現条件を説明した場合、読み違いが原因で、人によっては問題を再現できないといった事が起こり得ます。また、デバッグ中に何度も再現試験を実施する際に、ヒューマンエラーで再現手順を間違えてしまうという事もあり得ます。 ↩
この後にforkしてすぐにexitするだけでも問題が再現するところまで再現プログラムを小さくしていました。そのため、実際は、次のステップに進む前にspawnというよりもfork内に原因があるということまで絞り込めていました。 ↩
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
全文検索エンジンgroongaを囲む夕べ 2: 「groonga村」と「mroongaのベンチマーク」用資料
Ruby関連のコンテストっぽいものへの応募例
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。