Linux网络子系统性能观测研究报告

网络子系统性能观测研究报告

1.概述

随着智能网联汽车的发展,对车辆内、外通信的需求越来越高,也推动汽车网络技术的发展。车内应用进行网络通信,离不开内核的网络协议栈以及相应驱动程序的支持,所以观测网络性能, 就需要从内核中提取相关数据进行分析处理。

2.网络子系统背景介绍

2.1 协议栈

Linux内核网络协议栈由多种协议分层组成,如图1所示, 协议栈中每一层都有自己的职责。例如,IP协议允许通过多个路由器和网络发送数据报,可以重新组合数据包,但不能保证某些数据丢失时的可靠性,这需要在更高一层的如TCP协议中实现。

图1

用户态应用程序通过socket接口与内核网络协议栈进行交互,协议栈又通过网卡驱动程序与网卡硬件进行交互,从而实现网络通信。

2.2 关键数据结构

纵观整个网络子系统,有几个关键的数据结构贯彻其中,如sk_buffnet_devicesocketsock等,他们之间的关系如图2所示。

img
2.2.1 struct sk_buff

所有网络分层都会使用sk_buff结构来储存其报头、有关用户数据的信息,以及协调其工作的其他内部信息。从第二层到第四层都会使用这个数据结构。

内核使用一个双向链表来维护sk_buff结构,next指针指向下一个sk_buff结构,如图3所示。

sk_buff insight – WHYFI

sk_buff结构体中重要字段描述:

字段 描述
head The start of the packet
data The start of the packet payload
tail The end of the packet payload
end The end of the packet
len The amount of data of the packet

指针指向区域描述:

区域名称 描述
head room 位于head至data之间的空间,用于存储protocol header,如:TCP header, IP header, Ethernet header .etc
user data 位于data至tail之间的空间,用于存储应用层数据,一般系统调用时会使用到。
tail room 位于tail至end之间的空间,用于填充用户数据未使用完的空间。
skb_shared_info 位于end之后,用于存储特殊数据结构skb_shared_info,该结构用于描述分片信息。
2.2.2 struct net_device

在内核中,网络设备被抽象为struct net_device结构,它是网络设备硬件与上层协议之间联系的接口,该结构包括网卡硬件类信息、统计类信息、上层协议处理接口、流控接口等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct net_device {
int irq;
struct list_head ptype_all; //监听此设备上所有报文的处理函数链表
int ifindex; //接口索引值
struct net_device_stats stats; //收发报文的统计信息
const struct net_device_ops *netdev_ops;
const struct ethtool_ops *ethtool_ops;
struct in_device __rcu *ip_ptr;
struct inet6_dev __rcu *ip6_ptr;
rx_handler_func_t __rcu *rx_handler;
void __rcu *rx_handler_data;
struct netdev_queue __rcu *ingress_queue;
struct Qdisc *qdisc; //流控
};

2.2.3 struct sock

