data-scale · Stage 3
負荷曲線と実測profileから安全容量を判断する
simulated負荷と実際のlocal profileを混同せず、tail、queue、回復、headroomから安全な容量判断を組み立てる。
到達目標
offered、accepted、success、tail latency、resource、queueを同じ負荷曲線で比較できる
- ボトルネック証拠、負荷曲線、容量限界を含む性能報告
- plateau、tail、error、queue、回復、profileから次の測定を選ぶ回答
simulated capacity evidenceと実環境で採取したprofileを区別し、bottleneck仮説を反証できる
- ボトルネック証拠、負荷曲線、容量限界を含む性能報告
- CPU使用率、最大RPS、平均latencyだけでは安全容量を決められない理由の5分説明
Littleの式の適用条件、回復hysteresis、headroomを明示して安全容量を更新できる
- plateau、tail、error、queue、回復、profileから次の測定を選ぶ回答
- request mixを変えてbottleneckと安全容量を再測定した比較
能力の進行
recognize
offered load、throughput、percentile、utilization、queue、headroomを区別できる
証拠: ボトルネック証拠、負荷曲線、容量限界を含む性能報告
explain
最大RPSと安全容量、profile上位関数と根本原因が同義でない理由を説明できる
証拠: CPU使用率、最大RPS、平均latencyだけでは安全容量を決められない理由の5分説明
apply
低負荷から超過と回復までの曲線を作り、Littleの式を定常区間だけで照合できる
証拠: ボトルネック証拠、負荷曲線、容量限界を含む性能報告
diagnose
tail増大、error、queue、downstream、profileを組み合わせてbottleneck仮説を絞れる
証拠: plateau、tail、error、queue、回復、profileから次の測定を選ぶ回答
lead
request mixと成長予測を更新し、headroomと再測定条件を合意できる
証拠: request mixを変えてbottleneckと安全容量を再測定した比較
なぜ重要か
性能は「速いコード」を探す作業ではなく、利用者が送る負荷、受理できた仕事、成功した仕事、待ち行列、依存先、resource、回復を同じ時間軸で結ぶ判断である。平均latencyやCPU使用率だけでは、tailで待たされる利用者、dropされたrequest、負荷を下げても残るqueueを見落とす。
最大RPSは観測した一点であり、安全容量ではない。通常のrequest mix、成長、障害時の縮退、測定誤差を含むheadroomを置き、どの観測で再評価するかを決めて初めて運用上の容量になる。
メンタルモデル
性能判断を、需要、受理、完了、待機、resource、依存、利用者結果のflowとして扱う。このfixtureは有限queue 30 requestを使い、request countの保存則Qnext=max(0,Qprev+admitted-completed)を各点で検査する。Δt=1 sでRPSをrequest countへ変換してから保存則へ入れる。acceptedはその区間に到着して即時処理または有限queueへadmittedされた仕事、successはその区間に既存backlogを含めてcompletedした仕事を表すため、回復区間ではsuccessがacceptedを上回り得る。
平均同時実行数L、平均到着または完了率λ、平均滞在時間Wの関係L = λWは、単位と観測窓を揃えた定常区間の整合確認に使う。式が合うことは、個別requestのtailや将来の容量を保証しない。
注記
図を読む際の補足情報です。
- この注記は旧図の読み順を保持する補助です。
- Fixture: request mix、payload、依存、処理上限、環境を固定する。
- Curve: 低負荷、限界付近、超過、回復を同じ列で記録する。
- Diagnose: plateau、tail、error、queue、resource、downstreamを結ぶ。
- Profile: 仮説に合う実processの局所証拠を別に採取する。
- Capacity: observed kneeからheadroomを引き、再測定条件を残す。
固定した負荷条件から、どの証拠を経て安全容量を決めるか。
- 原因
- Fixture(負荷条件)
- request mix、payload、依存、処理上限、環境を固定する。
- 機構
- Bottleneck mechanism
- resource上限、queue蓄積、downstream待機が処理率を制限し、tail latencyとerrorを増幅する。
- 結果
- Curve
- 低負荷、限界付近、超過、回復を同じ列で記録する。
- Profile
- 仮説に合う実processの局所証拠を別に採取する。
- 対策
- Capacity
- observed kneeからheadroomを引き、再測定条件を残す。
- Fixture(負荷条件) → Bottleneck mechanism: 固定条件でresource、queue、downstreamの飽和を起こす
- Bottleneck mechanism → Curve: plateau、tail、error、queueとして現れる
- Bottleneck mechanism → Profile: 仮説に合う局所証拠を採る
- Curve → Capacity: observed kneeからheadroomを引く
- Profile → Capacity: 局所証拠を再測定条件へ反映する
負荷曲線のknee、tail、queue、局所profileを結び、headroomを引いた容量と再測定条件を示せる。
| パラメータ | 選択肢 | 既定値 |
|---|---|---|
| 負荷帯 | 低負荷、限界付近、超過、負荷を戻した回復区間 | low |
| worker処理枠とrequest mix | read 80%・capacity 100 RPS、write 70%・capacity 80 RPS | baseline |
- baseline低負荷: Δt=1s;Qstart=0req;arrivals=40req;admitted=40req;immediate=40req;backlogDone=0req;completed=40req;Qend=0req;rejected=0req;failed=0req;p99=70ms.; 条件
load-band=low、workers=baseline; nodefixture、curve; edgefixture-exposes-mechanism、mechanism-shapes-curve - baseline限界付近: Δt=1s;Qstart=0req;arrivals=100req;admitted=100req;immediate=100req;backlogDone=0req;completed=100req;Qend=0req;rejected=0req;failed=0req;p99=100ms.; 条件
load-band=near、workers=baseline; nodecurve、capacity; edgecurve-bounds-capacity - baseline飽和: Δt=1s;Qstart=0req;arrivals=150req;admitted=130req;immediate=100req;backlogDone=0req;completed=100req;Qend=30req;rejected=20req;failed=20req;p99=450ms.; 条件
load-band=overload、workers=baseline; nodebottleneck-mechanism、curve、capacity; edgemechanism-shapes-curve、curve-bounds-capacity - baseline回復hysteresis: Δt=1s;Qstart=30req;arrivals=50req;admitted=50req;immediate=50req;backlogDone=30req;completed=80req;Qend=0req;rejected=0req;failed=0req;p99=180ms.; 条件
load-band=recovery、workers=baseline; nodecurve、capacity; edgecurve-bounds-capacity - write中心低負荷: Δt=1s;Qstart=0req;arrivals=40req;admitted=40req;immediate=40req;backlogDone=0req;completed=40req;Qend=0req;rejected=0req;failed=0req;p99=90ms.; 条件
load-band=low、workers=write; nodefixture、curve; edgefixture-exposes-mechanism、mechanism-shapes-curve - write中心限界付近: Δt=1s;Qstart=0req;arrivals=80req;admitted=80req;immediate=80req;backlogDone=0req;completed=80req;Qend=0req;rejected=0req;failed=0req;p99=190ms.; 条件
load-band=near、workers=write; nodecurve、capacity; edgecurve-bounds-capacity - write中心飽和: Δt=1s;Qstart=0req;arrivals=120req;admitted=110req;immediate=80req;backlogDone=0req;completed=80req;Qend=30req;rejected=10req;failed=10req;p99=900ms.; 条件
load-band=overload、workers=write; nodebottleneck-mechanism、curve、capacity; edgemechanism-shapes-curve、curve-bounds-capacity - write中心回復hysteresis: Δt=1s;Qstart=30req;arrivals=50req;admitted=50req;immediate=50req;backlogDone=30req;completed=80req;Qend=0req;rejected=0req;failed=0req;p99=360ms.; 条件
load-band=recovery、workers=write; nodecurve、capacity; edgecurve-bounds-capacity
| イベント | 開始 | 終了 | 条件 |
|---|
| 結果 | 状態 |
|---|---|
| baseline 40 arrivalsを全件admit・completeし、Qend=0で保存則が成立する。 | stable-load |
| baseline 100 arrivalsを全件即時completeするkneeで、safe capacityは20% headroom後の80 RPS。 | near-capacity |
| baseline超過では130 admit、100 complete、Qend=30、20 reject/failedとなる。 | saturation |
| baseline回復は30 backlogをdrainしてQend=0に戻すが、p99=180 msのhysteresisが残る。 | capacity-recovered |
| write中心40 arrivalsを全件admit・completeし、Qend=0で保存則が成立する。 | write-stable-load |
| write中心80 arrivalsを全件即時completeするkneeで、safe capacityは20% headroom後の64 RPS。 | write-near-capacity |
| write中心超過では110 admit、80 complete、Qend=30、10 reject/failedとなる。 | write-saturation |
| write中心回復は30 backlogをdrainしてQend=0に戻すが、p99=360 msのhysteresisが残る。 | write-capacity-recovered |
現在の状態: baseline低負荷 — Δt=1s;Qstart=0req;arrivals=40req;admitted=40req;immediate=40req;backlogDone=0req;completed=40req;Qend=0req;rejected=0req;failed=0req;p99=70ms.
このモデルは例示的かつ決定的であり、実システムの完全な再現ではありません。
動く例で考える
注文APIのsimulated load curveとactual local profileを分ける
- 前提
- production計測ではない固定simulationを使う。read比率80%とwrite比率70%の二つのrequest mixへ、低負荷、限界付近、超過、回復のload pointを与える。各load pointはΔt=1 sで、queue上限は30 requestとする。各点のmeanはbounded latency sampleの算術平均、active concurrencyはLittleの式から作らない独立synthetic入力とする。profileだけは、この場のCPython 3.13 processで実際に採取する。cProfile/pstatsはPython 3.13標準libraryを対象とし、version固定のないGo Diagnosticsは2026-07-31確認時点の範囲として扱う。
- 入力
- baselineは40、100、150、50 RPS、write中心transferは40、80、120、50 RPSの順で負荷を変える。各点に7件のlatency sampleを固定し、最後の点は超過後の回復区間とする。
- 操作
- 区間開始queue、arrivals、admitted、immediate work、backlog completed、completed、区間終了queue、rejected/failedを同じrequest単位で計算する。FIFOの既存backlogを先に処理し、残りcapacityで新規到着を即時処理し、overflowは30 requestまでadmitする。throughput plateau、tail growth、reject発生が初めて同時成立する点の直前をcurve上のkneeとし、20%のheadroomを引く。
- 観測
- baseline curveは100 RPS、write中心curveは80 RPSが違反点直前のkneeとなる。超過区間のqueueは30 requestで上限に達し、回復区間で30 requestをdrainしてQend=0となる一方、p99のhysteresisは残る。両方のsafe capacityは、それぞれのcurveから導いたkneeへ同じ20% headroomを適用して80 RPSと64 RPSになる。
- 結論
- 安全容量は最大値ではなく、tailとerrorが急増する前のkneeから運用余力を引いた値である。simulationの数値は教材の因果を検査するfixtureであり、このserviceのproduction性能を表さない。
python3.13 - <<'PY'
import cProfile
import json
import math
import pstats
HARNESS = "performance_capacity_lab_v1"
CAPACITY_RPS = 100
INTERVAL_SECONDS = 1
QUEUE_LIMIT_REQUESTS = 30
HEADROOM_FRACTION = 0.2
KNEE_CRITERIA = [
"throughput-plateau",
"tail-growth",
"error-growth",
]
BASELINE_POINT_INPUTS = [
{
"stage": "low",
"offered_rps": 40,
"observed_active_concurrency": 2.0,
"latency_samples_ms": [30, 34, 36, 40, 45, 55, 70],
},
{
"stage": "near-limit",
"offered_rps": 100,
"observed_active_concurrency": 6.2,
"latency_samples_ms": [40, 45, 50, 55, 60, 75, 100],
},
{
"stage": "overload",
"offered_rps": 150,
"observed_active_concurrency": 12.4,
"latency_samples_ms": [80, 100, 120, 160, 220, 300, 450],
},
{
"stage": "recovery",
"offered_rps": 50,
"observed_active_concurrency": 4.6,
"latency_samples_ms": [45, 50, 60, 75, 95, 130, 180],
},
]
TRANSFER_POINT_INPUTS = [
{
"stage": "low",
"offered_rps": 40,
"observed_active_concurrency": 2.4,
"latency_samples_ms": [35, 40, 45, 50, 60, 75, 90],
},
{
"stage": "near-limit",
"offered_rps": 80,
"observed_active_concurrency": 7.0,
"latency_samples_ms": [55, 65, 75, 85, 110, 145, 190],
},
{
"stage": "overload",
"offered_rps": 120,
"observed_active_concurrency": 15.0,
"latency_samples_ms": [120, 160, 220, 300, 420, 600, 900],
},
{
"stage": "recovery",
"offered_rps": 50,
"observed_active_concurrency": 6.8,
"latency_samples_ms": [75, 90, 110, 140, 190, 260, 360],
},
]
def nearest_rank(samples, quantile):
ordered = sorted(samples)
index = math.ceil(quantile * len(ordered)) - 1
return ordered[max(0, min(index, len(ordered) - 1))]
def simulate(
stage,
offered_rps,
capacity,
previous_queue,
write_fraction,
observed_active_concurrency,
latency_samples_ms,
):
interval_seconds = INTERVAL_SECONDS
queue_start_requests = previous_queue
arrivals_requests = offered_rps * interval_seconds
service_budget_requests = capacity * interval_seconds
# Existing FIFO backlog consumes service capacity first. This ordering makes
# recovery drain explicit and keeps every request in one conservation ledger.
backlog_completed_requests = min(
queue_start_requests,
service_budget_requests,
)
remaining_service_requests = (
service_budget_requests - backlog_completed_requests
)
immediate_work_requests = min(
arrivals_requests,
remaining_service_requests,
)
arrival_overflow_requests = arrivals_requests - immediate_work_requests
remaining_backlog_requests = (
queue_start_requests - backlog_completed_requests
)
queue_space_requests = QUEUE_LIMIT_REQUESTS - remaining_backlog_requests
queued_current_requests = min(
arrival_overflow_requests,
queue_space_requests,
)
admitted_requests = immediate_work_requests + queued_current_requests
completed_requests = immediate_work_requests + backlog_completed_requests
queue_end_requests = remaining_backlog_requests + queued_current_requests
rejected_requests = arrivals_requests - admitted_requests
failed_requests = rejected_requests
assert queue_end_requests == max(
0,
queue_start_requests + admitted_requests - completed_requests,
)
assert arrivals_requests == admitted_requests + rejected_requests
accepted_rps = admitted_requests / interval_seconds
success_rps = completed_requests / interval_seconds
samples = list(latency_samples_ms)
# Bounded samples keep this local harness deterministic and cheap. The
# arithmetic mean intentionally has a separate derivation from percentiles
# so a plausible-looking p50/p95 midpoint cannot masquerade as observation.
assert 5 <= len(samples) <= 20
assert all(0 < sample <= 10_000 for sample in samples)
mean_ms = sum(samples) / len(samples)
p50_ms = nearest_rank(samples, 0.50)
p95_ms = nearest_rank(samples, 0.95)
p99_ms = nearest_rank(samples, 0.99)
return {
"stage": stage,
"interval_seconds": interval_seconds,
"queue_start_requests": queue_start_requests,
"arrivals_requests": arrivals_requests,
"admitted_requests": admitted_requests,
"immediate_work_requests": immediate_work_requests,
"backlog_completed_requests": backlog_completed_requests,
"completed_requests": completed_requests,
"queue_end_requests": queue_end_requests,
"rejected_requests": rejected_requests,
"failed_requests": failed_requests,
"offered_rps": offered_rps,
"accepted_rps": accepted_rps,
"success_rps": success_rps,
"p50_ms": p50_ms,
"p95_ms": p95_ms,
"p99_ms": p99_ms,
"mean_ms": mean_ms,
"mean_source": "bounded-latency-samples-arithmetic-mean",
"latency_samples_ms": samples,
"observed_active_concurrency": observed_active_concurrency,
"cpu_percent": min(99.0, 20.0 + success_rps * 0.65),
"memory_mb": 128 + queue_end_requests * 2,
"queue_depth": queue_end_requests,
"downstream_ms": 15 + int(write_fraction * 50) + queue_end_requests // 2,
}
def safe_capacity(observed_knee_rps, headroom_fraction):
return int(observed_knee_rps * (1 - headroom_fraction))
def processing_model_for_mix(write_fraction):
# Request mix changes the simulated processing envelope. The reported knee
# is not taken from this branch: detect_knee derives it from the resulting
# curve so the decision remains inspectable and mutation-sensitive.
if write_fraction > 0.5:
return {"bottleneck": "storage-write", "capacity_rps": 80}
return {"bottleneck": "cpu-transform", "capacity_rps": CAPACITY_RPS}
def build_load_curve(point_inputs, processing_capacity_rps, write_fraction):
curve = []
previous_queue = 0
for point_input in point_inputs:
point = simulate(
point_input["stage"],
point_input["offered_rps"],
processing_capacity_rps,
previous_queue,
write_fraction,
point_input["observed_active_concurrency"],
point_input["latency_samples_ms"],
)
curve.append(point)
previous_queue = point["queue_depth"]
return curve
def detect_knee(load_curve):
# Capacity is the last passing offered-load point before all three failure
# signals first coincide, never a constant copied from the simulator input.
for previous, current in zip(load_curve, load_curve[1:]):
criteria = {
"throughput-plateau": (
current["success_rps"] <= previous["success_rps"]
),
"tail-growth": current["p99_ms"] > previous["p99_ms"],
"error-growth": current["rejected_requests"] > 0,
}
if all(criteria.values()):
return {
"observed_knee_rps": previous["offered_rps"],
"first_violation_stage": current["stage"],
"criteria": criteria,
}
raise AssertionError("load-curve-causal-invariant: no measured knee")
def profile_target(values):
total = 0
for value in values:
for multiplier in (1, 3, 5, 7):
total += (value * multiplier) % 97
return total
def main():
baseline_write_fraction = 0.2
transfer_write_fraction = 0.7
baseline_model = processing_model_for_mix(baseline_write_fraction)
transfer_model = processing_model_for_mix(transfer_write_fraction)
curve = build_load_curve(
BASELINE_POINT_INPUTS,
baseline_model["capacity_rps"],
baseline_write_fraction,
)
transfer_curve = build_load_curve(
TRANSFER_POINT_INPUTS,
transfer_model["capacity_rps"],
transfer_write_fraction,
)
# This boundary is separate from the authored eight scenario states. With
# total work above capacity, it proves FIFO backlog receives service before
# current arrivals; aggregate Qend alone cannot distinguish arrival-first.
fifo_boundary_point = simulate(
"fifo-backlog-first-boundary",
90,
CAPACITY_RPS,
30,
baseline_write_fraction,
10.0,
[40, 45, 50, 55, 60, 75, 100],
)
fifo_backlog_first_boundary = {
"interval_seconds": fifo_boundary_point["interval_seconds"],
"queue_start_requests": fifo_boundary_point["queue_start_requests"],
"arrivals_requests": fifo_boundary_point["arrivals_requests"],
"service_capacity_requests": CAPACITY_RPS * INTERVAL_SECONDS,
"queue_limit_requests": QUEUE_LIMIT_REQUESTS,
"backlog_completed_requests": fifo_boundary_point[
"backlog_completed_requests"
],
"immediate_work_requests": fifo_boundary_point[
"immediate_work_requests"
],
"admitted_requests": fifo_boundary_point["admitted_requests"],
"completed_requests": fifo_boundary_point["completed_requests"],
"queue_end_requests": fifo_boundary_point["queue_end_requests"],
"rejected_requests": fifo_boundary_point["rejected_requests"],
"failed_requests": fifo_boundary_point["failed_requests"],
}
assert fifo_backlog_first_boundary == {
"interval_seconds": 1,
"queue_start_requests": 30,
"arrivals_requests": 90,
"service_capacity_requests": 100,
"queue_limit_requests": 30,
"backlog_completed_requests": 30,
"immediate_work_requests": 70,
"admitted_requests": 90,
"completed_requests": 100,
"queue_end_requests": 20,
"rejected_requests": 0,
"failed_requests": 0,
}
by_stage = {point["stage"]: point for point in curve}
assert by_stage["overload"]["immediate_work_requests"] == CAPACITY_RPS
assert by_stage["overload"]["queue_depth"] > 0
assert by_stage["recovery"]["p99_ms"] > by_stage["low"]["p99_ms"]
little_source = by_stage["near-limit"]
throughput_per_second = little_source["success_rps"]
mean_response_seconds = little_source["mean_ms"] / 1_000
observed_concurrency = little_source["observed_active_concurrency"]
calculated_concurrency = (
throughput_per_second * mean_response_seconds
)
relative_error = abs(
observed_concurrency - calculated_concurrency
) / observed_concurrency
profiler = cProfile.Profile()
profiler.enable()
profile_result = profile_target(range(400))
profiler.disable()
stats = pstats.Stats(profiler)
assert profile_result > 0
measured_entries = [
{
"function": function_key[2],
"primitive_calls": values[0],
"total_calls": values[1],
"cumulative_seconds": values[3],
}
for function_key, values in stats.stats.items()
]
measured_top = max(
measured_entries,
key=lambda entry: (
entry["cumulative_seconds"],
entry["total_calls"],
entry["function"],
),
)
baseline_knee_evidence = detect_knee(curve)
transfer_knee_evidence = detect_knee(transfer_curve)
baseline_knee = baseline_knee_evidence["observed_knee_rps"]
mutated_knee = transfer_knee_evidence["observed_knee_rps"]
baseline_safe_capacity = safe_capacity(
baseline_knee,
HEADROOM_FRACTION,
)
mutated_safe_capacity = safe_capacity(
mutated_knee,
HEADROOM_FRACTION,
)
request_mix_mutation = {
"changed_assumption": "request-mix",
"baseline_request_mix": {
"read_fraction": 1 - baseline_write_fraction,
"write_fraction": baseline_write_fraction,
},
"mutated_request_mix": {
"read_fraction": 1 - transfer_write_fraction,
"write_fraction": transfer_write_fraction,
},
"baseline_bottleneck": baseline_model["bottleneck"],
"mutated_bottleneck": transfer_model["bottleneck"],
"baseline_observed_knee_rps": baseline_knee,
"mutated_observed_knee_rps": mutated_knee,
"baseline_safe_capacity_rps": baseline_safe_capacity,
"mutated_safe_capacity_rps": mutated_safe_capacity,
"mutated_load_curve": transfer_curve,
"baseline_knee_evidence": baseline_knee_evidence,
"mutated_knee_evidence": transfer_knee_evidence,
"knee_criteria": KNEE_CRITERIA,
"recomputed": (
baseline_model["bottleneck"] != transfer_model["bottleneck"]
and baseline_knee != mutated_knee
and baseline_safe_capacity != mutated_safe_capacity
),
}
assert request_mix_mutation["recomputed"]
report = {
"harness": HARNESS,
"fixture": "order-api-load-v1",
"simulation_metadata": {
"kind": "simulated",
"provenance": "lesson-defined deterministic capacity model",
"purpose": "負荷変化と容量判断の因果を反証する",
"limitations": (
"production latency、hardware、network、vendor性能を表さない"
),
},
"load_curve": curve,
"fifo_backlog_first_boundary": fifo_backlog_first_boundary,
"curve_analysis": {
"throughput_plateau": (
by_stage["near-limit"]["success_rps"]
== by_stage["overload"]["success_rps"]
),
"tail_growth": (
by_stage["overload"]["p99_ms"]
> by_stage["near-limit"]["p99_ms"]
),
"error_growth": by_stage["overload"]["rejected_requests"] > 0,
"recovery_hysteresis": (
by_stage["recovery"]["p99_ms"]
> by_stage["low"]["p99_ms"]
),
},
"capacity": {
"observed_knee_rps": baseline_knee,
"safe_capacity_rps": baseline_safe_capacity,
"headroom_fraction": HEADROOM_FRACTION,
"knee_evidence": baseline_knee_evidence,
"decision": "80 RPSを通常運用上限として再測定する",
},
"little_law": {
"scope": "near-limit steady-state average only",
"source_stage": little_source["stage"],
"throughput_per_second": throughput_per_second,
"mean_response_seconds": mean_response_seconds,
"mean_response_source": little_source["mean_source"],
"observed_concurrency": observed_concurrency,
"observed_concurrency_source": (
"independent-simulated-fixture-input"
),
"calculated_concurrency": calculated_concurrency,
"relative_error": relative_error,
},
"local_profile": {
"evidence_kind": "actual-local-measurement",
"profiler": "cProfile",
"declared_target": profile_target.__name__,
"total_calls": stats.total_calls,
"measured_top_function": measured_top["function"],
"measured_functions": sorted(
{entry["function"] for entry in measured_entries}
),
"measurement_derived": True,
"tool_scope": {
"python": "3.13",
"profiler": "cProfile/pstats standard library",
"go_docs": "rolling; reviewed 2026-07-31",
},
"environment_limitations": (
"このCPython processのbounded fixtureだけを測り、"
"production bottleneckを主張しない"
),
},
"request_mix_mutation": request_mix_mutation,
"runtime_bound": {
"iterations": 1656,
"subprocesses": 0,
},
"mastery_evidence": {
"lab_steps": [
{"step": 1, "evidence": "simulation metadataとrequest mix"},
{"step": 2, "evidence": "4段階load_curveの全観測列"},
{"step": 3, "evidence": "curve_analysisとcapacity"},
{"step": 4, "evidence": "Little照合とcProfile"},
{"step": 5, "evidence": "request_mix_mutation"},
],
"assessments": [
{"assessment": 1, "evidence": "queueとdownstreamを含む診断"},
{"assessment": 2, "evidence": "Littleの式のscopeと誤差"},
],
"rubric_dimensions": [
"technical-correctness",
"judgment",
"evidence",
"communication",
],
"transfer": {
"task": (
"参照中心から書込み中心へ変わるrequest mixで"
"bottleneckと安全容量を再測定する"
),
"changed_assumption": "request-mix",
"evidence": "bottleneckとsafe capacityの再計算",
},
},
"external_network_used": False,
}
assert all(report["curve_analysis"].values())
assert baseline_knee in {point["offered_rps"] for point in curve}
assert mutated_knee in {
point["offered_rps"] for point in transfer_curve
}
assert 0 < report["little_law"]["relative_error"] <= 0.05
return report
print(json.dumps(main(), ensure_ascii=False, indent=2))
PY
注記
図を読む際の補足情報です。
- 値はlesson内のbaseline synthetic fixtureであり、本番環境の保証値ではない。
- offered loadだけでなく、completed、queue、rejected、p99を同じ行で読む。
- recoveredはqueueが0でもp99が低負荷値へ即時には戻らないhysteresisを示す。
同じbaseline fixtureで負荷帯を変えたとき、latencyとqueueのどの変化をcapacity判断へ使うか。
| 項目 | load / throughputoffered、admitted、completed、rejectedを分ける。 | p99 latency平均値では隠れるtailと回復遅延を見る。 | queue区間終了時の未処理件数で飽和とdrainを確認する。 | capacity判断knee、headroom、再測定条件を明記する。 |
|---|---|---|---|---|
| stable40 RPSの低負荷でqueueを作らず完了する基準点。 | 40 offered / 40 completed / 0 rejected | p99 70 ms | Qend 0 | 低負荷の基準点。容量上限とはみなさない |
| saturation150 RPSを投入し、処理上限を超えてqueueとrejectが生じる。 | 150 offered / 100 completed / 20 rejected | p99 450 ms | Qend 30 | knee 100 RPSを超過。safe capacityはheadroom 20%後の80 RPS |
| recovered負荷を50 RPSへ戻し、30件のbacklogをdrainした直後。 | 50 offered / 80 completed / 0 rejected | p99 180 ms | Qstart 30 → Qend 0 | queue回復だけで完了にせずtailの再測定を続ける |
stable、saturation、recoveredを横並びにし、回復後も残るtail latencyを含めてobserved kneeからheadroomを引く理由を説明できる。
トレードオフと失敗モード
| 観測 | 次の行動 | 代償 | 停止条件 |
|---|---|---|---|
| CPU上昇とthroughput plateauが同時 | CPU profileと実行経路を採取する | profile overheadと環境差が入る | 別resourceまたは負荷生成側が先に飽和した時 |
| CPU余裕があるがqueueとdownstreamが増加 | 依存先、pool、lock、I/O待機を分解する | cross-systemの相関計測が増える | offeredとacceptedの差が負荷生成側で生じた時 |
| 超過後に負荷を戻してもtailが残る | queue drainを容量とrunbookへ含める | 平常時の利用率を下げる | 回復時間が利用者目標内で安定した時 |
- 誤診: CPU profileの最大関数を最適化すればsystem bottleneckは必ず消える。反証: 待機、lock、downstream、負荷生成器の限界はCPU profileだけでは見えず、変更後のend-to-end曲線が必要である。
- 誤診: 一度達成した最大RPSがproductionの安全容量である。反証: request mix、tail、error、queue、障害時縮退、成長とheadroomを含まず、回復可能性も示していない。
- 失敗モード: simulationをmeasured production値として共有すると、hardwareと依存先の差を隠した架空の容量計画になる。
- 失敗モード: 平均latencyだけでは少数の遅いrequestとretry増幅を隠すため、p95とp99、goodputを併記する。
知識チェック
- offered、accepted、success throughputを分けると何を診断できるか。
- Littleの式を回復区間へそのまま適用できない理由を説明せよ。
- CPU使用率が低いままp99が増える時の候補と追加証拠を三つ挙げよ。
- request mix変更を容量計画の再評価条件へどう組み込むか。
出典と次の学習
profileとtraceの選択はGo Diagnostics、実行したcProfile/pstatsの意味はPython 3.13のThe Python Profilers、overload制御はGoogle SRE Chapter 21を一次資料として照合する。Goの文書はrolling URLのため2026-07-31に再確認した範囲である。平均queueの整合にはLittleの原論文、tailのsystem効果にはThe Tail at Scaleを使う。いずれも特定fixtureの最大RPSを保証する資料ではない。
次のcore-15では、容量曲線を利用者journeyのgood event、SLO、error budget、multi-window alertへ接続し、性能劣化をpageとticketへ分ける。
実践ラボ
注文APIの負荷曲線とlocal profileを分離して検証する
提出成果物: ボトルネック証拠、負荷曲線、容量限界を含む性能報告
- read中心の固定request mixと処理容量をsimulated fixtureとして明示する
- 低負荷、限界付近、超過、回復のoffered、accepted、success、p50、p95、p99、resource、queue、downstreamを計算する
- throughput plateau、tailとerrorの増加、回復hysteresisからobserved kneeとheadroom付き安全容量を求める
- 定常区間だけでLittleの式を照合し、stdlib cProfileによるactual local measurementを別の証拠として採取する
- write中心へrequest mixを変え、bottleneckと安全容量が再計算されることをmutationで検証する
説明して理解を確かめる
5分で、CPUが最大の関数が常にbottleneckではない理由、Littleの式が定常平均を前提にする理由、最大RPSを安全容量にできない理由を説明する。
アセスメント
問い: throughputは横ばいだがCPUは70%でp99とqueueが増えた。何をbottleneck候補にし、どの証拠を追加するか。
期待する証拠: offeredとacceptedの差、downstream latency、queue、tail、memory、profile、負荷生成側限界を分離する診断
問い: 平均latencyからLittleの式が合ったため超過負荷でも容量予測は正しい、という主張を評価する。
期待する証拠: 定常性、平均値、観測窓、dropとretry、回復hysteresis、誤差を使った適用限界
別問題へ転用する
参照中心から書込み中心へ変わるrequest mixでbottleneckと安全容量を再測定する
復習スケジュール
- 1日後
offered loadとaccepted throughputが分かれる時に何を観測するか
- 7日後
Littleの式を使えない非定常な反例を挙げる
- 30日後
request mix変更でbottleneck仮説をどう更新するか
- 90日後
offered loadとaccepted throughputが分かれる時に何を観測するか
評価ルーブリック
| 観点 | 未達 | 発展途上 | 熟達 | 卓越 |
|---|---|---|---|---|
| technical-correctness | offered loadと成功throughputを同一視し平均だけを記録する | percentileとresourceはあるがqueue、error、回復を結び付けない | 負荷曲線、Littleの式、profile、headroomを適用条件付きで整合させる | 負荷生成側と依存先の限界、hysteresis、測定overheadまで反証する |
| judgment | 最大RPSをそのままproduction容量にする | headroomを置くが根拠と再評価条件がない | tail、error、queue、成長、運用余力から安全容量を選ぶ | request mixと障害時縮退を含む複数容量境界を運用判断へ接続する |
| evidence | simulated値または単発profileをproduction実測として提示する | simulationとprofileは分けるがbottleneck判断を再現できない | fixture、計算、actual local profile、mutationを再実行可能に残す | 環境差とprofile overheadを明示し、別手法の証拠で仮説を交差検証する |
| communication | 一つの最大値だけを示して利用者影響を説明しない | 曲線は示すが観測値と判断を区別しない | 観測、推論、限界、決定、再測定条件を同じ報告で追える | 開発、運用、事業がrequest mixとheadroomのtrade-offを更新できる |
出典
以下の外部資料は利用者が選択したときだけ開きます。
- Diagnostics (primary)
- Handling Overload (primary)
- A Proof for the Queuing Formula: L = λW (peer-reviewed)
- The Tail at Scale (peer-reviewed)
- The Python Profilers (primary)