mybook

プロローグ — 嬉しい悲鳴

運命の月曜日

午前10時17分。

アキラのSlackに通知が爆発した。

「Buzzのサーバーが落ちてます!」「ページが開きません!」「エラー500が大量発生中!」「ユーザーから問い合わせが殺到しています!」「投資家から電話が来てます!」

SNSアプリ「Buzz」のCTOであるアキラは、コーヒーを手に持ったまま固まった。昨夜まで快調に動いていたシステムが、今朝から応答を返せなくなっていた。デスクにいたエンジニアのユイが青い顔でやってきた。「アキラさん、全部落ちてます」

原因はすぐにわかった——嬉しい悲鳴だった。

有名インフルエンサーの「りか」が「Buzzが面白い!フォロワーみんな登録して!」とポストしたのだ。りかのフォロワーは300万人。それが拡散し、通常の100倍のトラフィックが押し寄せた。登録ユーザーは昨日の1,000人から一夜にして50,000人に膨れ上がり、アプリは悲鳴を上げて倒れた。

INFO

スタートアップにとって「スケーリング問題」は成功の証拠でもある。しかし、それを解決できなければチャンスは二度と戻らない。「サーバーが落ちた」という報告は最悪のニュースでもあり、最高のニュースでもある。

現場の惨状

モニタリングダッシュボードを開くと、惨状が広がっていた。

[ERROR] PG::ConnectionBad: could not connect to server
[ERROR] ActiveRecord::StatementInvalid: PGError: ERROR: too many connections
[ERROR] Sidekiq workers: 0/25 running (all crashed)
[WARN]  Response time: 45,000ms (target: < 200ms)
[ALERT] CPU: 99% | Memory: 98% | DB connections: 100/100
[FATAL] Puma worker 0: Out of memory. Killed.
[FATAL] Puma worker 1: Out of memory. Killed.

Railsアプリは単一のt3.mediumインスタンスで動いていた。PostgreSQLも同じサーバー上。Sidekiqのワーカーが接続を食い尽くし、新規リクエストはすべてエラーになっていた。RailsとPostgreSQLとSidekiqが同居し、3者がリソースを奪い合っていた。

# 当時のPuma設定(問題あり)
# config/puma.rb
workers 4              # ワーカー4本
threads 10, 10         # スレッド最大10本
# 合計: 4 × 10 = 40スレッド = 40 DB接続
# DB接続上限: 100
# Sidekiq: 25接続
# 残り: 35接続(これが全リクエストで奪い合い)
Loading diagram...

アキラは深呼吸した。パニックになっても何も解決しない。まず計測だ。

Buzzの現状把握

アキラはチームを集めてホワイトボードに現状を書き出した。

システム構成(現在)

  • Webサーバー: Rails 7 on t3.medium(1台)— 2 vCPU / 4GB RAM
  • DB: PostgreSQL 15 on 同一サーバー
  • キャッシュ: なし
  • CDN: なし
  • バックグラウンドジョブ: Sidekiq(同一サーバー)
  • デプロイ: Capistrano(手動)
  • 監視: なし(!)

ユーザー数の推移

時期ユーザー数状態
3ヶ月前100人快適
1ヶ月前1,000人少し遅い
昨日1,000人普通
今朝50,000人完全停止

「このシステム、100人向けに作ったんだよな」とアキラは苦笑いした。「正確には、ユーザーが増えることを全く考えずに作った。今まで動いていたのが奇跡だ」

「でも動いていたのは本当の話です」とユイが言った。「1,000ユーザーまでは問題なかった。問題は50倍に一気に跳ね上がったことです」

スケーリングとは何か

チームのシニアエンジニア、ユイが口を開いた。

「スケーリングには大きく2種類あります。垂直スケーリング水平スケーリングです」

Loading diagram...

垂直スケーリング(スケールアップ)

  • サーバーのスペックを上げる(CPU・メモリ増強)
  • 簡単だが、上限がある(最大でも数十倍)
  • ダウンタイムが発生することも
  • コストが急激に上がる(線形より高い)

