2 คะแนน โดย GN⁺ 2024-05-10 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ปัญหาเรื่องความหน่วงในระบบกระจายมักถูกแก้ได้เพียงแค่เปิดใช้ TCP_NODELAY ซ้ำแล้วซ้ำเล่า ซึ่งสะท้อนว่าพฤติกรรมพื้นฐานของ TCP อาจไม่สอดคล้องกับเวิร์กโหลดสมัยใหม่
  • อัลกอริทึมของ Nagle ถูกสร้างขึ้นใน RFC896 ปี 1984 เพื่อลดต้นทุนส่วนหัวของแพ็กเก็ต TCP ขนาดเล็ก โดยทำงานด้วยการไม่ส่งเซกเมนต์ใหม่ก่อนจะได้รับ ACK
  • เมื่อใช้ร่วมกับ delayed ACK ฝั่งหนึ่งจะรอ ACK ขณะที่อีกฝั่งรอข้อมูลตอบกลับหรือตัวจับเวลา ทำให้ไม่เหมาะกับแอปพลิเคชันแบบ pipeline ที่ไวต่อความหน่วง
  • แม้ RTT ภายในดาต้าเซ็นเตอร์จะอยู่ที่ราว 500μs แต่เซิร์ฟเวอร์สมัยใหม่สามารถประมวลผลได้มากภายในเวลานั้น จึงไม่ชัดเจนว่าการเลื่อนการส่งออกไปหนึ่ง RTT จะให้ประโยชน์คุ้มค่าแค่ไหน
  • ระบบกระจายสมัยใหม่มี TLS, การเข้ารหัส, การทำ serialization และขนาดข้อความระดับแอปพลิเคชันที่ใหญ่ขึ้น ทำให้ปัญหาแพ็กเก็ตขนาด 1 ไบต์ลดลง และในสภาพแวดล้อมที่ไวต่อความหน่วง การปิดใช้อัลกอริทึมของ Nagle ดูเป็นทางเลือกที่เป็นธรรมชาติมากกว่า

การตั้งค่าที่ควรดูก่อนเมื่อดีบักความหน่วง

  • เมื่อเกิดปัญหาความหน่วงในระบบกระจาย มักจะตรวจดูก่อนว่าเปิดใช้ TCP_NODELAY อยู่หรือไม่
  • นักพัฒนาระบบกระจายจำนวนมากเสียเวลากับปัญหาที่แก้ได้ด้วยตัวเลือกซ็อกเก็ตง่าย ๆ เพียงตัวเดียวนี้มานาน
  • การที่เรื่องนี้เกิดซ้ำ ๆ บ่งชี้ว่าพฤติกรรมพื้นฐานของ TCP อาจไม่เหมาะกับระบบกระจายในปัจจุบัน หรืออัลกอริทึมของ Nagle เองอาจล้าสมัยแล้ว

ปัญหาที่อัลกอริทึมของ Nagle พยายามแก้

  • RFC896 เป็นเอกสารปี 1984 ที่กล่าวถึงปัญหาแพ็กเก็ตขนาดเล็ก
  • ในเวลานั้น เมื่อส่งข้อมูลที่เข้ามาทีละตัวอักษร เช่น การพิมพ์จากคีย์บอร์ด ผ่าน TCP จะเกิดความไม่มีประสิทธิภาพจากการที่ข้อมูล 1 ไบต์ มีส่วนหัว 40 ไบต์ ติดไปด้วยทุกครั้ง
    • ข้อมูลจริง 1 ไบต์ต้องแบกรับส่วนหัว 40 ไบต์ คิดเป็น overhead 4000%
    • แม้จะพอทนได้ในโหลดเบา ๆ แต่ไม่ดีต่อ throughput ของเครือข่าย
  • เป้าหมายของอัลกอริทึมของ Nagle คือเพิ่ม throughput โดยเฉลี่ยต้นทุนส่วนหัวของ TCP ให้คุ้มขึ้น
    • แพ็กเก็ตเล็กมักเกิดจากแอปพลิเคชันที่โต้ตอบกับมนุษย์อย่างเชลล์ หรือจากอิมพลีเมนเทชันที่ส่งข้อมูลเข้ามายังเคอร์เนลทีละน้อยผ่านการเรียก write หลายครั้ง
  • หลักการสำคัญคือ หากข้อมูลที่ส่งไปก่อนหน้ายังไม่ได้รับ ACK จะยังไม่ส่งข้อมูลใหม่ออกไปทันทีเป็น TCP segment แยกต่างหาก
  • มักมีการอธิบายอัลกอริทึมของ Nagle ควบคู่กับตัวจับเวลา แต่ตัว RFC896 เองไม่ได้ใช้ตัวจับเวลาแยกต่างหากนอกเหนือจาก round-trip time (RTT) ของเครือข่าย

