ออกแบบ Retry Logic สำหรับ API ให้พร้อมใช้งานจริง
การเรียกใช้ API การชำระเงินล้มเหลวตอนตี 2 เพราะเครือข่ายขัดข้อง ติด rate limit หรือเซิร์ฟเวอร์ล่ม? คำตอบจะเป็นตัวตัดสินว่าการลองใหม่จะช่วยบันทึกรายการ หรือทำให้ลูกค้าถูกเรียกเก็บเงินซ้ำ
การลองใหม่ (retry) เป็น resilience pattern ที่ใช้บ่อยที่สุดใน distributed systems และเป็นสิ่งที่มักออกแบบผิดพลาดที่สุด การวนลูปเรียก HTTP ใหม่อาจดูเหมือนการเขียนโปรแกรมเชิงป้องกัน แต่หากไม่มี backoff และ jitter การหยุดทำงาน 30 วินาทีอาจกลายเป็น 30 นาที เพราะลูกค้านับพันรายส่งคำขอไปยังเซิร์ฟเวอร์ที่กำลังมีปัญหาพร้อมกัน
บทความนี้ครอบคลุมแนวทางที่ production systems ใช้จริง:
- สถานะใดควรลองใหม่และสถานะใดควร fail fast
- Exponential backoff พร้อม full jitter
- การรองรับ
Retry-After - Idempotency keys สำหรับคำขอ
POST - Retry budgets และ circuit breakers
- การจำลองข้อผิดพลาด
429และ503ด้วย mock server ของ Apidog
Retry logic ที่ไม่เคยทดสอบกับเซิร์ฟเวอร์ล้มเหลว ยังเป็นเพียงการคาดเดา ไม่ใช่การออกแบบ
ทำไม Naive Retries ทำให้การหยุดทำงานแย่ลง
สมมติว่าบริการรองรับ 1,000 คำขอต่อวินาที และสะดุดเป็นเวลา 5 วินาที หากลูกค้าทุกคนลองใหม่ทันทีคนละ 3 ครั้ง ทราฟฟิกจะเพิ่มจาก 1,000 เป็น 4,000 คำขอต่อวินาที และทั้งหมดพุ่งไปยังเซิร์ฟเวอร์ที่มีปัญหาอยู่แล้ว
รูปแบบนี้เรียกว่า retry storm ส่วนการที่ลูกค้าจำนวนมากกลับมาส่งคำขอพร้อมกันหลังเซิร์ฟเวอร์ฟื้นตัวเรียกว่า thundering herd โดย SRE ของ Google ระบุว่าการลองใหม่โดยไม่มี backoff สามารถเพิ่มภาระงานในช่วงที่ระบบรับไม่ไหว และทำให้บริการหยุดทำงานต่อแม้จะแก้ไขปัญหาเดิมแล้วก็ตาม
ข้อผิดพลาดในการออกแบบที่พบบ่อยมี 2 อย่าง:
- ไม่มีการหน่วงเวลา — ลองใหม่ทันทีและเพิ่มภาระงานในจังหวะที่แย่ที่สุด
- ใช้ fixed delay — หากลูกค้าทุกคนรอ 1 วินาทีเท่ากัน พวกเขาจะกลับมาพร้อมกันเป็นคลื่น
ทางแก้ไม่ใช่ “ห้ามลองใหม่” แต่คือการลองใหม่อย่างเลือกสรร ใช้ delay ที่เพิ่มขึ้นแบบสุ่ม และกำหนดขีดจำกัดภาระงานเพิ่มเติมไว้อย่างชัดเจน
ลองใหม่เฉพาะความล้มเหลวชั่วคราว
ก่อนคำนวณ backoff ให้กำหนด decision table ก่อน การลองใหม่คำขอที่เซิร์ฟเวอร์ปฏิเสธว่าไม่ถูกต้องจะสิ้นเปลืองทรัพยากรและทำให้ log มีสัญญาณรบกวน
สถานะที่ควรลองใหม่
| สัญญาณ | ความหมาย |
|---|---|
429 Too Many Requests |
ติด rate limit ควรถอยออกไปแล้วค่อยกลับมาใหม่ |
502 Bad Gateway |
Upstream hop ส่งข้อมูลผิดพลาด มักเป็นความล้มเหลวชั่วคราว |
503 Service Unavailable |
เซิร์ฟเวอร์โอเวอร์โหลดหรือกำลังรีสตาร์ต |
504 Gateway Timeout |
Upstream dependency ตอบช้าเกินไป |
| Connection reset, DNS error, socket timeout | คำขออาจยังไปไม่ถึงเซิร์ฟเวอร์ |
504 Gateway Timeout ต้องระวังเป็นพิเศษ เพราะต้นทางอาจประมวลผลคำขอเสร็จแล้ว แม้ gateway จะหยุดรอไปก่อน ความแตกต่างนี้สำคัญมากเมื่อพิจารณาเรื่อง idempotency
สถานะที่ควร fail fast
| สัญญาณ | ความหมาย |
|---|---|
400 Bad Request |
Payload ไม่ถูกต้อง และจะยังไม่ถูกต้องในการลองครั้งถัดไป |
401 Unauthorized |
Credential ไม่ถูกต้องหรือหมดอายุ ควรรีเฟรช token |
403 Forbidden |
ไม่มีสิทธิ์ การลองใหม่ไม่ได้เพิ่มสิทธิ์ |
422 Unprocessable Entity |
Validation ล้มเหลว ต้องแก้ข้อมูลก่อน |
กฎง่าย ๆ คือ:
ลองใหม่เมื่อปัญหาเกิดจากสถานะของเซิร์ฟเวอร์หรือเครือข่าย และ fail fast เมื่อปัญหาเกิดจากคำขอของเรา
429 อยู่ตรงกลาง: ลองใหม่ได้ แต่การเกิดซ้ำบ่งชี้ว่าควรแก้ rate limiting ด้วย client-side throttling หรือแคช ไม่ใช่เพิ่มจำนวน retry อย่างเดียว
Exponential Backoff และความสำคัญของ Jitter
Exponential backoff ทำให้การลองแต่ละครั้งรอนานขึ้นเป็นลำดับ:
delay = base * 2^retry_count
หาก base = 500 มิลลิวินาที จะได้ delay เป็น 0.5, 1, 2, 4 และ 8 วินาที ควรกำหนดเพดานเพื่อไม่ให้คำขอหนึ่งรายการรอนานหลายนาที:
delay = min(cap, base * 2^retry_count)
อย่างไรก็ตาม exponential backoff ธรรมดายังมีปัญหาการซิงโครไนซ์ หากไคลเอ็นต์ 5,000 รายล้มเหลวพร้อมกัน พวกเขาจะกลับมาที่ t=0.5s, t=1s และ t=2s พร้อมกันอยู่ดี
ใช้ Full Jitter
Jitter ทำลายการซิงโครไนซ์ด้วยการสุ่ม delay:
delay = random_between(0, min(cap, base * 2^retry_count))
Full jitter กระจายไคลเอ็นต์อย่างสม่ำเสมอทั่วช่วงเวลา จึงลด spike ของคำขอและช่วยให้เซิร์ฟเวอร์รับภาระได้ราบรื่นขึ้น การวิเคราะห์ของ AWS พบว่า full jitter ให้จำนวนคำขอรวมต่ำและเวลาเสร็จสิ้นสั้น เมื่อเทียบกับ plain backoff และ equal jitter
AWS ยังทดสอบ equal jitter และ decorrelated jitter ด้วย โดย full jitter และ decorrelated jitter ทำได้ดีกว่าในหลายกรณี แต่ full jitter เป็นวิธีที่ง่ายที่สุดในการเขียนให้ถูกต้อง จึงเหมาะเป็นค่าเริ่มต้น เว้นแต่ข้อมูลการวัดผลของระบบจะชี้ไปทางอื่น
เคารพ Retry-After
Backoff คือการคาดเดาของไคลเอ็นต์ แต่บางครั้งเซิร์ฟเวอร์จะบอกเวลาที่เหมาะสมมาให้โดยตรง:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
เมื่อมี Retry-After ให้ใช้ค่านี้แทน backoff ที่คำนวณเอง เพราะเซิร์ฟเวอร์รู้ว่า rate-limit window จะรีเซ็ตหรือการบำรุงรักษาจะเสร็จเมื่อใด
อย่างไรก็ดี ควรทำสิ่งต่อไปนี้เสมอ:
- ตรวจสอบและ parse ค่า header
- จำกัด delay ด้วย
capของไคลเอ็นต์ - จำกัดจำนวน retry สูงสุด
- ป้องกันค่าที่ผิดปกติ เช่น
Retry-After: 86400ไม่ให้ worker ค้างทั้งวัน
Idempotency: เงื่อนไขสำคัญของการ Retry POST
โดยทั่วไป GET, PUT และ DELETE เป็น idempotent ตามสัญญา การส่งซ้ำทำให้ระบบอยู่ในสถานะเดียวกัน แต่ POST ไม่จำเป็นต้องเป็นเช่นนั้น
หาก POST /v1/payments timeout หลังจากเซิร์ฟเวอร์ประมวลผลเสร็จแล้ว การ retry อาจสร้างการชำระเงินรายการที่สองได้
วิธีแก้คือ idempotency key ซึ่งเป็น ID ที่ไคลเอ็นต์สร้างขึ้นและส่งเป็น header:
Idempotency-Key: <unique-client-generated-id>
เซิร์ฟเวอร์จะจัดเก็บ key พร้อม response แรก และส่ง response เดิมกลับมาเมื่อพบคำขอซ้ำ แนวทางนี้ถูกใช้โดย Stripe และ API การชำระเงินหรือ provisioning จำนวนมาก
กฎสำคัญมี 2 ข้อ:
- การดำเนินการเดียวกันต้องใช้ key เดิม — retry ทุกครั้งของการชำระเงินเดียวกันต้องใช้ key เดิม ส่วนการดำเนินการใหม่ต้องสร้าง key ใหม่
- สร้าง key ก่อนส่งครั้งแรก — อย่าสร้างภายใน retry loop เพราะแต่ละครั้งจะถูกมองว่าเป็นการดำเนินการใหม่
หาก API ไม่มี idempotency key อย่า retry การเขียนที่ไม่ใช่ idempotent โดยอัตโนมัติ ให้ส่งความล้มเหลวกลับไป และให้มนุษย์หรือ reconciliation job เป็นผู้ตัดสินใจ
Retry Budgets และ Circuit Breakers
Backoff กำหนดว่า retry จะเกิดขึ้นเมื่อใด แต่ไม่ได้จำกัดภาระงานรวม หาก API gateway retry 3 ครั้ง และ service client retry อีก 3 ครั้ง การคลิกเพียงครั้งเดียวอาจกลายเป็น 9 คำขอ
Retry Budgets
แทนที่จะกำหนดเพียง “3 ครั้งต่อคำขอ” ให้กำหนดงบประมาณระดับระบบ เช่น:
Retry ต้องเพิ่มทราฟฟิกไม่เกิน 10% ภายใน sliding window
เมื่อใช้งบประมาณหมด คำขอใหม่จะถูกส่งกลับทันที วิธีนี้จำกัด retry amplification แม้มีคำขอล้มเหลวพร้อมกันจำนวนมาก โดย Linkerd และ Envoy รองรับแนวคิดนี้ในระดับการกำหนดค่า
Circuit Breakers
Circuit breaker ติดตามอัตราความล้มเหลวของแต่ละ downstream dependency เมื่อเกิน threshold เบรกเกอร์จะเปิดและทำให้คำขอล้มเหลวทันทีโดยไม่เรียกเครือข่าย
หลัง cooldown จะอนุญาตคำขอทดสอบจำนวนเล็กน้อย:
- หาก dependency ฟื้นตัว เบรกเกอร์จะปิด
- หากยังล้มเหลว เบรกเกอร์จะเปิดต่อ
Backoff ช่วยชะลอ retry อย่างสุภาพ ส่วน circuit breaker ช่วยหยุด retry เมื่อเห็นว่าปลายทางยังไม่พร้อม ระบบที่จริงจังควรใช้ทั้งสองอย่างร่วมกัน
ตัวอย่าง Python ที่พร้อมใช้ใน Production
ตัวอย่างนี้รวม pattern สำคัญทั้งหมด:
- กรองสถานะที่ retry ได้
- ใช้ full jitter
- รองรับ
Retry-After - ใช้ idempotency key
- จำกัดจำนวน retry อย่างแน่นอน
import random
import time
import uuid
import requests
RETRYABLE = {429, 502, 503, 504}
BASE = 0.5 # seconds
CAP = 30.0 # ceiling on any single delay
MAX_RETRIES = 5
def create_payment(payload):
idempotency_key = str(uuid.uuid4()) # one key per logical payment
headers = {"Idempotency-Key": idempotency_key}
for retry_count in range(MAX_RETRIES + 1):
try:
resp = requests.post(
"https://api.acmepay.com/v1/payments",
json=payload, headers=headers, timeout=10,
)
if resp.status_code < 400:
return resp.json()
if resp.status_code not in RETRYABLE:
resp.raise_for_status() # 400/401/403/422: fail fast
retry_after = resp.headers.get("Retry-After")
except (requests.ConnectionError, requests.Timeout):
retry_after = None # network fault: fall through to backoff
if retry_count == MAX_RETRIES:
raise RuntimeError("payment failed after all retries")
if retry_after and retry_after.isdigit():
delay = min(CAP, float(retry_after))
else:
delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
time.sleep(delay)
สิ่งที่ควรตรวจสอบ:
- Key ถูกสร้างครั้งเดียวและอยู่นอกลูป
-
Retry-Afterมีความสำคัญกว่า backoff ที่คำนวณเอง - Delay ยังอยู่ภายใต้
CAP - สถานะที่ retry ไม่ได้จะ fail fast
- จำนวน retry ถูกจำกัดด้วย
MAX_RETRIES
หากใช้ JavaScript ไลบรารี axios-retry ก็รองรับ pattern เดียวกันผ่าน retryCondition และ retryDelay แต่ decision table ยังคงต้องออกแบบเหมือนเดิม
ทดสอบ Retry Logic ก่อน Production
หลายทีมทดสอบเฉพาะ happy path และปล่อยให้เส้นทาง 503 ทำงานครั้งแรกระหว่าง outage จริง วิธีที่ดีกว่าคือจำลองความล้มเหลวไว้ตั้งแต่ต้น
1. จำลองความล้มเหลวด้วย Mock Server
Apidog Smart Mock ช่วยสร้าง endpoint เช่น /v1/payments และกำหนด response ได้ เช่น:
- คืน
503สองครั้งแรก แล้วคืน200ในครั้งที่สาม - คืน
429พร้อมRetry-After: 5 - เพิ่ม delay 15 วินาทีเพื่อทดสอบ timeout
ชี้ไคลเอ็นต์ไปยัง mock URL แล้วตรวจสอบว่า retry loop จัดการแต่ละสถานการณ์ได้ถูกต้อง โดยไม่ต้องรอ outage จริง
2. ยืนยันผลด้วย Test Scenarios
ใช้ test scenarios เพื่อเชื่อม request เข้ากับ assertions และการตรวจสอบเวลา ตรวจสอบอย่างน้อยว่า:
- คำขอสำเร็จในที่สุด
- เวลารวมอยู่ในช่วง backoff ที่คาดไว้
- มีการสร้าง resource เพียงรายการเดียว
- idempotency key ป้องกันการสร้างข้อมูลซ้ำ
- scenario ทำงานใน CI ทุกครั้งที่มี commit
นี่คือความแตกต่างระหว่าง “เราเพิ่ม retry แล้ว” กับ “เรายืนยันแล้วว่าไคลเอ็นต์รับมือกับ rate limit และ partial failure ได้จริง”
FAQ
ควร retry รหัส 429 หรือไม่?
ควร retry และ 429 เป็นสถานะที่เซิร์ฟเวอร์มักบอกแนวทางกลับมา อ่าน Retry-After และรออย่างน้อยตามค่าที่ระบุ หากไม่มี header ให้ใช้ exponential backoff พร้อม jitter
หาก 429 เกิดซ้ำ ให้แก้ปัญหาด้วย client-side throttling หรือแคช เพราะไม่ควรถือว่าเป็นพฤติกรรมปกติของระบบ
Full jitter คืออะไร?
Full jitter คือการสุ่ม delay อย่างสม่ำเสมอระหว่างศูนย์กับ exponential cap:
random(0, min(cap, base * 2^n))
ช่วยป้องกันคลื่น retry ที่ซิงโครไนซ์จากไคลเอ็นต์จำนวนมาก และจากการจำลองของ AWS ทำได้ดีกว่า plain backoff และ equal jitter ในด้านจำนวนคำขอรวมและเวลาเสร็จสิ้น
การ retry คำขอ POST ปลอดภัยหรือไม่?
ปลอดภัยเมื่อคำขอนั้น idempotent ในทางปฏิบัติ ซึ่งสำหรับ POST มักหมายถึงการส่ง idempotency key ที่เซิร์ฟเวอร์ใช้กำจัดคำขอซ้ำ
หากไม่มี key การ retry หลัง timeout อาจสร้างการชำระเงิน คำสั่งซื้อ หรือ record ซ้ำ เพราะเซิร์ฟเวอร์อาจประมวลผลคำขอไปแล้ว แม้ไคลเอ็นต์จะมองว่าเป็นความล้มเหลว
หลักการเดียวกันนี้ใช้กับ AI agents ที่เรียก write API ได้แก่:
- Keyed writes
- Capped retries
- Circuit breakers
ควร retry กี่ครั้ง?
โดยทั่วไป 3–5 ครั้งเพียงพอสำหรับความล้มเหลวชั่วคราวส่วนใหญ่ หลังจากนั้นอัตราความสำเร็จมักเพิ่มขึ้นน้อยมาก แต่ภาระงานและ latency ยังเพิ่มต่อไป
ควรจับคู่ per-request limit กับ retry budget ระดับระบบ เช่น จำกัดให้ retry เพิ่มทราฟฟิกไม่เกิน 10% หาก dependency ยังล้มเหลวหลัง retry ครั้งสุดท้าย นั่นควรเป็นหน้าที่ของ circuit breaker ไม่ใช่การเพิ่มจำนวน retry ต่อไป
ลิงก์อ้างอิง
- ตรรกะการลองใหม่สำหรับ API ด้าน FinTech
- [การจัดการความล้มเหล?utm_source=dev.to&utm_medium=wanda&utm_content=n8n-post-automation)
- การจำกัดอัตรา (Rate Limiting)
- การวิเคราะห์ Exponential Backoff และ Jitter
- Retry-After
- Idempotency Key
- คำขอ Idempotent ของ Stripe
- ดาวน์โหลด Apidog
- รูปแบบการกู้คืนข้อผิดพลาดของ Agent
Top comments (0)