LESSON 30分

ストーリー

「障害が起きてから気づくのでは遅い。その前兆を捕まえるんだ」

高橋アーキテクトがモニタリングダッシュボードを見せる。CPU使用率、クエリレスポンスタイム、コネクション数…。

「データベースは”沈黙のインフラ”だ。問題を自分から報告してくれない。だから我々がメトリクスを監視し、閾値を超えたらアラートを飛ばす仕組みを作る必要がある」


監視すべきメトリクス

システムメトリクス

メトリクス閾値目安問題の兆候
CPU使用率> 80%クエリ最適化が必要
メモリ使用率> 85%shared_buffers調整、メモリリーク
ディスク使用率> 80%容量拡張、不要データ削除
ディスクI/O待機時間 > 10msSSD化、クエリ最適化

データベースメトリクス

メトリクス閾値目安対策
アクティブコネクション数> 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(),
    });
  }
});

アラート設計

アラートレベル条件アクション
CriticalDB接続不可即座にオンコール対応
Criticalレプリケーション遅延 > 30秒即座にオンコール対応
WarningCPU > 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分