New Relic入門 第3A部 - APM基礎:アプリケーション監視をマスターしよう

Part 2でセットアップを完了したら、いよいよNew Relicの真価を発揮する**APM(Application Performance Monitoring)**を本格活用しましょう。本記事では、実際の問題解決事例とともに、APMによるアプリケーション監視の基礎から実践まで詳しく解説します。

🎯 この記事で習得できること

  • [ ] APMの基本概念とアーキテクチャ理解
  • [ ] パフォーマンス問題の特定手法(実際の改善事例付き)
  • [ ] メモリリーク検出と対処方法
  • [ ] 分散トレーシングによるマイクロサービス問題特定
  • [ ] エラー追跡とアラート設定のベストプラクティス

3.1 APMの基本概念とアーキテクチャ

📊 APMが監視する主要メトリクス

**APM(Application Performance Monitoring)**は、アプリケーションのパフォーマンスをリアルタイムで監視し、問題の早期発見と迅速な解決を支援する機能です。

yaml
# Core APM Metrics
Response Time(レスポンス時間):
  - Application response time(平均・95パーセンタイル)
  - Database query time
  - External service call time

Throughput(スループット):
  - Requests per minute (RPM)
  - Pages per minute (PPM)
  - Background job processing rate

Error Rate(エラー率):
  - Error percentage
  - Exception frequency
  - HTTP error codes (4xx, 5xx)

Resource Utilization(リソース使用率):
  - CPU usage by transaction
  - Memory consumption patterns
  - Database connection pool usage

🏗️ APMアーキテクチャの理解

mermaid
graph TD
    A[ユーザーリクエスト] --> B[ロードバランサー]
    B --> C[Webサーバー]
    C --> D[APMエージェント]
    D --> E[アプリケーション]
    E --> F[データベース]
    E --> G[外部API]
    
    D --> H[New Relic Collector]
    H --> I[New Relic Platform]
    I --> J[ダッシュボード]
    
    style D fill:#f9f,stroke:#333,stroke-width:2px
    style H fill:#9ff,stroke:#333,stroke-width:2px

APMエージェントの役割:

  • アプリケーション内でのメトリクス収集
  • 分散トレーシング情報の生成
  • エラーとエクセプションの自動検出
  • リアルタイムでのデータ送信

3.2 パフォーマンス問題の特定手法

🔍 1. レスポンス時間劣化の分析

実際の問題シナリオ:ECサイトの商品検索API

状況: ECサイトの商品検索APIが急激に遅くなった(通常200ms → 現在1.2s)

分析ステップ:

javascript
// Step 1: New Relic APMダッシュボードでの確認項目
1. Overview画面でのResponse Time trend確認
   - 通常: 200ms → 現在: 1.2s
   - 問題発生時刻: 2025-07-15 14:30頃
   
2. Transactionsページでのボトルネック特定
   - GET /api/products/search: 85% of total time
   - 他のエンドポイントは正常
   
3. Database queriesタブでのSlow Query特定
   - SELECT * FROM products WHERE name LIKE '%keyword%': 950ms
   - 実行回数: 1回のリクエストで平均3.2回実行

実際のクエリ最適化例:

sql
-- 🚫 問題のあるクエリ
SELECT * FROM products 
WHERE name LIKE '%キーワード%' 
   OR description LIKE '%キーワード%'
ORDER BY created_at DESC;

-- ✅ 最適化後のクエリ
SELECT id, name, price, thumbnail_url, category_id
FROM products 
WHERE MATCH(name, description) AGAINST('キーワード' IN BOOLEAN MODE)
  AND status = 'active'
  AND stock_quantity > 0
ORDER BY relevance_score DESC, created_at DESC
LIMIT 20;

-- インデックス追加
CREATE FULLTEXT INDEX idx_products_search ON products(name, description);
CREATE INDEX idx_products_status_stock ON products(status, stock_quantity, created_at);
CREATE INDEX idx_products_category ON products(category_id, status);

最適化結果の測定:

yaml
改善前:
  - レスポンス時間: 1.2s
  - スループット: 50 RPM
  - エラー率: 3.2%
  - DB接続時間: 950ms

改善後:
  - レスポンス時間: 180ms(85%改善)
  - スループット: 300 RPM(6倍向上)
  - エラー率: 0.1%(96%削減)
  - DB接続時間: 45ms(95%改善)

New RelicでのSlow Queryの確認方法

javascript
// APMダッシュボードでの確認手順
1. APM → Applications → [Your App Name]
2. "Database" タブをクリック
3. "Slow Queries" セクションで詳細を確認
4. クエリ実行時間、呼び出し頻度、影響度を分析

