บทความ

ภาพที่คุ้นเคย: คำขอสั้นๆ วิ่งปรู๊ดปร๊าดเหมือนนกนางแอ่น แต่พอเริ่มอัปโหลดใหญ่ๆ หรือดาวน์โหลดไฟล์หนักๆ ผ่านโมบายพร็อกซี การเชื่อมต่อกลับขาดกลางคัน ไฟล์มาได้ 40 เปอร์เซ็นต์ แล้วก็เงียบไป การส่งคำขอใหม่บางทีก็ช่วย บางทีก็ไม่ และที่แย่สุดคือ log ไม่รายงานอะไร เซิร์ฟเวอร์ดูเหมือนยังทำงานอยู่ แต่ข้อมูลไปไม่ถึง ถ้าคุณเคยเจอแบบนี้ แสดงว่ามาถูกที่แล้ว

บทความนี้ไม่ได้พูดถึงรหัสตอบสนองหรือตรรกะการส่งซ้ำในระดับ HTTP (เกี่ยวกับ 429 และ exponential backoff มีบทความแยกที่เราจะกล่าวถึง) ที่นี่เราจะเจาะลึก ระดับการขนส่ง (transport layer): ทำไม TCP ถึงขาด ใครกันแน่ที่ตัดการเชื่อมต่อ ไทม์เอาต์สามแบบที่อยู่บนเส้นทางของแพ็กเก็ตทำงานอย่างไร MTU และการแตกแฟรกเมนต์ในเครือข่ายเซลลูลาร์คืออะไร และทำไมการเปลี่ยนสถานีฐานถึงฆ่าเซสชันยาวๆ และที่สำคัญ เราจะสร้างไคลเอนต์ที่รับมือกับทั้งหมดนี้ได้

บทนำ: การดาวน์โหลดหลุดกลางคัน แต่คำขอสั้นๆ ผ่านฉลุย

ทำไมหัวข้อนี้ถึงสำคัญมากในปี 2026? เพราะโมบายพร็อกซีกลายเป็นเครื่องมือทำงานจริงสำหรับการเก็บข้อมูล (parsing), ระบบอัตโนมัติ, การทดสอบ และการทำงานกับมาร์เก็ตเพลส เครือข่ายเซลลูลาร์โดยธรรมชาติไม่เสถียรเท่าเครือข่ายแบบมีสาย มันถูกออกแบบมาสำหรับคนถือโทรศัพท์ที่เปิดหน้าเว็บ อ่าน แล้วปิด มันไม่ได้ถูกออกแบบมาสำหรับเครื่องจักรที่เปิดการเชื่อมต่อ TCP เดียวค้างไว้นานสิบนาทีแล้วโหลดข้อมูลเป็นกิกะไบต์ผ่านมัน

นี่คือที่มาของความขัดแย้งที่พาดหัวในหัวข้อนี้ คำขอสั้นๆ ผ่านได้เพราะมันเสร็จทันก่อนที่ไทม์เอาต์ตัวใดจะทำงาน หรือก่อนเกิด handover (การสลับระหว่างสถานีฐาน) ส่วนการทำงานแบบยาวๆ อยู่ได้นานกว่า จึงยิ่งเพิ่มโอกาสเจอปัญหาแต่ละอย่าง: การค้างจนชนไทม์เอาต์, การขาดตอนเมื่อเปลี่ยนเซลล์, การสูญเสียแพ็กเก็ตขนาดใหญ่เพราะ MTU ผิด

สิ่งที่คุณจะได้รู้เมื่ออ่านจนจบ:

  • วิธีรู้ใน 5 นาทีว่า ใครกันแน่ ที่ตัดการเชื่อมต่อ: ไคลเอนต์ของคุณ, พร็อกซี, ผู้ให้บริการ หรือเซิร์ฟเวอร์ปลายทาง
  • ความแตกต่างเชิงหลักการระหว่าง TCP keepalive กับ HTTP keep-alive และทำไมต้องใช้ทั้งคู่
  • วิธีวัดไทม์เอาต์สามตัวบนเส้นทาง และหาตัวที่สั้นที่สุดซึ่งเป็นตัวชี้ชะตาการเชื่อมต่อ
  • ทำไม MTU ในเครือข่ายเซลลูลาร์ถึงต่ำกว่า 1500 ไบต์มาตรฐาน และ PMTUD พังมีหน้าตาเป็นอย่างไร (อาการ: ค้างเมื่อได้รับข้อมูลขนาดใหญ่)
  • โค้ดไคลเอนต์พร้อมใช้บน Python และ Node ที่รองรับการดาวน์โหลดต่อด้วย Range, การแบ่งอัปโหลด และไทม์เอาต์ที่ถูกต้อง

เราจะพูดภาษาทางเทคนิค แต่จะอธิบายทุกคำศัพท์แบบมนุษย์เข้าใจง่าย ไปลุยกันเลย

พื้นฐาน: การเชื่อมต่อผ่านโมบายพร็อกซีทำงานอย่างไร

ก่อนจะวิเคราะห์ปัญหา ขอตกลงเรื่องภาพรวมกันก่อน เมื่อคุณส่งคำขอผ่านโมบายพร็อกซี แพ็กเก็ตต้องเดินทางผ่านสายโซ่ยาว การเข้าใจสายโซ่นี้คือครึ่งหนึ่งของความสำเร็จในการวินิจฉัย

เส้นทางของแพ็กเก็ตจากไคลเอนต์ถึงเซิร์ฟเวอร์

ลองนึกถึงเส้นทางของพัสดุ มันผ่านขั้นตอนต่างๆ และแต่ละขั้นตอนอาจทำให้ดีเลย์หรือสูญหายได้:

  1. ไคลเอนต์ของคุณ - โปรแกรมที่ส่งคำขอ มีไทม์เอาต์และการตั้งค่า socket ของตัวเอง
  2. พร็อกซีเซิร์ฟเวอร์ - รับการเชื่อมต่อจากคุณ แล้วเปิดการเชื่อมต่อของตัวเองไปยังเป้าหมาย บ่อยครั้งเป็น TCP สองเส้นที่เชื่อมติดกัน
  3. โมเด็มมือถือและอินเทอร์เฟซวิทยุ - ช่วงที่แคบและดื้อรั้นที่สุด ที่นี่แพ็กเก็ตบินผ่านคลื่นวิทยุไปยังสถานีฐาน
  4. แกนเครือข่ายของผู้ให้บริการ - NAT, เกตเวย์, ระบบจัดลำดับความสำคัญของทราฟฟิก
  5. อินเทอร์เน็ตสาธารณะ - ช่องทางหลักไปยังดาต้าเซ็นเตอร์ของเป้าหมาย
  6. เซิร์ฟเวอร์ปลายทาง - จุดสุดท้ายที่มีขีดจำกัดเรื่องความยาวของการเชื่อมต่อของตัวเองเช่นกัน

ข้อคิดสำคัญ: การเชื่อมต่อผ่านพร็อกซีไม่ใช่ท่อเดียว แต่อย่างน้อยสองท่อ ไคลเอนต์ถือการเชื่อมต่อไปยังพร็อกซี พร็อกซีถือการเชื่อมต่อไปยังเป้าหมาย การขาดสามารถเกิดขึ้นได้ในทุกส่วน และอาการจะต่างกันออกไป

เซสชันคืออะไร และทำไมมันเปราะบาง

การเชื่อมต่อ TCP คือช่องทางเสมือนจริง ในทางกายภาพไม่มีสายไฟระหว่างคุณกับเซิร์ฟเวอร์ มีเพียงข้อตกลงระหว่างสองฝ่ายว่าจะแลกเปลี่ยนไบต์ที่มีหมายเลขกำกับกัน ตราบใดที่ทั้งสองฝ่ายยังจำหมายเลขและสถานะได้ การเชื่อมต่อก็ยังมีชีวิต ทันทีที่ฝ่ายหนึ่งลืมสถานะ (NAT รีบูต, ไทม์เอาต์หมด, เซลล์เปลี่ยน) การเชื่อมต่อก็ตายจริงๆ แม้อีกฝ่ายอาจจะยังไม่รู้ไปอีกนาน

นี่คือที่มาของ ครึ่งเปิด (half-open) อันแสนเจ้าเล่ห์ ฝ่ายหนึ่งคิดว่าช่องทางยังมีชีวิตและรอข้อมูล แต่อีกฝ่ายลืมไปนานแล้ว ไคลเอนต์ค้าง ไม่มีข้อผิดพลาดใดๆ เข้ามา เวลาผ่านไป คุ้นๆ ไหม?

สามระดับที่มักเกิดปัญหา

  • ระดับวิทยุ - การสูญเสียแพ็กเก็ต, ดีเลย์, การเปลี่ยนเซลล์ คือธรรมชาติของเครือข่ายเซลลูลาร์
  • ระดับเครือข่าย - NAT ไทม์เอาต์ของผู้ให้บริการ, MTU, การแตกแฟรกเมนต์
  • ระดับแอปพลิเคชัน - ไทม์เอาต์ของ HTTP เซิร์ฟเวอร์ทั้งพร็อกซีและเป้าหมาย, ขีดจำกัดขนาดของตัวตอบสนอง

ต่อไปเราจะไล่ดูแต่ละระดับ และเรียนรู้วิธีแยกแยะมันออกจากกัน

เจาะลึก: ใครบ้างที่ตัดการเชื่อมต่อได้ และจะรู้ได้ยังไงว่าเป็นใคร

