18 คะแนน โดย GN⁺ 2025-12-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อต้องดีบักปัญหา latency ในระบบแบบกระจาย สิ่งแรกที่ควรตรวจสอบคือการตั้งค่า TCP_NODELAY
  • อัลกอริทึมของ Nagle เป็นแนวทางที่ถูกเสนอใน RFC896 เมื่อปี 1984 โดยออกแบบมาเพื่อลดโอเวอร์เฮดของ TCP header ในการส่งแพ็กเก็ตขนาดเล็ก
  • แต่เมื่อทำงานร่วมกับกลไก delayed ACK จะทำให้การส่งข้อมูลล่าช้าจนกว่าจะได้รับ ACK และ ทำให้ประสิทธิภาพของแอปพลิเคชันที่ไวต่อ latency แย่ลง
  • ในสภาพแวดล้อมดาต้าเซ็นเตอร์ยุคใหม่ RTT สั้นมาก และระบบส่วนใหญ่ก็ส่งข้อความขนาดใหญ่กันอยู่แล้ว ทำให้ ข้อดีของอัลกอริทึมของ Nagle แทบไม่เหลืออยู่เลย
  • ดังนั้น ในระบบแบบกระจายสมัยใหม่ควรเปิดใช้ TCP_NODELAY เป็นค่าเริ่มต้น และอัลกอริทึมของ Nagle ก็ไม่จำเป็นอีกต่อไป

ที่มาของอัลกอริทึมของ Nagle

  • RFC896 ของ John Nagle ในปี 1984 ถูกเสนอขึ้นเพื่อแก้ปัญหา โอเวอร์เฮด 4000% จากข้อมูล 1 ไบต์เทียบกับ header 40 ไบต์ ที่เกิดขึ้นในการส่งข้อมูลขนาดเล็ก เช่น การพิมพ์จากคีย์บอร์ด
    • ปัญหาในเวลานั้นคือทุกครั้งที่ผู้ใช้พิมพ์ทีละตัวอักษร ระบบจะส่งแพ็กเก็ตขนาดเล็กออกไป ทำให้ประสิทธิภาพเครือข่ายต่ำ
    • วิธีแก้คือ ห้ามส่งเซกเมนต์ใหม่ หากข้อมูลก่อนหน้ายังไม่ได้รับ ACK
  • แนวทางนี้มีประสิทธิภาพในสภาพแวดล้อมเครือข่ายยุคนั้น แต่ไม่เหมาะกับระบบสมัยใหม่ที่ latency มีความสำคัญ

ปฏิสัมพันธ์ระหว่างอัลกอริทึมของ Nagle กับ Delayed ACK

  • Delayed ACK (RFC813, RFC1122) คือวิธีที่ฝั่งรับจะไม่ส่ง ACK ทันที แต่จะหน่วงไว้จนกว่าจะมีข้อมูลตอบกลับหรือจนกว่า timer จะหมดเวลา
  • อัลกอริทึมของ Nagle จะหยุดส่งเพื่อรอ ACK ขณะที่ delayed ACK ก็เลื่อน ACK ออกไป จึงเกิด ภาวะค้างที่ทั้งสองฝั่งรอกันเอง
  • แม้แต่ John Nagle เองก็เรียกการจับคู่นี้ว่า “ชุดผสมที่เลวร้าย” และชี้ว่าฟีเจอร์ทั้งสองถูกนำมาแยกกัน แต่เมื่อใช้ร่วมกันกลับ ก่อให้เกิด latency

ปัญหาในสภาพแวดล้อมยุคใหม่

  • ภายในดาต้าเซ็นเตอร์ RTT อยู่ราว 500μs และแม้ในรีเจียนเดียวกันก็อยู่ในระดับไม่กี่มิลลิวินาที ซึ่งสั้นมาก
  • ในสภาพแวดล้อมแบบนี้ การหน่วงการส่งออกไปหนึ่ง RTT ย่อมนำไปสู่การสูญเสียประสิทธิภาพ
  • อีกทั้งระบบแบบกระจายสมัยใหม่ยังส่งข้อความที่มีขนาดใหญ่พออยู่แล้วจาก TLS, serialization และ protocol overhead ทำให้ ปัญหาแพ็กเก็ตขนาด 1 ไบต์แทบไม่มีอยู่จริง
  • การเพิ่มประสิทธิภาพสำหรับข้อความขนาดเล็กในปัจจุบันจึงถูก จัดการที่ชั้นแอปพลิเคชัน

