mybook

エピローグ — スケーリングの判断基準

100万ユーザー達成の夜

Buzzのユーザー数が100万を超えた夜、アキラはチームと小さな祝杯を挙げた。

渋谷のバーカウンターで、ユイはビールのグラスをアキラのグラスに当てた。「乾杯、アキラさん。1年前の今頃、t3.mediumが一台落ちてサービス全停止だったの覚えてます?」

「忘れるわけがない」アキラは笑った。「あの夜の3時に電話してきたのはユイじゃないか」

1年前、アキラは単一のt3.mediumで動くRailsアプリを持っていた。それが今や、4つのAWSリージョン、30台以上のECSタスク、Aurora Global Database、Kinesis、OpenSearch——複雑な分散システムへと成長していた。

しかし、ビールを飲みながらアキラは思った。「本当に全部必要だったのか?」

Buzzの現在の構成(1年後):
  東京リージョン(Primary):
    ECS Fargate タスク: 18台(API 10 + Worker 8)
    Aurora: Writer 1台 + Reader 3台
    ElastiCache: 3ノードクラスター
    OpenSearch: 3ノードクラスター

  バージニアリージョン(Secondary):
    ECS Fargate タスク: 8台
    Aurora: Reader 2台
    ElastiCache: 2ノードクラスター

  シンガポール・アイルランドリージョン(各Secondary):
    各 ECS 5台 + Aurora Reader 2台

  その他サービス:
    Kinesis Data Streams: 10シャード
    DynamoDB: Sessions + ActivityFeed テーブル
    S3 + CloudFront: メディア配信
    SQS: 非同期処理キュー

  月間コスト: $12,400
  エンジニア: 12人(インフラ2名含む)

早すぎる最適化の罠

「最初から全部やっていたらどうなっていた?」アキラは問いかけた。

Loading diagram...

ユイが答えた。「開発が3倍遅くなっていたと思います。機能開発に使うべき時間をインフラに使っていた。100ユーザーのときにシャーディングを設計していたら、ユーザーが離れていたでしょうね」

「その通りだ」アキラは続けた。「シャーディングを実装したのは50万ユーザーを超えてからだった。でも、正直に言うと、それでも少し早かったかもしれない。キャッシュ最適化とAurora Read Replicaだけで80万ユーザーまで耐えられた可能性がある」

WARNING

Premature optimization is the root of all evil — Donald Knuth

スケーリング問題は「成功した証拠」だ。問題が起きてから解決すればいい。問題が起きる前から複雑にすると、問題が起きる前にプロダクトが死ぬ。機能開発の速度こそがスタートアップの生命線だ。

何を「計測」して判断するか

アキラが学んだ判断基準を整理する。「計測なき最適化はギャンブルだ」というのが1年間の最大の学びだった。

スケーリングを検討すべきシグナル

