この記事は技術的にコアな内容を含みます!
当記事に書いてある内容を実践して、何らかの損害が発生しても、
当ブログ主は一切の責任は負いません。全て自己責任といたします!
今回は XserverVPS においてメモリの使用状況を知りたかったので、
ChatGPTと共同でメモリのリソース状況の確認方法を模索しました。
XserverVPS のサーバーコントロールパネルには以下のリソース状況確認項目がありました。
CPU
ディスクI/O
転送量
しかし、メモリの項目はなかったため、自作してリソース状況を把握することにしました。
メモリのリソース状況の把握には、以下の条件付けを行いました。
条件その1:
基本的にサーバーに負荷をかけたり、重い挙動にならないこと
条件その2:
統計データが欲しいので、後から見直すことができるようにすること
条件その3:
統計データを数字だけでなく、視覚的にも理解できるようにすること
環境:
サーバーのホストOSはRockyLinux9.4
ローカル環境のOSはWindows10
使用した言語:
シェルスクリプト(Linux)
PowerShell(ver5.1)
ExcelVBA(Excel2019を使用)
手順1:sysstat(sar)の導入
sysstat(sar)を利用してメモリ使用状況をログ化する
下記の通り、sysstat(sar)をインストールし、サービスを常駐させる。
sudo dnf install sysstat sudo systemctl enable --now sysstat
メモリリソース確認例:
sar -r # メモリ使用率(履歴)を確認 sar -S # スワップの利用状況(履歴)を確認
/var/log/sa/saXX に日ごと保存され、CPU・メモリをまとめて確認可能。
記録の仕組み:
自動で 10分間隔くらいで CPU/メモリの統計を /var/log/sa/saXX に保存してくれます
※XXは日付
負荷はほぼゼロに近いので常時入れても問題ありません。
手順2:sysstat(sar)のログを整形、送信するスクリプト
ChatGPTと共同して最終的に以下のシェルスクリプトを考案しました。
ファイル名は「send_sar_mem_csv_mail.sh」となります。
※必ずメールアドレスに自分のメールアドレスを指定してください
#!/usr/bin/env bash # send_sar_mem_csv_mail.sh # 昨日分の sar ログを CSV(カンマ区切り) にして mailx で送信する set -euo pipefail # ===== 基本設定 ===== RECIPIENT="★メールアドレス★" # ←メール送信先★要変更★ HOST="$(hostname -f 2>/dev/null || hostname)" # デフォルトは昨日。外から YDAY_NUM / YDAY_ISO / SA_FILE を与えればそれを優先 YDAY_NUM="${YDAY_NUM:-$(date -d 'yesterday' +%d)}" YDAY_ISO="${YDAY_ISO:-$(date -d 'yesterday' +%F)}" SA_FILE="${SA_FILE:-/var/log/sa/sa${YDAY_NUM}}" OUTDIR="/var/log/sar_exports" SUBJECT="[${HOST}] SAR memory report ${YDAY_ISO}" BODY="sar のメモリ・スワップ統計を添付します。対象日: ${YDAY_ISO}" # ===== 前提チェック ===== command -v sadf >/dev/null || { echo "sadf(sysstat) が未インストールです"; exit 2; } command -v mailx >/dev/null || { echo "mailx コマンドがありません (s-nail)"; exit 2; } [[ -r "${SA_FILE}" ]] || { echo "SARファイルがありません: ${SA_FILE}"; exit 3; } # ===== CSV 生成(; → , へ変換して Excel で開きやすく)===== mkdir -p "${OUTDIR}" MEM_CSV="${OUTDIR}/${HOST}_mem_${YDAY_ISO}.csv" SWP_CSV="${OUTDIR}/${HOST}_swap_${YDAY_ISO}.csv" # メモリ(sar -r)/ スワップ(sar -S) # sadf -d はセミコロン区切りなので tr でカンマに変換 sadf -d "${SA_FILE}" -- -r | tr ';' ',' > "${MEM_CSV}" sadf -d "${SA_FILE}" -- -S | tr ';' ',' > "${SWP_CSV}" # (任意)60日より前の古い CSV を整理 find "${OUTDIR}" -type f -name "${HOST}_*.csv" -mtime +60 -delete || true # ===== 添付して送信(mailx -a)===== echo "${BODY}" | mailx -s "${SUBJECT}" \ -a "${MEM_CSV}" \ -a "${SWP_CSV}" \ "${RECIPIENT}"
上記シェルスクリプトに実行権限を与え、crontab に登録し、日時処理とする
0 4 * * * root /home/user/cron/send_sar_mem_csv_mail.sh # sysstat導入
上記は crontab 追記用のサンプル。実際のコードはスクリプトを置いた位置に設定。
また、サンプルでは毎日AM4:00に実行。
正しくスクリプトが動作するかの確認は、以下のコマンドで実行可能
※ただしsysstat(sar)のサービスを有効化してから最低10分経ってから(ログ生成待機)
cd ★シェルスクリプトを置いたパス★ YDAY_NUM=$(date +%d) SA_FILE=/var/log/sa/sa$(date +%d) ./send_sar_mem_csv_mail.sh
手順3:PowerShellでメール添付ログのExcelデータ置換
手順2の send_sar_mem_csv_mail.sh を実装して正確に実行できると
メモリのリソース状況のCSVログがメールに添付されて送られてきます。
以下のようなファイルがメールで添付されていれば成功です。
※hostnameの部分は環境によって異なり、日付は先のサンプル手動実行では当日
hostname_mem_2025-09-26.csv
hostname_swap_2025-09-26.csv
以下のPowerShellスクリプトを「MakeExcelSarReport.ps1」というファイル名で保存。
# MakeExcelSarReport.ps1 # 使い方: # - 何も渡さず実行 → ファイル選択ダイアログで mem/swap CSV を選ぶ # - mem.csv, swap.csv, 出力xlsx をドラッグ&ドロップ(順不同でもOK) # - フォルダを渡すと、その中から *_mem_*.csv / *_swap_*.csv を自動検出 # MakeExcelSarReport.ps1 (PS 5.1 compatible) param( [string]$MemCsv, [string]$SwapCsv, [string]$OutXlsx, [switch]$ShowExcel ) # ---------- ユーティリティ ---------- # UTC文字列 → JST の [datetime] function To-JstDate { param([string]$s) $t = $s -replace '\s*UTC$','' $ci = [System.Globalization.CultureInfo]::InvariantCulture $style = [System.Globalization.DateTimeStyles]::AssumeUniversal try { $dto = [DateTimeOffset]::Parse($t, $ci, $style) } catch { $fmt = "yyyy-MM-dd HH:mm:ss 'UTC'" $dto = [DateTimeOffset]::ParseExact($s, $fmt, $ci, $style) } return $dto.ToOffset([TimeSpan]::FromHours(9)).DateTime # [datetime] を返す } # UTC文字列 → JST OADate(double) function To-JstOaDouble { param([string]$s) $dt = To-JstDate $s # 必ず [datetime] return ($dt.ToOADate()) # 必ず [double] } function Resolve-PathSafe { param([string]$PathLike) if (-not $PathLike) { return $null } try { $rp = Resolve-Path -LiteralPath $PathLike -ErrorAction Stop return $rp.Path } catch { return $null } } function Resolve-PathsFromArgs { param([string[]]$Inputs) $result = [ordered]@{ MemCsv = $null SwapCsv = $null OutXlsx = $null } foreach ($inp in $Inputs) { $p = Resolve-PathSafe $inp if (-not $p) { continue } if (Test-Path -LiteralPath $p -PathType Container) { # フォルダ → 中から検出 $mem = Get-ChildItem -LiteralPath $p -File -Recurse -Include *_mem_*.csv -ErrorAction SilentlyContinue | Select-Object -First 1 $swp = Get-ChildItem -LiteralPath $p -File -Recurse -Include *_swap_*.csv -ErrorAction SilentlyContinue | Select-Object -First 1 if ($mem) { $result['MemCsv'] = $mem.FullName } if ($swp) { $result['SwapCsv'] = $swp.FullName } continue } if ($p -match '(_mem_).+\.csv$') { $result['MemCsv'] = $p; continue } if ($p -match '(_swap_).+\.csv$') { $result['SwapCsv'] = $p; continue } if ($p -match '\.xlsx$') { $result['OutXlsx'] = $p; continue } } return $result } # 引数/ドラッグ&ドロップから自動解決 $inputs = @() if ($MemCsv) { $inputs += $MemCsv } if ($SwapCsv) { $inputs += $SwapCsv } if ($OutXlsx) { $inputs += $OutXlsx } if ($args -and $args.Count -gt 0) { $inputs += $args } $auto = Resolve-PathsFromArgs $inputs if (-not $MemCsv) { $MemCsv = $auto['MemCsv'] } if (-not $SwapCsv) { $SwapCsv = $auto['SwapCsv'] } if (-not $OutXlsx) { $OutXlsx = $auto['OutXlsx'] } # 入力が未確定ならダイアログ Add-Type -AssemblyName System.Windows.Forms | Out-Null function Pick-File([string]$title) { $dlg = New-Object System.Windows.Forms.OpenFileDialog $dlg.Title = $title $dlg.Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*" $dlg.Multiselect = $false if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { return $dlg.FileName } return $null } if (-not $MemCsv) { $MemCsv = Pick-File "mem CSV(*_mem_*.csv)を選択" } if (-not $SwapCsv) { $SwapCsv = Pick-File "swap CSV(*_swap_*.csv)を選択" } if (-not (Test-Path -LiteralPath $MemCsv -PathType Leaf)) { throw "mem CSV が不正: $MemCsv" } if (-not (Test-Path -LiteralPath $SwapCsv -PathType Leaf)) { throw "swap CSV が不正: $SwapCsv" } # ===== OutXlsx(未指定時)の自動命名:SAR_Report_YYYYMMDD.xlsx ===== if (-not $OutXlsx) { # 1) ファイル名から抽出(*_mem_YYYY-MM-DD.csv) $reportDate = $null if ($MemCsv -match '_(\d{4})-(\d{2})-(\d{2})\.csv$') { $reportDate = "$($matches[1])$($matches[2])$($matches[3])" # YYYYMMDD } # 2) 取れなければデータの timestamp から抽出 if (-not $reportDate) { $first = $mem | Select-Object -First 1 if ($first -and $first.timestamp -match '(\d{4})-(\d{2})-(\d{2})') { $reportDate = "$($matches[1])$($matches[2])$($matches[3])" } } # 3) 出力パスを決定(mem.csv と同フォルダ) $dir = Split-Path -Path $MemCsv -Parent $name = if ($reportDate) { "SAR_Report_$reportDate.xlsx" } else { "SAR_Report.xlsx" } $OutXlsx = Join-Path -Path $dir -ChildPath $name } # ---------- CSV 読み込み ---------- function Import-SarCsv { param([string]$Path) $raw = Get-Content -LiteralPath $Path -Raw $lines = $raw -split "`r?`n" | Where-Object { $_ -and $_.Trim() -ne "" } if ($lines.Count -lt 2) { throw "CSVに行がありません: $Path" } # 1) 実ヘッダを特定: '# 'で始まり 'timestamp' を含む行を優先 $headerIndex = $null for ($i=0; $i -lt $lines.Count; $i++) { $ln = $lines[$i] if ($ln -match '^\s*#' -and $ln -match '(?i)\btimestamp\b') { $headerIndex = $i; break } } # 念のため:見つからなければ 'timestamp' を含む最初の行をヘッダに if ($null -eq $headerIndex) { for ($i=0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '(?i)\btimestamp\b') { $headerIndex = $i; break } } } if ($null -eq $headerIndex) { throw "ヘッダ行(timestamp を含む)が見つかりません: $Path" } # 2) ヘッダ行の整形(先頭の '# ' を除去) $headerRaw = $lines[$headerIndex] $header = ($headerRaw -replace '^\s*#\s*','').Trim() # 3) クリーン配列を作る:ヘッダを1つだけ入れ、以降のデータ行を追加 $clean = New-Object System.Collections.Generic.List[string] $clean.Add($header) | Out-Null for ($j=$headerIndex+1; $j -lt $lines.Count; $j++) { $ln = $lines[$j] # LINUX-RESTART 等は除外 if ($ln -match 'LINUX-RESTART') { continue } # 重複ヘッダを除外('#'付きでも同一内容なら弾く) $lnStripped = ($ln -replace '^\s*#\s*','').Trim() if ($lnStripped -eq $header) { continue } # コメント行は基本除外 if ($ln -match '^\s*#') { continue } $clean.Add($ln) | Out-Null } # 4) ConvertFrom-Csv(カンマ区切り前提) ($clean -join "`r`n") | ConvertFrom-Csv -Delimiter ',' } $mem = Import-SarCsv $MemCsv $swp = Import-SarCsv $SwapCsv function To-Double([object]$v) { if ($null -eq $v -or $v -eq "") { return $null } [double]$v } $mem | ForEach-Object { $_.'%memused' = To-Double $_.'%memused' $_.kbavail = To-Double $_.kbavail $_.'%commit' = To-Double $_.'%commit' } $swp | ForEach-Object { $_.'%swpused' = To-Double $_.'%swpused' } function New-Stats([double[]]$arr){ $list = @() foreach ($x in $arr) { if ($x -ne $null) { $list += $x } } if ($list.Count -eq 0) { return [pscustomobject]@{Avg=$null;Max=$null;Min=$null} } [pscustomobject]@{ Avg = ($list | Measure-Object -Average).Average Max = ($list | Measure-Object -Maximum).Maximum Min = ($list | Measure-Object -Minimum).Minimum } } $memusedStats = New-Stats ($mem | ForEach-Object { $_.'%memused' }) $kbavailStats = New-Stats ($mem | ForEach-Object { $_.kbavail }) $commitStats = New-Stats ($mem | ForEach-Object { $_.'%commit' }) $swpusedStats = New-Stats ($swp | ForEach-Object { $_.'%swpused' }) # ---------- Excel COM ---------- $xlLine = 4 $xlLegendPositionBottom = -4107 $excel = New-Object -ComObject Excel.Application $excel.Visible = [bool]$ShowExcel $wb = $excel.Workbooks.Add() $wsSwap = $wb.Worksheets.Item(1); $wsSwap.Name = "swap" $wsMem = $wb.Worksheets.Add(); $wsMem.Name = "mem" $wsSummary = $wb.Worksheets.Add(); $wsSummary.Name = "Summary" # Summary $wsSummary.Range("A1").Value2 = "Metric" $wsSummary.Range("B1").Value2 = "Avg" $wsSummary.Range("C1").Value2 = "Max" $wsSummary.Range("D1").Value2 = "Min" $rows = @( @("%memused(%)", [math]::Round($memusedStats.Avg,2), [math]::Round($memusedStats.Max,2), [math]::Round($memusedStats.Min,2)), @("kbavail(KB)", [math]::Round($kbavailStats.Avg,0), [math]::Round($kbavailStats.Max,0), [math]::Round($kbavailStats.Min,0)), @("%commit(%)", [math]::Round($commitStats.Avg,2), [math]::Round($commitStats.Max,2), [math]::Round($commitStats.Min,2)), @("%swpused(%)", [math]::Round($swpusedStats.Avg,2), [math]::Round($swpusedStats.Max,2), [math]::Round($swpusedStats.Min,2)) ) $r=2 foreach($row in $rows){ $wsSummary.Cells.Item($r,1).Resize(1,4).Value2 = $row $r++ } $wsSummary.Columns.AutoFit() | Out-Null # mem シート $wsMem.Range("A1").Value2 = "timestamp" $wsMem.Range("B1").Value2 = "%memused" $wsMem.Range("C1").Value2 = "kbavail" $wsMem.Range("D1").Value2 = "%commit" # 列Aの書式を変更 $wsMem.Range("A:A").NumberFormat = "hh:mm" $r=2 foreach($o in $mem){ $wsMem.Cells.Item($r,1).Value2 = (To-JstOaDouble $o.timestamp) # ★JSTにして書き込む $wsMem.Cells.Item($r,2).Value2 = $o.'%memused' $wsMem.Cells.Item($r,3).Value2 = $o.kbavail $wsMem.Cells.Item($r,4).Value2 = $o.'%commit' $r++ } $lastRowMem = $r-1 $wsMem.Columns.AutoFit() | Out-Null # 折れ線(%memused + kbavail(第2軸)) $co1 = $wsMem.ChartObjects().Add(10,10,780,360) $c1 = $co1.Chart $c1.ChartType = $xlLine $c1.HasTitle = $true $c1.ChartTitle.Text = "Memory usage trend" # 系列1: %memused(第1軸) $s1 = $c1.SeriesCollection().NewSeries() $s1.Name = "%memused" $s1.XValues = $wsMem.Range("A2:A$lastRowMem") $s1.Values = $wsMem.Range("B2:B$lastRowMem") # 系列2: kbavail(第2軸) $s2 = $c1.SeriesCollection().NewSeries() $s2.Name = "kbavail" $s2.XValues = $wsMem.Range("A2:A$lastRowMem") $s2.Values = $wsMem.Range("C2:C$lastRowMem") $s2.AxisGroup = 2 # 第2縦軸 # 系列3: %commit(第1軸) $s3 = $c1.SeriesCollection().NewSeries() $s3.Name = "%commit" $s3.XValues = $wsMem.Range("A2:A$lastRowMem") $s3.Values = $wsMem.Range("D2:D$lastRowMem") # 第1軸に重ねるので AxisGroup 設定は不要(既定が第1軸 $c1.Legend.Position = $xlLegendPositionBottom # swap シート $wsSwap.Range("A1").Value2 = "timestamp" $wsSwap.Range("B1").Value2 = "%swpused" # 列Aの書式を変更 $wsSwap.Range("A:A").NumberFormat = "hh:mm" $u=2 foreach($o in $swp){ $wsSwap.Cells.Cells.Item($u,1).Value2 = (To-JstOaDouble $o.timestamp) # ★JST $wsSwap.Cells.Item($u,2).Value2 = $o.'%swpused' $u++ } $lastRowSwp = $u-1 $wsSwap.Columns.AutoFit() | Out-Null # 折れ線(%swpused) $co2 = $wsSwap.ChartObjects().Add(10,10,780,360) $c2 = $co2.Chart $c2.ChartType = $xlLine $c2.HasTitle = $true $c2.ChartTitle.Text = "Swap usage trend" $s3 = $c2.SeriesCollection().NewSeries() $s3.Name = "%swpused" $s3.XValues = $wsSwap.Range("A2:A$lastRowSwp") $s3.Values = $wsSwap.Range("B2:B$lastRowSwp") $c2.Legend.Position = $xlLegendPositionBottom # 保存 if (Test-Path -LiteralPath $OutXlsx) { Remove-Item -LiteralPath $OutXlsx -Force } $wb.SaveAs($OutXlsx) if (-not $ShowExcel) { $wb.Close($true); $excel.Quit() } [System.Runtime.Interopservices.Marshal]::ReleaseComObject($wsSwap) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($wsMem) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($wsSummary)| Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($wb) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null [gc]::Collect(); [gc]::WaitForPendingFinalizers() Write-Host "Done: $OutXlsx"
以下のコードを「MakeExcelSarReport.bat」としてバッチファイルを作成
@echo off setlocal REM このBATのある場所のPS1を呼ぶ。引数(ドラッグ&ドロップ)はそのまま渡す powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0MakeExcelSarReport.ps1" %* endlocal PAUSE
以下が基本的な使用方法:
1.「MakeExcelSarReport.ps1」と「MakeExcelSarReport.bat」を同階層に配置
2.バッチファイル実行(ダブルクリック)
3.memのCSVを選択
4.swapのCSVを選択
5.memのCSVと同階層、同日付でSAR_ReportのExcelファイルが生成される。
SAR_ReportExcelファイルの中身は
・1シート目:Summary(要約)
・2シート目:memデータ
・3シート目:swapデータ
手順4:出力Excelデータのメモリリソース状況の項目について
ログのメモリリソース状況が、グラフ化されて各シートに記載されます。
その際の項目の内容については、下記をご覧ください。
%memused (%)
メインメモリ(RAM)の使用率を示す値。
サーバー全体で、どの程度の割合が利用されているかをパーセンテージで表す。
おおむね 80%を超えるようになると、システムは空きメモリを確保しにくくなり、スワップ発生の可能性が高まる。
kbavail (KB)
利用可能なメモリ容量をキロバイト単位で示す値。
Linuxカーネルのメモリ管理アルゴリズムに基づき、実際に新しいプロセスやアプリケーションが利用できる「目安」となる。
%memused よりも、サーバーに余力が残っているかを直感的に把握しやすい。
%commit (%)
コミット済みメモリの割合を表す。
Linux は「オーバーコミット」という仕組みにより、実際の物理メモリ以上の割り当てをアプリケーションに行うことがある。
%commit が高すぎると、実際の要求に応えられなくなり OOM Killer(強制プロセス終了)が発動するリスクがある。
%swpused (%)
スワップ領域の使用率を示す。
物理メモリからあふれたページがスワップに退避されると、この数値が上昇する。
0%で安定していれば健全。高い値を示す場合、ディスクI/Oが増えパフォーマンス低下につながる。
※スクリプトファイルの文字コードについて
- シェルスクリプト send_sar_mem_csv_mail.sh は UTF-8(LF改行)で保存してください
- Windows 環境では CRLF改行や Shift-JIS で保存されることがあります
- PowerShellスクリプト MakeExcelSarReport.ps1 は CRLF改行や Shift-JIS でOK
※補足:今回手間取った点
1:UTC時刻日付からJST時刻日付の特定のフォーマットに調整
→時刻のフォーマットは”mm:dd”という形式でないとグラフ化できなかった
→Excelの日付入力書式がこれでないと反応しない?MSのブラックボックス
2:Summaryのシートを一番始めに処理させないと動かない
→シートでグラフを処理する際、アクティブシートだのの影響?
→ただし、コード上ではシート追加構成の部分とSummaryの処理位置をずらせば可能
→つまり書き方によってはSummaryを後ろにもできる
→ただしこれもMSのブラックボックス(;´・ω・)