Fullnat系列(三):为什么经过fullnat了以后,select查询变慢了呢?
by 伊布
问题现象是这样的:我们在客户的环境上,通过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的报文。
Subscribe via RSS