Skip to content

✍️ 필사 모드: バックエンドパフォーマンスエンジニアリング完全ガイド2025:プロファイリング、負荷テスト、ボトルネック分析、最適化

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

目次

1. パフォーマンスエンジニアリングのマインドセット

1.1 まず測定、最適化は後で

パフォーマンスエンジニアリングの黄金律(おうごんりつ)は「推測するな、測定せよ」です。直感(ちょっかん)に基づく最適化は、ほとんどの場合、間違った場所に時間を浪費します。

パフォーマンス最適化の3段階

  1. 測定(Measure):現在のパフォーマンスを定量的に測定
  2. 分析(Analyze):ボトルネックポイントを正確に識別
  3. 最適化(Optimize):最も影響の大きいボトルネックから解決

1.2 アムダールの法則(Amdahl's Law)

システム全体のパフォーマンス向上は、改善可能な部分の比率によって制限されます。

全体の高速化 = 1 / ((1 - P) + P / S)

P = 改善可能な部分の比率
S = その部分の速度向上倍率

例:全体の20%を占めるコードを10倍速くすると
= 1 / ((1 - 0.2) + 0.2 / 10)
= 1 / (0.8 + 0.02)
= 1.22倍(22%向上)

一方、全体の80%を占めるコードを2倍速くすると
= 1 / ((1 - 0.8) + 0.8 / 2)
= 1 / (0.2 + 0.4)
= 1.67倍(67%向上)

ポイント:小さな部分を劇的に改善するよりも、大きな部分を適度に改善する方が効果的です。

1.3 パフォーマンスバジェット

# パフォーマンスバジェット定義例
performance_budget:
  api_endpoints:
    p50_latency_ms: 50
    p95_latency_ms: 200
    p99_latency_ms: 500
    max_latency_ms: 2000
    error_rate_percent: 0.1
    throughput_rps: 1000

  database:
    query_p95_ms: 50
    query_p99_ms: 200
    connection_pool_utilization: 70
    slow_query_threshold_ms: 100

  external_services:
    p95_latency_ms: 300
    timeout_ms: 5000
    retry_count: 3
    circuit_breaker_threshold: 50

2. プロファイリング

2.1 CPUプロファイリングとFlame Graph

Flame Graphは、CPU時間がどこで消費(しょうひ)されているかを視覚的に表示する強力なツールです。

Node.js CPUプロファイリング

// Node.js - 内蔵プロファイラーを使用
// 実行: node --prof app.js
// 分析: node --prof-process isolate-*.log > profile.txt

// またはv8-profiler-nextを使用
const v8Profiler = require('v8-profiler-next');

function startProfiling(durationMs = 30000) {
  const title = `cpu-profile-${Date.now()}`;
  v8Profiler.startProfiling(title, true);

  setTimeout(() => {
    const profile = v8Profiler.stopProfiling(title);
    profile.export((error, result) => {
      if (!error) {
        require('fs').writeFileSync(
          `./profiles/${title}.cpuprofile`,
          result
        );
      }
      profile.delete();
    });
  }, durationMs);
}

// 特定リクエストのプロファイリングミドルウェア
function profilingMiddleware(req, res, next) {
  if (req.headers['x-profile'] !== 'true') {
    return next();
  }

  const title = `req-${req.method}-${req.path}-${Date.now()}`;
  v8Profiler.startProfiling(title, true);

  const originalEnd = res.end;
  res.end = function (...args) {
    const profile = v8Profiler.stopProfiling(title);
    profile.export((error, result) => {
      if (!error) {
        require('fs').writeFileSync(
          `./profiles/${title}.cpuprofile`,
          result
        );
      }
      profile.delete();
    });
    originalEnd.apply(res, args);
  };

  next();
}

Go CPUプロファイリング

package main

import (
    "net/http"
    _ "net/http/pprof"
    "runtime"
)

func main() {
    // pprofエンドポイントを有効化
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()

    // CPUプロファイル収集: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
    // Flame Graph生成: go tool pprof -http=:8080 profile.pb.gz

    runtime.SetBlockProfileRate(1)
    runtime.SetMutexProfileFraction(1)

    // アプリケーションロジック
    startServer()
}

Pythonプロファイリング

import cProfile
import pstats
from pyinstrument import Profiler

# cProfileを使用
def profile_with_cprofile(func):
    def wrapper(*args, **kwargs):
        profiler = cProfile.Profile()
        profiler.enable()
        result = func(*args, **kwargs)
        profiler.disable()

        stats = pstats.Stats(profiler)
        stats.sort_stats('cumulative')
        stats.print_stats(20)  # 上位20関数
        return result
    return wrapper

# pyinstrumentを使用(より読みやすい出力)
def profile_with_pyinstrument(func):
    def wrapper(*args, **kwargs):
        profiler = Profiler()
        profiler.start()
        result = func(*args, **kwargs)
        profiler.stop()
        print(profiler.output_text(unicode=True))
        return result
    return wrapper

2.2 メモリプロファイリング

// Node.jsヒープスナップショット
const v8 = require('v8');

function takeHeapSnapshot() {
  const snapshotStream = v8.writeHeapSnapshot();
  console.log(`Heap snapshot written to: ${snapshotStream}`);
  return snapshotStream;
}

// メモリ使用量モニタリング
function monitorMemory(intervalMs = 5000) {
  setInterval(() => {
    const usage = process.memoryUsage();
    console.log({
      rss_mb: Math.round(usage.rss / 1024 / 1024),
      heapTotal_mb: Math.round(usage.heapTotal / 1024 / 1024),
      heapUsed_mb: Math.round(usage.heapUsed / 1024 / 1024),
      external_mb: Math.round(usage.external / 1024 / 1024),
    });
  }, intervalMs);
}

// メモリリーク検出パターン
class MemoryLeakDetector {
  constructor(options = {}) {
    this.samples = [];
    this.maxSamples = options.maxSamples || 60;
    this.threshold = options.thresholdMB || 50;
  }

  sample() {
    const usage = process.memoryUsage();
    this.samples.push({
      timestamp: Date.now(),
      heapUsed: usage.heapUsed
    });

    if (this.samples.length > this.maxSamples) {
      this.samples.shift();
    }

    return this.detectLeak();
  }

  detectLeak() {
    if (this.samples.length < 10) return null;

    const first = this.samples[0].heapUsed;
    const last = this.samples[this.samples.length - 1].heapUsed;
    const diffMB = (last - first) / 1024 / 1024;

    let increasing = 0;
    for (let i = 1; i < this.samples.length; i++) {
      if (this.samples[i].heapUsed > this.samples[i - 1].heapUsed) {
        increasing++;
      }
    }

    const increaseRatio = increasing / (this.samples.length - 1);

    if (diffMB > this.threshold && increaseRatio > 0.7) {
      return {
        suspected: true,
        growthMB: diffMB.toFixed(2),
        increaseRatio: increaseRatio.toFixed(2),
      };
    }
    return null;
  }
}

2.3 I/Oプロファイリング

# Python - I/Oプロファイリング
import time
import functools
import logging

logger = logging.getLogger('io_profiler')

class IOProfiler:
    """I/O操作の時間測定デコレータ"""

    _stats = {}

    @classmethod
    def track(cls, operation_name):
        def decorator(func):
            @functools.wraps(func)
            async def async_wrapper(*args, **kwargs):
                start = time.perf_counter()
                try:
                    result = await func(*args, **kwargs)
                    duration = time.perf_counter() - start
                    cls._record(operation_name, duration, success=True)
                    return result
                except Exception:
                    duration = time.perf_counter() - start
                    cls._record(operation_name, duration, success=False)
                    raise

            @functools.wraps(func)
            def sync_wrapper(*args, **kwargs):
                start = time.perf_counter()
                try:
                    result = func(*args, **kwargs)
                    duration = time.perf_counter() - start
                    cls._record(operation_name, duration, success=True)
                    return result
                except Exception:
                    duration = time.perf_counter() - start
                    cls._record(operation_name, duration, success=False)
                    raise

            import asyncio
            if asyncio.iscoroutinefunction(func):
                return async_wrapper
            return sync_wrapper
        return decorator

    @classmethod
    def _record(cls, name, duration, success):
        if name not in cls._stats:
            cls._stats[name] = {
                'count': 0, 'total_time': 0,
                'min_time': float('inf'), 'max_time': 0,
                'errors': 0
            }
        stats = cls._stats[name]
        stats['count'] += 1
        stats['total_time'] += duration
        stats['min_time'] = min(stats['min_time'], duration)
        stats['max_time'] = max(stats['max_time'], duration)
        if not success:
            stats['errors'] += 1

    @classmethod
    def report(cls):
        for name, stats in sorted(cls._stats.items()):
            avg = stats['total_time'] / stats['count'] if stats['count'] else 0
            logger.info(
                f"{name}: count={stats['count']}, "
                f"avg={avg*1000:.1f}ms, "
                f"min={stats['min_time']*1000:.1f}ms, "
                f"max={stats['max_time']*1000:.1f}ms, "
                f"errors={stats['errors']}"
            )

3. 負荷テスト(Load Testing)

3.1 負荷テストツール比較

ツール言語プロトコル強み弱み
k6JavaScriptHTTP, WebSocket, gRPC開発者フレンドリー、CI/CD統合ブラウザテスト限定的
ArtilleryJavaScriptHTTP, WebSocket, Socket.io設定ベース、拡張性複雑なシナリオ困難
LocustPythonHTTPPythonスクリプト、分散プロトコル限定的
GatlingScala/JavaHTTP, WebSocket詳細レポート、JVM性能学習曲線
JMeterJava多様GUI、多様なプロトコルリソース消費大、古い

3.2 k6スクリプト例

import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';

// カスタムメトリクス
const errorRate = new Rate('errors');
const apiDuration = new Trend('api_duration', true);
const requestCount = new Counter('requests');

// テストオプション
export const options = {
  scenarios: {
    normal_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },
        { duration: '5m', target: 50 },
        { duration: '2m', target: 100 },
        { duration: '5m', target: 100 },
        { duration: '2m', target: 0 },
      ],
    },
    spike_test: {
      executor: 'ramping-vus',
      startVUs: 0,
      startTime: '16m',
      stages: [
        { duration: '10s', target: 500 },
        { duration: '1m', target: 500 },
        { duration: '10s', target: 0 },
      ],
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<200', 'p(99)<500'],
    errors: ['rate<0.01'],
    http_req_failed: ['rate<0.01'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';

export default function () {
  const authToken = login();

  group('API Operations', () => {
    group('List Products', () => {
      const res = http.get(`${BASE_URL}/api/products?page=1&limit=20`, {
        headers: { Authorization: `Bearer ${authToken}` },
        tags: { name: 'GET /api/products' },
      });

      check(res, {
        'status is 200': (r) => r.status === 200,
        'response time OK': (r) => r.timings.duration < 200,
        'has products': (r) => JSON.parse(r.body).data.length > 0,
      });

      errorRate.add(res.status !== 200);
      apiDuration.add(res.timings.duration);
      requestCount.add(1);
    });

    group('Create Order', () => {
      const payload = JSON.stringify({
        productId: Math.floor(Math.random() * 1000) + 1,
        quantity: Math.floor(Math.random() * 5) + 1,
        shippingAddress: '123 Test Street',
      });

      const res = http.post(`${BASE_URL}/api/orders`, payload, {
        headers: {
          Authorization: `Bearer ${authToken}`,
          'Content-Type': 'application/json',
        },
        tags: { name: 'POST /api/orders' },
      });

      check(res, {
        'order created': (r) => r.status === 201,
      });

      errorRate.add(res.status !== 201);
      apiDuration.add(res.timings.duration);
    });
  });

  sleep(Math.random() * 3 + 1);
}

function login() {
  const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
    email: `user${__VU}@test.com`,
    password: 'testpassword',
  }), {
    headers: { 'Content-Type': 'application/json' },
  });
  return res.status === 200 ? JSON.parse(res.body).token : '';
}

