お金をかけずにサーバーの勉強をしよう

Windowsサーバーの負荷を自動でグラフ化

2022年6月20日

メニューへ戻る

Windowsサーバーの負荷データ取得」で Windowsサーバーの負荷状況を記録する簡便な方法を書きましたが、ここではもう一歩・二歩進んで、以下の事をやってみたいと思います。

  1. Powershellスクリプトでパフォーマンスカウンタの値を取得
  2. 取得した値を編集して外部の NoSQLデータベースに格納
  3. Pythonで NoSQLデータベースから値を取得
  4. Pythonで取得した値をグラフ化して画像ファイルにする

環境は以下でやります。

WindowsでPowershellを使って負荷データを取得しましょう。

パフォーマンスカウンタの取得には、Get-Counterコマンドレットを使います。
取得できるカウンタは、-ListSetオプションでこんな感じに検索できます。

PS C:\bin> get-counter -List "Network Interface*"

CounterSetName     : Network Interface
MachineName        : .
CounterSetType     : SingleInstance
Description        : Network Interface パフォーマンス オブジェクトには、ネットワーク接続を使用したバイトとパケットの送受信のレートを計測するカウンターがあります。接続エラーを監視するカウンターも含まれます。
Paths              : {\Network Interface(*)\Bytes Total/sec, \Network Interface(*)\Packets/sec, \Network Interface(*)\Packets Received/sec, \Network Interface(*)\Packets Sent/sec...}
PathsWithInstances : {\Network Interface(Intel[R] 82574L Gigabit Network Connection)\Bytes Total/sec, \Network Interface(Intel[R] 82574L Gigabit Network Connection)\Packets/sec, \Network Interface(Intel[R] 82574L Gigabit Network Connection)\Packets Received/sec, \Network Interface(Intel[R] 82574L Gigabit Network Connection)\Packets Sent/sec...}Counter: {\Network Interface(*)\Bytes Total/sec, \Network Interface(*)\Packets/sec, \Network Interface(*)\Packets Received/sec, \Network Interface(*)\Packets Sent/sec...}

ここでは以下の値を取ることにしました。

CPU使用率(%)¥Processor(_total)¥% Processor Time
メモリ使用量(byte)¥Memory¥Committed Bytes
ディスク読込量(byte)¥PhysicalDisk(_total)¥Avg. Disk Bytes/Read
ディスク書込量(byte)¥PhysicalDisk(_total)¥Avg. Disk Bytes/Write
ネット送信量(bytes)¥Network Interface(Intel[R] 82574L Gigabit Network Connection)¥Bytes Sent/sec
ネット受信量(bytes)¥Network Interface(Intel[R] 82574L Gigabit Network Connection)¥Bytes Received/sec

※ネットワークのカウンター名はハードウェアによって変わります。

Powershellスクリプト C:¥bin¥performance.ps1 はこんな感じ。

$counter_cpu = "\Processor(_total)\% Processor Time"
$counter_mem = "\Memory\Committed Bytes"
$counter_disk_r = "\PhysicalDisk(_total)\Avg. Disk Bytes/Read"
$counter_disk_w = "\PhysicalDisk(_total)\Avg. Disk Bytes/Write"
$counter_net_s  = "\Network Interface(Intel[R] 82574L Gigabit Network Connection)\Bytes Sent/sec"
$counter_net_r  = "\Network Interface(Intel[R] 82574L Gigabit Network Connection)\Bytes Received/sec"

$counters = @( $counter_cpu,
               $counter_mem,
               $counter_disk_r,
               $counter_disk_w,
               $counter_net_s,
               $counter_net_r   )

$results = ( $counters | Get-Counter ).CounterSamples

$value_cpu    = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_cpu }    ).CookedValue
$value_mem    = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_mem }    ).CookedValue
$value_disk_r = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_disk_r } ).CookedValue
$value_disk_w = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_disk_w } ).CookedValue
$value_net_s  = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_net_s }  ).CookedValue
$value_net_r  = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_net_r }  ).CookedValue

$value_cpu
$value_mem
$value_disk_r
$value_disk_w
$value_net_s
$value_net_r

カウンタ1つにつき Get-Counterを実行した方がシンプルじゃね?って思いますが、Get-Counterって結構時間がかかるんです。
このサンプルでは4つしかカウンタを取得していないので問題ないのですが、10個・20個とやり始めるとかなり実行時間が伸びてしまうので、Get-Counterを1回で済ませてからカウンタ毎に値を引っ張り出すことにしました。