# app/services/scaling_signal_detector.rb
class ScalingSignalDetector
  THRESHOLDS = {
    response_time_p95:     1000,   # P95 が 1秒を超えたら
    response_time_p99:     3000,   # P99 が 3秒を超えたら
    error_rate:            0.01,   # エラー率が 1% を超えたら
    cpu_utilization:         70,   # CPU が 70% を超えたら
    memory_utilization:      80,   # メモリが 80% を超えたら
    db_connection_usage:     80,   # DB接続が 80% を超えたら
    cache_hit_rate:          80,   # キャッシュヒット率が 80% を下回ったら
    queue_depth:         10_000,   # キュー深度が 10,000 を超えたら
    slow_query_count:        100   # スロークエリが 100/分 を超えたら
  }
 
  def self.check_all
    signals = {
      response_time: check_response_time,
      error_rate:    check_error_rate,
      db:            check_db_connections,
      cache:         check_cache_hit_rate,
      queue:         check_queue_depth
    }
 
    alerts = signals.filter_map do |metric, data|
      data[:alert] ? "[ALERT] #{metric}: #{data[:message]}" : nil
    end
 
    result = { healthy: alerts.empty?, signals: signals, alerts: alerts }
 
    # Slackに通知
    if alerts.any?
      SlackNotifier.alert(
        channel: '#eng-alerts',
        message: "Scaling signals detected:\n#{alerts.join("\n")}"
      )
    end
 
    result
  end
 
  # CloudWatch から P95 を取得
  def self.check_response_time
    cloudwatch = Aws::CloudWatch::Client.new(region: ENV['AWS_REGION'])
 
    response = cloudwatch.get_metric_statistics(
      namespace:   'AWS/ApplicationELB',
      metric_name: 'TargetResponseTime',
      statistics:  ['p95'],
      period:      300,
      start_time:  5.minutes.ago,
      end_time:    Time.current,
      dimensions:  [{ name: 'LoadBalancer', value: ENV['ALB_ARN_SUFFIX'] }]
    )
 
    p95_ms = (response.datapoints.last&.extended_statistics&.[]('p95') || 0) * 1000
 
    {
      value:     p95_ms.round,
      threshold: THRESHOLDS[:response_time_p95],
      alert:     p95_ms > THRESHOLDS[:response_time_p95],
      message:   "P95 レスポンスタイム #{p95_ms.round}ms(閾値: #{THRESHOLDS[:response_time_p95]}ms)"
    }
  end
 
  # エラー率(5xx / 全リクエスト)
  def self.check_error_rate
    cloudwatch = Aws::CloudWatch::Client.new(region: ENV['AWS_REGION'])
 
    dim = [{ name: 'LoadBalancer', value: ENV['ALB_ARN_SUFFIX'] }]
    opts = { period: 300, start_time: 5.minutes.ago, end_time: Time.current, dimensions: dim }
 
    errors_5xx = cloudwatch.get_metric_statistics(
      namespace: 'AWS/ApplicationELB', metric_name: 'HTTPCode_Target_5XX_Count',
      statistics: ['Sum'], **opts
    ).datapoints.sum(&:sum)
 
    total = cloudwatch.get_metric_statistics(
      namespace: 'AWS/ApplicationELB', metric_name: 'RequestCount',
      statistics: ['Sum'], **opts
    ).datapoints.sum(&:sum)
 
    rate = total.positive? ? errors_5xx / total.to_f : 0.0
 
    {
      value:     (rate * 100).round(3),
      threshold: THRESHOLDS[:error_rate] * 100,
      alert:     rate > THRESHOLDS[:error_rate],
      message:   "エラー率 #{(rate * 100).round(2)}%(閾値: #{THRESHOLDS[:error_rate] * 100}%)"
    }
  end
 
  # RDS接続数使用率
  def self.check_db_connections
    cloudwatch = Aws::CloudWatch::Client.new(region: ENV['AWS_REGION'])
 
    current = cloudwatch.get_metric_statistics(
      namespace:   'AWS/RDS',
      metric_name: 'DatabaseConnections',
      statistics:  ['Average'],
      period:      300,
      start_time:  5.minutes.ago,
      end_time:    Time.current,
      dimensions:  [{ name: 'DBClusterIdentifier', value: ENV['AURORA_CLUSTER_ID'] }]
    ).datapoints.last&.average || 0
 
    max_connections = 1000  # db.r7g.large の max_connections
    usage_pct = (current / max_connections.to_f * 100).round
 
    {
      value:     usage_pct,
      threshold: THRESHOLDS[:db_connection_usage],
      alert:     usage_pct > THRESHOLDS[:db_connection_usage],
      message:   "DB接続使用率 #{usage_pct}%(#{current.round}/#{max_connections})"
    }
  end
 
  # Redis キャッシュヒット率
  def self.check_cache_hit_rate
    info = $redis.info('stats')
    hits   = info['keyspace_hits'].to_i
    misses = info['keyspace_misses'].to_i
    total  = hits + misses
 
    hit_rate = total.positive? ? (hits / total.to_f * 100).round : 100.0
 
    {
      value:     hit_rate,
      threshold: THRESHOLDS[:cache_hit_rate],
      alert:     hit_rate < THRESHOLDS[:cache_hit_rate],
      message:   "キャッシュヒット率 #{hit_rate}%(閾値: #{THRESHOLDS[:cache_hit_rate]}%以上)"
    }
  end
 
  # Sidekiq キュー深度
  def self.check_queue_depth
    stats = Sidekiq::Stats.new
    total_enqueued = stats.enqueued
 
    {
      value:     total_enqueued,
      threshold: THRESHOLDS[:queue_depth],
      alert:     total_enqueued > THRESHOLDS[:queue_depth],
      message:   "キュー深度 #{total_enqueued}(閾値: #{THRESHOLDS[:queue_depth]})"
    }
  end
