Network Computing · แล็ปโน้ต 3 จาก 5

Failure, Deployment and Observation

ความแตกต่างสำคัญระหว่างโปรแกรมธรรมดากับ networked system: เครื่องอื่นและเครือข่ายล้มเหลวได้อย่างไม่พร้อมกัน บางส่วน และไม่แจ้งเตือน — หัวข้อนี้สำคัญกว่าการรีบสอน microservice pattern จำนวนมาก

🔗
ต่อยอดจาก: หัวข้อที่ 2 — Network Services (ระบบสองบริการ Service A/B) และ TCP timeout/retransmission — หัวข้อนี้พาแนวคิดเดียวกันไปสู่ระดับ distributed system

1Latency, Packet Loss และ Partial Failure

Partial Failure คือหัวใจของปัญหา
ในโปรแกรมตัวเดียวบนเครื่องเดียว ถ้าฟังก์ชันหนึ่งพัง โปรแกรมทั้งตัวมักพังตามในทันที (fail-stop) — แต่ใน distributed system เมื่อ Service A เรียก Service B แล้ว B ไม่ตอบ Service A ไม่รู้แน่ชัดว่า: B ยังไม่ได้รับ request เลย, B รับแล้วกำลังประมวลผลอยู่ (ช้า), B ประมวลผลเสร็จแล้วแต่ response หายระหว่างทางกลับ, หรือ B ล่มไปแล้ว — เรียกความไม่แน่นอนนี้ว่า partial failure และเป็นสิ่งที่ไม่มีทางแยกแยะได้ 100% จากฝั่ง caller เพียงอย่างเดียว
ปรากฏการณ์ความหมาย
Latencyเวลาที่ request ใช้เดินทางไป-กลับ — ไม่คงที่ ผันผวนตามสภาพเครือข่ายและโหลดของปลายทาง (ดู intro-7 Performance เรื่อง RTT)
Packet lossแพ็กเก็ตหายระหว่างทาง (ดู Congestion Control) — TCP retransmit ให้อัตโนมัติ แต่ทำให้ latency สูงขึ้นแบบไม่แน่นอน
Partial failureบาง component ของระบบล้มเหลวขณะที่ส่วนอื่นยังทำงานปกติ — ต่างจาก failure ของโปรแกรมเดี่ยวที่มักเป็น all-or-nothing

2Connection Refused เทียบกับ Timeout

สอง error ที่หน้าตาคล้ายกันแต่ความหมายต่างกันโดยสิ้นเชิง — ต้องแยกให้ออกเพื่อออกแบบ retry logic ให้ถูกต้อง:

Connection RefusedTimeout
เกิดขึ้นเมื่อไรทันที — OS ปลายทางตอบ TCP RST กลับมาเพราะไม่มี process ใด listen() ที่ port นั้นไม่มีการตอบกลับเลยภายในเวลาที่กำหนด — อาจเพราะ packet หาย, เครื่องปลายทางไม่ตอบ, หรือ process ค้าง
สื่อความหมายว่าเครื่องปลายทางยังออนไลน์ แต่ไม่มี service รันอยู่ที่ port นั้น (service ตายหรือ config port ผิด)ไม่รู้อะไรเลยแน่ชัด — เครื่องอาจล่ม, เครือข่ายอาจขาด, หรือ service อาจแค่ทำงานช้ามาก
ระดับความมั่นใจว่า request ไปถึงหรือไม่มั่นใจว่าไม่ถึง (connection ก่อตั้งไม่สำเร็จเลยด้วยซ้ำ)ไม่มั่นใจเลย — นี่คือรากของปัญหา idempotency ในหัวข้อถัดไป
ทำไมแยกสองอย่างนี้สำคัญ
Connection refused ปลอดภัยที่จะ retry ทันที (รู้แน่ว่า request ไม่ถึงปลายทาง) — แต่ timeout ไม่ปลอดภัยเสมอไปที่จะ retry แบบเดิม เพราะ request อาจไปถึงและถูกประมวลผลไปแล้วจริงๆ เพียงแต่ response หายกลับมาไม่ถึง

3Timeout และ Retry