カウンタ値は取得できるものという前提で、エラーハンドルはしていません。

PS C:\bin> .\performance.ps1
0.657068534423866
1997549568
0
4096
53.0253776511584
1431.68519658128

6つのカウンタの値を取得することがでました。


次は Azure Table Storageに格納するところを実装します。

これには Powershellの拡張モジュール「Azure Az PowerShell module」及び「AzTable」のインストールが必要です。
インストールの方法はこちら。

Install the Azure Az PowerShell module

この手順では「-Scope CurrentUser」としてインストール実行ユーザーに有効範囲を限定していますが、ここでは後工程でタスクスケジューラーからシステムユーザーの「SYSTEM」で実行させたいので、範囲を全ユーザーにします。

それぞれの範囲でインストール先が変わって、
AllUsers: $env:ProgramFiles\PowerShell\Modules
CurrentUser: $home\Documents\PowerShell\Modules
となります。

インストールします。

PS C:\bin> Install-Module -Name Az -Scope AllUsers -Repository PSGallery -Force

続行するには NuGet プロバイダーが必要です
PowerShellGet で NuGet ベースのリポジトリを操作するには、'2.8.5.201' 以降のバージョンの NuGet プロバイダーが必要です。NuGet プロバイダーは
'C:\Program Files\PackageManagement\ProviderAssemblies' または 'C:\Users\Administrator\AppData\Local\PackageManagement\ProviderAssemblies'
に配置する必要があります。'Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force' を実行して NuGet
プロバイダーをインストールすることもできます。今すぐ PowerShellGet で NuGet プロバイダーをインストールしてインポートしますか?
[Y] はい(Y)  [N] いいえ(N)  [S] 中断(S)  [?] ヘルプ (既定値は "Y"): Y

PS C:\bin> Install-Module -Name AzTable -Scope AllUsers -Repository PSGallery -Force
※インストールにはちょっと時間がかかります。
いくら待っても終わる様子がないので、試しに Enterキーを押してみると終わっていました。
※NuGetのインストールは初回のみと思います。

ここでテーブルの設計に入ります。

Azure Table Storageの構造はこうなっているようです。
Azure Table Storageの図
テーブルの中にパーティションがあり、このパーティション内の検索は早く、パーティションを跨ぐ検索は遅くなります。

データには必ず「パーティションキー」と「ローキー」を入れ、この2つのキーでユニーク(行が特定される)になるようにしなければいけません。

テーブル名は「PerformanceTable」とすることにしました。
このテーブルには、1分おきにサーバーの負荷データを書くことにして、1日に1回、24時間分(1440分)のデータを一気に読み込みたいのです。
そこで、パーティションキーは日付(YYYMMDD)にして、ローキーを時間(HHMM)にすることにします。
負荷のデータは1分に1回書き込まれるので、日付+時刻でユニークになるはずです。


これで一通りの準備ができましたので、スクリプトを以下のようにしました。

$now = [datetime]::Now
$date = $now.ToString("yyyyMMdd")
$time = $now.ToString("HHmm")
$table_name = "PerformanceTable"
$az_storage_context = New-AzStorageContext -ConnectionString "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;TableEndpoint=http://192.168.1.150:10002/devstoreaccount1;"

$counter_cpu    = "\Processor(_total)\% Processor Time"
$counter_mem    = "\Memory\Committed Bytes"
$counter_disk_r = "\PhysicalDisk(_total)\Avg. Disk Bytes/Read"
$counter_disk_w = "\PhysicalDisk(_total)\Avg. Disk Bytes/Write"
$counter_net_s  = "\Network Interface(Intel[R] 82574L Gigabit Network Connection)\Bytes Sent/sec"
$counter_net_r  = "\Network Interface(Intel[R] 82574L Gigabit Network Connection)\Bytes Received/sec"

$counters = @( $counter_cpu,
               $counter_mem,
               $counter_disk_r,
               $counter_disk_w,
               $counter_net_s,
               $counter_net_r   )

$results = ( $counters | Get-Counter ).CounterSamples

$value_cpu    = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_cpu }    ).CookedValue
$value_mem    = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_mem }    ).CookedValue
$value_disk_r = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_disk_r } ).CookedValue
$value_disk_w = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_disk_w } ).CookedValue
$value_net_s  = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_net_s }  ).CookedValue
$value_net_r  = ( $results | Where-Object { $_.Path -eq '\\' + $ENV:computername + $counter_net_r }  ).CookedValue

