エピローグ — スケーリングの判断基準
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名含む)
早すぎる最適化の罠
「最初から全部やっていたらどうなっていた?」アキラは問いかけた。
ユイが答えた。「開発が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スケーリングの判断フレームワーク
アキラがチームに共有したフレームワーク。どのエンジニアも同じプロセスで判断できるようにした。
判断マトリクス
スケーリングの判断は感覚でなくデータで行う。以下のマトリクスに沿って判断すれば、チームの誰でも同じ結論に達せるはずだ。
| シグナル | 閾値 | まず試みること | エスカレーション |
|---|---|---|---|
| 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年の指針を考えた。
各ステージで追加すべきコンポーネントを詳しく見ていく。
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のシステムは今日も進化し続けている。そして、アキラとユイは次のボトルネックを待ちながら、ユーザーへの価値提供に集中している。