นี่คือส่วนวินิจฉัยที่สำคัญที่สุด ตราบใดที่คุณยังไม่รู้ตัวการ คุณก็ยิงมั่วไปเรื่อย ข่าวดีก็คือ ผู้ร้ายแต่ละคนมีลายมือต่างกัน

ผู้ต้องสงสัยสี่ราย

1. ไคลเอนต์ของคุณ ผู้ร้ายที่พบบ่อยสุดและถูกมองข้ามที่สุด ไลบรารี HTTP มีค่าไทม์เอาต์ดีฟอลต์ที่คุณอาจไม่สังเกต เช่น ไทม์เอาต์รวมของคำขอ, ไทม์เอาต์การอ่าน socket, ไทม์เอาต์เมื่อไม่มีการเคลื่อนไหว ถ้าไคลเอนต์ของคุณตัดการเชื่อมต่อเอง ใน log จะมี error ประมาณ read timeout หรือ socket timeout พร้อมระบุโค้ดของคุณใน stack trace

2. พร็อกซีเซิร์ฟเวอร์ พร็อกซีมีขีดจำกัดของตัวเอง: อายุการใช้งานสูงสุดของการเชื่อมต่อ, idle timeout, ขนาดสูงสุดของคำตอบ เมื่อพร็อกซีตัดการเชื่อมต่อ คุณมักจะเจอ connection reset กะทันหัน หรือการปิดโดยไม่มีคำตอบกลางทางตอนส่งข้อมูล ถึงอย่างนั้น ping ไปยังพร็อกซียังผ่าน และคำขอสั้นๆ ยังทำงานได้

3. ผู้ให้บริการเครือข่าย จอมเงียบที่สุด ตาราง NAT ของผู้ให้บริการมีอายุของแต่ละเรคคอร์ด หากไม่มีการรับส่งข้อมูลผ่านการเชื่อมต่อเป็นเวลานาน ผู้ให้บริการจะลบเรคคอร์ดออกจาก NAT แล้วก็ไม่มีที่ให้ส่งแพ็กเก็ตถัดไป อาการคือ การเชื่อมต่อค้างตอนหยุดนิ่ง ไม่ใช่ตอนกำลังส่งข้อมูล และผู้ให้บริการก็เป็นตัวการเมื่อเปลี่ยนสถานีฐานด้วย

4. เซิร์ฟเวอร์ปลายทาง มีการตั้งค่า keep-alive และขีดจำกัดของตัวเอง เซิร์ฟเวอร์หลายตัวปิดการเชื่อมต่อหลังจาก N คำขอ หรือหลังจาก T วินาที อาการคือ เซิร์ฟเวอร์ส่ง header ว่า Connection: close หรือจบการเชื่อมต่ออย่างถูกต้อง (FIN) แทนที่จะขาดแบบรุนแรง (RST)

วินิจฉัยจากอาการ: ตารางลายมือ

มาดูสัญญาณเฉพาะของแต่ละแบบ เพื่อให้คุณระบุตัวการได้ภายในไม่กี่นาที

  • ขาดตอนตอนหยุดนิ่งเท่านั้น ตอนส่งข้อมูลทุกอย่างปกติ - เกือบแน่นอนว่าเป็น NAT ไทม์เอาต์ของผู้ให้บริการหรือ idle timeout ของพร็อกซี รักษาด้วยทราฟฟิก keepalive
  • ขาดตอนที่ปริมาณข้อมูลเท่าๆ กันเสมอ (เช่น ประมาณ 8 หรือ 10 เมกะไบต์) - ขีดจำกัดขนาดคำตอบของพร็อกซีหรือเซิร์ฟเวอร์ รักษาด้วยการแบ่งผ่าน Range
  • ขาดตอนที่เวลาประมาณเท่ากันเสมอ (เช่น 60 หรือ 300 วินาทีพอดี) - ฮาร์ดลิมิตความยาวการเชื่อมต่อ ให้หาไทม์เอาต์ที่สั้นที่สุด
  • ค้างเฉพาะคำตอบขนาดใหญ่ แต่คำตอบเล็กผ่าน - อาการคลาสสิกของ PMTUD พังและปัญหา MTU เต็มๆ มีหัวข้อใหญ่ด้านล่าง
  • ขาดสุ่มๆ ไม่เกี่ยวกับปริมาณหรือเวลา และเกิดบ่อยเมื่อเคลื่อนที่ - การเปลี่ยนสถานีฐาน, handover, สัญญาณวิทยุเสื่อม
  • RST ทันทีที่พยายามส่งข้อมูล - การเชื่อมต่อตายแล้วฝั่งใดฝั่งหนึ่ง (half-open) หรือพร็อกซีสับขาดอย่างจงใจ

เครื่องมือวินิจฉัยเบื้องต้น

เพื่อแยก FIN (การปิดอย่างสุภาพ) กับ RST (การสับขาดแบบหยาบๆ) และรู้ว่าขาดที่ส่วนไหน ให้ใช้:

  • การ log ในระดับ socket - บันทึกเวลาเกิดการขาดที่แน่นอน, จำนวนไบต์ที่ส่งได้, ประเภท exception
  • การวิเคราะห์ทราฟฟิก - เครื่องมือดักจับแพ็กเก็ตจะบอกว่าใครส่ง RST หรือ FIN ถ้า RST มาจาก address ของพร็อกซี แปลว่าพร็อกซีผิด ถ้าการเชื่อมต่อเงียบไปเฉยๆ โดยไม่มีแพ็กเก็ต แปลว่าตัวกลาง (ผู้ให้บริการ) ผิด
  • การวัดเปรียบเทียบ - ลองทำแบบเดียวกันผ่านการเชื่อมต่อแบบมีสายโดยตรง ถ้าผ่านสายนิ่งๆ แต่ผ่านโมบายพร็อกซีขาด ปัญหาอยู่ที่ส่วนโมบาย

ข้อสังเกตจากผู้เขียน: ใน 70 เปอร์เซ็นต์ของเคสที่เราเคยวิเคราะห์ ผู้ร้ายคือ NAT ไทม์เอาต์ของผู้ให้บริการ หรือ idle timeout ดีฟอลต์ ไม่ใช่พร็อกซีหรือเซิร์ฟเวอร์เลย ผู้คนมักโทษพร็อกซี แต่ทางแก้อยู่ที่การตั้งค่า socket ไม่กี่บรรทัด

Keep-alive และ idle timeout: ไทม์เอาต์สามตัวบนเส้นทาง ตัวที่สั้นที่สุดชนะ

นี่คือหลักการพื้นฐานที่ควรสักไว้ในใจ: บนเส้นทางของแพ็กเก็ตมีไทม์เอาต์ไร้กิจกรรมหลายตัวทำงานอิสระ และชะตากรรมของการเชื่อมต่อถูกตัดสินโดยตัวที่สั้นที่สุด เหมือนโซ่ที่ขาดที่จุดอ่อนที่สุด

ไทม์เอาต์อาศัยอยู่ที่ไหนบ้าง

  1. Idle timeout ของไคลเอนต์คุณ - โปรแกรมของคุณยอมรอข้อมูลนานแค่ไหนโดยไม่ได้รับอะไรเลย โดยดีฟอลต์ในไลบรารีต่างๆ ตั้งแต่ 30 วินาทีไปจนถึงไม่มีที่สิ้นสุด
  2. Idle timeout ของพร็อกซี - พร็อกซีเก็บการเชื่อมต่อที่ไม่ได้ใช้งานไว้นานแค่ไหน ค่าทั่วไปอยู่ที่ 60 ถึง 300 วินาที
  3. NAT ไทม์เอาต์ของผู้ให้บริการ - แกนเครือข่ายเก็บเรคคอร์ดการแปลที่อยู่ไว้นานแค่ไหนโดยไม่มีทราฟฟิก สำหรับ TCP มักอยู่ที่ 300-600 วินาที แต่สำหรับ UDP หรือช่วงชั่วโมงคับคั่งอาจเหลือ 30-60 วินาที
  4. Keep-alive ไทม์เอาต์ของเซิร์ฟเวอร์ปลายทาง - เซิร์ฟเวอร์เก็บการเชื่อมต่อระหว่างคำขอนานแค่ไหน มักอยู่ที่ 5-75 วินาที

สมมติว่าไคลเอนต์ยอมรอ 120 วินาที พร็อกซีตัดที่ 90 วินาที และผู้ให้บริการล้าง NAT ที่ 60 วินาที ใครชนะ? ผู้ให้บริการ การเชื่อมต่อจะตายที่วินาทีที่ 60 ของการนิ่ง และทั้งไคลเอนต์และพร็อกซีจะไม่รู้ตัวทันที

วิธีวัดไทม์เอาต์ที่สั้นที่สุด

วิธีง่ายและเชื่อถือได้ ตั้งการเชื่อมต่อ ส่งคำขอหนึ่งครั้ง จากนั้นเงียบและรอ จับเวลาจนกว่าจะขาด ทำซ้ำหลายรอบเพื่อตัดความบังเอิญออก

  1. เปิดการเชื่อมต่อผ่านพร็อกซีไปยังเซิร์ฟเวอร์ทดสอบที่รองรับ keep-alive
  2. ส่งคำขอสั้นๆ หนึ่งครั้ง แล้วรับคำตอบ
  3. อย่าปิดการเชื่อมต่อ เริ่มจับเวลา
  4. ตรวจเป็นระยะ (ทุกวินาที) ว่าการเชื่อมต่อยังมีชีวิตไหม โดยลองอ่านจาก socket ในโหมด non-blocking
  5. บันทึกช่วงเวลาที่มี RST, FIN หรือ socket อ่านไม่ได้