ทำไมต้อง Timeout เสมอ
Synchronous call (จาก หัวข้อที่ 2) ที่ไม่ตั้ง timeout จะบล็อกรอไม่มีกำหนดถ้าปลายทางไม่ตอบ — เหมือนปัญหา "server อาจรอไม่สิ้นสุด" ที่เจอใน หัวข้อที่ 1 แต่ตอนนี้เกิดในระดับ service-to-service และลาม (cascade) ไปยัง caller ของ caller ต่อเป็นทอดๆ ได้ถ้าไม่มีใครตั้ง timeout เลยตลอด chain

Retry คือการส่ง request ซ้ำเมื่อล้มเหลว — ฟังดูตรงไปตรงมา แต่มีอันตรายซ่อนอยู่ในหัวข้อถัดไป

4อันตรายจากการ Retry: Duplicate Request และ Idempotency

โจทย์สำคัญของหัวข้อนี้
Client ส่งคำสั่งซื้อ (POST /orders) แล้ว timeout — clientไม่รู้ว่า server บันทึกสำเร็จไปแล้วหรือยัง (ดูหัวข้อ 1: partial failure) ถ้า client ตัดสินใจ retry ส่ง POST /orders ซ้ำ แล้วครั้งแรกจริงๆ สำเร็จไปแล้ว (แค่ response หายตอนส่งกลับ) ผลคือออร์เดอร์ถูกสร้างซ้ำสองใบ — ลูกค้าโดนตัดเงินสองครั้ง
ตารางความเป็นไปได้เมื่อ Retry หลัง Timeout
กรณีที่ 1: Request ครั้งแรกไม่ถึง server เลย → retry ปลอดภัย 100%
กรณีที่ 2: Request ครั้งแรกถึง server, ประมวลผลสำเร็จ, แต่ response หาย → retry สร้างข้อมูลซ้ำถ้า operation ไม่ idempotent
กรณีที่ 3: Request ครั้งแรกถึง server แต่ยังประมวลผลไม่เสร็จตอน retry มาถึง → เสี่ยงประมวลผลซ้ำ/แข่งกัน (race)
Idempotency คือทางแก้
Operation เป็น idempotent เมื่อเรียกซ้ำกี่ครั้งก็ได้ผลลัพธ์เหมือนเรียกครั้งเดียว — PUT /keys/course (จากหัวข้อที่ 2) idempotent อยู่แล้วโดยธรรมชาติ (set ค่าซ้ำกี่ครั้งผลลัพธ์เหมือนเดิม) แต่ POST /orders (สร้างออร์เดอร์ใหม่ทุกครั้งที่เรียก) ไม่ idempotent โดยธรรมชาติ — ต้องออกแบบให้เป็น idempotent เพิ่มเติมถ้าต้องการ retry ได้อย่างปลอดภัย
เทคนิคทำให้ POST ปลอดภัยต่อการ Retry: Idempotency Key
POST /orders
Idempotency-Key: 8f14e45f-...   (client สุ่มสร้างครั้งเดียวต่อ "เจตนา" การสั่งซื้อหนึ่งครั้ง)
{"item": "laptop", "qty": 1}

ฝั่ง server เก็บ mapping Idempotency-Key → ผลลัพธ์ที่เคยตอบไปแล้ว — ถ้า request ที่มี key เดิมมาถึงซ้ำ (ไม่ว่าเพราะ client retry ตอน timeout จริง) server ไม่สร้างออร์เดอร์ใหม่ แต่ตอบผลลัพธ์เดิมกลับไปแทน ทำให้ operation ที่ไม่ idempotent โดยธรรมชาติ กลาย "ปลอดภัยต่อการ retry" ได้ในทางปฏิบัติ

ตอบโจทย์สำคัญของหัวข้อนี้
Client ส่งคำสั่งซื้อแล้ว timeout — วิธีแก้ที่ถูกต้อง: (1) client แนบ idempotency key เดียวกันทุกครั้งที่ retry คำขอเดียวกัน (2) server เก็บผลลัพธ์ของ key นั้นไว้ ถ้าเจอ key ซ้ำให้ตอบผลลัพธ์เดิมแทนที่จะสร้างออร์เดอร์ใหม่ (3) ถ้าออกแบบไม่ได้ ทางเลือกรองคือให้ client query สถานะออร์เดอร์ก่อน retry (เช่น GET /orders?ref=...) เพื่อเช็คว่าสำเร็จไปแล้วหรือยังก่อนตัดสินใจส่งซ้ำ