3.3 負荷テストの種類

種類目的VUパターン期間
Smoke基本動作確認1-51〜5分
Load予想トラフィック処理確認予想値15〜60分
Stress限界点探索予想値超過30〜60分
Spike急激なトラフィック対応突然の急増5〜10分
Soak長時間安定性確認一定レベル維持2〜24時間
Breakpointシステム破壊点探索持続的増加可変的

4. 主要パフォーマンスメトリクス

4.1 REDメソッド

# REDメソッドのモニタリング実装
from prometheus_client import Counter, Histogram

# Rate: 秒あたりリクエスト数
request_count = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

# Errors: エラー率
error_count = Counter(
    'http_errors_total',
    'Total HTTP errors',
    ['method', 'endpoint', 'error_type']
)

# Duration: 応答時間分布
request_duration = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration',
    ['method', 'endpoint'],
    buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)

4.2 レイテンシパーセンタイル

              平均(Mean)   p50    p95    p99    p99.9   Max
ユーザー影響度  低         中     高      高     非常に高い  極端

p50(中央値): 50%のリクエストがこの時間内に完了
p95: 95%のリクエストがこの時間内に完了(20回に1回がこれより遅い)
p99: 99%のリクエストがこの時間内に完了(100回に1回がこれより遅い)

なぜ平均は危険か?
- 平均50msでもp99が5000msの場合がある
- 100回に1回、ユーザーが5秒待機
- ヘビーユーザーほど高パーセンタイルに遭遇する確率が上昇

