☕ 5-Min Tech Story #35

พอร์ตหมดเครื่อง (TIME_WAIT Storm): แค่ยิง API รัวๆ ทำไมระบบถึงต่อเน็ตออกไปหาใครไม่ได้อีกเลย?

⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 25 วินาที

ไมโครเซอร์วิสของคุณต้องยิง Request ไปดึงข้อมูลจาก Payment Gateway หรือ Third-party API อยู่ตลอดเวลา...

ในช่วงแคมเปญลดราคา ทราฟฟิกกระชากขึ้นมาที่ 2,000 Requests ต่อวินาที ทันใดนั้นระบบเริ่มล้มเหลวระนาว แต่พอเปิดดูแดชบอร์ด CPU ใช้งานไปแค่ 25% และ RAM ยังเหลืออีกบานเบอะ!

แต่เมื่อเปิดดู Error Log บนเซิร์ฟเวอร์ ข้อความสั้นๆ ชวนงงกลับพ่นออกมาเป็นล้านแถว:

java.net.NoRouteToHostException: Cannot assign requested address
(หรือ dial tcp: lookup: cannot assign requested address)

เน็ตเวิร์กไม่ได้ล่ม เซิร์ฟเวอร์ปลายทางยังทำงานสบายดี แล้วทำไมแอปของเราถึงฟ้องว่า "ไม่สามารถจัดสรรแอดเดรสได้" จนกลายเป็นใบ้ ไม่ยอมยิงออกไปหาใครเลย?

🚪 ทรัพยากรล่องหน: Ephemeral Ports (พอร์ตชั่วคราว)

เวลาเซิร์ฟเวอร์เปิดรอรับแขก มันจะเปิดฟังที่พอร์ตประจำ เช่น 80 หรือ 443

แต่ในทางกลับกัน เมื่อแอปของคุณสวมหมวกเป็น Client เพื่อยิง Request ออกไปข้างนอก ระบบปฏิบัติการ Linux จะต้องสุ่มแจก พอร์ตต้นทางฝั่งเรา (Outbound / Ephemeral Port) 1 พอร์ต เพื่อใช้เป็นคู่สนทนาเสมอ

บน Linux ทั่วไป ช่วงของพอร์ตชั่วคราวนี้ถูกจำกัดไว้ที่:

$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999 # มีให้ใช้งานประมาณ 28,232 พอร์ตเท่านั้น!

⏳ คำสาป 60 วินาทีแห่งสถานะ TIME_WAIT

ตามกฎของโปรโตคอล TCP เมื่อฝั่งเราเป็นคนสั่ง "ปิดการเชื่อมต่อ" (Close Connection) ก่อน ท่อส่งข้อมูลนั้นจะไม่ได้สลายตัวหายไปในทันที

Linux Kernel จะสั่งล็อกพอร์ตนั้นให้อยู่ในสถานะ TIME_WAIT เป็นเวลา 2MSL (Maximum Segment Lifetime ซึ่งโดยมาตรฐานคือ 60 วินาที):

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

ทีนี้ลองดูคณิตศาสตร์ง่ายๆ:

เมื่อคำขอที่ 28,233 วิ่งมาขอพอร์ตว่างเพื่อยิงต่อ... Linux กางมือออกแล้วบอกว่า "ไม่มีพอร์ตว่างให้แกใช้แล้ว!" จึงพ่น Cannot assign requested address ออกมาทันที!

🛡️ 2 ท่าปลดล็อกพอร์ต: ใช้ซ้ำที่แอป และสั่ง Reuse ที่ Kernel

1. อย่าสร้าง Client ใหม่เรื่อยเปื่อย ให้ใช้ Connection Pooling (หัวใจสำคัญ):

ห้ามสร้าง HTTP Client หรือ Connection แบบใช้ครั้งเดียวทิ้งเด็ดขาด! ให้สร้างเป็น Singleton พร้อมเปิดใช้ Connection Pool และ HTTP Keep-Alive:

// ตัวอย่าง Go: ใช้ http.Client ตัวเดิมร่วมกันเสมอ
var client = &http.Client{
  Transport: &http.Transport{
    MaxIdleConns: 500,
    MaxIdleConnsPerHost: 100,
    IdleConnTimeout: 90 * time.Second,
  },
}

พอร์ตเดิมเพียงไม่กี่ร้อยพอร์ตจะถูกเปิดค้างไว้เพื่อส่งข้อความซ้ำๆ โดยไม่มีการสลับไปติดสถานะ TIME_WAIT ให้เสียของ

2. จูน Linux Kernel ให้หมุนเวียนพอร์ตกลับมาใช้ใหม่:

สำหรับเซิร์ฟเวอร์เกตเวย์ที่มีโหลดสูงลิ่ว ให้ปรับแต่งคอนฟิกใน /etc/sysctl.conf:

# 1. ขยายช่วงพอร์ตชั่วคราวให้กว้างขึ้น (ได้เกือบ 64,000 พอร์ต)
net.ipv4.ip_local_port_range = 1024 65535

# 2. ยอมให้นำ Socket ในสถานะ TIME_WAIT กลับมาใช้ใหม่ได้อย่างปลอดภัย
net.ipv4.tcp_tw_reuse = 1

💡 5-Min Takeaway

"ทรัพยากรบนเซิร์ฟเวอร์ไม่ได้มีแค่ CPU และ RAM แต่รวมถึงจำนวนพอร์ต TCP ขาออกด้วย" การสร้าง HTTP Client หรือ Connection ใหม่ในทุกคำขอคือการเผาผลาญ Ephemeral Port ทิ้งอย่างสูญเปล่า การใช้ HTTP Connection Pool เพื่อหมุนเวียน Socket เดิมมาใช้ซ้ำ (Keep-Alive) คือกฎเหล็กข้อแรกของการเขียนระบบประสิทธิภาพสูงที่จะช่วยให้ระบบไม่สำลักตายจากพายุ TIME_WAIT