- เมื่อต้องดีบักปัญหา 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ตอนนั้นโฮสต์หลายเครื่องใช้ช่องสัญญาณ Ethernet ร่วมกัน จึงใช้ CSMA/CD เพื่อหลีกเลี่ยงการชนกัน
แต่ปัจจุบัน Ethernet ส่วนใหญ่เป็นโครงสร้างแบบ point-to-point และอยู่ในสภาพแวดล้อม full duplex ที่รับส่งพร้อมกันได้
ดังนั้น CSMA จึงไม่จำเป็นอีกต่อไป และคิดว่าการตั้งค่า TCP_NODELAY เพื่อปิดการทำงานของอัลกอริทึม Nagle เป็นสิ่งที่สมเหตุสมผลในกรณีส่วนใหญ่
การตั้งให้เป็นค่าเริ่มต้นถือเป็นหนึ่งในความผิดพลาดครั้งใหญ่ในประวัติศาสตร์เครือข่าย
ราวปี 2014 ตอนเปลี่ยนสวิตช์ในดาต้าเซ็นเตอร์ เคยต้องคงอุปกรณ์เก่าบางส่วนไว้เพราะไม่รองรับ 10Mbit half duplex
เพราะช่วยป้องกันการสร้างแพ็กเก็ตที่เล็กเกินไป
Nagle เป็นการปรับแต่งในชั้น TCP เพื่อเพิ่มประสิทธิภาพด้วยการรวมแพ็กเก็ตเล็ก ๆ
ส่วน CSMA เป็นปัญหาในชั้นกายภาพ/ดาต้าลิงก์ ซึ่งแยกจาก Nagle
แบ็กเอนด์ที่เขียนด้วย Go เปิดใช้ TCP_NODELAY เป็นค่าเริ่มต้นอยู่แล้วเลยไม่ใช่สาเหตุ แต่ส่วนที่พูดถึงการรับรู้ปัญหาของ Nagle น่าสนใจมาก
มีการถกเถียงก่อนหน้านี้ด้วย ดูได้ในเธรดนี้
ใน การสื่อสารแบบแชตตี้ อย่างโปรโตคอล DICOM การตั้ง TCP_NODELAY=1 ช่วยเพิ่ม throughput ได้มาก
ดูลิงก์ที่เกี่ยวข้อง
คิดว่าใน workload ยุคนี้ delayed ACK ไม่ได้ให้ประโยชน์มากนัก
ในสภาพแวดล้อมสมัยใหม่ที่มี HTTP เป็นศูนย์กลาง การปิดทั้ง Nagle และ delayed ACK น่าจะดีกว่า
เพราะ RTT ระหว่างดาต้าเซ็นเตอร์อยู่ในระดับหลายร้อยไมโครวินาที การเพิ่มความหน่วงแม้เพียงหนึ่ง RTT ก็อาจกลับกลายเป็นผลเสีย
ลิงก์วิกิ
แอปพลิเคชันควรเป็นฝ่ายตัดสินใจเองว่าจะส่งเมื่อไรและจะบัฟเฟอร์เมื่อไร
บน Linux มันเป็น hint ให้เคอร์เนลรู้ว่าจะมีข้อมูลเพิ่มเติมส่งตามมาในไม่ช้า ซึ่งมีประโยชน์เวลาแยกส่ง header กับข้อมูล
ถ้าใช้ร่วมกับ io_uring ก็จะยิ่งมีประสิทธิภาพมากขึ้น
ถ้ามีความสามารถในการล้างบัฟเฟอร์แล้วส่งออกทันทีหลังข้อความที่ต้องตอบสนองฉับไว ก็น่าจะดี
ทุกวันนี้ช่องสัญญาณ TCP มักปะปนทั้งข้อความแบบ synchronous และ asynchronous ทำให้ยิ่งซับซ้อน
อยากให้โปรโตคอลอย่าง SCTP ถูกใช้อย่างแพร่หลายมากกว่านี้
แม้แต่ในการครอบด้วย TLS การหาขอบเขตของข้อความก็ยังยุ่งยาก
ในอุดมคติควรมีบิต “อนุญาตให้บัฟเฟอร์” เพื่อแบ่งการส่งก้อนใหญ่ และตอนท้ายค่อยระบุว่า “ส่งทันที”
TCP_CORK ก็เป็นทางเลือกที่คล้ายกันอยู่บ้าง แต่ค่อนข้างหยาบ
file I/O ก็เจอปัญหาคล้ายกัน
เนื้อหาค่อนข้างน่าสนใจ
แอปพลิเคชันควรสามารถควบคุมสมดุลระหว่าง latency และ throughput ได้ด้วยตัวเอง
แต่ถ้าจะไปทำในระดับแอปพลิเคชันก็ต้องรู้เรื่อง unacked data ซึ่งไม่มีประสิทธิภาพ
แค่มี ตัวจับเวลา flush 20ms แบบง่าย ๆ ก็คงดีกว่ามากแล้ว