5 คะแนน โดย GN⁺ 2024-07-29 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การปรับจูนประสิทธิภาพเครือข่ายบน Linux คือการตีความเส้นทางที่แพ็กเก็ตเคลื่อนจาก บัฟเฟอร์ริงของ NIC, IRQ, NAPI, softIRQ, qdisc, บัฟเฟอร์ TCP ไปจนถึงซ็อกเก็ตของแอปพลิเคชัน โดยมองจากจุดคอขวดเป็นหลัก
  • ในเส้นทางรับข้อมูล NIC จะเขียนแพ็กเก็ตลง RAM ด้วย DMA แล้วสร้าง HardIRQ จากนั้นไดรเวอร์จะกำหนดเวลาให้ NAPI ทำงาน เพื่อให้ NET_RX_SOFTIRQ ล้างบัฟเฟอร์ริง แล้วส่งต่อไปยังเลเยอร์ IP/TCP และบัฟเฟอร์รับของซ็อกเก็ต
  • ethtool, /proc/net/softnet_stat, ss, netstat, sysctl คือจุดเริ่มต้นของการสังเกตและปรับแต่ง โดยแกนหลักได้แก่ interrupt coalescing, IRQ affinity, RSS/RPS/RFS/aRFS, netdev_budget, netdev_max_backlog, txqueuelen, บัฟเฟอร์ read/write ของ TCP
  • ไม่มีค่าตั้งเดียวที่ใช้ได้กับทุกระบบ การเพิ่มบัฟเฟอร์ริงอาจช่วยลดการดรอปได้แต่เพิ่ม latency และ interrupt coalescing อาจลดการใช้ CPU กับ HardIRQ ได้ แต่แลกมาด้วยต้นทุนด้าน latency
  • การประมวลผลแพ็กเก็ตประสิทธิภาพสูงสามารถขยายไปใช้ตัวเลือกอย่าง PACKET_MMAP, DPDK, PF_RING, XDP/AF_XDP ได้ แต่แต่ละแนวทาง ไม่ว่าจะเป็นการข้ามเคอร์เนล zero-copy หรือ fast path ภายในเคอร์เนล ล้วนมีข้อพึ่งพาฮาร์ดแวร์ การครอง CPU และข้อกำหนดเวอร์ชันเคอร์เนลที่ต่างกัน

เส้นทางรับข้อมูลของ Linux: จาก NIC ถึงซ็อกเก็ต

  • อุปกรณ์เครือข่ายจะสร้าง IRQ เพื่อแจ้งว่ามีแพ็กเก็ตมาถึง และการแมป IRQ ของ Linux จะถูกเก็บไว้ใน /proc/interrupts
  • IRQ handler ทำงานด้วยลำดับความสำคัญสูงมาก และบางกรณีจะป้องกันไม่ให้มีการสร้าง IRQ เพิ่ม ไดรเวอร์จึงเลื่อนงานที่ใช้เวลานานออกไปนอกบริบทของ IRQ
  • การประมวลผลแบบเลื่อนเวลานี้ใช้ softIRQ และในการประมวลผลรับข้อมูลเครือข่าย จะมีการสร้างเธรดเคอร์เนล ksoftirqd/<cpu-number>, softnet_data, poll_list สำหรับแต่ละ CPU
  • net_dev_init ลงทะเบียน NET_RX_SOFTIRQ กับระบบ softIRQ และ handler ที่เกี่ยวข้องคือ net_rx_action
  • การมาถึงของแพ็กเก็ตและการประมวลผล NAPI

    • NIC บันทึกข้อมูลที่ได้รับจากเครือข่ายลงในบัฟเฟอร์ริงใน RAM ด้วย DMA
    • NIC บางรุ่นเป็น multiqueue NIC ที่มีบัฟเฟอร์ริงหลายชุด
    • เมื่อ NIC สร้าง HardIRQ แล้ว IRQ handler ของไดรเวอร์จะทำงาน
    • ไดรเวอร์จะเคลียร์ IRQ ของ NIC และเรียก napi_schedule เพื่อเริ่มลูป poll ของ NAPI softIRQ
    • napi_schedule เพิ่มโครงสร้าง NAPI poll ของไดรเวอร์เข้าใน poll_list ของ CPU ปัจจุบัน และตั้งค่า softIRQ pending bit
    • เมื่อ ksoftirqd เรียก __do_softirq handler net_rx_action ของ NET_RX_SOFTIRQ ที่อยู่ในสถานะ pending จะทำงาน
  • GRO และการเข้าสู่โปรโตคอลสแต็ก

    • net_rx_action ตรวจสอบ NAPI poll list และตรวจสอบ budget กับเวลาที่ผ่านไป เพื่อไม่ให้ softIRQ ยึด CPU ไว้แต่ผู้เดียว
    • ฟังก์ชัน poll ของไดรเวอร์จะเก็บเกี่ยวแพ็กเก็ตจากบัฟเฟอร์ริงใน RAM
    • แพ็กเก็ตถูกส่งต่อไปยัง napi_gro_receive
    • GRO(Generic Receive Offloading) เป็นเทคนิค offloading แบบซอฟต์แวร์ที่รวมแพ็กเก็ตขนาดเล็กกลับเป็นแพ็กเก็ตขนาดใหญ่ เพื่อลดจำนวนแพ็กเก็ตที่แอปพลิเคชันต้องประมวลผล
    • หาก GRO ไม่พักแพ็กเก็ตไว้ แพ็กเก็ตจะเคลื่อนขึ้นไปยังโปรโตคอลสแต็กผ่าน netif_receive_skb
  • การแยกเส้นทางตามการเปิดใช้ RPS

    • กรณีที่ RPS ถูกปิดใช้งาน:
      • netif_receive_skb ส่งข้อมูลต่อไปยัง __netif_receive_core
      • __netif_receive_core ส่งข้อมูลต่อไปยัง tap และ handler ของเลเยอร์โปรโตคอลที่ลงทะเบียนไว้
    • กรณีที่ RPS ถูกเปิดใช้งาน:
      • netif_receive_skb ส่งข้อมูลต่อไปยัง enqueue_to_backlog
      • แพ็กเก็ตเข้าสู่ input queue ของแต่ละ CPU
      • โครงสร้าง NAPI ของ CPU ระยะไกลถูกเพิ่มเข้าใน poll_list ของ CPU นั้น และมีการคิว IPI เพื่อปลุกเธรด softIRQ ของ CPU ระยะไกล
      • ksoftirqd ของ CPU ระยะไกลใช้ฟังก์ชัน poll process_backlog เพื่อเก็บเกี่ยวแพ็กเก็ตจาก CPU input queue
  • IP, TCP และบัฟเฟอร์รับของซ็อกเก็ต

    • แพ็กเก็ตถูกรับที่เลเยอร์ IPv4 ผ่าน ip_rcv แล้วผ่าน netfilter และการปรับแต่ง routing ให้เหมาะสม
    • ข้อมูลที่มุ่งมายังระบบปัจจุบันจะถูกส่งต่อไปยังเลเยอร์โปรโตคอลระดับสูง เช่น UDP หรือ TCP
    • ในเส้นทางรับของ TCP แพ็กเก็ตจะผ่าน tcp_v4_rcv, TCP finite state machine และการค้นหาซ็อกเก็ต ก่อนเข้าสู่บัฟเฟอร์รับ
    • ขนาดบัฟเฟอร์รับเป็นไปตามกฎของ tcp_rmem
    • หากเปิดใช้ tcp_moderate_rcvbuf เคอร์เนลจะปรับบัฟเฟอร์รับโดยอัตโนมัติ
    • tcp_rmem เก็บค่าต่ำสุด ค่าเริ่มต้น และค่าสูงสุดของบัฟเฟอร์รับของซ็อกเก็ต TCP
    • การใช้ SO_RCVBUF จะปิดการปรับบัฟเฟอร์รับอัตโนมัติของซ็อกเก็ตนั้น
    • net.core.rmem_max คือเพดานของขนาดบัฟเฟอร์รับ TCP และหน้าต่างที่ใหญ่ขึ้นสามารถทำให้ส่งข้อมูลได้มากขึ้นก่อนส่ง ACK ช่วยลด latency และเพิ่ม throughput