end

このサービスを毎分 Sidekiq で実行する。

# app/jobs/scaling_monitor_job.rb
class ScalingMonitorJob < ApplicationJob
  queue_as :monitoring
 
  def perform
    result = ScalingSignalDetector.check_all
 
    # CloudWatch カスタムメトリクスとして送信
    cloudwatch = Aws::CloudWatch::Client.new(region: ENV['AWS_REGION'])
 
    metric_data = result[:signals].map do |name, data|
      {
        metric_name: "BuzzScalingSignal_#{name}",
        value:       data[:value].to_f,
        unit:        'None',
        timestamp:   Time.current,
        dimensions:  [{ name: 'Environment', value: Rails.env }]
      }
    end
 
    cloudwatch.put_metric_data(
      namespace:   'Buzz/Scaling',
      metric_data: metric_data
    )
  end
end
 
# config/sidekiq.yml
:schedule:
  scaling_monitor:
    cron: '* * * * *'   # 毎分
    class: ScalingMonitorJob

スケーリングのコスト計算

「技術的な判断だけでなく、コストも計算しないといけない」アキラはチームに言っていた。

# app/services/scaling_cost_calculator.rb
class ScalingCostCalculator
  # 各スケーリング手法の月額コスト試算
  AWS_PRICING = {
    # EC2 / Fargate
    fargate_vcpu_hour:     0.04048,  # vCPU/時間
    fargate_gb_hour:       0.004445, # GB/時間
 
    # RDS / Aurora
    aurora_r7g_large_hour:  0.260,
    aurora_r7g_xlarge_hour: 0.520,
 
    # ElastiCache
    elasticache_r7g_large_hour: 0.182,
 
    # 転送料金
    data_transfer_gb:       0.020,   # リージョン間
 
    # OpenSearch
    opensearch_r7g_medium_hour: 0.111
  }
 
  def self.compare_options(current_rps:, target_rps:)
    scale_factor = (target_rps.to_f / current_rps).ceil
 
    {
      option_a_optimize_code: {
        description: "コード最適化(N+1解消・クエリ改善)",
        monthly_cost: 0,  # エンジニアコストは別
        rps_gain:    "2〜5倍(現状による)",
        implementation_days: 3,
        risk: :low,
        note: "最初に試みること。お金がかからない"
      },
 
      option_b_add_cache: {
        description: "ElastiCacheノード追加",
        monthly_cost: (AWS_PRICING[:elasticache_r7g_large_hour] * 24 * 30).round,
        rps_gain:    "3〜10倍(キャッシュヒット率次第)",
        implementation_days: 1,
        risk: :low,
        note: "読み取り重視のワークロードに有効"
      },
 
      option_c_add_read_replica: {
        description: "Aurora Read Replica 追加",
        monthly_cost: (AWS_PRICING[:aurora_r7g_large_hour] * 24 * 30).round,
        rps_gain:    "2〜3倍(読み取りが多い場合)",
        implementation_days: 1,
        risk: :low,
        note: "読み取り: 書き込み = 9:1 以上なら特に有効"
      },
 
      option_d_scale_out_fargate: {
        description: "Fargateタスク数を#{scale_factor}倍に増やす",
        monthly_cost: (
          AWS_PRICING[:fargate_vcpu_hour] * 2 * scale_factor * 24 * 30 +
          AWS_PRICING[:fargate_gb_hour]   * 4 * scale_factor * 24 * 30
        ).round,
        rps_gain:    "#{scale_factor}倍(ほぼ線形)",
        implementation_days: 0,  # Auto Scalingが自動でやる
        risk: :low,
        note: "アプリがステートレスなら簡単。ボトルネックがDBの場合は効果なし"
      },
 
      option_e_vertical_aurora: {
        description: "Aurora を r7g.large → r7g.xlarge にスケールアップ",
        monthly_cost: (
          (AWS_PRICING[:aurora_r7g_xlarge_hour] - AWS_PRICING[:aurora_r7g_large_hour]) * 24 * 30
        ).round,
        rps_gain:    "1.5〜2倍(CPUバウンドの場合)",
        implementation_days: 0,  # 数分でダウンタイムなし変更可
        risk: :low,
        note: "Aurora は停止なしでインスタンスタイプ変更可能"
      },
 
      option_f_sharding: {
        description: "データベースの水平シャーディング",
        monthly_cost: (AWS_PRICING[:aurora_r7g_large_hour] * 24 * 30 * 3).round,  # 3クラスター追加
        rps_gain:    "4倍以上(理論上は無制限)",
        implementation_days: 90,  # 3ヶ月の大型プロジェクト
        risk: :high,
        note: "最後の手段。クロスシャードクエリが不可能になる。技術的負債が大きい"
      }
    }
  end
 
  def self.recommend(current_rps:, target_rps:, bottleneck:)
    options = compare_options(current_rps: current_rps, target_rps: target_rps)
 
    case bottleneck
    when :slow_queries
      puts "推奨: まずコード最適化(option_a)、次にRead Replica追加(option_c)"
      [options[:option_a_optimize_code], options[:option_c_add_read_replica]]
    when :high_cpu
      puts "推奨: Fargateスケールアウト(option_d)"
      [options[:option_d_scale_out_fargate]]
    when :cache_miss
      puts "推奨: ElastiCacheノード追加(option_b)"
      [options[:option_b_add_cache]]
    when :db_write_iops
      puts "推奨: Aurora スケールアップ(option_e)、長期的にはシャーディング(option_f)"
      [options[:option_e_vertical_aurora], options[:option_f_sharding]]
    else
      puts "ボトルネックを特定してください。計測から始めてください。"
      []
    end
  end
end

スケーリングの判断フレームワーク

アキラがチームに共有したフレームワーク。どのエンジニアも同じプロセスで判断できるようにした。

Loading diagram...

判断マトリクス

スケーリングの判断は感覚でなくデータで行う。以下のマトリクスに沿って判断すれば、チームの誰でも同じ結論に達せるはずだ。

シグナル閾値まず試みることエスカレーション
P95 > 1秒継続30分以上N+1クエリを Bullet で確認Auroraスロークエリログ分析
エラー率 > 1%5分以上ログで5xxの原因を特定インシデント宣言
CPU > 70%10分以上Fargateタスク数を+50%プロファイリングで根本原因
DB接続 > 80%5分以上PgBouncerのpool_size調整Read Replica追加
キャッシュHit率 < 80%1時間以上Redisメモリ使用量確認ElastiCacheノード追加
キュー深度 > 10,000継続増加中Workerタスク数を+50%Kinesisシャード追加
地理遅延 > 500ms特定リージョンRoute53レイテンシー確認そのリージョンにECS追加
# 判断ログを記録(なぜこの変更をしたか残す)
class ScalingDecisionLog
  def self.record(decision:, reason:, signal_data:, expected_impact:)
    DecisionLog.create!(
      decision:        decision,
      reason:          reason,
      signal_data:     signal_data.to_json,
      expected_impact: expected_impact,
      decided_by:      Current.user&.email || 'system',
      decided_at:      Time.current
    )
 
    # Slackにも通知
    SlackNotifier.info(
      channel: '#eng-scaling',
      message: <<~MSG
        Scaling Decision Recorded:
        Decision: #{decision}
        Reason: #{reason}
        Expected: #{expected_impact}
        By: #{Current.user&.email || 'system'}
      MSG
    )
  end