วัดสามครั้ง ถ้าขาดที่ประมาณ 60 วินาทีสม่ำเสมอ นั่นคือเพดานความนิ่งของคุณ ดังนั้นต้องส่งทราฟฟิก keepalive ถี่กว่าทุก 60 วินาที โดยเผื่อไว้เยอะๆ เช่นทุก 20-25 วินาที

กลยุทธ์เอาชนะ idle timeout

เมื่อไทม์เอาต์ที่สั้นที่สุดเป็นตัวชี้ขาด หน้าที่เราคือไม่ปล่อยให้การเชื่อมต่อนิ่งเกินขีดจำกัดนั้น มีสองแนวทาง:

  • เติมทราฟฟิกที่มีประโยชน์ลงในการเชื่อมต่อ - ตอนกำลังส่งข้อมูล idle ไทม์เอาต์จะไม่ทำงานเพราะไม่มีความนิ่ง ดังนั้นการดาวน์โหลดต่อเนื่องไม่ค่อยเจอปัญหา idle timeout แต่มักเจอเรื่องขีดจำกัดขนาดและเวลาอายุการเชื่อมต่อ
  • ส่ง keepalive probe ช่วงพัก - เมื่อไม่มีข้อมูลที่เป็นประโยชน์ (เช่น รอให้เซิร์ฟเวอร์สร้างรายงาน) ต้องประคองการเชื่อมต่อด้วยทราฟฟิกเทียม ตรงนี้แหละที่กลไก keep-alive สองแบบจะเข้ามามีบทบาท ซึ่งเราจะแยกพูดถึง

TCP keepalive กับ HTTP keep-alive: คนละกลไก ต้องใช้ทั้งคู่

ความสับสนครั้งใหญ่ในวงการเกิดจากชื่อที่คล้ายกัน TCP keepalive และ HTTP keep-alive เป็นคนละสิ่งกันโดยสิ้นเชิง ทำงานคนละระดับ และสำหรับโมบายไคลเอนต์ที่เสถียร ต้องใช้ทั้งคู่

HTTP keep-alive: การใช้ซ้ำการเชื่อมต่อ

HTTP keep-alive (หรือ persistent connection) คือการ ไม่เปิด TCP คอนเนกชันใหม่ทุกครั้งที่มีคำขอ แต่ใช้การเชื่อมต่อเดียวรองรับหลายคำขอต่อเนื่องกัน นี่คือระดับแอปพลิเคชัน

ทำไมเรื่องนี้ถึงสำคัญกับเครือข่ายโมบาย? เพราะการตั้ง TCP คอนเนกชันใหม่ผ่านเครือข่ายเซลลูลาร์แพงมาก ต้องทำ three-way handshake บวกกับ TLS handshake ถ้าใช้การเข้ารหัส บนช่องสัญญาณวิทยุที่มีดีเลย์สูง อาจกินเวลาหลายร้อยมิลลิวินาที การใช้ซ้ำการเชื่อมต่อจะช่วยประหยัดเวลานั้นในทุกคำขอถัดไป

แต่! HTTP keep-alive ไม่ได้ช่วยเรื่อง NAT ไทม์เอาต์ของผู้ให้บริการในช่วงพักเลย มันแค่ทำให้เปิดการเชื่อมต่อไว้สำหรับคำขอถัดไป แต่ตัวมันไม่ได้สร้างทราฟฟิกในช่วงระหว่างนั้น

TCP keepalive: ชีพจรในระดับขนส่ง

TCP keepalive คือกลไกของโปรโตคอล TCP เอง ระบบปฏิบัติการจะส่งแพ็กเก็ตบริการเปล่า (keepalive probe) เป็นระยะ เพื่อเช็คว่าอีกฝั่งยังมีชีวิตไหม และช่วยรีเฟรชเรคคอร์ดในตาราง NAT ระหว่างทาง นี่คือระดับเคอร์เนลของ OS

TCP keepalive คืออาวุธหลักของคุณในการสู้กับ NAT ไทม์เอาต์ของผู้ให้บริการและ idle ไทม์เอาต์ของโหนดกลาง ทุก probe คือทราฟฟิกที่รีเซ็ตตัวนับความนิ่งตลอดเส้นทาง

TCP keepalive มีพารามิเตอร์หลักสามตัว:

  • keepalive idle (หรือ keepalive time) - หลังนิ่งไปกี่วินาทีจึงเริ่มส่ง probe โดยดีฟอลต์ในระบบปฏิบัติการส่วนใหญ่คือ 7200 วินาที หรือสองชั่วโมง ซึ่งเป็นหายนะสำหรับเครือข่ายโมบาย
  • keepalive interval - ช่วงห่างระหว่าง probe เมื่อไม่มีการตอบกลับ
  • keepalive count (probes) - จำนวน probe ที่ไม่ตอบกลับติดต่อกันกี่ครั้งถึงจะถือว่าการเชื่อมต่อตาย

ค่าที่แนะนำสำหรับเครือข่ายโมบาย

ค่าดีฟอลต์สองชั่วโมงใช้ไม่ได้เลย ผู้ให้บริการล้าง NAT ก่อน probe แรกไปไกลแล้ว นี่คือค่าที่ใช้งานได้จริงและผ่านการพิสูจน์มาแล้ว:

  • keepalive idle: 15-25 วินาที - เริ่มส่ง probe หลังพักสั้นๆ เพื่อให้ทัน NAT ไทม์เอาต์ที่ใจร้ายที่สุดแน่นอน
  • keepalive interval: 10-15 วินาที - ถ้า probe ไม่ถึงก็ส่งซ้ำในช่วงสั้นๆ
  • keepalive count: 3-4 - หลัง probe ที่ไม่ตอบกลับ 3-4 ครั้ง ถือว่าเชื่อมต่อตาย แล้วตั้งค่าใหม่ทันที แทนที่จะค้างรอไปเรื่อย

ค่าชุดนี้ให้ประโยชน์สองต่อ: การเชื่อมต่อไม่ถูกผู้ให้บริการตัดตอนพัก และถ้ามันตายจริงๆ (เช่นจาก handover) ไคลเอนต์จะรู้ภายใน 45-70 วินาที แทนที่จะเป็นชั่วโมง การตรวจจับการเชื่อมต่อที่ตายได้ไวคือครึ่งหนึ่งของความเสถียร

ทำไมต้องใช้ทั้งคู่

อุปมาอุปไมย: HTTP keep-alive ก็เหมือนการจองห้องประชุมไว้ทั้งวันเพื่อไม่ต้องวิ่งไปจองใหม่ทุกครั้ง ส่วน TCP keepalive คือการแวะเข้าห้องประชุมเป็นระยะ เพื่อไม่ให้พนักงานทำความสะอาดคิดว่าห้องว่างแล้วล็อกห้อง จองไว้แต่ไม่เข้าไป - เขาจะล็อก เข้าไปแต่ต้องจองใหม่ทุกครั้ง - เสียเวลา ต้องใช้ทั้งคู่

MTU และการแตกแฟรกเมนต์: ทำไมแพ็กเก็ตในเครือข่ายเซลลูลาร์ถึงเล็กกว่า

มาถึงสาเหตุการค้างที่ถูกมองข้ามมากที่สุด ถ้าคุณมี คำตอบเล็กๆ ผ่าน แต่คำตอบใหญ่ค้างสนิท - โอกาส 90 เปอร์เซ็นต์ว่าปัญหาอยู่ที่ MTU และ PMTUD ที่พัง

MTU คืออะไร พูดแบบง่ายๆ

MTU (Maximum Transmission Unit) คือขนาดสูงสุดของแพ็กเก็ตเดียวที่ส่งเข้าสู่เครือข่ายได้โดยไม่ต้องแตก ในอีเทอร์เน็ตคลาสสิกคือ 1500 ไบต์ นึกภาพว่ามันคือความกว้างของประตู เฟอร์นิเจอร์ที่กว้างกว่าประตูต้องถอดแยกชิ้นเพื่อให้ผ่านเข้าไปได้

ปัญหาคือในเครือข่ายเซลลูลาร์ ค่า MTU ที่ใช้งานจริงมักต่ำกว่า 1500 สาเหตุคือ การห่อหุ้ม (encapsulation) ทราฟฟิกโมบายถูกห่อด้วยโปรโตคอลเลเยอร์เพิ่มเติมภายในเครือข่ายผู้ให้บริการ แต่ละชั้นเพิ่มไบต์ส่วนหัว และทำให้พื้นที่สำหรับข้อมูลจริงลดลง MTU ที่ใช้งานจริงในเครือข่ายโมบายมักอยู่ช่วง 1400-1480 ไบต์ และบางครั้งก็ต่ำกว่านั้น

PMTUD คืออะไร และทำไมมันพัง

PMTUD (Path MTU Discovery) คือกลไกหา MTU สูงสุดตลอดเส้นทางโดยอัตโนมัติ หลักการคือ ไคลเอนต์ส่งแพ็กเก็ตใหญ่โดยตั้งค่า Don't Fragment (ห้ามแตก) ไว้ ถ้าเจอโหนดที่มี MTU เล็กกว่าระหว่างทาง โหนดนั้นจะทิ้งแพ็กเก็ตแล้วส่ง ICMP กลับมาแบบ fragmentation needed พร้อมบอกขนาดที่ยอมรับได้ ไคลเอนต์ได้รับคำใบ้ก็จะลดขนาดแพ็กเก็ตลง