เส้นทางส่งข้อมูลของ Linux: จากแอปพลิเคชันถึง NIC

  • เส้นทางส่งข้อมูลเรียบง่ายกว่าเส้นทางรับ แต่มี qdisc, TCP write buffer, DMA และ IRQ เข้ามาเกี่ยวข้อง
  • เมื่อแอปพลิเคชันส่งข้อความด้วยการเรียกอย่าง sendmsg เส้นทางส่งของ TCP จะจัดสรร skb_buff
  • แพ็กเก็ตเข้าสู่ write buffer ของซ็อกเก็ตตามขนาด tcp_wmem
    • tcp_wmem เก็บค่าต่ำสุด ค่าเริ่มต้น และค่าสูงสุดของบัฟเฟอร์ส่งของซ็อกเก็ต TCP
    • เคอร์เนลปรับขนาดบัฟเฟอร์ส่ง TCP แบบไดนามิกระหว่างค่าต่ำสุดและค่าสูงสุด
    • การใช้ SO_SNDBUF จะปิดการปรับบัฟเฟอร์ส่งอัตโนมัติของซ็อกเก็ตนั้น
    • net.core.wmem_max คือเพดานของขนาดบัฟเฟอร์ส่ง TCP
  • มีการสร้าง TCP header และ IP header จากนั้นผ่าน LOCAL_OUT, routing, POST_ROUTING, fragmentation แล้วเรียกฟังก์ชันส่ง L2 ผ่าน dev_queue_xmit
  • qdisc ฝั่งขาออกใช้ความยาว txqueuelen และอัลกอริทึม default_qdisc
  • ไดรเวอร์ใส่แพ็กเก็ตลงใน TX ring buffer และทำ NET_TX_SOFTIRQ หลังจาก timeout tx-usecs หรือหลัง tx-frames
  • NIC ดึงแพ็กเก็ตจาก RAM ด้วย DMA แล้วส่งออกไป และสร้าง HardIRQ หลังส่งเสร็จ
  • ไดรเวอร์ประมวลผล IRQ นี้และกำหนดเวลาให้ระบบ NAPI poll เพื่อคืนหน่วยความจำ