5. 一般的なボトルネック

5.1 データベースのボトルネック

N+1クエリ問題

# BAD: N+1クエリ - 注文100件で101回クエリ実行
orders = Order.objects.all()[:100]
for order in orders:
    print(f"Order {order.id} by {order.user.name}")

# GOOD: Eager loading - 2回のクエリで解決
orders = Order.objects.select_related('user').all()[:100]
for order in orders:
    print(f"Order {order.id} by {order.user.name}")

# GOOD: Prefetch (M:N関係)
orders = Order.objects.prefetch_related('items__product').all()[:100]
// Node.js + Prisma - N+1解決
// BAD: N+1
const orders = await prisma.order.findMany({ take: 100 });
for (const order of orders) {
  const user = await prisma.user.findUnique({
    where: { id: order.userId }
  });
}

// GOOD: Include (Join)
const orders = await prisma.order.findMany({
  take: 100,
  include: {
    user: true,
    items: { include: { product: true } }
  }
});

// GOOD: DataLoaderパターン
const DataLoader = require('dataloader');

const userLoader = new DataLoader(async (userIds) => {
  const users = await prisma.user.findMany({
    where: { id: { in: [...userIds] } }
  });
  const userMap = new Map(users.map(u => [u.id, u]));
  return userIds.map(id => userMap.get(id));
});

