calico IPIP 分析

概述

当集群中所有的主机都在同一个二层时,calico cni 可以仅靠路由,使得所有的 Pod 网络互通。但是纯二层的环境在很多场景下都不一定能满足,因此当主机之间仅3层互通时,就可以使用 calico IPIP(全称 IP in IP) 模式。

IP in IP 是一种 IP 隧道协议,其核心技术点就是发送方将一个 IP 数据包封装到另一个 IP 数据包之中发送,接受方收到后,从外层 IP 数据包中解析出内部的 IP 数据包进行处理。常用在 VPN 等技术中,用来打通两个内网环境。

calico IPIP 流量分析

之前的文章proxy_arp在calico中的妙用简单讲了 calico 是如何通过路由打通不同主机上的 Pod 网络的,其实这个方案有一个前提,就是不同的主机之间需要二层互通。当网络环境满足不了时,就可以通过使用路由 + IPIP 的方式来打通网络。

这里可以通过一个简单的实验来验证一下该方案。

# node.sh
ip netns add n1
ip link add veth1 type veth peer name veth2
ip link set veth2 netns n1
ip netns exec n1 ip link set veth2 up
ip netns exec n1 ip route add 169.254.1.1 dev veth2 scope link
ip netns exec n1 ip route add default via 169.254.1.1
ip netns exec n1 ip addr add 172.19.1.10/24 dev veth2
ip link set veth1 up
ip route add 172.19.1.10 dev veth1 # 这个路由必须有
ip netns exec n1 ip route del 172.19.1.0/24 dev veth2 proto kernel scope link src 172.19.1.10
echo 1 > /proc/sys/net/ipv4/conf/veth1/proxy_arp
echo 1 > /proc/sys/net/ipv4/ip_forward

上面的脚本是用来创建一个虚拟的 Pod 的,可以在不同的主机上执行一下,这里要记得修改一下 IP 地址,来保证两个 Pod 的 IP 不同。

之后在宿主机上创建 IP 隧道。也是两台主机都要执行。

ip tunnel add mode ipip
ip link set tunl0 up
ip route add 172.19.1.0/24 via 192.168.105.135 dev tunl0 proto bird onlink

这里在创建 IP 隧道时,并没有指定隧道对端的地址,因为在实际的集群中,1对1的隧道是没使用场景的。而是使用路由告诉这个隧道的对端地址。这时候在 netns n1 内就可以 ping 通对端的 IP 了。

流程图如下

calico ipip

proxy_arp在calico中的妙用

概述

proxy_arp 是网卡的一个配置,在开启后,该网卡会使用自己的 MAC 地址应答非自身 IP 的 ARP Request。常见的用途就是当两台主机的 IP 在同一个网段内,二层却不通,就可以使用额外的一台主机作为 proxy,将这台主机的网卡开启 proxy_arp,来作为中间代理打通网络。如下图所示:

img

开启网卡的 proxy_arp 也很简单:

echo 1 > /proc/sys/net/ipv4/conf/veth1/proxy_arp

calico 是一个使用路由方案打通网络的网络插件,在作为 k8s cni 时,其也使用了 proxy_arp,作为打通路由的一个环节。在了解 calico 如何使用 proxy_arp 之前,我们先看一下 flannel 的 host-gw 是如何使用路由打通 pod 网络的。

flannel host-gw 路由方案

两台二层互通的主机上的 pod,如果要通过路由来互相访问,常见的方式是类似于 flannel 的 host-gw 模式。其流量路径如下:

  1. 每台主机上都有一个 bridge,pod 通过 veth pair 接入到 bridge 上。
  2. pod 将 bridge 的 ip 作为网关。这样 pod 访问其他网段的 IP 时,流量就会到达 bridge 上。
  3. 流量到达 bridge 后,就可以根据宿主机上的路由表转发到对端主机。
  4. 对端主机也会根据路由表,将流量从 bridge 转发到 pod 内。

flannel-host-gw

calico 的路由方案

相比于 flannel host-gw 模式,calico 采用了更巧妙的方法,省掉了 bridge。

其 veth pair 的一端在 Pod 内,设置为 pod 的 IP,另一端在宿主机中,没有设置 IP,也没有接入 bridge,但是设置了 proxy_arp=1。

pod 内有以下的路由表:

default via 169.254.1.1 dev veth2 
169.254.1.1 dev veth2 scope link 

169.254.0.0/16 是一个特殊的 IP 段,只会在主机内出现。不过这里这个 IP 并不重要,只是为了防止冲突才选择了这个特殊值。当 Pod 要访问其他 IP 时,如果该 IP 在同一个网段,那就需要获取该 IP 的 MAC 地址。如果不在一个网段,那么根据路由表,就要获取网关的 IP 地址。所以无论如何,arp 请求都会到达下图中的 veth1。

因为 veth1 设置了 proxy_arp=1,所以就会返回自己的 MAC 地址,然后 Pod 的流量就发到了主机的网络协议栈。到达网络协议栈之后,就和 flannel host-gw 一样,被转发到对端的主机上。

流量到达对端主机后,和 flannel host-gw 不一样的是,主机上直接设置了 pod 的路由:

172.19.2.10 dev veth1 scope link

也就是直接从 veth1 发到 pod 内。

proxy_arp

参考

2.2. Proxy ARP

戳穿 Calico 的谎言

linux 网络数据包接收流程(一)

概述

Linux 作为最流行的服务器操作系统,其提供的网络能力也是经过了各种各样场景的考验。因此如果经常和 linux server 打交道的话,了解 linux 的数据包处理流程也是很有必要的。

网络数据包的接收处理可以分成两个部分,一是从物理网卡进入到达 linux 内核的网络协议栈,二是经网络协议栈处理后交给上层应用或者转发出去。本篇文档主要说明第一部分,并且不会去深入细节点(因为我也不太熟)。

重要概念和数据结构

在说明网络数据包的处理流程之前,有必要提前讲一下一些相关的概念,因为这些概念决定了后面的内容是否能够理解。

硬中断

硬中断是由硬件在发生某些事件后发出的,称为中断请求(IRQ),CPU 会响应硬中断,并执行对应的 IRQ Handler。对于网卡来说,在有网络流量进入后,网卡会通过硬中断通知 CPU 有网络流量进来了,CPU 会调用对应网卡驱动中的处理函数。

硬中断在处理期间,是屏蔽外部中断的,所以硬中断的处理时间要尽可能的短。

软中断

软中断是由软件执行指令发出的,因为硬中断的特点不能处理耗时的任务,所以软中断往往用来替代硬中断来处理耗时任务。

比如网络流量的处理,网卡在发出硬中断通知 CPU 处理后,这次硬中断的处理方法中又会触发软中断,由软中断接着去处理网络流量数据。

网卡驱动

驱动是打通硬件和操作系统的通道,linux 通过网卡驱动,可以支持不同厂商,不同型号,不同特性的网卡。网卡驱动主要负责将从网卡中进来的流量解析并转换成 sk_buff,交给内核协议栈。

DMA

DMA是一种无需CPU的参与就可以让外设和系统内存之间进行双向数据传输的硬件机制。网卡会通过 DMA 直接将网络流量数据存储到一块提前申请好的内存区域中。

NAPI

全称 New API,因为没有更好的名字,所以就直接用 NAPI 了。这是用于支持高速网卡处理网络数据包的一种机制。非 NAPI 往往是只依靠硬中断的方式让 CPU 来处理数据包,NAPI 引入了硬中断+轮询的方式,有效的缓解了硬中断带来的性能问题。

sk_buff

sk_buff 是一个非常大而通用的 struct,可以用来表示2,3,4层的数据包。它被分成两个部分:head 和 data。

head 部分有单独的字段表示不同层的网络头:

  • transport_header:用来表示传输层(4层)的 header,包括 tcp, udp, icmp 等协议头
  • network_header:用来表示网络层(3层)的 header,包括 ip, ipv6, arp 等协议头
  • mac_header:用来表示链路层(2层)的 header。