$cloudTable = (Get-AzStorageTable -Name $table_name -Context $az_storage_context).CloudTable

Add-AzTableRow -Table $cloudTable -PartitionKey $date -RowKey ($time) -property @{ "cpu" = $value_cpu ; "memory" = $value_mem; "disk_r" = $value_disk_r; "disk_w" = $value_disk_w; "net_s" = $value_net_s; "net_r" = $value_net_r } -UpdateExisting | Out-Null
※New-AzStorageContextの引数は -ConnectionStringだけの形を取っていますが、これは Azuriteだからで、本物の Azureの場合は、-StorageAccountKeyなどを使う形が普通かと思います。


実行してみると上手く動いているようです。 Get-AzTableRowコマンドレットでテーブルのデータを取ったところ、こんな感じの結果が取れました。

net_s          : 0
disk_w         : 0
net_r          : 1814.01576158379
disk_r         : 0
memory         : 1857855488
cpu            : 0
PartitionKey   : 20220620
RowKey         : 0733
TableTimestamp : 2022/06/20 7:33:08 +09:00
Etag           : W/"datetime'2022-06-19T22%3A33%3A08.684177Z'"



できあがった Powershellスクリプトをタスクスケジューラーで1分に1回実行するようにします。

Windows Server 2022で、[スタート]-[Windows管理ツール]-[タスクスケジューラ]と選択します。

右側にある[タスクの作成]クリックします。
Windowsタスクへの登録 1

[名前]に任意のタスク名を付けます。
このタスクの実行ユーザーを Administratorのままにしておくと、Administratorのパスワード変更の際にタスクに設定しているパスワードも変更しなければならなくなるので、実行ユーザーをパスワードが必要ないシステムユーザーの「SYSTEM」に変えたいです。
ユーザーまたはグループの変更を押します。
Windowsタスクへの登録 2

[選択するオブジェクト名を入力して下さい]に「SYSTEM」と入れ、OKを押します。
Windowsタスクへの登録 3

パフォーマンスカウンタの取得は、Administrator権限が必要なので、[最上位の特権で実行する]をチェックします。
Windowsタスクへの登録 4

[トリガー]タブに移って、新規を押します。
Windowsタスクへの登録 5

スケジュールはこんな感じにしました。
毎日実行・2022年6月20日 0:00開始
[間隔]: 1日
[繰り返し間隔]: 1分間(選択肢にはないが直接入力できる)
[継続時間]: 1日間
これで毎日1分間隔で実行されるスケジュールになります。
OKを押します。
Windowsタスクへの登録 6

トリガーが登録されました。
Windowsタスクへの登録 7

[操作]タブに移って、新規を押します。
Windowsタスクへの登録 8

以下を設定します。
[操作]: プログラムの開始
[プログラム/スクリプト]: powershell.exe
[引数の追加]: C:¥bin¥Performance.ps1
OKを押します。
Windowsタスクへの登録 9

操作が登録されました。
OKを押します。
Windowsタスクへの登録 10

タスクが登録されました。
Windowsタスクへの登録 11

登録されたタスクを選択して、右クリックするとメニューが出ますので、[実行する]を選択します。
Windowsタスクへの登録 12

これで次の日から1分おきに実行されて、Windows Server 2022の負荷データが、Azure Table Storage(ここではエミュレーターの Azurite)に書かれるはずです。


最後に Pythonのスクリプトで Azure Table Storageに書かれたデータを取得してグラフ化します。
バッチジョブ化をするため、Ubuntu Server 22.04で実行します。

Pythonで Azure Table Storageを使うAPIは激しく変わっていっているようですが、マイクロソフトは Azure CosmosDBと共通の APIにしたいらしく、2022年6月20日現在では、以下のサイトにあるものが最新の方法だと思われます。

クイックスタート: Python SDK と Azure Cosmos DB で Table API アプリを構築する

正直なところ、この数年で余りに変わり過ぎて着いて行けません…。
「azure-data-tables」というモジュールを使うようになりましたので、インストールします。