end
 
# 使い方:
# ScalingDecisionLog.record(
#   decision: "Aurora Reader を 2台 → 4台に増加",
#   reason: "DB接続使用率が85%を超えた(閾値: 80%)",
#   signal_data: { db_connection_pct: 85, current_readers: 2 },
#   expected_impact: "DB接続使用率を45%程度まで低下させる"
# )

INFO

判断ログを残す意味は2つある。1つは「なぜこの構成になったか」を未来のチームメンバーに伝えること。もう1つは、意思決定の品質を振り返るため。「先月追加したReaderは効果があったか?」を測定できる。

技術的負債との向き合い方

「スケーリングを急いでやり過ぎた部分もある」アキラは率直に言った。

Buzzが積み上げた技術的負債(正直なリスト):

① シャーディングでクロスDBクエリが不可能になった
  具体的な影響: "フォロー中ユーザーの最新投稿" を取得するために
  アプリ層で複数シャードから取得してマージする必要がある
  解決策: CQRS + 非正規化(Redis/DynamoDBで結果を事前計算)

② マルチリージョン展開で運用が複雑化した
  具体的な影響: 障害時にどのリージョンで何が起きているか把握に時間がかかる
  解決策: Datadog APM でリージョン横断の分散トレーシングを導入

③ 複数DBの運用コストが増大した
  具体的な影響: DBA 1名をフルタイムで割く必要が出てきた
  解決策: 各DBのメトリクスを統合ダッシュボードで可視化、自動チェックを増やす

④ 新メンバーがシステム全体を理解するのに3ヶ月かかる
  具体的な影響: オンボーディングが遅く、採用コストが高い
  解決策: アーキテクチャドキュメントの整備、ローカル環境でのミニ版構成

教訓(後から考えると):
  × 10万ユーザーで入れたシャーディング → 50万で良かった
  × 5万ユーザーでのマルチリージョン   → 30万で良かった
  ✓ 1万ユーザーでのRead Replica       → 正しいタイミング
  ✓ 5,000ユーザーでのSidekiq導入      → 正しいタイミング
  ✓ 1,000ユーザーでのN+1対策         → 正しいタイミング(むしろ最初から)

アンチパターンリスト

1年間で見てきた「やりすぎ」の例。

# アンチパターン1: 不要な非同期化
# ────────────────────────────────
# ユーザー登録メールは同期で送っても100ms程度。非同期にする必要なし。
# 非同期にすると: 失敗時のリトライ、冪等性、デッドレター管理が必要になる
 
class RegistrationsController < ApplicationController
  def create
    @user = User.create!(user_params)
 
    # NG: 「なんとなく非同期」
    # WelcomeMailJob.perform_later(@user.id)
 
    # OK: 同期で十分、エラーはそのままユーザーに返せる
    WelcomeMailer.welcome(@user).deliver_now
 
    redirect_to dashboard_path
  end
end
 
# アンチパターン2: 管理画面のキャッシュ
# ────────────────────────────────────
# 管理者が一日数回見る統計ページをキャッシュしても意味はない
# キャッシュを入れると: キャッシュ無効化のバグ、古いデータ表示などの問題が発生
 
