VPS(Lightsail)の空きメモリ状況を常時収集してpandas+matplotlibで可視化

#やりたいこと
タイトル通りです。VPS(Amazon Lightsail)インスタンスの空きメモリ状況を常時収集し、可視化します。

#目的
現在友人で集まるゲームのマルチプレーサーバーを運営しています。ログイン人数が増加した際などに時折サーバー側のアプリケーションがクラッシュするので、まずどのような場合にクラッシュするのか、特にメモリ不足の可能性について検討します。

#サーバー側設定
###サーバーの種類
今回使用しているサーバーはAmazonが運営しているVPSサービスであるLightsailのメモリ4GBプランを利用しています。OSはUbuntu 18.04.2 LTSです。
ちなみに、OS情報は`/etc/os-release`等を見ることで調べることができます。

```
$ cat /etc/os-release
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.2 LTS"
VERSION_ID="18.04"
︙
```

###サーバーの状態を取得
LightsailのUbuntuにはデフォルトで`vmstat`というコマンドが入っており、マシンの状況を調べることができます。調べていませんが、その他のLinux OSにも大体入っているかと思います。`vmstat`の使い方・結果の見方は[こちらの記事](https://qiita.com/aosho235/items/c4d6995743dd1dac16e1)を参照してください。

サーバーの状態を常時監視するためには`vmstat`の結果を常時どこかに出力しておく必要があります。今回はこのようなスクリプト(`vmstat.sh`)を用意しました。1秒ごとにvmstatの結果を出力し、1時間ぶんを書き出したらgzip圧縮して保管します。

``` bash:vmstat.sh
DATETIME="$(date +'%Y%m%d-%H%M%S')"

cd /home/username/logs/
vmstat -t 1 3600 > vmstat-"${DATETIME}".log
gzip vmstat-"${DATETIME}".log
```

この`vmstat.sh`を1時間ごとに実行すれば毎時間ごとにファイル分けされたログを得ることができます。
定期的にファイルを分ける理由はサーバーのリソース不足時にできるだけログが破壊されないようにするためと、データ圧縮のためです。サイズについては一ヶ月分溜めると無圧縮では約300MBほどになりますが、圧縮すれば約40MBと無視できない差になります。
1時間ごとにスクリプトを実行するにはcronを利用します。詳細は[こちらの記事](https://qiita.com/UNILORN/items/a1a3f62409cdb4256219)などを参照してください。今回は`/etc/crontab`を編集します。原則、crontabの編集にはsudoが必要です。

```bash:/etc/crontab
# m h dom mon dow user	command
︙
1  *  *	 *  * root /path/to/vmstat.sh

```

この行を`/etc/crontab`の末尾に追加すると、毎時1分にvmstat.shを実行してくれます。

**注意**：`crontab`の最後の行の後には改行を必ず付けましょう。
　　　末尾が改行でないと無効なファイルとして扱われ、動作しません。

編集したらcronを再起動します。`sudo service cron restart` を実行すればOKです。

#解析側設定
解析の流れは以下のようになります。

1. `vmstat`の結果を一括でダウンロード
2. gzipを解凍し、不要な行(ヘッダー)を除いて整形
3. 必要な行を抽出し、プロット

今回は1, 2をシェルスクリプト(bash)で、3をpython3+matplotlibで行います。
今回のスクリプトを実行するためにはpython3にpandas, matplotlib, seabornが必要です。anacondaやpipで入手してください。また、データを自動でダウンロードするためにサーバーには事前にssh keyを登録しておきましょう。

```bash:analyze.sh
#!/bin/bash

rsync -amuvh --size-only --progress user@server_address:logs/*.gz .
rsync -amuvh --size-only --progress user@server_address:logs/*.log .

ls -1 *.gz | xargs -L1 gunzip -k
ls -1 *log | tail -n48 | xargs cat | grep ":" > concat.log
python plot_vmstat.py concat.log
rm vmstat-*.log
```

`analyze.sh`は`rsync`でサーバーのログがあるディレクトリ（今回は${HOME}/logs）からデータをダウンロードします。

次に`ls -1 *.gz`でダウンロードした.gzファイルの一覧を取得し、
その各ファイルに対して(`xargs -L1`)gunzipを適用します。
`-k`オプションにより既にダウンロードしたファイルを削除せず、次回以降のダウンロード量を節約します。

その後`ls -1 *.log`で解凍した.logファイルと最新の圧縮されていない.logファイルの一覧を取得し、
`-tail -n48`で直近48ファイル(48時間相当)についての結果のファイル名を取得します。ファイル名に時間が入っているので、ファイル名を辞書順に並べたとき後ろが最新です。
`xargs cat`でそれらのファイル名に対応するファイルの中身を表示します。
`grep ":"`でテンプレートでない部分（タイムスタンプの`":"`）を抽出し、`concat.log`に保存します。
最後に`plot_vmstat.py`で中身をプロットします。

`plot_vmstat.py`の中身は以下のようになっています。

```python:plot_vmstat.py

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# register matplotlib converters for "parse_dates" of pd.read_csv
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
sns.set(rc={'figure.figsize':(11, 4)})

# read vmstat log file
data = pd.read_csv("concat.log", delim_whitespace=True, header=None, parse_dates=[[17, 18]])
data.sort_values(by="17_18")

date_time = data.loc[:,"17_18"] # date-time
free = data.loc[:, 3]//1024 # freemem (KB to MB)
free_cache = (data.loc[:, 3]+data.loc[:, 5])//1024 # free+cache (KB to MB)

# plot
plt.figure()
plt.plot(date_time, free, color="r", linewidth=1, label="Free memory")
plt.plot(date_time, free_cache, color="b", linewidth=1, label="Free+Cache memory")
plt.xlabel("Date")
plt.ylabel("Memory size [MB]")

plt.legend(bbox_to_anchor=(1, 1), loc='upper right', borderaxespad=0, fontsize=14)
plt.tight_layout()

plt.savefig("free_memory.png")

```

複数のスペースで整形されているテキストファイルは`delim_whitespace=True`できれいに読めます。時刻は0から数えて17番目(date)と18番目(time)にあります。スペースで区切られていますが`parse_dates=[[17, 18]]`で連結してくれます。空きメモリ(free)は0から数えて3番目のカラムにあり、ページキャッシュ (cache)は5番目のカラムにあります。ついでにKBからMBに変換しておきます。
プロットするカラムを変えれば他の指標もプロットできます。

#出力
![free_memory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441236/6e7d3ce6-5310-6918-3e4a-b270097ca6c9.png)

このようなグラフが出力されます。サーバーは空きメモリがなくなるとキャッシュを使用し始め、キャッシュも足りなくなるとアプリケーションがクラッシュしメモリが解放されているようです。しかし、すぐに再起動してまたメモリが消費されていることがわかります。

#結論
メモリ状況を常時収集し、必要なタイミングで可視化するという目的は達成できました。メモリの割り当てを調整するか、よりメモリの多いインスタンスを借りる必要がありそうです。
なんらかの事情でログが取れなかった場合にグラフが飛んでしまう（06-19 15時あたりの斜め線がそうです）ことの解決等、細かい課題はありますが、概ね目標は達成できました。

##追記・修正
コメントに従い、シェルスクリプトの`${}`を`"${}"`に変更しました。