インスタンスの負荷状況をリアルタイムにプロットする

![vmstat-real-time.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261594/c9fcd78c-a16c-bef5-0d95-0be11d462449.gif)

## 概要
業務でEC2のインスタンスサイズの選定を行う機会がありました。
選定にあたって、開発環境のインスタンスに負荷をかけ、その負荷状況から本番のインスタンスで許容したいアクセス数にはどのくらいのリソースが必要かを見積もりとして出す、ということをしてみました。

その過程でインスタンスの各種統計情報をローカルに落として手元でプロットしてみたので備忘録として記しておきます。

方針としては、
- インスタンスにSSH接続
- ```vmstat```にて統計情報を取得
- 上記をローカルのファイルに追記
- ローカルでファイルをwatchしておいて、更新をプロットに反映

という手順で行ってみました。

~~EC2ならcloudwatch logs agent使えばいいのでh~~　聞こえませんね。


## 統計の取集
まずssh経由でインスタンスの統計情報を取得し、ローカルに落としてくる必要があります。
と言ってもコマンド一つで済むのが楽なところですね。

今回はCPU使用率だけ取れれば十分だったので、```vmstat```の出力結果にタイムスタンプを付与し、CSV形式に加工するにとどめました。

実際のコマンドは以下のようになりました。
``` bash
ssh (user@)host "vmstat -n 1 | awk 'BEGIN{OFS=\",\"} { \$1=\$1; print strftime(\"%s\") , \$0; system(\"\") }; '" > /tmp/vmstat.log
```
これで1秒ごとにインスタンスの統計情報がローカルのファイルに追記されます。
```vmstat```の頻度がミリ秒単位で指定できなそうなので、1秒以上の頻度で収集したい場合は別のコマンドか、```do while```等でうまいことしてあげる必要がありそうです。

出力の加工にあたって```awk```コマンドを利用しましたが、こいつがなかなか癖のあるやつで。。

こちらの記事が詳しく掘られてらっしゃいますね。
https://zariganitosh.hatenablog.jp/entry/20131209/minimum_awk

``` bash
awk 'BEGIN{OFS=","} { $1=$1; print strftime("%s") , $0; system("") }; 
```

まずコマンドの先頭でOFS(output field separator)をカンマ区切りに変換していますが、その後のメインパートで何かしらのフィールド操作がないと有効にならないらしく、```$1=$1```でOFSの指定を有効化しています。
また、```awk```コマンドは通常だとバッファがコマンド終了まで出力されないらしく、リアルタイムに出力を出したい場合は、明示的にバッファを吐き出してあげる必要があります。
```awk```では```system("")```を呼び出すことでそれまでのバッファがflushされるようです。
このテクニックはドキュメントにも記載があります。
https://www.gnu.org/software/gawk/manual/html_node/I_002fO-Functions.html#index-buffers-2

## 統計のプロット
そしていよいよ収集した情報のプロットです。正直上記ができていれば後はもうなんでもいいと思うのですが、今回はpyqtgraphによるリアルタイムプロットを使ってみました。

pythonのグラフの描写用ライブラリといえば、一番有名なのは間違いなくmatplotlibだと思うのですが、欠点の一つにスピードの遅さがあります。pyqtgraphはリアルタイムプロットにおいて分があるようです。


### ファイルの監視
まずローカルに落としたファイルに追記があるかを別スレッドで定期的に確認させます。
追記があった場合、プロット対象のデータをそれぞれの配列に```append```します。
今回はmemory free,cpu usage,cpu io waitをプロットしてみました。

