Skip to content

Real-time KVM tuning

Chạy VM cho workload cần độ trễ thấp và ổn định (xử lý tín hiệu, vRAN, điều khiển, trading): mục tiêu không phải thông lượng trung bình mà là độ trễ tối đa bị chặn (worst-case latency, jitter). Trang này gom các kỹ thuật dùng chung cho mọi VM kiểu đó, phần riêng cho gNodeB xem vRAN trên VM.

Cách làm là dành riêng một số core, bộ nhớ và thiết bị cho VM, đẩy mọi thứ khác (ngắt, kernel thread, tiến trình quản lý) sang các core còn lại. Đây vẫn là hypervisor tổng quát (KVM). Nếu cần cô lập cứng và dễ chứng nhận hơn thì xem Partitioning hypervisor.

Jitter đến từ đâu

Nguồn Biểu hiện Hướng xử lý
Scheduler của host vCPU bị preempt bởi thread khác, hoặc chuyển core Cô lập core, pin vCPU
Ngắt và timer tick Ngắt thiết bị, tick định kỳ rơi vào core của VM irqaffinity, nohz_full, rcu_nocbs
Thread của QEMU (emulator, I/O) Thread phụ chạy chung core với vCPU Pin riêng emulator thread và iothread
TLB miss, page fault Nested paging tốn page walk 2D, trang bị swap hoặc cấp phát muộn Huge page 1G, khóa và cấp sẵn bộ nhớ
Truy cập bộ nhớ xa Vượt NUMA node Giữ vCPU, RAM và NIC trên cùng một node
Quản lý nguồn C-state sâu, đổi tần số Tắt C-state, cố định tần số
SMT Hai hyper-thread chung core vật lý tranh tài nguyên Tắt SMT hoặc không chia sibling cho bên khác
VM exit Mỗi exit cộng thêm độ trễ (CPU Virtualization) Passthrough, ngắt do phần cứng đưa thẳng vào guest
SMI (firmware) Core bị dừng mà OS không thấy Cấu hình BIOS, kiểm tra bằng hwlatdetect

Cấu hình host

Cô lập core

Giả sử máy có 16 core, core 0-1 dành cho host (housekeeping), core 2-15 dành cho VM. Tham số kernel của host:

isolcpus=domain,managed_irq,2-15 nohz_full=2-15 rcu_nocbs=2-15 irqaffinity=0-1
Tham số Tác dụng
isolcpus=domain,managed_irq,... Bỏ các core khỏi scheduler domain (không cân bằng tải tiến trình vào đó) và tránh gửi managed IRQ của thiết bị tới chúng
nohz_full=... Tắt tick định kỳ trên core khi chỉ có một task chạy. Cần ít nhất một core housekeeping để làm việc timekeeping
rcu_nocbs=... Chuyển xử lý callback RCU sang kthread ở core khác
irqaffinity=... Mặc định đẩy ngắt về các core housekeeping

