问题现象是这样的:我们在客户的环境上,通过LVS设备访问ADS的时候,发现在对一个宽表select * from查询时,响应的时间很规律的发生的倍增:

1 row in set (0.22 sec)
1 row in set (0.42 sec)
1 row in set (0.83 sec)
1 row in set (1.62 sec)
1 row in set (3.23 sec)
...

如果断开重连,又从0.22重新开始倍增。 一开始我们认为是后端ADS的处理有问题,但绕过LVS直接访问ADS的时候,响应时间是不变的,那么显然问题出在LVS上。内核LVS的调试信息比较少,在LVS机器的ADS侧抓包,我们看到了一个奇怪的现象:

抓包

看上去是LVS机器收到了一个1913的包,处理不了,所以回了ICMP差错报文,告诉ADS端要分片Fragment;ADS端收到差错报文后重传,由于是同一条连接,所以每次收到ICMP报文后,重传定时器都进行了指数退避,现象上来看就是select的时间发生了倍增。 这里有3个问题:

问题1:为什么会有超过1500的大包?

之前在交换机上,报文发送时会查看mtu,如果超出,总是会分片;但在服务器上,如果有的网卡支持TSO/GSO/GRO,那么发送的报文大小会超过mtu,也就是上面我们看到的1912这种报文。下面是从 Chenny的部落格抄过来的:

GSO(generic-segmentation-offload)/ TSO(TCP-segmentation-offload) 所谓的GSO,实际上是对TSO的增强。TSO将tcp协议的一些处理下放到网卡完成以减轻协议栈处理占用CPU的负载。通常以太网的MTU是1500Bytes,除去IP头(标准情况下20Bytes)、TCP头(标准情况下20Bytes),TCP的MSS (Max Segment Size)大小是1460Bytes。当应用层下发的数据超过了mss时,协议栈会对这样的payload进行分片,保证生成的报文长度不超过MTU的大小。但是对于支持TSO/GSO的网卡而言,就没这个必要了,可以把最多64K大小的payload直接往下传给协议栈,此时IP层也不会进行分片,一直会传给网卡驱动,支持TSO/GSO的网卡会自己生成TCP/IP包头和帧头,这样可以offload很多协议栈上的内存操作,checksum计算等原本靠CPU来做的工作都移给了网卡。

GRO(generic-receive-offload)/ LRO(large-receive-offload) LRO通过将接收到的多个TCP数据聚合成一个大的数据包,然后传递给网络协议栈处理,以减少上层协议栈处理 开销,提高系统接收TCP数据包的能力。 而GRO的基本思想跟LRO类似,克服了LRO的一些缺点,更通用。后续的驱动都使用GRO的接口,而不是LRO。

问题1的解决方法很简单,将涉及到的硬件关闭TSO/GSO/GRO/LRO即可。

$ ethtool -K etho tso off
$ ethtool -K etho gso off
$ ethtool -K etho gro off
$ ethtool -K etho lro off
$ ethtool  -k eth0
Features for eth0:
rx-checksumming: on
tx-checksumming: on
scatter-gather: on
tcp-segmentation-offload: off
udp-fragmentation-offload: off
generic-segmentation-offload: off
generic-receive-offload: off
large-receive-offload: off
rx-vlan-offload: on
tx-vlan-offload: on
ntuple-filters: on
receive-hashing: on

不过这样重启后就失效了,还是需要写到配置文件里去。

todo:还不知道怎么在ifcfg-ethx里记录,所以我把上面几条命令写到/etc/rc.local里了。

问题2:为什么超过mtu的报文,LVS返回了ICMP差错报文?

额。没有为什么,支持Fullnat以后,内核对于超过mtu的报文会直接丢弃并返回ICMP差错报文,具体查看patch里代码:

+/* Response transmit icmp to client
+ * Used for NAT / local client / FULLNAT.
+ */
+int
+ip_vs_fnat_response_icmp_xmit(struct sk_buff *skb, struct ip_vs_protocol *pp,
+			      struct ip_vs_conn *cp, int offset)
+{
+	struct rtable *rt;	/* Route to the other host */
+	int mtu;
+	struct iphdr *iph = ip_hdr(skb);
+
+	/* lookup route table */
+	if (!(rt = ip_vs_get_rt(&cp->caddr, RT_TOS(iph->tos))))
+		goto tx_error_icmp;
+
+	/* MTU checking */
+	mtu = dst_mtu(&rt->u.dst);
+	if ((skb->len > mtu) && (iph->frag_off & htons(IP_DF))) {
+		ip_rt_put(rt);
+		IP_VS_DBG_RL_PKT(0, pp, skb, 0,
+				 "fnat_response_icmp(): frag needed for");
+		goto tx_error;
+	}

吐槽一下,内核真的没法accept这样的patch:

  • 收到目的地址是virtual service的报文,不管报文端口号跟virtual service的端口对不对的上,全部做转发
  • 超过mtu的报文直接drop

问题3:为什么我们实验室的环境并没有出现这个问题?

这个问题困扰了我好几天。一开始是怀疑客户现场对LVS的ADS侧配置了聚合口,但真的不科学。后来看了下实验室环境的LVS虚拟机里的ethtool,才恍然大悟。

原因说起来也很简单,我们的LVS是个虚拟机,其网卡是qemu-kvm虚拟的。实验室环境网卡的Device model是Hypervisor default,在qemu-kvm环境里实际是老旧的RTL8139网卡,它不支持TSO等特性;而在客户现场的Device model是virtio,它支持TSO等,所以LVS在内核里会收到超过mtu的报文。