เครื่องมือสังเกตการณ์และจุดตรวจสอบพื้นฐาน

  • /proc/net/softnet_stat

    • แต่ละบรรทัดของ /proc/net/softnet_stat แทนคอร์ CPU หนึ่งคอร์ โดยเริ่มจาก CPU0
    • สถิติในแต่ละคอลัมน์แสดงเป็น เลขฐานสิบหก
    • คอลัมน์ที่ 1 คือจำนวนเฟรมที่ interrupt handler ได้รับ
    • คอลัมน์ที่ 2 คือจำนวนเฟรมที่ถูกดรอปเพราะเกิน netdev_max_backlog
    • คอลัมน์ที่ 3 คือจำนวนครั้งที่ ksoftirqd ใช้ netdev_budget หรือเวลา CPU หมด ทั้งที่ยังมีงานค้างให้ประมวลผล
    • คอลัมน์ที่เหลืออาจแตกต่างกันไปตามเวอร์ชัน Linux
  • /proc/net/sockstat และ ss

    • ใน /proc/net/sockstat ให้ตรวจสอบฟิลด์ mem
    • ค่านี้คำนวณจากผลรวมของ sk_buff->truesize ของทุกซ็อกเก็ต
    • ss เป็นเครื่องมือสำหรับ dump สถิติซ็อกเก็ต และสามารถแสดงข้อมูลที่คล้ายกับ netstat รวมถึงข้อมูล TCP และสถานะที่มากกว่าได้
    • ss -tm ใช้ตรวจสอบการใช้หน่วยความจำของ TCP socket
    • rmem_alloc: หน่วยความจำที่จัดสรรให้แพ็กเก็ตขาเข้า
    • rcv_buf: หน่วยความจำทั้งหมดที่สามารถจัดสรรให้แพ็กเก็ตขาเข้าได้
    • wmem_alloc: หน่วยความจำที่ใช้กับแพ็กเก็ตขาออกที่ถูกส่งต่อไปยังเลเยอร์ 3 แล้ว
    • snd_buf: หน่วยความจำทั้งหมดที่สามารถจัดสรรให้แพ็กเก็ตขาออกได้
    • wmem_queued: หน่วยความจำที่จัดสรรให้แพ็กเก็ตขาออกที่ยังไม่ได้ถูกส่งต่อไปยังเลเยอร์ 3
    • sock_drop: จำนวนแพ็กเก็ตที่ถูกดรอปก่อนถูก demultiplex ไปยังซ็อกเก็ต
  • netstat และ sysctl

    • netstat เป็นเครื่องมือบรรทัดคำสั่งที่แสดงการเชื่อมต่อเครือข่ายที่เปิดอยู่และสถิติของ protocol stack โดยดึงข้อมูลจากไฟล์ระบบ /proc/net/
    • /proc/net/dev: ข้อมูลอุปกรณ์
    • /proc/net/tcp: ข้อมูล TCP socket
    • /proc/net/unix: ข้อมูล Unix domain socket
    • sysctl เป็นคำสั่งสำหรับเปลี่ยนการตั้งค่าระบบและเครือข่าย แทนการเขียนค่าลงในไฟล์ระบบ /proc โดยตรง
    • sysctl -w variable=value ใช้สำหรับการเปลี่ยนแปลงชั่วคราว ส่วนการเปลี่ยนแปลงถาวรให้แก้ไข /etc/sysctl.conf แล้วใช้ sysctl -p เพื่อปรับใช้

NIC ring buffer และการปรับ interrupt

  • NIC ring buffer

    • RX ring buffer คือ FIFO circular buffer ขนาดคงที่ที่อยู่ใน RAM
    • ตัว ring buffer เองไม่ได้เก็บข้อมูลแพ็กเก็ต แต่เก็บ descriptor ที่ชี้ไปยัง skb ซึ่งถูกนำเข้า RAM ด้วย DMA
    • หากพบการดรอปหรือ overrun สามารถเพิ่มขนาดคิวได้ แต่ผลข้างเคียงคือ latency อาจเพิ่มขึ้น
    • ในหลายกรณี การเพิ่มขนาด receive buffer เพียงอย่างเดียวก็อาจป้องกัน packet drop ได้ และช่วยให้ kernel มีเวลามากขึ้นเล็กน้อยในการเคลียร์ buffer
    • ตรวจสอบและเปลี่ยนค่าได้ด้วย ethtool
    • ethtool -g eth3: ตรวจสอบขนาด RX/TX ring ปัจจุบันและค่าสูงสุด
    • ethtool -G eth3 rx 8192 tx 8192: เพิ่ม RX/TX buffer เป็นค่าสูงสุด
    • มอนิเตอร์ด้วย ethtool -S eth3 และเคาน์เตอร์อย่าง err, drop, over, miss, timeout, reset, collis
  • การรวม interrupt ระดับฮาร์ดแวร์

    • NIC สามารถสะสม packet reference ไว้ใน RX ring buffer จนถึงเงื่อนไข timeout rx-usecs หรือ rx-frames แล้วจึงสร้าง HardIRQ ได้ สิ่งนี้เรียกว่า Interrupt coalescence
    • หาก interrupt เร็วเกินไป kernel จะถูกขัดจังหวะงานที่กำลังรันอยู่บ่อยครั้ง ทำให้ประสิทธิภาพระบบแย่ลง
    • หาก interrupt ช้าเกินไป NIC อาจเคลียร์ทราฟฟิกออกไม่เร็วพอ จนเกิดการเขียนทับและทราฟฟิกสูญหาย
    • การปรับ interrupt coalescence อาจลดการใช้ CPU และ HardIRQ และเพิ่ม throughput ได้ แต่มีต้นทุนด้าน latency
    • ตรวจสอบพารามิเตอร์ coalesce ด้วย ethtool -c eth3 และเปลี่ยนค่าได้ เช่น ethtool -C eth3 adaptive-rx off rx-usecs 0 rx-frames 0
    • adaptive mode จะให้การ์ดประเมินค่า coalescing แบบไดนามิกจากรูปแบบทราฟฟิกและรูปแบบการรับของ kernel

