日々、書きたい記事が溜まっていく今日この頃、今朝は早起きしたので、失敗からの学びにつながった記事を書きます。
EC2(t3.micro)にRuby 3.1.4をインストールする際、
rbenv install 3.1.4
を実行すると、コンパイルが途中で止まり、
待っていると数分後にエラー、Rubyのインストールは失敗に終わる…
調査すると、今回の環境では主に次の2つが原因でした。
- メモリ不足によるOOM Killer
-
/tmpとして使用されているtmpfsの容量不足
この記事では、それぞれの確認方法と対応方法をまとめます。
先に結論を書くと、今回の環境ではruby-buildの一時ファイルの保存先を、TMPDIRを使ってtmpfsの/tmpからEBS側へ変更することで、容量不足を回避してRubyをインストールできました。
また、調査の途中でメモリ不足によるOOMも発生していたため、MAKE_OPTS="-j1"でコンパイルの並列数を減らすなど、メモリ使用量を抑える対応も行っています。
初学者が学習しながら整理した内容のため、誤りや不正確な表現があるかもしれません。
もし間違いがあれば、ご指摘いただけると嬉しいです。
Killed signal terminated program cc1が発生する
Rubyのインストールを実行すると、
compiling continuation.c
linking shared-object continuation.so
compiling coverage.c
linking shared-object coverage.so
compiling date_core.c ← ここで止まる
のように、コンパイルの途中で処理が止まりました。
最初は「コンパイルに時間がかかっているだけかもしれない」と思い少し待ってみましたが、その後、
gcc: fatal error: Killed signal terminated program cc1
make[2]: *** [Makefile:297: bigdecimal.o] Error 1
というエラーが。
知らない言葉だ〜、調べます。
cc1は、GCCがC言語のソースコードをコンパイルするときに内部で使用するプログラムです。
Killedと表示されているため、コンパイル中のプロセスが何らかの理由で終了させられています。
今回使用しているt3.microのメモリは RUNTEQの学習推奨環境PCの8-16GB にも全く及ばない 1GB しかないので、まずメモリ不足を疑いました。
メモリ不足を確認する
まず、現在のメモリ状況を確認します。
free -h
freeは、Linuxのメモリ使用状況を確認するコマンドです。
-hはhuman-readableの意味で、バイト数ではなくMiBやGiBなど読みやすい単位で表示してくれます。
また、表示される項目の中ではfreeだけではなく、availableも確認します。
availableは、現在使われていないメモリだけでなく、必要になったときに解放できるキャッシュなども考慮した「新しい処理に利用できるメモリの目安」です。
今回確認した環境ではメモリにあまり余裕がなく、Swapも設定されていませんでした。
ただし、これだけでは今回のエラーがメモリ不足によるものだとは断定できません。
実際にメモリ不足でプロセスが終了した記録がないか確認します。
sudo dmesg | grep -i -E 'oom|killed process|out of memory'
ここでは、
-
sudo:管理者権限で実行 -
dmesg:Linuxカーネルのログを表示 -
|:左側の結果を右側のコマンドへ渡す -
grep:必要な文字列を含む行を探す -
-i:大文字・小文字を区別しない -
-E:複数のパターンを|で指定できるようにする
という処理をしています。
今回は、
Out of memory: Killed process ... (cc1)
というログが確認できました。
これによって、Rubyのコンパイル中にメモリが不足し、OOM Killerによってcc1が終了させられていたことが分かりました。
OOM Killerとは
OOMはOut Of Memoryの略です。
Linuxでは使用できるメモリが不足すると、システム全体が動かなくなることを防ぐため、プロセスを強制終了することがあります。
今回の場合は、
Rubyをコンパイル
↓
RAMが不足
↓
OOM Killerが動く
↓
cc1が終了
↓
コンパイル失敗
という流れでした。
何がメモリを使っているのか確認する
メモリ不足だと分かったので、次は何がメモリを使用しているのか確認します。
ps aux --sort=-%mem | head
psは現在動いているプロセスを確認するコマンドです。
今回は、
--sort=-%mem
によってメモリ使用率が高い順に並べ、
| head
によって先頭の数件だけを表示しています。
確認すると、EC2上で動いていたmysqldが多くのメモリを使用していました。
さらに状態を確認します。
systemctl status mysqld
今回確認した時点では、mysqldだけで400MB以上のメモリを使用していました。
勝手に動いているとは盲点でした...
Rubyのインストール中はEC2上のMySQLを使用していなかったため、一時的に停止!!
sudo systemctl stop mysqld
停止後にもう一度、
free -h
で確認すると、利用可能なメモリが大きく増えていました。
コンパイルの並列数を減らす
さらに、Rubyのコンパイルで一度に使用するメモリを抑えるため、makeの並列数も減らしました。
今回は元が2だったので、1に変更します。
MAKE_OPTS="-j1" rbenv install 3.1.4
MAKE_OPTSを使うと、Rubyのビルド時に実行されるmakeへオプションを渡せます。
今回の、
-j1
は、同時に実行する処理を1つにする指定です。
処理時間は長くなりますが、同時に複数のコンパイル処理を実行しないことで、ピーク時のメモリ使用量を抑えることができます。
これで再度Rubyをインストールしました。
絶対成功する、よくやったぞ俺、ここまでの技術記事書こう!!と思っていたところ
今度は別のエラーが発生しました。
No space left on deviceが発生する
再度Rubyをビルドすると、
fatal error: error writing to /tmp/cctqXeTx.s: No space left on device
というエラーが発生しました。
No space left on deviceは、書き込み先に空き容量がないことを表しています。
そこで最初は、
まさかですが、EC2に設定しているEBSを使い切ったか?
と考えました。
EBSの空き容量を確認する
まず、ルートファイルシステムの容量を確認します。
df -h /
dfは、ファイルシステムの使用量や空き容量を確認するコマンドです。
ここでも-hを付けることで、MiBやGiBなど人間が読みやすい単位で表示しています。
結果は、
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 8.0G 3.4G 4.7G 42% /
となっており、EBSには約4.7GiBの空きがありました。
つまり、EBS全体がいっぱいになっているわけではありませんね。
エラーをもう一度確認すると、
error writing to /tmp/cctqXeTx.s
となっており、書き込み先として/tmpが表示されています。
では、/tmp自体はどうなっているんだろう?
ということで、/tmpを個別に確認します。
/tmpの容量を確認する
df -h /tmp
結果は、
Filesystem Size Used Avail Use% Mounted on
tmpfs 459M 457M 2.4M 100% /tmp
100%!!
EBSには約4.7GiBの空きがある一方で、/tmpはいっぱいになっています。
次に、/tmpの中で何が容量を使っているのか確認します。
du -sh /tmp/ruby-build.* 2>/dev/null
duは、ファイルやディレクトリが実際にどのくらい容量を使用しているか確認するコマンドです。
-
-s:合計だけを表示 -
-h:読みやすい単位で表示 -
2>/dev/null:エラー出力を表示しない
という指定をしています。
結果は、
457M /tmp/ruby-build.20261002052348.2038.Ewn3RB
/tmpの上限459MiBに対して、ruby-buildだけで457MiB。
これがNo space left on deviceの原因でした。
RAM・tmpfs・EBSの関係
ここで一つ疑問が。
「EBSには4.7GiB空いているのに、なんで/tmpは459MiBしか使えないんだ?」
df -h /tmpの結果をもう一度見ると、
Filesystem
tmpfs
となっています。
このtmpfsについて調べてみました。
最初は/tmpとtmpfsを同じようなものだと思っていましたが、そうではなく、
/tmp
→ 一時ファイルを置く「場所」
tmpfs
→ その場所で使われている「ファイルシステム」
という関係でした。
今回の環境では、/tmpにtmpfsというファイルシステムがマウントされています。
そしてtmpfsは、ざっくりいうとRAMを使ってファイルを扱う仕組みです。
そもそもRAMとEBSは何が違う?
ここで、RAMとEBSの違いも簡単に整理しました。
| RAM | EBS | |
|---|---|---|
| ざっくり | プログラムの処理中に使うメモリ | ファイルなどを保存するストレージ |
| 速度 | とても速い | RAMと比べると遅い |
| 今回の容量 | 約916MiB | 8GiB |
| データ | 電源を失うと保持されない | EC2を停止しても保持できる |
RAMは高速なので、tmpfsを使うことで一時ファイルを高速に扱えるというメリットがあります。
一方で、当然RAMには限りがあります。
今回の環境をざっくり整理すると、
RAM
├─ OSや実行中のプロセスが利用
└─ tmpfsも必要に応じて利用
EBS
└─ OSや通常のファイルなどを保存
という感じ。
つまり、
RAMは速いけど容量が小さい。
EBSはRAMより遅いけど、今回はこちらの方が容量に余裕がある。
という違いがあります。
tmpfsがRAMの半分?
今回のEC2では、
物理RAM:約916MiB
/tmp(tmpfs)の上限:459MiB
となっていました。
おおよそ物理RAMの半分ですね。
ただし、ここは最初ちょっと勘違いしました。
tmpfsが459MiB
=
最初からRAMを459MiB確保している
という意味ではありません。
459MiBはあくまでtmpfsとして使える上限です。
実際には、tmpfsへファイルを書き込んだ分だけメモリを使用していきます。
今回はruby-buildが、
457M /tmp/ruby-build...
まで使っていたので、ほぼ上限いっぱい。
これならNo space left on deviceになるのも納得です。
そしてtmpfsはRAMを利用するので、ファイルを書き込んでいけばRAM側への負荷にもなります。
じゃあSwapを増やせばいいのでは?
最初のOOMを調べたときに、メモリ不足対策としてSwapという方法も出てきました。
Swapは簡単にいうと、
RAMが足りなくなったときに、ディスクの一部を退避先として利用する仕組み
です。
RAMが足りない
↓
一部をSwapへ退避
↓
ディスクを使って補助
それなら、
「Swapを追加すれば全部解決するのでは?」
とも思いました。
ただ、ここも少し違いました。
Swapを追加しても、物理RAMそのものが増えるわけではありません。(ここ重要!!)
また、tmpfsのデータはメモリ管理の対象になるため、Swapがある環境では一部がSwapへ退避されることもあります。
でも、
/tmpの上限 = 459MiB
というtmpfs自体の容量上限が、自動的に増えるわけではありません。
つまり今回の問題は、
OOM
→ RAMが足りない問題
/tmpが100%
→ tmpfsの容量上限に達した問題
似ているようで、分けて考える必要がありました。
SwapはRAM不足の補助にはなりますが、/tmpの459MiBという上限を直接解決してくれるものではありません。
ではtmpfsの上限を大きくすればいいのか?
それも方法としては考えられます。
ただ、今回使用しているt3.microは、そもそも物理RAMが約916MiB。
tmpfsの上限を大きくして、実際にそこへ大量のファイルを書き込めば、今度はまた物理RAMを圧迫する可能性があります。
そこで、
「それなら空きが4.7GiBもあるEBS側に一時ファイルを置けないのか?」
という方向で考えることにしました。
ruby-buildの一時ファイルの保存先を変更したい
ここまでを一度整理すると、
/tmp
→ tmpfs
→ 主にRAMを利用
→ 速い
→ でも今回は459MiBが上限
EBS
→ RAMと比べると遅い
→ でも約4.7GiB空いている
という状態です。
今回やりたいのはRubyをインストールすること。
多少遅くなったとしても、コンパイルを最後まで完了できる方が大事です。
そこで、
「ruby-buildが使う一時ファイルの保存先を、/tmpから空きのあるEBS側へ変更できないか?」
と考えました。
ただ、どうやってやるんだろう。
また調べます。
TMPDIRというものがあるらしい
一時ファイルの保存先を変更する方法を調べると、TMPDIRという環境変数があることを発見!
TMPDIRは、プログラムが一時ファイルを作成するときに、保存先として参照できる環境変数です。
今回であれば、
変更前
ruby-build
↓
/tmp
↓
tmpfs
↓
上限459MiB
となっていたものを、
変更後
ruby-build
↓
TMPDIRで指定した場所
↓
EBS
とするイメージです。
これならtmpfsの上限を無理に大きくするのではなく、空き容量のあるEBSをruby-buildの作業場所として利用できそうです。
EBS側に一時ファイル用のディレクトリを作る
まず、ホームディレクトリ配下にtmpディレクトリを作ります。
mkdir ~/tmp
~は現在のユーザーのホームディレクトリを表すため、今回の場合は、
/home/ec2-user/tmp
が作られます。
ただ、
「ホームディレクトリに作ったからEBSでしょ!」
と決めつけるのも怖いので、一応確認します。
df -h ~/tmp
結果は、
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 8.0G 3.4G 4.7G 42% /
でした。
/tmpを確認したときは、
tmpfs
でしたが、今回作成した~/tmpは、
/dev/nvme0n1p1
となっています。
ルートファイルシステムと同じなので、~/tmpがEBS側にあることを確認できました。
TMPDIRをEBS側に変更する
保存先を用意できたので、TMPDIRに先ほど作成したディレクトリを指定します。
export TMPDIR=/home/ec2-user/tmp
ここで使用しているexportは、設定した環境変数を、このシェルから起動する子プロセスにも引き継がせるためのコマンドです。
今回であれば、このあと実行するruby-buildからも、
TMPDIR=/home/ec2-user/tmp
を参照できるようにしています。
ちゃんと設定できているか確認。
echo $TMPDIR
結果は、
/home/ec2-user/tmp
となりました。
これで一時ファイルの保存先として、EBS上の/home/ec2-user/tmpを指定できました。
今回はRubyのインストール時だけ必要なので、.bash_profileなどには追加せず、一時的な環境変数として設定しています。
Rubyを再度インストールする
ここまで長かったですが、最終的には、
OOM対策
├─ mysqldを一時停止
└─ MAKE_OPTS="-j1"で並列数を減らす
/tmpの容量不足対策
└─ TMPDIRをEBS側へ変更
という状態になりました。
今度こそ...
MAKE_OPTS="-j1" rbenv install 3.1.4
すると、
Building native extensions. This could take a while...
installing bundled gem cache: /home/ec2-user/.rbenv/versions/3.1.4/lib/ruby/gems/3.1.0/cache
==> Installed ruby-3.1.4 to /home/ec2-user/.rbenv/versions/3.1.4
きた!!!!
ようやくRuby 3.1.4のインストールに成功しました。
Ruby 3.1.4が使用できることを確認する
最後に、インストールしたRubyを使用するように設定します。
rbenv global 3.1.4
rbenv rehash
rbenv global 3.1.4で、rbenvがデフォルトで使用するRubyを3.1.4に設定します。
その後、バージョンを確認。
ruby -v
結果は、
ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux]
無事Ruby 3.1.4になりました。
まとめ
今回は、Rubyのインストールを通してメモリの大事さに気づくことができました。
普段遭遇するのはコードの書き方や実装によるエラーが多く、今回のようにRAMや一時ファイルの容量など、実行する環境が原因でエラーになる経験はあまりありませんでした。
もちろん、EC2のスペックを上げるなど、もっと余裕のある環境を使えば簡単に解決できた部分もあると思います。
ただ今回は、カリキュラムで用意されている環境の中で進める必要があったからこそ、
「今あるリソースの中で、なぜ失敗しているのか?どうすれば動かせるのか?」
と考える良い機会になりました。
いつものようにコードだけを見るのではなく、メモリやストレージなど、コードが動いている環境にも目を向けることが大切なんだなと学べたエラーでした。