メインコンテンツまでスキップ

第06章:CAPを日常例で理解する(まずは暗記しない)🍞📦

この章の結論1行📝✨

「通信が切れる(P)瞬間、システムは “一致を守って止まる(C寄り)” か “止まらず返してズレを許す(A寄り)” のどっちかを選ぶ」です。(Google Cloud)


6.1 CAPって、まず“日常語”にしてみよ〜🍞☕️

CAPの3文字は、こう言い換えるとスッと入ります😊

  • C(Consistency:一致)🧩 「どこで見ても、同じ答えが返ってくる(最新 or エラー)」 ※ここでのCは、いわゆるACIDのCとは別物扱い(“いつ見ても最新”寄りの強い意味)だよ〜⚠️(AWSドキュメント)

  • A(Availability:応答)📨 「動いてるノードに届いたリクエストには、とにかく返事する(中身が最新とは限らない)」 ※CAPのAは“100%応答”みたいな強め定義として語られがちで、一般に言う“高可用性”の感覚とズレやすいよ〜😵‍💫

  • P(Partition tolerance:分断耐性)🔌 「ネットワークが切れたり遅すぎたり、メッセージが落ちても、システムとして動き続ける前提」 (“メッセージが落ちる/遅れる”まで含めてPと考えるのが超大事!)(AWSドキュメント)

CAP定理の三角形


6.2 “暗記しない”ためのコツ🧠💡

CAPは「3つのうち2つ選べ!」みたいに言われがちだけど、そこから入ると混乱しやすいです🙅‍♀️

覚え方はこれだけ👇

✅ ステップ1:まず「Pは起きるもの」とする🔌

分散はネットワーク越しなので、切断や遅延は“起きうる前提”で設計する、が基本です。(Google Cloud)

✅ ステップ2:Pが起きた“その瞬間”だけ注目する⏱️

普段(分断が起きてない時)は、CっぽくもAっぽくもできる。 問題は「分断が起きた瞬間に、どっちを捨てる?」 です。


6.3 日常たとえ話でCAPをつかむ🍞📦🧾

たとえ①:パン屋さんの“在庫”が2つのレジに分かれてる🍞🧾

  • レジAとレジBがあって、本当は在庫を共有したい
  • でも突然、レジ同士の通信が切れた(P)🔌

このときの2択👇

🧩 C寄り(正しさ優先):売らない/止める

「今この在庫が本当に残ってるか分からないなら、売らない(エラー返す)」

  • 👍 いいところ:在庫の正しさが守れる
  • 👎 つらいところ:お客さんは買えない(応答できない/失敗が増える)

📨 A寄り(応答優先):とにかく売る

「とりあえず売って、あとで在庫が合わないかも」

  • 👍 いいところ:お客さんは買える(返事はする)
  • 👎 つらいところ:売りすぎ(在庫マイナス)など“ズレ”が起きうる

これが 「Pが起きたら C か A どっち?」 の感覚だよ〜⚖️🔥(Google Cloud)

CAPトレードオフ


たとえ②:グループチャット📱💬

通信が切れても「送信ボタン押したら送れた感」を出す(A寄り)と、 あとで順番が前後したり、既読がズレたりしがち😵‍💫 逆に「通信不安定なので送信できません」(C寄り)なら、矛盾は減るけど使い勝手は落ちる…!


たとえ③:お金の振込💸🏦

「残高」や「二重引き落とし」は事故ると致命的💥 だから多くは C寄り(正しさ優先で止める/待たせる)になりやすいです。 (UXは落ちるけど、事故の方がヤバいからね…!)


6.4 いちばん大事な誤解ポイント⚠️😵‍💫

❌「CAPは3つのうち2つを自由に選べる」

実は、“分断が起きたときに” どっちを優先するかの話として捉えるとスッキリします。(Google Cloud)

さらに、「CPデータベース」「APデータベース」って雑にラベリングするのも危険って話もよく出ます(状況や設定で挙動が変わるから)。(martin.kleppmann.com)


6.5 ハンズオン:分断中の挙動を2パターン作る(拒否 vs 受付)🧪🔌

ここでは“超ミニ分散”を、Windows上でサクッと再現します😊 同じ「購入API」なのに、分断(P)中の設計方針で体験が変わるのがゴール!

🎯 作るもの

  • Node A(ポート 4001)🅰️
  • Node B(ポート 4002)🅱️
  • 在庫 stock を持ってて、購入すると減る📦
  • A↔Bで「購入ログ」を送り合って同期っぽくする📨
  • 通信を切るスイッチで“分断”を作る🔌

6.5.1 AIに“たたき台”を作らせるプロンプト例🤖📝