IRQ affinity และการกระจายโหลดระหว่าง CPU

  • IRQ affinity

    • IRQ มีคุณสมบัติ smp_affinity ที่กำหนดคอร์ CPU ซึ่งสามารถรัน ISR ของ IRQ นั้นได้
    • หากจัด interrupt affinity และ application thread affinity ให้ตรงกับคอร์ CPU เฉพาะ อาจช่วยเพิ่มประสิทธิภาพแอปพลิเคชันได้จากการแชร์ cache line
    • โดยค่าเริ่มต้นจะถูกควบคุมโดย daemon irqbalance
    • ก่อนปรับด้วยตนเองต้องหยุด irqbalance
    • /proc/irq/<IRQ_NUMBER>/smp_affinity เก็บ bitmask เลขฐานสิบหกที่แทนคอร์ CPU
    • ในเซิร์ฟเวอร์ 4 คอร์ ค่าเริ่มต้น f หมายถึงทุก CPU สามารถประมวลผล IRQ ได้
    • echo 1 > /proc/irq/32/smp_affinity ทำให้ใช้เฉพาะ CPU0
    • ในระบบที่มีมากกว่า 32 คอร์ จะใช้ comma แยกกลุ่ม 32-bit
    • IRQ affinity สามารถเพิ่มประสิทธิภาพได้เฉพาะในคอนฟิกที่เจาะจงมากและ workload ที่กำหนดไว้ล่วงหน้าเท่านั้น และอาจเป็น ดาบสองคม
  • RSS

    • หากรับแพ็กเก็ตบน NIC ความเร็วสูงด้วยคิวเดียวและ CPU เดียว คอร์หนึ่งอาจต้องรับผิดชอบการประมวลผลข้อมูลทั้งหมด ขณะที่คอร์อื่นว่างอยู่
    • RSS(Receive-side scaling) เป็นเทคโนโลยีของ NIC ที่ให้ NIC กระจายทราฟฟิกไปยังคิวรับส่งหลายคิว
    • NIC คำนวณแฮชจาก source/destination IP และ TCP/UDP source/destination port จัดแพ็กเก็ตของ flow เดียวกันไปยังคิวเดียว และกระจาย flow ให้สม่ำเสมอระหว่างคิวต่างๆ
    • RSS ให้ข้อดีของการประมวลผลรับแบบขนานในสภาพแวดล้อม multiprocessor
    • ตามเอกสาร Linux kernel ควรเปิดใช้ RSS เมื่อ latency สำคัญหรือการประมวลผล receive interrupt เป็นคอขวด และในเครือข่าย latency ต่ำ การตั้งค่าที่เหมาะสมคือจัดสรรคิวเท่ากับจำนวน CPU ของระบบ
  • RPS, RFS, aRFS

    • RPS(Receive Packet Steering) ใกล้เคียงกับการทำ RSS ในซอฟต์แวร์
    • ขณะที่ RSS เลือกคิวและ CPU ที่จะรัน hardware interrupt handler, RPS จะเลือก CPU ที่จะประมวลผลโปรโตคอลเหนือ interrupt handler
    • ต้องมี CONFIG_RPS และจะเปิดใช้งานเป็นค่าเริ่มต้นบน SMP
    • ตั้งค่าด้วย CPU bitmap ใน /sys/class/net/<dev>/queues/rx-<n>/rps_cpus
    • หากมี RSS อาจไม่จำเป็น แต่จะมีประโยชน์เมื่อจำนวน CPU มากกว่าจำนวนคิว
    • RFS(Receive Flow Steering) ขยาย RPS ไปจนถึง locality ของแอปพลิเคชัน
    • RPS กระจายแพ็กเก็ตตาม flow แต่ไม่ได้พิจารณาว่าแอปพลิเคชันใน user space รันอยู่บน CPU ใด
    • RFS รักษา rps_sock_flow_table ซึ่งเป็นตาราง global flow-to-CPU
    • สามารถปรับขนาดตารางได้ด้วย net.core.rps_sock_flow_entries
    • per-queue rps_dev_flow_table ใช้เพื่อลดปัญหาลำดับผิดเมื่อ scheduler ย้ายแอปพลิเคชันไปยัง CPU ใหม่ แต่ยังมีแพ็กเก็ตค้างอยู่
    • aRFS(Accelerated RFS) เป็นกลไกกระจายโหลดที่เร่งด้วยฮาร์ดแวร์สำหรับ RFS
    • เนื่องจากส่งแพ็กเก็ตไปยัง CPU ที่อยู่ใกล้กับเธรดที่ใช้ข้อมูลโดยตรง จึงให้ประสิทธิภาพดีกว่า RFS ได้
    • ต้องมี ndo_rx_flow_steer, ntuple filtering ของ NIC และ CONFIG_RFS_ACCEL
    • mapping ระหว่าง CPU กับคิวจะถูกอนุมานอัตโนมัติจาก IRQ affinity ของแต่ละคิวรับ ดังนั้นจึงไม่ต้องตั้งค่าเพิ่มเติม