Ghi chú:

  • nohz_full chỉ tắt tick, không thay thế isolcpus hay cpuset, nên dùng cùng nhau.
  • Tắt irqbalance hoặc cấu hình nó bỏ qua các core cô lập (IRQBALANCE_BANNED_CPULIST), đồng thời kiểm tra /proc/interrupts và /proc/irq/*/smp_affinity_list.
  • Kernel thread (kworker, ksoftirqd...) có thể vẫn xuất hiện trên core cô lập, dùng ps -eLo psr,pid,comm hoặc ftrace để tìm và gắn lại affinity.
  • Trên RHEL và dẫn xuất, profile tuned đóng gói sẵn phần lớn cấu hình này (cpu-partitioning, realtime-virtual-host, realtime-virtual-guest).

Với yêu cầu khắt khe, bật cả PREEMPT_RT cho kernel của host và của guest (xem Linux Embedded về real-time). PREEMPT_RT làm thread kernel trên host đáng tin hơn, nhưng không thay thế được việc cô lập core.

BIOS và nguồn

  • Bật VT-x/AMD-V, VT-d/IOMMU, SR-IOV.
  • Tắt C-state sâu (hoặc đặt giới hạn bằng processor.max_cstate=1 intel_idle.max_cstate=0 trên kernel), dùng governor performance, cố định tần số.
  • Tắt SMT, hoặc nếu giữ lại thì gán cả hai sibling của một core vật lý cho cùng một VM.
  • Kiểm tra SMI bằng hwlatdetect (rt-tests), vì SMI do firmware gây ra không có cách nào chặn từ OS.

Pin vCPU và emulator thread

Mỗi vCPU là một thread của QEMU (KVM & QEMU), nên pin vCPU là đặt CPU affinity cho thread đó. QEMU còn các thread khác: main loop (emulator), iothread, thread của vhost. Chúng không được chạy trên core của vCPU, nếu không sẽ tranh chấp ngay trên core đang chạy workload real-time.

Với libvirt:

<vcpu placement='static'>4</vcpu>
<cputune>
  <vcpupin vcpu='0' cpuset='2'/>
  <vcpupin vcpu='1' cpuset='3'/>
  <vcpupin vcpu='2' cpuset='4'/>
  <vcpupin vcpu='3' cpuset='5'/>
  <emulatorpin cpuset='0-1'/>
  <iothreadpin iothread='1' cpuset='0-1'/>
  <vcpusched vcpus='0-3' scheduler='fifo' priority='1'/>
</cputune>
<cpu mode='host-passthrough' check='none'>
  <topology sockets='1' dies='1' cores='4' threads='1'/>
  <feature policy='require' name='invtsc'/>
</cpu>
  • emulatorpin và iothreadpin đưa các thread phụ về core housekeeping.
  • vcpusched đặt chính sách SCHED_FIFO cho vCPU thread. Chỉ cần khi trên cùng core còn thread khác cần bị đẩy nhường. Nếu core đã cô lập hoàn toàn và chỉ có vCPU chạy thì có thể bỏ. Cẩn thận: vCPU SCHED_FIFO chạy 100% có thể làm đói thread kernel cần thiết trên cùng core.
  • host-passthrough để guest thấy đúng CPU thật (flag, cache). invtsc cho phép guest dùng TSC ổn định làm clocksource.

Nếu dùng QEMU trực tiếp, pin bằng taskset lên từng thread. Tên thread vCPU có dạng CPU 0/KVM:

ps -T -p $(pgrep -f qemu-system) -o spid,comm   # tìm TID của từng vCPU
taskset -pc 2 <tid_vcpu0>

Có thể kiểm tra kết quả bằng virsh vcpuinfo <vm> hoặc virsh emulatorpin <vm>.

Huge page 1G

Với nested paging, TLB miss tốn page walk hai tầng (Memory Virtualization). Huge page 1G giảm số lượng entry cần cache, và bộ nhớ cấp sẵn từ huge page không bị swap hay cấp phát muộn, nên độ trễ ổn định hơn.

Dành sẵn huge page lúc boot (cấp muộn dễ thất bại vì bộ nhớ bị phân mảnh), chia theo từng NUMA node:

# tham số kernel của host
default_hugepagesz=1G hugepagesz=1G hugepages=32

# hoặc cấp muộn theo từng node (dễ thất bại khi RAM đã phân mảnh)
echo 16 > /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages

Dùng cho VM trong libvirt:

<memoryBacking>
  <hugepages>
    <page size='1048576' unit='KiB' nodeset='0'/>
  </hugepages>
  <nosharepages/>
  <locked/>
</memoryBacking>
<numatune>
  <memory mode='strict' nodeset='0'/>
</numatune>

Với QEMU trực tiếp, dùng memory-backend-file trên hugetlbfs, cấp sẵn toàn bộ (prealloc=on):

mount -t hugetlbfs -o pagesize=1G none /dev/hugepages1G
qemu-system-x86_64 ... \
    -object memory-backend-file,id=mem0,size=16G,mem-path=/dev/hugepages1G,share=on,prealloc=on,host-nodes=0,policy=bind \
    -numa node,memdev=mem0 -m 16G

Cần lưu ý khi dùng huge page cho RT VM:

  • Tắt Transparent Huge Page (khi cần, transparent_hugepage=never) vì khi gom/chia trang có thể gây trễ, thay bằng huge page cấp tường minh.
  • Không dùng ballooning, KSM hay swap cho VM này (<nosharepages/>, <locked/>). Xem các cơ chế này ở Memory Virtualization.
  • Huge page đã cấp cho VM thì không còn dùng cho việc khác, cần tính trước tổng dung lượng.

NUMA

Truy cập RAM hoặc thiết bị ở node khác tốn thêm độ trễ và dao động hơn. Nguyên tắc: vCPU, RAM và NIC của một VM nằm trên cùng một NUMA node.

lscpu | grep -i numa                          # core nào thuộc node nào
numactl -H                                    # RAM mỗi node
cat /sys/bus/pci/devices/0000:3b:00.0/numa_node   # NIC nằm ở node nào
  • Chọn các core của VM từ node chứa NIC (hoặc accelerator) được gán cho nó.
  • Ràng buộc bộ nhớ vào node đó (<numatune> ở trên, hoặc policy=bind trong QEMU).
  • Nếu VM bắt buộc vượt node, cấu hình topology NUMA cho guest khớp với host (-numa node,...), để guest biết mà đặt bộ nhớ và thread cho đúng.

Cấu hình bên trong guest

Guest cũng là một Linux cần tuning như một máy real-time bình thường:

  • Kernel parameter của guest: isolcpus, nohz_full, rcu_nocbs cho các vCPU dành cho workload, để lại vCPU 0 làm housekeeping.
  • Kernel PREEMPT_RT nếu workload cần.
  • Huge page 1G trong guest (nếu app dùng, ví dụ DPDK), cấu hình tương tự host.
  • Clocksource: với invtsc, clocksource=tsc thường ổn định hơn kvm-clock. Kiểm tra cat /sys/devices/system/clocksource/clocksource0/current_clocksource.
  • Thread real-time dùng SCHED_FIFO, khóa bộ nhớ bằng mlockall(), đặt affinity tường minh.
  • Không chạy dịch vụ không cần thiết trên vCPU cô lập, tắt irqbalance trong guest.
  • Hạn chế nguồn gây VM exit trên vCPU quan trọng: tránh truy cập thiết bị giả lập (PIT, HPET, đĩa emulated), ưu tiên thiết bị passthrough hoặc virtio với polling.

Điều cần nhớ: timer của guest vẫn gây VM exit ở mỗi lần hẹn giờ nếu không có hỗ trợ phần cứng. nohz_full trong guest giảm số lần đó vì tắt tick, nhưng không loại bỏ hoàn toàn. Vì thế cần đo, không nên chỉ tin cấu hình (xem phần dưới).

Đo bằng cyclictest trong guest

cyclictest (gói rt-tests) đo độ trễ từ lúc timer đáng lẽ đánh thức tới lúc thread thực sự chạy. Chạy trong guest, trên đúng các vCPU cô lập:

cyclictest -m -p 95 -a 2-3 -t 2 -i 200 -D 1h -q -h 400 > hist.txt
Tham số Ý nghĩa
-m Khóa bộ nhớ (mlockall)
-p 95 Ưu tiên SCHED_FIFO 95
-a 2-3 -t 2 Hai thread, gắn vào vCPU 2 và 3
-i 200 Chu kỳ 200 µs
-D 1h Chạy 1 giờ
-q -h 400 Chỉ in kết quả cuối, kèm histogram tới 400 µs

Cách đọc và phương pháp:

  • Max quan trọng hơn trung bình. Một lần vượt deadline đủ để hỏng.
  • Chạy đủ lâu (nhiều giờ, lý tưởng qua đêm), vì sự kiện hiếm mới là thứ cần bắt.
  • Chạy cùng lúc có tải: stress-ng trên core housekeeping, các VM khác, I/O mạng. Đo lúc rảnh không đại diện.
  • Đo baseline trên host (trên core cô lập tương ứng) trước, rồi đo trong guest. Chênh lệch giữa hai kết quả là chi phí ảo hóa, từ đó mới tìm nguyên nhân.
  • Ngưỡng chấp nhận lấy từ deadline của ứng dụng, không có con số chung.

Khi thấy đỉnh trễ, xác định nó nằm ở đâu:

perf kvm stat record -a sleep 60 && perf kvm stat report     # thống kê VM exit theo lý do
trace-cmd record -e kvm:kvm_exit -e kvm:kvm_entry -C 2        # theo dõi exit trên core 2 (host)
hwlatdetect --duration=300                                    # kiểm tra SMI/nhiễu từ firmware

Trong guest, các latency tracer của ftrace (xem Linux Embedded) giúp tìm lý do tắt preempt hay ngắt kéo dài.

Checklist

  1. BIOS: VT-x, VT-d, SR-IOV bật. C-state sâu, SMT tắt hoặc kiểm soát. Tần số cố định.
  2. Host: isolcpus, nohz_full, rcu_nocbs, irqaffinity, tắt irqbalance. Huge page 1G dành sẵn.
  3. VM: pin vCPU và emulator/iothread, bộ nhớ huge page cấp sẵn và khóa, ràng buộc NUMA, host-passthrough.
  4. Guest: cô lập vCPU, kernel (RT nếu cần), clocksource, huge page cho app.
  5. Đo: cyclictest có tải, so với baseline host, đọc max.
  6. Nếu có thiết bị gán vào VM: kiểm tra IOMMU group, NUMA node của thiết bị, ngắt của thiết bị nằm đúng core.

Đọc thêm