เป็นแผนการที่สวยงาม แต่มันยืนอยู่บนเงื่อนไขเปราะบางเดียว: ข้อความ ICMP ต้องเดินทางกลับมาถึงไคลเอนต์ได้ แต่ในความเป็นจริง หลายเครือข่าย ไฟร์วอลล์ และการตั้งค่าความปลอดภัยบล็อก ICMP ทั้งหมด และนี่คือจุดเกิดหายนะที่เรียกว่า PMTUD black hole (หลุมดำ)

PMTUD พังมีหน้าตาแบบไหน: กายวิภาคของการค้าง

สถานการณ์คลาสสิกและจำได้แม่น:

  1. ไคลเอนต์ตั้งการเชื่อมต่อ handshake ผ่านด้วยแพ็กเก็ตเล็กๆ ทุกอย่างดี
  2. ไคลเอนต์ส่งคำขอสั้นๆ แพ็กเก็ตเล็กผ่าน คำตอบกลับมา คำตอบเล็กๆ ใช้งานได้
  3. เซิร์ฟเวอร์เริ่มส่งคำตอบใหญ่เป็นแพ็กเก็ตเต็มขนาด 1500 ไบต์พร้อมตั้งค่า Don't Fragment
  4. ระหว่างทางเจอโหนดที่ MTU 1400 ทิ้งแพ็กเก็ตพวกนั้น เพราะมันใหญ่เกินและห้ามแตก
  5. โหนดส่ง ICMP fragmentation needed กลับมา แต่ ICMP นี้ถูกไฟร์วอลล์บล็อก ไม่ถึงผู้ส่ง
  6. เซิร์ฟเวอร์ไม่ได้รับคำใบ้ ยังคงส่งแพ็กเก็ตใหญ่เดิมไปเรื่อย แล้วก็ถูกทิ้งซ้ำ วนลูปไม่รู้จบ
  7. ผลลัพธ์: แพ็กเก็ตเล็ก (หัวข้อ, ACK) ผ่าน แต่ข้อมูลคำตอบไม่มา การเชื่อมต่อค้างสนิทกลางคำตอบใหญ่

นี่คือเหตุผลที่ คำขอสั้นๆ ผ่าน แต่คำตอบใหญ่ค้าง ไม่ใช่เรื่องลี้ลับ แต่คือหลุมดำ PMTUD และนี่อาจเป็นต้นตอผมหงอกที่พบบ่อยที่สุดของวิศวกรที่ทำงานกับทราฟฟิกโมบาย

วิธีรักษาปัญหา MTU

มีวิธีแก้หลายระดับ ตั้งแต่ระดับแอปไปจนถึงระดับเครือข่าย:

  • MSS clamping - วิธีที่น่าเชื่อถือที่สุดในระดับเครือข่าย MSS (Maximum Segment Size) คือขนาดสูงสุดของเซกเมนต์ TCP ที่ทั้งสองฝ่ายตกลงกันตอนเริ่มต้นของ handshake ถ้าจำกัด MSS ให้ชัดเจนเพื่อให้แพ็กเก็ตสุดท้ายพอดีกับ MTU จริง (เช่น MSS 1360-1400) เซิร์ฟเวอร์จะส่งแพ็กเก็ตขนาดที่เหมาะสมตั้งแต่แรก และไม่ต้องพึ่ง ICMP เลย นี่คือการรักษาหลุมดำที่ต้นเหตุ
  • ลด MTU ของอินเทอร์เฟซ - ถ้าคุณดูแลโมเด็มหรือเครื่องที่ทราฟฟิกไหลผ่าน ก็ตั้ง MTU ของอินเทอร์เฟซเป็น 1400 หรือต่ำกว่า แล้วแพ็กเก็ตขาออกทั้งหมดจะไม่ใหญ่เกินขนาดปลอดภัย
  • อนุญาตให้ ICMP ผ่าน - ถ้าควบคุมไฟร์วอลล์บนเส้นทางได้ ให้อนุญาต ICMP แบบ fragmentation needed แล้ว PMTUD มาตรฐานจะทำงานตามที่ออกแบบ
  • ระดับแอปพลิเคชัน - ถ้าดาวน์โหลดไฟล์ใหญ่ ให้แบ่งเป็นส่วนๆ ผ่าน Range request แต่ละส่วนคือการส่งสั้นๆ อิสระ ซึ่งมีโอกาสน้อยที่จะชนปัญหาของสตรีมขนาดใหญ่ รายละเอียดอยู่ในภาคปฏิบัติ

ข้อคิด: ต่อให้คุณไม่ได้ควบคุมเครือข่าย คุณก็เกือบจะปรับ MSS ได้เสมอผ่านการตั้งค่าการเชื่อมต่อหรือพารามิเตอร์ของบริการพร็อกซี ผู้ให้บริการโมบายพร็อกซีที่ดีจะตั้งค่า MSS clamping อย่างถูกต้องไว้แล้วบนฝั่งของเขา ช่วยคุณให้พ้นจากหลุมดำ นี่คือหนึ่งในเกณฑ์วัดคุณภาพบริการ

การเปลี่ยนสถานีฐานและการย้ายข้ามเครือข่าย: ทำไม TCP รับมือไม่ได้

มาถึงความเข้ากันไม่ได้ขั้นพื้นฐานที่สุด เครือข่ายเซลลูลาร์ถูกสร้างมาเพื่อความคล่องตัว แต่ TCP ไม่ใช่ และนี่คือความขัดแย้งระดับสถาปัตยกรรม

ทำไม TCP ถึงผูกติดกับที่อยู่

การเชื่อมต่อ TCP ถูกนิยามด้วยชุดสี่ค่า: ที่อยู่ต้นทาง, พอร์ตต้นทาง, ที่อยู่ปลายทาง, พอร์ตปลายทาง เหมือนที่อยู่ไปรษณีย์ของการเชื่อมต่อ ถ้าองค์ประกอบใดเปลี่ยนไป นั่นคือ การเชื่อมต่ออีกอัน และอันเก่าใช้ไม่ได้ทันที

แล้วเกิดอะไรขึ้นในเครือข่ายโมบาย? ในบางสถานการณ์ของการเปลี่ยนสถานีฐาน หรือการย้ายระหว่างเทคโนโลยี (เช่น ระหว่างรุ่นเครือข่ายต่างๆ หรือข้ามเครือข่าย) IP ภายนอกของโหนดโมบายอาจเปลี่ยนไป และตรงนี้ TCP ทำอะไรไม่ได้ มันไม่รู้จักวิธีสานต่อการเชื่อมต่อด้วยที่อยู่ใหม่ การเชื่อมต่อเดิมตายทันที ข้อมูลทั้งหมดที่อยู่ระหว่างทางหายไป

Handover: ไม่ได้อันตรายถึงตายเสมอไป แต่เสี่ยงเสมอ

พูดกันตามจริง เครือข่ายสมัยใหม่พยายามทำให้ handover ไร้รอยต่อ โดยเก็บ IP เดิมไว้ตอนสลับเซลล์ บ่อยครั้งก็สำเร็จ และคุณไม่รู้สึกว่าเปลี่ยน แต่บางครั้งก็เกิดการเปลี่ยน IP, การขาดของช่องสัญญาณวิทยุสั้นๆ 1-2 วินาที หรือแพ็กเก็ตสูญเสียพุ่งกระฉูด สำหรับคำขอสั้นๆ ไม่เห็นผล แต่สำหรับเซสชันสิบนาที นี่คือลอตเตอรีที่คุณเสี่ยงทุกครั้ง

อาการขาดตอนเมื่อเกิด handover

  • การขาดเกิดขึ้นแบบสุ่ม ไม่เกี่ยวกับปริมาณข้อมูลหรือเวลา
  • ความถี่ของการขาดเพิ่มขึ้นถ้าอุปกรณ์เคลื่อนที่จริง (ซึ่งเป็นเรื่องปกติของโมบายพร็อกซีบนซิมจริง)
  • หลังขาด การเชื่อมต่อใหม่ตั้งได้ปกติ - เพราะเครือข่ายยังมีชีวิต แค่การเชื่อมต่อเก่าเฉพาะอันนั้นตายไป

แล้วจะทำยังไง: ยอมรับความจริงแล้วสร้างความยืดหยุ่น

การเอาชนะการเปลี่ยน IP ในระดับ TCP เป็นไปไม่ได้ แต่เราสร้างไคลเอนต์ที่มองว่าการขาดการเชื่อมต่อเป็นเรื่องปกติ ไม่ใช่เหตุฉุกเฉินได้ ปรัชญาง่ายๆ: การเชื่อมต่อผ่านเครือข่ายโมบายนั้นไม่จีรังโดยนิยาม และไคลเอนต์ต้องรู้จักตั้งการเชื่อมต่อใหม่ได้อย่างไร้รอยต่อและทำงานต่อจากจุดที่ค้างไว้