``` python3:filewatch.py
import time
import threading


class AwkClass:
    def handle_line(line: str):
        pass


class VmstatAwkClass(AwkClass):
    count: int = 0

    def __init__(self) -> None:
        super().__init__()
        self.timestamps = []
        self.memory_frees = []
        self.cpu_uses = []
        self.cpu_was = []

    def handle_line(self, line: str):
        self.count += 1
        # ヘッダ分はスキップする
        if self.count <= 2:
            return
        if line.endswith("\n"):
            line = line[:-1]
        columns = list(map(lambda s: float(s), line.split(",")))
        # timestamp r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
        timestamp, memory_free, cpu_us, cpu_wa = columns[0], columns[4], columns[13], columns[16]

        self.timestamps.append(timestamp)
        self.memory_frees.append(memory_free)
        self.cpu_uses.append(cpu_us)
        self.cpu_was.append(cpu_wa)


class Watcher:
    def __init__(self, file_path: str, awk: AwkClass):
        self.file_path = file_path
        self.aws = awk

    def activate(self):
        self.fs = open(self.file_path)
        self.flag = True
        thread = threading.Thread(target=self._watch)
        thread.start()

    def _watch(self):
        while self.flag:
            line = self.fs.readline()
            if not line:
                time.sleep(0.1)
                continue
            self._handle_line(line)

    def _handle_line(self, line: str):
        self.aws.handle_line(line)


if __name__ == "__main__":
    file_path = "/tmp/vmstat.log"
    vmstat_awk = VmstatAwkClass()
    watcher = Watcher(file_path=file_path, awk=vmstat_awk)
    watcher.activate()

```


### ファイルのプロット
あとは上記で作った配列を定期的に出力するプロット用のwidgetを作って起動すれば完了です。

まず利用するライブラリをinstallします。

``` bash
pip3 install pyqtgraph PyQt5
```


```python3:plot.py

from PyQt5 import QtWidgets, QtCore
import pyqtgraph as pg
import sys
from filewatch import Watcher, VmstatAwkClass


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, windowTitle: str, awk: VmstatAwkClass, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.awk = awk
        timestamp, mem_free, cpu_us, cpu_wa = self.__get_current_array()

        self.graphWidget = pg.GraphicsLayoutWidget(
            show=True, title=windowTitle)
        self.graphWidget.setBackground('w')

        pen = pg.mkPen(color=(255, 0, 0))
        self.memory_free_plot = self.graphWidget.addPlot(
            title="memory free").plot(timestamp, mem_free, pen=pen)

        self.cpu_us_plot = self.graphWidget.addPlot(
            title="cpu us").plot(timestamp, cpu_us, pen=pen)

        self.cpu_wa_plot = self.graphWidget.addPlot(
            title="cpu wa").plot(timestamp, cpu_wa, pen=pen)

        self.timer = QtCore.QTimer()
        self.timer.setInterval(50)
        self.timer.timeout.connect(self.update_plot_data)
        self.timer.start()

    def __get_current_array(self):
        return self.awk.timestamps[-100:], self.awk.memory_frees[-100:], self.awk.cpu_uses[-100:], self.awk.cpu_was[-100:]

    def update_plot_data(self):
        timestamp, mem_free, cpu_us, cpu_wa = self.__get_current_array()
        self.memory_free_plot.setData(timestamp, mem_free)
        self.cpu_us_plot.setData(timestamp, cpu_us)
        self.cpu_wa_plot.setData(timestamp, cpu_wa)


if __name__ == "__main__":
    awk = VmstatAwkClass()
    watcher = Watcher(f"/tmp/vmstat.log", awk)
    watcher.activate()

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow(f"vmstat instance", awk)
    w.show()
    sys.exit(app.exec_())
```

各項目のデータを最大100個表示するようにしました。
横軸はタイムスタンプなので時系列になっています。

こいつを実行してやるとめでたくリアルタイムにプロットされます。（ディスプレイがちっちゃいので少し窮屈なグラフになってしまいましたが、、、）

```bash
python3 plot.py
```

![vmstat-real-time.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261594/c9fcd78c-a16c-bef5-0d95-0be11d462449.gif)

あとは色々収集項目を変えたり、プロットの形式を変えたりすればいい感じの資料が作れそうですね。最後まで読んでいただきましてありがとうございます。