5.2 インデックス不足とフルテーブルスキャン

-- 遅いクエリの検出(PostgreSQL)
SELECT query, calls, mean_exec_time, total_exec_time, rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- 実行計画の分析
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.created_at > NOW() - INTERVAL '7 days'
ORDER BY o.created_at DESC
LIMIT 50;

-- 複合インデックス作成(クエリパターンに合わせて)
CREATE INDEX CONCURRENTLY idx_orders_status_created
ON orders (status, created_at DESC)
WHERE status IN ('pending', 'processing');

5.3 コネクションプール枯渇(こかつ)

# PgBouncer設定(PostgreSQLコネクションプーラー)
"""
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
pool_mode = transaction
default_pool_size = 25
min_pool_size = 5
reserve_pool_size = 5
max_client_conn = 1000
max_db_connections = 50
server_idle_timeout = 600
server_lifetime = 3600
"""

5.4 ロック競合(きょうごう)

// Go - シャーディングでロック競合を分散
package main

import "sync"

// BAD: グローバルミューテックスでマップ全体をロック
type BadCache struct {
    mu    sync.Mutex
    items map[string]interface{}
}

// GOOD: シャーディングでロック競合を分散
type ShardedCache struct {
    shards    [256]shard
    shardMask uint8
}

type shard struct {
    mu    sync.RWMutex
    items map[string]interface{}
}

func NewShardedCache() *ShardedCache {
    c := &ShardedCache{shardMask: 255}
    for i := range c.shards {
        c.shards[i].items = make(map[string]interface{})
    }
    return c
}

func (c *ShardedCache) getShard(key string) *shard {
    hash := fnv32(key)
    return &c.shards[hash&uint32(c.shardMask)]
}