内核中网络相关的很多函数,参数往往都是struct sock,函数内部依照不同的逻辑,将struct sock转换为不同的结构。struct sock *sk是贯穿并连接于L2~L5各层之间的纽带,也是网络中最核心的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* include/net/sock.h */
struct sock {
struct sock_common __sk_common; // sock通用结构体

socket_lock_t sk_lock; // 套接字同步锁
atomic_t sk_drops; // IP/UDP包丢包统计
int sk_rcvlowat; // SO_SO_RCVLOSO_RCVLOWAT标记位

struct sk_buff_head sk_error_queue;
struct sk_buff *sk_rx_skb_cache;
struct sk_buff_head sk_receive_queue; //接收数据包队列

int sk_forward_alloc;
int sk_rcvbuf; //接收缓存大小
int sk_sndbuf; //发送缓存大小

union {
struct socket_wq __rcu *sk_wq; // 等待队列
struct socket_wq *sk_wq_raw;
};

/* ===== cache line for TX ===== */
int sk_wmem_queued; //传输队列大小
refcount_t sk_wmem_alloc; //已确认的传输字节数
unsigned long sk_tsq_flags; //TCP Small Queue标记位
union {
struct sk_buff *sk_send_head; //发送队列队首
struct rb_root tcp_rtx_queue;
};
struct sk_buff_head sk_write_queue; //发送队列

u32 sk_pacing_status; /* see enum sk_pacing */
long sk_sndtimeo; //SO_SNDTIMEO标记位
struct timer_list sk_timer; //套接字清空计时器
__u32 sk_priority; //SO_PRIORITY标记位

unsigned long sk_pacing_rate; /* bytes per second */
unsigned long sk_max_pacing_rate; //最大发包速率
struct page_frag sk_frag; //缓存页帧

int sk_err, //上次错误
sk_err_soft;
u32 sk_ack_backlog; //ack队列长度
u32 sk_max_ack_backlog; //最大ack队列长度
kuid_t sk_uid; //user id
struct pid *sk_peer_pid; //套接字对应peer id
const struct cred *sk_peer_cred;
long sk_rcvtimeo; //接收超时
ktime_t sk_stamp; //时间戳
};
2.2.4 struct socket

内核中的进程通过文件描述符fd中的struct socket结构体与内核网络协议栈进行沟通,如图4所示。

img
1
2
3
4
5
6
7
8
9
10
11
struct socket
{
socket_state state; // socket state
short type ; // socket type
unsigned long flags; // socket flags
struct fasync_struct *fasync_list;
wait_queue_head_t wait;
struct file *file;
struct sock *sock; // socket在网络层的表示;
const struct proto_ops *ops;
}

2.3 内核收包过程

收包过程大致可分为网卡初始化、网卡收包、DMA将包复制到RX队列、触发硬件中断IRQ、内核调度到ksoftirqd线程、软中断处理从ringbuffer取数据送到协议栈、协议栈L2处理、协议栈L3处理、协议栈L4处理这9个过程,如图5所示。

image-20230207192247422
  • 网卡收包触发硬中断

数据从网线进入网卡,通过DMA写到ringbuffer,然后就该内核来收包了。由于硬中断期间可能会导致事件丢失,所以在硬中断处理期间不能有耗时操作,只是完成了将数据包放入EQ(Event Queue),之后执行napi_schedule()调度NAPI去处理。在napi_schedule()中,会调用__raise_softirq_irqoff()触发一个NET_RX_SOFTIRQ类型软中断,并触发执行软中断处理函数net_rx_action()。NAPI方式结合了轮询和中段两种方式。每次执行到NAPIpoll()方法时,会批量从ringbuffer收包,并且会尽量把所有待收的包都收完。

  • 软中断处理

net_rx_action从处理ringbuffer开始,遍历当前CPU队列的NAPI变量队列,依次执行其poll方法,如mlx5e网卡驱动的mlx5e_napi_poll()。在该函数中,会依次处理TX和RX队列,包含XDP程序(Driver模式)也是在这里执行。之后,从ringbuffer中初始化一个struct sk_buff *skb结构体变量,也就是常说的skb数据包。然后调用napi_gro_receive()执行GRO,GRO的功能是对分片的包进行重组然后交给更上层,以提高吞吐。最后,调用netif_receive_skb_list_internal()进入内核协议栈处理。

  • 协议栈L2处理

netif_receive_skb_list_internal()中,会根据是否开启RPS执行不同逻辑。在未开启RPS时,会通过__netif_receive_skb_core完成将数据送到协议栈处理。在该函数中,会依次进行以下操作:(1)处理skb时间戳。(2)执行Generic XDP程序。(3)处理VLAN header。(4)TAP处理,如tcpdump抓包、流量过滤。(5)流量控制TC,包括TC规则和TC BPF程序。(6)Netfilter,处理iptables规则。最后通过skb->dev->rx_handler(&skb)进入L3 ingress处理,如IPv4处理。

  • 协议栈L3处理(以IPv4为例)