水平スケーリング(スケールアウト)

  • サーバーの台数を増やす
  • 理論上は無限にスケール可能
  • アーキテクチャの変更が必要
  • コストは台数に比例(線形)

「でも、どれを先にやるべきかわからない」とアキラは言った。「闇雲に高いサーバーに変えても意味がないかもしれない」

「そうです」とユイが答えた。「だからまず計測するんです。ボトルネックがCPUなのかメモリなのかDBなのかによって、対処法が全然違います」

スケーリングの旅のロードマップ

アキラは今後の方針を整理した。スタートアップが陥りがちな罠は「全部一度に解決しようとすること」だ。段階的に、問題が起きたものから対処していく。

Loading diagram...

「いきなり100万ユーザー対応を目指すんじゃない」アキラは言った。「まず1万人、次に10万人、そして100万人——段階的に育てていく。各ステップで計測して、本当に必要なものだけを追加する」

WARNING

早すぎる最適化は諸悪の根源(premature optimization is the root of all evil)。計測なしに最適化を始めると、ボトルネックではない部分に時間を浪費する。100ユーザーのアプリにシャーディングを入れる必要はない。

今日の目標:まず生き返らせる

まず、今すぐシステムを復旧させなければならない。アキラは短期対策を決めた。

即時対応(今日中)

  1. インスタンスタイプをt3.medium → t3.xlargeに変更(スケールアップ)
  2. DB接続プール数を適切に設定
  3. Pumaのワーカー数とスレッド数を最適化
# 緊急対応: インスタンスタイプを一時的に変更
# AWS コンソールから t3.medium → t3.xlarge に変更
# t3.xlarge: 4 vCPU / 16GB RAM(t3.mediumの4倍)
 
# DB接続の計算式:
# (Pumaワーカー数 × スレッド数) + Sidekiqスレッド数 + 余裕
# = (2 × 5) + 10 + 5 = 25接続(上限100に対して適切)
# config/database.yml
production:
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  connect_timeout: 10
  checkout_timeout: 10
  # 接続タイムアウトを設定(枯渇時に無限待ちを防ぐ)
# config/puma.rb(修正版)
# ワーカー数 = CPUコア数(t3.xlarge: 4コア)
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
 
# スレッド数 = DBコネクションプール数と一致させる
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
 
preload_app!  # メモリ節約のためにpreloadを有効化
 
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
 
on_worker_boot do
  # ワーカー起動時にDB接続を再確立
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
# config/sidekiq.yml(接続数を制限)
:concurrency: 10  # 25 → 10に削減(DB接続を節約)
:queues:
  - [critical, 6]
  - [default, 3]
  - [low, 1]

30分後、Buzzは復旧した。しかしアキラは知っていた。これは応急処置に過ぎない。次のトラフィックスパイクが来れば、また倒れる。

緊急対応の振り返り

「今日起きたことを整理しよう」アキラはチームに言った。

根本原因の分析:

問題1: リソース競合
  Rails + PostgreSQL + Sidekiq が同一サーバー上で動いていた
  → CPU・メモリ・DB接続を3者が奪い合っていた

問題2: 接続プール設定ミス
  Puma: 40スレッド × 2ワーカー = 80接続
  Sidekiq: 25接続
  合計: 105接続(上限100をオーバー!)

問題3: 水平スケール不可能な設計
  セッションをサーバーメモリに保存
  → サーバーが複数になったらセッションが共有できない

問題4: 監視なし
  ユーザーから報告されて初めて障害を知った
  → アラートがなければ障害を事前に検知できない

INFO

今日の「緊急対応」はあくまで延命措置だ。根本的な解決には、計測→ボトルネック特定→段階的なアーキテクチャ改善が必要。次週から本格的なスケーリング作業を始める。

監視の第一歩:New Relicを入れる

障害対応しながら、アキラはひとつ決断した。「まず何より、見えるようにする」