ความหน่วงที่เกิดเมื่อรวมกับ delayed ACK

  • delayed ACK คือการไม่ส่งการยืนยันรับแพ็กเก็ตทันที แต่รอจนกว่าจะมีข้อมูลจะส่งกลับหรือจนกว่าตัวจับเวลาจะหมด
  • RFC813 เป็นเอกสารยุคแรกในปี 1982 ที่เสนอการหน่วง ACK โดยกล่าวถึงว่าสามารถตั้งตัวจับเวลาให้ผู้รับเลื่อนการส่ง ACK ไปก่อนแล้วค่อยส่งภายหลังได้ในบางสถานการณ์
  • RFC1122 ทำให้ delayed ACK เป็นทางการมากขึ้น
  • ฟีเจอร์ทั้งสองอย่างต่างมีเหตุผลในตัวเอง แต่เมื่อใช้ร่วมกันอาจสร้างความหน่วงได้
    • อัลกอริทึมของ Nagle รอ การได้รับ ACK ก่อนจะส่งข้อมูลเพิ่ม
    • delayed ACK เลื่อน การส่ง ACK ออกไปจนกว่าข้อมูลตอบกลับจะพร้อมหรือตัวจับเวลาหมด
    • แม้จะช่วยให้เติมแพ็กเก็ตได้เต็มขึ้น แต่ไม่เหมาะกับแอปพลิเคชันแบบ pipeline ที่ไวต่อความหน่วง
  • ความเห็นของ John Nagle บน Hacker News ก็มองว่าปัญหานี้ไม่ได้อยู่ที่การป้องกัน tinygram แต่เป็นการรวมกันของการหน่วง ACK กับตัวจับเวลาแบบคงที่
  • นี่เป็นตัวอย่างที่ฟีเจอร์โปรโตคอลสองอย่างซึ่งต่างก็สมเหตุสมผล เมื่อนำมารวมกันกลับก่อให้เกิดพฤติกรรมที่ไม่ต้องการ และเป็นเหตุผลว่าทำไมการออกแบบโปรโตคอลจึงยาก