IP层在函数inet_init中将自身注册到了ptype_base哈希表中。在deliver_skb()中会调用注册的.func方法,对应IPv4的ip_rcv()函数。ip_rcv()中进行了数据合法性验证、统计计数器更新等,在最后会以netfilter hook的方式调用ip_rcv_finish方法。处理结束的时候,调用ip_local_deliver_finish(),通过寻找注册在这个协议上的struct net_protocol变量,将数据包送到协议栈更上层。如TCP协议对应的tcp_v4_rcv、UDP协议对应的udp_v4_rcv

  • 协议栈L4处理(以UDP为例)

inet_init()中,通过inet_add_protocol()注册了udp_protocol,其中.handler方法指向了udp_rcv函数。这是从IP层进入UDP层的入口。在该函数中,调用了__udp4_lib_rcv()接收UDP报文。在__udp4_lib_rcv中,首先进行合法性检查,获取UDP头、UDP数据报长度、源地址、目标地址等信息,然后进行一些完整性检测和checksum验证。在IP层中,送到更上面一层协议前,会将一个dst_entry关联到skb,如果对应的dst_entry找到了,并且有对应的socket,udp4_lib_rcv 会将 packet 放到 socket 的接收队列。网络数据通过__skb_queue_tail()进入socket接收队列,并且会在之前进行一些检查和更新计数,并通过sk_filter执行socket BPF程序。

3.网络性能观测方法

3.1 概述

网络性能包括链路上的包转发时延、吞吐量、带宽等指标,也包括主机侧的实时网络状况,传统工具包括:ss、ip、nstat、netstat、sar、nicstat、ethtool、tcpdump等,BPF工具包括:netsize、nettxlat、superping、tcpconnect、tcplife、tcptop、udpconnect、sockstat等,这些工具包含了网络子系统的各个层面,如图所示,可以对系统网络状况进行较为全面的了解,从而进一步去分析可能存在的问题与瓶颈,提升网络性能。

bpftools

3.2 链路层与网络层

(1) 传统工具
  • ethtool

ethtool可以利用-i-k选项检查网络接口的静态配置信息,也可利用-S选项打印驱动程序统计信息。如图所示。

image-20230211122851629

这行命令从内核中的ethtool框架中获取统计信息,大部分网络设备驱动程序都支持该框架。网络设备驱动程序也可定义自己的ethtool指标。

使用-i选项可展示驱动细节信息,使用-k 可展示网络接口的可调节项,如图所示。

image-20230211123322383
  • nicstat

nicstat可以打印网络接口的统计信息,如图所示。

image-20230211124725911

这个输出中包括了一些饱和度统计信息,可以用来识别网络接口的饱和程度。

  • netstat

netstat是一个用来汇报各种类型的网络统计信息的传统工具,包括以下命令:

参数 描述
(default) 列出所有处于打开状态的套接字
-a 列出所有套接字的信息
-s 网络软件栈统计信息
-i 网络接口统计信息
-r 列出路由表
image-20230211154338651
  • ip

ip是一个管理路由、网络设备、接口以及隧道的工具,可用来打印各种对象的统计信息,如link、address、route等。如图为使用ip -s link打印link的统计信息。

image-20230211154647754
  • sar

系统报表活动工具sar可以打印出各种网络统计信息表。

参数 说明
-n DEV 网络接口统计信息
-n EDEV 网络接口错误统计信息
-n IP,IP6 IPv4、IPv6数据包统计信息
-n EIP,EIP6 IPv4、IPv6错误统计信息
-n ICMP,ICMP6 IPv4、IPv6 ICMP统计信息
-n EICMP,EICMP6 IPv4、IPv6 ICMP错误统计信息
-n TCP TCP统计信息
-n ETCP TCP错误统计信息
-n SOCK,SOCK6 IPv4、IPv6 套接字用量
image-20230211152114811

上图为sar -n DEV,IP运行结果,可以看到关于网络设备和IPv4统计信息

(2) BPF工具

netsize从网络设备层展示发送和接收的包的大小,可以同时显示软件分段托管之前和之后的大小(GSO和GRO)。该输出可以用来调查发送之前的碎片化情况。