class Admin::StatsController < ApplicationController
  def index
    # NG: 管理者は1人、1日数回しか見ない
    # @stats = Rails.cache.fetch('admin:daily_stats', expires_in: 1.hour) do
    #   calculate_heavy_stats
    # end
 
    # OK: 複雑でもDBから直接取得で十分
    @stats = calculate_heavy_stats
  end
end
 
# アンチパターン3: 早すぎるマイクロサービス化
# ─────────────────────────────────────────
# Shopify は $100B 企業でも「Modular Monolith」
# Netflix でさえ最初はモノリスだった
 
# NG: 100万ユーザー未満でマイクロサービスへ分割
#   → デプロイが複雑、ネットワーク遅延、サービス間認証、分散トレーシング必要
#   → エンジニア10人未満では管理コストが開発コストを上回る
 
# OK: 「Vertical Slice」でモノリス内を論理分割
# app/
#   domains/
#     posts/      # 投稿ドメイン
#       models/
#       services/
#       controllers/
#     users/      # ユーザードメイン
#     notifications/ # 通知ドメイン
#
# これで十分。1000万ユーザーまではモノリスで戦える。
 
# アンチパターン4: 「念のため」のインフラ
# ────────────────────────────────────
# 「いつか使うかも」でコストをかけない
 
# NG: 最初からKinesisを入れる
#   → 10万イベント/日 = SQSで十分、Kinesisは不要
#   → 5,000万イベント/日 を超えてから検討する
 
# OK: SQS → Kinesis の段階的な移行

WARNING

マイクロサービスは「組織の問題」を解決するためのアーキテクチャだ。技術的なスケーリング問題の解決策ではない。コンウェイの法則:システムの構造は組織のコミュニケーション構造を反映する。エンジニア10人未満でマイクロサービスに移行しても、組織がそれに見合っていなければ複雑さが増すだけだ。

スケーリングの真実

アキラは手帳を取り出した。この1年で学んだことを書き留めた。

スケーリングで学んだ10の真実:

1. 計測しない最適化は賭け
   ── 「遅い気がする」だけで変更するな。数値を出してから判断する
   ── CloudWatch、Datadog、rack-mini-profiler があれば計測できる

2. シンプルさは力
   ── シンプルなシステムは理解できる
   ── 理解できるシステムは夜中の障害でも直せる
   ── 複雑なシステムは賢い人しか触れない → バス係数が下がる

3. ボトルネックは必ず一か所
   ── 全部を同時に最適化しようとするな
   ── 一番遅いところを直すと、別の場所が一番遅くなる
   ── それがシステムの限界まで続く

4. キャッシュは薬、乱用すると毒
   ── キャッシュは問題を隠すが解決しない
   ── キャッシュで解決した問題は、キャッシュが壊れると再発する
   ── コードで解決できる問題にキャッシュを使うな

5. 水平スケールは銀の弾丸ではない
   ── ステートフルなコードは水平スケールできない
   ── セッションをローカルメモリに保存するコードは、タスクを増やしても速くならない
   ── まずステートレス化してからスケールアウトする

6. DBは最後のボトルネック
   ── アプリで解決できることをDBにやらせるな
   ── 集計処理はアプリ層で、DBはCRUDのみ
   ── DBをスケールするのは最もコストがかかる

7. 非同期化は副作用がある
   ── 整合性、リトライ、冪等性、デッドレター処理が必要になる
   ── 同期で十分な処理を非同期にしてはいけない
   ── 「速くなりそう」だけで非同期にするな

8. マルチリージョンはコストが高い
   ── インフラコストだけでなく、運用コスト・認知コストも増える
   ── 本当に必要になるまでやるな
   ── 北米進出前の準備として3週間前から始めれば十分

9. 技術的負債は返済計画とともに
   ── 速く動くために妥協した複雑さは、必ず後で返す
   ── 返済する Sprint を定期的に設ける
   ── 負債の記録を残す(なぜ妥協したか)