ความจำเป็นของ TCP_NODELAY

  • ใน ระบบแบบกระจายที่ไวต่อ latency แนะนำให้เปิดใช้ TCP_NODELAY เพื่อปิดการทำงานของอัลกอริทึมของ Nagle
    • นี่ไม่ใช่การตั้งค่าที่ “ไม่มีประสิทธิภาพ” หรือ “ผิดพลาด” แต่เป็นทางเลือกที่เหมาะกับฮาร์ดแวร์และลักษณะทราฟฟิกสมัยใหม่
  • ผู้เขียนยืนยันว่า TCP_NODELAY ควรเป็นค่าเริ่มต้น
    • แม้โค้ดบางส่วนที่ “ส่งทุกครั้งที่มีการเรียก write()” อาจช้าลง แต่โค้ดลักษณะนั้นก็ควรถูกแก้ไขที่ต้นเหตุอยู่แล้ว

ตัวเลือกอื่นที่เกี่ยวข้อง

  • ตัวเลือก TCP_QUICKACK ช่วยลดการหน่วง ACK ได้ แต่ ไม่ใช่คำตอบระดับรากฐาน เพราะมีปัญหาเรื่อง portability และพฤติกรรมที่ไม่สม่ำเสมอ
  • แก่นของปัญหาคือ เคอร์เนลเก็บข้อมูลไว้นานกว่าจังหวะที่แอปพลิเคชันตั้งใจจะส่ง และเมื่อมีการเรียก write() ก็ควรถูกส่งออกทันที

บทสรุป

  • อัลกอริทึมของ Nagle เคยเป็นนวัตกรรมที่ยอดเยี่ยมสำหรับเพิ่มประสิทธิภาพเครือข่ายในอดีต แต่
    ในสภาพแวดล้อมของเครือข่ายความเร็วสูงและระบบแบบกระจายสมัยใหม่ มันกลับกลายเป็นฟีเจอร์ล้าสมัยที่สร้าง latency แทน
  • ดังนั้น การเปิดใช้ TCP_NODELAY ตลอดเวลาควรถูกมองเป็นหลักพื้นฐานของการออกแบบระบบสมัยใหม่