5Exponential Backoff

ทำไมไม่ควร Retry ทันทีซ้ำๆ
ถ้า service ปลายทางกำลังโอเวอร์โหลดอยู่แล้ว (สาเหตุของ timeout) การให้ client ทุกตัว retry ทันทีพร้อมกันจะยิ่งซ้ำเติมโหลด กลายเป็นวงจรที่ทำให้ปัญหาแย่ลง (คล้าย congestion collapse ที่เจอใน Congestion Control) — Exponential backoff แก้ด้วยการเว้นระยะเวลาระหว่าง retry ให้เพิ่มขึ้นแบบเอกซ์โปเนนเชียล
สูตร Exponential Backoff พร้อม Jitter
delay = min(base × 2ⁿ, max_delay)
actual_delay = random(0, delay) // jitter — สุ่มเพื่อไม่ให้ client หลายตัว retry พร้อมกันเป๊ะ
(n = จำนวนครั้งที่ retry แล้ว, base = ค่าเริ่มต้น เช่น 100ms, max_delay = เพดานสูงสุด)
Python: Retry พร้อม Exponential Backoff + Jitter
import time, random, requests

def call_with_retry(url, max_retries=5, base=0.1, max_delay=10):
    for attempt in range(max_retries):
        try:
            resp = requests.get(url, timeout=3)
            if resp.status_code < 500:
                return resp                  # สำเร็จ หรือ 4xx ที่ retry ไปก็ไม่ช่วย
        except requests.exceptions.RequestException:
            pass                              # timeout / connection refused → ลอง retry

        delay = min(base * (2 ** attempt), max_delay)
        time.sleep(random.uniform(0, delay))  # jitter
    raise TimeoutError("all retries exhausted")
ข้อควรระวัง
อย่า retry เมื่อได้ 4xx (client error) — request ผิดเอง (เช่น field หาย, unauthorized) ส่งซ้ำกี่ครั้งก็ยังผิดเหมือนเดิม เสียเวลาและเพิ่มโหลดโดยเปล่าประโยชน์ ควร retry เฉพาะ timeout, connection error, และ 5xx บางกรณี (เช่น 503 Service Unavailable) เท่านั้น

6Health Check

นิยาม
Health check คือ endpoint พิเศษ (เช่น GET /health) ที่บอกว่า service พร้อมรับ request หรือไม่ — ใช้โดย load balancer/orchestrator (เช่น Docker, Kubernetes) เพื่อตัดสินใจว่าจะส่ง traffic ไปยัง instance นั้นหรือไม่ และเพื่อ restart instance ที่ไม่ตอบสนอง
ประเภทตรวจอะไร
LivenessProcess ยัง "มีชีวิต" อยู่ไหม (ตอบ request ได้ไหม) — ถ้าไม่ผ่าน มักสั่ง restart container
ReadinessService พร้อมรับ traffic ใหม่ไหม (เช่น เชื่อมต่อ database สำเร็จแล้วหรือยัง) — ถ้าไม่ผ่าน ถอดออกจาก load balancer ชั่วคราวแต่ไม่ restart

Health check ทำงานร่วมกับ retry/backoff ของ caller: caller ที่ฉลาดจะเช็ค health ก่อนส่ง request จริง หรือใช้ผล health check ของ orchestrator เพื่อไม่ส่ง traffic ไปยัง instance ที่รู้อยู่แล้วว่าไม่พร้อม

7Log และ Correlation / Request ID

ปัญหาการ Debug ใน Distributed System
Request หนึ่งจาก external client อาจไหลผ่านหลาย service ต่อกัน (เช่น Service A → Service B → Service C) — ถ้าแต่ละ service log แยกกันโดยไม่มีอะไรเชื่อมโยง การหาสาเหตุว่า "request ไหนของ user คนไหนไปพังที่ไหน" แทบเป็นไปไม่ได้เมื่อระบบมี traffic พร้อมกันหลายพันคำขอ
Correlation ID / Request ID
สร้าง ID เฉพาะ (เช่น UUID) ที่จุดแรกที่ request เข้าระบบ แล้วส่งต่อ ID เดียวกันนี้ผ่าน header (เช่น X-Request-ID) ไปทุก service ที่เกี่ยวข้องในการประมวลผล request นั้น — ทุก log line ที่เกี่ยวข้องกับ request นี้ log ID เดียวกันกำกับไว้ ทำให้ค้นหา/เรียงลำดับเหตุการณ์ข้าม service ได้ทั้งหมดด้วยการกรองตาม ID เดียว
การส่งต่อ Correlation ID (Python)
import uuid, requests
from flask import Flask, request, g

