Bug #8711
closed最近NoMemoryErrorが多い
Description
最近 rubyci で NoMemoryError を出して失敗することが多いので、それを追跡するスレ
= TestFiber#test_many_fibers
http://u64b.rubyci.org/~chkbuild/ruby-trunk/log/20130730T233301Z.diff.html.gz
http://rbci.lakewood.privs.net/ruby-trunk/log/20130731T001002Z.diff.html.gz
= FiberError: can't alloc machine stack to fiber
http://u64.rubyci.org/~chkbuild/ruby-trunk/log/20130729T200302Z.diff.html.gz
Updated by naruse (Yui NARUSE) almost 11 years ago
http://u64b.rubyci.org/~chkbuild/ruby-trunk/log/20130731T113303Z.diff.html.gz
/proc/meminfo みても特にメモリが足りなくなっているようには見えないので悩んでいたところ、
卜部さんに setrlimit と ASLR のコンボでアドレス空間が無くなっているのではないかとの示唆を受け、
LIMIT_AS を増やしてみたところ、64bit 環境では再現しなくなりました。
32bit はこれから Process::RLIM_INFINITY を指定します。
Updated by naruse (Yui NARUSE) almost 11 years ago
http://u32.rubyci.org/~chkbuild/ruby-trunk/log/20130801T103302Z.log.html.gz
で 32bit でも安定したような気がします。
もうしばらく様子を見ます。
より多くのアドレス空間を必要とするようになった事自体は仕様って理解でいいんですよね>ささださん
Updated by ko1 (Koichi Sasada) over 10 years ago
(2013/08/01 20:18), naruse (Yui NARUSE) wrote:
http://u32.rubyci.org/~chkbuild/ruby-trunk/log/20130801T103302Z.log.html.gz
で 32bit でも安定したような気がします。もうしばらく様子を見ます。
より多くのアドレス空間を必要とするようになった事自体は仕様って理解でいいんですよね>ささださん
こちら、だいたいわかったんじゃないかなぁ、と思うので調査結果を報告します
(日本語)。小崎さん、チェックしてくれると助かります。
簡単な報告:
TestFiber#test_many_fibers が沢山仮想メモリを確保し、それを(なぜか)解
放しないため、その後の fork が ENOMEM で失敗します。
詳細な報告:
前提:
64bit OS で、2GB の物理メモリ+500MB の swap を持つシステム(VM)で検証
しています。Linux のバージョンは 3.2.0-51-generic。virtualbox 上で実行し
ています。
(1) TestFiber#test_many_fibers が仮想メモリを沢山確保してしまう
今は、Fiber のスタックは mmap で確保しています。Fiber が GC されると
munmap で解放するはずなんですが、何かの拍子にプロセスのアドレス空間が広
がったままになっているようです。これについては要検証。単に Fiber が GC
されない、というか、GC されたけど、実際の解放は遅延している、という気が
します。
Linux 側の RSS を見ると、1GB 弱しか使っていないことがわかりますが、仮想
メモリは 4GB ほど確保していることが観測できました。
Ruby 2.0 から、Fiber のためのスタックサイズが広がったため、この問題が生
じました(多分)。
(2) fork が ENOMEM で失敗
物理メモリ(+swap)以上の仮想メモリを持つプロセスを fork しようとする
と、ENOMEM が起って失敗するようです。
(3) ENOMEM になってしまう理由
Linux は、(デフォルトでは)一度に物理メモリ(+α)以上の mmap は出来な
いようになっているようです。
http://passingloop.tumblr.com/post/11957331420/overcommit-and-oom-killer
0
オーバーコミット有効
一回の malloc で確保できるのは実際に利用可能なメモリの大きさまで
多分、この制限で fork() が失敗してるんじゃないかなぁ、と思います。ちなみ
に、このページの malloc って、mmap かなぁ。
Linux kernel のソースをおおざっぱに追ったのですが、fork() がこのチェック
をするところにどう到達するかは見ることが出来ませんでした。ので、推測にな
ります。
考えられる解決策:
- TestFiber#test_many_fibers を削除
- TestFiber#test_many_fibers を別プロセスで実行
- TestFiber#test_many_fibers 後にちゃんと物理メモリを解放するように頑張る
余談:
fork では、CoW になるから、ページテーブル操作だけ必要で、物理メモリの制
限はないだろー(fork 出来るだろう)、と思っていたんですが、そうでもない
んですね。
しかし、この Linux の制限って少し不思議ですね。mmap(4GB) を行うと、(3)
の条件で失敗しますが、mmap(2GB) を 2 回やると成功します。これだけだと連
続領域 4GB の確保が出来ないのかな、とも思いますが、実は MAP_WRITE を外す
と mmap(4GB) は成功します。どうやら、書き込み可能な部分だけ、この(3) の
チェックが入るようです。で、mprotect() で 2GB 単位でこの 4GB の連続領域
を書き込み可能にすることが可能です。つまり、
size = 4GB;
addr = mmap(0, size, PROT_EXEC | PROT_READ, ...);
mprotect(addr , size/2, 書き込み可能に);
mprotect(addr+size/2, size/2, 書き込み可能に);
とすると、書き込み可能な 4GB の連続領域を得ることが出来ます。もちろん、
実際に書き込んでいくとページが割り当てられてメモリ足りなくなりますが。
こういう設計なのは、なにがしかの理由があるんでしょうけれども、意外でした。
--
// SASADA Koichi at atdot dot net
Updated by kosaki (Motohiro KOSAKI) over 10 years ago
2013/8/19 SASADA Koichi ko1@atdot.net:
(2013/08/01 20:18), naruse (Yui NARUSE) wrote:
http://u32.rubyci.org/~chkbuild/ruby-trunk/log/20130801T103302Z.log.html.gz
で 32bit でも安定したような気がします。もうしばらく様子を見ます。
より多くのアドレス空間を必要とするようになった事自体は仕様って理解でいいんですよね>ささださん
こちら、だいたいわかったんじゃないかなぁ、と思うので調査結果を報告します
(日本語)。小崎さん、チェックしてくれると助かります。
調査お疲れ様です。
こちらで確認した限りでは、この内容で間違いないと思われます。
簡単な報告:
TestFiber#test_many_fibers が沢山仮想メモリを確保し、それを(なぜか)解
放しないため、その後の fork が ENOMEM で失敗します。詳細な報告:
前提:
64bit OS で、2GB の物理メモリ+500MB の swap を持つシステム(VM)で検証
しています。Linux のバージョンは 3.2.0-51-generic。virtualbox 上で実行し
ています。(1) TestFiber#test_many_fibers が仮想メモリを沢山確保してしまう
今は、Fiber のスタックは mmap で確保しています。Fiber が GC されると
munmap で解放するはずなんですが、何かの拍子にプロセスのアドレス空間が広
がったままになっているようです。これについては要検証。単に Fiber が GC
されない、というか、GC されたけど、実際の解放は遅延している、という気が
します。Linux 側の RSS を見ると、1GB 弱しか使っていないことがわかりますが、仮想
メモリは 4GB ほど確保していることが観測できました。Ruby 2.0 から、Fiber のためのスタックサイズが広がったため、この問題が生
じました(多分)。(2) fork が ENOMEM で失敗
物理メモリ(+swap)以上の仮想メモリを持つプロセスを fork しようとする
と、ENOMEM が起って失敗するようです。(3) ENOMEM になってしまう理由
Linux は、(デフォルトでは)一度に物理メモリ(+α)以上の mmap は出来な
いようになっているようです。
そうですね。
http://passingloop.tumblr.com/post/11957331420/overcommit-and-oom-killer
0
オーバーコミット有効
一回の malloc で確保できるのは実際に利用可能なメモリの大きさまで多分、この制限で fork() が失敗してるんじゃないかなぁ、と思います。ちなみ
に、このページの malloc って、mmap かなぁ。
はい。そうです
Linux kernel のソースをおおざっぱに追ったのですが、fork() がこのチェック
をするところにどう到達するかは見ることが出来ませんでした。ので、推測にな
ります。
してます。
do_fork()
copy_process()
copy_mm()
dup_mm()
dup_mmap()
security_vm_enough_memory_mm()
__vm_enough_memory()
というルートになります。
考えられる解決策:
- TestFiber#test_many_fibers を削除
- TestFiber#test_many_fibers を別プロセスで実行
- TestFiber#test_many_fibers 後にちゃんと物理メモリを解放するように頑張る
システム全体で制限がかかっているので別プロセスで実行は本質的では
ないように思います。
ちゃんと開放するようにテスト(と、GC)を変えて、そもそもシステムの
メモリが小さすぎるときはテストをスキップするようにしないと、
不思議な箇所でエラーを吐くと思います。
余談:
fork では、CoW になるから、ページテーブル操作だけ必要で、物理メモリの制
限はないだろー(fork 出来るだろう)、と思っていたんですが、そうでもない
んですね。しかし、この Linux の制限って少し不思議ですね。mmap(4GB) を行うと、(3)
の条件で失敗しますが、mmap(2GB) を 2 回やると成功します。これだけだと連
続領域 4GB の確保が出来ないのかな、とも思いますが、実は MAP_WRITE を外す
と mmap(4GB) は成功します。どうやら、書き込み可能な部分だけ、この(3) の
チェックが入るようです。で、mprotect() で 2GB 単位でこの 4GB の連続領域
を書き込み可能にすることが可能です。つまり、size = 4GB; addr = mmap(0, size, PROT_EXEC | PROT_READ, ...); mprotect(addr , size/2, 書き込み可能に); mprotect(addr+size/2, size/2, 書き込み可能に);
とすると、書き込み可能な 4GB の連続領域を得ることが出来ます。もちろん、
実際に書き込んでいくとページが割り当てられてメモリ足りなくなりますが。
全くその通りです。
こういう設計なのは、なにがしかの理由があるんでしょうけれども、意外でした。
いや、これは歴史的なゴミなので、あんまり気にしなくていいです。
overcommit_always がきちっとした管理で、overcommit_guessは基本的に
チェックしないけど、あまりにもアホなサイズはきっとオペミスだから
助けてやろう。ぐらいのノリ
ちゃんと設計されてないので、mprotectまわりで矛盾が発生しており、
最初に PROT_READでmapしてから、PROT_WRITEに変えると
アカウントされない → アカウントされる と遷移するけど、
逆にPROT_WRITEでmapしてPROT_READに変更するとアカウントされる
ままなのだよ。一回write modeになってしまうと何が書いてあるか
分からないからそうするしかないんだけど、汚い。
そして数々の勘違いアプリコードを生んだのであった。
Updated by ko1 (Koichi Sasada) over 10 years ago
(2013/08/19 15:42), KOSAKI Motohiro wrote:
考えられる解決策:
- TestFiber#test_many_fibers を削除
- TestFiber#test_many_fibers を別プロセスで実行
- TestFiber#test_many_fibers 後にちゃんと物理メモリを解放するように頑張る
システム全体で制限がかかっているので別プロセスで実行は本質的では
ないように思います。
ちゃんと開放するようにテスト(と、GC)を変えて、そもそもシステムの
メモリが小さすぎるときはテストをスキップするようにしないと、
不思議な箇所でエラーを吐くと思います。
プロセスが肥大化し、元に戻らないのが原因のようなので、とりあえず応急処置
にはなるかと。
で、fork() が失敗する条件は、一度に mmap できないような連続したメモリ領
域がある場合なのですが、何かと調べたり考えたりしたところ、(まだ確証はな
いのですが)VM のスタックであることに思い至りました。
現状、VM stack は Fiber 生成時、および Fiber 解放時に割り当て、解放を
ALLOC_N()、つまり malloc() 経由で行っています。これにはいくつか問題があ
ります。
(1) Fiber の初回起動前、および Fiber の終了後には VM stack は不要
(2) malloc で割り当てているため、free しても OS に返すかどうかは不明
(1) は、単純に改良をすればいいだけです。ただし、変更はちょっと面倒そうです。
(2) は、例えば mmap にすれば解決しますが、システムコールのオーバヘッドを
どう考えるか、ということになるかと思います。理想的には、Fiber が burst
的に発生する、という前提では、初期値は小さく、オーバーフローしそうになっ
たら増やしていくような感じにしたいと思って居ます(これはスレッドも同
じ)。が、これは凄く面倒な話。
とりあえず、当該テストで GC.start をする、という腰砕けな対応になっています。
--
// SASADA Koichi at atdot dot net
Updated by kosaki (Motohiro KOSAKI) over 10 years ago
2013/8/19 SASADA Koichi ko1@atdot.net:
(2013/08/19 15:42), KOSAKI Motohiro wrote:
考えられる解決策:
- TestFiber#test_many_fibers を削除
- TestFiber#test_many_fibers を別プロセスで実行
- TestFiber#test_many_fibers 後にちゃんと物理メモリを解放するように頑張る
システム全体で制限がかかっているので別プロセスで実行は本質的では
ないように思います。
ちゃんと開放するようにテスト(と、GC)を変えて、そもそもシステムの
メモリが小さすぎるときはテストをスキップするようにしないと、
不思議な箇所でエラーを吐くと思います。プロセスが肥大化し、元に戻らないのが原因のようなので、とりあえず応急処置
にはなるかと。で、fork() が失敗する条件は、一度に mmap できないような連続したメモリ領
域がある場合なのですが、何かと調べたり考えたりしたところ、(まだ確証はな
いのですが)VM のスタックであることに思い至りました。現状、VM stack は Fiber 生成時、および Fiber 解放時に割り当て、解放を
ALLOC_N()、つまり malloc() 経由で行っています。これにはいくつか問題があ
ります。(1) Fiber の初回起動前、および Fiber の終了後には VM stack は不要
(2) malloc で割り当てているため、free しても OS に返すかどうかは不明(1) は、単純に改良をすればいいだけです。ただし、変更はちょっと面倒そうです。
(2) は、例えば mmap にすれば解決しますが、システムコールのオーバヘッドを
どう考えるか、ということになるかと思います。理想的には、Fiber が burst
的に発生する、という前提では、初期値は小さく、オーバーフローしそうになっ
たら増やしていくような感じにしたいと思って居ます(これはスレッドも同
じ)。が、これは凄く面倒な話。とりあえず、当該テストで GC.start をする、という腰砕けな対応になっています。
えーと、ENOMEMが出るからにはGB単位のメモリが未開放でないといけないのですが、
なんでfreeしてもOSに戻ってないんでしょう。
実害があるのが分かっているのだから、手を入れたほうがよいのではないかと
Updated by ko1 (Koichi Sasada) over 10 years ago
(2013/08/20 0:33), KOSAKI Motohiro wrote:
えーと、ENOMEMが出るからにはGB単位のメモリが未開放でないといけないのですが、
なんでfreeしてもOSに戻ってないんでしょう。
malloc のライブラリの問題じゃないかなぁ、と。勘です。どうやって調べるの
がいいのかな。
実害があるのが分かっているのだから、手を入れたほうがよいのではないかと
単に、開発優先度の問題ですね。(1) は出来そうだな、と思ったんだけど、なん
か混み合っていて、直すのは気合いを入れる必要がありそうなので、まだやって
いないという話です。
--
// SASADA Koichi at atdot dot net
Updated by akr (Akira Tanaka) over 10 years ago
2013年8月19日 15:42 KOSAKI Motohiro kosaki.motohiro@gmail.com:
考えられる解決策:
- TestFiber#test_many_fibers を削除
- TestFiber#test_many_fibers を別プロセスで実行
- TestFiber#test_many_fibers 後にちゃんと物理メモリを解放するように頑張る
システム全体で制限がかかっているので別プロセスで実行は本質的では
ないように思います。
ちゃんと開放するようにテスト(と、GC)を変えて、そもそもシステムの
メモリが小さすぎるときはテストをスキップするようにしないと、
不思議な箇所でエラーを吐くと思います。
別プロセスでテストを実行すれば、そのテストが要求したメモリは、
そのプロセスが終了しさえすれば解放される、という話に思えるのですが、
なにが問題なのでしょうか。
プロセスが終了しても影響が残る可能性を否定できないという話があるのでしょうか。¶
[田中哲][たなかあきら][Tanaka Akira]
Updated by kosaki (Motohiro KOSAKI) over 10 years ago
2013/8/20 Tanaka Akira akr@fsij.org:
2013年8月19日 15:42 KOSAKI Motohiro kosaki.motohiro@gmail.com:
考えられる解決策:
- TestFiber#test_many_fibers を削除
- TestFiber#test_many_fibers を別プロセスで実行
- TestFiber#test_many_fibers 後にちゃんと物理メモリを解放するように頑張る
システム全体で制限がかかっているので別プロセスで実行は本質的では
ないように思います。
ちゃんと開放するようにテスト(と、GC)を変えて、そもそもシステムの
メモリが小さすぎるときはテストをスキップするようにしないと、
不思議な箇所でエラーを吐くと思います。別プロセスでテストを実行すれば、そのテストが要求したメモリは、
そのプロセスが終了しさえすれば解放される、という話に思えるのですが、
なにが問題なのでしょうか。プロセスが終了しても影響が残る可能性を否定できないという話があるのでしょうか。
システム全体のリミットに引っかかった場合、死ぬのはrubyかもしれないし、
まったくrubyと関係ない別のプロセスかもしれない。誰がババを引くかは確定的には
いえないのではないでしょうか。
テスト結果からす推測するに、今のささださんの環境ではruby以外に大量の
アロケートするプロセスはいないようですが(そのため、Rubyだけがしきい値を超える可能性がある)
一般的には保証のない話だと思います。
さらにいうと、Fiberを使うとVMスタックがとんでもなく肥大化する(可能性がある)というのは
別にテスト環境以外でも発生しうる条件ですよね。
Updated by kosaki (Motohiro KOSAKI) over 10 years ago
えーと、ENOMEMが出るからにはGB単位のメモリが未開放でないといけないのですが、
なんでfreeしてもOSに戻ってないんでしょう。malloc のライブラリの問題じゃないかなぁ、と。勘です。どうやって調べるの
がいいのかな。
まず、LD_PRELOADで違うmallocロードしても再現するか確認するところからじゃないでしょうか。
Updated by hsbt (Hiroshi SHIBATA) over 10 years ago
- Status changed from Open to Feedback
こちらどうでしょう。時間経っていますが引き続き調査します?
Updated by usa (Usaku NAKAMURA) over 10 years ago
- Status changed from Feedback to Closed
ちょっと正確なrevisionは出ませんが、最近の笹田さんと樽家さんの変更で
問題が出なくなったんじゃないかと。
一応閉じておきます。