ストーリー
「障害が起きてから気づくのでは遅い。その前兆を捕まえるんだ」
高橋アーキテクトがモニタリングダッシュボードを見せる。CPU使用率、クエリレスポンスタイム、コネクション数…。
「データベースは”沈黙のインフラ”だ。問題を自分から報告してくれない。だから我々がメトリクスを監視し、閾値を超えたらアラートを飛ばす仕組みを作る必要がある」
監視すべきメトリクス
システムメトリクス
| メトリクス | 閾値目安 | 問題の兆候 |
|---|---|---|
| CPU使用率 | > 80% | クエリ最適化が必要 |
| メモリ使用率 | > 85% | shared_buffers調整、メモリリーク |
| ディスク使用率 | > 80% | 容量拡張、不要データ削除 |
| ディスクI/O | 待機時間 > 10ms | SSD化、クエリ最適化 |
データベースメトリクス
| メトリクス | 閾値目安 | 対策 |
|---|---|---|
| アクティブコネクション数 | > max_connections の 80% | コネクションプール調整 |
| クエリレスポンスタイム(p95) | > 100ms | スロークエリ最適化 |
| スロークエリ数 | > 10件/分 | インデックス追加、クエリ改善 |
| デッドロック数 | > 0 | ロック順序の見直し |
| レプリケーション遅延 | > 1秒 | レプリカの性能確認 |
| テーブル膨張率 | > 20% | VACUUM調整 |
PostgreSQLの監視クエリ
アクティブクエリの確認
-- 現在実行中のクエリ
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE state != 'idle'
AND query NOT LIKE '%pg_stat_activity%'
ORDER BY duration DESC;
スロークエリの特定
-- pg_stat_statementsの有効化が必要
-- shared_preload_libraries = 'pg_stat_statements'
SELECT
query,
calls,
total_exec_time / calls AS avg_time_ms,
rows / calls AS avg_rows,
total_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
テーブルの統計
-- テーブルごとのスキャン方式と行数
SELECT
relname AS table_name,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
インデックスの使用状況
-- 使われていないインデックス(削除候補)
SELECT
indexrelname AS index_name,
relname AS table_name,
idx_scan AS times_used,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
コネクション数の監視
-- コネクション状態の集計
SELECT
state,
COUNT(*) AS count,
MAX(now() - state_change) AS max_duration
FROM pg_stat_activity
GROUP BY state;
-- 設定上限の確認
SHOW max_connections;
アプリケーション側のモニタリング
// クエリパフォーマンスのログ収集
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'event' },
{ level: 'warn', emit: 'event' },
],
});
// スロークエリの検出
prisma.$on('query', (e) => {
if (e.duration > 100) { // 100ms以上
console.warn(`[SLOW QUERY] ${e.duration}ms: ${e.query}`);
// メトリクス送信
metrics.histogram('db.query.duration', e.duration, {
query: e.query.substring(0, 100),
});
}
});
// コネクションプールの監視
class ConnectionPoolMonitor {
private poolMetrics = {
totalConnections: 0,
activeConnections: 0,
idleConnections: 0,
waitingRequests: 0,
};
reportMetrics(): void {
metrics.gauge('db.pool.total', this.poolMetrics.totalConnections);
metrics.gauge('db.pool.active', this.poolMetrics.activeConnections);
metrics.gauge('db.pool.idle', this.poolMetrics.idleConnections);
metrics.gauge('db.pool.waiting', this.poolMetrics.waitingRequests);
}
}
ヘルスチェックエンドポイント
// データベースのヘルスチェック
app.get('/health/db', async (req, res) => {
const checks: Record<string, HealthCheck> = {};
try {
// 1. 接続チェック
const start = Date.now();
await prisma.$queryRaw`SELECT 1`;
checks.connection = {
status: 'ok',
responseTimeMs: Date.now() - start,
};
// 2. レプリケーション遅延チェック
const [lag] = await prisma.$queryRaw<any[]>`
SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS lag_seconds
`;
checks.replication = {
status: (lag?.lag_seconds ?? 0) < 5 ? 'ok' : 'warning',
lagSeconds: lag?.lag_seconds ?? 0,
};
// 3. コネクション数チェック
const [connInfo] = await prisma.$queryRaw<any[]>`
SELECT count(*) AS active,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_conn
FROM pg_stat_activity WHERE state = 'active'
`;
const connUsage = connInfo.active / connInfo.max_conn;
checks.connections = {
status: connUsage < 0.8 ? 'ok' : 'warning',
active: connInfo.active,
maxConnections: connInfo.max_conn,
usagePercent: Math.round(connUsage * 100),
};
const overallStatus = Object.values(checks).every(c => c.status === 'ok')
? 'healthy' : 'degraded';
res.status(overallStatus === 'healthy' ? 200 : 503).json({
status: overallStatus,
checks,
timestamp: new Date().toISOString(),
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message,
timestamp: new Date().toISOString(),
});
}
});
アラート設計
| アラートレベル | 条件 | アクション |
|---|---|---|
| Critical | DB接続不可 | 即座にオンコール対応 |
| Critical | レプリケーション遅延 > 30秒 | 即座にオンコール対応 |
| Warning | CPU > 80% が5分継続 | 30分以内に確認 |
| Warning | ディスク使用率 > 80% | 24時間以内に対応 |
| Info | スロークエリ検出 | 次回スプリントで対応 |
| Info | 未使用インデックス検出 | 定期メンテナンスで対応 |
定期メンテナンスタスク
// 定期メンテナンスのチェックリスト
const maintenanceTasks = [
{
task: 'VACUUM ANALYZE',
frequency: 'daily',
description: '不要領域の回収と統計情報の更新',
},
{
task: 'インデックス使用状況の確認',
frequency: 'weekly',
description: '未使用インデックスの特定と削除検討',
},
{
task: 'テーブル膨張率の確認',
frequency: 'weekly',
description: '膨張率20%超のテーブルにVACUUM FULL',
},
{
task: 'バックアップリストアテスト',
frequency: 'weekly',
description: 'バックアップからのリストアを検証',
},
{
task: 'スロークエリレビュー',
frequency: 'weekly',
description: 'Top 20 スロークエリの改善検討',
},
{
task: 'ディスク容量予測',
frequency: 'monthly',
description: '成長率から3ヶ月後の容量を予測',
},
];
まとめ
| ポイント | 内容 |
|---|---|
| システムメトリクス | CPU、メモリ、ディスク、I/O |
| DBメトリクス | コネクション、レスポンスタイム、スロークエリ |
| 監視クエリ | pg_stat_activity, pg_stat_statements |
| ヘルスチェック | 接続、レプリケーション、コネクション |
| 定期メンテナンス | VACUUM、インデックス見直し、バックアップテスト |
理解度チェックリスト
- 監視すべき主要メトリクスを5つ以上挙げられる
- PostgreSQLの監視クエリを書ける
- ヘルスチェックエンドポイントを実装できる
- アラートの閾値設計ができる
次のステップ
次は演習:マイグレーション計画を立てよう。実際のシナリオに対して、マイグレーション計画、バックアップ計画、モニタリング設計を総合的に行う。
推定読了時間: 30分