ジョブ制御とシグナル
シェルのジョブ制御とシグナルは、プロセスのライフサイクルを操る基本技術。
85 分で読めます42,053 文字
ジョブ制御とシグナル
シェルのジョブ制御とシグナルは、プロセスのライフサイクルを操る基本技術。 バックグラウンド処理、プロセス間通信、堅牢なスクリプト作成のすべてに関わる重要概念である。
この章で学ぶこと
- フォアグラウンド/バックグラウンドのジョブ制御ができる
- シグナルの種類と使い方を理解する
- nohup / disown でセッション切断後も実行を継続できる
- trap でシグナルハンドラを設定し、堅牢なスクリプトを書ける
- wait / timeout で並列処理とタイムアウトを制御できる
- プロセスグループとセッションの概念を理解する
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- プロセス監視(ps, top, htop) の内容を理解していること
1. ジョブ制御
1.1 フォアグラウンドとバックグラウンド
# フォアグラウンド実行(デフォルト)
# → 端末がブロックされ、コマンドが完了するまで入力を受け付けない
sleep 100 # フォアグラウンド実行
# バックグラウンド実行(& を末尾に付ける)
# → 端末は空き、別のコマンドを実行可能
sleep 100 & # バックグラウンド実行
# [1] 12345 ← ジョブ番号とPID が表示される
# 複数のバックグラウンドジョブ
sleep 60 & # [1] 12345
sleep 120 & # [2] 12346
find / -name "*.log" > /tmp/logs.txt 2>/dev/null & # [3] 12347
# バックグラウンドジョブの標準出力/エラー
# バックグラウンドジョブの出力は端末に混ざって表示される
# → リダイレクトしておくのがベストプラクティス
long_task > output.log 2>&1 & # 出力をファイルにリダイレクト
# バックグラウンドで実行しつつPIDを記録
long_task &
PID=$! # $! = 直前のバックグラウンドプロセスのPID
echo "Started with PID: $PID"1.2 ジョブ一覧と状態確認
# ジョブ一覧
jobs # 現在のシェルのジョブ一覧
jobs -l # PID付き一覧
jobs -r # 実行中のジョブのみ
jobs -s # 停止中のジョブのみ
jobs -p # PIDのみ表示
# 出力例:
# [1]+ Running sleep 100 &
# [2]- Stopped vim file.txt
# [3] Running find / -name "*.log" > /tmp/logs.txt 2>/dev/null &
# ↑ ↑ ↑
# ジョブ番号 状態
# + = カレントジョブ(最後に操作した/起動したジョブ)
# - = 前のジョブ
# ジョブの状態:
# Running: 実行中(バックグラウンド)
# Stopped: 一時停止中(Ctrl+Z で停止)
# Done: 完了(次のプロンプト表示時に通知)
# Terminated: シグナルで終了
# Killed: SIGKILLで強制終了
# Exit N: 終了コード N で終了1.3 ジョブの切り替え操作
# Ctrl+Z: フォアグラウンドジョブを一時停止(SIGTSTP送信)
vim file.txt # vim で編集中
# Ctrl+Z # vim が一時停止(Stoppedになる)
# [1]+ Stopped vim file.txt
# fg: ジョブをフォアグラウンドに戻す
fg # カレントジョブ(+のついたジョブ)
fg %1 # ジョブ番号1をフォアグラウンドに
fg %vim # vim で始まるジョブを指定
fg %?file # "file" を含むジョブを指定
# bg: 停止中のジョブをバックグラウンドで再開
bg # カレントジョブをバックグラウンドで再開
bg %2 # ジョブ番号2をバックグラウンドで再開
# 典型的なワークフロー
vim file.txt # vim で編集中
# Ctrl+Z # 一時停止
make build # ビルド実行
fg # vim に戻る
# 典型的なワークフロー2: フォアグラウンドをバックグラウンドに移す
long_running_command # フォアグラウンドで実行してしまった
# Ctrl+Z # 一時停止
bg # バックグラウンドで再開
# これで端末が使える
# 典型的なワークフロー3: 複数のエディタを切り替え
vim file1.txt # 編集
# Ctrl+Z
vim file2.txt # 別のファイルを編集
# Ctrl+Z
jobs # ジョブ確認
fg %1 # file1 に戻る1.4 ジョブ指定の書式
# ジョブの指定方法
%1 # ジョブ番号1
%2 # ジョブ番号2
%% # カレントジョブ(%+ と同じ)
%+ # カレントジョブ(最後に操作したジョブ)
%- # 前のジョブ(カレントの1つ前)
%string # コマンドがstringで始まるジョブ
%?string # コマンドにstringを含むジョブ
# 使用例
fg %vim # vim で始まるジョブをフォアグラウンドに
bg %2 # ジョブ2をバックグラウンドで再開
kill %?sleep # sleep を含むジョブを終了
kill %% # カレントジョブを終了
kill %1 %2 %3 # 複数ジョブを終了
wait %1 # ジョブ1の完了を待つ
# 注意: ジョブ番号はシェルごとに独立
# 別の端末/シェルのジョブにはアクセスできない
# → PID を使う場合は kill コマンドを使う1.5 バックグラウンドジョブの注意点
# 注意1: バックグラウンドジョブの出力は端末に混ざる
long_task &
# → 出力がプロンプトに割り込む可能性
# 対策: リダイレクト
long_task > /tmp/output.log 2>&1 &
# 注意2: バックグラウンドジョブの入力
# バックグラウンドジョブが端末からの入力を要求すると停止する
cat & # 入力待ちで自動停止
# [1]+ Stopped cat
# 注意3: シェルを終了するとバックグラウンドジョブにSIGHUPが送られる
# → nohup や disown を使う(後述)
# 注意4: bash の huponexit オプション
shopt -s huponexit # シェル終了時に全ジョブにSIGHUP送信(デフォルトOFF)
shopt -u huponexit # SIGHUP送信しない
# 注意5: スクリプト内でのバックグラウンドジョブ
#!/bin/bash
task1 &
task2 &
wait # 全バックグラウンドジョブの完了を待つ
echo "全タスク完了"
# wait を忘れると、スクリプトがジョブの完了前に終了する2. シグナル
2.1 シグナルの基礎
# シグナルとは: カーネルからプロセスへの非同期通知メカニズム
# プロセスを制御する(終了、停止、再開など)ための仕組み
# シグナル一覧
kill -l # 全シグナル一覧(名前)
kill -l 15 # シグナル番号→名前(TERM)
kill -l TERM # シグナル名→番号(15)
# シグナルの配送:
# 1. ユーザーがシグナルを送信(kill コマンド、Ctrl+C など)
# 2. カーネルがプロセスにシグナルを配送
# 3. プロセスがシグナルを処理:
# a. デフォルト動作を実行(終了、停止など)
# b. カスタムハンドラを実行(trap で設定)
# c. シグナルを無視(一部のシグナルのみ)
# ※ SIGKILL と SIGSTOP は捕捉も無視もできない2.2 主要シグナルの詳細
# ┌────────┬───────────┬──────────────────┬─────────────────────────────────┐
# │ 番号 │ 名前 │ デフォルト動作 │ 用途・説明 │
# ├────────┼───────────┼──────────────────┼─────────────────────────────────┤
# │ 1 │ SIGHUP │ 終了 │ ハングアップ / 設定再読み込み │
# │ 2 │ SIGINT │ 終了 │ Ctrl+C(割り込み) │
# │ 3 │ SIGQUIT │ コアダンプ+終了 │ Ctrl+\(デバッグ用終了) │
# │ 6 │ SIGABRT │ コアダンプ+終了 │ abort() 呼び出し │
# │ 9 │ SIGKILL │ 強制終了 │ 捕捉不可(最終手段) │
# │ 11 │ SIGSEGV │ コアダンプ+終了 │ セグメンテーション違反 │
# │ 13 │ SIGPIPE │ 終了 │ 壊れたパイプへの書き込み │
# │ 14 │ SIGALRM │ 終了 │ alarm() タイマー満了 │
# │ 15 │ SIGTERM │ 終了 │ 正常終了要求(kill のデフォルト)│
# │ 17 │ SIGCHLD │ 無視 │ 子プロセスの状態変更 │
# │ 18 │ SIGCONT │ 再開 │ 停止プロセスの再開 │
# │ 19 │ SIGSTOP │ 停止 │ 捕捉不可の一時停止 │
# │ 20 │ SIGTSTP │ 停止 │ Ctrl+Z(端末からの停止) │
# │ 21 │ SIGTTIN │ 停止 │ バックグラウンドプロセスの入力 │
# │ 22 │ SIGTTOU │ 停止 │ バックグラウンドプロセスの出力 │
# │ 28 │ SIGWINCH │ 無視 │ 端末のウィンドウサイズ変更 │
# │ 10 │ SIGUSR1 │ 終了 │ ユーザー定義シグナル1 │
# │ 12 │ SIGUSR2 │ 終了 │ ユーザー定義シグナル2 │
# └────────┴───────────┴──────────────────┴─────────────────────────────────┘
# 注意: シグナル番号はOS/アーキテクチャによって異なる場合がある
# → スクリプトでは名前で指定するのが安全(kill -TERM, kill -HUP)
# SIGKILL(9)と SIGSTOP(19)の特殊性:
# - 捕捉(trap)できない
# - 無視できない
# - ブロックできない
# → カーネルが直接処理する2.3 シグナルの送信
# kill コマンド(名前が紛らわしいが、任意のシグナルを送信するコマンド)
kill 1234 # SIGTERM(デフォルト)
kill -15 1234 # SIGTERM(番号で指定)
kill -TERM 1234 # SIGTERM(名前で指定)
kill -s TERM 1234 # SIGTERM(-s オプション)
kill -9 1234 # SIGKILL(強制終了)
kill -KILL 1234 # SIGKILL(名前で指定)
kill -HUP 1234 # SIGHUP(設定再読み込み)
kill -USR1 1234 # SIGUSR1(ユーザー定義)
kill -USR2 1234 # SIGUSR2(ユーザー定義)
kill -CONT 1234 # SIGCONT(停止プロセスの再開)
kill -STOP 1234 # SIGSTOP(強制停止)
kill -0 1234 # シグナルは送らない(プロセス存在確認)
if kill -0 1234 2>/dev/null; then
echo "PID 1234 は存在する"
else
echo "PID 1234 は存在しない"
fi
# 複数プロセスへの送信
kill 1234 5678 9012 # 複数PIDに送信
kill -TERM 1234 5678 # 複数PIDにTERM
# killall — プロセス名でシグナル送信
killall nginx # nginx という名前の全プロセスにTERM
killall -9 nginx # 全 nginx プロセスを強制終了
killall -u gaku python # ユーザー gaku の python プロセスを終了
killall -i nginx # 確認プロンプト付き(1つずつ)
killall -v nginx # 詳細表示
killall -w nginx # 全プロセスが終了するまで待機
killall -e "python3 server.py" # 完全一致(exactマッチ)
killall -s HUP nginx # シグナル指定
killall -o 1h nginx # 1時間以上前に起動したもの
killall -y 30m nginx # 30分以内に起動したもの
# pkill — 柔軟なパターンマッチングでシグナル送信
pkill nginx # nginx にマッチするプロセスにTERM
pkill -9 nginx # 強制終了
pkill -f "python server.py" # コマンドライン全体でマッチ
pkill -u root nginx # ユーザー指定
pkill -u root -HUP nginx # ユーザー + シグナル指定
pkill -P 1234 # 親PID 1234 の子プロセスにTERM
pkill -t pts/0 # 端末 pts/0 のプロセスにTERM
pkill -g 1234 # プロセスグループ 1234 にTERM
pkill -x nginx # 完全一致
pkill --signal HUP nginx # シグナル指定(長いオプション形式)
pkill -c nginx # マッチしたプロセス数を表示
pkill -e nginx # 終了させたプロセスを表示
# プロセスグループ全体にシグナルを送信
kill -TERM -1234 # PID の前にマイナスをつける → プロセスグループ
kill -- -1234 # -- で負の数と区別2.4 シグナルの正しい使い方(段階的終了)
# === 推奨される段階的な終了手順 ===
# ステップ1: 正常終了を要求(SIGTERM)
kill 1234 # SIGTERM
# → プロセスはクリーンアップ処理を実行して終了するチャンス
# ステップ2: 数秒待つ
sleep 5
# ステップ3: まだ生きていれば強制終了(SIGKILL)
kill -0 1234 2>/dev/null && kill -9 1234
# ワンライナー版
kill 1234; sleep 5; kill -0 1234 2>/dev/null && kill -9 1234
# スクリプト版(関数化)
graceful_kill() {
local pid=$1
local timeout=${2:-10} # デフォルト10秒
# プロセスが存在するか確認
if ! kill -0 "$pid" 2>/dev/null; then
echo "PID $pid は既に存在しない"
return 0
fi
# SIGTERM を送信
echo "PID $pid に SIGTERM を送信..."
kill "$pid"
# timeout 秒待機
local elapsed=0
while [ $elapsed -lt $timeout ]; do
if ! kill -0 "$pid" 2>/dev/null; then
echo "PID $pid が正常終了"
return 0
fi
sleep 1
elapsed=$((elapsed + 1))
done
# まだ生きていれば SIGKILL
echo "タイムアウト。PID $pid に SIGKILL を送信..."
kill -9 "$pid"
sleep 1
if kill -0 "$pid" 2>/dev/null; then
echo "警告: PID $pid が SIGKILL でも終了しない(D状態の可能性)"
return 1
fi
echo "PID $pid を強制終了"
return 0
}
# 使用例
graceful_kill 1234
graceful_kill 1234 30 # 30秒のタイムアウト
# === なぜ kill -9 を最初に使わないのか? ===
#
# kill -9 (SIGKILL) の問題点:
# 1. クリーンアップ処理が実行されない
# → 一時ファイルが残る
# → PIDファイルが残る
# → ソケットファイルが残る
#
# 2. ファイルのフラッシュが行われない
# → バッファ内のデータが失われる
# → ログの最後の数行が失われる
# → データベースの未コミットトランザクションが失われる
#
# 3. ロックの解放が行われない
# → ファイルロックが残り、次回起動時にデッドロック
# → 共有メモリセグメントが残る
#
# 4. 子プロセスへの影響
# → 子プロセスがゾンビ化する可能性
# → 子プロセスの終了処理が行われない
#
# 5. trap ハンドラが実行されない
# → スクリプトのクリーンアップコードが無視される2.5 キーボードショートカットとシグナル
# 端末のキーボードショートカットとシグナルの対応
# Ctrl+C → SIGINT (2) 割り込み(プロセスの終了)
# Ctrl+\ → SIGQUIT (3) 終了(コアダンプ付き)
# Ctrl+Z → SIGTSTP (20) 一時停止
# Ctrl+D → EOF(シグナルではない、標準入力の終端)
# ショートカットの確認・変更
stty -a # 全端末設定の表示
# intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D;
# susp = ^Z; ...
# キーバインドの変更(一時的)
stty intr ^X # Ctrl+X で SIGINT に変更
stty susp ^Y # Ctrl+Y で SIGTSTP に変更
stty intr ^C # 元に戻す
# Ctrl+C を無効化(Ctrl+C で終了させたくないスクリプト内で)
stty -isig # 全シグナルキーを無効化
stty isig # 元に戻す2.6 SIGHUP の活用
# SIGHUP の2つの用途:
# 1. 端末が切断されたときの通知(元の意味: Hang Up)
# 2. デーモンへの設定再読み込み要求(慣例的な使い方)
# 設定ファイルの再読み込み(プロセスを再起動せずに)
kill -HUP $(pgrep -o nginx) # nginx の設定リロード
kill -HUP $(cat /var/run/nginx.pid) # PIDファイルから
kill -HUP $(pgrep sshd | head -1) # sshd の設定リロード
# systemd 管理のサービスの場合(こちらが推奨)
sudo systemctl reload nginx # nginx設定リロード
sudo systemctl reload sshd # sshd設定リロード
sudo systemctl reload postgresql # PostgreSQL設定リロード
# SIGHUP で設定再読み込みをサポートする主要デーモン:
# nginx: ワーカープロセスの graceful restart
# Apache: 設定再読み込み
# sshd: 設定再読み込み
# rsyslog: 設定再読み込み
# logrotate: ログローテーション時に使用
# PostgreSQL: postgresql.conf の再読み込み
# HAProxy: 設定再読み込み
# SIGHUP で再読み込みされる内容(nginx の場合):
# - nginx.conf とインクルードファイル
# - 新しいワーカープロセスが新設定で起動
# - 古いワーカープロセスは現在の接続を処理してから終了
# - マスタープロセスは再起動しない
# → ダウンタイムなしで設定変更が可能3. セッション切断後の実行継続
3.1 nohup
# nohup: SIGHUP を無視してコマンドを実行
# SSH接続が切れてもプロセスが終了しない
# 基本的な使い方
nohup long_task.sh & # バックグラウンドで実行
# 出力は nohup.out に自動リダイレクト(カレントディレクトリ)
# nohup.out に書けない場合は ~/nohup.out にフォールバック
# 出力先を明示指定(推奨)
nohup long_task.sh > /var/log/task.log 2>&1 &
echo $! # PID を表示
# PIDを記録
nohup long_task.sh > /tmp/task.log 2>&1 &
echo $! > /tmp/task.pid
# 後で確認: cat /tmp/task.pid
# nohup の動作:
# 1. SIGHUP を無視(SIG_IGN)に設定
# 2. 標準出力が端末の場合、nohup.out にリダイレクト
# 3. 標準エラー出力が端末の場合、標準出力にリダイレクト
# 4. コマンドを exec する
# nohup の確認方法
cat /proc/$(cat /tmp/task.pid)/status | grep SigIgn
# SigIgn: 0000000000000001 ← SIGHUP(1) が無視されている3.2 disown
# disown: 実行中のジョブをシェルのジョブテーブルから切り離す
# nohup を付け忘れて実行してしまった場合に有用
# 基本的な使い方
long_task.sh & # バックグラウンドで開始
disown %1 # ジョブ1をシェルから切り離す
# → シェル終了時にSIGHUPが送信されなくなる
# SIGHUP だけ無視(ジョブリストには残る)
long_task.sh &
disown -h %1 # SIGHUPだけ無視(jobsには残る)
# 全ジョブを切り離す
disown -a # 全ジョブを切り離す
# 最後のバックグラウンドジョブを切り離す
long_task.sh &
disown # 引数なし = 最後のジョブ
# 典型的なワークフロー: nohup を忘れた場合の対処
long_running_command # フォアグラウンドで実行中
# Ctrl+Z # 一時停止
bg # バックグラウンドで再開
disown %1 # シェルから切り離す
# → SSH接続が切れてもプロセスは継続する
# 注意: disown は出力をリダイレクトしない
# → 出力が端末に残る場合、端末を閉じるとSIGPIPEが送信される可能性
# → 可能であればリダイレクトしてから disown する
long_task.sh > /tmp/output.log 2>&1 &
disown %13.3 nohup vs disown の比較
# ┌────────────┬──────────────────────────────────┬────────────────────────────────┐
# │ 特徴 │ nohup │ disown │
# ├────────────┼──────────────────────────────────┼────────────────────────────────┤
# │ 実行タイミング │ コマンド実行前に指定 │ 実行後に適用可能 │
# │ SIGHUP │ 無視 │ シェルからの送信を防ぐ │
# │ 出力 │ nohup.out に自動リダイレクト │ リダイレクトしない │
# │ ジョブリスト │ 通常通り表示 │ 切り離されるとリストから消える │
# │ 再接続 │ 不可 │ 不可 │
# │ 主な用途 │ 計画的な長時間タスク │ nohup 忘れの救済 │
# └────────────┴──────────────────────────────────┴────────────────────────────────┘3.4 setsid
# setsid: 新しいセッションでコマンドを実行
# 端末から完全に切り離される
setsid long_task.sh # 新セッションリーダーとして実行
setsid -f long_task.sh # フォーク後に新セッション作成
# setsid の動作:
# 1. fork() で子プロセスを作成
# 2. 子プロセスで setsid() を呼び出し
# 3. 新しいセッションの作成(制御端末を持たない)
# 4. コマンドを exec
#
# nohup/disown との違い:
# - 完全に新しいセッションを作成(端末から完全分離)
# - デーモン化に近い動作
# - プロセスグループも新しくなる3.5 tmux / screen との比較
セッション切断対策の比較:| 方法 | 特徴 |
|---|---|
| nohup | 簡単。出力は nohup.out へ。再接続不可 |
| disown | 実行後に適用可能。再接続不可 |
| setsid | 完全にセッション分離。再接続不可 |
| tmux | セッション管理。切断後も再接続可能 ← 推奨 |
| screen | tmuxと同等。古くからあり互換性高い |
| systemd | サービスとして管理。ログ、再起動対応 |
実務での使い分け:
一時的なコマンド実行 → nohup(最も簡単)
nohup を忘れた場合 → disown(救済策)
対話的な長時間作業 → tmux ← 最も推奨
デーモン的なサービス → systemd / supervisor
3.6 tmux でのセッション管理
# tmux の基本操作(ジョブ制御の文脈で)
# セッション作成
tmux new -s work # "work" という名前でセッション作成
# 作業実行
# ... 長時間タスクを実行 ...
# デタッチ(切断してもセッションは維持)
# Ctrl+b d # デタッチ
# 再接続(SSH再接続後でも可能)
tmux attach -t work # "work" セッションに再接続
tmux a -t work # 省略形
# セッション一覧
tmux ls # セッション一覧
# セッション終了
tmux kill-session -t work # セッションを終了
# ウィンドウ操作
# Ctrl+b c # 新しいウィンドウ
# Ctrl+b n # 次のウィンドウ
# Ctrl+b p # 前のウィンドウ
# Ctrl+b 0-9 # 番号でウィンドウ切替
# Ctrl+b w # ウィンドウ一覧
# ペイン操作
# Ctrl+b % # 垂直分割
# Ctrl+b " # 水平分割
# Ctrl+b 矢印キー # ペイン間移動
# Ctrl+b z # ペインのズーム切替
# スクリプトでの tmux 活用
tmux new-session -d -s build 'make all && echo Done'
# バックグラウンドでセッションを作成してコマンド実行
# 後で tmux a -t build で結果を確認可能4. trap — シグナルハンドラ
4.1 基本構文
# trap 'コマンド' シグナル [シグナル...]
# 基本的な使い方
trap 'echo "Ctrl+C を受信しました"' INT
# 複数のシグナルを捕捉
trap 'echo "終了します"' INT TERM
# シグナルを無視
trap '' INT # SIGINTを無視(Ctrl+Cが効かなくなる)
trap '' HUP # SIGHUPを無視
# デフォルト動作に戻す
trap - INT # SIGINTをデフォルトに戻す
trap - HUP TERM # 複数シグナルをリセット
# 現在のtrap設定を表示
trap -p # 全trap設定
trap -p INT # 特定シグナルの設定4.2 EXIT トラップ(最重要パターン)
#!/bin/bash
# EXIT トラップ: スクリプト終了時に必ず実行される
# 正常終了でも異常終了でも呼ばれる(SIGKILLを除く)
# パターン1: 一時ファイルのクリーンアップ
TMPFILE=$(mktemp)
TMPDIR=$(mktemp -d)
trap 'rm -f "$TMPFILE"; rm -rf "$TMPDIR"; echo "クリーンアップ完了"' EXIT
echo "一時ファイル: $TMPFILE"
echo "一時ディレクトリ: $TMPDIR"
# ... 処理 ...
# スクリプト終了時に自動的にクリーンアップ
# パターン2: ロックファイル管理
LOCKFILE="/tmp/myapp.lock"
if [ -f "$LOCKFILE" ]; then
echo "別のインスタンスが実行中です(ロックファイル: $LOCKFILE)" >&2
exit 1
fi
trap 'rm -f "$LOCKFILE"' EXIT
echo $$ > "$LOCKFILE"
# ... 処理 ...
# パターン3: PIDファイル管理
PIDFILE="/var/run/myapp.pid"
trap 'rm -f "$PIDFILE"' EXIT
echo $$ > "$PIDFILE"
# パターン4: サービスの停止処理
trap 'echo "シャットダウン中..."; stop_service; echo "完了"' EXIT
# パターン5: SSH接続のクリーンアップ
trap 'ssh -O exit user@server 2>/dev/null' EXIT
ssh -M -S /tmp/ssh_mux_%h_%p_%r user@server4.3 ERR トラップ
#!/bin/bash
# ERR トラップ: コマンドが非ゼロ終了コードを返したときに実行される
# set -e と組み合わせて使うことが多い
# パターン1: エラー発生箇所の表示
trap 'echo "エラー発生: 行 $LINENO コマンド \"$BASH_COMMAND\" 終了コード $?" >&2' ERR
# set -e と組み合わせ
set -e
trap 'echo "行 $LINENO でエラー: $BASH_COMMAND" >&2' ERR
# パターン2: エラー時のスタックトレース
trap 'echo "Error at ${BASH_SOURCE[0]}:${LINENO} in ${FUNCNAME[0]:-main}"' ERR
# パターン3: 詳細なエラーハンドリング
error_handler() {
local exit_code=$?
local line_no=$1
local command=$2
echo "=============================" >&2
echo "エラー発生!" >&2
echo " 行番号: $line_no" >&2
echo " コマンド: $command" >&2
echo " 終了コード: $exit_code" >&2
echo " スクリプト: ${BASH_SOURCE[1]}" >&2
echo "=============================" >&2
# コールスタック表示
local i=0
echo "コールスタック:" >&2
while caller $i; do
((i++))
done 2>/dev/null >&2
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
# 注意: ERR トラップはサブシェルには伝播しない(デフォルト)
# set -E で ERR トラップをサブシェルに伝播させる
set -eE
trap 'echo "Error at line $LINENO" >&2' ERR4.4 DEBUG トラップ
#!/bin/bash
# DEBUG トラップ: 各コマンドの実行前に呼ばれる
# パターン1: 実行コマンドの追跡(デバッグ用)
trap 'echo "DEBUG: $BASH_COMMAND (行 $LINENO)"' DEBUG
echo "step 1"
echo "step 2"
# 出力:
# DEBUG: echo "step 1" (行 5)
# step 1
# DEBUG: echo "step 2" (行 6)
# step 2
# パターン2: 実行時間の計測
LAST_TIME=$(date +%s%N)
trap '
NOW=$(date +%s%N)
ELAPSED=$(( (NOW - LAST_TIME) / 1000000 ))
if [ $ELAPSED -gt 100 ]; then
echo "SLOW: ${ELAPSED}ms - $BASH_COMMAND" >&2
fi
LAST_TIME=$NOW
' DEBUG4.5 RETURN トラップ
#!/bin/bash
# RETURN トラップ: 関数やsourceから戻るときに呼ばれる
trap 'echo "関数から戻りました"' RETURN
my_function() {
echo "関数内"
return 0
}
my_function
# 出力:
# 関数内
# 関数から戻りました4.6 trap の実践パターン集
#!/bin/bash
# === 堅牢なスクリプトのテンプレート ===
set -euo pipefail
# グローバル変数
SCRIPT_NAME=$(basename "$0")
TMPDIR=""
LOCKFILE=""
CLEANUP_DONE=false
# クリーンアップ関数
cleanup() {
if [ "$CLEANUP_DONE" = true ]; then
return
fi
CLEANUP_DONE=true
echo "[$SCRIPT_NAME] クリーンアップ中..."
# 一時ディレクトリの削除
if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then
rm -rf "$TMPDIR"
fi
# ロックファイルの削除
if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then
rm -f "$LOCKFILE"
fi
# バックグラウンドジョブの終了
jobs -p | xargs -r kill 2>/dev/null
echo "[$SCRIPT_NAME] クリーンアップ完了"
}
# エラーハンドラ
error_handler() {
local exit_code=$?
local line_no=$1
echo "[$SCRIPT_NAME] エラー: 行 $line_no 終了コード $exit_code" >&2
cleanup
exit "$exit_code"
}
# トラップの設定
trap cleanup EXIT
trap 'error_handler $LINENO' ERR
trap 'echo "[$SCRIPT_NAME] 割り込みを受信"; exit 130' INT
trap 'echo "[$SCRIPT_NAME] 終了要求を受信"; exit 143' TERM
# 初期化
TMPDIR=$(mktemp -d "/tmp/${SCRIPT_NAME}.XXXXXX")
LOCKFILE="/tmp/${SCRIPT_NAME}.lock"
if [ -f "$LOCKFILE" ]; then
EXISTING_PID=$(cat "$LOCKFILE")
if kill -0 "$EXISTING_PID" 2>/dev/null; then
echo "エラー: 別のインスタンスが実行中 (PID: $EXISTING_PID)" >&2
exit 1
else
echo "警告: 古いロックファイルを削除します" >&2
rm -f "$LOCKFILE"
fi
fi
echo $$ > "$LOCKFILE"
# メイン処理
echo "[$SCRIPT_NAME] 開始 (PID: $$, TMPDIR: $TMPDIR)"
# ... ここにメイン処理 ...
echo "[$SCRIPT_NAME] 正常完了"# === Ctrl+C で中断可能なループ ===
#!/bin/bash
RUNNING=true
trap 'RUNNING=false; echo "中断要求を受信..."' INT
echo "処理を開始します (Ctrl+C で中断)"
count=0
while $RUNNING && [ $count -lt 100 ]; do
echo "処理中... ($count/100)"
sleep 1
count=$((count + 1))
done
if $RUNNING; then
echo "全処理が完了しました"
else
echo "処理が中断されました ($count/100 完了)"
fi# === SIGUSR1/SIGUSR2 を使ったプロセス間通信 ===
#!/bin/bash
# ワーカースクリプト(worker.sh)
STATS_REQUESTS=0
PAUSED=false
# SIGUSR1 → 統計情報を出力
trap 'echo "統計: リクエスト数=$STATS_REQUESTS, 一時停止=$PAUSED"' USR1
# SIGUSR2 → 一時停止/再開のトグル
trap '
if $PAUSED; then
PAUSED=false
echo "再開"
else
PAUSED=true
echo "一時停止"
fi
' USR2
echo "ワーカー開始 (PID: $$)"
while true; do
if ! $PAUSED; then
# ... 実際の処理 ...
STATS_REQUESTS=$((STATS_REQUESTS + 1))
fi
sleep 1
done
# 制御側:
# kill -USR1 <PID> # 統計表示
# kill -USR2 <PID> # 一時停止/再開5. wait / timeout — 並列処理の制御
5.1 wait — バックグラウンドジョブの完了待ち
# 全バックグラウンドジョブの完了を待つ
task1 &
task2 &
task3 &
wait # 全ジョブが終了するまでブロック
echo "全タスク完了"
# 特定のPIDの完了を待つ
task1 &
PID1=$!
task2 &
PID2=$!
wait $PID1
echo "task1 完了 (終了コード: $?)"
wait $PID2
echo "task2 完了 (終了コード: $?)"
# 特定ジョブの完了を待つ
task1 &
wait %1
echo "ジョブ1 完了"
# どれか1つの完了を待つ(bash 4.3+)
task1 &
PID1=$!
task2 &
PID2=$!
task3 &
PID3=$!
wait -n # 最初に終了したジョブを待つ
echo "最初のジョブが完了 (終了コード: $?)"
# wait -n で完了したPIDを取得(bash 5.1+)
wait -n -p DONE_PID $PID1 $PID2 $PID3
echo "PID $DONE_PID が完了"5.2 並列処理パターン
# パターン1: 単純な並列実行
#!/bin/bash
for file in *.csv; do
process_file "$file" &
done
wait
echo "全ファイル処理完了"
# パターン2: 並列数を制限した並列処理
#!/bin/bash
MAX_PARALLEL=4
count=0
for file in *.csv; do
process_file "$file" &
count=$((count + 1))
if [ $count -ge $MAX_PARALLEL ]; then
wait -n # 1つ完了するのを待つ
count=$((count - 1))
fi
done
wait # 残りの全ジョブを待つ
# パターン3: 結果の収集
#!/bin/bash
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
PIDS=()
for i in $(seq 1 10); do
(
result=$(some_task "$i")
echo "$result" > "$TMPDIR/result_$i"
) &
PIDS+=($!)
done
# 全完了を待ち、エラーチェック
ERRORS=0
for pid in "${PIDS[@]}"; do
if ! wait "$pid"; then
ERRORS=$((ERRORS + 1))
fi
done
echo "完了: 成功=$((10 - ERRORS)), 失敗=$ERRORS"
cat "$TMPDIR"/result_* | sort
# パターン4: xargs による並列処理
find . -name "*.jpg" | xargs -P 4 -I {} convert {} -resize 800x600 resized_{}
# -P 4: 4並列
# パターン5: GNU parallel による並列処理
# parallel がインストールされている場合
find . -name "*.csv" | parallel -j 4 process_file {}
seq 100 | parallel -j 8 'curl -s "https://api.example.com/item/{}" > /tmp/item_{}.json'5.3 timeout — タイムアウト付き実行
# timeout: 指定時間でコマンドを自動終了
# 基本的な使い方
timeout 60 long_command # 60秒でタイムアウト(SIGTERM)
timeout 30s curl -s https://example.com # 30秒でタイムアウト
timeout 5m make build # 5分でタイムアウト
timeout 2h rsync -avz src/ dst/ # 2時間でタイムアウト
# 時間の単位:
# s: 秒(デフォルト)
# m: 分
# h: 時間
# d: 日
# タイムアウト時のシグナルを指定
timeout -s KILL 60 long_command # 60秒後にSIGKILL
timeout --signal=HUP 30 daemon # 30秒後にSIGHUP
# 段階的タイムアウト(-k オプション)
timeout -k 10 60 long_command
# 60秒後に SIGTERM を送信
# それでも終了しなければ 10秒後に SIGKILL を送信
# 終了コードの確認
timeout 5 sleep 10
echo $? # 124 = タイムアウトで終了
# 終了コード:
# 124: SIGTERM でタイムアウト
# 137: SIGKILL でタイムアウト(128 + 9)
# それ以外: コマンド自身の終了コード
# タイムアウトかどうかの判定
timeout 5 some_command
EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
echo "タイムアウトしました"
elif [ $EXIT_CODE -eq 0 ]; then
echo "正常完了"
else
echo "エラー終了 (コード: $EXIT_CODE)"
fi
# フォアグラウンドで実行(--foreground)
timeout --foreground 60 interactive_command
# 対話的なコマンドに使う場合
# 実践例: APIのタイムアウト付きヘルスチェック
timeout 5 curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health
if [ $? -eq 124 ]; then
echo "API応答タイムアウト"
fi
# 実践例: タイムアウト付きファイル待機
timeout 60 bash -c 'while [ ! -f /tmp/ready.flag ]; do sleep 1; done'
if [ $? -eq 124 ]; then
echo "ファイルが作成されませんでした(タイムアウト)"
fi6. プロセスグループとセッション
6.1 概念の理解
# プロセスの階層構造:
#
# セッション(SID)
# └─ プロセスグループ(PGID)
# └─ プロセス(PID)
# └─ スレッド(TID)
#
# セッション: ログインから始まる一連のプロセスの集合
# プロセスグループ: パイプラインなどで関連するプロセスの集合
# セッションリーダー: セッションを開始したプロセス(ログインシェル)
# 確認方法
ps -eo pid,ppid,pgid,sid,tty,cmd | head -20
# 例: cat file | grep pattern | sort
# この3つのプロセスは同じプロセスグループに属する
# → Ctrl+C で3つ全部にSIGINTが送られる
# 現在のプロセスの情報
echo "PID: $$" # プロセスID
echo "PPID: $PPID" # 親プロセスID
ps -p $$ -o pid,ppid,pgid,sid # グループ・セッション情報
# プロセスグループIDの確認
ps -eo pid,pgid,cmd | grep nginx
# プロセスグループにシグナル送信
kill -TERM -$(ps -o pgid= -p 1234 | tr -d ' ')
# PGID の前にマイナスをつけて、グループ全体にシグナル送信6.2 フォアグラウンド/バックグラウンドプロセスグループ
# 端末には1つのフォアグラウンドプロセスグループと
# 0個以上のバックグラウンドプロセスグループがある
# フォアグラウンドプロセスグループ:
# - 端末からの入力を受け取れる
# - Ctrl+C, Ctrl+Z のシグナルを受け取る
# - 1つの端末に1つだけ
# バックグラウンドプロセスグループ:
# - 端末からの入力を受け取れない(試みるとSIGTTIN/SIGTTOUで停止)
# - Ctrl+C, Ctrl+Z のシグナルを受け取らない
# - 複数存在可能
# fg/bg はフォアグラウンドプロセスグループの切り替え7. 実践パターン集
7.1 暴走プロセスの対処
# CPU 90%以上のプロセスを表示
ps aux --sort=-%cpu | awk 'NR<=1 || $3>90'
# 確認してから kill
kill $(ps aux --sort=-%cpu | awk 'NR==2 {print $2}')
# 特定ユーザーの全プロセスを終了(慎重に)
pkill -u problematic_user
# プロセスを段階的に終了
graceful_kill() {
local pid=$1
kill -TERM "$pid" 2>/dev/null || return 0
for i in $(seq 1 10); do
sleep 1
kill -0 "$pid" 2>/dev/null || return 0
done
kill -KILL "$pid" 2>/dev/null
}7.2 全子プロセスの終了
# 親PIDの子プロセスを全て終了
pkill -P 1234 # 直接の子プロセスのみ
# 子孫プロセス全体を終了(再帰的)
kill_descendants() {
local pid=$1
local children=$(pgrep -P "$pid")
for child in $children; do
kill_descendants "$child"
done
kill -TERM "$pid" 2>/dev/null
}
kill_descendants 1234
# プロセスグループ全体を終了(より簡単)
kill -TERM -1234 # PGIDの全プロセスを終了7.3 バックグラウンドタスクの管理
# バックグラウンドタスクの完了を待ち、結果を収集
#!/bin/bash
declare -A TASK_PIDS
# タスク起動
for server in web1 web2 web3 db1 db2; do
ssh "$server" "uptime" > "/tmp/uptime_${server}.txt" 2>&1 &
TASK_PIDS[$server]=$!
done
# 結果収集
FAILED=()
for server in "${!TASK_PIDS[@]}"; do
pid=${TASK_PIDS[$server]}
if wait "$pid"; then
echo "OK: $server - $(cat /tmp/uptime_${server}.txt)"
else
echo "FAIL: $server"
FAILED+=("$server")
fi
done
if [ ${#FAILED[@]} -gt 0 ]; then
echo "失敗したサーバー: ${FAILED[*]}"
exit 1
fi7.4 タイムアウト付きリトライ
#!/bin/bash
# retry_with_timeout.sh - タイムアウト付きリトライ
retry_command() {
local max_retries=${1:-3}
local timeout_sec=${2:-30}
local retry_delay=${3:-5}
shift 3
local attempt=0
while [ $attempt -lt $max_retries ]; do
attempt=$((attempt + 1))
echo "試行 $attempt/$max_retries: $*"
if timeout "$timeout_sec" "$@"; then
echo "成功 (試行 $attempt)"
return 0
fi
local exit_code=$?
if [ $exit_code -eq 124 ]; then
echo "タイムアウト (${timeout_sec}秒)"
else
echo "失敗 (終了コード: $exit_code)"
fi
if [ $attempt -lt $max_retries ]; then
echo "${retry_delay}秒後にリトライ..."
sleep "$retry_delay"
fi
done
echo "全 $max_retries 回失敗"
return 1
}
# 使用例
retry_command 3 10 5 curl -s https://api.example.com/health7.5 デーモン化スクリプト
#!/bin/bash
# simple_daemon.sh - シンプルなデーモン化スクリプト
PIDFILE="/var/run/myapp.pid"
LOGFILE="/var/log/myapp.log"
start() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "既に実行中 (PID: $(cat "$PIDFILE"))"
return 1
fi
echo "起動中..."
nohup /usr/local/bin/myapp > "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
echo "起動完了 (PID: $(cat "$PIDFILE"))"
}
stop() {
if [ ! -f "$PIDFILE" ]; then
echo "PIDファイルがありません"
return 1
fi
local pid=$(cat "$PIDFILE")
if ! kill -0 "$pid" 2>/dev/null; then
echo "プロセスが存在しません (PID: $pid)"
rm -f "$PIDFILE"
return 1
fi
echo "停止中 (PID: $pid)..."
kill "$pid"
local timeout=30
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
timeout=$((timeout - 1))
done
if kill -0 "$pid" 2>/dev/null; then
echo "SIGKILL で強制停止..."
kill -9 "$pid"
fi
rm -f "$PIDFILE"
echo "停止完了"
}
status() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "実行中 (PID: $(cat "$PIDFILE"))"
else
echo "停止中"
[ -f "$PIDFILE" ] && rm -f "$PIDFILE"
fi
}
restart() {
stop
sleep 2
start
}
case "${1:-}" in
start) start ;;
stop) stop ;;
restart) restart ;;
status) status ;;
*) echo "使い方: $0 {start|stop|restart|status}" ;;
esac実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
トラブルシューティング
よくあるエラーと解決策
| エラー | 原因 | 解決策 |
|---|---|---|
| 初期化エラー | 設定ファイルの不備 | 設定ファイルのパスと形式を確認 |
| タイムアウト | ネットワーク遅延/リソース不足 | タイムアウト値の調整、リトライ処理の追加 |
| メモリ不足 | データ量の増大 | バッチ処理の導入、ページネーションの実装 |
| 権限エラー | アクセス権限の不足 | 実行ユーザーの権限確認、設定の見直し |
| データ不整合 | 並行処理の競合 | ロック機構の導入、トランザクション管理 |
デバッグの手順
- エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
- 再現手順の確立: 最小限のコードでエラーを再現する
- 仮説の立案: 考えられる原因をリストアップする
- 段階的な検証: ログ出力やデバッガを使って仮説を検証する
- 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
# ロガーの設定
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
def debug_decorator(func):
"""関数の入出力をログ出力するデコレータ"""
@wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
try:
result = func(*args, **kwargs)
logger.debug(f"戻り値: {func.__name__} -> {result}")
return result
except Exception as e:
logger.error(f"例外発生: {func.__name__}: {e}")
logger.error(traceback.format_exc())
raise
return wrapper
@debug_decorator
def process_data(items):
"""データ処理(デバッグ対象)"""
if not items:
raise ValueError("空のデータ")
return [item * 2 for item in items]パフォーマンス問題の診断
パフォーマンス問題が発生した場合の診断手順:
- ボトルネックの特定: プロファイリングツールで計測
- メモリ使用量の確認: メモリリークの有無をチェック
- I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
- 同時接続数の確認: コネクションプールの状態を確認
| 問題の種類 | 診断ツール | 対策 |
|---|---|---|
| CPU負荷 | cProfile, py-spy | アルゴリズム改善、並列化 |
| メモリリーク | tracemalloc, objgraph | 参照の適切な解放 |
| I/Oボトルネック | strace, iostat | 非同期I/O、キャッシュ |
| DB遅延 | EXPLAIN, slow query log | インデックス、クエリ最適化 |
設計判断ガイド
選択基準マトリクス
技術選択を行う際の判断基準を以下にまとめます。
| 判断基準 | 重視する場合 | 妥協できる場合 |
|---|---|---|
| パフォーマンス | リアルタイム処理、大規模データ | 管理画面、バッチ処理 |
| 保守性 | 長期運用、チーム開発 | プロトタイプ、短期プロジェクト |
| スケーラビリティ | 成長が見込まれるサービス | 社内ツール、固定ユーザー |
| セキュリティ | 個人情報、金融データ | 公開データ、社内利用 |
| 開発速度 | MVP、市場投入スピード | 品質重視、ミッションクリティカル |
アーキテクチャパターンの選択
| アーキテクチャ選択フロー |
|---|
| ① チーム規模は? |
| ├─ 小規模(1-5人)→ モノリス |
| └─ 大規模(10人+)→ ②へ |
| ② デプロイ頻度は? |
| ├─ 週1回以下 → モノリス + モジュール分割 |
| └─ 毎日/複数回 → ③へ |
| ③ チーム間の独立性は? |
| ├─ 高い → マイクロサービス |
| └─ 中程度 → モジュラーモノリス |
トレードオフの分析
技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:
1. 短期 vs 長期のコスト
- 短期的に速い方法が長期的には技術的負債になることがある
- 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く
2. 一貫性 vs 柔軟性
- 統一された技術スタックは学習コストが低い
- 多様な技術の採用は適材適所が可能だが、運用コストが増加
3. 抽象化のレベル
- 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
- 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
"""ADR (Architecture Decision Record) の作成"""
def __init__(self, title: str):
self.title = title
self.context = ""
self.decision = ""
self.consequences = []
self.alternatives = []
def set_context(self, context: str):
"""背景と課題の記述"""
self.context = context
return self
def set_decision(self, decision: str):
"""決定内容の記述"""
self.decision = decision
return self
def add_consequence(self, consequence: str, positive: bool = True):
"""結果の追加"""
self.consequences.append({
'description': consequence,
'type': 'positive' if positive else 'negative'
})
return self
def add_alternative(self, name: str, reason_rejected: str):
"""却下した代替案の追加"""
self.alternatives.append({
'name': name,
'reason_rejected': reason_rejected
})
return self
def to_markdown(self) -> str:
"""Markdown形式で出力"""
md = f"# ADR: {self.title}\n\n"
md += f"## 背景\n{self.context}\n\n"
md += f"## 決定\n{self.decision}\n\n"
md += "## 結果\n"
for c in self.consequences:
icon = "✅" if c['type'] == 'positive' else "⚠️"
md += f"- {icon} {c['description']}\n"
md += "\n## 却下した代替案\n"
for a in self.alternatives:
md += f"- **{a['name']}**: {a['reason_rejected']}\n"
return md実務での適用シナリオ
シナリオ1: スタートアップでのMVP開発
状況: 限られたリソースで素早くプロダクトをリリースする必要がある
アプローチ:
- シンプルなアーキテクチャを選択
- 必要最小限の機能に集中
- 自動テストはクリティカルパスのみ
- モニタリングは早期から導入
学んだ教訓:
- 完璧を求めすぎない(YAGNI原則)
- ユーザーフィードバックを早期に取得
- 技術的負債は意識的に管理する
シナリオ2: レガシーシステムのモダナイゼーション
状況: 10年以上運用されているシステムを段階的に刷新する
アプローチ:
- Strangler Fig パターンで段階的に移行
- 既存のテストがない場合はCharacterization Testを先に作成
- APIゲートウェイで新旧システムを共存
- データ移行は段階的に実施
| フェーズ | 作業内容 | 期間目安 | リスク |
|---|---|---|---|
| 1. 調査 | 現状分析、依存関係の把握 | 2-4週間 | 低 |
| 2. 基盤 | CI/CD構築、テスト環境 | 4-6週間 | 低 |
| 3. 移行開始 | 周辺機能から順次移行 | 3-6ヶ月 | 中 |
| 4. コア移行 | 中核機能の移行 | 6-12ヶ月 | 高 |
| 5. 完了 | 旧システム廃止 | 2-4週間 | 中 |
シナリオ3: 大規模チームでの開発
状況: 50人以上のエンジニアが同一プロダクトを開発する
アプローチ:
- ドメイン駆動設計で境界を明確化
- チームごとにオーナーシップを設定
- 共通ライブラリはInner Source方式で管理
- APIファーストで設計し、チーム間の依存を最小化
# チーム間のAPI契約定義
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum
class Priority(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class APIContract:
"""チーム間のAPI契約"""
endpoint: str
method: str
owner_team: str
consumers: List[str]
sla_ms: int # レスポンスタイムSLA
priority: Priority
def validate_sla(self, actual_ms: int) -> bool:
"""SLA準拠の確認"""
return actual_ms <= self.sla_ms
def to_openapi(self) -> dict:
"""OpenAPI形式で出力"""
return {
'path': self.endpoint,
'method': self.method,
'x-owner': self.owner_team,
'x-consumers': self.consumers,
'x-sla-ms': self.sla_ms
}
# 使用例
contracts = [
APIContract(
endpoint="/api/v1/users",
method="GET",
owner_team="user-team",
consumers=["order-team", "notification-team"],
sla_ms=200,
priority=Priority.HIGH
),
APIContract(
endpoint="/api/v1/orders",
method="POST",
owner_team="order-team",
consumers=["payment-team", "inventory-team"],
sla_ms=500,
priority=Priority.CRITICAL
)
]シナリオ4: パフォーマンスクリティカルなシステム
状況: ミリ秒単位のレスポンスが求められるシステム
最適化ポイント:
- キャッシュ戦略(L1: インメモリ、L2: Redis、L3: CDN)
- 非同期処理の活用
- コネクションプーリング
- クエリ最適化とインデックス設計
| 最適化手法 | 効果 | 実装コスト | 適用場面 |
|---|---|---|---|
| インメモリキャッシュ | 高 | 低 | 頻繁にアクセスされるデータ |
| CDN | 高 | 低 | 静的コンテンツ |
| 非同期処理 | 中 | 中 | I/O待ちが多い処理 |
| DB最適化 | 高 | 高 | クエリが遅い場合 |
| コード最適化 | 低-中 | 高 | CPU律速の場合 |
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 操作 | コマンド | 備考 |
|---|---|---|
| バックグラウンド実行 | command & | PID は $! で取得 |
| 一時停止 | Ctrl+Z | SIGTSTP 送信 |
| フォアグラウンドに戻す | fg %N | |
| バックグラウンドで再開 | bg %N | |
| 正常終了要求 | kill PID | SIGTERM(デフォルト) |
| 強制終了(最終手段) | kill -9 PID | SIGKILL(クリーンアップなし) |
| 設定再読み込み | kill -HUP PID | SIGHUP |
| プロセス名で終了 | pkill -f "pattern" | 正規表現対応 |
| 切断後も継続(事前) | nohup command & | 出力は nohup.out |
| 切断後も継続(事後) | disown %N | ジョブリストから除外 |
| シグナル捕捉 | trap 'handler' SIGNAL | EXIT が最重要 |
| 全ジョブ完了待ち | wait | スクリプトの並列処理 |
| タイムアウト付き実行 | timeout 60 command | 124=タイムアウト |
次に読むべきガイド
参考文献
- Barrett, D. "Efficient Linux at the Command Line." Ch.8-9, O'Reilly, 2022.
- Shotts, W. "The Linux Command Line." Ch.10-11, No Starch Press, 2019.
- Cooper, M. "Advanced Bash-Scripting Guide." Ch.15 (Signals), tldp.org.
- Kerrisk, M. "The Linux Programming Interface." Ch.20-22 (Signals), No Starch Press, 2010.
- "signal(7) — Linux manual page." man7.org.