「set -xで実行トレースを有効にしても、タイムスタンプがないので処理時間がわからない」
シェルスクリプトは手軽に書けるぶん、パフォーマンス上の問題を抱えていても気づきにくい。ループ内で外部コマンドを繰り返し呼び出したり、パイプラインの段数が増えたりすると、実行時間が予想の数倍に膨らむことがある。
この記事では、PS4変数・EPOCHREALTIME・timeコマンドを組み合わせたプロファイリング設計を解説する。行レベルの実行時刻の記録から、ブロック単位の計測・ボトルネックの特定まで、RHEL 9.4 / Ubuntu 24.04 LTSで動作確認した実践パターンを紹介する。
この記事のポイント
・PS4変数にEPOCHREALTIMEを設定すると行単位で実行時刻を記録できる
・BASH_XTRACEFDでトレース出力を別ファイルに分離して後から分析できる
・time { ... }でスクリプト内のフェーズ別に処理時間を計測できる
・prof_start/prof_end関数でコードを汚さずプロファイリングを組み込める
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトは遅くなるのか
シェルスクリプトのパフォーマンス問題は、主に次の3つの原因から生じる。・外部コマンドの呼び出しコスト:コマンド置換
$(command) やパイプ | は、毎回サブシェルプロセスを起動する。ループ内で繰り返すと、その起動コストが積み重なる・ループ内でのファイル操作:ループの中で毎回
grep や awk を呼ぶと、ファイル読み込みとプロセス起動が繰り返される・パイプラインの段数:
cat | grep | sed | awk のように段数が増えると、各ステージ間のデータ転送オーバーヘッドが増えるまず「どこが遅いか」を計測してから最適化しないと、速くならない箇所に時間をかける無駄が生じる。計測なき最適化は危険という原則は、シェルスクリプトでも同じだ。
timeコマンドでスクリプトの処理時間を計測する
time は bash の組み込みコマンドで、コマンドやブロックの実行時間を計測する。インストール不要で、あらゆる bash 環境で使える最初の計測手段だ。1. timeの基本的な使い方
スクリプト全体の実行時間を計測する場合は、スクリプトの呼び出し前にtime を付けるだけだ。# スクリプト全体を計測する time bash deploy.sh # 実行結果例(server-prod-01 / RHEL 9.4) real 0m4.523s user 0m0.047s sys 0m0.891s
・user:スクリプト自身のCPU使用時間
・sys:カーネルが処理したCPU時間(ファイルI/Oなど)
real が大きく user+sys が小さい場合は、外部コマンドの待機・スリープ・I/O待ちがボトルネックだ。
2. TIMEFORMATで出力を整形する
TIMEFORMAT 変数を設定すると、time の出力フォーマットをカスタマイズできる。ログファイルに組み込む場合に、人が読みやすい形にしたい時に使う。#!/bin/bash # %R = real経過時間(秒・小数点以下3桁) # %U = userモードCPU時間 %S = sysモードCPU時間 TIMEFORMAT="[TIME] real=%Rs user=%Us sys=%Ss" time { echo "processing..." sleep 1 ls /var/log/ > /dev/null } # 出力例 # processing... # [TIME] real=1.012s user=0.001s sys=0.004s
3. スクリプト内のブロック単位で計測する
time { ... } の形式で、スクリプト内の任意のブロックを計測できる。どのフェーズに時間がかかっているかを細かく把握したい時に使う。#!/bin/bash TIMEFORMAT="[PROF] %Rs" echo "=== フェーズ1: ファイル同期 ===" time { rsync -a /src/ /backup/ } echo "=== フェーズ2: ログ圧縮 ===" time { find /var/log -name "*.log" -mtime +7 | xargs gzip } echo "=== フェーズ3: DBバックアップ ===" time { mysqldump --single-transaction mydb > /backup/mydb_$(date +%Y%m%d).sql } # 実行結果例(server-prod-01 / RHEL 9.4) # === フェーズ1: ファイル同期 === # [PROF] 1.247s # === フェーズ2: ログ圧縮 === # [PROF] 4.831s ← ここがボトルネック # === フェーズ3: DBバックアップ === # [PROF] 2.156s
xargs gzip を xargs -P4 gzip に変えて並列化するといった最適化の判断ができる。PS4変数でコマンド単位の実行時刻を記録する
time はブロック単位の計測に向いているが、「ループ内のどの行が遅いか」を行レベルで特定したい場合は、PS4 変数と set -x の組み合わせが有効だ。1. EPOCHREALTIMEをPS4に設定する(bash 5.0以降)
EPOCHREALTIME は bash 5.0(2019年1月リリース)で追加された変数で、Unixエポックからの経過秒数をマイクロ秒精度(小数点以下6桁)で返す。#!/bin/bash # PS4にEPOCHREALTIMEを設定してset -xを有効にする export PS4='+ [${EPOCHREALTIME}] ${BASH_SOURCE##*/}:${LINENO}: ' set -x echo "start" sleep 0.5 echo "end" # 実行結果例(server-prod-01 / RHEL 9.4) # + [1723867200.123456] deploy.sh:5: echo start # start # + [1723867200.124001] deploy.sh:6: sleep 0.5 # + [1723867200.625987] deploy.sh:7: echo end # end
echo "start" の実行は1ms未満、sleep 0.5 が実際に502ms かかっていることが数値で確認できる。2. BASH_XTRACEFDでトレースを別ファイルに分離する
set -x のトレース出力は、デフォルトでは標準エラー出力(stderr)に出る。実際の処理出力と混在して読みにくいため、BASH_XTRACEFD を使って別ファイルに分離するのが実務上のベストプラクティスだ。#!/bin/bash # トレースログの保存先 TRACE_LOG="/tmp/trace_$(date +%Y%m%d_%H%M%S).log" # ファイルディスクリプタ7番をトレースログに割り当てる exec 7>"${TRACE_LOG}" BASH_XTRACEFD=7 # PS4にタイムスタンプを設定 export PS4='+ [${EPOCHREALTIME}] ${BASH_SOURCE##*/}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' set -x # スクリプトの処理(コンソールには通常出力のみ表示される) echo "処理開始" for i in $(seq 1 100); do ls /var/log/ > /dev/null done # 計測終了・クローズ set +x exec 7>&- echo "トレースを ${TRACE_LOG} に保存しました"
3. トレースファイルからボトルネックを特定する
トレースファイルを解析して、前の行との時間差が大きい行を抽出すると、遅い処理を素早く絞り込める。# トレースファイルの先頭を確認する head -5 /tmp/trace_20260817_091500.log # 実際の出力例(server-prod-01) # + [1723867200.123456] backup.sh:12: sync_files(): rsync -a /src/ /backup/ # + [1723867204.567890] backup.sh:13: sync_files(): echo "sync complete" # + [1723867204.568001] backup.sh:18: compress_logs(): find /var/log ... # 0.1秒以上かかったコマンドだけ抽出する awk -F'[][]' '/^\+/{ ts = $2 if (prev_ts != "") { diff = ts - prev_ts if (diff > 0.1) printf "[%.3f秒] %s ", diff, $0 } prev_ts = ts }' /tmp/trace_20260817_091500.log
実務での設計パターン|計測用ラッパー関数でコードを汚さない
プロファイリングコードをスクリプト本体に直接書き散らすと、本番運用時の可読性が損なわれる。計測用の関数を分離してオプションで切り替える設計が実務に向いている。#!/bin/bash # --profileオプションで計測モードを有効化する PROFILE_MODE=false [[ "$1" == "--profile" ]] && PROFILE_MODE=true # タイマー開始 prof_start() { "${PROFILE_MODE}" || return 0 _PROF_START="${EPOCHREALTIME}" _PROF_LABEL="${1:-unknown}" } # タイマー終了・結果を標準エラー出力に報告 prof_end() { "${PROFILE_MODE}" || return 0 local elapsed elapsed=$(printf "%.3f" "$(echo "${EPOCHREALTIME} - ${_PROF_START}" | bc -l)") echo "[PROF] ${_PROF_LABEL}: ${elapsed}秒" >&2 } # ---- スクリプト本体 ---- main() { prof_start "ファイル同期" rsync -a /src/ /backup/ prof_end prof_start "ログ圧縮" find /var/log -name "*.log" -mtime +7 | xargs gzip prof_end prof_start "DBバックアップ" mysqldump --single-transaction mydb > "/backup/mydb_$(date +%Y%m%d).sql" prof_end } main "$@"
# 通常実行(計測なし) bash backup.sh # プロファイリング有効化して実行 bash backup.sh --profile # 出力例(server-prod-01 / RHEL 9.4 / 計測結果は標準エラー出力) # [PROF] ファイル同期: 1.247秒 # [PROF] ログ圧縮: 4.831秒 # [PROF] DBバックアップ: 2.156秒
--profile オプションを付けない場合、prof_start / prof_end はすぐに return 0 で抜けるため、本番実行への影響はほぼゼロだ。シェルスクリプトの設計手法をさらに深めたい方は、シェルスクリプト実践ガイド も合わせて確認してほしい。
トラブルシュート
「EPOCHREALTIMEが使えない」場合(bash 4.x環境)
EPOCHREALTIME は bash 5.0 以降でのみ使える。RHEL 8 以前や古いサーバーでは bash 4.x が使われていることがある。# bashのバージョンを確認する bash --version # GNU bash, version 4.4.20(1)-release (x86_64-redhat-linux-gnu) # bash 4.x では EPOCHREALTIME が使えないため、 # PS4に date +%s.%N を組み合わせて代替する # ただし date コマンドの起動コスト(数ms)が計測誤差になる点に注意 export PS4='+ [$(date "+%s.%N")] ${BASH_SOURCE##*/}:${LINENO}: ' set -x
「トレースが膨大になって読みにくい」場合
ループ処理や多段の関数呼び出しがある場合、トレースログが数千行になることがある。計測対象を絞り込むには、set -x / set +x でトレース範囲を限定する。#!/bin/bash # 前処理(トレース不要) preprocess # 計測したいブロックだけ囲む set -x slow_function # ボトルネックが疑われる関数 set +x # 後処理(トレース不要) cleanup
本記事のまとめ
シェルスクリプトのプロファイリングで使う主な手法をまとめる。| 手法 | 使い方 | 向いているケース |
|---|---|---|
| フェーズ計測 | time { ... } |
処理のどの段階が遅いか大まかに把握したい |
| 出力整形 | TIMEFORMAT="..." |
ログに組み込みやすい形式で計測結果を記録したい |
| 行レベルのタイムスタンプ | PS4 + EPOCHREALTIME |
コマンド単位でどの行が遅いか特定したい |
| トレース分離 | BASH_XTRACEFD=7 |
通常出力とトレースを分けてファイル解析したい |
| コード侵食ゼロ設計 | prof_start/prof_end関数 |
本番コードに影響を与えずデバッグ時だけ計測したい |
| bash 4.x対応 | date +%s.%N |
bash 5.0未満の環境でも動かしたい(精度は劣る) |
time で計測してフェーズを絞り込み、次に PS4 で行を特定する」という2段階のアプローチだ。最初から PS4 トレースを有効にすると出力が膨大になりすぎるため、time でフェーズを絞った後で詳細計測に進むのがセミナーで3,100名以上を指導してきた経験から見ても効率的な手順だ。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:bashのcallerコマンドでシェルスクリプトのコールスタックを記録する方法|エラー発生箇所と呼び出し元ログの設計
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます