นโยบายการลองใหม่ — คือชุดของกฎที่กำหนดว่าแอปพลิเคชันมือถือจะลองเรียกเครือข่ายที่ล้มเหลวอีกครั้งโดยอัตโนมัติเมื่อใดและอย่างไร เมื่อการเชื่อมต่อไม่เสถียรหรือเกิดข้อผิดพลาดชั่วคราวของเซิร์ฟเวอร์ นโยบายการลองใหม่ที่ออกแบบมาอย่างดีจะช่วยเพิ่มความน่าเชื่อถือของแอปพลิเคชันโดยไม่ต้องให้ผู้ใช้ดำเนินการ ตามการวิจัยของ Google Developer Relations (2025) การใช้นโยบายการลองใหม่อย่างถูกต้องจะลดเปอร์เซ็นต์ของคำขอที่สูญหายลง 40–60% ในแอปพลิเคชันมือถือที่มีการดำเนินการเครือข่ายบ่อยครั้ง
ประเด็นสำคัญ
นโยบายการลองใหม่ — คือกลยุทธ์ซอฟต์แวร์ที่กำหนดพฤติกรรมของไคลเอนต์เมื่อคำขอเครือข่ายล้มเหลว: ข้อผิดพลาดใดที่ควรลองใหม่ กี่ครั้ง ด้วยความล่าช้าเท่าใด และเมื่อใดควรหยุดลอง ในแอปพลิเคชันมือถือ นโยบายการลองใหม่มีความสำคัญอย่างยิ่งเนื่องจากความไม่เสถียรของเครือข่ายมือถือและความเป็นไปได้ของความล้มเหลวชั่วคราวฝั่งเซิร์ฟเวอร์
นโยบายการลองใหม่พื้นฐานประกอบด้วยสามพารามิเตอร์: จำนวนการลองใหม่สูงสุด (maxRetries), ความล่าช้าเริ่มต้น (baseDelay) และกลยุทธ์ backoff นอกจากนี้ สามารถระบุรายการรหัสสถานะ HTTP ที่ควรกระตุ้นให้ลองใหม่ และหมดเวลาสำหรับยกเลิกการลองทั้งหมด
ตามหนังสือ “Designing Data-Intensive Applications” โดย Martin Kleppmann 50% ของความล้มเหลวในระบบกระจายเป็นแบบชั่วคราวและแก้ไขได้ด้วยการลองใหม่ สิ่งนี้ทำให้นโยบายการลองใหม่เป็นหนึ่งในวิธีที่มีประสิทธิภาพและถูกที่สุดในการปรับปรุงความทนทานต่อข้อผิดพลาดของแอปพลิเคชันมือถือโดยไม่ต้องเปลี่ยนแปลงสถาปัตยกรรมเซิร์ฟเวอร์
ข้อผิดพลาดชั่วคราว (Retriable) — เป็นความล้มเหลวประเภทเดียวที่นโยบายการลองใหม่ควรตอบสนอง ซึ่งรวมถึงการหมดเวลาการเชื่อมต่อ (SocketTimeoutException), การไม่พร้อมใช้งานชั่วคราวของเซิร์ฟเวอร์ (HTTP 503, 502) และข้อผิดพลาด DNS ข้อผิดพลาดถาวร — HTTP 400, 401, 403, 404 — ไม่ควรลองใหม่เพราะบ่งบอกถึงปัญหาในคำขอ ไม่ใช่เครือข่ายหรือเซิร์ฟเวอร์
ตาม AWS Architecture Blog การจำแนกข้อผิดพลาดอย่างถูกต้องเป็น Retriable และ Non-retriable เป็นการตัดสินใจที่สำคัญที่สุดในการออกแบบนโยบายการลองใหม่ การลองคำขอที่ไม่ใช่ idempotent ซ้ำด้วย HTTP 401 อาจทำให้บัญชีถูกล็อค และการลอง HTTP 400 ซ้ำอาจสร้างข้อมูลซ้ำซ้อน กำหนดค่ารายการรหัสสำหรับการลองใหม่อย่างชัดเจนเสมอ
ช่วงเวลาคงที่ — กลยุทธ์ที่ง่ายที่สุด: การลองใหม่แต่ละครั้งเกิดขึ้นหลังจากช่วงเวลาเดียวกัน ตัวอย่างเช่น ด้วยความล่าช้า 2 วินาที แอปพลิเคชันจะลองคำขอใหม่หลังจาก 2, 2, 2 วินาที ช่วงเวลาคงที่ง่ายต่อการนำไปใช้และคาดเดาได้ แต่สร้างภาระที่สม่ำเสมอบนเซิร์ฟเวอร์เมื่อเกิดความล้มเหลวจำนวนมาก
ช่วงเวลาเพิ่มขึ้น — ความล่าช้าเพิ่มขึ้นเป็นเส้นตรงในการลองใหม่แต่ละครั้ง: การลองใหม่ครั้งแรกหลังจาก 1 วินาที ครั้งที่สองหลังจาก 2 ครั้งที่สามหลังจาก 3 เป็นต้น กลยุทธ์นี้ให้เวลาเซิร์ฟเวอร์มากขึ้นในการกู้คืนเมื่อเกิดความล้มเหลวซ้ำ ๆ แต่ยังคงคาดเดาได้สำหรับไคลเอนต์จำนวนมากที่ล้มเหลวพร้อมกัน
| กลยุทธ์ | สูตรความล่าช้า | เวลาสะสม (3 ครั้ง) | การประยุกต์ใช้ |
|---|---|---|---|
| คงที่ | delay = D | 3 × D | สถานการณ์ง่าย, การหมดเวลาเฉพาะที่ |
| เพิ่มขึ้น | delay = N × D | 6 × D | การลดภาระแบบค่อยเป็นค่อยไป |
| Exponential | delay = D × 2^N | 7 × D | ความล้มเหลวจำนวนมาก, บริการคลาวด์ |
| Exponential + Jitter | delay = random(0, D × 2^N) | แปรผัน | ภาระสูง, ไมโครเซอร์วิส |
การเลือกกลยุทธ์ขึ้นอยู่กับลักษณะของแอปพลิเคชัน สำหรับงานเบื้องหลังของการซิงโครไนซ์ข้อมูลบนอุปกรณ์มือถือ กลยุทธ์ exponential กับ jitter เหมาะสมที่สุด — ให้ความน่าจะเป็นของความสำเร็จสูงสุดโดยมีภาระน้อยที่สุดบนเซิร์ฟเวอร์และอุปกรณ์ของผู้ใช้
Exponential backoff — กลยุทธ์ที่ความล่าช้าระหว่างการลองใหม่เพิ่มเป็นสองเท่าในการลองแต่ละครั้ง หากความล่าช้าเริ่มต้นคือ 1 วินาที ลำดับความล่าช้าจะเป็น 1, 2, 4, 8, 16 วินาที ซึ่งให้เวลาเซิร์ฟเวอร์ในการกู้คืนเพิ่มขึ้นแบบทวีคูณ
Jitter — การเบี่ยงเบนความล่าช้าแบบสุ่มที่ป้องกันคำขอลองใหม่พร้อมกันจากไคลเอนต์หลายตัว (ปัญหา thundering herd) หากไม่มี jitter ไคลเอนต์พันรายการที่มีนโยบายการลองใหม่เดียวกันจะลองคำขอพร้อมกัน สร้างภาระสูงสุดบนเซิร์ฟเวอร์ Jitter กระจายการลองใหม่ตามเวลา
Coroutines ของ Kotlin ช่วยให้นำ exponential backoff กับ jitter ไปใช้ได้โดยไม่บล็อกเธรดหลัก ฟังก์ชัน retry จาก kotlinx-coroutines รับเงื่อนไขการลองใหม่และบล็อกที่มีเนื้อหาคำขอ จัดการความล่าช้าและจำนวนครั้งโดยอัตโนมัติ
suspend fun RetryPolicy.executeWithRetry(
block: suspend () -> Result<T>
): Result<T> {
var lastError: Throwable? = null
repeat(maxRetries + 1) { attempt ->
try {
return block()
} catch (e: Exception) {
if (!isRetriable(e) || attempt == maxRetries) {
return Result.failure(e)
}
val delay = (baseDelayMs * (1 shl attempt))
.toLong()
val jitteredDelay = (delay * (0.5 + Random.nextDouble())).toLong()
delay(jitteredDelay)
lastError = e
}
}
return Result.failure(lastError!!)
}
ฟังก์ชัน executeWithRetry รับ lambda ที่มีการเรียกเครือข่ายและดำเนินการด้วย exponential backoff และ jitter หากข้อผิดพลาดไม่สามารถลองใหม่ได้หรือเกินจำนวนครั้งสูงสุด ฟังก์ชันจะส่งคืนข้อผิดพลาด ความล่าช้าจะถูกคูณด้วยปัจจัยสุ่มตั้งแต่ 0.5 ถึง 1.5 เพื่อการกระจายการลองใหม่อย่างสม่ำเสมอ
Circuit Breaker — รูปแบบการออกแบบที่ป้องกันคำขอลองใหม่ไม่รู้จบเมื่อบริการไม่พร้อมใช้งานเป็นเวลานาน เมื่อจำนวนข้อผิดพลาดเกินเกณฑ์ Circuit Breaker จะเปลี่ยนเป็นสถานะ OPEN และส่งคืนข้อผิดพลาดทันทีโดยไม่ดำเนินการตามคำขอ ให้เวลาเซิร์ฟเวอร์ในการกู้คืน
ในแอปพลิเคชันมือถือ Circuit Breaker มีประโยชน์อย่างยิ่งเมื่อ API ไม่พร้อมใช้งานเนื่องจากการบำรุงรักษาตามแผนหรือความล้มเหลวของเครือข่ายผู้ให้บริการ หากไม่มี Circuit Breaker แอปพลิเคชันจะสิ้นเปลืองแบตเตอรี่และการรับส่งข้อมูลในการลองใหม่ไม่รู้จบ ทำให้ประสบการณ์ผู้ใช้แย่ลงและลดอายุการใช้งานแบตเตอรี่ของอุปกรณ์
Circuit Breaker มีสามสถานะ: CLOSED (การทำงานปกติ, คำขอถูกดำเนินการ), OPEN (ความล้มเหลว, คำขอถูกบล็อก) และ HALF_OPEN (คำขอทดสอบเพื่อตรวจสอบการกู้คืน) หลังจากหมดเวลาที่กำหนดในสถานะ OPEN เบรกเกอร์จะเปลี่ยนเป็น HALF_OPEN และดำเนินการหนึ่งคำขอ — หากสำเร็จจะกลับไปที่ CLOSED หากล้มเหลวจะกลับไปที่ OPEN
class CircuitBreaker(
private val failureThreshold: Int = 3,
private val timeoutMs: Long = 30000
) {
private var state = State.CLOSED
private var failureCount = 0
private var lastFailureTime: Long = 0
suspend fun T.protect(block: suspend () -> T): T {
checkState()
return try {
val result = block()
onSuccess()
result
} catch (e: Exception) {
onFailure()
throw e
}
}
}
การนำ Circuit Breaker ไปใช้ใน Kotlin ประกอบด้วยตัวนับข้อผิดพลาดและตัวจับเวลาการกู้คืน เมธอด protect ตรวจสอบสถานะปัจจุบันก่อนดำเนินการตามคำขอที่ถูกห่อ และอัปเดตตัวนับข้อผิดพลาดเมื่อเกิดความล้มเหลว หลังจากถึง failureThreshold คำขอทั้งหมดจะถูกปฏิเสธทันทีจนกว่า timeoutMs จะหมดอายุ
เครือข่ายมือถือ มีลักษณะที่ทำให้นโยบายการลองใหม่สำคัญเป็นพิเศษ การสลับระหว่าง Wi-Fi และข้อมูลมือถือ การสูญเสียสัญญาณในรถไฟใต้ดินและอุโมงค์ การบล็อกชั่วคราวในระดับผู้ให้บริการ — สถานการณ์ทั้งหมดนี้นำไปสู่ความล้มเหลวของคำขอที่สามารถจัดการได้สำเร็จด้วยการลองใหม่
บน Android ไลบรารี Retrofit และ OkHttp มีกลไกการลองใหม่ในตัวผ่าน Interceptor บน iOS งานจะแก้ไขผ่าน URLSessionConfiguration และการมอบหมายแบบกำหนดเอง สำหรับการพัฒนาข้ามแพลตฟอร์ม Ktor (KMP) รวมถึงการรองรับการลองใหม่ในตัวพร้อมกลยุทธ์ที่กำหนดค่าได้
Combine — เฟรมเวิร์กของ Apple สำหรับการเขียนโปรแกรมเชิงรับ ตัวดำเนินการ retry ใน Combine จะทำซ้ำ publisher ตามจำนวนครั้งที่ระบุเมื่อเกิดข้อผิดพลาด แต่ไม่อนุญาตให้กำหนดค่าความล่าช้าระหว่างการลองใหม่ สำหรับนโยบายการลองใหม่ที่สมบูรณ์ จะใช้การรวมแบบกำหนดเองของ catch และ flatMap กับความล่าช้า
extension Publisher {
func retryWithBackoff(
retries: Int = 3,
baseDelay: TimeInterval = 1.0
) -> AnyPublisher<Output, Failure> {
return self.catch { error -> AnyPublisher in
guard retries > 0 else {
return Fail(error).eraseToAnyPublisher()
}
return Just(())
.delay(for: .seconds(baseDelay), scheduler: DispatchQueue.main)
.flatMap { self.retryWithBackoff(
retries: retries - 1,
baseDelay: baseDelay * 2
) }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
ส่วนขยาย retryWithBackoff สำหรับ Publisher ใน Combine นำ exponential backoff ไปใช้ผ่านการเรียกซ้ำโดยลดตัวนับและเพิ่มความล่าช้าเป็นสองเท่า ตัวดำเนินการ delay สร้างการหยุดชั่วคราวระหว่างการลองใหม่ ในขณะที่ catch ดักจับข้อผิดพลาดและตัดสินใจว่าจะลองใหม่หรือส่งคืนความล้มเหลว
ข้อผิดพลาดแรก — การลองคำขอซ้ำโดยไม่ตรวจสอบ idempotency หากเซิร์ฟเวอร์สร้างทรัพยากรแล้วแต่ไม่ส่งคืนการยืนยันเนื่องจากเครือข่ายล้มเหลว การลองใหม่จะสร้างข้อมูลซ้ำซ้อน สำหรับคำขอ POST ให้ใช้คีย์ idempotency (Idempotency-Key) ในส่วนหัวเสมอ หรือจำกัดการลองใหม่เฉพาะ GET, PUT และ DELETE
ข้อผิดพลาดที่สอง — การลองใหม่ไม่รู้จบ กำหนดจำนวนครั้งสูงสุดเสมอ (3–5 สำหรับแอปพลิเคชันมือถือ) และหมดเวลารวมสำหรับการลองทั้งหมด การลองใหม่ไม่รู้จบทำให้แบตเตอรี่หมดและสร้างภาระปรสิตบนเซิร์ฟเวอร์ โดยเฉพาะอย่างยิ่งระหว่างการย้ายฐานข้อมูลหรือการเปลี่ยนแปลง API
ข้อผิดพลาดที่สาม — การละเลยบริบทของแอปพลิเคชัน หากผู้ใช้ปิดแอปพลิเคชันหรือเปลี่ยนไปทำงานเบื้องหลัง นโยบายการลองใหม่ที่ทำงานอยู่ควรถูกยกเลิกอย่างถูกต้อง ใช้ coroutines กับ SupervisorScope หรือ Combine กับวงจรชีวิต UI เพื่อยกเลิกการลองใหม่โดยอัตโนมัติเมื่อปิดหน้าจอ
ข้อผิดพลาดที่สี่ — การไม่บันทึกการลองใหม่ หากไม่มีการบันทึก คุณจะไม่รู้ว่ามีคำขอจำนวนเท่าใดที่ถูกลองใหม่ ข้อผิดพลาดใดที่เกิดขึ้น และนโยบายการลองใหม่ของคุณมีประสิทธิภาพเพียงใด เพิ่มเมตริก: จำนวนการลองใหม่ ความสำเร็จหลังการลองใหม่ การกระจายความล่าช้า ข้อมูลนี้จะช่วยปรับพารามิเตอร์กลยุทธ์ที่เหมาะสมสำหรับแอปพลิเคชันเฉพาะของคุณ
คำถามที่พบบ่อย
จำนวนการลองใหม่ที่เหมาะสมคือ 3–5 ครั้งสำหรับสถานการณ์ส่วนใหญ่ สำหรับการซิงโครไนซ์เบื้องหลัง 5–7 ครั้งก็ยอมรับได้ สำหรับคำขอเชิงโต้ตอบ (เช่น การส่งแบบฟอร์ม) ไม่เกิน 3 ครั้ง การลองใหม่มากกว่าไม่ได้เพิ่มโอกาสสำเร็จแต่สิ้นเปลืองแบตเตอรี่และการรับส่งข้อมูลของผู้ใช้
Exponential backoff — คือการเพิ่มความล่าช้าระหว่างการลองใหม่เป็นสองเท่า: 1 วินาที, 2, 4, 8, 16 ไปเรื่อย ๆ หากเซิร์ฟเวอร์โอเวอร์โหลด การหยุดสั้น ๆ ระหว่างการลองครั้งแรกช่วยให้ตอบสนองได้เร็ว ในขณะที่การหยุดที่เพิ่มขึ้นในการลองครั้งต่อ ๆ ไปให้เวลาเซิร์ฟเวอร์มากขึ้นในการกู้คืน
ลองใหม่เฉพาะข้อผิดพลาดชั่วคราว: 408 (Request Timeout), 429 (Too Many Requests), 502 (Bad Gateway), 503 (Service Unavailable), 504 (Gateway Timeout) ข้อผิดพลาด 4xx (ยกเว้น 408 และ 429) บ่งบอกถึงปัญหาไคลเอนต์ — การลองใหม่นั้นไม่มีประโยชน์และอาจเป็นอันตรายต่อข้อมูลผู้ใช้
นโยบายการลองใหม่ จัดการการลองใหม่ของคำขอเดียวเมื่อเกิดความล้มเหลว Circuit Breaker จัดการสถานะการเชื่อมต่อกับบริการ: เมื่อข้อผิดพลาดสะสม จะเปิดวงจร (OPEN) และบล็อกคำขอใหม่ การลองใหม่ทำงานในระดับการเรียกแต่ละครั้ง Circuit Breaker ทำงานในระดับการรวมบริการ
สำหรับการทดสอบนโยบายการลองใหม่ ให้ใช้ NetworkInterceptor (OkHttp) บน Android และ URLProtocol (URLSession) บน iOS เพื่อจำลองความล้มเหลวของเครือข่าย กำหนดพารามิเตอร์: ความถี่ข้อผิดพลาด ระยะเวลาไม่พร้อมใช้งาน และรหัสตอบสนอง การทดสอบหน่วยกับ MockWebServer (OkHttp) หรือ OHHTTPStubs (iOS) ตรวจสอบตรรกะการลองใหม่โดยไม่มีเครือข่ายจริง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม