关键词
- Red Hat Enterprise Linux (RHEL) 7.6
- SKB linearization failed
- vm.min_free_kbytes
一、问题现象
一台业务主机dmesg 日志中频繁有以下报错:
[qede_ start_ xmit :1289(p1p2)]SKB linearization failed - silently dropping this SKB。
二、问题分析
1、基础概念
Packet:通过网卡收发的报文,包括链路层、网络层、传输层的协议头和携带的数据
Data Buffer:用于存储 packet 的内存空间
SKB:struct sk_buffer 的简写
Struct sk_buffer (SKB)是 linux TCP/IP stack 中,用于管理Data Buffer的结构。Sk_buffer 在数据包的发送和接收中起着重要的作用。为了提高网络处理的性能,应尽量避免数据包的拷贝。Linux 内核开发者们在设计 sk_buffer 结构的时候,充分考虑到这一点。目前 Linux 协议栈在接收数据的时候,需要拷贝两次:数据包进入网卡驱动后拷贝一次,从内核空间递交给用户空间的应用时再拷贝一次。
2、源码查错
The qede_start_xmit() 源码的 1287-1290行指向了本次报错内容
1256 /* Main transmit function */
1257 netdev_tx_t qede_start_xmit(struct sk_buff *skb, struct net_device *ndev)
1258 {
1259 struct qede_dev *edev = netdev_priv(ndev);
1260 struct netdev_queue *netdev_txq;
1261 struct qede_tx_queue *txq;
1262 struct eth_tx_1st_bd *first_bd;
1263 struct eth_tx_2nd_bd *second_bd = NULL;
1264 struct eth_tx_3rd_bd *third_bd = NULL;
1265 struct eth_tx_bd *tx_data_bd = NULL;
1266 u16 txq_index, val = 0;
1267 u8 nbd = 0;
1268 dma_addr_t mapping;
1269 int rc, frag_idx = 0, ipv6_ext = 0;
1270 u8 xmit_type;
1271 u16 idx;
1272 u16 hlen;
1273 bool data_split = false;
1274
1275 /* Get tx-queue context and netdev index */
1276 txq_index = skb_get_queue_mapping(skb);
1277 WARN_ON(txq_index >= QEDE_TSS_COUNT(edev));
1278 txq = edev->fp_array[edev->fp_num_rx + txq_index].txq;
1279 netdev_txq = netdev_get_tx_queue(ndev, txq_index);
1280
1281 WARN_ON(qed_chain_get_elem_left(&txq->tx_pbl) < (MAX_SKB_FRAGS + 1));
1282
1283 xmit_type = qede_xmit_type(skb, &ipv6_ext);
1284
1285 #if ((MAX_SKB_FRAGS + 2) > ETH_TX_MAX_BDS_PER_NON_LSO_PACKET)
1286 if (qede_pkt_req_lin(skb, xmit_type)) {
>1287 if (skb_linearize(skb)) {
>1288 DP_NOTICE(edev,
>1289 "SKB linearization failed - silently dropping this SKB\n");
>1290 dev_kfree_skb_any(skb);
1291 return NETDEV_TX_OK;
1292 }
1293 }
1294 #endif
从qede_start_xmit() 源码可以看到drop发生在skb_linearize() 函数
继续看skb_linearize()函数说明了和内存有关
2925 /**
2926 * skb_linearize - convert paged skb to linear one
2927 * @skb: buffer to linarize
2928 *
2929 * If there is no free memory -ENOMEM is returned, otherwise zero
2930 * is returned and the old skb data released.
2931 */
2932 static inline int skb_linearize(struct sk_buff *skb)
2933 {
2934 return skb_is_nonlinear(skb) ? __skb_linearize(skb) : 0;
2935 }
2920 static inline int __skb_linearize(struct sk_buff *skb)
2921 {
2922 return __pskb_pull_tail(skb, skb->data_len) ? 0 : -ENOMEM;
2923 }
__pskb_pull_tail 说明了skb与buffer reallocate有关,怀疑最小KB数(vm.min_free_kbytes)较低导致用于存储 packet 的内存空间不足,出现丢失SKB情况。
1654 /**
1655 * __pskb_pull_tail - advance tail of skb header
1656 * @skb: buffer to reallocate
1657 * @delta: number of bytes to advance tail
1658 *
1659 * The function makes a sense only on a fragmented &sk_buff,
1660 * it expands header moving its tail forward and copying necessary
1661 * data from fragmented part.
1662 *
1663 * &sk_buff MUST have reference count of 1.
1664 *
1665 * Returns %NULL (and &sk_buff does not change) if pull failed
1666 * or value of new tail of skb in the case of success.
1667 *
1668 * All the pointers pointing into skb header may change and must be
1669 * reloaded after call to this function.
1670 */
1671
1672 /* Moves tail of skb head forward, copying data from fragmented part,
1673 * when it is necessary.
1674 * 1. It may fail due to malloc failure.
1675 * 2. It may change skb pointers.
1676 *
1677 * It is pretty complicated. Luckily, it is called only in exceptional cases.
1678 */
1679 void *__pskb_pull_tail(struct sk_buff *skb, int delta)
三、处理过程
vm.min_free_kbytes参数调优
什么是vm.min_free_kbytes?
官方释义是:这用于强制 Linux VM 保持最小数量的可用千字节。VM 使用这个数字来计算系统中每个 lowmem 区域的 watermark[WMARK_MIN] 值。每个 lowmem 区域都会根据其大小按比例获得一些保留的空闲页面。满足 PF_MEMALLOC 分配需要一些最小的内存量;如果您将其设置为低于 1024KB,您的系统将被巧妙地破坏,并且在高负载下容易死锁。设置得太高会立即 OOM 你的机器。
所以设定这个参数时请小心,因为该值过低和过高都有问题。min_free_kbytes太低可防止系统重新利用内存。这可导致系统挂起并让 OOM 杀死多个进程。但将这个参数值设定太高(占系统总内存的 5-10%)会让您的系统很快会内存不足。Linux 的设计是使用所有可用 RAM 缓存文件系统数据。设定高min_free_kbytes值的结果是在该系统中花费太多时间重新利用内存。
min_free_kbytes这个参数不仅仅是影响Linux内存回收中的water_mark(尤其是direct reclaim回收方式,Linux有两种内存回收方式:一种是kswapd后台回收,在早期的内核版本kswapd是周期性唤醒,因此又叫周期回收,但是没有必要,所以在现代Linux内核版本中已经不再是周期唤醒,而是在分配内存的时候会基于zone的water_mark来唤醒做后台回收内存;另一种就是direct reclaim,这种又叫同步回收,因为此时系统可用内存到了water_mark_min,意味着系统内存非常紧张,所以allocate page申请内存的进程会被阻塞直到回收可用内存。当然,如果经历了内存回收流程仍旧没有回收到足够的内存,那么在allocate page函数中会走out_of_memory函数的oom流程),还会影响系统的可用内存available memory。
设置了/proc/sys/vm/min_free_kbytes之后,通过water_mark_min计算water_mark_low和water_mark_high的默认公式:
watermark[min] = per_zone_min_free_pages (min_free_kbytes换算为page单位)
watermark[low] = watermark[min] * 5 / 4
watermark[high] = watermark[min] * 3 / 2
min 和 low的区别:
1、min下的内存是保留给内核使用的;当到达min,会触发内存的direct reclaim
2、low水位比min高一些,当内存可用量小于low的时候,会触发 kswapd回收内存,当kswapd慢慢的将内存 回收到high水位,就开始继续睡眠
内存回收方式有两种,主要对应low ,min
1、direct reclaim : 触发min水位线时执行
2、kswapd reclaim : 触发low水位线时执行
四、经验总结
如果发现系统因为direct reclaim而导致卡顿、延迟(此时IO相关的指标会比较异常,并且系统负载会增加),那么就需要调高min_free_kbytes,可以通过sar -B命令观测pgscand和%vmeff来慢慢调整。这是为什么呢?因为min=min_free_kbytes,low=1.25min,high=1.5min,到low会唤醒kswapd做内存的后台回收,这时候即便是刷脏页、swap out等虽然会消耗磁盘io性能,但是绝大多数情况下不会影响进程;但是到了min,所有此时申请物理内存的进程都会被阻塞做direct reclaim,直到回收满足申请的内存。如果min_free_kbytes太小,那么就意味着kswapd后台回收启动没多久就进入direct reclaim,如果能把两者的时间间隔拉长,让后台回收有充分的时间来回收内存,那么就会降低direct reclaim的影响。
对于线上128G的内存的机器,可以考虑将min设置为512M左右。因为太大了,可能会导致内存的浪费;如果只有40G的物理机,更不要考虑把min设置超过1G了,这样会导致频繁的触发内存回收;具体优化也要根据业务来看。
关键是在于调整内存的内核参数的时候, 调大的风险远大于调小的风险! 如果有人想将vm.min_free_kbytes 调大,千万要注意当前的水位,如果一旦调大vm.min_free_kbytes 立刻触发direct reclaim,可能会导致机器hang住,ping的通,ssh不上,影响业务。hang住的原因是当vm.min_free_kbytes 是512M的时候, free只有1G,此时正常运行,如果调大vm.min_free_kbytes 到5G,将会direct reclaim失败。