การปรับแต่ง softIRQ, qdisc และบัฟเฟอร์ TCP

  • งบประมาณ softIRQ

    • รูทีน NAPI polling ถูกจำกัดด้วย netdev_budget_usecs, netdev_budget, dev_weight เพื่อไม่ให้ softIRQ ยึดครอง CPU
    • ค่าเริ่มต้นของ net.core.netdev_budget คือ 300 ซึ่งหมายถึงการล้างข้อความ 300 รายการจาก NIC ก่อนที่กระบวนการ softIRQ จะออกจาก CPU
    • net.core.netdev_budget_usecs คือจำนวนไมโครวินาทีสูงสุดของ NAPI polling cycle หนึ่งรอบ
    • net.core.dev_weight คือจำนวนแพ็กเก็ตสูงสุดต่อ CPU ที่เคอร์เนลสามารถประมวลผลได้ใน NAPI interrupt
    • หากคอลัมน์อื่นนอกเหนือจากคอลัมน์ที่ 1 ใน /proc/net/softnet_stat เพิ่มขึ้น อาจจำเป็นต้องปรับ budget แต่การเพิ่มขึ้นเล็กน้อยอาจถือเป็นเรื่องปกติ
  • ingress qdisc และ netdev_max_backlog

    • netdev_max_backlog คือคิวภายในเคอร์เนลที่เก็บทราฟฟิกหลังจากได้รับจาก NIC และก่อนจะประมวลผลผ่าน protocol stack เช่น IP/TCP
    • แต่ละคอร์ของ CPU จะมี backlog queue หนึ่งคิว
    • หากอินเทอร์เฟซรับแพ็กเก็ตได้เร็วกว่าความเร็วที่เคอร์เนลสามารถประมวลผลได้ คิวฝั่ง INPUT จะเต็มจนถึง netdev_max_backlog และเมื่อเกินกว่านั้นจะถูกดรอป
    • ค่าเริ่มต้นคือ 1000 ซึ่งอาจไม่เพียงพอสำหรับอินเทอร์เฟซ 1Gbps หลายตัว หรืออินเทอร์เฟซ 10Gbps ตัวเดียว
    • คอลัมน์ที่ 2 ของ /proc/net/softnet_stat คือเคาน์เตอร์ที่เพิ่มขึ้นจาก backlog queue overflow
    • การเปลี่ยนค่าทำได้ด้วย sysctl -w net.core.netdev_max_backlog <value>
  • egress qdisc, txqueuelen, default_qdisc

    • txqueuelen ตั้งค่าจำนวนแพ็กเก็ตที่อนุญาตใน kernel transmit queue ของอุปกรณ์อินเทอร์เฟซเครือข่าย
    • ค่าเริ่มต้นอาจเป็น 1000 ขึ้นอยู่กับไดรเวอร์ของอินเทอร์เฟซ
    • เปลี่ยนได้ด้วย ifconfig <interface> txqueuelen value และตรวจสอบ RX/TX dropped ใน ip -s link
    • default_qdisc คือ queuing discipline เริ่มต้นที่จะใช้กับอุปกรณ์เครือข่าย
    • สามารถระบุทางเลือกอื่นแทน pfifo_fast ได้ เช่น sfq, codel, fq_codel
    • ตรวจสอบตัวชี้วัดอย่าง dropped, overlimits, requeues ได้ใน tc -s qdisc ls dev <interface>
  • TCP read/write buffer และคิวการเชื่อมต่อ

    • tcp_rmem และ tcp_wmem กำหนดค่าต่ำสุด ค่าเริ่มต้น และค่าสูงสุดของบัฟเฟอร์รับ·ส่ง TCP ตามลำดับ
    • เปลี่ยนค่าได้ดังนี้
      • sysctl -w net.ipv4.tcp_rmem="min default max"
      • sysctl -w net.ipv4.tcp_wmem="min default max"
    • ตรวจสอบสถานะการใช้บัฟเฟอร์ได้ด้วย /proc/net/sockstat
    • Accept queue และ SYN queue ได้รับผลจาก net.core.somaxconn และ net.ipv4.tcp_max_syn_backlog
    • net.core.somaxconn คือเพดานบนของพารามิเตอร์ backlog ของ listen() และหากเปลี่ยนค่านี้ แอปพลิเคชันก็ต้องเปลี่ยนเป็นค่าที่เข้ากันได้ด้วย
    • net.ipv4.tcp_syncookies เปิดหรือปิด SYN cookie ซึ่งมีประโยชน์ในการป้องกันการโจมตี SYN flood
    • net.ipv4.tcp_congestion_control ตั้งค่าอัลกอริทึม congestion control ที่จะใช้กับการเชื่อมต่อใหม่