image-20230211151345524
  • netqtop

netqtop对指定网络接口的每个队列的传输和接收的数据包进行统计,帮助开发者检查其流量负载是否平衡。 结果显示为一个表格,列有PPS、BPS、平均大小和数据包计数。每隔给定的时间间隔以秒为单位进行打印,如图所示。

image-20230211133203018

该工具使用net:net_dev_start_xmitnet:netif_receive_skb内核追踪点。 由于它使用tracepoint,该工具只在Linux 4.7以上版本上工作。 netqtop在网络流量大的时候会引入大量的开销。

3.3 传输层

(1) 传统工具
  • tcpdump

tcpdump可以用来抓取网络包进行分析。借助Wireshark GUI工具,可以利用tcpdump的输出文件来检查包头、跟踪某个TCP连接、进行包重组以及其他操作,如图所示。

image-20230212115923283
  • sar

通过sar获取TCP统计信息sar -n TCP,ETCP

image-20230212094305723
(2) BPF工具
  • tcptop

tcptop是一个BCC工具,可以展示使用TCP的进程,如图所示。该工具跟踪TCP发送和接收的代码路径,包括tcp_sendmsgtcp_cleanup_rbuff,并将数据记录在BPF Map中。通过参数-p可以指定仅跟踪的进程PID。

image-20230212111446136
  • tcpwin

tcpwin跟踪TCP发送拥塞窗口的尺寸,以及其他的内核参数,以便分析拥塞控制算法的性能,如图所示。

image-20230212111812047
  • udpconnect

udpconnect跟踪本机通过connect系统调用发起的UDP连接(不包括无连接的UDP通信),如图所示。该工具跟踪内核中的UDP连接函数,并且由于该函数调用频率较低,所以该工具的额外开销很低。

image-20230212112132666

3.4 套接字

(1) 传统工具
  • ss

ss是一个套接字统计工具,可以简要输出当前打开的套接字信息。默认的输出提供了网络套接字的高层信息,可以使用以下选项显示更多信息:

参数 描述
-a,–all 显示所有套接字
-m,–memory 显示套接字内存使用情况
-i,–info 显示内部TCP信息
-t,–tcp 仅显示TCP套接字
-u,–udp 仅显示UDP套接字
image-20230211161523945

通过-ti参数可以显示更多tcp内部信息,包括以下信息:

参数 描述
cong_alg 拥塞算法名称,默认为”cubic”
wscale 窗口放大倍数
rto TCP重传超时值,毫秒
backoff 用于指数回退重传
rtt RTT(平均往返时间)
mss MSS(最大数据分段长度)
cwnd 拥塞窗口大小
pmtu 路径MTU
ssthresh TCP拥塞窗口慢启动阈值
bytes_acked/bytes_received ack/rcv的字节数
segs_out/segs_in 发出和接收的分段数
lastsnd/lastrcv/lastack 距离最后一个send/rcv/ack的包的时间,毫秒

通过-as显示套接字概况

image-20230212120220554
(2) BPF工具
  • soprotocol

soprotocol按进程名和传输协议来跟踪新套接字连接的建立,如图所示。

image-20230212123557846
  • socketio

socketio按进程、方向、协议和端口来展示套接字的I/O统计信息,如图所示。

image-20230212124407482

4.eBPF网络性能数据提取方法

4.1 概述

nic_throughput是LMP下观测网络性能的eBPF工具,其可以每秒输出指定网卡发送与接收的字节数、包数与包平均大小。(BPS:每秒多少字节 PPS:每秒多少包)

命令行参数:

1
2
3
4
5
6
7
8
9
10
-n,--name
[必选] 网卡名称
-i, --interval
[可选] 输出时间间隔,默认为1s
-c, --count
[可选] 输出条数,默认为99999999
--print
[可选] 在命令行打印结果
--visual
[可选] 将结果通过influxdb-grafana可视化

4.2 原理

