FPGA调优是提高硬件性能的重要手段。本文将总结一些常见的FPGA调优技巧和方法,从我实际的经验出发,分享在FPGA开发过程中的感悟。
丢包情况检查
网卡如果存在丢包可以通过以下方式检查:
# 查看网卡统计信息ifconfig
# 如果没有ifconfigip -s link show enp161s0f0
# 查看特定网卡的统计信息ethtool -S enp161s0f0 | grep drop确定onload是否被挂载:
onload_stackdump | lesscat /proc/<PID>/maps | grep libonloa使用onload来启动程序
EF_RXQ_SIZE=4096 EF_MAX_PACKETS=524288 EF_MAX_RX_PACKETS=393216 EF_HIGH_THROUGHPUT_MODE=1 EF_STACK_PER_THREAD=1 EF_TCP=0 EF_UDP=1 onload --profile=latency ./程序轮询和中断
轮询处理
上面的参数中 --profile=latency 表示使用低延迟配置,这些配置文件通常都是一些.opf文件,可以通过下面的命令查看参数:
cat /usr/libexec/onload/profiles/latency.opf其中最重要的参数就是其中的
EF_POLL_USEC=100000通常情况下,当应用程序尝试从Socket读取数据但未能获取到数据时,应用程序会进入操作系统内核并陷入阻塞状态。当数据可用后,网络适配器会中断 CPU 的运作,从而使内核能够重新安排应用程序的继续执行流程。
Onload 可以配置为在用户模式下在处理器上持续运行,持续时间为指定的微秒数,同时等待来自网络的数据。如果持续运行的时间到期,处理器将恢复到传统的阻塞模式。而非阻塞Socket则始终会立即返回结果,因为它们不受持续运行的影响。
Onload 使用 EF_POLL_USEC 环境变量来配置旋转超时的时间长度。
设置 EF_POLL_USEC 参数。这会使 Onload 在处理器上运行一段时间,持续时间最多为指定的微秒数,之后才会被阻塞。此设置适用于 TCP 和 UDP 协议,同时也在 recv() 、 select() 、 pselect() 和 poll() 、 ppoll() 和 epoll_wait() 、 epoll_pwait() 和 onload_ordered_epoll_wait() 中应用。
如果 EF_POLL_USEC 和 EF_SPIN_USEC 这两个参数都没有被设置,那么 Onload 将会采用默认的中断驱动行为,因为 EF_INT_DRIVEN 环境变量默认是启用的。
设置 EF_POLL_USEC 变量的同时,也会设置以下环境变量。
EF_SPIN_USEC=EF_POLL_USECEF_SELECT_SPIN=1EF_EPOLL_SPIN=1EF_POLL_SPIN=1EF_PKT_WAIT_SPIN=1EF_TCP_SEND_SPIN=1EF_UDP_RECV_SPIN=1EF_UDP_SEND_SPIN=1EF_TCP_RECV_SPIN=1EF_BUZZ_USEC=MIN(EF_POLL_USEC, 100)EF_SOCK_LOCK_BUZZ=1EF_STACK_LOCK_BUZZ=1注意,这里需要系统关闭自适应调节功能,并将中断调节的优先级设置为较高值(以微秒为单位),以避免系统因过多的中断而陷入混乱。请使用以下命令来实现这一操作:
/sbin/ethtool -C eth2 rx-usecs 60 adaptive-rx off使用onload_stackdump
查看文档:
onload_stackdump doconload_stackdump -h输出stack信息:
onload_stackdump#stack-id stack-name pids6 teststack 28570输出所有lots输出的统计信息和描述:
onload_stackdump 4 describe_stats输出onload加载的进程:
onload_stackdump processes输出onload加载的进程的环境变量:
onload_stackdump env减少内存分配
建议预先分配内存,以避免由于分配和内存映射所带来的延迟抖动问题。
可以设置 EF_PREALLOC_PACKETS 和 EF_MAX_PACKETS 环境变量,以便在堆栈创建时分配数据包缓冲区。例如:
EF_PREALLOC_PACKETS=1 EF_MAX_PACKETS=64000 onload <myapplication>...为某个堆栈分配预缓存包缓冲区会占用其他堆栈可用的缓冲区数量。用户应当考虑,为某个堆栈过度分配缓冲区容量意味着可以分配给其他位置的额外缓存空间。
尽量在用户空间中处理
许多应用程序在大部分处理工作在用户级别而非内核级别时,能够表现得更加出色。要了解某个应用程序的性能表现,请执行以下命令:
onload_stackdump lots | grep polls| 计数器 | 描述 |
|---|---|
k_polls | 从内核处获取套接字事件队列的次数。 |
u_polls | 从用户空间轮询套接字事件队列的次数。 |
periodic_polls | 周期性计时器进行事件查询的次数。 |
interrupt_polls | 用于轮询网络事件的中断次数。 |
deferred_polls | 该轮询被推迟到堆栈锁持有者手中的次数。 |
timeout_interrupt_polls | 用于轮询网络事件超时的中断次数。 |
通过设置EF_POLL_USEC参数来尽量在用户空间进行轮询处理。
中断越少越好
一个设计良好的应用程序能够实现处理的中断次数与完成的实际工作量的平衡。例如,通过每次中断处理多个数据包,而不是只处理一个数据包,就可以提高处理效率。即使是那些需要持续运行的应用程序也能从偶尔发生的中断中受益。例如,当一个持续运行的线程从 CPU 上被移除后,在 250 微秒后发生的超时中断会促使该线程重新执行任务。
onload_stackdump lots | grep ^interrupt| 计数器 | 描述 |
|---|---|
Interrupts | 堆栈接收到的中断总数量。 |
Interrupt polls | 堆栈被轮询的次数,通过中断调用实现。 |
Interrupt events | 当被中断请求调用时,所处理的事件数量。 |
Interrupt wakes | 应用程序因中断而被唤醒的次数。 |
Interrupt primes | 在通过自旋或轮询堆栈后,中断被重新激活的次数。 |
Interrupt no events | 没有发生需要恢复的事件的堆栈调查数量。 |
Interrupt lock contends | 该应用程序在中断触发之前就已经获取了锁资源。 |
Interrupt budget limited | 在通过中断处理轮询时,当达到 NAPI 预算限制时,轮询会被停止。此时,剩余的事件会被处理在堆栈工作队列中。 |
如果中断较多,应当增加EF_POLL_USEC的值。
减少丢包
网络性能的优劣会受到任何数据包丢失情况的影响。这一点在那些基于单播或组播 UDP Socket 构建的可靠数据传输协议中表现得尤为明显。
首先,需要检查数据包是否在到达 Onload 堆栈之前就被网络适配器丢弃了。可以使用 ethtool 从网络适配器中直接获取统计信息。
ethtool -S enp161s0f0 | grep drop| 计数器 | 描述 |
|---|---|
rx_noskb_drops | 当没有更多的套接字缓冲区可供使用时,会丢失多少数据包。 |
port_rx_nodesc_drops | 当接收环缓冲区中没有更多的描述符可供接收时,会丢失多少个数据包。 |
port_rx_dp_di_dropped_packets | 由于过滤器认为某些数据包应该被丢弃,因此产生了丢包的情况。这种情况发生在数据包不符合任何过滤条件时,或者匹配到的过滤器指示该数据包应该被丢弃时。 |
port_rx_dp_q_disabled_packets | 发送到不存在的队列中的数据包数量。在初始化或销毁过程中可能会看到少量此类情况,但如果数量较大或持续增加,则表明 VI 集的大小与实际 VI 的数量之间存在不匹配的问题。 |
port_rx_pm_discard_bb_overflow | 由于数据包内存缓冲区溢出,导致的一些数据包被丢弃。 |
port_rx_pm_discard_vfifo_full | 由于适配器没有足够的主包内存来接收数据包,导致数据包被丢弃的次数。 |
port_rx_pm_discard_mapping | 由于设置了 802.1p 优先级,导致的数据包被丢弃的数量。 |
出现这种情况的最常见原因是应用程序被重新调度了。可以通过查看 cat /proc/<pid>/sched 提供的应用程序调度统计信息来发现这一点。 nr_involuntary_switches 计数器记录了该进程被重新调度的次数,这通常是因为有中断处理程序或其他任务在同一 CPU 核心上运行所导致的。应确保应用程序使用的 CPU 核心能够被隔离,以避免重新调度。如果无法将核心隔离起来,那么可以考虑切换到中断模式运行。
如果在网络层面观察到数据包丢失现象,原因是接收缓冲区不足,可以尝试通过调整 EF_RXQ_SIZE 参数来增大接收描述符队列的大小。如果在套接字层面出现数据包丢失的情况,请参考相关应用程序文档。或许还可以尝试调整套接字缓冲区的尺寸(参见 EF_UDP_RCVBUF)。将 EF_EVS_PER_POLL 变量的值设置为更高的值也能提高效率。
最小化锁争用
锁争用现象会严重影响性能。当多个线程共享一个栈时,持有栈锁的线程会阻止其他线程执行有用的操作。对于线程数量较少的应用程序来说,或许可以为每个线程创建一个独立的栈(参考 EF_STACK_PER_THREAD 和 Stacks API)。
使用 onload_stackdump 来标识锁定争用的情况:
onload_stackdump lots | egrep "(lock_)|(sleep)"| 计数器 | 描述 |
|---|---|
periodic_lock_contends | 周期性计时器无法锁定堆栈的次数。 |
interrupt_lock_contends | 中断处理程序尝试获取堆栈锁的次数,因为该锁已经被用户级或其他上下文持有而无法获取。 |
timeout_interrupt_lock_contends | 由于超时中断的原因,堆栈锁定时断了多次。 |
sock_sleeps | 单个套接字上某个线程处于阻塞状态的次数。 |
sock_sleep_primes | 选中/轮询/epoll 功能启用的中断次数。 |
unlock_slow | 采用较慢的路径来解锁堆栈锁的次数。 |
unlock_slow_pkt_waiter | 由于数据包内存不足,导致解锁操作延迟的次数。 |
unlock_slow_socket_list | 延迟的套接字列表引发解锁慢路径的次数。 |
unlock_slow_need_prime | 中断触发程序引发解锁慢路径出现的次数。 |
unlock_slow_wake | 采用解锁慢路径来唤醒线程的次数。 |
unlock_slow_swf_update | 为了更新 SW 过滤器,采用了多少次解锁慢路径的方式。 |
unlock_slow_close | 采用解锁慢路径来关闭插座/管道的次数。 |
unlock_slow_syscall | 在解锁慢路径时,需要调用系统调用的次数。 |
lock_wakes | 当线程被阻塞在堆栈锁上时,该线程被唤醒的次数。 |
stack_lock_buzz | 线程在等待堆栈锁时重复执行的次数。 |
sock_lock_sleeps | 线程等待互斥锁锁定的次数。 |
sock_lock_buzz | 线程在等待 sock 锁时重复执行的次数。 |
tcp_send_ni_lock_contends | TCP 在堆栈锁上竞争的次数。 |
udp_send_ni_lock_contends | UDP 进程尝试获取堆栈锁的次数。 |
getsockopt_ni_lock_contends | getsockopt() 尝试锁定栈锁定的次数。 |
setsockopt_ni_lock_contends | setsockopt() 尝试锁定栈锁定的次数。 |
lock_dropped_icmps | 由于竞争导致未能处理的 ICMP 消息数量。 |
当堆栈争用情况降至最低时,性能将会提升。当线程共享堆栈时,线程在等待堆栈锁的情况下,最好采用自旋而非睡眠。可以通过增加 EF_BUZZ_USEC 的值来减少“睡眠”时间。在可能的情况下,建议使用每个线程一个堆栈。
一个 Onload Stack 指的是一个 TCP/IP 协议的栈实例。该栈包含了传输和接收缓冲区、已打开的连接以及相关的端口号和栈选项。每个栈都会关联一个或多个虚拟网卡(通常每个物理端口对应一个虚拟网卡)。
在正常的使用场景中,每个加速进程都会拥有自己的 Onload Stack,该堆栈由该进程创建的所有连接共享。此外,多个进程也可以共享同一个 Onload 堆栈实例。一个应用程序还可以拥有多个 Onload 堆栈。
对于其中关于UDP的输出
| Sample Output | Description |
|---|---|
UDP | Socket configuration: |
4:1 | Stack |
lcl=192.168.1.2:38142 | Local ip |
rmt=192.168.1.1:42638 | Remote ip |
UDP | Connection is UDP. |
lock: 20000000 LOCKED | Internal socket lock status, as a hexadecimal number, followed by status names including:
|
rx_wake=000e69b0 | Internal sequence value that is incremented each time a receive queue is ‘woken’.(RQ) indicates that a wake has been requested. |
tx_wake=000e69b1 | Internal sequence value that is incremented each time a transmit queue is ‘woken’.(RQ) indicates that a wake has been requested. |
flags: | Flags (if any), including:
|
ul_poll: | User-level poll(): |
|
|
|
|
uid=0 | User id that owns this socket. |
s_flags: FILTER | Socket flags, including:
|
rcvbuf=129024 | Socket receive buffer size. |
sndbuf=129024 | Socket send buffer size. |
bindtodev=-1(01,0:0) | Device to which the socket is bound, given as an interface, or -1 if unbound. This is followed by an (interface index, hardware port |
ttl=64 | Initial TTL value. |
rcvtimeo_ms=0 | Timeout value (microseconds) before an error is generated for receive functions, as set by SO_RCVTIMEO. |
sndtimeo_ms=0 | Timeout value (microseconds) before an error is generated for send functions, as set by SO_SNDTIMEO. |
sigown=0 | The PID receiving signals from this socket. |
cmsg= | Current message flags, including:
|
rx_errno=0 | Zero whilst data can still arrive, otherwise contains error code. |
tx_errno=0 | Zero if transmit can still happen, otherwise contains error code. |
so_error=0 | Current socket error, or zero if no error. |
os_sock=0 | 0 if the socket is handled by Onload, 1 if the socket is handled by the OS and not by Onload. |
TX | Socket is being used for transmit. |
epoll3: ready_list_id 0 | List of ready sockets from the epoll3 set. |
udpflags: FILT MCAST_LOOP RXOS | UDP flags currently set for this socket, including:
|
rcv: | Receive data: |
|
|
|
|
|
|
rcv: | Further receive data: |
|
|
|
|
|
|
|
|
|
|
rcv: | Further receive data: |
|
|
os_slow=0 |
|
os_error=0 |
|
snd: | Send data: |
|
|
|
|
|
|
snd: | Send data… |
| …about locks: |
|
|
|
|
|
|
|
|
|
|
snd: | Send data… |
| …about multicast: |
|
|
|
|
|
|
snd: | Send data… |
| …about unconnected sends: |
|
|
|
|
|
|
| Status of cached control plane information, one of:
|
snd: | Further send data… |
| …about unconnected sends: |
|
|
|
|
|
|
|
|
|
|
snd: | Further send data… |
| …about unconnected sends: |
|
|
|
|
snd: | Further send data… |
| …about unconnected sends: |
|
|
|
|
| Status of cached control plane information, one of:
|
snd: | Further send data… |
| …about unconnected sends: |
|
|
|
|
|
|
|
|
|
|
snd: | Further send data: |
|
|
|
|
|
|
snd: | Further send data: |
|
|
|
|
|
|
snd: | Further send data: |
|
|
|
|
|
|
|
|
Parameter Reference
所有参数的介绍见这里。
EF_RXQ_SIZE
最大值为32768,设置接收描述符环的大小。该数值必须是 2 的幂次值。具体有效值取决于架构类型。对于 EF10 架构,对应的数值为 512 、 1024 、 2048 或 4096 。
更大的环尺寸可以吸收更大的数据包流量,而不会出现性能下降的情况,不过由于工作集的大小增加,效率可能会降低。
EF_MAX_PACKETS
默认值为32768,最小值为1024。每个 Onload 堆栈中数据包缓冲区的数量上限。数据包缓冲区需要硬件资源,如果多个堆栈各自使用大量数据包缓冲区,就会成为限制因素。可以使用此选项来限制堆栈所使用的硬件资源和内存数量。该选项的上限由 max_packets_per_stack Onload 模块选项决定。
EF_MAX_RX_PACKETS
接收数据路径可以使用的协议栈中数据包缓冲区的最大数量。此值应小于 EF_MAX_PACKETS,以确保为发送路径保留一些数据包缓冲区。
EF_HIGH_THROUGHPUT_MODE
名称是rx_merge_mode,默认值为0。启用高吞吐量模式。该模式会在接收路径中启用数据包合并功能,从而提高吞吐量。该模式会增加延迟来强化吞吐。
EF_STACK_PER_THREAD
名称是“stack_per_thread,默认值为0`。启用每线程堆栈模式。该模式会为每个线程创建一个独立的 Onload 堆栈实例,从而减少锁争用和提高性能。
EF_TCP
名称是ul_tcp,默认值为1。启用 TCP 协议栈。设置为0时,可以关闭Onload对TCP的加速。
EF_UDP
名称是ul_udp,默认值为1。启用 UDP 协议栈。设置为0时,可以关闭Onload对UDP的加速。