NUMA และประสิทธิภาพเครือข่าย

  • NUMA(Non-uniform memory access) คือสถาปัตยกรรมหน่วยความจำที่โปรเซสเซอร์สามารถเข้าถึง local memory ได้เร็วกว่า non-local memory
  • ในการประมวลผลเครือข่าย CPU ต้องเข้าถึงหน่วยความจำของ ring buffer ดังนั้น NUMA locality จึงอาจส่งผลต่อประสิทธิภาพเครือข่าย
  • NUMA แบ่ง CPU, หน่วยความจำ และอุปกรณ์ออกเป็นหลาย node และทำงานเหมือนคอมพิวเตอร์ขนาดเล็กหลายเครื่องที่มี interconnect ความเร็วสูงและ OS ร่วมกัน
  • ในระบบ NUMA เป้าหมายของการปรับแต่งคือการรวม interrupt ของอุปกรณ์นั้นไปยังคอร์ CPU ใน node เดียวกับที่อุปกรณ์สังกัดอยู่
  • อย่างไรก็ตาม ระบบ NUMA อาจโต้ตอบกับแอปพลิเคชันแบบเรียลไทม์ได้ไม่ดี และทำให้เกิดความล่าช้าของเหตุการณ์ที่คาดไม่ถึง
  • ตรวจสอบ NUMA node ได้ที่ /sys/devices/system/node/node*
  • ตรวจสอบ locality ของอุปกรณ์ได้ที่ /sys/class/net/<interface>/device/numa_node
    • -1 หมายถึงกรณีที่แพลตฟอร์มฮาร์ดแวร์ไม่ใช่ NUMA จริง หรือเคอร์เนลจำลอง NUMA ขึ้นมา หรืออุปกรณ์ไม่มี NUMA locality
  • Linux kernel รองรับ NUMA ตั้งแต่เวอร์ชัน 2.5 และดิสโทรที่อิง RedHat และ Debian มี numactl, numad ให้ใช้งาน
  • numad ตรวจสอบ topology ของระบบและการใช้ทรัพยากร และพยายามจัดวางโปรเซสที่มีการใช้หน่วยความจำและโหลด CPU สูงพอไว้ใน NUMA locality ที่มีประสิทธิภาพ