func (c *ShardedCache) Get(key string) (interface{}, bool) {
    s := c.getShard(key)
    s.mu.RLock()
    defer s.mu.RUnlock()
    val, ok := s.items[key]
    return val, ok
}

func (c *ShardedCache) Set(key string, value interface{}) {
    s := c.getShard(key)
    s.mu.Lock()
    defer s.mu.Unlock()
    s.items[key] = value
}

func fnv32(key string) uint32 {
    hash := uint32(2166136261)
    for i := 0; i < len(key); i++ {
        hash *= 16777619
        hash ^= uint32(key[i])
    }
    return hash
}

6. データベース最適化

6.1 クエリ最適化戦略

-- 1. サブクエリをJOINに変換
-- BAD
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');

-- GOOD
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';

-- 2. カーソルベースのページネーション
-- BAD: OFFSETベース(深いページで遅い)
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 10000;

-- GOOD: カーソルベース(一定のパフォーマンス)
SELECT * FROM products
WHERE id > 10000
ORDER BY id
LIMIT 20;

-- 3. パーティショニング
CREATE TABLE orders (
  id BIGSERIAL,
  user_id BIGINT NOT NULL,
  status VARCHAR(20) NOT NULL,
  created_at TIMESTAMP NOT NULL,
  total_amount DECIMAL(10,2)
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2025_q1 PARTITION OF orders
  FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');

6.2 リードレプリカ

# SQLAlchemy - リード/ライト分離
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

class DatabaseRouter:
    def __init__(self):
        self.writer = create_engine(
            'postgresql://writer:pass@primary:5432/mydb',
            pool_size=10, max_overflow=20
        )
        self.readers = [
            create_engine(
                f'postgresql://reader:pass@replica{i}:5432/mydb',
                pool_size=10, max_overflow=20
            )
            for i in range(1, 4)
        ]
        self._reader_index = 0

    def get_writer_session(self):
        Session = sessionmaker(bind=self.writer)
        return Session()

    def get_reader_session(self):
        reader = self.readers[self._reader_index % len(self.readers)]
        self._reader_index += 1
        Session = sessionmaker(bind=reader)
        return Session()

7. キャッシュ戦略

7.1 Cache-Asideパターン

import redis
import json
from functools import wraps

redis_client = redis.Redis(host='localhost', port=6379, db=0)

class CacheAside:
    """Cache-Aside(Lazy Loading)パターン実装"""

    @staticmethod
    def cached(key_prefix, ttl_seconds=300):
        def decorator(func):
            @wraps(func)
            async def wrapper(*args, **kwargs):
                cache_key = f"{key_prefix}:{':'.join(str(a) for a in args)}"

                # 1. キャッシュを確認
                cached = redis_client.get(cache_key)
                if cached:
                    return json.loads(cached)

                # 2. キャッシュミス - DBから取得
                result = await func(*args, **kwargs)

                # 3. 結果をキャッシュに保存
                if result is not None:
                    redis_client.setex(
                        cache_key, ttl_seconds,
                        json.dumps(result, default=str)
                    )
                return result
            return wrapper
        return decorator

    @staticmethod
    def invalidate(key_pattern):
        """パターンベースのキャッシュ無効化(むこうか)"""
        keys = redis_client.keys(key_pattern)
        if keys:
            redis_client.delete(*keys)

7.2 Write-ThroughとWrite-Behind

class WriteThrough:
    """Write-Through: キャッシュとDBを同時に更新"""

    async def update(self, key, value, ttl=300):
        await self.db.update(key, value)
        redis_client.setex(f"wt:{key}", ttl, json.dumps(value, default=str))


class WriteBehind:
    """Write-Behind: キャッシュに先に書き、非同期でDBに反映"""

    def __init__(self):
        self.write_queue = asyncio.Queue()
        self.batch_size = 100
        self.flush_interval = 5

    async def update(self, key, value, ttl=300):
        redis_client.setex(f"wb:{key}", ttl, json.dumps(value, default=str))
        await self.write_queue.put((key, value))

    async def flush_worker(self):
        """バックグラウンドワーカー:キューからDBにバッチ書き込み"""
        while True:
            batch = []
            try:
                while len(batch) < self.batch_size:
                    item = await asyncio.wait_for(
                        self.write_queue.get(),
                        timeout=self.flush_interval
                    )
                    batch.append(item)
            except asyncio.TimeoutError:
                pass

            if batch:
                try:
                    await self.db.bulk_update(batch)
                except Exception:
                    for item in batch:
                        await self.write_queue.put(item)
                    await asyncio.sleep(1)

7.3 TTL戦略

# 階層型TTL戦略
class TieredTTLCache:
    TTL_CONFIG = {
        # 頻繁に変更されるデータ
        'user:session': 1800,          # 30分
        'cart:items': 900,             # 15分

        # 定期的に変更されるデータ
        'product:detail': 3600,        # 1時間
        'product:list': 600,           # 10分
        'search:results': 300,         # 5分

        # ほとんど変更されないデータ
        'category:list': 86400,        # 24時間
        'config:settings': 86400,      # 24時間
        'static:content': 604800,      # 7日
    }

8. 非同期処理

8.1 メッセージキューベースの非同期処理

# Celeryを使用した非同期タスク処理
from celery import Celery, chain, group

app = Celery('tasks', broker='redis://localhost:6379/0')

app.conf.update(
    task_serializer='json',
    task_acks_late=True,
    worker_prefetch_multiplier=1,
    task_routes={
        'tasks.send_email': {'queue': 'email'},
        'tasks.process_image': {'queue': 'image'},
        'tasks.generate_report': {'queue': 'report'},
    }
)

@app.task(bind=True, max_retries=3, default_retry_delay=60)
def send_email(self, to, subject, body):
    try:
        email_service.send(to=to, subject=subject, body=body)
    except Exception as exc:
        self.retry(exc=exc)

@app.task(bind=True, max_retries=3)
def process_order(self, order_id):
    """注文処理パイプライン"""
    try:
        workflow = chain(
            validate_inventory.s(order_id),
            process_payment.s(order_id),
            send_confirmation_email.s(order_id),
            update_analytics.s(order_id)
        )
        workflow.apply_async()
    except Exception as exc:
        self.retry(exc=exc, countdown=30)

@app.task
def bulk_process_orders(order_ids):
    """並列バッチ処理"""
    job = group(process_order.s(oid) for oid in order_ids)
    return job.apply_async()

8.2 イベント駆動アーキテクチャ

// Node.js - EventEmitterベースの非同期処理
const EventEmitter = require('events');

class OrderEventBus extends EventEmitter {
  constructor() {
    super();
    this.setMaxListeners(20);
  }
}

const orderBus = new OrderEventBus();

// イベントハンドラー登録(関心事の分離)
orderBus.on('order.created', async (order) => {
  await inventoryService.decrementStock(order.items);
});

orderBus.on('order.created', async (order) => {
  await emailService.sendOrderConfirmation(order);
});

orderBus.on('order.created', async (order) => {
  await analyticsService.trackOrder(order);
});

class OrderService {
  async createOrder(orderData) {
    const order = await this.orderRepo.create(orderData);
    orderBus.emit('order.created', order);
    return order; // 高速レスポンス
  }
}

9. バッチ最適化

9.1 バルクインサート

# SQLAlchemy バルクインサート比較
import time

# BAD: 1件ずつ挿入(N回のINSERT)
def insert_one_by_one(session, records):
    start = time.time()
    for record in records:
        session.add(MyModel(**record))
    session.commit()
    print(f"One by one: {time.time() - start:.2f}s")

# GOOD: バルクインサート(1回のINSERT)
def bulk_insert(session, records):
    start = time.time()
    session.bulk_insert_mappings(MyModel, records)
    session.commit()
    print(f"Bulk insert: {time.time() - start:.2f}s")

# パフォーマンス比較(10,000件)
# One by one: 12.5s
# Bulk insert: 0.8s

9.2 バッチAPI呼び出し

// バッチ外部API呼び出しの最適化
class BatchAPIClient {
  constructor(options = {}) {
    this.batchSize = options.batchSize || 50;
    this.concurrency = options.concurrency || 5;
    this.retryAttempts = options.retryAttempts || 3;
    this.delayBetweenBatches = options.delayMs || 100;
  }

  async processBatch(items, processFn) {
    const results = [];
    const errors = [];

    const batches = [];
    for (let i = 0; i < items.length; i += this.batchSize) {
      batches.push(items.slice(i, i + this.batchSize));
    }

    for (let i = 0; i < batches.length; i += this.concurrency) {
      const concurrentBatches = batches.slice(i, i + this.concurrency);

      const batchResults = await Promise.allSettled(
        concurrentBatches.map(batch => this.processWithRetry(batch, processFn))
      );

      for (const result of batchResults) {
        if (result.status === 'fulfilled') {
          results.push(...result.value);
        } else {
          errors.push(result.reason);
        }
      }

      if (i + this.concurrency < batches.length) {
        await new Promise(r => setTimeout(r, this.delayBetweenBatches));
      }
    }

    return { results, errors, total: items.length, processed: results.length };
  }

  async processWithRetry(batch, processFn, attempt = 1) {
    try {
      return await processFn(batch);
    } catch (error) {
      if (attempt < this.retryAttempts) {
        const delay = Math.pow(2, attempt) * 1000;
        await new Promise(r => setTimeout(r, delay));
        return this.processWithRetry(batch, processFn, attempt + 1);
      }
      throw error;
    }
  }
}

10. HTTP最適化

10.1 圧縮とプロトコル最適化

// Express.js 圧縮設定
const compression = require('compression');

app.use(compression({
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false;
    return compression.filter(req, res);
  },
  level: 6,
  threshold: 1024,
  memLevel: 8,
}));