app = Flask(__name__)

@app.before_request
def assign_request_id():
    g.request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))

@app.post("/orders")
def create_order():
    print(f"[{g.request_id}] received order request")
    # ส่งต่อ ID เดิมไปยัง service ถัดไปในสาย
    resp = requests.get(
        "http://service-b:5001/inventory/laptop",
        headers={"X-Request-ID": g.request_id},
        timeout=3,
    )
    print(f"[{g.request_id}] inventory check → {resp.status_code}")
    ...

8Docker Networking และ Service Name

ทำไมไม่ใช้ localhost หรือ IP ตายตัว
เมื่อรัน service หลายตัวเป็น container แยกกัน แต่ละ container มี network namespace ของตัวเอง — localhost ใน container ของ Service A จะไม่ชี้ไปที่ container ของ Service B (ต่างจากตอนรันบนเครื่องเดียวกันแบบ process ธรรมดา) และ IP address ของ container เปลี่ยนได้ทุกครั้งที่ restart

Docker (ผ่าน Docker Compose) แก้ปัญหานี้ด้วย service discovery ผ่านชื่อ: container ที่อยู่ใน network เดียวกันเรียกหากันได้ด้วยชื่อ service (ตามที่ตั้งไว้ใน docker-compose.yml) ราวกับเป็น hostname — Docker มี DNS ภายในที่ resolve ชื่อ service ไปเป็น IP ของ container นั้นให้อัตโนมัติ (สอดคล้องกับ DNS ที่เรียนไปแล้วใน Application Layer แต่ทำงานในสโคปแคบแค่ภายใน Docker network)

docker-compose.yml สำหรับระบบสองบริการ + Client
version: "3.9"
services:
  service-a:
    build: ./service_a
    ports:
      - "5000:5000"
    depends_on:
      - service-b
    environment:
      - INVENTORY_URL=http://service-b:5001   # ใช้ "service-b" แทน IP/localhost

  service-b:
    build: ./service_b
    ports:
      - "5001:5001"

  client:
    build: ./client
    depends_on:
      - service-a
สังเกตว่า Service A อ้างถึง Service B ด้วย http://service-b:5001 ตรงกับที่ใช้ไว้แล้วในโค้ด Service A ของหัวข้อที่ 2 พอดี

9Reverse Proxy และ Load Balancing (เบื้องต้น)

Reverse Proxy
Server กลางที่รับ request จาก client ภายนอกทั้งหมดก่อน แล้วค่อย forward ต่อไปยัง service จริงที่อยู่ด้านหลัง (เช่น Nginx) — ประโยชน์: ซ่อนโครงสร้างภายในจาก client ภายนอก, จัดการ TLS ที่จุดเดียว (ดู TLS/HTTPS), และเป็นจุดเดียวที่ทำ routing/load balancing
Load Balancing
เมื่อมี service เดียวกันรันหลาย instance พร้อมกัน (เพื่อรองรับโหลดสูง/เพิ่มความทนทาน) load balancer กระจาย request ไปยัง instance ต่างๆ ตามกลยุทธ์ เช่น round-robin (วนตามลำดับ) หรือ least-connections (ส่งไปยัง instance ที่มีงานค้างน้อยสุด) — ทำงานร่วมกับ health check (หัวข้อ 6) เพื่อไม่ส่ง traffic ไปยัง instance ที่ตอบว่า "ไม่พร้อม"
เชื่อมโยงกับหัวข้อก่อนหน้า
Load balancing เป็นไปได้ในทางปฏิบัติเพราะ service เป็น stateless (จาก หัวข้อที่ 2) — request จาก client เดิมไปลง instance ไหนก็ได้ผลลัพธ์เหมือนกัน ถ้า service เก็บ state ไว้ใน memory ของ instance ใดตัวหนึ่งเป็นการเฉพาะ (stateful) load balancing แบบนี้จะพังทันที

