使用 Onload

本页面介绍了如何将 Onload 与 U4 Compute Engine 实例搭配使用。

关于 Onload

Onload 是一种高性能网络 堆栈,适用于需要超低延迟、最小 抖动和一致性能的延迟敏感型应用。Onload 提供了一种 TCP/IP 实现,该实现绕过操作系统内核并直接在用户空间中运行,同时允许应用使用标准 BSD 套接字 API。

将 Onload 与 ULL Solution 搭配使用时,支持以下功能:

  • 流转向:您可以将特定流量直接转向到指定的接收队列 (RX),从而绕过默认的 接收端缩放 (RSS) 哈希 。支持 3 元组流 转向(协议、目标 IP 地址、目标端口)。

准备工作

在 U4 Compute Engine 实例上使用 Onload 之前,您必须满足以下要求。

创建 U4 实例

如果尚未创建 U4 Compute Engine 实例,请使用以下任一过程创建,这些过程包含 Onload 所需的配置:

使用 SSH 连接到您的实例

如果尚未连接到您的实例,请使用 SSH 连接。

切换到根用户

以下过程中的命令和脚本会修改系统级设置、内核参数和网络接口。如需成功运行这些命令和脚本,您必须以根用户身份执行它们。您可以通过 运行 sudo su 切换到根 shell,也可以根据需要添加 sudo,然后再运行命令。

设置 Onload

本部分介绍了在 U4 实例上设置 Onload 所需的步骤。

安装依赖项

  1. 如果您使用的是 Rocky Linux,请启用 CodeReady Builder (CRB) 代码库。如果您使用的是 Red Hat Enterprise Linux (RHEL),请跳过此步骤。

    dnf -y config-manager --enable crb
    
  2. 安装 Onload 所需的依赖项:

    dnf -y install git clang \
       python3-setuptools \
       linuxptp \
       libcap-devel libbpf-devel libxdp-devel
    

拉取 Onload 源代码

如需拉取包含所需更改的 onload 代码库 ,请运行以下命令:

umask 0022
mkdir -p /usr/src/

git clone https://github.com/Xilinx-CNS/onload /usr/src/onload

# 9.2.0.43 / 9.2.1, origin/v9_2 as of May 18, 2026
git -C /usr/src/onload checkout origin/v9_2

# Pull Google-specific Onload changes not yet merged as of v9_2
curl -L https://github.com/Xilinx-CNS/onload/pull/279.patch | git -C /usr/src/onload am
curl -L https://github.com/Xilinx-CNS/onload/pull/282.patch | git -C /usr/src/onload am
curl -L https://github.com/Xilinx-CNS/onload/pull/325.patch | git -C /usr/src/onload am
curl -L https://github.com/Xilinx-CNS/onload/pull/327.patch | git -C /usr/src/onload am

构建 Onload

如需构建 Onload,请运行以下命令:

cd /usr/src/onload
USEONLOADEXT=1 ./scripts/onload_install --no-sfc

pushd ./src/tools/bpf_link_helper
clang xdp_onload_prepare.c  -lbpf -o xdp_onload_prepare
clang -target bpf -O2 -g -c xdp_tstamp.c -o ./xdp_tstamp.o
popd

停用间接分支跟踪 (IBT)

必须停用 IBT,才能使用 Onload,如 间接分支跟踪 (IBT) 不兼容 中所述。

如需停用 IBT,请运行以下命令:

grubby --args="ibt=off" --update-kernel=ALL
reboot

加载 Onload

本部分介绍了如何在实例上加载 Onload。

在 U4P 或 U4C 实例上加载 Onload

如需在 U4P 或 U4C 裸金属实例上加载 Onload,请使用以下脚本。

IFNAMES=($( find /sys/class/net -type l -not -lname '*virtual*' -printf '%l %f\n' | sort | awk '{print $2}'))

for IFNAME in "${IFNAMES[@]}"; do
  ethtool -L "${IFNAME}" rx 16 tx 16
  ethtool -G "${IFNAME}" rx 1024 rx-buf-len 2048
  ethtool -K "${IFNAME}" ntuple on
  echo 0 > "/sys/class/net/${IFNAME}/threaded"
  /usr/src/onload/src/tools/bpf_link_helper/xdp_onload_prepare \
    "${IFNAME}" /usr/src/onload/src/tools/bpf_link_helper/xdp_tstamp.o
done

setenforce 0
numactl --cpunodebind=0,2 onload_tool reload --onload-only
for IFNAME in "${IFNAMES[@]}"; do
  echo "${IFNAME}" 16 > /sys/module/sfc_resource/afxdp/register
  until [[ $(cat "/sys/class/net/${IFNAME}/carrier") == 1 ]]; do
    sleep 1
  done
  hwstamp_ctl -i "${IFNAME}" -r 1
done

echo 1 > /sys/module/sfc_resource/parameters/enable_af_xdp_flow_filters
echo 256 > /sys/module/onload/parameters/xdp_headroom
echo -1 > /sys/module/onload/parameters/inject_kernel_gid

在 U4S 实例上加载 Onload

如需在 U4S 虚拟机实例上加载 Onload,请使用以下脚本。

IFNAME=NIC_NAME
ALLOCATED_QUEUES=ALLOCATED_QUEUES

ethtool -L "$IFNAME" rx "${ALLOCATED_QUEUES}" tx "${ALLOCATED_QUEUES}"
ethtool -G "$IFNAME" rx 1024 rx-buf-len 2048
ethtool -K "$IFNAME" ntuple on
echo 0 > "/sys/class/net/${IFNAME}/threaded"
/usr/src/onload/src/tools/bpf_link_helper/xdp_onload_prepare "$IFNAME" \
  /usr/src/onload/src/tools/bpf_link_helper/xdp_tstamp.o

setenforce 0
numactl --cpunodebind=0 onload_tool reload --onload-only

echo "${IFNAME} ${ALLOCATED_QUEUES}" | tee /sys/module/sfc_resource/afxdp/register
until [[ $(cat "/sys/class/net/${IFNAME}/carrier") == 1 ]]; do
    sleep 1
done
hwstamp_ctl -i "$IFNAME" -r 1

echo 1 > /sys/module/sfc_resource/parameters/enable_af_xdp_flow_filters
echo 256 > /sys/module/onload/parameters/xdp_headroom
echo -1 > /sys/module/onload/parameters/inject_kernel_gid

替换以下内容:

  • NIC_NAME:网络接口的操作系统名称,例如 enp22s0f0
  • ALLOCATED_QUEUES:要在网络接口上为 Onload 分配的接收 (RX) 和传输 (TX) 队列的数量。将此值设置为分配给 vNIC 的 RX 或 TX 队列总数的一半。

    对于 U4S 实例,队列总数(分别为 RX 或 TX 队列)等于 num_vcpus / num_vnics,每个 vNIC 最多 16 个队列。 例如,如果 vNIC 有 4 个 TX 队列,请将此值设置为 2。 如果 vNIC 有 16 个 TX 队列,请将此值设置为 8

    如需详细了解默认队列分配,请参阅 接收和传输队列

配置 Onload 标志

如需优化性能并帮助减少延迟,您可以在使用 Onload 运行应用时使用以下环境变量和标志集。本部分包含建议的设置,您可以根据应用的需要进行调整。

您必须在应用命令之前指定这些参数。例如,如需使用这些设置运行应用,请使用以下格式:

env EF_NO_FAIL=0 \
  EF_POLL_USEC=100000 \
  EF_RX_TIMESTAMPING=3 \
  EF_MAX_ENDPOINTS=1048576 \
  EF_WODA_SINGLE_INTERFACE=1 \
  EF_UL_EPOLL=3 \
  EF_USE_HUGE_PAGES=0 \
  EF_EPOLL_CTL_HANDOFF=0 \
  EF_FDS_MT_SAFE=0 \
  EF_NONAGLE_INFLIGHT_MAX=-1 \
  EF_RXQ_SIZE=4096 \
  EF_TCP_RCVBUF_ESTABLISHED_DEFAULT=65536 \
  EF_MAX_PACKETS=65536 \
  EF_PREFAULT_PACKETS=65536 \
  EF_EVS_PER_POLL=256 \
  onload -v --profile=latency APPLICATION_COMMAND