// カスタムメトリクスでの追跡
const newrelic = require('newrelic');

async function searchProducts(keyword) {
  const startTime = Date.now();
  
  try {
    const results = await database.search(keyword);
    
    // 検索パフォーマンスをトラッキング
    newrelic.recordMetric('Custom/Search/ResponseTime', Date.now() - startTime);
    newrelic.recordMetric('Custom/Search/ResultCount', results.length);
    
    return results;
  } catch (error) {
    newrelic.noticeError(error, {
      searchKeyword: keyword,
      searchDuration: Date.now() - startTime
    });
    throw error;
  }
}

🧠 2. メモリリーク検出と対処

Node.js アプリケーションのメモリリーク例

問題の症状:

  • アプリケーションが時間経過とともに徐々に遅くなる
  • メモリ使用量が増加し続ける
  • 最終的にOut of Memory エラーでクラッシュ
javascript
// 🚫 メモリリークを起こすコード例
class DataProcessor {
  constructor() {
    this.cache = new Map();
    this.eventListeners = [];
  }
  
  // 問題1: キャッシュのクリアが不十分
  processData(data) {
    const cacheKey = `${data.id}_${Date.now()}`;
    this.cache.set(cacheKey, data); // キャッシュが永続的に蓄積
    
    return this.transformData(data);
  }
  
  // 問題2: イベントリスナーの解除忘れ
  setupWebSocket(ws) {
    const handler = (message) => {
      this.handleMessage(message);
    };
    
    ws.on('message', handler);
    this.eventListeners.push(handler); // 参照が残り続ける
  }
}

// ✅ 修正されたコード
class OptimizedDataProcessor {
  constructor() {
    this.cache = new Map();
    this.eventListeners = new WeakMap(); // WeakMapを使用
    this.maxCacheSize = 1000;
  }
  
  processData(data) {
    // キャッシュサイズ制限
    if (this.cache.size >= this.maxCacheSize) {
      this.cleanupOldCache();
    }
    
    const cacheKey = `${data.id}`;
    this.cache.set(cacheKey, {
      data: data,
      timestamp: Date.now(),
      ttl: 300000 // 5分でTTL
    });
    
    return this.transformData(data);
  }
  
  cleanupOldCache() {
    const now = Date.now();
    for (const [key, value] of this.cache.entries()) {
      if (now - value.timestamp > value.ttl) {
        this.cache.delete(key);
      }
    }
  }
  
  setupWebSocket(ws) {
    const handler = (message) => {
      this.handleMessage(message);
    };
    
    ws.on('message', handler);
    
    // クリーンアップ関数を提供
    const cleanup = () => {
      ws.removeListener('message', handler);
    };
    
    ws.on('close', cleanup);
    ws.on('error', cleanup);
    
    return cleanup;
  }
}

New Relicでのメモリ監視設定:

javascript
// カスタムメトリクスでメモリ使用量を追跡
const newrelic = require('newrelic');

// 定期的なメモリ監視
setInterval(() => {
  const memUsage = process.memoryUsage();
  
  newrelic.recordMetric('Custom/Memory/RSS', memUsage.rss);
  newrelic.recordMetric('Custom/Memory/HeapUsed', memUsage.heapUsed);
  newrelic.recordMetric('Custom/Memory/HeapTotal', memUsage.heapTotal);
  newrelic.recordMetric('Custom/Memory/External', memUsage.external);
  
  // メモリリークの早期警告
  const heapUsedMB = memUsage.heapUsed / 1024 / 1024;
  if (heapUsedMB > 500) { // 500MB閾値
    newrelic.addCustomAttribute('memory_warning', true);
    newrelic.recordMetric('Custom/Memory/Warning', 1);
  }
}, 30000); // 30秒間隔

🔗 3. 分散トレーシングによるマイクロサービス問題特定

マイクロサービス環境での問題分析

シナリオ: ユーザー注文処理で断続的にタイムアウトが発生

yaml
# マイクロサービス構成
Frontend API → Auth Service → Order Service → Payment Service → Inventory Service
                            → Email Service
                            → Notification Service

分散トレーシングでの問題特定:

javascript
// Frontend API (Node.js)
const newrelic = require('newrelic');

app.post('/api/orders', async (req, res) => {
  const transaction = newrelic.getTransaction();
  
  try {
    // 1. 認証サービス呼び出し
    const authResult = await authService.validateUser(req.headers.authorization);
    newrelic.addCustomAttribute('auth.user_id', authResult.userId);
    
    // 2. 注文処理開始
    const orderData = {
      ...req.body,
      userId: authResult.userId,
      traceId: transaction.traceId // トレースIDを伝播
    };
    
    const order = await orderService.createOrder(orderData);
    newrelic.addCustomAttribute('order.id', order.id);
    
    res.json({ success: true, orderId: order.id });
    
  } catch (error) {
    newrelic.noticeError(error, {
      service: 'frontend-api',
      endpoint: '/api/orders',
      userId: req.body.userId
    });
    res.status(500).json({ error: 'Order processing failed' });
  }
});