10. チームの理解がスケーラビリティ
    ── システムを理解できないチームは、障害時に復旧できない
    ── 本番システムを触れるエンジニアは最低3人必要
    ── ドキュメントがないシステムは一人の天才に依存する

Buzzの成長ロードマップ

1年間の成長を振り返り、次の1年の指針を考えた。

Loading diagram...

各ステージで追加すべきコンポーネントを詳しく見ていく。

1,000ユーザーで始める(必須のみ):
  ✓ Rails + PostgreSQL(同一サーバー、t3.medium)
  ✓ Bullet gem でN+1検出(開発環境必須)
  ✓ 基本的なインデックス(外部キー、検索用カラム)
  ✓ rack-mini-profiler で計測(開発時のみ)
  ✓ Sentry / Honeybadger でエラー追跡

10,000ユーザーで追加する:
  ✓ RDS を別サーバーに分離(t3.medium → db.t3.large)
  ✓ ElastiCache Redis(セッション管理 + ページキャッシュ)
  ✓ Sidekiq(メール・通知を非同期に。最初はEC2上で可)
  ✓ CloudFront + S3(静的アセット + 画像配信)
  ✓ CloudWatch アラーム(CPU・メモリ・エラー率)
  月間コスト: $300〜500

100,000ユーザーで追加する:
  ✓ ECS Fargate + ALB(アプリを複数タスクに。EC2管理から解放)
  ✓ Aurora(マルチAZ、自動フェイルオーバー)
  ✓ Aurora Read Replica × 1台
  ✓ Auto Scaling(ECS Service Auto Scaling)
  ✓ PgBouncer(コネクションプーリング)
  月間コスト: $800〜1,200

500,000ユーザーで追加する:
  ✓ Aurora Read Replica × 2〜3台
  ✓ OpenSearch(全文検索を pg_search から移行)
  ✓ Kinesis Data Streams(イベント収集)
  ✓ 垂直パーティショニング(users/posts/social/notificationsをDB分割)
  ✓ DynamoDB(アクティビティフィード、セッション)
  ✓ WAF(Bot対策、レート制限)
  月間コスト: $3,000〜5,000

1,000,000ユーザーで追加する:
  ✓ Aurora Global Database(マルチリージョンレプリケーション)
  ✓ マルチリージョン展開(ECS + ALB を各リージョンに)
  ✓ Route53 レイテンシーベースルーティング
  ✓ 水平シャーディング(書き込みがボトルネックになったら)
  月間コスト: $10,000〜15,000

10,000,000ユーザー(次のステージ):
  ? グローバルなActive-Active構成
  ? データプレーンとコントロールプレーンの分離
  ? サービスメッシュ(Istio / App Mesh)
  ? AI/MLパイプライン(レコメンデーション)
  月間コスト: $50,000+
各ステージのアーキテクチャサマリー:

ユーザー数    キーテクノロジー                         コスト/月
〜1,000      シングルサーバー                          $50
〜10,000     RDS + ElastiCache + Sidekiq              $400
〜100,000    ECS + Aurora + Auto Scaling              $1,000
〜500,000    ECS + Aurora Read × 3 + OpenSearch       $4,000
〜1,000,000  マルチリージョン + Kinesis + DynamoDB    $12,000
〜5,000,000  Global Active-Passive + Sharding        $40,000

INFO

このロードマップはガイドラインだ。実際には「計測してから判断する」が絶対原則。1,000ユーザーでもすでにパフォーマンス問題があれば早く対処し、100,000ユーザーでも問題がなければ変えない。コストとのバランスも常に考える。月$1,000のインフラ費が月$10,000の収益を守るなら正当化できる。月$1,000のインフラ費でサービスが無料なら再考が必要だ。

最後のリファクタリング — 哲学のコード化

「スケーリングの哲学をコードで表現できるか?」アキラは面白い問いを立てた。

# lib/scaling_philosophy.rb
# Buzzが1年で学んだスケーリングの哲学をコードに
 