卸载 Onload

如需卸载 Onload,请使用以下脚本。

IFNAMES=($( find /sys/class/net -type l -not -lname '*virtual*' -printf '%l %f\n' | sort | awk '{print $2}'))

for IFNAME in "${IFNAMES[@]}"; do
  rm -f "/sys/fs/bpf/onload_xdp_xsk_${IFNAME}"
done

onload_tool unload --onload-only

for IFNAME in "${IFNAMES[@]}"; do
  # (optional) Disable threaded busypolling in case it's up. See busypolling
  # section
  echo 0 > "/sys/class/net/${IFNAME}/threaded"

  ip link set dev "${IFNAME}" xdp off
done

配置自动启动 Onload 的 systemd 服务

如需在实例启动时自动启动 Onload,您可以将其注册为 systemd 服务。使用以下模板创建服务文件:

[Unit]
Description=ULL Solution -- Loading & instance tuning for Onload
After=network-online.target
After=google-guest-agent-manager.service google-guest-agent.service
Before=multi-user.target
Before=sshd.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=START_SCRIPT_PATH
ExecStartPost=OPTIMIZATION_SCRIPT_PATH
ExecStop=STOP_SCRIPT_PATH

[Install]
WantedBy=multi-user.target

替换以下内容:

配置忙轮询

本部分提供了有关如何在实例上配置忙轮询的示例。

忙轮询会持续检查新的网络数据包,而不是等待设备中断,这有助于减少延迟和抖动。如需详细了解忙轮询,请参阅 Linux 内核文档 中的忙轮询。

获取 Onload 堆栈正在使用的 RX 队列

如需获取 Onload 堆栈正在使用的 RX 队列,请执行以下操作:

  1. 运行 onload_stackdump以获取 Onload 堆栈 ID:

    onload_stackdump
    
  2. 由于 Onload 堆栈 ID 和 NAPI 队列 ID 可能并不总是匹配,因此请使用以下脚本从堆栈 ID 获取相应的接口名称、索引和队列 ID。

    ONLOAD_STACK=ONLOAD_STACK_ID
    
    INTF_HWPORT_MAP=($(onload_stackdump "${ONLOAD_STACK}" netif_extra | grep -oP "intf_i_to_hwport=\K.*$" | tr ',' '\n'))
    HWPORT_IFINDEX_MAP=($(onload_stackdump "${ONLOAD_STACK}" hwport_to_base_ifindex | grep -oP "\d+$"))
    while read -r INTF_ID QUEUE_ID; do
    HW_PORT="${INTF_HWPORT_MAP[INTF_ID]}"
    IFINDEX="${HWPORT_IFINDEX_MAP[HW_PORT]}"
    IFNAME=$(ip -j link | jq -r ".[] | select(.ifindex == ${IFINDEX}) | .ifname")
    echo "ifname=${IFNAME} ifindex=${IFINDEX} queue_id=${QUEUE_ID}"
    done < <(onload_stackdump "${ONLOAD_STACK}" netif | grep -oP "((intf|vi)=)\K\d+" | xargs -n 2)

    ONLOAD_STACK_ID 替换为要启用或停用忙轮询的堆栈的 ID。

  3. 记录这些值,以便在以下部分中启用停用忙轮询时使用。

在 RX 队列上启用忙轮询

本部分提供了一个示例,说明如何在 Onload 堆栈 正在使用的 特定 RX 队列上启用忙轮询。

  1. 在终端中运行以下 bash 脚本。enable_single_queue 函数会执行以下操作:

    • 使用 netlink (ynl) 获取与 RX 队列对应的 napi_id
    • napi_id 上设置 threaded: busy-poll 属性
    • 获取忙轮询 napi_id 的线程的 kthread_pid
    • 使用 tasksetkthread_pid 绑定到特定 CPU
    readonly NETDEV_YAML=${NETDEV_YAML:-"/usr/share/ynl/specs/netdev.yaml"}
    
    call_ynl() {
      ynl --spec "${NETDEV_YAML}" "$@"
    }
    
    enable_single_queue() {
      local -r interface="$1"
      local -r ifindex=$(cat "/sys/class/net/${interface}/ifindex")
      local -r q_id="$2"
      local -r cpu="$3"
    
      local napi_id
      napi_id=$(call_ynl --output-json --do queue-get \
        --json "{\"ifindex\": ${ifindex}, \"id\": ${q_id}, \"type\": \"rx\"}" | \
        jq -r '."napi-id"')
    
      if [[ -z "${napi_id}" || "${napi_id}" == "null" ]]; then
        echo "Error: No napi_id found for queue ${q_id} on interface ${interface}" >&2
        exit 1
      fi
    
      echo "Enabling busypolling for queue ${q_id} (NAPI ${napi_id}) on CPU ${cpu}"
      call_ynl --do napi-set --json "{\"id\": \"${napi_id}\", \"threaded\": \"busy-poll\"}" >/dev/null
    
      local napi_kthread_pid
      napi_kthread_pid=$(call_ynl --do napi-get --output-json \
        --json "{\"id\": \"${napi_id}\"}" | jq -r '."pid" // empty')
    
      if [[ -z "${napi_kthread_pid}" ]]; then
        echo "Error: Could not get PID for NAPI ${napi_id}" >&2
        exit 1
      fi
    
      taskset -pc "${cpu}" "${napi_kthread_pid}" >/dev/null
    }
  2. 运行以下命令以调用 enable_single_queue 函数:

    enable_single_queue NIC_NAME QUEUE_ID CPU_ID
    

    替换以下内容:

    • NIC_NAME:网络接口的操作系统名称,例如 ens8f0
    • QUEUE_ID:您之前获取的队列 ID。
    • CPU_ID:要在其上运行忙轮询线程的 CPU 的 ID,例如 5
  3. 确保您已规划可能会影响忙轮询配置的线程重新创建事件

规划线程重新创建事件

当内核重新创建线程时,关联的线程配置(例如 CPU 亲和性掩码和调度政策)不会保留。以下事件会导致内核重新创建忙轮询 NAPI 的线程:

  • 链接翻动/重置
  • XDP 程序附件(例如,在运行脚本以加载 Onload 或附加自定义 XDP 程序时)
  • 环形参数更改 (ethtool -G)
  • 队列计数更改 (ethtool -L)

为避免出现问题,请考虑在正常操作期间避免执行会导致线程重新创建事件的任务。

如需在重新创建线程后保持忙轮询配置,您必须获取线程的新进程 ID (PID),并将其重新绑定到 CPU。您可以再次运行 enable_single_queue 函数来完成此操作。

在 RX 队列上停用忙轮询

本部分提供了一个示例,说明如何在 Onload 堆栈 正在使用的 特定 RX 队列上停用忙轮询。

  1. 运行 onload_stackdump 以获取 Onload 堆栈正在使用的 RX 队列:

    onload_stackdump
    
  2. 在终端中运行以下 bash 脚本。disable_single_queue 函数会执行以下操作:

    • 使用 netlink (ynl) 获取与 RX 队列对应的 napi_id
    • napi_id 的 threaded 属性设置为 disabled
    disable_single_queue() {
      local -r interface="$1"
      local -r ifindex=$(cat "/sys/class/net/${interface}/ifindex")
      local -r q_id="$2"
    
      local napi_id
      napi_id=$(call_ynl --output-json --do queue-get \
        --json "{\"ifindex\": ${ifindex}, \"id\": ${q_id}, \"type\": \"rx\"}" | \
        jq -r '."napi-id"')
    
      if [[ -z "${napi_id}" || "${napi_id}" == "null" ]]; then
        echo "Error: No napi_id found for queue ${q_id} on interface ${interface}" >&2
        exit 1
      fi
    
      echo "Disabling busypolling for queue ${q_id} (NAPI ${napi_id})"
      call_ynl --do napi-set --json "{\"id\": \"${napi_id}\", \"threaded\": \"disabled\"}" >/dev/null
    }
  3. 运行以下命令以调用 disable_single_queue 函数:

    disable_single_queue NIC_NAME QUEUE_ID
    

    替换以下内容:

    • NIC_NAME:网络接口的操作系统名称,例如 ens8f0
    • QUEUE_ID:您之前获取的队列 ID。

获取队列的忙轮询状态

如需检查队列的 NAPI 状态以查看其是否正在忙轮询,您可以使用以下命令:

IFNAME=NIC_NAME
QUEUE_ID=QUEUE_ID
QUEUE_TYPE=QUEUE_TYPE

IFINDEX=$(cat "/sys/class/net/${IFNAME}/ifindex")
NAPI_ID=$(ynl --spec /usr/share/ynl/specs/netdev.yaml \
  --output-json --do queue-get \
  --json '{"ifindex": '${IFINDEX}', "id": '${QUEUE_ID}', "type": "'${QUEUE_TYPE}'"}' | \
  jq '."napi-id"')
ynl --spec /usr/share/ynl/specs/netdev.yaml \
  --output-json --do napi-get \
  --json '{"id": '${NAPI_ID}'}' | jq -r '"status: \(.threaded)"'

替换以下内容:

  • NIC_NAME:网络接口的操作系统名称,例如 ens8f0
  • QUEUE_ID:您要检查的队列的 ID。
  • QUEUE_TYPErxtx

优化性能

本部分提供了有关优化 U4 裸金属实例(U4P 和 U4C)性能的一般指导。根据工作负载的需要调整此指导中的示例。

查看 U4 裸金属实例的 NUMA 拓扑

下表介绍了 U4 裸金属实例的哪些网络接口使用哪些 NUMA 节点:

NIC(Google Cloud 名称) NIC(操作系统名称) NUMA 节点 PCIE BDF
nic0 enp22s0f0 0 0000:16:00.0
nic1 ens8f0 0 0000:27:00.0
nic2 ens48f0 2 0000:b8:00.0

上表包含 RHEL 的典型操作系统分配的网络接口名称。实际名称可能有所不同。

确定 CPU 隔离方案

为获得最佳性能,我们建议您隔离以下内容:

  • 应用使用的 CPU
  • 用于忙轮询 Onload RX 队列的 CPU
  • 用于内核和驱动程序中断的 CPU

下表提供了一个示例,说明如何在 U4 裸金属实例上隔离 CPU。根据工作负载的需要调整映射,例如,您可能需要更多应用 CPU。

用途 CPU
常规内核中断 0,1,30,31,60,61,90,91
nic0 驱动程序中断(队列 0-11) 2
nic1 驱动程序中断(队列 0-11) 3
nic0nic1 驱动程序中断(队列 12-15) 4
nic1 忙轮询 5-16
nic1 应用线程 (Onload) 17-29
nic0 忙轮询 32-43
nic0 应用线程 (Onload) 44-59
nic2 驱动程序中断(队列 0-11) 62
nic2 驱动程序中断(队列 12-15) 63
nic2 忙轮询 64-75
nic2 应用线程 (Onload) 76-89

安装用于性能优化的依赖项

如需安装用于性能优化的所需依赖项,请运行以下命令:

dnf -y install numactl tuna jq

配置内核启动参数

如需将 CPU 与内核调度隔离,请运行以下命令。这还会停用 Intel QuickAssist Technology (QAT),使其不会干扰隔离的内核。

以下示例命令会隔离 CPU 2-2932-5962-89,并将 0,1,30,31,60,61,90,91 指定为常规内核中断。这些值 与示例 CPU 隔离方案对应。根据 CPU 隔离方案的需要替换这些值。

