APIは問題なく動く。ステージングの負荷も処理。そしてローンチ、バイラル、10,000ユーザーが同時に叩く。レスポンスタイムがスパイク。エラーがカスケード。データベースが悲鳴。
これはスケーリングが直感に反するから起きる。
推測するな。測定しろ。
APIはCPUバウンド?I/Oバウンド?メモリバウンド?解決策は劇的に異なる。
ほとんどのWeb APIはI/Oバウンド。待ちに時間使ってる—データベース、外部API、ファイルシステム。これは良いニュース。I/Oバウンドな問題には既知の解決策がある。
最速のデータベースクエリは、決して実行しないクエリ。
積極的にキャッシュ。短いTTLで始める。鮮度要件を理解したら延長。
垂直スケーリング(大きいサーバー)は限界がある。水平スケーリング(多いサーバー)が本当の成長。
要件:
APIはスケールする。データベースはできる?
コネクションプーリング — 接続を開くのは高価。プールを準備しておく。PostgresにはPgBouncer。これだけでスループット2倍になることも。
リードレプリカ — プライマリが書き込み、レプリカが読み取り。ほとんどのアプリは90%以上が読み取り。
欠けてるインデックス — しばしばボトルネック全体。
すべてをリクエスト中にやる必要はない。
すぐリターン。バックグラウンドで処理。ユーザーは待たない。
行儀の悪い1クライアントがAPI全体を落とすべきじゃない。
APIキーあたり毎分100リクエスト。超えたら429。Redisで簡単。
ステップ7に飛ぶな。ほとんどは必要ない。
スケーリングは1つの技術じゃない。ラダーだ。一歩ずつ登れ。
— blanho
部分障害、ネットワークの嘘、クロックドリフト。分散システムを悪夢にするすべて。
ACIDは単純に聞こえるが、READ COMMITTEDが実際に何を許容するか知るまでは。
また間違ったものを最適化した。パフォーマンスを推測しても絶対うまくいかない理由がこれ。
# 前: 毎回200ms
def get_user(user_id):
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
# 後: 初回200ms、以降1ms
def get_user(user_id):
cached = redis.get(f"user:{user_id}")
if cached:
return json.loads(cached)
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
redis.setex(f"user:{user_id}", 300, json.dumps(user))
return user# 前: リクエスト2秒かかる
def signup(user):
create_user(user)
send_welcome_email(user) # 500ms
create_audit_log(user) # 200ms
notify_slack(user) # 300ms
return {"status": "ok"}
# 後: リクエスト100msかかる
def signup(user):
create_user(user)
queue.push("welcome_email", user.id)
queue.push("audit_log", user.id)
queue.push("slack_notify", user.id)
return {"status": "ok"}-- このクエリ2秒かかってる?
SELECT * FROM orders WHERE user_id = 123;
-- これ追加:
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- 5msになった