10Lab: Docker Compose + จำลอง Failure

ใช้ระบบสองบริการจากหัวข้อที่ 2 เป็นฐาน แล้วทำตามลำดับ:

  1. รัน client และสอง service ด้วย Docker Composedocker compose up ยืนยันว่า Service A เรียก Service B ผ่านชื่อ service-b สำเร็จตามปกติ
  2. ใส่ delay ให้ service หนึ่ง — เพิ่ม time.sleep(5) ใน Service B แล้วสังเกตว่า Service A ค้างรอ (ถ้าตั้ง timeout ไว้ 3 วินาที ควรเห็น timeout error ที่ Service A)
  3. ปิด service ระหว่างรับคำขอdocker compose stop service-b ขณะ Service A กำลังส่ง request สังเกตว่าได้ connection refused/reset แทน timeout (เทียบหัวข้อ 2)
  4. เพิ่ม timeout และ retry — ใช้โค้ด exponential backoff จากหัวข้อ 5 ใน Service A แล้วรัน Service B แบบ intermittent (ล่มๆ หายๆ) ดูว่า retry ช่วยให้ request สำเร็จในที่สุดได้จริงหรือไม่
  5. สังเกตว่า retry ทำให้รายการซ้ำหรือไม่ — เปลี่ยน endpoint ที่ retry จาก GET /inventory (idempotent โดยธรรมชาติ) เป็น POST /orders (ไม่ idempotent) แล้วนับจำนวน order ที่ถูกสร้างจริงเทียบกับจำนวนครั้งที่ user ตั้งใจสั่งซื้อ
  6. ปรับ operation ให้เป็น idempotent — เพิ่ม idempotency key ตามหัวข้อ 4 แล้วทดสอบซ้ำขั้นตอนที่ 5 ยืนยันว่าจำนวน order ที่สร้างจริงตรงกับจำนวนความตั้งใจของ user แล้ว ไม่ว่าจะ retry กี่ครั้งก็ตาม
ผลลัพธ์ที่ Lab นี้ควรพิสูจน์ให้เห็นด้วยตา
  • Timeout กับ connection refused เกิดจากสาเหตุคนละแบบ และดู log/behavior ต่างกันจริง (ขั้น 2 vs ขั้น 3)
  • Retry ที่ไร้เดียงสา (ไม่มี idempotency) สร้างข้อมูลซ้ำได้จริง ไม่ใช่แค่ทฤษฎี (ขั้น 5)
  • Idempotency key แก้ปัญหานั้นได้จริง วัดผลได้เป็นตัวเลข (ขั้น 6)
คำถามซ้อมสอบ
  1. อธิบาย partial failure คืออะไร และทำไมมันต่างจาก failure ของโปรแกรมตัวเดียวบนเครื่องเดียว
  2. เปรียบเทียบ connection refused กับ timeout ในแง่ความหมายและผลต่อการตัดสินใจ retry
  3. อธิบายว่าทำไม exponential backoff ถึงต้องมี jitter ด้วย ไม่ใช่แค่เพิ่ม delay แบบ deterministic
  4. ยกตัวอย่าง operation ที่ idempotent โดยธรรมชาติ และ operation ที่ไม่ใช่ พร้อมอธิบายวิธีทำให้ operation หลังปลอดภัยต่อการ retry
  5. อธิบายว่า correlation ID ช่วยแก้ปัญหาอะไรใน distributed system ที่ log ธรรมดาแก้ไม่ได้
  6. โจทย์: Client ส่งคำสั่งซื้อแล้ว timeout แต่ไม่รู้ว่า server บันทึกสำเร็จหรือยัง ถ้าส่งใหม่อาจเกิดอะไรขึ้น และจะแก้อย่างไร — ตอบโดยอ้างอิงแนวคิด idempotency key

11ชุดการทดลอง Failure และ Deployment

ความตั้งใจของแล็ป
Distributed system ไม่ได้ล้มแบบสวิตช์ไฟที่มีเพียงเปิดกับปิด บาง service ยังทำงาน บางเส้นทางช้า บางคำขอสำเร็จแต่คำตอบกลับไม่ถึง แล็ปนี้จึงฝึกวินิจฉัยจากอาการและหลักฐาน เปรียบเหมือนหมอตรวจคนไข้: ข้อความ error เป็นเพียงอาการ ส่วน log, metric, packet และ timeline คือผลตรวจที่ใช้หาสาเหตุ

การทดลองที่ 1 — สร้าง Baseline ก่อนทำให้พัง

  1. รันระบบสองบริการด้วย Docker Compose ตรวจ docker compose ps และเรียก happy path 20 ครั้ง
  2. บันทึก response time ต่ำสุด ค่ากลาง สูงสุด อัตราสำเร็จ และ request ID ถ้ายังไม่มี baseline จะบอกไม่ได้ว่าระบบช้าลงจริงหรือเพียงรู้สึกว่าช้า
  3. ตรวจว่า container คุยกันด้วย service name ไม่ใช่ localhost เพราะ localhost ภายใน container หมายถึง container ตัวนั้นเอง
  4. เก็บ config และ image tag ที่ใช้ เพื่อให้เพื่อนทำการทดลองซ้ำได้
docker compose up --build
docker compose ps
docker compose logs --tail=50 service-a service-b
curl -w "\nstatus=%{http_code} time=%{time_total}\n" \
  http://localhost:5000/orders/1

การทดลองที่ 2 — แผนที่อาการของ Failure

วิธีทำให้เกิดสิ่งที่คาดว่าจะเห็นหลักฐาน
หยุด process แต่ port ยังถูกอ้างถึงconnection refused เร็วexception และ TCP RST
ใช้ hostname ที่ไม่มีDNS resolution errorresolver error, ไม่มี TCP handshake
หน่วง service เกิน timeoutclient ยกเลิกการรอเวลาประมาณค่า timeout
คืน HTTP 500เชื่อมต่อสำเร็จแต่ application ล้มเหลวstatus, response body, server log
คืน JSON ผิด schemadownstream ตอบแต่ caller ใช้คำตอบไม่ได้parse/validation error

ให้ทำทุกแถวจริงและเขียน “ชั้นที่เกิดปัญหา” กำกับ การแยก DNS, TCP, TLS, HTTP และ application ออกจากกันช่วยไม่ให้เราแก้ผิดชั้น เช่น เพิ่ม retry ไม่ช่วยกรณี URL สะกดผิด

การทดลองที่ 3 — Timeout Budget ไม่ใช่ตัวเลขที่เดาเอา

  1. สร้างสาย Client → A → B ให้ client รอ 3 s, A รอ B 5 s แล้วสังเกตว่า A ยังทำงานต่อหลัง client เลิกรอหรือไม่
  2. ปรับให้ timeout ด้านในสั้นกว่าด้านนอก เผื่อเวลา serialize ส่งคำตอบ และ cleanup เช่น client 3 s, A→B 2 s
  3. ทดลอง B มี latency แกว่ง วาด histogram อย่างง่าย และเลือก timeout จากความต้องการของผู้ใช้กับ percentile ไม่ใช่ค่าเฉลี่ยอย่างเดียว
  4. แยก connect timeout กับ read timeout เพราะ “ต่อไม่ติด” และ “ต่อแล้วตอบช้า” บอกคนละปัญหา

การทดลองที่ 4 — Retry ที่ช่วยได้และ Retry ที่ทำร้ายระบบ

จำลอง B ล้มเหลว 50% แล้วเปรียบเทียบ 4 แบบ: ไม่ retry, retry ทันที 3 ครั้ง, exponential backoff, และ backoff พร้อม jitter เปิด client 20 ตัวพร้อมกันแล้วนับ request จริงที่ B ได้รับ

# แนวคิด ไม่ใช่สูตรตายตัว
delay = min(cap, base * (2 ** attempt))
sleep(random.uniform(0, delay))   # full jitter ลดการตื่นพร้อมกัน
สิ่งที่ต้องค้นพบ
Retry เหมือนกดโทรซ้ำเมื่อสายไม่ว่าง คนเดียวกดซ้ำอาจช่วย แต่ถ้าคนนับพันกดพร้อมกันชุมสายที่กำลังฟื้นจะล้มอีก ต้อง retry เฉพาะงานที่มีโอกาสสำเร็จในครั้งถัดไป มีเพดาน มี jitter และอยู่ในเวลารวมที่ผู้ใช้ยอมรับ