ตัวเลือกสำหรับการประมวลผลแพ็กเก็ตที่เร็วขึ้น

  • AF_PACKET v4 และ PACKET_MMAP

    • AF_PACKET v4 เป็นอินเทอร์เฟซแพ็กเก็ตความเร็วสูงของ Linux โดยตัด system call ออกจาก data path และโดยพื้นฐานใช้ copy-mode
    • เมื่อใช้ PACKET_ZEROCOPY จะสามารถใช้โหมด true zero-copy ที่แมป DMA packet buffer ไปยังพื้นที่ผู้ใช้ได้
    • เส้นทางทั่วไปที่อ่านไฟล์แล้วส่งผ่าน socket ต้องมี context switch ระหว่าง user mode กับ kernel mode และต้อง copy ข้อมูลหลายครั้ง
    • zero-copy ช่วยเพิ่มประสิทธิภาพด้วยการกำจัดการ copy ข้อมูลซ้ำซ้อน
    • PACKET_MMAP เป็น Linux API สำหรับการทำ packet sniffing ความเร็วสูง
    • ให้บริการ mmapped ring buffer ที่พื้นที่ผู้ใช้และเคอร์เนลใช้ร่วมกัน
    • ลด system call และการ copy ระหว่างพื้นที่ผู้ใช้กับเคอร์เนลสำหรับแพ็กเก็ตที่รับและส่ง
  • DPDK

    • DPDK(Data Plane Development Kit) คือไลบรารีพื้นที่ผู้ใช้และเฟรมเวิร์กไดรเวอร์สำหรับการประมวลผลแพ็กเก็ตความเร็วสูง
    • เป้าหมายคือการรับส่งแพ็กเก็ตเครือข่ายระหว่าง NIC กับแอปพลิเคชันผู้ใช้ด้วย native speed
    • มุ่งเป้าไปที่ NIC 10Gb หรือ 40Gb โดยความเร็วเป็นเกณฑ์สำคัญที่สุด
    • ไม่ใช่ network stack แต่เน้นที่ packet forwarding
    • เมื่อ DPDK ควบคุม NIC ทราฟฟิกทั้งหมดจะข้ามเคอร์เนล และ NIC นั้นจะมองไม่เห็นจากเคอร์เนล
    • พอร์ตจะถูก unbind จากไดรเวอร์เคอร์เนล Linux และถูกจัดการด้วยไดรเวอร์อย่าง vfio_pci, igb_uio, uio_pci_generic
    • หลังจากนั้น DPDK PMD จะรับผิดชอบการสื่อสารระหว่างแอปพลิเคชันกับ NIC
    • DPDK ต้องตั้งค่า hugepages เพื่อจัดสรร chunk หน่วยความจำขนาดใหญ่
    • องค์ประกอบหลัก:
      • EAL: generic interface ที่ซ่อนความแตกต่างของสภาพแวดล้อม
      • librte_ring: API FIFO แบบ lockless สำหรับ multi-producer, multi-consumer
      • librte_mempool: การจัดสรร pool ของอ็อบเจ็กต์หน่วยความจำ
      • librte_mbuf: การสร้างและจัดการ buffer ที่บรรจุแพ็กเก็ตเครือข่าย
      • librte_timer: timer service สำหรับรันฟังก์ชันแบบอะซิงโครนัส
      • PMD: ไดรเวอร์ที่ให้ CPU poll NIC อย่างต่อเนื่องแทนการใช้ interrupt
    • ข้อจำกัด:
      • พึ่งพาฮาร์ดแวร์สูง
      • ต้องจัดสรร CPU core เฉพาะเพื่อรัน PMD และจะใช้ CPU 100%
  • PF_RING

    • PF_RING คือ Linux kernel module และ user-space framework ที่ประมวลผลแพ็กเก็ตด้วยความเร็วสูง และให้ API ที่สม่ำเสมอแก่แอปพลิเคชันประมวลผลแพ็กเก็ต
    • PF_RING ใช้ Linux NAPI เพื่อ poll แพ็กเก็ตจาก NIC
    • NAPI copy แพ็กเก็ตจาก NIC ไปยัง PF_RING circular buffer และแอปพลิเคชันพื้นที่ผู้ใช้อ่านแพ็กเก็ตจาก ring
    • โครงสร้างนี้มี poller สองตัวคือแอปพลิเคชันและ NAPI ทำให้ CPU cycle ถูกใช้ไปกับ polling
    • ข้อดีคือสามารถกระจาย incoming packet ไปยังหลาย ring พร้อมกันได้
    • ด้วยโครงสร้างแบบโมดูล จึงสามารถใช้องค์ประกอบเพิ่มเติมอย่าง ZC module, FPGA-based card module, Stack module, Timeline module, Sysdig module ได้
    • PF_RING ลดต้นทุนของ packet capture และ userland forwarding แต่มีข้อจำกัดด้านประสิทธิภาพสูงสุดเพราะเป็นโครงสร้างสอง actor ที่เคอร์เนล copy จาก NIC ไปยัง ring และ userland อ่านจาก ring มาประมวลผล
    • PF_RING ตั้งแต่เวอร์ชัน 7.5 มีการรองรับ AF_XDP adapter
  • XDP และ AF_XDP

    • XDP(eXpress Data Path) คือการใช้งาน eBPF ที่ดักจับแพ็กเก็ตตั้งแต่ช่วงต้นใน data path ของเครือข่าย Linux
    • XDP ประมวลผล RX packet page โดยตรงภายใน device driver RX function ก่อนการจัดสรร SKB
    • eBPF คือ bytecode แบบ sandboxed ที่ผู้ใช้กำหนดเองและรันในเคอร์เนล
    • ใช้ register 64-bit จำนวน 11 ตัว และ stack ขนาด 512-byte
    • ด้วย LLVM backend สามารถคอมไพล์จาก C, Lua, Go, P4, Rust ฯลฯ ไปเป็น eBPF ได้
    • มี in-kernel verifier และ JIT compiler พร้อมรองรับฟีเจอร์อย่าง map, tail call, helper
    • กรณีใช้งาน XDP:
      • pre-stack filtering เพื่อบรรเทา DoS
      • forwarding และ load balancing
      • เทคนิค batching อย่าง GRO
      • flow sampling และ monitoring
      • ULP processing
    • XDP ไม่ใช่ kernel bypass แต่เป็น fast path ภายในเคอร์เนลสแต็ก และไม่ได้แทนที่ TCP/IP stack
    • ไม่ต้องใช้ฮาร์ดแวร์เฉพาะ แต่มีข้อกำหนดอย่าง multi-queue NIC, TX/RX checksum offload, RSS, TSO
    • ข้อดีของ XDP เมื่อเทียบกับ DPDK:
      • เลือกใช้ busy polling หรือ interrupt driven networking ได้
      • ไม่ต้องใช้ huge pages
      • ไม่มีข้อกำหนดด้านฮาร์ดแวร์พิเศษ
      • ไม่จำเป็นต้องมี CPU เฉพาะ
      • ไม่จำเป็นต้องฉีดแพ็กเก็ตกลับเข้าเคอร์เนลจาก 3rd party userspace application
      • ไม่จำเป็นต้องมี security model ใหม่สำหรับการเข้าถึงฮาร์ดแวร์เครือข่าย
      • ไม่จำเป็นต้องใช้ 3rd party code/licensing
    • AF_XDP เป็น socket ประเภทใหม่ที่ถูกนำมาใช้ใน Linux 4.18
    • สามารถใช้ฟังก์ชันของเคอร์เนลเพื่อสร้างโครงสร้างคล้าย DPDK หรือ AF_PACKET ได้ โดยไม่ต้องข้ามเคอร์เนลทั้งหมด
    • XDP program สามารถ redirect frame ไปยัง memory buffer ในพื้นที่ผู้ใช้ด้วย eBPF ได้
    • DMA transfer รองรับ zero-copy โดยใช้หน่วยความจำพื้นที่ผู้ใช้
    • สามารถเพิ่มประสิทธิภาพได้ 3~20 เท่าเมื่อเทียบกับ AF_PACKET
    • ข้อจำกัด:
      • เป็นโปรเจกต์ที่ค่อนข้างใหม่
      • การรองรับอย่างสมบูรณ์ต้องใช้ Linux kernel 5.4 ขึ้นไป

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

 
GN⁺ 2024-07-29
ความคิดเห็นจาก Hacker News
  • ถ้าได้เห็นข้อมูลนี้ตั้งแต่ไม่กี่สัปดาห์ก่อนคงมีประโยชน์มาก
    ผมพยายามทำ การเข้ารหัสลิงก์ L2 ระหว่างดาต้าเซ็นเตอร์ เลยไปขอใบเสนอราคาจากหลายเจ้าเรื่องฮาร์ดแวร์ appliance แต่รู้สึกว่าแพงเกินไป เลยลองทำเอง
    ผมตั้งค่าบนฮาร์ดแวร์ทั่วไปให้ขนส่ง Ethernet frame บน overlay network ของ WireGuard เพื่อให้ได้ 10Gbps และหลังจากทำงานอยู่ราวสิบวัน ก็ทำได้ในต้นทุนที่ถูกกว่าข้อเสนอที่ถูกที่สุดประมาณ 70% และถูกกว่าข้อเสนอที่แพงที่สุดประมาณ 95% แต่ก็ต้องอ่านเอกสารละเอียด ๆ และทดลองเองเยอะมาก
    ผมอยากใช้เนื้อหาในบทความนี้ตรวจสอบว่าความเข้าใจของตัวเองถูกต้องไหม และมองเผิน ๆ ก็ดูน่าสนใจและครอบคลุมมาก

    • อยากรู้ว่าเป็นกรณีใช้งานแบบไหนถึงทำให้ L3 tunnel อย่างเดียวไม่พอ
    • ถ้าแชร์โค้ดได้ก็อยากดูมาก อยากรู้จริง ๆ ว่าคุณทำ implementation แบบนั้นอย่างไร
  • ถ้ามีพารามิเตอร์ที่ปรับได้เยอะขนาดนี้ ก็น่าจะคุ้มที่จะทำ ซอฟต์แวร์ autotuning
    ดูเหมือนจะทำแนวทางคล้าย gradient descent ได้ โดยสุ่มเลือกพารามิเตอร์จาก whitelist แล้วขยับขึ้นหรือลงเล็กน้อยภายในช่วงที่อนุญาต วัดประสิทธิภาพสักพัก ถ้าแย่ลงก็ย้อนกลับ ถ้าดีขึ้นก็ปรับต่อ

  • น่าสนใจดี แต่ในฐานะวิศวกรซอฟต์แวร์ ผมแทบไม่มีโอกาสได้รันคำสั่งในบทความจริง ๆ
    ระบบส่วนใหญ่รันอยู่ในคอนเทนเนอร์ลินุกซ์แบบย่อส่วนบางอย่าง, ไม่มีสิทธิ์เข้าถึงเชลล์บนระบบโปรดักชัน, และสภาพแวดล้อม dev หรือ QA ก็แตกต่างจากโปรดักชันมากเกินไปในแง่โหลดและอย่างอื่น ทำให้การจำลองบั๊กส่วนใหญ่ไม่ค่อยช่วยอะไร
    สุดท้ายแล้วโอกาสที่จะได้ลองรันคำสั่งในบทความก็มีแค่ตอนจัดการเครื่องส่วนตัวเอง และคิดว่าน่าจะมีประโยชน์ถ้าทำงานเป็น platform engineer

    • ฟีเจอร์ระดับล่างส่วนใหญ่ก็มักจะใช้ไม่ได้หรือไม่ค่อยมีประโยชน์อยู่ดี เพราะ implementation ของ container network interface มักบังคับให้ต้องเจอกับ veth pair และมีการทำอะไรประหลาด ๆ ใน user space เต็มไปหมด
      อย่างหนึ่งที่ผมไม่ค่อยชอบใน Kubernetes คือโมเดลด้าน networking มันตั้งสมมติฐานว่ามี network card แค่ใบเดียว และแอปพลิเคชันก็ควรง่ายพอที่จะไม่ต้องรู้อะไรเกี่ยวกับชั้นล่างกว่านั้น
      โมเดล networking ทั้งหมดดูเหมือนยังมีพื้นที่ให้ยกเครื่องครั้งใหญ่ในแบบยุค 2020s เพื่อให้เรียบง่ายขึ้นและดีขึ้น
    • ถ้ามี staging environment ที่ใกล้เคียงโปรดักชันมากพอ ก็อาจช่วยได้ตามสถานการณ์ เพราะคุณสามารถทดลองและวิเคราะห์บนระบบที่คล้ายโปรดักชันและมีสิทธิ์เข้าถึง
  • ในบทความบอกว่า net.core.wmem_max คือค่าขีดบนของขนาด TCP send buffer และก็มี net.ipv4.tcp_wmem ด้วย เลยสงสัยสองอย่าง

    1. ทำไมไม่มีค่าที่เทียบเท่าสำหรับ IPv6 2. แล้วมันต่างจาก net.core.wmem_max อย่างไร
    • net.core.wmem_max ก็เป็น ค่าสูงสุด ตามชื่อเลย
      ส่วน net.ipv4.tcp_wmem เป็นค่าแบบสามตัว คือค่าต่ำสุด ค่าเริ่มต้น และค่าสูงสุด โดยค่าสูงสุดที่ระบุตรงนี้จะเกิน net.core.wmem_max ไม่ได้
      TCP ควรเป็นโปรโตคอลเดียวกันไม่ว่าจะขนส่งผ่าน IPv4 หรือ IPv6
      ตัวอย่าง: https://docs.redhat.com/en/documentation/red_hat_data_grid/7...
  • สิ่งที่ขาดไปนิดหน่อยตรงนี้คือการดีบักและการจูนสำหรับ throughput เกิน 100Gbps
    เมื่อให้บริการ HTTP ในระดับนั้น คอขวดแรกมักเป็น memory bandwidth จนหลายครั้งต้องใช้ kTLS
    เครื่องมืออย่าง AMD μProf มีประโยชน์มากสำหรับการดีบัก และการทำ continuous profiling ด้วย eBPF ก็ช่วยได้มากเช่นกันถ้าอยากเข้าใจให้ชัดว่าในเคอร์เนลและ user space กำลังเกิดอะไรขึ้นบ้าง

  • ดูเจ๋งทีเดียว ตลอดเส้นทางอาชีพของผมที่ผ่านมา เวลาอยากได้ประสิทธิภาพ มักเริ่มจาก kernel bypass ก่อนเสมอ