Copilot / Codex にこう投げると早いよ〜✨

  • 「TypeScriptで、2つのHTTPサーバー(4001/4002)を起動して、/buyで在庫を減らすミニ例を作って。peerへPOSTして同期する。通信を切るフラグも入れて。」
  • 「CAPの観点で、分断中に“拒否(C寄り)”と“受付(A寄り)”の2モードを切り替えられるようにして。」

出てきたコードは、そのまま使わずに👇をチェック✅

  • 例外処理ちゃんとある?😵‍💫
  • タイムアウト入ってる?⏱️
  • ログ見れば挙動が追える?🕵️‍♀️

6.5.2 サンプル実装(最小構成)💻✨

📁 ファイル:src/node.ts

import http from "node:http";

type Mode = "cp" | "ap";

const NODE_ID = process.env.NODE_ID ?? "A";
const PORT = Number(process.env.PORT ?? (NODE_ID === "A" ? 4001 : 4002));
const PEER_URL = process.env.PEER_URL ?? (NODE_ID === "A" ? "http://localhost:4002" : "http://localhost:4001");
const MODE = (process.env.MODE ?? "cp") as Mode;

// 分断スイッチ(trueなら通信OK、falseなら“分断中”)
const LINK_UP = (process.env.LINK_UP ?? "true") === "true";

// 状態(超ミニなのでインメモリ)
let stock = 5;
const applied = new Set<string>(); // どの購入イベントを適用したか(重複防止の超入門)
const outbox: any[] = []; // APモードの“あとで送る”用(簡易)

function json(res: http.ServerResponse, status: number, body: unknown) {
const text = JSON.stringify(body);
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
res.end(text);
}

async function readBody(req: http.IncomingMessage): Promise<any> {
const chunks: Buffer[] = [];
for await (const c of req) chunks.push(Buffer.from(c));
const text = Buffer.concat(chunks).toString("utf-8");
return text ? JSON.parse(text) : {};
}

async function postWithTimeout(url: string, body: any, timeoutMs: number) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const r = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: ctrl.signal,
});
return { ok: r.ok, status: r.status, text: await r.text() };
} finally {
clearTimeout(t);
}
}

function applyEvent(ev: { eventId: string; qty: number; from: string }) {
if (applied.has(ev.eventId)) return; // 重複は無視(この章では“雰囲気”でOK)
applied.add(ev.eventId);
stock -= ev.qty;
}

const server = http.createServer(async (req, res) => {
try {
if (!req.url) return json(res, 404, { error: "no url" });

// ヘルス
if (req.method === "GET" && req.url === "/health") {
return json(res, 200, { node: NODE_ID, ok: true, mode: MODE, linkUp: LINK_UP });
}

// 在庫を見る
if (req.method === "GET" && req.url === "/stock") {
return json(res, 200, { node: NODE_ID, stock, applied: applied.size, outbox: outbox.length });
}

// peerからの同期イベント受信
if (req.method === "POST" && req.url === "/sync") {
const ev = await readBody(req);
applyEvent(ev);
console.log(`[${NODE_ID}] sync <-`, ev);
return json(res, 200, { ok: true });
}

// 購入(qtyだけ減らす)
if (req.method === "POST" && req.url === "/buy") {
const { qty = 1 } = await readBody(req);
const eventId = `${Date.now()}-${Math.random().toString(16).slice(2)}-${NODE_ID}`;
const ev = { eventId, qty: Number(qty), from: NODE_ID };

// ここがCAPの分かれ道⚖️
if (MODE === "cp") {
// C寄り:peerにも反映できないなら“失敗”で返す(Aを捨てる)
if (!LINK_UP) {
console.log(`[${NODE_ID}] CP: partition -> reject`);
return json(res, 503, { ok: false, reason: "partition: reject to keep consistency-ish" });
}
const r = await postWithTimeout(`${PEER_URL}/sync`, ev, 200);
if (!r.ok) {
console.log(`[${NODE_ID}] CP: peer failed -> reject`, r.status);
return json(res, 503, { ok: false, reason: "peer unavailable: reject" });
}
// peerがOKなら自分も適用
applyEvent(ev);
console.log(`[${NODE_ID}] CP: commit`, ev);
return json(res, 200, { ok: true, mode: "cp", eventId, stock });
}

if (MODE === "ap") {
// A寄り:とにかく受付して返事する(Cはあとで崩れるかも)
applyEvent(ev);
console.log(`[${NODE_ID}] AP: accept`, ev);

if (!LINK_UP) {
outbox.push(ev);
console.log(`[${NODE_ID}] AP: queued (partition)`, ev.eventId);
return json(res, 200, { ok: true, mode: "ap", note: "queued due to partition", eventId, stock });
}

const r = await postWithTimeout(`${PEER_URL}/sync`, ev, 200);
if (!r.ok) {
outbox.push(ev);
console.log(`[${NODE_ID}] AP: queued (peer fail)`, ev.eventId);
return json(res, 200, { ok: true, mode: "ap", note: "queued due to peer fail", eventId, stock });
}

return json(res, 200, { ok: true, mode: "ap", note: "synced", eventId, stock });
}
}

// outbox再送(APモード用)
if (req.method === "POST" && req.url === "/retry") {
if (!LINK_UP) return json(res, 200, { ok: true, note: "still partition", outbox: outbox.length });

let sent = 0;
const rest: any[] = [];
for (const ev of outbox) {
const r = await postWithTimeout(`${PEER_URL}/sync`, ev, 200);
if (r.ok) sent++;
else rest.push(ev);
}
outbox.length = 0;
outbox.push(...rest);
return json(res, 200, { ok: true, sent, remain: outbox.length });
}

return json(res, 404, { error: "not found" });
} catch (e: any) {
return json(res, 500, { error: e?.message ?? String(e) });
}
});