module ScalingPhilosophy
  # 計測なき最適化はギャンブル
  def measure_first
    before = benchmark { yield }
    result = yield
    after  = benchmark { yield }
 
    Rails.logger.info "Performance: #{before.real.round(3)}s → #{after.real.round(3)}s"
    result
  end
 
  # シンプルから始める
  def simple_first
    # 複雑な解決策を実装する前に、シンプルな方法を試す
    # バグはシンプルなコードに少ない
    # シンプルなコードは修正が速い
    yield
  rescue => e
    # シンプルな方法が失敗したら、複雑な方法を検討する
    raise ScalingRequired, "Simple approach failed: #{e.message}"
  end
 
  # 問題が起きてから解決する
  def scale_when_needed(threshold:)
    result = yield
    signal = ScalingSignalDetector.check_all
 
    if signal[:alerts].any?
      Rails.logger.warn "Scaling signal detected: #{signal[:alerts].join(', ')}"
      # 自動スケーリングが起動する(ECS Auto Scaling)
    end
 
    result
  end
end
 
# この哲学の実践:
# 1. コードを書く前に計測する
# 2. シンプルな解決策を試す
# 3. 問題が起きてから最適化する
# 4. 最適化の効果を計測する
# 5. 繰り返す

アキラの結論

バーカウンターで、アキラはノートにこう書いた。

スケーリングの旅で学んだこと(一言で):

「ユーザーが困っているときにシステムを直す。
 ユーザーが困っていないときは機能を作る」

それだけだ。

「次は1000万ユーザーだ」アキラは言った。「でも今日はここまで。祝杯を挙げよう」

ユイが笑った。「次のボトルネックは、もうわかってるんじゃないですか?」

「もちろん」アキラは答えた。「DynamoDBのホットパーティション問題が出てくる。でも、それは問題が起きてから考える。今はユーザーに価値を届けることに集中する」

翌日から、アキラはユーザーからのフィードバックを読み、次の機能を考え始めた。スケーリングの旅は、ユーザーへの価値提供の旅でもある。

計測し、ボトルネックを見つけ、最小限の変更で最大の効果を出す。そして、シンプルさを愛する。

それが、100ユーザーのアプリを100万ユーザーのシステムに育てる方法だ——そしてそれは、機能ではなく価値を作り続けることと、表裏一体だ。


付録: Buzzのインフラ as Code テンプレート

本書で紹介したすべての構成は Terraform で管理している。重要なモジュール構成を示す。

terraform/
  modules/
    vpc/              # VPC・サブネット・セキュリティグループ
    ecs/              # ECS Cluster + Service + Task Definition
    aurora/           # Aurora Cluster + Instances
    elasticache/      # ElastiCache Cluster
    alb/              # Application Load Balancer
    cloudfront/       # CloudFront Distribution
    waf/              # WAF Rules
    kinesis/          # Kinesis Data Streams
    opensearch/       # OpenSearch Domain
    monitoring/       # CloudWatch Dashboards + Alarms
  environments/
    staging/          # ステージング環境
    production/       # 本番環境
      ap-northeast-1/ # 東京(Primary)
      us-east-1/      # バージニア(Secondary)
      eu-west-1/      # アイルランド(Secondary)
      ap-southeast-1/ # シンガポール(Secondary)
# 本番環境へのデプロイ(GitHub Actions経由)
# .github/workflows/terraform.yml からのみ実行される
 
# ステージング
cd terraform/environments/staging
terraform plan -out=tfplan
terraform apply tfplan
 
# 本番(東京)
cd terraform/environments/production/ap-northeast-1
terraform plan -out=tfplan
# → Pull Request でレビュー後にマージしてapply

スケーリングの旅はここで終わらない。Buzzのシステムは今日も進化し続けている。そして、アキラとユイは次のボトルネックを待ちながら、ユーザーへの価値提供に集中している。