この記事は技術的にコアな内容を含みます!
当記事に書いてある内容を実践して、何らかの損害が発生しても、
当ブログ主は一切の責任は負いません。全て自己責任といたします!
今回は 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のブラックボックス(;´・ω・)