nic_throughput使用了两个tracepoint跟踪点,分别是:net_dev_start_xmitnetif_receive_skb,分别对应TX和RX路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* TX路径 */
static int xmit_one(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq, bool more)
{
unsigned int len;
int rc;

if (dev_nit_active(dev))
dev_queue_xmit_nit(skb, dev);

len = skb->len;
PRANDOM_ADD_NOISE(skb, dev, txq, len + jiffies);
trace_net_dev_start_xmit(skb, dev); //! tracepoint 挂载点
rc = netdev_start_xmit(skb, dev, txq, more);
trace_net_dev_xmit(skb, rc, dev, len);

return rc;
}

/* RX路径 */
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
struct packet_type **ppt_prev)
{
struct packet_type *ptype, *pt_prev;
rx_handler_func_t *rx_handler;
struct sk_buff *skb = *pskb;
struct net_device *orig_dev;
bool deliver_exact = false;
int ret = NET_RX_DROP;
__be16 type;

net_timestamp_check(!netdev_tstamp_prequeue, skb);

trace_netif_receive_skb(skb); //! tracepoint 挂载点

orig_dev = skb->dev;

skb_reset_network_header(skb);
if (!skb_transport_header_was_set(skb))
skb_reset_transport_header(skb);
skb_reset_mac_len(skb);

pt_prev = NULL;
...
...
}

通过编写eBPF程序,得到sk_buff结构中字段len的长度,得到包的大小,并且记录次数作为包的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* TRACEPOINT部分 */
TRACEPOINT_PROBE(net, net_dev_start_xmit){
/* read device name */
struct sk_buff* skb = (struct sk_buff*)args->skbaddr;
...
updata_data(data, skb->len);
}

TRACEPOINT_PROBE(net, netif_receive_skb){
struct sk_buff skb;

bpf_probe_read(&skb, sizeof(skb), args->skbaddr);
...
updata_data(data, skb.len);
}

/* MAP部分 */

struct queue_data{
u64 total_pkt_len;
u32 num_pkt;
};

BPF_HASH(tx_q, u16, struct queue_data, MAX_QUEUE_NUM);
BPF_HASH(rx_q, u16, struct queue_data, MAX_QUEUE_NUM);

static void updata_data(struct queue_data *data, u64 len){
data->total_pkt_len += len;
data->num_pkt ++;
}

在用户态程序,通过读取tx_qrx_q这两个map,得到单位时间内的总的字节数和包数,与该时间进行除法运算得到BPS和PPS

1
2
3
4
5
6
7
8
9
table = b['tx_q'] #或b['rx_q']

for k, v in table.items():
qids += [k.value]
tlen += v.total_pkt_len
tpkt += v.num_pkt

tBPS = tlen / print_interval
tPPS = tpkt / print_interval

4.3 结果展示与分析

nic_throughput与sar同时进行输出,如图所示。在同一时间点上,对比nic_throughput的数据和sar的数据输出是不同的,尤其是BPS。

image

结果的不同是由于数据来源的不同。sar工具的数据来源(DEV参数)是/proc/net/dev,如图所示。

image-20230212144030451

从内核中寻找数据来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/* net/core/net-procfs.c */
static const struct seq_operations dev_seq_ops = {
.start = dev_seq_start,
.next = dev_seq_next,
.stop = dev_seq_stop,
.show = dev_seq_show, //show方法
};

static int dev_seq_show(struct seq_file *seq, void *v)
{
if (v == SEQ_START_TOKEN)
seq_puts(seq, "Inter-| Receive "
" | Transmit\n"
" face |bytes packets errs drop fifo frame "
"compressed multicast|bytes packets errs "
"drop fifo colls carrier compressed\n"); //表头
else
dev_seq_printf_stats(seq, v); //数据从此打印
return 0;
}