เทคนิคที่เราจะ Implement ในโค้ด:

  • การทำ operation ให้ idempotent - ออกแบบคำขอให้ส่งซ้ำได้อย่างปลอดภัย การดาวน์โหลดแบบ Range เป็น idempotent เพราะการขอช่วงไบต์เดียวกันซ้ำให้ผลลัพธ์เดิม
  • โหลดต่อ ไม่ใช่โหลดใหม่ - เมื่อขาด อย่าเริ่มไฟล์ใหม่ตั้งแต่ต้น ให้ต่อจากไบต์ล่าสุดที่ได้มา ประหยัดทั้งทราฟฟิกและเวลา
  • ตรวจจับการตายของการเชื่อมต่อให้ไว - ด้วย TCP keepalive แบบ aggressive ที่พูดถึงไป คุณจะรู้ว่าการเชื่อมต่อตายแล้วภายในวินาที ไม่ใช่นาที
  • ทำธุรกรรมสั้นๆ - ยิ่งการส่งแต่ละครั้งสั้นเท่าไร โอกาสที่ handover จะมาเจอตอนนั้นก็ยิ่งน้อย การแบ่งอัปโหลดใหญ่เป็นส่วนๆ คือผลโดยตรงของหลักการนี้

ปฏิบัติด้วย Python: ไคลเอนต์ที่ยืดหยุ่นพร้อมการโหลดต่อและไทม์เอาต์

พอแล้วกับทฤษฎี มาลงมือสร้างกัน เริ่มจาก Python เราจะสร้างไคลเอนต์ที่: ตั้งค่า TCP keepalive, กำหนดไทม์เอาต์ที่สมเหตุสมผล, ดาวน์โหลดไฟล์เป็นส่วนๆ ผ่าน Range และโหลดต่อได้หลังขาดตอน

ขั้นตอนที่ 1: ตั้งค่า TCP keepalive บน socket

ไลบรารี HTTP มาตรฐานไม่ได้ตั้งค่า keepalive แบบ aggressive มาให้ ต้องลงไปถึงระดับ socket ในระบบนิเวศของ requests ทำได้ผ่าน transport adapter ที่กำหนด option ของ socket: เปิด keepalive, ตั้ง idle 20 วินาที, interval 10 วินาที, count 3 ระดับคำอธิบายคือ เราลงทะเบียน adapter ที่จะตั้งค่าพารามิเตอร์ระดับต่ำตอนสร้างการเชื่อมต่อ

ส่วนสำคัญของการตั้งค่าในรูปแบบ pseudocode พูดง่ายๆ คือ เปิด option ชื่อ SO_KEEPALIVE จากนั้นตั้ง TCP_KEEPIDLE เท่ากับ 20, TCP_KEEPINTVL เท่ากับ 10, TCP_KEEPCNT เท่ากับ 3 ชื่อ option จะต่างกันเล็กน้อยในแต่ละระบบปฏิบัติการ ดังนั้นใน production ต้องเลือกโดยเช็คแพลตฟอร์มก่อน

ขั้นตอนที่ 2: ไทม์เอาต์ที่สมเหตุสมผล

กฎสำคัญ: ต้องกำหนดไทม์เอาต์อย่างชัดเจนเสมอ และแยกไทม์เอาต์การเชื่อมต่อกับไทม์เอาต์การอ่านออกจากกัน การไม่ตั้งไทม์เอาต์คือกับดักที่ทำให้ไคลเอนต์ค้างตลอดกาลบนการเชื่อมต่อแบบ half-open

  • Connect timeout: 10 วินาที - การตั้งการเชื่อมต่อผ่านเครือข่ายโมบายช้ากว่า แต่ 10 วินาทีเผื่อไว้เพียงพอ
  • Read timeout: 30 วินาที - นี่คือไทม์เอาต์ของความนิ่งระหว่างข้อมูลแต่ละส่วน ไม่ใช่ไทม์เอาต์รวมทั้งหมด ถ้า 30 วินาทีไม่มีไบต์ใดเข้ามาเลย ถือว่าการเชื่อมต่อมีปัญหา
  • งบประมาณรวมของ operation - ให้ควบคุมแยกกันที่ตรรกะการโหลดต่อ ดีกว่าใช้ไทม์เอาต์ยักษ์ตัวเดียวครอบคลุมทั้งคำขอ

ขั้นตอนที่ 3: ดาวน์โหลดแบบ Range พร้อมการโหลดต่อ

แนวคิดของอัลกอริทึม: เราเปิดไฟล์ปลายทางเพื่อเขียน หาขนาดที่ดาวน์โหลดไปแล้ว (ถ้าไฟล์มีอยู่บางส่วน) ขอช่วงที่เหลือผ่าน header ชื่อ Range: bytes ตั้งแต่ตำแหน่งปัจจุบันไปจนจบ เขียนสตรีมลงไฟล์เรื่อยๆ เมื่อข้อมูลมา ถ้าเจอการขาด (exception การอ่าน, การเชื่อมต่อหลุด) อย่าตกใจ ให้ดูว่าในดิสก์มีกี่ไบต์แล้ว แล้วส่งคำขอใหม่โดยเริ่มจากตำแหน่งใหม่ ทำแบบนี้จนกว่าจะได้ไฟล์ครบ

ตรรกะทีละขั้นพูดเป็นภาษาไทย:

  1. หาขนาดเต็มของไฟล์ด้วยการขอ header (HEAD หรือ GET ด้วย Range: bytes=0-0 เพื่ออ่าน Content-Range)
  2. เช็คขนาดไฟล์ในเครื่องที่ดาวน์โหลดไปแล้ว
  3. ถ้าขนาดในเครื่องเท่ากับขนาดเต็ม แสดงว่าไฟล์พร้อมแล้ว จบการทำงาน
  4. ถ้าไม่ ให้สร้างคำขอแบบ Range โดยเริ่มจากขนาดในเครื่อง
  5. อ่านคำตอบเป็นชิ้นๆ ครั้งละ 64-256 กิโลไบต์ แล้วเขียนต่อท้ายไฟล์
  6. เมื่อสำเร็จ ให้ตรวจสอบความถูกต้อง (ขนาด, และ checksum ถ้าทำได้)
  7. เมื่อขาด ให้เพิ่มจำนวนครั้ง พักสั้นๆ แล้วกลับไปข้อ 2
  8. จำกัดจำนวนครั้งสูงสุดในระดับขนส่ง (เช่น 5-8) เพื่อไม่ให้วนลูปตลอดไปเมื่อมีปัญหาระบบ

จุดสำคัญ: ต้องเช็คว่าเซิร์ฟเวอร์รองรับ Range ไหม สัญญาณคือ header ชื่อ Accept-Ranges: bytes ในคำตอบ และรหัส 206 Partial Content สำหรับคำขอแบบ Range ถ้าเซิร์ฟเวอร์ตอบ 200 และไม่สนใจ Range การโหลดต่อจะทำไม่ได้ ต้องดาวน์โหลดใหม่ทั้งไฟล์ แต่ตอนนั้น keepalive และ MSS ที่ถูกต้องจะยิ่งสำคัญมาก

ขั้นตอนที่ 4: การแบ่งอัปโหลดขนาดใหญ่

เมื่อคุณไม่ได้ดาวน์โหลด แต่ต้องอัปโหลดปริมาณมาก (เช่น ส่งข้อมูล หรือรับรายงานขนาดใหญ่) ให้ใช้หลักการแบ่งส่วนเดียวกัน แบ่งงานเป็นหน้า (page) หรือชิ้น (chunk) ขนาดคงที่ แต่ละชิ้นคือธุรกรรมสั้นๆ อิสระที่มีการจัดการ error ของตัวเอง มี log ความคืบหน้าว่าชิ้นไหนได้รับการยืนยันแล้ว เมื่อขาดก็ส่งซ้ำเฉพาะชิ้นที่ยังไม่ยืนยัน วิธีนี้เปลี่ยน operation ใหญ่ที่เปราะบางสิบนาทีให้กลายเป็นชุดของงานสั้นๆ ที่เสถียร

ส่วนกลยุทธ์การส่งซ้ำตามรหัสตอบสนองของเซิร์ฟเวอร์ - เราจะไม่พูดถึงในนี้ เพราะเป็นหัวข้อของบทความแยกเรื่อง 429 และ exponential backoff ที่คุณควรอ่านต่อจากบทความนี้ จุดโฟกัสของเราคือระดับขนส่ง: การขาด, ไทม์เอาต์, ขนาด

ปฏิบัติด้วย Node.js: HTTP agent ที่ยืดหยุ่นและการสตรีม

ต่อไปใช้หลักการชุดเดียวกันในระบบนิเวศ Node.js ตรงนี้วัตถุ agent ซึ่งจัดการพูลการเชื่อมต่อมีบทบาทสำคัญ

ขั้นตอนที่ 1: ตั้งค่า agent แบบ keep-alive

ใน Node สร้าง HTTP หรือ HTTPS agent โดยเปิด keepAlive พารามิเตอร์ keepAlive true ทำให้ agent ใช้การเชื่อมต่อซ้ำ (ระดับ HTTP) จากนั้นตั้ง keepAliveMsecs ซึ่งคือช่วงเวลาที่ส่ง TCP keepalive probe ในระดับ socket สำหรับเครือข่ายโมบาย ให้ตั้ง keepAliveMsecs ประมาณ 15000-20000 มิลลิวินาที เพื่อให้ทัน NAT ไทม์เอาต์ของผู้ให้บริการ และจำกัด maxSockets กับ maxFreeSockets เพื่อไม่ให้สร้างการเชื่อมต่อเกินจำเป็น

ขั้นตอนที่ 2: ไทม์เอาต์ในหลายระดับ