10.2 Keep-Aliveと接続再利用

# Python requests - セッション再利用
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# BAD: 毎回新しい接続
def fetch_bad(urls):
    results = []
    for url in urls:
        response = requests.get(url)  # 毎回TCPハンドシェイク
        results.append(response.json())
    return results

# GOOD: セッション再利用(Keep-Alive)
def fetch_good(urls):
    session = requests.Session()
    retry_strategy = Retry(
        total=3, backoff_factor=0.5,
        status_forcelist=[500, 502, 503, 504]
    )
    adapter = HTTPAdapter(
        max_retries=retry_strategy,
        pool_connections=10,
        pool_maxsize=20,
    )
    session.mount("http://", adapter)
    session.mount("https://", adapter)

    results = []
    for url in urls:
        response = session.get(url)  # 接続再利用
        results.append(response.json())
    session.close()
    return results

11. プロダクションモニタリング

11.1 SLOベースのアラート設定

# Prometheusアラートルール
groups:
  - name: slo-alerts
    rules:
      # p99レイテンシSLO違反
      - alert: HighP99Latency
        expr: |
          histogram_quantile(0.99,
            rate(http_request_duration_seconds_bucket[5m])
          ) > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "p99 latency exceeds 500ms"

      # エラー率SLO違反
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m]))
          /
          sum(rate(http_requests_total[5m])) > 0.01
        for: 3m
        labels:
          severity: critical

      # コネクションプール枯渇間近
      - alert: ConnectionPoolExhaustion
        expr: |
          hikaricp_connections_active
          / hikaricp_connections_max > 0.85
        for: 2m
        labels:
          severity: warning