static void dev_seq_printf_stats(struct seq_file *seq, struct net_device *dev)
{
struct rtnl_link_stats64 temp;
const struct rtnl_link_stats64 *stats = dev_get_stats(dev, &temp); //数据来源

seq_printf(seq, "%6s: %7llu %7llu %4llu %4llu %4llu %5llu %10llu %9llu "
"%8llu %7llu %4llu %4llu %4llu %5llu %7llu %10llu\n",
dev->name, stats->rx_bytes, stats->rx_packets,
stats->tx_bytes, stats->tx_packets,
stats->tx_errors, stats->tx_dropped,
...
}

/* net/core/dev.c */

struct rtnl_link_stats64 *dev_get_stats(struct net_device *dev,
struct rtnl_link_stats64 *storage)
{
const struct net_device_ops *ops = dev->netdev_ops;

if (ops->ndo_get_stats64) { //数据来源
memset(storage, 0, sizeof(*storage));
ops->ndo_get_stats64(dev, storage);
} else if (ops->ndo_get_stats) {
netdev_stats_to_stats64(storage, ops->ndo_get_stats(dev));
} else {
netdev_stats_to_stats64(storage, &dev->stats);
}
storage->rx_dropped += (unsigned long)atomic_long_read(&dev->rx_dropped);
storage->tx_dropped += (unsigned long)atomic_long_read(&dev->tx_dropped);
storage->rx_nohandler += (unsigned long)atomic_long_read(&dev->rx_nohandler);
return storage;
}
EXPORT_SYMBOL(dev_get_stats);

这里ndo_get_statsndo_get_stats64需要网卡驱动程序实现,以ixgbe网卡驱动为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* drivers/net/ethernet/intel/ixgbe/ixgbe_main.c */

static const struct net_device_ops ixgbe_netdev_ops = {
.ndo_open = ixgbe_open,
.ndo_stop = ixgbe_close,
.ndo_start_xmit = ixgbe_xmit_frame,
...
.ndo_get_stats64 = ixgbe_get_stats64, //数据来源
...
}

static void ixgbe_get_stats64(struct net_device *netdev,
struct rtnl_link_stats64 *stats)
{
struct ixgbe_adapter *adapter = netdev_priv(netdev);
int i;

rcu_read_lock();
for (i = 0; i < adapter->num_rx_queues; i++) {
struct ixgbe_ring *ring = READ_ONCE(adapter->rx_ring[i]);
u64 bytes, packets;
unsigned int start;

if (ring) {
do {
start = u64_stats_fetch_begin_irq(&ring->syncp);
packets = ring->stats.packets;
bytes = ring->stats.bytes; //从ixgbe网卡的ixgbe_ring结构中取得
} while (u64_stats_fetch_retry_irq(&ring->syncp, start));
stats->rx_packets += packets;
stats->rx_bytes += bytes;
}
}
...
}

可以看出,在ixgbe网卡的最终数据是从ixgbe_ring结构中得到的,而写这个数据的是ixgbe_clean_tx_irqixgbe_clean_rx_irq两个函数,而这两个函数正是ixgbe_pollTX和RX队列处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* drivers/net/ethernet/intel/ixgbe/ixgbe_main.c */

static bool ixgbe_clean_tx_irq(struct ixgbe_q_vector *q_vector,
struct ixgbe_ring *tx_ring, int napi_budget)
{
...
tx_ring->stats.bytes += total_bytes;
tx_ring->stats.packets += total_packets;
...
}

static int ixgbe_clean_rx_irq(struct ixgbe_q_vector *q_vector,
struct ixgbe_ring *rx_ring,
const int budget)
{
...
rx_ring->stats.packets += total_rx_packets;
rx_ring->stats.bytes += total_rx_bytes;
...
}

所以,可以看出,二者数据不同的原因是数据源不同。eBPF程序使用tracepoint静态跟踪点统计sk_buff结构中的len字段,而通过sar等传统工具利用的proc文件系统下的net/dev文件数据来源于设备驱动,而这个数据是网卡设备统计的,与tracepoint跟踪点不在一个层面,所以数据值不同。


Linux网络子系统性能观测研究报告
http://ziyangfu.github.io/2023/04/13/Linux网络子系统性能观测研究报告/
作者
FZY
发布于
2023年4月13日
许可协议