グローバルスケーリング — 世界展開する
グローバルへの挑戦
Buzzが日本で100万ユーザーを突破した6ヶ月後、アメリカのインフルエンサーが「This Japanese app is amazing!」とポストした。一夜にして北米ユーザーが10万人増えた。
しかしパフォーマンスは最悪だった。
北米ユーザーからのフィードバック:
"The app is incredibly slow. 8 seconds to load my feed?"
"Everything times out. Unusable."
"Great concept but the latency makes it unworkable."
"It's fine for a few seconds, then just... freezes."
計測結果(ニューヨーク → 東京サーバー):
物理遅延(往復): 270ms(光ファイバーの物理的限界)
DNS解決: 50ms
TCP接続: 90ms
TLS 1.3 ハンドシェイク: 180ms
Time to First Byte: 320ms
─────────────────────────────
最初のバイトまで合計: 910ms
ページ完全読み込み: 7.2秒
※日本ユーザーの場合: 200ms
「北米ユーザーはリージョンを変えるだけで解決する」とユイが言った。「でもデータの整合性はどうする?東京のユーザーと北米のユーザーが同じ投稿にいいねしたら、どっちが正確?」
これがグローバルスケーリングの核心問題だ。一貫性(Consistency)とレイテンシ(Latency)はトレードオフの関係にある。
INFO
CAP定理(Consistency・Availability・Partition Tolerance)のうち、分散システムではパーティション耐性は必須なので、一貫性と可用性のどちらを優先するかを選ぶ。グローバルSNSでは「少し古い情報でも速く見せる」(結果整合性)が正解であることが多い。
マルチリージョン戦略の選択
BuzzはまずActive-Passiveから始める。読み取りはローカルリージョン、**書き込みはホームリージョン(東京)**に集約する。
リージョン構成(フェーズ1):
Primary : ap-northeast-1(東京) — 書き込み + 読み取り
Secondary : us-east-1(バージニア) — 読み取りのみ
Secondary : eu-west-1(アイルランド)— 読み取りのみ
Secondary : ap-southeast-1(シンガポール) — 読み取りのみ
書き込みフロー:
北米ユーザーが投稿 → us-east-1の Rails → 東京のAurora Primary
※ 書き込みに270msの追加レイテンシ(投稿は頻度が低いので許容範囲)
読み取りフロー:
北米ユーザーがタイムライン → us-east-1のRails → us-east-1のAurora Read Replica
※ レイテンシ: 270ms → 15msに改善!
Aurora Global Database
Aurora Global Databaseは1つのPrimaryと最大5つのSecondaryリージョンを持てる。レプリケーションラグは通常1秒未満だ。
# terraform/aurora_global.tf
# グローバルクラスター(コントロールプレーン)
resource "aws_rds_global_cluster" "buzz" {
global_cluster_identifier = "buzz-global"
engine = "aurora-postgresql"
engine_version = "15.4"
database_name = "buzz_production"
storage_encrypted = true
}
# 東京リージョン(Primary)
resource "aws_rds_cluster" "tokyo_primary" {
provider = aws.ap_northeast_1
cluster_identifier = "buzz-tokyo-primary"
global_cluster_identifier = aws_rds_global_cluster.buzz.id
engine = "aurora-postgresql"
engine_version = "15.4"
master_username = var.db_username
master_password = var.db_password
db_subnet_group_name = aws_db_subnet_group.tokyo.id
vpc_security_group_ids = [aws_security_group.aurora_tokyo.id]
lifecycle {
ignore_changes = [replication_source_identifier]
}
}
resource "aws_rds_cluster_instance" "tokyo_writer" {
provider = aws.ap_northeast_1
identifier = "buzz-tokyo-writer"
cluster_identifier = aws_rds_cluster.tokyo_primary.id
instance_class = "db.r7g.xlarge"
engine = "aurora-postgresql"
}
# バージニアリージョン(Secondary)
resource "aws_rds_cluster" "us_east_secondary" {
provider = aws.us_east_1
cluster_identifier = "buzz-us-east-secondary"
global_cluster_identifier = aws_rds_global_cluster.buzz.id
engine = "aurora-postgresql"
engine_version = "15.4"
# Secondaryは master_username/password を指定しない
db_subnet_group_name = aws_db_subnet_group.us_east.id
vpc_security_group_ids = [aws_security_group.aurora_us_east.id]
lifecycle {
ignore_changes = [replication_source_identifier, master_password, master_username]
}
}
resource "aws_rds_cluster_instance" "us_east_reader" {
provider = aws.us_east_1
count = 2 # 2台のReaderで負荷分散
identifier = "buzz-us-east-reader-${count.index}"
cluster_identifier = aws_rds_cluster.us_east_secondary.id
instance_class = "db.r7g.large"
engine = "aurora-postgresql"
}# config/database.yml(マルチリージョン対応)
production:
# 書き込み: 常に東京のWriter(全リージョン共通)
primary:
adapter: postgresql
url: <%= ENV['DATABASE_WRITER_URL'] %>
pool: 10
# 読み取り: 現在のリージョンのReader
primary_replica:
adapter: postgresql
url: <%= ENV['DATABASE_READER_URL'] %>
pool: 10
replica: true# config/application.rb(リージョン自動検出)
module Buzz
class Application < Rails::Application
config.after_initialize do
current_region = ENV.fetch('AWS_REGION', 'ap-northeast-1')
# ECSのメタデータからリージョンを取得
if ENV['ECS_CONTAINER_METADATA_URI_V4']
metadata = JSON.parse(
Net::HTTP.get(URI("#{ENV['ECS_CONTAINER_METADATA_URI_V4']}/task"))
)
current_region = metadata.dig('TaskARN', /arn:aws:ecs:([^:]+):/).to_a[1] || current_region
end
reader_url = case current_region
when 'us-east-1'
ENV['DATABASE_READER_US_URL']
when 'eu-west-1'
ENV['DATABASE_READER_EU_URL']
when 'ap-southeast-1'
ENV['DATABASE_READER_SG_URL']
else
ENV['DATABASE_READER_URL'] # 東京(デフォルト)
end
ENV['DATABASE_READER_URL'] = reader_url if reader_url.present?
Rails.logger.info "Region: #{current_region}, Reader: #{reader_url&.split('@')&.last}"
end
end
endGlobal Accelerator
AWS Global Acceleratorは世界中のAWSエッジロケーションからAWSバックボーンネットワーク経由でリクエストをルーティングする。公共インターネットより遅延が少なく安定している。
Global Acceleratorの効果:
通常のインターネット経由:
NYから東京: 270ms(ルーティングが最適でない場合もある)
パケットロス: 変動あり
Global Accelerator経由:
最寄りのAWSエッジからAWSバックボーンで東京へ
NYから東京: 220ms(AWSの専用回線)
パケットロス: 極めて少ない
DDoS保護: 標準搭載
ただし:
Buzzのマルチリージョン構成では、
us-east-1のECSに直接接続する方がずっと速い(15ms)
→ Global Acceleratorは「単一リージョン + DDoS保護」に有効
# terraform/global_accelerator.tf
resource "aws_globalaccelerator_accelerator" "buzz" {
name = "buzz-global"
ip_address_type = "IPV4"
enabled = true
attributes {
flow_logs_enabled = true
flow_logs_s3_bucket = aws_s3_bucket.logs.bucket
flow_logs_s3_prefix = "global-accelerator/"
}
}
resource "aws_globalaccelerator_listener" "https" {
accelerator_arn = aws_globalaccelerator_accelerator.buzz.id
client_affinity = "NONE" # スティッキーセッションなし
protocol = "TCP"
port_range {
from_port = 443
to_port = 443
}
}
# 東京エンドポイント
resource "aws_globalaccelerator_endpoint_group" "tokyo" {
listener_arn = aws_globalaccelerator_listener.https.id
endpoint_group_region = "ap-northeast-1"
health_check_path = "/health"
health_check_port = 443
health_check_protocol = "HTTPS"
health_check_interval_seconds = 30
threshold_count = 3
traffic_dial_percentage = 100
endpoint_configuration {
endpoint_id = aws_alb.tokyo.arn
weight = 128
client_ip_preservation_enabled = true
}
}
# 北米エンドポイント
resource "aws_globalaccelerator_endpoint_group" "us_east" {
listener_arn = aws_globalaccelerator_listener.https.id
endpoint_group_region = "us-east-1"
health_check_path = "/health"
health_check_port = 443
health_check_protocol = "HTTPS"
health_check_interval_seconds = 30
threshold_count = 3
traffic_dial_percentage = 100
endpoint_configuration {
endpoint_id = aws_alb.us_east.arn
weight = 128
client_ip_preservation_enabled = true
}
}Route53 レイテンシーベースルーティング
Global AcceleratorとRoute53を組み合わせてリージョンごとにルーティングする。
# terraform/route53.tf
# ヘルスチェック(東京)
resource "aws_route53_health_check" "tokyo" {
fqdn = aws_alb.tokyo.dns_name
port = 443
type = "HTTPS"
resource_path = "/health"
failure_threshold = 3
request_interval = 30
}
# レイテンシーベースレコード(東京)
resource "aws_route53_record" "api_tokyo" {
zone_id = aws_route53_zone.buzz.zone_id
name = "api.buzz-app.com"
type = "A"
set_identifier = "tokyo"
latency_routing_policy {
region = "ap-northeast-1"
}
health_check_id = aws_route53_health_check.tokyo.id
alias {
name = aws_alb.tokyo.dns_name
zone_id = aws_alb.tokyo.zone_id
evaluate_target_health = true
}
}
# レイテンシーベースレコード(北米)
resource "aws_route53_record" "api_us_east" {
zone_id = aws_route53_zone.buzz.zone_id
name = "api.buzz-app.com"
type = "A"
set_identifier = "us-east-1"
latency_routing_policy {
region = "us-east-1"
}
health_check_id = aws_route53_health_check.us_east.id
alias {
name = aws_alb.us_east.dns_name
zone_id = aws_alb.us_east.zone_id
evaluate_target_health = true
}
}INFO
Route53のレイテンシーベースルーティングはユーザーを最もレイテンシーが低いリージョンに自動的にルーティングする。ヘルスチェックと組み合わせて、障害時は自動でフェイルオーバーする。応答時間が最短のリージョンに振られるため、地理的に近いリージョンが選ばれることが多い。
データ整合性の管理
グローバル展開での最大の課題はデータ整合性だ。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# 書き込みは常に東京のPrimaryへ(全リージョン共通)
def write_to_primary(&block)
ActiveRecord::Base.connected_to(role: :writing) do
yield
end
end
# 読み取りはローカルリージョンのReplicaへ(15ms程度)
def read_from_local_replica(&block)
ActiveRecord::Base.connected_to(role: :reading) do
yield
end
end
end# レプリケーションラグを考慮した読み取り
class PostsController < ApplicationController
def show
post_id = params[:id]
# 直近1分以内に自分で書き込んだデータはPrimaryから読む
if session[:recent_writes]&.any? { |w| w[:id] == post_id && w[:at] > 1.minute.ago }
@post = write_to_primary { Post.find(post_id) }
else
@post = read_from_local_replica { Post.find(post_id) }
end
end
def create
@post = write_to_primary { Post.create!(post_params) }
# 自分の書き込みを記録(1分間)
session[:recent_writes] ||= []
session[:recent_writes] << { id: @post.id.to_s, at: Time.current }
session[:recent_writes].reject! { |w| w[:at] < 1.minute.ago }
redirect_to @post
end
def update
@post = write_to_primary { Post.find(params[:id]) }
@post.update!(post_params)
session[:recent_writes] ||= []
session[:recent_writes] << { id: @post.id.to_s, at: Time.current }
redirect_to @post
end
end# リージョン情報をヘッダーに含める(デバッグ用)
class ApplicationController < ActionController::Base
after_action :add_region_header
private
def add_region_header
response.headers['X-Buzz-Region'] = ENV.fetch('AWS_REGION', 'unknown')
response.headers['X-Buzz-DB-Role'] = @db_role || 'unknown'
response.headers['X-Buzz-Cache-Hit'] = @cache_hit ? 'true' : 'false'
end
endフェイルオーバーの実装と検証
グローバル展開では障害が必ず起きる。事前に訓練する。
# サーキットブレーカーパターン
# Gemfile: gem 'stoplight'
class DatabaseCircuitBreaker
def self.with_read_fallback(&block)
Stoplight("aurora-read-#{ENV.fetch('AWS_REGION', 'unknown')}") { yield }
.with_fallback do |error|
Rails.logger.error "Aurora Read Replica unavailable: #{error.message}"
# フォールバック: キャッシュから読む
nil # 呼び出し元でnilを処理
end
.with_cool_off_time(30) # 30秒はオープン状態を維持
.with_threshold(5) # 5回失敗でオープン
.run
end
end#!/bin/bash
# scripts/failover_test.sh - フェイルオーバーテスト
echo "=== Buzz グローバルフェイルオーバーテスト ==="
echo "開始時刻: $(date)"
# 東京リージョンのALBのヘルスチェックを意図的に失敗させる
echo "東京ALBの全ターゲットを unhealthy に設定中..."
# ECSサービスのタスク数を0にして全タスクを停止
aws ecs update-service \
--cluster buzz-cluster-tokyo \
--service buzz-web \
--desired-count 0 \
--region ap-northeast-1
echo "東京タスクを停止しました。Route53のフェイルオーバーを待機..."
# Route53のヘルスチェック間隔: 30秒
# フェイルオーバーまでの時間: 約90秒
sleep 90
echo "フェイルオーバー後のレスポンス確認(東京ユーザーが北米に振られることを確認):"
for i in {1..10}; do
result=$(curl -s -o /dev/null -w "%{http_code} %{time_total}s %header{x-buzz-region}" \
https://api.buzz-app.com/health)
echo " リクエスト $i: $result"
sleep 2
done
echo ""
echo "東京サービスを復旧中..."
aws ecs update-service \
--cluster buzz-cluster-tokyo \
--service buzz-web \
--desired-count 2 \
--region ap-northeast-1
aws ecs wait services-stable \
--cluster buzz-cluster-tokyo \
--services buzz-web \
--region ap-northeast-1
echo "東京サービス復旧完了"
echo "=== テスト完了 ==="# カオスエンジニアリング(ステージング環境で定期実行)
# lib/chaos/chaos_monkey.rb
module Chaos
class ChaosMonkey
ENABLED = ENV['CHAOS_ENABLED'] == 'true' && Rails.env.staging?
def self.inject_failure!(probability: 0.005) # 0.5%の確率
return unless ENABLED
return unless rand < probability
case rand(4)
when 0
# DBタイムアウトをシミュレート
raise ActiveRecord::StatementInvalid, "[Chaos] DB timeout"
when 1
# ランダムな遅延
delay = rand(1..5)
Rails.logger.warn "[Chaos] Injecting #{delay}s delay"
sleep(delay)
when 2
# Redisが落ちたシミュレート
raise Redis::CannotConnectError, "[Chaos] Redis unavailable"
when 3
# 大量メモリアロケーション(OOMに近い状況)
_ = Array.new(100_000) { '0' * 1024 } # 約100MB確保
end
end
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :chaos_test
private
def chaos_test
Chaos::ChaosMonkey.inject_failure!
rescue Chaos::ChaosMonkey::ChaosError => e
render json: { error: 'chaos', message: e.message }, status: :service_unavailable
end
endグローバル展開の効果
Global Accelerator + マルチリージョン導入後:
各地域のページ読み込み時間:
日本ユーザー:
導入前: 200ms(東京)
導入後: 180ms(東京)
改善: 10%
北米ユーザー:
導入前: 7,200ms(東京から)
導入後: 180ms(バージニアから)
改善: 40倍!
東南アジアユーザー:
導入前: 800ms(東京から)
導入後: 120ms(シンガポールから)
改善: 6.7倍
可用性の改善:
単一リージョン時: 月間ダウンタイム 約43分(99.9%)
マルチリージョン後: 月間ダウンタイム 約2.1分(99.995%)
※ 一方のリージョンが落ちても他方で継続
グローバルユーザー増加(3ヶ月後):
日本: 350万ユーザー
北米: 250万ユーザー
東南アジア: 200万ユーザー
欧州: 100万ユーザー
合計: 900万ユーザー ← 大台が見えてきた
マルチリージョンのコスト最適化
マルチリージョン構成の月次コスト:
東京(Primary):
Aurora Primary: $340
ECS Fargate (5タスク平均): $435
ALB: $45
ElastiCache: $270
小計: $1,090
北米(Secondary):
Aurora Secondary: $240 ← Primaryより安い(ストレージ共有)
ECS Fargate (3タスク平均): $261
ALB: $45
ElastiCache: $270
小計: $816
シンガポール・アイルランド(各Secondary):
各 $700 程度
Global Accelerator: $75/月 + $0.015/GB転送料
Route53: $1/ゾーン + $0.40/100万クエリ
合計: 約 $3,500/月
コスト削減のヒント:
□ Aurora の reserved instance(1年予約で30%割引)
□ ECS の Fargate Spot(最大70%割引、中断許容が必要)
□ 各リージョンのタスク数を需要に応じてAuto Scaling
□ CloudFrontでオリジンへのアクセスを削減
「100万ユーザー突破おめでとう」とユイが言った。アキラは笑った——でも思った。「これからどんな判断が待っているだろう?技術的な問題ではなく、ビジネス判断としてのスケーリングの問題が」
最終章では、スケーリングの判断基準——早すぎる最適化の罠、コストとのバランス、技術的負債との向き合い方を学ぶ。
付録: マルチリージョン構成の考慮事項
データ主権(Data Sovereignty):
□ EUユーザーのデータをEU外に保存してはいけない(GDPR)
□ 日本のユーザーデータは国内に保存(個人情報保護法)
□ リージョンごとにデータ分離が必要な場合がある
レプリケーション:
□ Aurora Global Databaseのレプリカラグを監視(目標: 1秒以内)
□ ElastiCacheは各リージョンで独立(レプリケーションなし)
□ DynamoDBは Global Tables で自動レプリケーション
障害対応:
□ フェイルオーバーの手順を文書化する
□ 月1回はフェイルオーバーテストを実施する
□ Auroraのフェイルオーバー(秒単位)とRoute53の切り替え(分単位)は別
コスト:
□ リージョン間のデータ転送料金($0.02/GB)に注意
□ CloudFrontがオリジンアクセスを減らすことでコスト削減
□ 需要の少ないリージョンは小さいインスタンスから始める