随着kubernetes逐渐成为应用部署的事实标准,开发运维人员可以更便捷的部署、拓展应用程序,同时也带来了一系列的云上疑难杂症。其中最莫过于头疼的无非是网络问题。由于传统的虚拟机应用使用内核网络比较清晰,内核网络故障概率也比较少,问题往往出现在物理网络,即使问题出现在内核网络,也相对比较容易定界。而容器使用内核网络的方式出现了颠覆性的变化,比如使用了更多的二层、三层转发、不同namespace的通信、各种路由防火墙策略。在如此复杂的环境下,可能某一条网络规则的配置错误或者某个虚拟网络设备的状态不正常都会导致网络故障。
在如此复杂的网络场景下,我们的SRE或者研发工程师很难全局把控容器网络,可以说整个内核网络是一个黑盒。所以,面对这一系列的困境,kindling正在逐步尝试将内核网络白盒化以及帮助工程师确定是容器网络问题还是物理网络问题
基于以上问题,假如告诉用户数据包在哪个网络设备之后就“断链”是不是就能缩小问题的排查范围了呢?下图是flannel网络的vxlan模式通信图。
vxlan模式通信图
当podA发起一次调用podB的请求时,中间会经过veth设备、cni网桥、vxlan设备、物理网卡。
对于该次请求而言,如果数据包已经从node1的ens192网卡上发送出去了,而node2并没有收到该次请求,则说明物理网络出现了问题。
同理,对于该次请求而言,在cni0上和flannel.1之间出现了“断链”,则说明cni0和flannel.1之间的通信出现了问题。
接着,用户工程师可以在这个范围内去排查具体问题,比如:
物理网络问题,就看物理设备状态等。
cni0和flannel.1的通信问题则查看路由规则,防火墙策略等。
在这个基础上,用户工程师在排查类似网络问题时,不至于像无头苍蝇一样不知道如何下手,明确了在哪一阶段出现了问题
为此,笔者动手使用eBPF去hook了一系列网络路径相关的函数,并且构造了一个cni0和flannel.1通信故障的场景。
使用以下命令开启阻断flannel.1设备接收数据包:
tc qdisc add dev flannel.1 root netem loss 100%
于是,笔者看到了大量网络数据包形成了“断链”,如图所示,该skb在经过cni0之后并没有如预想的那样到达flannel.1设备:
我们再来看看正常的网络数据包路径,使用以下命令关闭阻断:
tc qdisc del dev flannel.1 root netem loss 100%
很明显,如下图所示,数据包传输正常了:
由于数据包在内核中都会以sk_buff数据结构传递,所以我们只需要追踪容器网络中skb会触达的关键函数。由于不同的CNI会对容器发送的原始数据包进行不同的处理,比如flannel的vxlan模式会在容器以太网帧外增加vxlan头和一些主机的ip信息,所以skb的包头往往不能作为串联起这一串函数的key。我们选择skb的内存地址作为key,但是内存地址也会带来很多问题,比如skb的fclone机制和复用机制会影响key作为唯一值的判断。
核心点位:net_dev_start_xmit(网卡发包)、netif_receive_skb(网卡收包)
辅助判断点位:__kfree_skb、net_dev_alloc_skb
为什么选择这些点位,选择的标准是什么?
靠近网卡或者网络设备收发包,用于判断skb是否到达网络设备,net_dev_start_xmit和netif_receive_skb分别靠近网络设备收和发包,所以选择这两个点位作为主要数据来源
除了主要数据的来源,我们还需要一些点位来知道skb一次使命的完成(笼统点来说就是一个包处理完成)。而完成的标志往往是这个skb代表的内存地址被释放,重新申请或者复用,所以我们有了以下很多选择来组合使用:
这些hook点被调用都非常频繁,为了尽量少的去hook,我们选择了kfree_skb+net_dev_alloc_skb,选择kfree_skb是因为释放的时候作为终点是最及时的,选择net_dev_alloc_skb是因为他不单单使用__alloc_skb来申请内存,而且有复用的逻辑,所以也必须解决这个场景,请看下列代码:
struct sk_buff *__netdev_alloc_skb(struct net_device *dev,
unsigned int length, gfp_t gfp_mask)
{
struct sk_buff *skb = NULL;
unsigned int fragsz = SKB_DATA_ALIGN(length + NET_SKB_PAD) +
SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
if (fragsz <= PAGE_SIZE && !(gfp_mask & (__GFP_WAIT | GFP_DMA))) {
void *data;
if (sk_memalloc_socks())
gfp_mask |= __GFP_MEMALLOC;
data = __netdev_alloc_frag(fragsz, gfp_mask);
if (likely(data)) {
// 复用逻辑
skb = build_skb(data, fragsz);
if (unlikely(!skb))
put_page(virt_to_head_page(data));
}
} else {
// else选择了 __alloc_skb
skb = __alloc_skb(length + NET_SKB_PAD, gfp_mask,
SKB_ALLOC_RX, NUMA_NO_NODE);
}
if (likely(skb)) {
skb_reserve(skb, NET_SKB_PAD);
skb->dev = dev;
}
return skb;
}
另外,fclone是较新版 kernel 中添加的一个特性,以前版本的 kernel 中 skb 在分配的时候都是 从后备高速缓存(lookaside cache)skbuff_head_cache 中获取 sk_buff 的结构;而现在可以 在调用 alloc_skb(),通过 fclone 参数选择从 skbuff_head_cache 或者 skbuff_fclone_cache 中分配。两者的区别在于 skbuff_head_cache 在创建时指定的单位内 存区域的大小是 sizeof(structsk_buff)而 skbuff_fclone_cache 在创建时指定的单位内存区域大小是 2*sizeof(struct sk_buff)。所以,一个内存地址可能完成两次使命,如果单单为了解决这个问题而去hook __alloc_skb 那无疑是不值得的,所以我们选择从skb的数据结构中或者fclone的值,来判断是否进入了该逻辑。
于是,我们针对这些hook点做了如下的数据分析:
为了判断skb最终有没有从物理网卡出去,我们需要智能或者手动配置物理网卡的名称或者mac地址,这里智能识别选择了收发包最多的网卡作为物理网卡,这个逻辑在大部分场景下是没有问题的。为了防止意外情况,也提供了手动配置的方式
对于同主机pod的调用,用路由来识别,发现路由指向本机,则过滤掉这些数据,因为这些数据会影响“断链”的判断
为了性能考虑,只传递出起始点到终点时间比较长的数据&&最终目的地不是容器网卡或者物理网卡的数据
字段 |
描述 |
类型 |
tuple |
eth0网卡抓到的四元组 |
tuple |
devs[] |
网络设备序列 |
char[] |
duration |
起始点到终点的时间段,比如容器网卡到物理网卡的时间 |
u32 |
可以看出,由于网络函数被触发频率比较高,所以在5000左右的tps下还是给用户程序的cpu造成了13%的额外损耗。
Kindling是一款基于eBPF的云原生可观测性开源工具,旨在帮助用户更好更快的定界云原生系统问题,并致力于打造云原生全故障域的定界能力。
Kindling项目地址:Kindling
在云可观测性方面有任何疑问欢迎与我们联系:Kindling官网