จุดที่ไม่สอดคล้องกับระบบกระจายสมัยใหม่

  • ต่อให้ไม่มี delayed ACK พฤติกรรมของอัลกอริทึมของ Nagle ก็อาจยังไม่ตรงกับสิ่งที่ระบบกระจายสมัยใหม่ต้องการ
  • ในสภาพแวดล้อมปัจจุบัน RTT เองก็เป็นต้นทุนที่มองข้ามไม่ได้
    • RTT เดี่ยวภายในดาต้าเซ็นเตอร์โดยทั่วไปอยู่ที่ประมาณ 500μs
    • RTT ระหว่างดาต้าเซ็นเตอร์ในรีเจียนเดียวกันอยู่ที่ระดับหลาย ms
    • เส้นทางทั่วโลกอาจสูงถึงหลายร้อย ms
  • เซิร์ฟเวอร์สมัยใหม่สามารถทำงานได้มากภายในเวลาเพียงไม่กี่ร้อย μs จึงยากจะบอกว่าการชะลอการส่งข้อมูลออกไปหนึ่ง RTT นั้นให้ประโยชน์ที่ชัดเจน
  • เหตุผลดั้งเดิมของอัลกอริทึมของ Nagle คือการลด overhead ของส่วนหัว 40 เท่า ที่เกิดจากแพ็กเก็ตขนาด 1 ไบต์
  • ฐานข้อมูลกระจายและระบบกระจายสมัยใหม่โดยทั่วไปไม่ได้ส่งแพ็กเก็ตขนาด 1 ไบต์
    • ตัวข้อมูลที่แอปพลิเคชันส่งมีขนาดใหญ่กว่าเดิม
    • มี overhead เพิ่มจากโปรโตคอลอย่าง TLS
    • ยังมี overhead จากการเข้ารหัสและ serialization เพิ่มเข้ามาอีก
  • ปัญหาที่ต้องหลีกเลี่ยงข้อความขนาดเล็กยังคงสำคัญ แต่ภาระนี้ได้ย้ายไปอยู่ที่ชั้นแอปพลิเคชันอย่างมีประสิทธิภาพแล้ว
  • การส่งข้อมูลที่ห่อด้วย JSON ทีละ 1 ไบต์นั้นไม่มีประสิทธิภาพอยู่แล้ว ไม่ว่าจะมีอัลกอริทึมของ Nagle หรือไม่ก็ตาม

เหตุผลที่มอง TCP_NODELAY เป็นตัวเลือกตั้งต้น

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