// Order Service (Java)
@RestController
public class OrderController {
    
    @PostMapping("/orders")
    @Trace(dispatcher = true)
    public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
        try {
            // 分散トレーシング情報を追加
            NewRelic.addCustomParameter("order.total", request.getTotal());
            NewRelic.addCustomParameter("order.items_count", request.getItems().size());
            
            // 在庫確認サービス呼び出し
            InventoryResponse inventory = inventoryService.checkAvailability(request);
            
            if (!inventory.isAvailable()) {
                NewRelic.recordMetric("Custom/Order/InventoryUnavailable", 1);
                throw new InventoryUnavailableException("Items not available");
            }
            
            // 決済処理
            PaymentResponse payment = paymentService.processPayment(request.getPayment());
            NewRelic.addCustomParameter("payment.transaction_id", payment.getTransactionId());
            
            // 注文作成
            Order order = orderRepository.save(new Order(request, payment.getTransactionId()));
            
            // 非同期通知(パフォーマンス改善)
            notificationService.sendOrderConfirmationAsync(order);
            
            return ResponseEntity.ok(order);
            
        } catch (Exception e) {
            NewRelic.noticeError(e);
            throw e;
        }
    }
}

分散トレーシングでの問題発見:

yaml
# New Relicで発見された問題パターン
Trace Analysis Results:
  - Frontend API: 50ms (正常)
  - Auth Service: 120ms (正常)
  - Order Service: 200ms (正常)
  - Inventory Service: 15,000ms (タイムアウト!)
  - Payment Service: 実行されず
  - Email Service: 実行されず

Root Cause:
  - Inventory Serviceで外部API呼び出しがタイムアウト
  - データベースのデッドロック発生
  - 接続プールの枯渇

Solution:
  - 在庫確認のキャッシュ実装
  - データベースクエリの最適化
  - 接続プール設定の見直し
  - サーキットブレーカーパターンの導入

3.3 エラー追跡とアラート設定

🚨 エラー分類とコンテキスト情報

javascript
// 効果的なエラー追跡の実装
const newrelic = require('newrelic');

class ErrorTracker {
  static trackError(error, context = {}) {
    // エラー分類
    const errorType = this.classifyError(error);
    
    // 詳細なコンテキスト情報
    const enrichedContext = {
      ...context,
      errorType: errorType,
      timestamp: new Date().toISOString(),
      userAgent: context.userAgent || 'unknown',
      sessionId: context.sessionId || 'anonymous',
      apiVersion: process.env.API_VERSION,
      environment: process.env.NODE_ENV
    };
    
    // New Relicにエラー送信
    newrelic.noticeError(error, enrichedContext);
    
    // カスタムメトリクス更新
    newrelic.recordMetric(`Custom/Errors/${errorType}`, 1);
    
    // 重要度に応じた追加処理
    if (this.isCriticalError(error)) {
      this.alertCriticalError(error, enrichedContext);
    }
  }
  
  static classifyError(error) {
    const errorMapping = {
      'ValidationError': 'validation',
      'AuthenticationError': 'authentication',
      'AuthorizationError': 'authorization',
      'DatabaseError': 'database',
      'NetworkError': 'network',
      'TimeoutError': 'timeout',
      'RateLimitError': 'rate_limit'
    };
    
    return errorMapping[error.constructor.name] || 'unknown';
  }
  
  static isCriticalError(error) {
    const criticalTypes = [
      'DatabaseError',
      'PaymentError',
      'SecurityError'
    ];
    
    return criticalTypes.includes(error.constructor.name);
  }
  
  static alertCriticalError(error, context) {
    // 重要なエラーの場合、追加のカスタム属性
    newrelic.addCustomAttribute('critical_error', true);
    newrelic.addCustomAttribute('alert_level', 'critical');
    newrelic.recordMetric('Custom/Errors/Critical', 1);
  }
}

// 使用例
app.use((error, req, res, next) => {
  ErrorTracker.trackError(error, {
    url: req.url,
    method: req.method,
    userAgent: req.get('User-Agent'),
    sessionId: req.session?.id,
    userId: req.user?.id
  });
  
  res.status(500).json({ error: 'Internal Server Error' });
});

📢 実践的なアラート設定