subro@UbuntuServer2204:~$ sudo pip install azure-data-tables
[sudo] subro のパスワード: パスワードを入れる
Collecting azure-data-tables
  Downloading azure_data_tables-12.4.0-py3-none-any.whl (113 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 113.9/113.9 KB 4.3 MB/s eta 0:00:00
Collecting azure-core<2.0.0,>=1.15.0
  Downloading azure_core-1.24.1-py3-none-any.whl (178 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 178.6/178.6 KB 11.1 MB/s eta 0:00:00
Collecting msrest>=0.6.21
  Downloading msrest-0.7.1-py3-none-any.whl (85 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 85.4/85.4 KB 13.8 MB/s eta 0:00:00
Collecting typing-extensions>=4.0.1
  Downloading typing_extensions-4.2.0-py3-none-any.whl (24 kB)
Requirement already satisfied: requests>=2.18.4 in /usr/lib/python3/dist-packages (from azure-core<2.0.0,>=1.15.0->azure-data-tables) (2.25.1)
Requirement already satisfied: six>=1.11.0 in /usr/lib/python3/dist-packages (from azure-core<2.0.0,>=1.15.0->azure-data-tables) (1.16.0)
Requirement already satisfied: certifi>=2017.4.17 in /usr/lib/python3/dist-packages (from msrest>=0.6.21->azure-data-tables) (2020.6.20)
Collecting isodate>=0.6.0
  Downloading isodate-0.6.1-py2.py3-none-any.whl (41 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.7/41.7 KB 13.4 MB/s eta 0:00:00
Collecting requests-oauthlib>=0.5.0
  Downloading requests_oauthlib-1.3.1-py2.py3-none-any.whl (23 kB)
Requirement already satisfied: oauthlib>=3.0.0 in /usr/lib/python3/dist-packages (from requests-oauthlib>=0.5.0->msrest>=0.6.21->azure-data-tables) (3.2.0)
Installing collected packages: typing-extensions, requests-oauthlib, isodate, azure-core, msrest, azure-data-tables
Successfully installed azure-core-1.24.1 azure-data-tables-12.4.0 isodate-0.6.1 msrest-0.7.1 requests-oauthlib-1.3.1 typing-extensions-4.2.0
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

いつものように、sudoを使っているので怒られますが無視します。
subroユーザーでやっても良かったかな、と思い始めていますが…。

Pythonでグラフ作成をするのに、matplotlibを使いますので、このモジュールもインストールします。

subro@UbuntuServer2204:~$ sudo pip install matplotlib
Collecting matplotlib
  Downloading matplotlib-3.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.9 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.9/11.9 MB 10.1 MB/s eta 0:00:00
Collecting kiwisolver>=1.0.1
  Downloading kiwisolver-1.4.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 11.3 MB/s eta 0:00:00
Collecting numpy>=1.17
  Downloading numpy-1.22.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (16.8 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.8/16.8 MB 8.9 MB/s eta 0:00:00
Requirement already satisfied: pyparsing>=2.2.1 in /usr/lib/python3/dist-packages (from matplotlib) (2.4.7)
Collecting cycler>=0.10
  Downloading cycler-0.11.0-py3-none-any.whl (6.4 kB)
Collecting fonttools>=4.22.0
  Downloading fonttools-4.33.3-py3-none-any.whl (930 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 930.9/930.9 KB 11.7 MB/s eta 0:00:00
Collecting packaging>=20.0
  Downloading packaging-21.3-py3-none-any.whl (40 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.8/40.8 KB 10.5 MB/s eta 0:00:00
Collecting pillow>=6.2.0
  Downloading Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.1 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.1/3.1 MB 11.1 MB/s eta 0:00:00
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (2.8.2)
Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)
Installing collected packages: pillow, packaging, numpy, kiwisolver, fonttools, cycler, matplotlib
Successfully installed cycler-0.11.0 fonttools-4.33.3 kiwisolver-1.4.3 matplotlib-3.5.2 numpy-1.22.4 packaging-21.3 pillow-9.1.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

IPA日本語フォントを使いたいのでインストールしておきます。

subro@UbuntuServer2204:~$ sudo apt install -y fonts-ipafont
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています... 完了
状態情報を読み取っています... 完了
以下の追加パッケージがインストールされます:
  fonts-ipafont-gothic fonts-ipafont-mincho
以下のパッケージが新たにインストールされます:
  fonts-ipafont fonts-ipafont-gothic fonts-ipafont-mincho
アップグレード: 0 個、新規インストール: 3 個、削除: 0 個、保留: 0 個。
8,246 kB のアーカイブを取得する必要があります。
この操作後に追加で 28.7 MB のディスク容量が消費されます。
取得:1 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-gothic all 00303-21ubuntu1 [3,513 kB]
取得:2 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-mincho all 00303-21ubuntu1 [4,724 kB]
取得:3 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont all 00303-21ubuntu1 [8,964 B]
8,246 kB を 2秒 で取得しました (3,660 kB/s)
以前に未選択のパッケージ fonts-ipafont-gothic を選択しています。
(データベースを読み込んでいます ... 現在 122368 個のファイルとディレクトリがインストールされています。)
.../fonts-ipafont-gothic_00303-21ubuntu1_all.deb を展開する準備をしています ...
fonts-ipafont-gothic (00303-21ubuntu1) を展開しています...
以前に未選択のパッケージ fonts-ipafont-mincho を選択しています。
.../fonts-ipafont-mincho_00303-21ubuntu1_all.deb を展開する準備をしています ...
fonts-ipafont-mincho (00303-21ubuntu1) を展開しています...
以前に未選択のパッケージ fonts-ipafont を選択しています。
.../fonts-ipafont_00303-21ubuntu1_all.deb を展開する準備をしています ...
fonts-ipafont (00303-21ubuntu1) を展開しています...
fonts-ipafont-mincho (00303-21ubuntu1) を設定しています ...
update-alternatives: /usr/share/fonts/truetype/fonts-japanese-mincho.ttf (fonts-japanese-mincho.ttf) を提供するために自動モードで /usr/share/fonts/opentype/ipafont-mincho/ipam.ttf を使います
fonts-ipafont-gothic (00303-21ubuntu1) を設定しています ...
update-alternatives: /usr/share/fonts/truetype/fonts-japanese-gothic.ttf (fonts-japanese-gothic.ttf) を提供するために自動モードで /usr/share/fonts/opentype/ipafont-gothic/ipag.ttf を使います
fonts-ipafont (00303-21ubuntu1) を設定しています ...
Scanning processes...
Scanning candidates...
Scanning linux images...

Running kernel seems to be up-to-date.

Restarting services...
 systemctl restart cron.service

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.

一通りインストールできました。


Pythonスクリプトはこんな感じで「performance.py」としています。
デフォルトでは実行日の前日のデータを取得するようにしていて、第一引数にYYYYMMDDの形で日付を与えるとその日のデータを取得します。
実行した時のカレントディレクトリに「YYYYMMDD.png」という 800x1200 の画像ファイルができます。

# コマンドライン引数の処理
import sys
import datetime

args = sys.argv

if len(args) > 1:
    if len(args[1]) != 8:
        print("入力日付が間違っています")
        sys.exit(1)
    try:
        print("第1引数:" + args[1])
        checked_date = datetime.datetime.strptime(args[1], "%Y%m%d")
        target_date = args[1]
    except:
        print("入力日付が間違っています")
        sys.exit(1)
else:
    today = datetime.date.today()
    yesterday = today + datetime.timedelta(days=-1)
    target_date = yesterday.strftime("%Y%m%d")

#################################################################
# 00:00〜23:59まで、時刻のリスト(1440行)を作る
#################################################################
time_list=[]

for i in range(24):                                        # 24時間のループ
    for j in range(60):                                    # 60分のループ
        time_list.append( '{:02}{:02}'.format(i, j) )

#################################################################
# 00:00〜22:59まで、カウンタの値が0になっている仮の辞書を作る
#################################################################
time_dict = dict.fromkeys( time_list, [ 0, 0, 0, 0, 0, 0] )

#############################################################################
# Windowsパフォーマンスカウンタ(Azule Tablesに記録したもの)から辞書を作る
#############################################################################
# Azule tables から取得するデータを入れる空の辞書作成
tables_dict = {}

from azure.data.tables import TableServiceClient

# テーブル操作のためのハンドラ取得
service = TableServiceClient.from_connection_string(conn_str='DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;TableEndpoint=http://192.168.1.150:10002/devstoreaccount1;')
table_client = service.get_table_client(table_name="PerformanceTable")

# 日付をキーにしてデータ検索
next_pk = None
next_rk = None
while True:
    entities = table_client.query_entities( "PartitionKey eq '" + target_date + "'", next_partition_key = next_pk, next_row_key = next_rk )

    # 検索結果の全行取り出しながら time_dict と同じ形の辞書を作っていく
    for entity in entities:
        try:
            tables_dict[ entity['RowKey'] ] =  ( entity['cpu'],
                                                 ( 1 - ( entity['memory'] / 8589934592 ) ) * 100,
                                                 entity['disk_r'] / 1000000,
                                                 entity['disk_w'] / 1000000,
                                                 entity['net_s'] / 1250000,
                                                 entity['net_r'] / 1250000
                                               )
        except:
            print( "取れないキーがあります。" + entity['RowKey'] )

    if hasattr( entities, 'x_ms_continuation' ):
        x_ms_continuation = getattr( entities, 'x_ms_continuation' )
        next_pk = x_ms_continuation[ 'nextpartitionkey' ]
        next_rk = x_ms_continuation[ 'nextrowkey' ]
    else:
        break;

###################################################################################
# time_dict に tables_dict を被せることで、取得できた時刻には取得した値が入り、
# 取得できなかった時刻の値は0になり、
# 全体で 24時間×60分の 1440 個の辞書に揃えられる
###################################################################################
time_dict.update( tables_dict )

# 時刻でのソートをしたいため辞書からリストに変更する
# [('0000', [0, 0, 0]), ('2359', [0, 0, 0])] のような階層のリスト(中にタプル)になる
# やらなくても並んでいるのかも知れない
perf_list = list( time_dict.items() )

# 時刻でソート
perf_list.sort()

# 各負荷のグラフ用にそれぞれの空のリストを作っておく
cpu_list = []
memory_list = []
disk_r_list = []
disk_w_list = []
net_s_list = []
net_r_list = []

# 各負荷値をそれぞれのリストに追加していく
for time_line in perf_list:
    cpu_list.append( time_line[1][0] )
    memory_list.append( time_line[1][1] )
    disk_r_list.append( time_line[1][2] )
    disk_w_list.append( time_line[1][3] )
    net_s_list.append( time_line[1][4] )
    net_r_list.append( time_line[1][5] )

#####################################################################################
# ここからグラフ描画
#####################################################################################
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

plt.rc( 'font',   family    = 'IPAGothic' )  # タイトルや軸名に日本語を使いたいため
plt.rc( 'xtick',  direction = 'in'        )  # X軸のメモリ線を内向きに
plt.rc( 'ytick',  direction = 'in'        )  # Y軸のメモリ線を内向きに
plt.rc( 'figure', figsize   = [ 8,12 ]    )  # 画像は 800x1200 のサイズになる

# fig, ax とカンマで区切っているのは Python の変数初期化の方法で、一度に複数の変数を初期化する方法
# matplotlib.pyplot.subplots()メソッドを実行すると、
#  のインスタンスと
#  のインスタンスを両方作ってくれる
# fig が絵全体
# ax に対して、縦4列、横1列のグラフを作る。結果として ax はax[0]とax[1]…というリストになる
fig, ax = plt.subplots( 4, 1 )

#####################################
#  1つ目のグラフ(CPU使用率)
#####################################

# グラフタイトル
ax[0].set_title( 'CPU使用率(%)' )
# グラフの描画
#    plot メソッド 第1引数: X軸の目盛
#                   第2引数: 実データ
#                   第3引数: X軸の凡例名(グラフに表示するのば別なところでやってる)
ax[0].plot( time_list, cpu_list, linewidth = 0.5 )
# X軸の目盛表示間隔を60分毎に
#    axesオブジェクト(ax[0])には,オブジェクト(ax[0].xaxis)が含まれてる
ax[0].xaxis.set_major_locator( ticker.MultipleLocator(60) )
# XY軸目盛りの装飾
#    axssオブジェクトのtick_paramsメソッド
#        パラメータ名を羅列して設定していく
#        axis: 軸指定
#        labelsize: フォントサイズ
#        labelrotation: 文字の回転(90度) 
ax[0].tick_params( axis = 'x', labelsize = 7, labelrotation = 90 )
ax[0].tick_params( axis = 'y', labelsize = 7 )
# XY軸の限界指定(限界というより軸の値の固定?)
#    X軸: 0000〜2359 (24時間)
#    Y軸: 0〜100 (%)
ax[0].set_xlim( 0, 60 * 24 )
ax[0].set_ylim( 0, 100 )
# グラフ内にグリッド線を描く
ax[0].grid( True )
# グラフ内に凡例を表示させる
#ax[0].legend() #グラフが1本しかないので非表示としてコメントアウト

#####################################
#  2つ目のグラフ(メモリ使用率)
#####################################

# グラフタイトル
ax[1].set_title('メモリ使用率(物理8GBに対しての仮想メモリ全体)(%)')
ax[1].plot( time_list, memory_list, linewidth = 0.5 )
ax[1].xaxis.set_major_locator(ticker.MultipleLocator(60))
ax[1].tick_params( axis = 'x', labelsize = 7, labelrotation = 90 )
ax[1].tick_params( axis = 'y', labelsize = 7 )
ax[1].set_xlim( 0, 60 * 24 )
ax[1].set_ylim( 0, 100 )
ax[1].grid( True )
#ax[1].legend() #グラフが1本しかないので非表示としてコメントアウト

#########################################
#  3つ目のグラフ(ディスク転送量)
#########################################

# グラフタイトル
ax[2].set_title('ディスク転送量(MB)')
ax[2].plot( time_list, disk_r_list, linewidth = 0.5, label = '読込' )
ax[2].plot( time_list, disk_w_list, linewidth = 0.5, label = '書込' )
ax[2].xaxis.set_major_locator(ticker.MultipleLocator(60))
ax[2].tick_params( axis = 'x', labelsize = 7, labelrotation = 90 )
ax[2].tick_params( axis = 'y', labelsize = 7 )
ax[2].set_xlim( 0, 60 * 24 )
ax[2].set_ylim( 0, 100 )
ax[2].grid( True )
ax[2].legend()

#########################################
#  4つ目のグラフ(ネットワーク使用率(%))
#########################################

# グラフタイトル
ax[3].set_title('ネットワーク使用率(1Gbit/秒)(%)')
ax[3].plot( time_list, net_s_list, linewidth = 0.5, label = '送信' )
ax[3].plot( time_list, net_r_list, linewidth = 0.5, label = '受信' )
ax[3].xaxis.set_major_locator( ticker.MultipleLocator(60) )
ax[3].tick_params( axis = 'x', labelsize = 7, labelrotation = 90 )
ax[3].tick_params( axis = 'y', labelsize = 7 )
ax[3].set_xlim( 0, 60 * 24 )
ax[3].set_ylim( 0, 100 )
ax[3].grid( True )
ax[3].legend()

########################################
# 描画
########################################
fig.tight_layout()

# png形式ファイルとして出力(YYYYMMDD.png)
# ファイルがあっても上書き
fig.savefig(target_date + '.png')


実行してみます。

subro@UbuntuServer2204:~/work$ ls -l
合計 12
-rw-rw-r-- 1 subro subro 9272  6月 20 13:19 performance.py

subro@UbuntuServer2204:~/work$ python3 performance.py

subro@UbuntuServer2204:~/work$ ls -l
合計 72
-rw-rw-r-- 1 subro subro 59805  6月 20 13:31 20220619.png
-rw-rw-r-- 1 subro subro  9272  6月 20 13:19 performance.py

できあがりはこんな絵になります。
サーバー負荷グラフ

これを cronかなにかで、毎日0:00分にでも実行するようにすれば、サーバーの負荷グラフの画像ファイルが毎日自動でできるという寸法です。


いくつかの要素を組み合わせてサーバーメトリクスの自動グラフ化というテーマをやってみましたが結構ハマりました。

マイクロソフトの Azure Table Storageをリモートデータベースとして使うようにしたため、Powershellと Pythonとでマイクロソフト製の SDKを使わないといけなくなったのですが、Azure Table Storageは後発の CosmosDBと API共通化が進められているところで、最近になってからの変更が大きかったです。
その割にマイクロソフトのサイトの説明はアレなのでかなり苦労しました。

最近 Azureでは CosmosDB推しが強くて Tableの存在感が薄いのですが、CosmosDBは課金が高くて中々手を出すことができません。
一方で、Tableは極端に安いので、CosmosDBを使う程の要件がないのなら、まず Tableの利用を考えてみるのが良いと思います。

でも MongoDBでやれば良かったと思いました。

Python + matplotlib については、これはもうスゴイとしか言いようが無く、こんなグラフをフリーのツールで作れるなんて素晴らしいです。
matplotlibに画像ファイル出力ができる機能があるため、Excelで作るよりも大幅に手間を省いてくれます。
サーバーメトリクスは数字の羅列でしかなく見える化するためにはグラフにするのが一番です。
それをここまで自動でできてしまうなら、もっと他の値も色々取って分析にまわしてみるのも面白いと思います。