server.listen(PORT, () => {
console.log(`[${NODE_ID}] listening on http://localhost:${PORT} mode=${MODE} linkUp=${LINK_UP} peer=${PEER_URL}`);
});

6.5.3 起動(PowerShell例)▶️💨

🅰️ ターミナル1:Node A(CPモード)

$env:NODE_ID="A"
$env:PORT="4001"
$env:PEER_URL="http://localhost:4002"
$env:MODE="cp"
$env:LINK_UP="true"
node .\dist\node.js

🅱️ ターミナル2:Node B(CPモード)

$env:NODE_ID="B"
$env:PORT="4002"
$env:PEER_URL="http://localhost:4001"
$env:MODE="cp"
$env:LINK_UP="true"
node .\dist\node.js

※TypeScriptのビルド(dist/node.js生成)は、手元のいつものTS手順でOKだよ〜🧰✨ (この章の主役はCAPなので、ここはサクッと!)


6.6 実験シナリオ:分断(P)を起こして比べる🔌🧪

  • Aに購入リクエスト → AもBも在庫が減る(同期できてる)😊

例(別ターミナルから):

Invoke-RestMethod -Method Post -Uri http://localhost:4001/buy -ContentType "application/json" -Body '{"qty":1}'
Invoke-RestMethod -Method Get -Uri http://localhost:4002/stock

たとえば Aだけ分断中にする:

  • Aの環境変数を LINK_UP="false" にして再起動(またはB側でもOK)

✅ CPモード(C寄り)だとどうなる?🧩

  • Aに /buy → 503で拒否(返事はするけど成功しない)
  • Bの在庫も壊れない 👉 Cを守るために、Aを捨てた って体験!(Google Cloud)

✅ APモード(A寄り)に変えると?📨

  • MODE="ap" で再起動して同じことをやると…
  • Aに /buy → 200で受付(在庫が減る)
  • でもBの在庫は減ってない(ズレる)😵‍💫 👉 Aを守るために、Cを捨てた って体験!(Google Cloud)

APモードなら、/retry を叩くと outbox が送られて、Bが追いつく(“収束っぽい”)になります📮💨

Invoke-RestMethod -Method Post -Uri http://localhost:4001/retry
Invoke-RestMethod -Method Get -Uri http://localhost:4002/stock

6.7 まとめ:CAPを“言葉で説明”できるようにする🎤✨

💬 口で言うテンプレ(超おすすめ)

  • 「分断(P)が起きたら、正しさ(C)を守るために止めるか、応答(A)を守るためにズレを許すかを決める」(Google Cloud)

✅ 1分チェック(YES/NO)🧠✅

  • 「分断中に“受付だけはしたい”」→ A寄り📨
  • 「分断中に“間違った結果は絶対ダメ”」→ C寄り🧩
  • 「分断はまあ起きうる」→ P前提🔌(Google Cloud)

6.8 ミニ問題(3問)✍️😆

Q1 🛒💳 決済(課金)

分断中に「二重課金」しうるなら、基本どっち寄り?

  • A寄り📨 / C寄り🧩

Q2 💬 SNSの“いいね”

分断中に一瞬ズレても、あとで直ればOKなら?

  • A寄り📨 / C寄り🧩

Q3 📦 在庫が1個しかない限定商品

分断中に売りすぎたら炎上🔥 どっち寄り?

  • A寄り📨 / C寄り🧩

(答えのコツ:事故コストが高いほどC寄り、UX優先ならA寄り😊)


この章で覚える最小ワード集📖✨

  • 分断(Partition)🔌:切断だけじゃなく、遅延・落ちも含む
  • C(一致)🧩:どこでも同じ答え(最新 or エラー)
  • A(応答)📨:生きてるノードは必ず返事(最新じゃないかも)
  • CAPの本質⚖️:「Pが起きた瞬間に、CかAのどっちを優先する?」(AWSドキュメント)