ใน Node ไทม์เอาต์ถูกกำหนดในหลายจุด และสำคัญมากที่จะไม่ลืมสักจุด:

  • ไทม์เอาต์การตั้งการเชื่อมต่อ - ผ่าน option ใน request หรือ event handler ตอน socket เชื่อมต่อ
  • ไทม์เอาต์ความนิ่งของ socket - method ที่ตั้ง socket timeout เมื่อมันทำงานต้อง destroy socket ทิ้งอย่างชัดเจนและจัดการเหมือนการขาด Node ไม่ได้ปิด socket อัตโนมัติตอนไทม์เอาต์ มันแค่ emit event - จุดนี้คนมักลืม แล้วการเชื่อมต่อก็ยังค้างต่อไป
  • การจัดการ event error และ close ทั้งบน request และ socket - ทุกอันต้องนำไปสู่ตรรกะการลองใหม่ที่ควบคุมได้

ค่าที่แนะนำคล้ายกับ Python: connect ประมาณ 10 วินาที, idle ของ socket ประมาณ 30 วินาที, keepalive probe ทุก 15-20 วินาที

ขั้นตอนที่ 3: การสตรีมดาวน์โหลดพร้อมโหลดต่อ

ใน Node การทำงานกับสตรีมเป็นเรื่องธรรมชาติ ตรรกะการโหลดต่อเหมือนกับ Python: เช็คขนาดไฟล์ในเครื่อง, เปิดสตรีมเขียนแบบต่อท้าย, สร้างคำขอด้วย header Range เริ่มจากขนาดปัจจุบัน, ลงทะเบียน handler สำหรับ event data, end และ error เมื่อ event data เกิดขึ้นก็เขียน chunk ลงไฟล์ เมื่อ event end เกิดขึ้นก็เช็คว่าได้ไฟล์ครบหรือยัง เมื่อ event error หรือ close ก่อนกำหนดโดยได้ข้อมูลน้อยกว่าที่คาดไว้ ก็เริ่มต้นใหม่จากตำแหน่งใหม่

จุดสำคัญของความยืดหยุ่นใน Node คือ การจัดการการจบสตรีมก่อนกำหนดอย่างถูกต้อง event end อาจมาแม้จะได้ข้อมูลไม่ครบ ถ้าการเชื่อมต่อขาด ดังนั้นต้องเทียบบริมาณที่ได้รับจริงกับที่คาดหวังจาก header Content-Length หรือ Content-Range เสมอ อย่าเชื่อแค่ว่า end ทำงานแล้ว

ขั้นตอนที่ 4: ครอบตรรกะการลองใหม่ในระดับขนส่ง

ครอบ operation การดาวน์โหลดทั้งหมดในลูปที่มีจำนวนครั้งจำกัด ระหว่างครั้งให้พักสั้นๆ (สำหรับการขาดระดับขนส่ง แค่ 1-3 วินาทีก็พอ เพราะสาเหตุไม่ใช่เซิร์ฟเวอร์โอเวอร์โหลด แต่เป็นเหตุการณ์ในเครือข่าย) นับจำนวนครั้ง เมื่อครบจำกัด ให้ส่ง error ขึ้นไปพร้อมข้อมูลวินิจฉัย: ได้กี่ไบต์, error ประเภทไหน, ลองไปกี่ครั้ง ข้อมูลนี้มีค่ามากเวลาไล่สาเหตุ

ข้อผิดพลาดทั่วไป: สิ่งที่ไม่ควรทำ

มาดู anti-pattern ที่เราเจอเป็นประจำในโค้ดคนอื่น ถ้าเลี่ยงได้ ครึ่งหนึ่งของปัญหาจะหายไป

ข้อผิดพลาด 1: ไม่ตั้งไทม์เอาต์อย่างชัดเจน

พบบ่อยสุดและเจ็บปวดที่สุด ไคลเอนต์ที่ไม่มีไทม์เอาต์บนการเชื่อมต่อ half-open จะค้างตลอดกาล เธรดถูกบล็อก ทรัพยากรไม่ถูกปล่อย และคุณคิดว่า operation ยังทำงานอยู่ ต้องกำหนดไทม์เอาต์ให้ชัดเจนเสมอ การไม่มีไทม์เอาต์ไม่ใช่ความอดทนไร้ขีดจำกัด แต่คือระเบิดเวลา

ข้อผิดพลาด 2: พึ่งพา TCP keepalive ดีฟอลต์

ค่าดีฟอลต์สองชั่วโมงทำให้ TCP keepalive ไร้ประโยชน์สำหรับเครือข่ายโมบาย หลายคนเปิด SO_KEEPALIVE แล้วก็สบายใจ โดยไม่รู้ว่า probe แรกจะออกไปหลัง 7200 วินาที ซึ่งผู้ให้บริการล้างทุกอย่างไปนานแล้ว ต้องตั้ง idle, interval และ count เองชัดเจน

ข้อผิดพลาด 3: โหลดไฟล์ใหม่ทั้งไฟล์ทุกครั้งที่ขาด

ขาดตอนที่ 95 เปอร์เซ็นต์แล้วต้องเริ่มใหม่จากศูนย์ ไม่ใช่แค่เสียเวลา แต่ยังสิ้นเปลืองทราฟฟิกโมบายซึ่งพร็อกซีมักคิดเงินด้วย การโหลดต่อแบบ Range เป็นเรื่องจำเป็นสำหรับการดาวน์โหลดขนาดใหญ่ทุกรายการ

ข้อผิดพลาด 4: ไม่สนใจ MTU และ MSS

ผู้คนสู้กับการค้างของคำตอบใหญ่เป็นเดือน เปลี่ยนไทม์เอาต์และพร็อกซีไปเรื่อย ทั้งที่สาเหตุคือหลุมดำ PMTUD ถ้าคำตอบใหญ่ค้าง แต่คำตอบเล็กผ่าน ให้เช็ค MTU และตั้งค่า MSS clamping ก่อนเลย จะประหยัดเวลาเป็นสัปดาห์

ข้อผิดพลาด 5: สับสนระหว่างการขาดระดับขนส่งกับคำตอบของเซิร์ฟเวอร์

การขาดระดับขนส่ง (RST, read timeout, socket ตาย) และคำตอบของเซิร์ฟเวอร์ที่มีรหัส error เป็นคนละสถานการณ์กัน ต้องใช้คนละกลยุทธ์ สำหรับการขาดระดับขนส่ง ควรส่งซ้ำเร็วๆ พักสั้นๆ แล้วโหลดต่อ สำหรับคำตอบเซิร์ฟเวอร์ที่มีรหัสเช่น 429 ต้องใช้ exponential backoff - ซึ่งเป็นหัวข้ออีกบทความ อย่าผสมสองชั้นนี้ใน handler ตัวเดียว

ข้อผิดพลาด 6: การส่งซ้ำที่ aggressive เกินไป

ลูปส่งซ้ำไม่รู้จบโดยไม่มีขีดจำกัด เมื่อมีปัญหาระบบ (เช่น พร็อกซีเข้าไม่ได้เลย) จะกลายเป็นภาระกาฝาก ควรตั้งเพดานจำนวนครั้งเสมอ และจบ operation อย่างมีสติพร้อมข้อมูลวินิจฉัย

ข้อผิดพลาด 7: ไม่ตรวจสอบความถูกต้องของผลลัพธ์

การดาวน์โหลดจบไม่เท่ากับได้ข้อมูลที่ถูกต้อง การขาดอาจทิ้งไฟล์ที่ถูกตัดทอนไว้ ซึ่งดูเหมือนเสร็จสมบูรณ์ในทุกทางการ ควรเทียบขนาดเสมอ และสำหรับข้อมูลสำคัญให้เทียบ checksum

ข้อผิดพลาด 8: ถือการเชื่อมต่อเดียวไว้นานเกินไป

ยิ่งการเชื่อมต่อผ่านเครือข่ายโมบายมีอายุยืนนานเท่าไร ความน่าจะเป็นสะสมที่จะเจอ handover, การเปลี่ยน IP หรือ NAT ไทม์เอาต์ก็ยิ่งสูง การตั้งการเชื่อมต่อใหม่เป็นระยะไม่ใช่จุดอ่อน แต่เป็นสุขอนามัยที่ฉลาด อย่ากลัวที่จะสร้างการเชื่อมต่อใหม่

เครื่องมือและทรัพยากรสำหรับการวินิจฉัยและการสร้างความยืดหยุ่น

เครื่องมือที่ถูกต้องช่วยประหยัดเวลาเป็นชั่วโมง นี่คือคลังแสงที่ควรมีติดมือ

การวินิจฉัยเครือข่ายและแพ็กเก็ต

  • การดักจับและวิเคราะห์ทราฟฟิก - เครื่องมือดักจับแพ็กเก็ตคือกล้องจุลทรรศน์ของคุณ มันจะบอกว่าใครส่ง RST, ICMP fragmentation needed มาถึงหรือไม่, แพ็กเก็ตขนาดจริงคือเท่าไร, และสตรีมขาดตรงไหน ถ้าไม่มีมัน การวินิจฉัย MTU และการหาตัวการขาดจะกลายเป็นการเดาสุ่ม
  • ยูทิลิตี้ตรวจเส้นทาง - เครื่องมือ trace route และตรวจ MTU ช่วยให้รู้ว่าขนาดแพ็กเก็ตถูกจำกัดที่จุดไหนระหว่างทาง มีโหมดที่เจาะจงหาแพ็กเก็ตขนาดใหญ่ที่สุดที่ผ่านได้ - ซึ่งคือสิ่งที่ต้องใช้ในการตั้งค่า MSS
  • เซิร์ฟเวอร์ echo สำหรับทดสอบ - เซิร์ฟเวอร์ง่ายๆ ที่รองรับ keep-alive และส่งข้อมูลตามขนาดที่กำหนดได้ เป็นสิ่งจำเป็นสำหรับวัด idle ไทม์เอาต์และจำลองปัญหาคำตอบใหญ่ในสภาพแวดล้อมที่ควบคุมได้