การทดลองที่ 5 — คำตอบหาย แต่ธุรกรรมอาจสำเร็จแล้ว

  1. ให้ B บันทึก order สำเร็จแล้วหน่วง/ตัด connection ก่อนตอบ A จากมุม A จะเหมือนล้มเหลว แต่ข้อมูลถูกเขียนแล้ว
  2. ให้ A retry และนับจำนวน order ที่เกิดซ้ำ จากนั้นเพิ่ม idempotency key กับ unique constraint
  3. ทดสอบ concurrent request ด้วย key เดียวกัน ถ้า check-then-insert แยกกันอาจยังแข่งกัน ต้องให้ฐานข้อมูลช่วยรับประกัน atomicity
  4. สรุปว่า exactly-once ที่ผู้ใช้เห็นมักสร้างจาก at-least-once delivery บวกการ deduplicate ไม่ได้เกิดจากการหวังว่าเครือข่ายจะไม่ทำคำตอบหาย

การทดลองที่ 6 — Liveness, Readiness และ Startup

สร้าง endpoint แยกกันสามตัว: liveness ตอบว่า process ยังเดิน, readiness ตรวจ dependency ที่จำเป็นต่อการรับงาน, startup บอกว่าการเริ่มต้นเสร็จหรือยัง จากนั้นตัด database แล้วตอบคำถามว่า orchestrator ควร restart process หรือเพียงหยุดส่ง traffic

Probeถ้าไม่ผ่านควรทำอะไรความผิดพลาดที่พบบ่อย
Livenessrestart เมื่อ process ค้างจริงผูกกับ database จนทุก container restart พร้อมกัน
Readinessถอดจาก load balancer ชั่วคราวตอบพร้อมก่อน warm-up เสร็จ
Startupให้เวลาเริ่มระบบใช้ liveness สั้นเกินจน service ถูกฆ่าระหว่างเริ่ม

การทดลองที่ 7 — Correlation ID และ Structured Log

  1. ให้ gateway รับหรือสร้าง request ID แล้วส่งต่อผ่าน header ทุก hop
  2. เขียน log แบบ JSON ที่มี timestamp, level, service, request_id, operation, duration_ms, status และ error_type
  3. ยิงคำขอพร้อมกัน 50 รายการ แล้วดึงเรื่องราวของคำขอหนึ่งรายการจากหลาย service ด้วย request ID เดียว
  4. ห้าม log password, token หรือข้อมูลส่วนตัวเต็ม ๆ และแยกข้อความสำหรับคนออกจาก field ที่เครื่องค้นได้

การทดลองที่ 8 — Deployment โดยผู้ใช้ไม่รู้สึก

รัน service สองรุ่นหลัง reverse proxy ให้ v1 ตอบชื่อรุ่นและ v2 มีการเปลี่ยนแปลงเล็กน้อย ยิง traffic ต่อเนื่องระหว่าง rolling update บันทึก error และ distribution ของ version จากนั้นทำให้ v2 readiness fail แล้วตรวจว่า traffic ยังไปถึงหรือไม่

สมุดบันทึกเหตุขัดข้องฉบับย่อ

เวลาอาการที่ผู้ใช้เห็นสมมติฐานหลักฐานที่ตรวจสาเหตุการป้องกัน
เช่น 10:15504 บางคำขอB ช้าA/B log, latency, tracepool เต็มlimit, timeout, scale

รายงานที่ดีต้องแยก timeline, impact, root cause, contributing factors และ action item การเขียนว่า “network มีปัญหา” กว้างเกินไป เพราะยังบอกไม่ได้ว่าผิดที่ DNS เส้นทาง TCP service หรือทรัพยากรของเครื่อง

ชิ้นงานที่ส่ง

12สรุปและขั้นตอนถัดไป

เป้าหมายไม่ใช่ทำให้ระบบ “ไม่ล้มเลย” เพราะทำไม่ได้ แต่คือทำให้ล้มในขอบเขตที่ควบคุมได้ รู้เร็ว อธิบายได้ และฟื้นได้โดยไม่สร้างความเสียหายซ้ำ ขั้นถัดไปคือเลือก pattern ให้ตรงกับแรงกดดันที่พบ ไม่ใช่ใส่ทุก pattern เพียงเพราะชื่อดูทันสมัย