Production Reliability, Security & Resiliency
11.3 Enterprise API Reliability & Security (Thai)
เจาะลึกสถาปัตยกรรมการออกแบบ API ระดับองค์กรที่ต้องรองรับโหลดมหาศาล พร้อมทั้งรับประกันความน่าเชื่อถือและความปลอดภัยสูงสุด
🖼️ Technical Architecture Diagram: API Security & Circuit Breaker
- Incoming requests pass through WAF and Rate Limiter.
- Service A calls Service B. Service B is struggling with high load.
- Circuit Breaker at Service A detects timeouts and trips (OPEN state).
- Subsequent requests instantly return a fallback response, preventing cascading failure.
🟢 Basic Level (ปูพื้นฐาน)
ถ้าปลั๊กไฟช็อต เบรกเกอร์จะตัดไฟทั้งบ้านเพื่อป้องกันไฟไหม้ Circuit Breaker ใน API ก็ตัดการเชื่อมต่อเพื่อกันเซิร์ฟเวอร์ล่ม
🟡 Intermediate Level (โค้ด/คอนฟิกจริง)
การใช้ไลบรารีอย่าง Resilience4j หรือ pybreaker กำหนด max_failures=5, timeout=2s, และ reset_timeout=30s
🔴 Professional Level (Under-the-hood & FinOps)
Distributed Circuit Breakers ผ่าน Service Mesh (Istio/Envoy) และลด Resource Exhaustion cost บน Cloud
🏢 Real-World Enterprise Scenario
แพลตฟอร์มเทรดหุ้นรับมือกับระบบ Payment Gateway ของธนาคารที่ล่ม โดยแสดงยอดเงินจำลองชั่วคราวแทนที่จะค้าง
1. Idempotency Keys (การป้องกันคำสั่งซ้ำด้วย Redis lock)
ทฤษฎีและกลไกการทำงาน (Theory & Mechanism)
Idempotency คือคุณสมบัติที่ทำให้การเรียก API ซ้ำกี่ครั้งก็ตามด้วยข้อมูลชุดเดิม จะได้ผลลัพธ์และการเปลี่ยนแปลงสถานะเหมือนการเรียกครั้งแรกเพียงครั้งเดียว (เช่น การตัดบัตรเครดิต) กลไกที่นิยมคือให้ Client ส่ง `Idempotency-Key` (มักเป็น UUID) มาใน Header ฝั่ง Server จะใช้ Redis `SETNX` (Set if Not eXists) เพื่อเก็บ Key นี้ หากเป็นการเรียกครั้งแรกจะผ่านไปได้และล็อค Key ไว้ หากมี Request อื่นที่ใช้ Key เดียวกันตามมาจะถูกปฏิเสธหรือคืนผลลัพธ์เดิมกลับไป
ตัวอย่างโค้ดหรือการตั้งค่า (Code/Config Example)
import redis
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
redis_client = redis.Redis(host='localhost', port=6379, db=0)
@app.post("/payments/")
async def process_payment(payment_data: dict, idempotency_key: str = Header(...)):
# 1. พยายามเซ็ตคีย์ใน Redis โดยมีอายุ 24 ชั่วโมง (86400 วินาที)
is_new_request = redis_client.set(f"idemp:{idempotency_key}", "processing", nx=True, ex=86400)
if not is_new_request:
# คีย์นี้มีอยู่แล้ว แปลว่าถูกเรียกซ้ำ
raise HTTPException(status_code=409, detail="Idempotency key already exists. Request is being processed or completed.")
try:
# 2. ประมวลผลการจ่ายเงินจริง
result = do_actual_payment(payment_data)
# 3. อัปเดตผลลัพธ์
redis_client.set(f"idemp:{idempotency_key}", str(result), ex=86400)
return {"status": "success", "result": result}
except Exception as e:
# หากล้มเหลว ให้ลบคีย์ทิ้งเพื่อให้ลองใหม่ได้
redis_client.delete(f"idemp:{idempotency_key}")
raise e
Use Case ในชีวิตจริง (Real-world Scenario)
แอปพลิเคชัน E-commerce ช่วง Flash Sale ลูกค้ากดปุ่ม "ชำระเงิน" รัวๆ เพราะเน็ตช้าหรือแอปค้าง หากไม่มี Idempotency บัตรเครดิตลูกค้าอาจถูกตัด 5 ครั้งตามจำนวนที่กด การใช้ `Idempotency-Key` ร่วมกับ Redis ช่วยรับประกันว่าจะมีการตัดเงินเพียงครั้งเดียวเท่านั้น
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations)
Pitfall: Client ส่ง Idempotency Key เดียวกันแต่ Body ไม่เหมือนเดิม หรือ Server คืน 500 แต่ไม่ได้ปลดล็อค ทำให้ Client ลองใหม่ด้วย Key เดิมไม่ได้
Mitigation: ควรมีการ Hash Request Body เก็บไว้เทียบกับตอนที่ส่งมาครั้งแรก และควรครอบ Exception เพื่อ Release Lock หรือมีระบบ Background Cleanup ให้สอดคล้องกับสถานะจริงใน Database
2. Cursor Pagination vs Offset Pagination
ทฤษฎีและกลไกการทำงาน (Theory & Mechanism)
Offset Pagination ใช้ `LIMIT X OFFSET Y` ฐานข้อมูลต้องกวาดข้อมูลตั้งแต่แถวแรกจนถึง OFFSET แล้วจึงตัดทิ้ง ทำให้ช้ามากเมื่อ OFFSET มีค่าสูง (Deep Paging) แถมยังมีปัญหาข้อมูลซ้ำ/หาย หากมีข้อมูลถูกแทรกหรือลบระหว่างเปลี่ยนหน้า
Cursor Pagination ใช้หลักการจำตำแหน่งล่าสุด (Cursor) ซึ่งมักเป็น Column ที่ Sort ไว้ (เช่น ID หรือ Timestamp) ควบคู่กับเงื่อนไข `WHERE id > :cursor LIMIT X` ทำให้ DB สามารถใช้ Index ข้ามไปยังตำแหน่งนั้นได้ทันที ทำงานได้เร็วคงที่ไม่ว่าข้อมูลจะเยอะแค่ไหน
ตัวอย่างโค้ดหรือการตั้งค่า (Code/Config Example)
-- แบบ Offset (ช้าเมื่อหน้าลึกๆ)
SELECT id, title, created_at
FROM posts
ORDER BY id DESC
LIMIT 20 OFFSET 100000;
-- แบบ Cursor (เร็วเสมอ)
-- สมมติว่า Cursor ที่ Client ส่งมาคือ next_cursor = 854039
SELECT id, title, created_at
FROM posts
WHERE id < 854039
ORDER BY id DESC
LIMIT 20;
Use Case ในชีวิตจริง (Real-world Scenario)
News Feed ของ Facebook หรือ Twitter ที่มีระบบ Infinite Scroll การดึง Feed ใช้ Cursor Pagination (มักเข้ารหัสแบบ Base64 เป็น Opaque Token) เพื่อป้องกันปัญหาคนโพสต์ข้อมูลแทรกแล้วทำให้ผู้ใช้เห็น Feed ซ้ำซ้อนตอนเลื่อนลง
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations)
Pitfall: Cursor ทำระบบ "กระโดดข้ามไปหน้า 50" ได้ยาก และ Column ที่ใช้ทำ Cursor ต้อง Unique เสมอ หากใช้ Timestamp แล้วมีข้อมูลที่ Timestamp เท่ากันเป๊ะ อาจทำให้ตกหล่นได้
Mitigation: ใช้ Multi-column cursor (เช่น `ORDER BY created_at DESC, id DESC`) และแนะนำให้ใช้ Cursor Pagination ร่วมกับ Infinite Scroll ไม่ใช่ UI แบบตัวเลขบอกหน้า
3. Circuit Breakers (Closed / Open / Half-Open)
ทฤษฎีและกลไกการทำงาน (Theory & Mechanism)
Circuit Breaker คือสถาปัตยกรรมป้องกันความล้มเหลวแบบลูกโซ่ (Cascading Failures) เมื่อเราเรียก External Service แล้วช้าหรือพัง เราไม่ควรฝืนเรียกต่อจน Resource เราหมดตาม กลไกมี 3 สถานะ: 1) Closed (ปกติ): คำสั่งส่งผ่านปกติ 2) Open (ตัดวงจร): หาก Error เกิน Threshold จะสับสวิตช์ คำสั่งใหม่จะถูก Reject ทันทีโดยไม่ต้องรอ Timeout (Fast Failure) 3) Half-Open (ทดสอบ): หลังผ่านไประยะหนึ่ง จะลองปล่อย Request ผ่านไปจำนวนน้อยๆ หากสำเร็จจะกลับไป Closed หากยังพังจะกลับไป Open
ตัวอย่างโค้ดหรือการตั้งค่า (Code/Config Example)
import pybreaker
import requests
# พัง 3 ครั้งติดกันให้เปิด Circuit, รอ 60 วินาทีก่อนลอง Half-Open
api_breaker = pybreaker.CircuitBreaker(fail_max=3, reset_timeout=60)
@api_breaker
def call_external_payment_gateway():
# หาก timeout หรือ error นับเป็น Failure
response = requests.post("https://slow-api.com/pay", timeout=2)
response.raise_for_status()
return response.json()
try:
result = call_external_payment_gateway()
except pybreaker.CircuitBreakerError:
# Circuit Open อยู่ ให้ทำงานแบบ Fallback ทันที
return {"status": "delayed", "message": "Payment system busy, processing later"}
Use Case ในชีวิตจริง (Real-world Scenario)
แอปสั่งอาหาร ส่ง Request ไปหาระบบตัดเงินของธนาคารตอนเที่ยงแล้วธนาคารล่ม หากไม่มี Circuit Breaker, Request ของผู้ใช้จะค้างรอ (Timeout) จน Thread/Connection ของแอปสั่งอาหารเต็มและล่มตามกันทั้งหมด Circuit Breaker จะตัดไฟ ทำให้แอปยังทำงานส่วนอื่นต่อได้ (เช่น โชว์หน้า Fallback ว่าให้เก็บเงินปลายทางแทน)
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations)
Pitfall: การตั้งค่า Threshold ต่ำเกินไปทำให้ Circuit เปิดบ่อยแม้เป็นแค่เน็ตเวิร์คกระตุก หรือตั้ง Timeout ผิดพลาดทำให้ Circuit ไม่เคยตัด
Mitigation: ต้องทำ Load Test เพื่อหาค่า Error Rate และ Response Time ที่เหมาะสม ควบคู่ไปกับการตั้ง Fallback (เช่น ส่งต่อเข้า Message Queue) อย่างเป็นระบบ
4. OWASP API Top 10 & PDPA/GDPR Masking
ทฤษฎีและกลไกการทำงาน (Theory & Mechanism)
ช่องโหว่ API ที่อันตรายสุดคือ BOLA (Broken Object Level Authorization) (การเข้าถึงข้อมูลคนอื่นโดยเปลี่ยน ID), BFLA (Broken Function Level Auth) (User ธรรมดาเรียก API Admin) และ Mass Assignment (User แอบส่งฟิลด์ is_admin=true เข้ามาอัปเดต) รวมถึงการคุ้มครองข้อมูลตามกฎหมาย PDPA/GDPR ด้วยการทำ Data Masking เพื่อปิดบังข้อมูลส่วนบุคคลไม่ให้หลุดผ่าน API โดยไม่ได้ตั้งใจ
ตัวอย่างโค้ดหรือการตั้งค่า (Code/Config Example)
# ตัวอย่างป้องกัน BOLA และ Mass Assignment
@app.put("/users/{user_id}/profile")
async def update_profile(user_id: int, request: ProfileUpdateRequest, current_user = Depends(get_current_user)):
# 1. BOLA Prevention: เช็คว่า ID ที่แก้ไข เป็นของตัวเองหรือเปล่า
if current_user.id != user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail="Forbidden")
# 2. Mass Assignment Prevention: รับเฉพาะฟิลด์ที่อนุญาตผ่าน Pydantic Model (ไม่รับ is_admin)
update_data = request.dict(exclude_unset=True) # ProfileUpdateRequest จะมีแค่ name, bio
# 3. Data Masking (PDPA)
masked_phone = mask_phone_number(current_user.phone) # '081-xxx-1234'
return {"status": "success", "phone": masked_phone}
Use Case ในชีวิตจริง (Real-world Scenario)
แอปพลิเคชันเรียก `/api/receipts/555` เพื่อดูใบเสร็จ แฮกเกอร์ลองเปลี่ยนตัวเลขเป็น `556` หากไม่มีการตรวจสอบ Authorization ที่ระดับ Object (BOLA) แฮกเกอร์จะเห็นข้อมูลใบเสร็จคนอื่นทั้งหมด
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations)
Pitfall: การเช็คสิทธิ์แบบกระจัดกระจาย ลืมเช็คบาง API หรือ Masking ข้อมูลช้าเกินไปจนหลุดไปใน Error Logs
Mitigation: ใช้ Middleware ในการตรวจจับ BOLA แบบรวมศูนย์ (เช่น ควบคุมผ่าน ORM Row-level Security) และทำการ Mask ข้อมูลสำคัญที่ชั้น Serialization ทันที
🛠️ Weekend Sandbox Challenge
โจทย์: สร้างระบบเติมเงิน (Wallet Top-up) ที่ไม่มีทางเติมเบิ้ล
- ใช้ FastAPI และ Redis จำลอง API `POST /topup`
- รับ Header `X-Idempotency-Key`
- ใส่ `time.sleep(2)` เพื่อจำลองว่าการทำงานช้า
- ลองยิง Request รัวๆ 5 ครั้งพร้อมกันด้วย cURL หรือ Postman
- ต้องเห็นผลลัพธ์ว่ามียอดเงินเข้าแค่ครั้งเดียว และ Request อื่นถูก Reject อย่างรวดเร็ว
💡 Senior Technical Interview Q&A
Q: หากเราใช้ Circuit Breaker ร่วมกับ Microservices 10 ตัวที่มี dependency กัน ถ้าตัวท้ายสุดล่ม จะเกิดอะไรขึ้นกับตัวอื่นๆ?
A: หากไม่มีการตั้ง Fallback หรือ Timeout ที่เหมาะสม จะเกิด Thread Exhaustion ขึ้นแบบโดมิโนย้อนกลับไปถึง Gateway แต่ถ้าแต่ละตัวมี Circuit Breaker ที่ตั้งค่าไว้อย่างถูกต้อง ตัวที่เรียก Service ที่พังจะเปิด Circuit และส่ง Error / Fallback กลับทันที ป้องกันไม่ให้คิวสะสม และรักษา Overall System Stability ไว้ได้
Q: ทำไม Cursor Pagination ถึงใช้กับคำสั่ง Sort แบบซับซ้อน เช่น Sort ตามคะแนนความนิยม (Popularity) ได้ยาก?
A: เพราะคะแนนความนิยมมีการเปลี่ยนแปลงได้ตลอดเวลา ไม่ใช่ค่าที่เป็น Monotonically Increasing เหมือน Timestamp หรือ ID หากเรียงตามคะแนนความนิยม ระหว่างที่เลื่อนหน้าลงมา ข้อมูลด้านบนอาจถูกลดคะแนนและร่วงมาอยู่ด้านล่าง ทำให้ Cursor ผิดเพี้ยน ต้องแก้ปัญหาโดยการ Cache Snapshot ข้อมูลทั้งชุด หรือใช้ Hybrid Approach