ไลบรารีและแนวทางสำหรับไคลเอนต์

  • HTTP ไคลเอนต์ที่ตั้งค่า socket ได้ยืดหยุ่น - เลือกไลบรารีที่ให้เข้าถึงพารามิเตอร์การเชื่อมต่อ และตั้งไทม์เอาต์กับ keepalive ในระดับต่ำได้
  • กลไกสตรีมมิ่ง - การประมวลผลตัวตอบสนองแบบสตรีมเป็นสิ่งจำเป็นสำหรับการดาวน์โหลดขนาดใหญ่ เพราะคุณไม่ควรเก็บทั้งคำตอบไว้ในหน่วยความจำ
  • การ log ความคืบหน้า - ที่เก็บสถานะง่ายๆ (ได้ chunk ไหนบ้าง, มีกี่ไบต์ในดิสก์) ทำให้การโหลดต่อกลายเป็นเรื่องง่ายมาก

คุณภาพของโครงสร้างพื้นฐานพร็อกซี

ขอเน้นแยกอีกครั้ง: ปัญหาระดับขนส่งส่วนใหญ่แก้ได้ที่ฝั่งผู้ให้บริการพร็อกซีคุณภาพ MSS clamping ที่ตั้งมาอย่างดี, idle ไทม์เอาต์ที่เหมาะสม, การรักษา IP ให้คงที่ตอน handover, การทำงานกับ keep-alive อย่างโปร่งใส - ทั้งหมดคือสัญญาณของบริการที่สมบูรณ์ บริการอย่าง MobileProxy.space ออกแบบโครงสร้างพื้นฐานโดยคำนึงถึงรายละเอียดระดับขนส่งเหล่านี้ ซึ่งช่วยลดปวดหัวให้ไคลเอนต์ได้มาก แต่ถึงมีพร็อกซีที่สมบูรณ์แบบ ไคลเอนต์ก็ต้องยืดหยุ่นอยู่ดี - เพราะช่องสัญญาณวิทยุไม่สามารถคาดเดาได้โดยธรรมชาติ

เคสและผลลัพธ์: มันทำงานจริงยังไง

มาดูเคสเชิงสรุปสองสามกรณีที่สะท้อนสถานการณ์ทั่วไปและวิธีแก้ ตัวเลขเป็นค่าเฉลี่ย แต่สะท้อนขนาดที่เกิดขึ้นจริง

เคส 1: การอัปโหลดแคตตาล็อกค้างกลางคัน

สถานการณ์ ทีมงานอัปโหลดแคตตาล็อกสินค้าขนาดใหญ่ผ่านโมบายพร็อกซีด้วยคำขอเดียว คำตอบประมาณ 15 เมกะไบต์ ค้างที่ประมาณ 6-8 เมกะไบต์เป็นประจำ ไม่มี error แค่เงียบไป จนกว่าไทม์เอาต์รวมจะทำงานหลังผ่านไปหลายนาที

การวินิจฉัย คำขอเล็กๆ ผ่านได้อย่างสมบูรณ์ การดักจับทราฟฟิกเผยว่าแพ็กเก็ตใหญ่ออกไปพร้อม flag Don't Fragment แต่ ICMP fragmentation needed ไม่กลับมา เป็นหลุมดำ PMTUD คลาสสิก

วิธีแก้ ตั้งค่า MSS clamping เป็นค่าที่การันตีว่าแพ็กเก็ตอยู่ใน MTU จริงของโมบายเน็ตเวิร์ก และแบ่งอัปโหลดเป็นคำขอรายหน้า หน้าละ 2 เมกะไบต์

ผลลัพธ์ การค้างหายไปหมด เวลาอัปโหลดคาดเดาได้ และเมื่อขาดเป็นครั้งคราวก็ส่งซ้ำแค่หน้าเดียว ไม่ใช่ทั้งแคตตาล็อก อัตราความสำเร็จเพิ่มจากประมาณ 40 เป็นเกือบ 100 เปอร์เซ็นต์

เคส 2: การเชื่อมต่อตายในช่วงพักรอ

สถานการณ์ ไคลเอนต์ส่งคำขอให้สร้างรายงานชิ้นใหญ่ เซิร์ฟเวอร์ใช้คิด 90-120 วินาที แล้วจะส่งผลลัพธ์กลับมา แต่พอถึงเวลาที่รายงานพร้อม การเชื่อมต่อตายไปแล้ว

การวินิจฉัย การวัด idle ไทม์เอาต์พบว่าขาดที่ประมาณ 60 วินาทีของการนิ่งเป็นประจำ ผู้ร้ายคือ NAT ไทม์เอาต์ของผู้ให้บริการ เพราะระหว่างรอไม่มีทราฟฟิกเลย

วิธีแก้ เปิด TCP keepalive ด้วย idle 20 วินาที และ interval 10 ตอนนี้ระหว่างรอ การเชื่อมต่อถูกรีเฟรชด้วย probe บริการทุก 20 วินาที

ผลลัพธ์ การเชื่อมต่ออยู่รอดจนรายงานพร้อม การขาดตอนช่วงพักหยุดลง โบนัสเพิ่มเติมคือ ไคลเอนต์รู้ได้ภายใน 45-60 วินาทีว่าการเชื่อมต่อตายจริง แทนที่จะค้างเป็นนาที

เคส 3: การขาดแบบสุ่มระหว่างทำ parsing หนักๆ

สถานการณ์ เซสชัน parsing ที่อยู่นานขาดแบบไม่เป็นระเบียบ ไม่เกี่ยวกับปริมาณหรือเวลา โดยเฉพาะช่วงเวลาหนึ่งๆ บ่อยมาก

การวินิจฉัย อาการบ่งชี้ว่าเป็นการเปลี่ยน IP ตอน handover หลังขาด การเชื่อมต่อใหม่ตั้งได้ทันที - เครือข่ายยังมีชีวิต

วิธีแก้ ปรับไคลเอนต์ใหม่ตามปรัชญาการเชื่อมต่อที่ไม่จีรัง: ธุรกรรมสั้นๆ ที่ idempotent, log ความคืบหน้า, การตั้งการเชื่อมต่อใหม่ทันทีเมื่อขาด, การส่งซ้ำแบบจำกัดจำนวนพร้อมพักสั้นๆ

ผลลัพธ์ การขาดไม่ได้หายไปทางกายภาพ (สู้การเปลี่ยน IP ไม่ได้) แต่มันไม่ใช่ปัญหาอีกต่อไป การขาดแต่ละครั้งเสียแค่การส่งธุรกรรมสั้นๆ ซ้ำหนึ่งครั้ง - แค่เศษเสี้ยววินาที ความน่าเชื่อถือรวมของกระบวนการเพิ่มขึ้นอย่างมาก และวิศวกรเลิกต้องเฝ้า log กลางคืน

บทสรุปรวมของเคส

สังเกตเห็นรูปแบบไหม: ในทุกเคส วิธีแก้ไม่ได้อยู่ที่การเปลี่ยนพร็อกซี แต่อยู่ที่การเข้าใจระดับขนส่งและการตั้งค่าไคลเอนต์อย่างถูกต้อง การวินิจฉัยจากอาการนำไปหาตัวการที่ชัดเจน จากนั้นการรักษาก็ตรงจุดและรวดเร็ว

FAQ: คำถามพบบ่อยเกี่ยวกับการขาดการเชื่อมต่อในเครือข่ายโมบาย

ทำไมผ่านโมบายพร็อกซี คำขอสั้นๆ ทำงานได้ แต่คำขอยาวๆ ขาด?

เพราะ operation ยาวๆ อยู่ได้นานกว่า และมีโอกาสเจอปัญหาแต่ละอย่าง: idle ไทม์เอาต์ตอนพัก, ขีดจำกัดขนาดคำตอบ, ขีดจำกัดอายุการเชื่อมต่อ, handover ที่เปลี่ยน IP และหลุมดำ PMTUD กับแพ็กเก็ตใหญ่ คำขอสั้นๆ เสร็จก่อนที่สิ่งเหล่านี้จะทำงาน วิธีแก้คือตั้งค่า keepalive, MSS และใช้การโหลดต่อแบบ Range

จะรู้ได้ยังไงว่าใครตัดการเชื่อมต่อ - พร็อกซี, ผู้ให้บริการ หรือเซิร์ฟเวอร์?

ดูที่ลายมือ: ขาดตอนพัก - NAT ไทม์เอาต์ของผู้ให้บริการหรือ idle ของพร็อกซี ขาดที่ปริมาณคงที่ - ขีดจำกัดขนาดของพร็อกซีหรือเซิร์ฟเวอร์ ขาดที่เวลาคงที่ - ขีดจำกัดอายุการเชื่อมต่อ ค้างกับคำตอบใหญ่ - MTU และ PMTUD ขาดสุ่มๆ ตอนเคลื่อนที่ - handover สำหรับคำตอบที่แม่นยำ ให้ใช้การดักจับทราฟฟิก มันจะบอกว่า RST มาจาก address ไหน และ ICMP มาถึงหรือไม่