12. 実践クイズ

Q1. アムダールの法則がパフォーマンス最適化に与える示唆と、実務への適用方法を説明してください。

アムダールの法則は、システム全体のパフォーマンス向上が改善可能な部分の比率によって制限されることを示しています。

主な示唆

  • 全体実行時間の小さな部分(例:5%)をいくら速くしても、全体のパフォーマンス向上はわずかです
  • 全体実行時間の大きな部分(例:80%)を2倍速くするだけで、かなりのパフォーマンス向上が得られます
  • そのため、プロファイリングで最大のボトルネックを先に特定し、その部分を集中的に改善すべきです

実務への適用

  1. プロファイリング(Flame Graph)で実行時間分布を把握
  2. 最も大きな比率を占めるボトルネックから順に最適化
  3. 各最適化後に再測定して新しいボトルネックを確認
  4. パフォーマンスバジェット内に収まったら最適化を終了
Q2. N+1クエリ問題とは何か、ORMでの3つの解決方法を説明してください。

N+1問題:親エンティティN件を取得した後、各親の子エンティティを個別クエリで取得し、合計N+1回のクエリが実行される問題です。100件の注文を取得すると101回のDBクエリが発生します。

解決方法

  1. Eager Loading(select_related/include):JOINを使用して親と子を1回のクエリで取得。1:1、N:1関係に効果的
  2. Prefetch(prefetch_related):別クエリで子エンティティを一括取得し、メモリでマッピング。1:N、M:N関係に効果的。IN句を使用
  3. DataLoaderパターン:複数の個別リクエストを自動的にバッチ処理して1つのクエリで実行。GraphQLで特に有用。Facebookが開発したパターン
