为什么应用容器化后数据包重传率会变高?

1. 前言

在上一篇应用容器化后为什么性能下降这么多? 中分析了应用容器化后性能大幅下降的原因,容器和宿主机使用veth pair通信不仅造成性能的下降,同时会导致网络包重传率增大,这又是为什么呢?带着这个疑问,开始下面的分析。

2. 压测结果

为了了解应用容器化以后各个环节的网络性能,我们对容器到宿主机的网络进行了压测,发现容器到宿主机的网络重传率相对较高,使用iperf3压测结果如下图所示。

其中Retr表示数据包重传次数,为695次,造成重传的原因可能是丢包也可能是包乱序导致的重传,接下来我们一步步来排查具体原因。

3. 问题定位

3.1 netstat分析

通过netstat可以看到和重传相关的fast retransmits在快速变化,如下图所示

由于fast retransmits在快速变化,有理由怀疑是快速重传导致的,快速重传是针对RTO超时重传的优化,当收到三个重复的ack后可以触发快速重传,优化网络性能,而当开启sack以后,快速重传的条件又发生的变化,具体详见下文分析。

为了进一步验证,所以将测试前后的数量进行结果比较,结果如下所示。

我们惊人的发现762388-761693 = 695,和压测结果的重传数据包数结果完全一致,所以我们怀疑容器到宿主机的过程中发生的重传,都是快速重传导致的,所以问题转化为是什么导致了快速重传?

3.2 调用栈分析

要想分析重传的原因,很自然会从重传函数tcp_retransmit_skb()入手,我们来看下调用栈,如下图所示。

调用核心链路为tcp_ack -> tcp_xmit_recovery -> tcp_xmit_retransmit_queue -> tcp_retransmit_skb,知道调用链路后,还需要根据调用链路,逐个分析内部的细节,tcp_retransmit_skb主要作用为重传数据包,tcp_xmit_retransmit_queue主要作用为遍历队列,随后调用tcp_retransmit_skb逐个发送需要重传的数据包,先从tcp_xmit_recovery入手分析

3.3 tcp_xmit_recovery分析

scss 复制代码
/* Congestion control has updated the cwnd already. So if we're in
 * loss recovery then now we do any new sends (for FRTO) or
 * retransmits (for CA_Loss or CA_recovery) that make sense.
 */
static void tcp_xmit_recovery(struct sock *sk, int rexmit)
{
	struct tcp_sock *tp = tcp_sk(sk);

  //判断rexmit的状态 是否满足 rexmit的状态决定了是否重传
	if (rexmit == REXMIT_NONE || sk->sk_state == TCP_SYN_SENT)
		return;

	if (unlikely(rexmit == REXMIT_NEW)) {
		__tcp_push_pending_frames(sk, tcp_current_mss(sk),
					  TCP_NAGLE_OFF);
		if (after(tp->snd_nxt, tp->high_seq))
			return;
		tp->frto = 0;
	}
	tcp_xmit_retransmit_queue(sk);
}

从tcp_xmit_recovery可以看到rexmit的状态,决定了是否调用tcp_xmit_retransmit_queue从而发起数据包重传,而rexmit是从参数直接传过来的,再来看tcp_ack函数,查看rexmit的状态是如何改变和传入的。

3.4 tcp_ack分析

scss 复制代码
/* This routine deals with incoming acks, but not outgoing ones. */
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
//初始化rexmit状态
int rexmit = REXMIT_NONE; /* Flag to (re)transmit to recover losses */
...
if (tcp_ack_is_dubious(sk, flag)) {
		if (!(flag & (FLAG_SND_UNA_ADVANCED |
			      FLAG_NOT_DUP | FLAG_DSACKING_ACK))) {
			num_dupack = 1;
			/* Consider if pure acks were aggregated in tcp_add_backlog() */
			if (!(flag & FLAG_DATA))
				num_dupack = max_t(u16, 1, skb_shinfo(skb)->gso_segs);
		}
         //拥塞状态机变更,变更rexmit状态和标记需要重传的数据包
		tcp_fastretrans_alert(sk, prior_snd_una, num_dupack, &flag,
				      &rexmit);
	}
...
	tcp_xmit_recovery(sk, rexmit);
...
}

在tcp_ack中,rexmit最初的状态为默认状态REXMIT_NONE,改变其状态的函数为tcp_fastretrans_alert,由于tcp_fatstretrans_alert非常复杂,我们分析相关的最核心部分

3.5 tcp_fatstretrans_alert 分析

arduino 复制代码
/* Process an event, which can update packets-in-flight not trivially.
 * Main goal of this function is to calculate new estimate for left_out,
 * taking into account both packets sitting in receiver's buffer and
 * packets lost by network.
 *
 * Besides that it updates the congestion state when packet loss or ECN
 * is detected. But it does not reduce the cwnd, it is done by the
 * congestion control later.
 *
 * It does _not_ decide what to send, it is made in function
 * tcp_xmit_retransmit_queue().
 */
static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
				  int num_dupack, int *ack_flag, int *rexmit)
{
  ...
  //数据包是否丢失状态
	bool do_lost = num_dupack || ((flag & FLAG_DATA_SACKED) &&
				      tcp_force_fast_retransmit(sk));
  ...

	if (!tcp_is_rack(sk) && do_lost)
		tcp_update_scoreboard(sk, fast_rexmit);//标记数据包状态为TCPCB_LOST,真正发送在后续tcp_xmit_retransmit_queue

	*rexmit = REXMIT_LOST;
}