当数据包进入网络协议栈之前,需要先被转换成 sk_buff。

流程梳理

数据包进入触发硬中断

流量进入到硬件中断

  1. 数据包进入网卡设备

  2. 网卡设备通过 DMA 直接写入的内存中。如果写不下就直接 drop 掉

  3. 网卡产生硬中断

  4. CPU 收到硬中断后,会直接提前注册好的该硬中断的 handler。这个 handler 是写在网卡驱动中的一个方法

  5. IRQ handler 禁用网卡的 IRQ。这是后面处理内存中的数据包是采用的 poll 模式。也就是说 cpu 会自己去内存中轮询数据包,直到一定时间/数量,或者全部处理完之后。这段时间内就不需要网卡通过硬中断来通知 CPU 了,并且硬中断会打断 CPU 的工作,带来一定的性能问题。

  6. 网卡驱动产生软中断。

软中断触发数据包的处理

软中断触发数据包的处理

这里为了方便表述,使用目前最常用的 NAPI 的处理流程进行说明。

  1. 在系统启动时,net_dev_init 方法中注册了 NET_RX_SOFTIRQ 对应的 handler 是 net_rx_action。上面触发软中断的方式是 __raise_softirq_irqoff(NET_RX_SOFTIRQ)。所以开始执行 net_rx_action
  2. net_rx_action 会从 poll_list 链表中获取第一个 poll,使用 napi_poll 轮询内存中的数据包。napi_poll 调用到网卡驱动提供的 poll 方法
  3. poll 方法中从内存中取出数据包
  4. 网卡驱动调用 napi_gro_receive 来处理数据包
  5. napi gro 会合并多个 skb 数据包,比如一个 IP 包会被分成多个 frame 这种。那么如果在接收的时候,在到达协议栈之前直接合并,会有一定的性能提升。这里最终会调用到 gro_normal_list 来批量处理 skb。
  6. 最终调用到 netif_receive_skb_list_internal,从 napi.rx_list 上处理 sk_buff 链表。
  7. 如果开启了 RPS,会根据 skb 的 hash 值找到对应的 cpu,将 skb 存储到该 cpu 上的 backlog 队列。backlog 队列是一种用软件方式将数据包处理负载均衡到多个 cpu 上的一种方法。
  8. 最终都会调用到 __netif_receive_skb_core。
  9. 如果有 AF_PACKET 的 socket,还会拷贝一份给它(tcpdump 的实现原理)。
  10. 最后递交给内核协议栈

参考

Linux协议栈–NAPI机制

Monitoring and Tuning the Linux Networking Stack: Receiving Data

linux kernel 网络协议栈之GRO(Generic receive offload)

kube-proxy iptables 流量处理流程

kube-proxy 在 iptables 模式下,主要是通过使用 iptables 提供从 service 到 pod 的访问。主要作用在两个表上:

  • NAT:访问 service 时,需要 DNAT 到 pod IP 上
  • Filter: 对流量做过滤,比如如果一个 service 没有 endpoints,就直接 REJECT 掉访问 cluster IP 的流量等。

NAT 主要作用在三个关键点:

  • PREROUTING: 在这里为进入 node 流量进行处理,如果是访问 service,则选择一个后端 pod DNAT,并在流量上做标记
  • OUTPUT: 在这里为从本机进程出来的流量进行处理,如果是访问 service,则选择一个后端 pod DNAT,并在流量上做标记。
  • POSTROUTING: 为做了标记的流量做 MASQUERADE,MASQUIERADE 可以理解为加强版的 SNAT,会自动根据出去的网卡选择 src IP。

Filter 主要作用在三个点:

  • INPUT: 发往本机的流量
  • FORWARD: 转发到其他 host 的流量
  • OUTPUT: 从本机进程出去的流量

分析 kube-proxy iptables 时,主要就是从上述的几个点去看,iptables 规则本身比较枯燥,没有太多可说的。下面是整理的 kube-proxy 使用 iptables 的流量处理流程。可以用来作参考。

kube-proxy-iptables