1 ความคิดเห็น

 
GN⁺ 2025-12-23
ความคิดเห็นจาก Hacker News
  • อธิบายภูมิหลังของอัลกอริทึม Nagle ที่ถูกสร้างขึ้นในยุคของ เครือข่ายแบบหลายจุด
    ตอนนั้นโฮสต์หลายเครื่องใช้ช่องสัญญาณ Ethernet ร่วมกัน จึงใช้ CSMA/CD เพื่อหลีกเลี่ยงการชนกัน
    แต่ปัจจุบัน Ethernet ส่วนใหญ่เป็นโครงสร้างแบบ point-to-point และอยู่ในสภาพแวดล้อม full duplex ที่รับส่งพร้อมกันได้
    ดังนั้น CSMA จึงไม่จำเป็นอีกต่อไป และคิดว่าการตั้งค่า TCP_NODELAY เพื่อปิดการทำงานของอัลกอริทึม Nagle เป็นสิ่งที่สมเหตุสมผลในกรณีส่วนใหญ่
    • สงสัยว่าแรงจูงใจที่เกี่ยวกับ CSMA มีอยู่จริงในการออกแบบอัลกอริทึม Nagle หรือแค่พูดถึงในฐานะบริบทตามยุคสมัย
    • จริง ๆ แล้วอัลกอริทึม Nagle มีจุดประสงค์แค่เพื่อ รวมแพ็กเก็ต (coalescing)
      การตั้งให้เป็นค่าเริ่มต้นถือเป็นหนึ่งในความผิดพลาดครั้งใหญ่ในประวัติศาสตร์เครือข่าย
    • สำหรับข้อมูลเพิ่มเติม Ethernet ใช้ CSMA/CD ส่วน WiFi ใช้ CSMA/CA
      ราวปี 2014 ตอนเปลี่ยนสวิตช์ในดาต้าเซ็นเตอร์ เคยต้องคงอุปกรณ์เก่าบางส่วนไว้เพราะไม่รองรับ 10Mbit half duplex
    • ถ้าแอปพลิเคชันไม่ได้ใส่ใจขนาดแพ็กเก็ตหรือไม่ได้ไวต่อ latency นัก Nagle ก็ถือว่าสมเหตุสมผลพอสมควร
      เพราะช่วยป้องกันการสร้างแพ็กเก็ตที่เล็กเกินไป
    • ดูเหมือนจะมี ความสับสนระหว่างเลเยอร์ของเครือข่าย
      Nagle เป็นการปรับแต่งในชั้น TCP เพื่อเพิ่มประสิทธิภาพด้วยการรวมแพ็กเก็ตเล็ก ๆ
      ส่วน CSMA เป็นปัญหาในชั้นกายภาพ/ดาต้าลิงก์ ซึ่งแยกจาก Nagle
  • เจอบทความนี้ตอนกำลังดีบักปัญหา latency เครือข่ายระหว่างพัฒนาเกม
    แบ็กเอนด์ที่เขียนด้วย Go เปิดใช้ TCP_NODELAY เป็นค่าเริ่มต้นอยู่แล้วเลยไม่ใช่สาเหตุ แต่ส่วนที่พูดถึงการรับรู้ปัญหาของ Nagle น่าสนใจมาก
    มีการถกเถียงก่อนหน้านี้ด้วย ดูได้ในเธรดนี้
    • ขอแนะนำบทความดี ๆ ของ Julia Evans ด้วย
      ใน การสื่อสารแบบแชตตี้ อย่างโปรโตคอล DICOM การตั้ง TCP_NODELAY=1 ช่วยเพิ่ม throughput ได้มาก
    • สงสัยว่ากำลังพัฒนาเกมอะไรอยู่ ฉันเองก็สนุกกับการพัฒนาเกมด้วย Ebitengine และ Golang เลยสนใจมาก
  • ตัว Nagle เองเคยบอกเมื่อราว 10 ปีก่อนว่าปัญหาที่แท้จริงคือ delayed ACK
    ดูลิงก์ที่เกี่ยวข้อง
    คิดว่าใน workload ยุคนี้ delayed ACK ไม่ได้ให้ประโยชน์มากนัก
    ในสภาพแวดล้อมสมัยใหม่ที่มี HTTP เป็นศูนย์กลาง การปิดทั้ง Nagle และ delayed ACK น่าจะดีกว่า
    • ในต้นฉบับก็พูดถึงเรื่องนี้
      เพราะ RTT ระหว่างดาต้าเซ็นเตอร์อยู่ในระดับหลายร้อยไมโครวินาที การเพิ่มความหน่วงแม้เพียงหนึ่ง RTT ก็อาจกลับกลายเป็นผลเสีย
  • ในภาษาโปแลนด์ “nagle” แปลว่า “ทันใดนั้น” ซึ่งเข้ากับชื่ออัลกอริทึมได้ดีจนน่าประหลาดใจ
    • ดูเหมือนจะเป็นอีกกรณีของ Nominative determinism
      ลิงก์วิกิ
    • ที่น่าสนใจคือเมื่อ “NODELAY on” ก็ส่งแบบทันที ส่วนตอน “off” ก็ส่งรวบทีเดียว เหมือนความหมายของคำครอบคลุมทั้งสองแบบ
    • จริง ๆ แล้วนี่คืออัลกอริทึมที่อิงตาม RFC 896 ซึ่งเขียนโดย John Nagle
  • คิดว่าการตั้งอัลกอริทึม Nagle เป็น ค่าเริ่มต้นของเคอร์เนล เป็นเรื่องแปลก
    แอปพลิเคชันควรเป็นฝ่ายตัดสินใจเองว่าจะส่งเมื่อไรและจะบัฟเฟอร์เมื่อไร
  • แปลกใจที่บทความไม่ได้พูดถึง MSG_MORE
    บน Linux มันเป็น hint ให้เคอร์เนลรู้ว่าจะมีข้อมูลเพิ่มเติมส่งตามมาในไม่ช้า ซึ่งมีประโยชน์เวลาแยกส่ง header กับข้อมูล
    ถ้าใช้ร่วมกับ io_uring ก็จะยิ่งมีประสิทธิภาพมากขึ้น
    • จริง ๆ แล้วในหนึ่ง system call ก็สามารถส่งข้อมูลหลายชิ้นได้โดยไม่ต้องคัดลอกด้วย
  • คิดว่าปัญหาของอัลกอริทึม Nagle คือ Socket API ไม่มีฟังก์ชันส่งทันที (flush)
    ถ้ามีความสามารถในการล้างบัฟเฟอร์แล้วส่งออกทันทีหลังข้อความที่ต้องตอบสนองฉับไว ก็น่าจะดี
    ทุกวันนี้ช่องสัญญาณ TCP มักปะปนทั้งข้อความแบบ synchronous และ asynchronous ทำให้ยิ่งซับซ้อน
    อยากให้โปรโตคอลอย่าง SCTP ถูกใช้อย่างแพร่หลายมากกว่านี้
    • เห็นด้วยว่า stream API ไม่มีฟังก์ชัน flush เป็นสิ่งที่ขาดไปอย่างชัดเจนในการออกแบบ
    • เข้าใจ ปรัชญาแบบ UNIX ที่พยายามทำให้ network I/O ถูกมองเหมือนไฟล์ แต่ถ้ามี API แบบ message-oriented มาตั้งแต่แรก ปัญหาแบบนี้คงไม่เกิด
      แม้แต่ในการครอบด้วย TLS การหาขอบเขตของข้อความก็ยังยุ่งยาก
    • ถ้าใส่ MSG_MORE ให้กับทุก send แล้วค่อยเอาออกเฉพาะตัวสุดท้าย ก็น่าจะได้ผลคล้าย flush ทางอ้อม
    • stream API ไม่สะดวกในหลายด้าน
      ในอุดมคติควรมีบิต “อนุญาตให้บัฟเฟอร์” เพื่อแบ่งการส่งก้อนใหญ่ และตอนท้ายค่อยระบุว่า “ส่งทันที”
      TCP_CORK ก็เป็นทางเลือกที่คล้ายกันอยู่บ้าง แต่ค่อนข้างหยาบ
      file I/O ก็เจอปัญหาคล้ายกัน
    • สงสัยว่า TCP_CORK คืออะไร
  • (2024) มีการถกเถียงก่อนหน้านี้อยู่ในลิงก์นี้
  • มีการพูดถึงหัวข้อนี้ในพอดแคสต์ตอนหนึ่งของ Oxide and Friends
    เนื้อหาค่อนข้างน่าสนใจ
    • Oxide เป็นบริษัทที่ออกแบบทั้ง server OS และฮาร์ดแวร์ขึ้นมาใหม่ ดังนั้นแนวทางทบทวนโปรโตคอลดั้งเดิมจึงเข้ากับปรัชญาของแบรนด์มาก
  • อัลกอริทึม Nagle ให้ความรู้สึกเหมือนเป็น การยัดนโยบายลงไปในเคอร์เนล จึงดูขัด ๆ
    แอปพลิเคชันควรสามารถควบคุมสมดุลระหว่าง latency และ throughput ได้ด้วยตัวเอง
    • หากไม่มี delayed ack มันก็เป็นอัลกอริทึมที่สมเหตุสมผล และเหตุผลที่มันอยู่ใน TCP stack ก็เพราะพยายามแก้ปัญหาในเลเยอร์นั้น
      แต่ถ้าจะไปทำในระดับแอปพลิเคชันก็ต้องรู้เรื่อง unacked data ซึ่งไม่มีประสิทธิภาพ
    • ในทางทฤษฎีก็จริง แต่ในทางปฏิบัติ โค้ด userspace ส่วนใหญ่ไม่ได้สนใจรายละเอียดระดับล่างของเครือข่าย
      แค่มี ตัวจับเวลา flush 20ms แบบง่าย ๆ ก็คงดีกว่ามากแล้ว
    • จริง ๆ แล้ว TCP_NODELAY ตั้งค่าในระดับซ็อกเก็ต ดังนั้นอาจมองได้ว่าใกล้เคียงกับ การตัดสินใจใน userspace ที่แอปพลิเคชันเลือกเอง
    • trade-off ของโปรแกรมหนึ่งอาจส่งผลต่ออีกโปรแกรมหนึ่งได้ จึงคิดว่าจำเป็นต้องมีเคอร์เนลคอยทำหน้าที่เป็น ตัวกลางไกล่เกลี่ย ในมุมมองของทั้งระบบ