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

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...

相关推荐
张青贤2 小时前
K8s中的containerPort与port、targetPort、nodePort的关系:
云原生·容器·kubernetes
懵逼的小黑子5 小时前
Django 项目的 models 目录中,__init__.py 文件的作用
后端·python·django
小林学习编程7 小时前
SpringBoot校园失物招领信息平台
java·spring boot·后端
java1234_小锋8 小时前
Spring Bean有哪几种配置方式?
java·后端·spring
zhojiew9 小时前
istio in action之服务网格和istio组件
云原生·istio
柯南二号10 小时前
【后端】SpringBoot用CORS解决无法跨域访问的问题
java·spring boot·后端
每天一个秃顶小技巧10 小时前
02.Golang 切片(slice)源码分析(一、定义与基础操作实现)
开发语言·后端·python·golang
gCode Teacher 格码致知11 小时前
《Asp.net Mvc 网站开发》复习试题
后端·asp.net·mvc
代码的奴隶(艾伦·耶格尔)11 小时前
微服务!!
微服务·云原生·架构
Moshow郑锴13 小时前
Spring Boot 3 + Undertow 服务器优化配置
服务器·spring boot·后端