# Gemfile
gem 'newrelic_rpm'
gem 'rack-mini-profiler', require: false  # 開発環境プロファイリング
# config/newrelic.yml
production:
  license_key: <%= ENV['NEW_RELIC_LICENSE_KEY'] %>
  app_name: "Buzz Production"
 
  transaction_tracer:
    enabled: true
    transaction_threshold: auto
    record_sql: obfuscated  # SQLをログ記録(マスキングあり)
    stack_trace_threshold: 0.500
 
  error_collector:
    enabled: true
 
  browser_monitoring:
    auto_instrument: true
# AWS CloudWatch でサーバーメトリクスを監視
# EC2 インスタンスに CloudWatch Agent をインストール
sudo yum install -y amazon-cloudwatch-agent
 
# 設定ファイル
cat > /opt/aws/amazon-cloudwatch-agent/bin/config.json << 'EOF'
{
  "metrics": {
    "metrics_collected": {
      "mem": { "measurement": ["mem_used_percent"] },
      "disk": { "measurement": ["disk_used_percent"] }
    }
  }
}
EOF

スケーリングの哲学

アキラは手帳にメモを書いた。この1日で学んだことを整理するために。

スケーリングは技術ではなく、設計の哲学だ。

ユーザーが増えることを前提に設計する。 しかし、ユーザーがいないうちから複雑にしない。 計測し、ボトルネックを見つけ、最小限の変更で最大の効果を出す。

ユイが付け加えた。「スケーリングで大事なのはもう一つある。チームが理解できるシステムにすること。誰も理解できないシステムは、障害が起きたときに誰も直せない」

「それは今日、痛感した」アキラは言った。「システムが落ちたとき、最初の15分は何が起きているのかさえわからなかった。モニタリングがなかったから」

今日学んだ教訓

  1. 計測なしの開発は危険
  2. リソースの分離(Rails / DB / Sidekiqを別サーバーへ)
  3. ステートレス設計(水平スケールを可能にする)
  4. 監視・アラートは最初から入れる

「さあ」とアキラはチームに向かって言った。「本当のスケーリングの旅を始めよう。最初にやることは——計測だ」

技術的負債の棚卸し

その夜、アキラはひとりでコードを眺めた。スケーリング以前に、改善すべき点がいくつも目に入ってきた。

# 発見した問題 ①: N+1クエリ(あちこちに)
# app/controllers/timeline_controller.rb
def index
  @users = User.limit(50)  # 1クエリ
  # ビューで user.posts を呼ぶたびに SELECT が走る = N+1
end
 
# 発見した問題 ②: インデックスがない
# posts テーブルに user_id のインデックスがない
# 200万行を全件スキャンしている
 
# 発見した問題 ③: セッションをメモリ保存
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store
# 複数台構成にしたらセッションが共有できない
 
# 発見した問題 ④: 画像処理を同期で実行
def create
  @post = Post.create!(post_params)
  ImageResizeService.call(@post)  # 1.2秒!ユーザーが待つ
  redirect_to @post
end

「これだけの問題を一度に直そうとしてはいけない」アキラは自分に言い聞かせた。「計測して、最もインパクトの大きいものから直す」

次章では、アキラがBuzzのパフォーマンスを計測し、真のボトルネックを見つける方法を探っていく。N+1クエリ、スロークエリ、メモリリーク——問題は必ず数字の中に隠れている。


付録: Buzzの技術スタック

フロントエンド:
  - Rails 7 + Hotwire/Turbo
  - Tailwind CSS
  - ActionCable(WebSocket)

バックエンド:
  - Ruby 3.3
  - Rails 7.1
  - Puma(Webサーバー)
  - Sidekiq(バックグラウンドジョブ)

データストア:
  - PostgreSQL 15(メインDB)
  ※ Redis なし、キャッシュなし

インフラ:
  - AWS EC2 t3.medium(1台)
  - Capistrano デプロイ

今後追加予定:
  - AWS ECS(コンテナ化)
  - Amazon Aurora(マネージドDB)
  - Amazon ElastiCache(Redis)
  - Amazon S3 + CloudFront(CDN)
  - GitHub Actions(CI/CD)

この構成からスタートして、100万ユーザーに耐えるシステムへと育てていく。それがアキラとユイの旅の始まりだ。