TCP keepalive กับ HTTP keep-alive ต่างกันยังไง?

HTTP keep-alive ทำงานในระดับแอปพลิเคชัน และให้ใช้การเชื่อมต่อเดียวซ้ำสำหรับหลายคำขอ ประหยัดการตั้งค่าใหม่ TCP keepalive ทำงานในระดับเคอร์เนลของ OS และส่ง probe บริการเป็นระยะ ทำให้การเชื่อมต่อมีชีวิตในช่วงพักและรีเฟรช NAT ตัวแรกประหยัดเวลาสำหรับคำขอใหม่ ตัวที่สองช่วยไม่ให้ขาดตอนนิ่ง ต้องใช้ทั้งคู่

ควรตั้งค่า TCP keepalive เท่าไรสำหรับเครือข่ายโมบาย?

จุดเริ่มต้น: idle 15-25 วินาที, interval 10-15 วินาที, count 3-4 ค่าชุดนี้ทัน NAT ไทม์เอาต์ที่ใจร้ายของผู้ให้บริการ และช่วยให้ตรวจจับการเชื่อมต่อที่ตายได้เร็วพร้อมกัน ค่าที่แม่นยำควรปรับโดยวัด idle ไทม์เอาต์ของผู้ให้บริการและพร็อกซีของคุณเอง

หลุมดำ PMTUD คืออะไร และสังเกตยังไง?

คือสถานการณ์ที่โหนดบนเส้นทางทิ้งแพ็กเก็ตใหญ่เกินไป และ ICMP แจ้งเตือนให้ลดขนาดถูกบล็อก ไม่ถึงผู้ส่ง ผลคือแพ็กเก็ตใหญ่สูญหายไม่รู้จบ ส่วนแพ็กเก็ตเล็กผ่านได้ สังเกตจากอาการ: handshake และคำตอบเล็กทำงานได้ แต่คำตอบใหญ่ค้างสนิท รักษาด้วย MSS clamping หรือลด MTU

รักษา TCP คอนเนกชันตอนเปลี่ยนสถานีฐานได้ไหม?

ถ้าเครือข่ายรักษา IP ภายนอกเดิมไว้ตอน handover - ได้ การเชื่อมต่อจะรอดผ่านการสลับ อาจดีเลย์สั้นๆ ถ้า IP เปลี่ยน - ไม่ได้ การเชื่อมต่อ TCP เดิมตายถาวร เพราะ TCP ผูกติดกับคู่ที่อยู่และพอร์ตอย่างเหนียวแน่น กลยุทธ์ที่ถูกคือ ไม่พยายามรักษาการเชื่อมต่อด้วยทุกวิถีทาง แต่สร้างไคลเอนต์ที่ตั้งการเชื่อมต่อใหม่ได้อย่างไร้รอยต่อและทำงานต่อจากจุดที่ขาด

วิธีโหลดไฟล์ต่อหลังขาดที่ถูกต้องคือยังไง?

เช็คว่าเซิร์ฟเวอร์รองรับ Range หรือไม่ (header Accept-Ranges: bytes และรหัส 206 สำหรับ Range request) จากนั้นเมื่อขาด ให้หาขนาดส่วนที่ได้มาแล้ว และขอส่วนที่เหลือด้วย header Range เริ่มจากไบต์นั้น เขียนต่อท้ายไฟล์เดิม ทำซ้ำจนได้ครบ โดยจำกัดจำนวนครั้ง สุดท้ายต้องเทียบขนาดสุดท้ายกับขนาดที่คาดหวังเสมอ

ควรตั้งไทม์เอาต์อะไรใน HTTP ไคลเอนต์สำหรับโมบายพร็อกซี?

แยกไทม์เอาต์การตั้งการเชื่อมต่อ (ประมาณ 10 วินาที) และไทม์เอาต์ความนิ่งในการอ่าน (ประมาณ 30 วินาที ถือเป็นช่วงพักระหว่างข้อมูลแต่ละส่วน ไม่ใช่ทั้งการส่ง) งบประมาณรวมของ operation ให้ควบคุมที่ตรรกะการโหลดต่อ แทนที่จะใช้ไทม์เอาต์ยักษ์เดียว และอย่าให้ไทม์เอาต์ไร้ขีดจำกัดเด็ดขาด

ต้องเปลี่ยนอะไรไหม ถ้ามีกลยุทธ์ส่งซ้ำตามรหัสตอบสนองอยู่แล้ว?

ใช่ การส่งซ้ำตามรหัสตอบสนอง (เช่น จัดการ 429 ด้วย exponential backoff) คือระดับแอปพลิเคชัน เป็นอีกหัวข้อหนึ่ง การขาดระดับขนส่ง - RST, read timeout, socket ตาย - ต้องการตรรกะของตัวเอง: ส่งซ้ำเร็วๆ พักสั้นๆ, โหลดต่อ, keepalive และ MSS ที่ถูกต้อง สองชั้นนี้แทนที่กันไม่ได้ และต้องอยู่ร่วมกันในไคลเอนต์

การเลือกผู้ให้บริการพร็อกซีส่งผลต่อความเสถียรระดับขนส่งไหม?

อย่างมีนัยสำคัญ ผู้ให้บริการที่โตเต็มที่จะตั้งค่า MSS clamping, idle ไทม์เอาต์ที่เหมาะสม, พยายามรักษา IP ตอน handover และทำงานกับ keep-alive อย่างถูกต้อง ซึ่งช่วยลดปัญหาส่วนหนึ่งก่อนถึงโค้ดคุณ แต่ถึงพร็อกซีจะสมบูรณ์แบบ ก็ไม่ได้ลบความจำเป็นในการสร้างไคลเอนต์ที่ยืดหยุ่น เพราะช่องสัญญาณวิทยุคาดเดาไม่ได้โดยธรรมชาติ

บทสรุป: การเชื่อมต่อไม่จีรัง แต่ข้อมูลต้องถึงมือ

เราเดินทางจากอาการไปจนถึงไคลเอนต์ที่ยืดหยุ่นแล้ว มาสรุปใจความสำคัญเพื่อให้บทความนี้เป็นบุ๊กมาร์กของคุณ

ข้อแรก การขาดของการเชื่อมต่อยาวผ่านโมบายพร็อกซีไม่ใช่เรื่องลี้ลับ แต่เป็นผลจากกลไกที่จับต้องได้: idle ไทม์เอาต์ในสามระดับ, ขีดจำกัดขนาดและเวลา, ปัญหา MTU และ handover แต่ละอย่างมีลายมือ และการวินิจฉัยจากอาการมักพาไปหาตัวการได้เกือบทุกครั้ง

ข้อสอง ไทม์เอาต์ที่สั้นที่สุดบนเส้นทางคือตัวชี้ชะตาการเชื่อมต่อ วัดมันแล้วตั้งค่า TCP keepalive ให้ aggressive กว่าขีดจำกัดนั้น - idle 15-25 วินาทีสำหรับเครือข่ายโมบาย อย่าลืมว่า TCP keepalive และ HTTP keep-alive เป็นคนละกลไก และต้องใช้ทั้งคู่

ข้อสาม ถ้าคำตอบใหญ่ค้าง แต่คำตอบเล็กผ่าน - เกือบแน่นอนว่าคือหลุมดำ PMTUD รักษาด้วย MSS clamping และ MTU ที่ถูกต้อง อย่าเสียเวลาเป็นสัปดาห์กับการลองไทม์เอาต์ ให้เช็คขนาดแพ็กเก็ตเป็นอย่างแรก

ข้อสี่ การเปลี่ยน IP ตอน handover เอาชนะไม่ได้ แต่ทำให้มันไม่น่ากลัวได้ สร้างไคลเอนต์บนฐานที่ว่าการเชื่อมต่อไม่จีรัง: ธุรกรรมสั้นๆ ที่ idempotent, การโหลดต่อแบบ Range, log ความคืบหน้า, การตั้งค่าใหม่ที่รวดเร็ว, การส่งซ้ำแบบจำกัด และการตรวจสอบความถูกต้องที่จำเป็น

ขั้นตอนต่อไปของคุณง่ายๆ: วัด idle ไทม์เอาต์บนพร็อกซีและผู้ให้บริการ เปิดและตั้งค่า TCP keepalive ทดสอบพฤติกรรมกับคำตอบใหญ่และตั้งค่า MSS ถ้าจำเป็น ใช้การโหลดต่อแบบ Range และอย่าลืมศึกษาเนื้อหาคู่กันเรื่องกลยุทธ์ส่งซ้ำระดับรหัสตอบสนอง - 429 และ exponential backoff - เพื่อปิดช่องโหว่ทั้งระดับแอปพลิเคชัน ไม่ใช่แค่ระดับขนส่ง

เครือข่ายโมบายโดยธรรมชาติคาดเดาไม่ได้ แต่ไคลเอนต์ที่สร้างขึ้นด้วยความเข้าใจธรรมชาติของมัน จะเปลี่ยนความคาดเดาไม่ได้นี้จากต้นตอของการเฝ้ายามกลางคืน ให้กลายเป็นเสียงรบกวนพื้นหลังทั่วไป การเชื่อมต่อไม่จีรัง - และนั่นเป็นเรื่องปกติ สิ่งสำคัญคือข้อมูลต้องไปให้ถึง ไม่ว่ายังไงก็ตาม ตอนนี้คุณรู้วิธีแล้ว