TCP_QUICKACK เป็นเพียงตัวเลือกเสริมมากกว่า

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

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

 
GN⁺ 2024-05-10
ความเห็นจาก Hacker News
  • ตลอดอาชีพการทำงานได้แก้ปัญหาความหน่วงที่เกิดจาก อัลกอริทึม Nagle มาหลายครั้งแล้ว จนตอนนี้มันกลายเป็นสิ่งแรกที่นึกสงสัย
    ตัวตรรกะเองก็สมเหตุสมผล แต่ไม่เหมาะกับ workload บางประเภท และมองว่าควรให้นักวิศวกรเลือกอย่างชัดเจนตอนสร้าง socket แทนที่จะปล่อยให้เป็นค่าเริ่มต้นของระบบปฏิบัติการ
    ปัญหาไม่ใช่ว่ามันเป็นออปชันที่ดีหรือแย่ แต่คือมีการตั้งค่าที่เปลี่ยนวิธีส่งข้อมูลอย่างค่อนข้างรุนแรงอยู่ ทั้งที่หลายคนไม่รู้ด้วยซ้ำว่ามันมีอยู่

    • ผมก็คล้ายกัน ทุกครั้งที่เห็น RPC framework ใหม่ ๆ จะชอบไปเปิด GitHub issue ถามว่า “ได้พิจารณา TCP_NODELAY ไหม หรือ framework นี้ทำได้แค่ 20 calls ต่อวินาที?”
      จนถึงตอนนี้เจอบั๊กทุกครั้ง
      ตัวอย่าง: https://cloud-haskell.atlassian.net/browse/DP-108 หรือ https://github.com/agentm/curryer/issues/3
      แต่ผมไม่เห็นด้วยกับคำว่า “ไม่ใช่ออปชันดี/แย่”
      มันคือ heuristic ฝั่ง kernel ที่มีไว้ “ซ่อมแบบมหัศจรรย์” ให้แอปพลิเคชันที่เขียนมาไม่ดี และอย่างที่บทความบอก แอปพลิเคชันที่ปกติไม่ควรทำ write() system call เครือข่ายทีละ 1 ไบต์
      ซอฟต์แวร์แบบนั้นควรถูกแก้
      กรณีที่ฟีเจอร์นี้ยังพอฟังขึ้นคือถ้าคุณเป็นผู้ดูแลระบบ kernel และอยู่ในสถานการณ์หายากที่แก้ซอฟต์แวร์บนเครื่องไม่ได้ด้วยเหตุผลอย่างการเมืองในทีม
      นอกเหนือจากนั้น มันทำให้ซอฟต์แวร์ที่ปกติดีอยู่แล้วซับซ้อนขึ้น
      หมายความว่าเราต้องปิดเวทมนตร์ประหลาดที่ใส่มาเพื่อเพิ่ม throughput ให้ซอฟต์แวร์ที่เขียนไม่ดีเพียงเล็กน้อยอย่างชัดเจน และสำหรับซอฟต์แวร์ที่เขียนมาถูกต้อง มันกลับสร้างความหน่วงใหญ่แบบคาดไม่ถึง
      John Nagle บอกในเธรดที่ลิงก์จากที่นี่ว่า delayed ACK แย่กว่า ซึ่งผมเห็นด้วย
      แต่รูปแบบ Send/Send/Receive ที่อัลกอริทึม Nagle ทำให้แย่ลงนั้นเป็นกรณีใช้งานที่สมเหตุสมผลและพบบ่อยมาก และครอบคลุมทุกอย่างที่ทำ pipeline RPC บน TCP
      ผมคิดว่าทั้ง delayed ACK และอัลกอริทึม Nagle ควรถูกปิดไว้เป็นค่าเริ่มต้น
      ชื่อก็ควรเป็นประมาณ TCP_DELAY และเปิดใช้เฉพาะตอนที่ไม่อยากทำ buffering พื้นฐานใน user space เองเท่านั้น
      คนไม่ควรจำเป็นต้องรู้เรื่องแบบนี้ และพฤติกรรมค่าเริ่มต้นก็ควรเป็นแบบที่ไม่ทำให้แปลกใจ
    • ถ้าจุดประสงค์หลักคือแก้แอปพลิเคชันที่มีพฤติกรรม write แย่ ๆ ตัวเลือกเปิด TCP_DELAY ก็ดูประหลาดทีเดียว
      เพราะมันแปลว่าต้องการวิศวกรซอฟต์แวร์ที่ฉลาดพอจะรู้จักออปชันนี้ แต่ไม่ฉลาดพอจะแบ่งการเรียก write ให้ดี หรือสร้าง buffering แบบ Nagle ที่เหมาะกับแอปของตัวเองได้
    • เห็นด้วย ในสาย การเทรดความถี่สูง/ความหน่วงต่ำ การปิดอัลกอริทึม Nagle เป็นเรื่องที่รู้กันมานานมากแล้ว น่าจะเกิน 15 ปี และก็เป็นหนึ่งในอย่างแรก ๆ ที่ผมจะเช็ก
    • สิ่งที่อยากได้จริง ๆ คือกำหนดความหน่วงเป็น n ไมโครวินาที แต่ไม่มีวิธีที่ดีนอกจากใส่ buffering ใน user space เองไว้หน้าระบบเรียก syscall
      ถ้าไม่มีอะไรอย่าง io_uring มาชดเชยต้นทุนของ system call ฝั่ง user space จะทำงานได้ดีกว่า
    • ตรรกะนี้เดิมทีออกแบบมาสำหรับอะไรอย่าง Telnet session
      ถ้าจำไม่ผิด นั่นคือแรงจูงใจหลัก
  • ข้อสรุปดูแปลก ๆ หน่อย อัลกอริทึม Nagle เป็นความพยายามทำ batching อย่างชัดเจน และไม่ว่าฮาร์ดแวร์ เครือข่าย แอปพลิเคชัน หรือ use case จะเป็นแบบไหน บางสถานการณ์ batching ก็ย่อมดีกว่า
    ทุกวันนี้งานคอมพิวติ้งจำนวนมากก็ยังใช้ batching และแอปเครือข่ายก็ได้ประโยชน์
    โปรโตคอลระดับสูงรุ่นใหม่กว่าอย่าง QUIC ก็ทำ batching การเขียน และแทบจะย้ายการจัดการการเชื่อมต่อและข้อผิดพลาดแบบอิสระของ TCP ขึ้นไปไว้ใน user space เพื่อให้โปรโตคอลดันข้อมูลเข้าแอปพลิเคชันได้เร็วที่สุด และให้ตัวแอปเป็นผู้รับผิดชอบการเชื่อมต่อและข้อผิดพลาดของแต่ละสตรีม แทนที่จะเป็น host TCP/IP stack หรือ router
    ถ้าวันหนึ่งเครือข่ายกลับไปอิ่มตัวอีกแบบสมัยก่อน อัลกอริทึม Nagle ก็จะกลับมาในรูปแบบดัดแปลงของ QUIC และน่าจะอยู่ลึกลงไปในโค้ดแอปมากขึ้น เป็นลักษณะรอส่งแพ็กเก็ต QUIC จนกว่าจะถึงเกณฑ์บางอย่าง
    ทุกอย่างในเทคโนโลยีจะถูกประดิษฐ์ซ้ำเมื่อฮาร์ดแวร์หรือซอฟต์แวร์ไปชนคอขวด เพราะประสิทธิภาพของทั้งสองฝั่งไม่ได้โตด้วยอัตราเท่ากัน สุดท้ายก็เลยเป็นแบบนั้นเสมอ
    นอกจากแบนด์วิดท์แล้ว อัลกอริทึม Nagle ก็ยังมีประโยชน์ในกรณีที่อิ่มตัวด้วยจำนวนแพ็กเก็ตต่อวินาทีเพราะมีแพ็กเก็ตเล็ก ๆ มากเกินไป

    • ความต่างระหว่าง QUIC กับ TCP อยู่ที่บาปดั้งเดิมของ TCP และบรรพบุรุษของมัน นั่นคือการเลียนแบบ การเชื่อมต่อพอร์ตอนุกรมแบบอะซิงโครนัสที่มองไม่เห็นชั้นข้อความ
      มันทำให้สามารถเข้าถึงบริการผ่าน teletyper ทางกายภาพได้ แต่ก็ทำให้ TCP ไม่รู้ขอบเขตของข้อความ และถึงตอนนี้แม้จะพอยัดความรู้นั้นกลับเข้าไปได้บางส่วน ซอฟต์แวร์ยุคแรกก็ไม่ได้มีแบบนั้น
      ในทางกลับกัน โปรโตคอลที่ไม่ใช่ TCP จำนวนมากอย่าง QUIC, SCTP และ TP4 ให้ขอบเขตข้อความอย่างชัดเจน
      อินเทอร์เฟซกับระบบจึงไม่ใช่พอร์ตอนุกรมที่จำลองขึ้น แต่เป็นแบบอิงข้อความที่อย่างมากก็แค่ต้องประกอบกลับ
    • ถูกต้อง แต่ implementation นี้โดยเฉพาะพึ่งพา heuristic ในการตัดสินวิธีทำ batching และดูเหมือนว่าสมมติฐานนั้นจะใช้ไม่ได้จริง
    • การทำ batching ควรถูก ควบคุมโดยแอปพลิเคชัน ไม่ใช่โปรโตคอล
      โปรโตคอลไม่มีบริบทมากพอจะทำ batching ได้อย่างถูกต้อง
  • ในทางกลับกัน จะปิด delayed ACK ดีไหม
    ปัญหาเกิดจากพฤติกรรมผิดปกติที่เกิดเมื่อการป้องกันแพ็กเก็ตขนาดเล็กกับ delayed ACK มีปฏิสัมพันธ์กัน
    ตัวเลือกที่เปิดเผยสำหรับปิดการป้องกันแพ็กเก็ตขนาดเล็กคือ TCP_NODELAY แล้ว delayed ACK จะปิดอย่างไรได้บ้าง
    เวลาที่อยาก benchmark ทั้งสี่แบบผสมเพื่อดูว่าแบบไหนเข้ากันได้ดีที่สุดน่ะ
    ลองค้นดูนิดหน่อยพบว่า Linux มี socket option TCP_QUICKACK แต่ต้องตั้งค่าใหม่ทุกครั้งที่มีการรับข้อมูล
    ยังมี /proc/sys/net/ipv4/tcp_delack_min และ /proc/sys/net/ipv4/tcp_ato_min ด้วย
    ส่วน FreeBSD มี net.inet.tcp.delayed_ack และ net.inet.tcp.delacktime

    • TCP_QUICKACK แก้ปัญหาในรูปแบบที่แย่ที่สุดได้ แต่ไม่ได้แก้ปัญหาทั้งหมด
      Nagle algorithm ยังอาจรอก่อนได้สูงสุดหนึ่งรอบเวลาไป-กลับก่อนส่งข้อมูล และถ้าทำตาม RFC ก็แทบไม่มีประโยชน์นอกจากเพิ่มความหน่วง
    • ใช่เลย การที่ TCP_QUICKACK ต้องตั้งทุกครั้งที่มีการรับข้อมูลนี่ เขาคิดอะไรกันอยู่
      จะอยากปิดไว้แค่บางช่วงเวลาทำไม
    • บน CentOS/RedHat สามารถเพิ่ม quickack 1 ที่ท้าย route เพื่อปิด delayed ACK ของเส้นทางนั้นได้
  • ในโลกที่แบนด์วิดท์มีจำกัด และขนาดต่ำสุดของแพ็กเก็ตคือ 64 ไบต์พร้อมทั้งยังต้องมี inter-frame gap การส่ง TCP packet ต่อหนึ่งไบต์ข้อมูลถือเป็นการสิ้นเปลืองแบนด์วิดท์อย่างมหาศาล
    บนเครือข่าย Ethernet ส่วนใหญ่ ทุกวันนี้ขนาดต่ำสุดก็ยังเป็นแบบนั้น และการส่ง ACK เปล่า ๆ ก็เช่นกัน
    แต่จุดยืนพื้นฐานของฉันคือ ไม่ใช่ TCP_NODELAY แต่เป็นแค่ TCP

    • อยากให้มีโปรโตคอลที่มีกลไกในตัวสำหรับตรวจจับได้ว่าท่อของฝั่งตรงข้ามขาดไปไม่ว่าด้วยเหตุผลอะไร
    • QUIC(https://en.wikipedia.org/wiki/QUIC) ไม่ควรเป็นตัวที่แก้ปัญหาเรื่องความหน่วงแบบนี้ของ TCP หรือ
  • ผมยังไม่ค่อยเชื่อกับตรรกะที่ว่า Nagle ไม่จำเป็นอีกต่อไป
    Telnet อาจไม่สำคัญแล้วในปัจจุบัน แต่ก็น่าจะยังมีแอปพลิเคชันจำนวนมากที่ทำอะไรประมาณนี้อยู่
    write(fd, "Host: "), write(fd, hostname), write(fd, "\r\n"), write(fd, "Content-type: ") เป็นต้น
    ถึงจะไม่ใช่ overhead 40 เท่า แต่ก็น่าจะเป็นประมาณ 5 เท่าได้

    • ก็แก้แอปพลิเคชันสิ
      เวลาจะเขียนลงไฟล์ก็คงไม่ทำแบบนี้แล้วคาดหวังประสิทธิภาพแบบปาฏิหาริย์ ทั้งที่ระบบปฏิบัติการก็มีบัฟเฟอร์ของมันเองอยู่แล้ว
      ไม่มีเหตุผลที่จะคาดหวังต่างออกไปเวลาเขียนลง socket และแต่เดิม Nagle ก็ไม่ได้ช่วยคุณจาก overhead ของ system call อยู่แล้ว
    • พอเห็นพูดถึง Telnet ก็เลยสงสัยว่า OpenSSH ทำอะไรอยู่ และพบว่ามันตั้ง TCP_NODELAY ให้กับทุกการเชื่อมต่อ รวมถึง interactive session ด้วย
      ยืนยันได้ทั้งจากการอ่านโค้ดและจากการสังเกตพฤติกรรมด้วย strace
    • ถ้ามองว่าใช้ asynchronous I/O การเอาข้อมูลไปใส่บัฟเฟอร์แทนที่จะค้างรอทุก write(2) เล็ก ๆ ดูจะเป็นแนวทางเดียวที่สมเหตุสมผล ดังนั้นรูปแบบนี้คงไม่ได้พบบ่อยมากแล้ว
      ฝั่งเซิร์ฟเวอร์มักต้องใช้ asynchronous I/O เพื่อให้ scale ได้ดี และฝั่งไคลเอนต์เองการค้างรอ network call ก็เป็นประสบการณ์ที่ไม่ดี
      โดยเฉพาะในทุกวันนี้ที่เครือข่ายเปลี่ยนแปลงบ่อยและหลุดนอกระยะได้ง่ายก็ยิ่งเป็นเช่นนั้น
    • ไม่ควรให้ทั้งอินเทอร์เน็ตต้องรับโทษเพียงเพราะนักพัฒนาบางคนเขียนโค้ดได้แย่
    • แต่แรกก็ไม่ควรทำแบบนี้อยู่แล้ว
      ต่อให้ไม่นับเรื่องเครือข่าย system call ก็มีต้นทุนค่อนข้างสูง จึงส่งผลเสียต่อประสิทธิภาพ
  • สงสัยว่ามีวิธีที่ดีในการเปิด TCP_NODELAY ให้กับ socket เมื่อเราเข้าถึง source ของแอปพลิเคชันไม่ได้หรือไม่
    หาไม่เจอทั้งการตั้งค่า kernel แบบถาวร หรือคำสั่งที่ใช้เปลี่ยนทีหลัง
    แม้จะปิด delayed ACK ได้ด้วยการใส่ quickack 1 ใน routing table แต่การเปิด TCP_NODELAY จากภายนอกแอปพลิเคชันดูยากเป็นพิเศษ
    ช่วงนี้ผมกำลังเจอปัญหาตามที่อธิบายไว้นี้แบบตรงตัว ระหว่างแอปพลิเคชันที่ผมเป็นเจ้าของกับแอปพลิเคชัน closed-source ที่มันโต้ตอบด้วย

    • อะไรอย่าง LD_PRELOAD interception สำหรับ socket(2) น่าจะใช้ได้ไหม
      คือเรียกฟังก์ชันจริงก่อน จากนั้นค่อย setsockopt อะไรทำนองนั้น แล้วคืน socket ที่แก้ไขแล้วกลับไป
    • ขึ้นอยู่กับสถานการณ์ที่เจาะจง อาจแทรก socat ไว้ตรงกลางได้
      จากเดิม your_app —> server ก็ให้กลายเป็น your_app -> localhost_socat -> server
      socat มีตัวเลือก command line สำหรับตั้งค่า tcp_nodelay
      แต่ก็ต้องเกลี้ยกล่อมให้แอป closed-source ไปเชื่อมต่อกับ localhost ให้ได้
      ถ้ามันทำ DNS lookup ก็อาจบังคับให้เชื่อมต่อ localhost ได้ด้วย entry ใน /etc/hosts
      แอปจะคุยกับ socat ผ่าน local socket ดังนั้น tcp_nodelay ฝั่งแอปจึงไม่ส่งผล
    • ถ้าแนบ debugger แล้วใช้ ptrace เพื่อเรียก setsockopt ได้ล่ะ
    • การเปิด /proc//fd/ แล้วตั้งค่า socket option จากตรงนั้นอาจใช้ได้ แม้ผมจะยังไม่ได้ทดสอบ
    • LD_PRELOAD
  • ราว ๆ 15 ปีก่อนผมเคยเล่น MMO ที่เน้นความเรียลไทม์มากเกมหนึ่ง และการสื่อสารทั้งหมดใช้ TCP
    เวลากดปุ่มแล้วการกระทำของตัวเองยังไม่แสดงบนหน้าจอจนกว่าจะมี response packet กลับมา
    สุดท้ายเด็ก ๆ ที่เล่นเกมนี้กัน รวมถึงผมเอง ต่างก็พบว่าถ้าเปิด TCP_NODELAY เกมจะลื่นขึ้นมาก
    โดยเฉพาะผู้เล่นฝั่ง California ที่อยู่ใกล้กับเกมเซิร์ฟเวอร์จะเห็นผลชัดมาก

    • ไม่แน่ใจว่าหมายถึง WoW หรือเปล่า แต่ช่วงนั้นมีการอัปเดตเกมที่เปลี่ยนเรื่องนี้พอดี และน่าจะเปลี่ยนอย่างอื่นด้วย
      ผลข้างเคียงที่น่าสนใจคือ ก่อนเปลี่ยนถ้า TCP stream ค้าง เกมจะค้างไปพักหนึ่งก่อนจะเล่นเหตุการณ์รับเข้าที่พลาดไปแบบเร็วมาก
      ซึ่งโดยมากก็คือฉากที่ผมตายนั่นเอง
      แต่หลังเปลี่ยนมันกลับกลายเป็นว่าการเชื่อมต่อหลุดไปเลยแทน
  • ตอนของพอดแคสต์ Oxide and Friends ที่เกี่ยวข้อง: https://www.youtube.com/watch?v=mqvVmYhclAg

    • เป็นตอนที่ยอดเยี่ยมมาก และแสดงให้เห็นความสำคัญของ visualization ได้อย่างชัดเจน
  • ถ้าใช้ภาษาสมัยใหม่ที่เปิด TCP_NODELAY เป็นค่าเริ่มต้นอย่าง Go ก็ไม่เข้าข่ายนี้ :-)

    • https://news.ycombinator.com/item?id=34179426
      https://github.com/golang/go/issues/57530
      ไม่เคยรู้มาก่อน
    • อย่างน้อย Node.js ก็ทำแบบนี้มาตั้งแต่ปี 2020
    • ต้องใช้ทั้งภาษาเพื่อสิ่งนั้นเลยเหรอ
      แค่ใช้ไลบรารีเครือข่ายที่ “ทันสมัย” ไม่ได้หรือ
  • ไม่ได้เป็นแบบนั้นทุกครั้ง บางทีก็เป็น DNS

    • ครั้งหนึ่ง line card ของเราเตอร์เสีย ทำให้บิตสุดท้ายของ IPv4 address กลายเป็น 0 เลยกลายเป็นทิกเก็ตแนว “เข้าได้เฉพาะ IPv4 address เลขคู่”
    • สำหรับผม ครั้งหนึ่งเป็นเพราะกระจกสกปรก
      ฝุ่นจากใกล้ไซต์ก่อสร้างไปเกาะในช่องว่างระหว่างเลเซอร์กับใยแก้วนำแสงของเราเตอร์ ทำให้สัญญาณถูกลดทอนลงมากพอจนเห็น packet loss 40~50%
      หลังจากไล่หาจุดที่สูญหาย NOC ก็ส่งอีเมลไปหาผู้ให้บริการทรานสปอร์ตเจ้านั้น และหนึ่งวันต่อมาช่างเทคนิคที่ถูกส่งไปหน้างานก็ตอบกลับมาเล่าเรื่องนี้
    • ทุก ๆ 50 ปี ครั้งหนึ่ง ที่ระยะห่าง 2 พันล้านกิโลเมตร มันอาจเป็นชิปหน่วยความจำที่เสียก็ได้
      แต่โดยปกติก็ยังแพตช์อ้อมได้ เลยไม่ใช่เรื่องใหญ่มาก
    • แล้วก็อย่าลืม BGP หรือดิสก์เต็มแบบไม่มีการแจ้งเตือนด้วย
    • ถ้าล้มเหลวก็เป็น DNS และถ้าแค่หยุดขยับก็เป็น TCP_NODELAY หรือ stream buffering
      Web ซึ่งเป็นระบบที่ซับซ้อนมากก็ล้มเหลวได้เพราะแคชเหมือนกัน