Q3. Cache-AsideとWrite-Throughキャッシュパターンの違いと、それぞれの適切なユースケースを説明してください。

Cache-Aside(Lazy Loading)

  • 読み取り時にキャッシュを確認、ミス時にDBから取得してキャッシュに保存
  • アプリケーションがキャッシュを直接管理
  • 最初のリクエストは必ずキャッシュミス(Cold Start)
  • 適切:読み取りが多く、すべてのデータをキャッシュする必要がない場合

Write-Through

  • 書き込み時にキャッシュとDBを同時に更新
  • キャッシュが常に最新の状態
  • 書き込みレイテンシが増加(2か所に書き込み)
  • 適切:データの一貫性が重要で、読み取りが書き込みより圧倒的に多い場合

Write-Behindはキャッシュに先に書き込みDBには非同期で反映するため、書き込みパフォーマンスを最大化しますが、データ損失リスクがあります。

Q4. 負荷テストの6種類(Smoke、Load、Stress、Spike、Soak、Breakpoint)をそれぞれいつ使用するか説明してください。
  1. Smoke Test:最小負荷(1-5 VU)でシステムの基本動作を確認。デプロイ後の基本検証
  2. Load Test:予想トラフィックレベルでパフォーマンスを確認。SLO達成の検証
  3. Stress Test:予想トラフィックを超えてシステム限界点を探索。キャパシティプランニングに活用
  4. Spike Test:突然のトラフィック急増(例:イベント)に対するシステムの反応を確認。オートスケーリングの検証
  5. Soak Test:長時間(数時間〜1日)一定負荷を維持して、メモリリークやコネクション枯渇などの段階的な問題を発見
  6. Breakpoint Test:負荷を持続的に増加させてシステムが完全に障害を起こす地点を探索。絶対的な限界の把握
Q5. コネクションプールチューニングで考慮すべき主要パラメータと、適切なプールサイズの決定方法を説明してください。

主要パラメータ

  • maximum-pool-size:最大コネクション数(多すぎるとDB負荷、少なすぎると待機発生)
  • minimum-idle:アイドルコネクション最小数(Cold Start防止)
  • connection-timeout:コネクション取得の待機時間
  • idle-timeout:アイドルコネクションの返却時間
  • max-lifetime:コネクションの最大寿命(DBファイアウォールのタイムアウトより短く)

適切なプールサイズの決定

  • HikariCPの公式:connections = ((core_count * 2) + effective_spindle_count)
  • SSDの場合:connections = core_count * 2 + 1 程度
  • 一般的に10〜20で十分な場合が多い
  • 大きすぎるプールはDBのコンテキストスイッチングコストを増加させる
  • モニタリングに基づいて調整:使用率が80%を超えたら増加を検討、待機スレッドが発生したら即座に増加

参考資料

  1. Google SRE Book - Performance Engineering
  2. k6 Documentation
  3. Artillery Documentation
  4. Brendan Gregg - Systems Performance
  5. Flame Graphs
  6. HikariCP - About Pool Sizing
  7. Redis Best Practices
  8. PostgreSQL Performance Tips
  9. Node.js Diagnostics Guide
  10. Go pprof Documentation
  11. Python cProfile Documentation
  12. Prometheus Monitoring
  13. DataLoader Pattern

현재 단락 (1/861)

パフォーマンスエンジニアリングの黄金律(おうごんりつ)は「推測するな、測定せよ」です。直感(ちょっかん)に基づく最適化は、ほとんどの場合、間違った場所に時間を浪費します。

작성 글자: 0원문 글자: 23,287작성 단락: 0/861