yaml
# New Relicアラートポリシー設定例
alert_policies:
  - name: "Application Performance Alerts"
    incident_preference: "PER_POLICY"
    
    conditions:
      - name: "High Error Rate"
        type: "apm_app_metric"
        entities: ["MyApp"]
        metric: "error_percentage"
        condition_scope: "application"
        terms:
          - priority: "warning"
            threshold: "5"     # 5%
            threshold_duration: "300"  # 5 minutes
            threshold_occurrences: "all"
          - priority: "critical"
            threshold: "10"    # 10%
            threshold_duration: "300"  # 5 minutes
            threshold_occurrences: "all"
        
      - name: "Slow Response Time"
        type: "apm_app_metric" 
        entities: ["MyApp"]
        metric: "response_time_web"
        condition_scope: "application"
        terms:
          - priority: "warning"
            threshold: "500"  # 500ms
            threshold_duration: "600"  # 10 minutes
            threshold_occurrences: "all"
          - priority: "critical"
            threshold: "1000"  # 1 second
            threshold_duration: "300"  # 5 minutes
            threshold_occurrences: "all"

notification_channels:
  - name: "DevOps Team Slack"
    type: "slack"
    configuration:
      url: "https://hooks.slack.com/services/xxx"
      channel: "#alerts"
      
  - name: "PagerDuty Integration"
    type: "pagerduty"
    configuration:
      service_key: "your-pagerduty-key"

🎯 アラート疲れを防ぐベストプラクティス

yaml
# 効果的なアラート戦略
Smart_Alerting:
  1. 段階的な重要度設定:
     Info: → Slackに通知
     Warning: → Slack + Email
     Critical: → Slack + Email + PagerDuty
  
  2. 時間ベースのしきい値:
     営業時間中: より敏感な設定
     営業時間外: 重要なアラートのみ
  
  3. 動的ベースライン:
     静的な閾値ではなく、過去のパフォーマンスベース
     季節性やトレンドを考慮
  
  4. アラートのグルーピング:
     関連するアラートをまとめて通知
     同一インシデントの重複通知を防止

# 実装例
alert_optimization:
  escalation_policy:
    - level: 1
      wait_time: 5_minutes
      targets: ["slack_channel"]
    - level: 2  
      wait_time: 15_minutes
      targets: ["on_call_engineer"]
    - level: 3
      wait_time: 30_minutes
      targets: ["engineering_manager"]

🎉 APM基礎マスター完了

✅ 習得チェックリスト

APM基礎を完了すると、以下ができるようになります:

  • [ ] APMダッシュボードでパフォーマンス問題を特定できる
  • [ ] レスポンス時間劣化の原因分析と対処ができる
  • [ ] メモリリークの検出と修正ができる
  • [ ] 分散トレーシングで複雑なサービス間の問題を追跡できる
  • [ ] 効果的なエラー追跡とアラート設定ができる

📈 実践的な改善事例

このPartで学んだ手法を実践することで実現できる改善:

yaml
Performance_Improvements:
  - レスポンス時間: 85%改善(1.2s → 180ms)
  - スループット: 6倍向上(50 RPM → 300 RPM)
  - エラー率: 96%削減(3.2% → 0.1%)
  - メモリ使用量: 40%削減(適切なキャッシュ管理)

Operational_Benefits:
  - MTTR(平均復旧時間): 90%短縮(4時間 → 30分)
  - 問題発見時間: 95%短縮(手動発見 → 自動アラート)
  - デプロイメント信頼性: 向上(パフォーマンス回帰の自動検出)

🚀 次のステップ

APM基礎をマスターしたら、以下の学習を進めてください:

  1. Part 3B: Infrastructure Monitoring - サーバー・インフラ監視の詳細活用
  2. 第9章: Browser・RUM監視 - フロントエンド監視とユーザー体験最適化
  3. Part 4: データ分析とカスタマイゼーション - NRQL習得とダッシュボード作成

💡 実践のコツ

yaml
APM_Best_Practices:
  日常運用:
    - 毎朝APM Overviewで前日の状況確認
    - 週次でSlow Queryの分析と最適化
    - 月次でエラー傾向の分析と対策立案
  
  問題対応:
    - まずAPM Overviewで全体像を把握
    - Transactionで具体的なボトルネックを特定
    - 分散トレーシングで問題の根本原因を追跡
  
  継続改善:
    - パフォーマンスベースラインの定期見直し
    - アラート精度の継続的改善
    - チーム内でのAPMノウハウ共有

🤖 より深い監視機能を学ぶ: Part 3B: Infrastructure Monitoringで、サーバーとクラウド環境の包括的な監視手法を習得しましょう!