grubby --args="isolcpus=domain,managed_irq,2-29,32-59,62-89 nohz=on nohz_full=2-29,32-59,62-89 rcu_nocbs=2-29,32-59,62-89 irqaffinity=0,1,30,31,60,61,90,91 rcu_nocb_poll modprobe.blacklist=intel_qat,qat_4xxx" --update-kernel=ALL
reboot

配置启动后 CPU 隔离

如需在启动后隔离 CPU,请运行以下命令。这些值 与示例 CPU 隔离方案对应。根据 CPU 隔离方案的需要替换这些值。

tuna isolate -c 2-29,32-59,62-89

将队列中断分配给特定 CPU

本部分介绍了如何将 gve 队列中断请求 (IRQ) 移至特定 CPU。gve 驱动程序由 GVNIC 网络接口 类型使用 Google Cloud。

  1. 确定给定网络接口和队列范围的 IRQ。请参阅以下 bash 示例,该示例定义了 irq_list 函数。

    irq_list() {
      local ifname=$1
      local queue_begin=$2
      local queue_end=$3
    
      pci_name=$(basename $(readlink /sys/class/net/${ifname}/device))
      rx_ntfy_blk_start=$(ethtool -l "${ifname}" | awk '
        /Pre-set maximums:/ { in_preset = 1 }
        /Current hardware settings:/ { in_preset = 0 }
        in_preset && $1 == "RX:" { rx = $2 }
        in_preset && $1 == "TX:" { tx = $2 }
        END { print int((rx + tx) / 2) }
      ')
      for i in $(seq "${queue_begin}" "${queue_end}"); do
        irq_tx="gve-ntfy-blk${i}@pci:${pci_name}"
        irq_rx="gve-ntfy-blk$(($i + rx_ntfy_blk_start))@pci:${pci_name}"
        # gve IRQ names are stored in a char[IFNAMSIZ + 16] so capped to 31 characters.
        echo "${irq_tx:0:31}"
        echo "${irq_rx:0:31}"
      done | paste -sd ','
    }
  2. 根据 CPU 隔离方案将 IRQ 分配给相应的 CPU。 以下示例脚本使用了 tuna 和上一步中的 irq_list 函数:

    tuna move -c 2 -q "$(irq_list enp22s0f0 0 11)"
    tuna move -c 4 -q "$(irq_list enp22s0f0 12 15)"
    
    tuna move -c 3 -q "$(irq_list ens8f0 0 11)"
    tuna move -c 4 -q "$(irq_list ens8f0 12 15)"
    
    tuna move -c 62 -q "$(irq_list ens48f0 0 11)"
    tuna move -c 63 -q "$(irq_list ens48f0 12 15)"

配置操作系统和设备设置

  1. 运行以下脚本来配置有助于最大限度减少延迟并防止默认操作系统行为干扰 Onload 配置的设置。

    echo 0 > /proc/sys/net/core/busy_poll
    echo 0 > /proc/sys/net/core/busy_read
    echo 0 > /proc/sys/kernel/timer_migration
    echo 0 > /proc/sys/net/core/rps_sock_flow_entries
    echo -1 > /proc/sys/kernel/sched_rt_runtime_us
    
    for IFNAME in "${IFNAMES[@]}"; do
      ethtool -C "${IFNAME}" rx-usecs 0 tx-usecs 0
      echo 0 > "/sys/class/net/${IFNAME}/napi_defer_hard_irqs"
      echo 15000 > "/sys/class/net/${IFNAME}/gro_flush_timeout"
    done
  2. 如需将流量从专用于 Onload 工作负载的队列中转向, 请使用 RSS (ethtool -X)。以下示例脚本基于 示例 CPU 隔离方案中的值。由于 Onload 使用队列 0-11,因此该脚本会将所有其他流量定向到队列 12-15

    for IFNAME in "${IFNAMES[@]}"; do
      ethtool -X "${IFNAME}" weight 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1
    done

后续步骤

  • 如需将实例系统时钟与主机服务器的物理 NIC 时钟同步,请参阅配置准确时间