可以看到tcp_fastretrans_alert最后将rexmit设置为REXMIT_LOST,当do_lost为true时,调用tcp_update_scoreboard,而该函数的目的就是将数据包状态标记为TCPCB_LOST,以便在tcp_xmit_retransmit_queue中遍历并重传状态为TCPCB_LOST的数据包。

rexmit 决定了是否调用tcp_xmit_retransmit_queue,do_lost决定了数据包是否LOSS,从而决定在tcp_xmit_retransmit_queue函数中,是否发送该数据包,所以只要搞清楚do_lost什么时候为true,也许就能找到重传的原因了,经过分析do_lost的值由tcp_force_fast_retransmit决定。

3.6 tcp_force_fast_retransmit 分析

rust 复制代码
static bool tcp_force_fast_retransmit(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);

	return after(tcp_highest_sack_seq(tp),
		     tp->snd_una + tp->reordering * tp->mss_cache);
}

tcp_force_fast_retransmit大致的意思是发送端已经ack的数据包和未ack的数据之间差值过大的话,就可以马上重传数据包,换句话说tp->reordering * tp->mss_cache影响了是否需要重传,如下图所示。

结论:容器和宿主机之间的网络丢包的概率不大,所以造成快速重传的原因大概率是由于包乱序导致的,通过调整tp->reordering的值可能可以减缓包乱序发生的概率。

4. 优化方案

经过上面的分析造成重传的原因大概率是是由于包乱序导致的,而tp->reordering表示乱序度,由/proc/sys/net/ipv4/tcp_reordering参数决定,默认值为3,将/proc/sys/net/ipv4/tcp_reordering的值调整为9,看看结果会如何?

通过测试,调整/proc/sys/net/ipv4/tcp_reordering似乎降低了一些重传数据包数,从695降低为149,到这里相信还有很多疑问。

  • 疑问1:虽然重传数据包数降低了,为啥还有149?
  • 疑问2:容器和宿主机之间的网络为什么会造成包乱序?

带着这些疑问,继续下面的分析。

5.包乱序原因分析

5.1 容器向宿主机发送数据

我们知道容器使用veth接口向宿主机发送数据,并使用软中断的触发对端接受数据,而在软中断之前,需要将数据包放到cpu的数据包队列poll_list中,对应的结构体为softnet_data。

scss 复制代码
static int netif_rx_internal(struct sk_buff *skb)
{
	int ret;

	net_timestamp_check(netdev_tstamp_prequeue, skb);

	trace_netif_rx(skb);

#ifdef CONFIG_RPS
	if (static_branch_unlikely(&rps_needed)) {
		struct rps_dev_flow voidflow, *rflow = &voidflow;
		int cpu;

		preempt_disable();
		rcu_read_lock();
               
		cpu = get_rps_cpu(skb->dev, skb, &rflow);
		if (cpu < 0)
			cpu = smp_processor_id();
                 //将数据包通过PRS的方式,放入指定cpu的队列中
		ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);

		rcu_read_unlock();
		preempt_enable();
	} else
#endif
	{
		unsigned int qtail;
                //将数据包放入当前cpu的队列中
		ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
		put_cpu();
	}
	return ret;
}

从上面代码可以看出,当没有配置RPS时,数据包选择放入的cpu是通过get_cpu()决定的,也就是当前cpu,而在多核的系统上,容器向宿主机发送数据的cpu可能会切换,这就导致数据可能随机放入各个cpu数据包队列中,而在后续触发软中断处理数据包的过程是异步的,这就可能导致最终包乱序。

5.2 RPS

RPS全称为(Receive Packet Steering),它可以在逻辑上通过hash(4元组),在软件层面实现数据包的重定向,从而实现和RSS一样的效果,具体RPS和RSS区别可以查看内核协议栈扩展

通过RPS可以保障同一个数据流都在同一个cpu上处理,从而减少乱序的可能性。

RPS的配置方式可以通过修改 /sys/class/net//queues/rx-/rps_cpus的值来实现。例如我测试机器的cpu核心数为2,使用calico为容器网络通信方案,配置如下:

bash 复制代码
echo 3 > /sys/devices/virtual/net/cali9deae4f562c/queues/rx-0/rps_cpus

在配置了RPS以后,我们再来做一轮benchmark,看看这次的结果,如下图所示:

这次结果没有令人失望,重传数量降为了0,问题终于得到解决。

6. 总结

通过在容器网络中发现重传率过高,一步步从netstat统计信息分析,到内核调用栈分析,再到原因的分析以及方案的优化,整个问题排查的链路非常长,非常耗时,这也是企业云原生过程中遇到的一大痛点,所以往往需要提供内核可观测性工具以及不断的踩坑积累,帮忙我们快速分析和定位问题。

最后,感谢大家观看,也希望和我讨论云原生过程中遇到的问题。

7. 参考资料

www.kernel.org/doc/Documen...

相关推荐
是芽芽哩!1 小时前
【Kubernetes 指南】基础入门——Kubernetes 基本概念(二)
云原生·容器·kubernetes
m0_663234012 小时前
云原生是什么
云原生
AI人H哥会Java2 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱2 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-2 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu
中國移动丶移不动3 小时前
Java 并发编程:原子类(Atomic Classes)核心技术的深度解析
java·后端
运维小文4 小时前
K8S中的服务质量QOS
云原生·容器·kubernetes
华为云开发者联盟4 小时前
Karmada v1.12 版本发布!单集群应用迁移可维护性增强
云原生·kubernetes·开源·容器编排·karmada
Hadoop_Liang4 小时前
Kubernetes Secret的创建与使用
云原生·容器·kubernetes
元气满满的热码式4 小时前
K8S集群部署实战(超详细)
云原生·容器·kubernetes