Kubernets(k8s) 网络原理一:Pod与宿主机通信

对于刚接触K8S的同学来说,K8S网络显得尤为复杂,例如Pod如何访问主机以及pod间如何进行通信等。本系列文章将站在一个初学者角度,逐层刨析Kubernetes网络实现原理,并利用基本的Linux命令加以实现。

网络虚拟化基石:network namespace

network namespace 在Linux内核2.6版本引入,作用是隔离Linux系统设备,使得它们有独自的协议栈信息,一个直观的例子就是:每个容器都可以有自己的虚拟网络设备,并且容器内进程可以放心的绑定在端口上而不担心冲突。

和其他namespace一样, network namespace也可以通过系统调用来创建,同时也可以助 ip 命令来完成各种操作。ip 命令来自于 iproute2 安装包。

ip命令管理的功能很多,操作network namespce的命令为:ip netns,可以使用ip netns help获取帮助。下面先介绍几条间的的network namespace管理命令。

创建一个名为ns1的network namespace可以使用以下命令:

# ip netns add ns1

当创建出一个network namespace空间后,Linux内核将该namespace挂载至/var/run/netns路径下,此时可以使用ip netns exec 进入该namespae,执行网络查询或者配置工作。

# ip netns exec ns1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

可以看到,一个新的network namespace 创建出来之后,其网卡信息中只包含了一块本地回环设备loopback,除此之外,防火墙规则、路由规则等也是一片空白。

想在主机上查看存在的network namespace,可以使用以下命令:

# ip netns list

想删除network namespace 可以通过以下命令实现

# ip netns delete ns1

虚拟设备桥梁:veth pair

veth pair是一种Linux内核技术,可用于连接两个虚拟网络接口,这两个虚拟网络接口总是成对出现,其工作原理就是向veth pair的一端发送的数据,数据经过协议栈后从另外一端出来。

正因为有一个特性,它常常充当着一个桥梁,连接着各种虚拟设备,典型例子就是:使用veth pair连接两个network namespace。

下面命令演示了如何创建一个veth pair:

# ip link add veth0 type veth peer name veth1
46: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 22:18:89:26:fd:63 brd ff:ff:ff:ff:ff:ff
47: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether f6:ca:0d:bf:85:34 brd ff:ff:ff:ff:ff:ff

Pod和宿主机通信

接下来,我们使用network namespace和veth pair技术来模拟Pod和主机通信

使用过kubernets的同学应该知道,当我们创建一个Pod之后,CNI Plugin将会这个Pod下的所有containers分配一个network namespace。同时如果使用calico等CNI Plugin时,还能观察到Pod所在宿主机上会多出一些虚拟网卡,这些虚拟网卡一端连接Pod所在network namespace,一端连接在宿主机上。

所谓Pod,从网络的角度来看,就是共享一个ns的多个容器,这些容器在网络上与外界完全隔离,它们既访问不了外面,外面也访问不了它们。要向他们互相通信,就需要kubernetes cni来完成这项工作,下面我们就用Linux命令来完成cni所做的事。

首先,我们创建一个名为pod-1的network namespace

# ip netns add pod-1

我们用这个pod-1代表pod所在namespace,进入pod-1查看网络设备信息:除了loopback设备外一片空白。

# ip netns exec ns1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

现在,我们创建一对 veth pair

# ip link add eth0 type veth peer name cali-001

将veth pair中的一块虚拟设备添加进pod-1

# ip link set eth0 netns pod-1

并且为eth0配置ip address,ip地址可以随意分配。

# ip netns exec pod-1 ip addr add 10.1.10.10 dev eth0

启动两块虚拟网络设备

# ip netns exec pod-1 ip link set dev eth0 up
# ip link set dev cali-001 up

进入pod-1,尝试ping一下host,这里我的host ip为:192.168.11.126

# ip netns exec pod-1 ping -c 1 192.168.11.126
connect: Network is unreachable

为什么会出现网络不可达呢,上文不是说veth pair的一端发送数据,会从另外一端出来吗?是上面说错了吗?其实不然,我们回想一下Linux基础网络知识:

当Linux需要向外发送一个数据包时,总是执行以下步骤:

  1. 查找该数据包目的地的路由信息,如果是直连路由,则在邻居表插在该目的地的MAC地址。
  2. 如果非直连路由,则在邻居表查找下一跳的MAC地址。
  3. 如果找不到对应路由信息,就报告”network is unreachable“。
  4. 如果邻居表没有相应MAC信息,则向外发送ARP请求询问。
  5. 找到MAC地址后,数据帧源MAC地址为发送网卡MAC地址,目标MAC则为下一跳MAC地址。

什么是直连路由和非直连路由

而这里,我们ping host,则会发出ICMP报文,因为没有路由,所以返回了network is unreachable

我们查看以下pod-1的路由信息

# ip netns exec pod-1 route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface

发现路由表为空,上文我们说过,当一个network namespce创建出来之后,其路由表为空。既然这样,我们添加路由信息

# ip netns exec pod-1 ip route add 169.254.1.1 dev eth0
# ip netns exec pod-1 ip route add default via 169.254.1.1 dev eth0

169.254.1.1是CNI插件calico的默认的网关,后面系列文章会讲到

再次查看路由信息

# ip netns exec pod-1 ip route
default via 169.254.1.1 dev eth0 
169.254.1.1 dev eth0 scope link 

此时,我们看到路由表中多了一条非直连路由,意思是默认流量走eth0网卡,下一跳为169.254.1.1

然后我们再次尝试我们ping host

# ip netns exec pod-1 ping -c 1 192.168.11.126
PING 192.168.11.126 (192.168.11.126) 56(84) bytes of data.

--- 192.168.11.126 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

还是不行,按照刚才所说的Linux网络知识第2点,我们查看一下邻居表

# ip netns exec pod-1 ip neigh
169.254.1.1 dev eth0  FAILED

这里可以看到,eth0为FAILED,意味着获取不到网关的MAC地址,即整个网络中没有一张网卡是这个地址,我们可以抓包证明

# ip netns exec pod-1 tcpdump -n 
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
07:57:00.780329 ARP, Request who-has 169.254.1.1 tell 10.1.10.10, length 28

可以看到,我们在pod-1 ping host的时候,eth0发起ARP广播,请求169.254.1.1的MAC地址,但是没有任何响应。

再次回到Linux网络知识第4点,当前数据帧源地址为eth0的MAC地址,通过邻居表知道,下一跳是169.254.1.1。这时候如果能给169.254.1.1一个MAC地址,则数据帧就满足发送要求了,那把谁的地址给他呢?

答案是:veth pair的另外一端,即主机上的cali-001

那如何将cali-001的MAC地址给到169.254.1.1呢?

答案是:通过ARP欺骗,让cali-001作为代理ARP的角色,收到eth0的ARP请求时,代为应答,告诉eth0自己的MAC地址,而eth0收到响应,则认为是169.254.1.1的MAC地址,然后写入邻居表

可以通过以下命令设置网络设备开启代理APR

 # echo 1 > /proc/sys/net/ipv4/conf/cali-001/proxy_arp

同时打开转发功能

# echo 1 > /proc/sys/net/ipv4/ip_forward

我们再次尝试ping host

# ip netns exec pod-1 ping -c 1 192.168.11.126
PING 192.168.11.126 (192.168.11.126) 56(84) bytes of data.

--- 192.168.11.126 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

仍然访问不通,如果这时候抓包,你会发现,发送的arp包仍然没有响应。同时邻居表中也没有写入cali-001的MAC地址,实验环境为centos7,不排除其他OS可以。

这时你需要执行以下命令,关闭反向校验

# echo 0  >  /proc/sys/net/ipv4/conf/cali-001/rp_filter

# echo 0  >  /proc/sys/net/ipv4/conf/all/rp_filter

对于某个网卡而言,实际生效的值为 相应网卡的值与 all 值两者中的最大值

这时候,你再次ping host,可以通吗?

结果是仍然不同,但幸运的是,这时候你如果查看pod-1的邻居表,会发现169.254.1.1有MAC地址了。

# ip netns exec pod-1 ip neigh
169.254.1.1 dev eth0 lladdr aa:bc:80:1d:6d:29 REACHABLE


# ip addr
52: cali-001@if53: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether aa:bc:80:1d:6d:29 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::a8bc:80ff:fe1d:6d29/64 scope link 
       valid_lft forever preferred_lft forever

可以看到169.254.1.1的MAC地址就等于cali-001的MAC地址,我们的代理arp生效了!!!

我们在主机上抓包cali-001看一下

# tcpdump -pne -i cali-001
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on cali-001, link-type EN10MB (Ethernet), capture size 262144 bytes
11:20:40.322404 a2:37:06:54:4f:f6 > aa:bc:80:1d:6d:29, ethertype IPv4 (0x0800), length 98: 10.1.10.10 > 192.168.11.126: ICMP echo request, id 41357, seq 1, length 64

终于不是arp请求了,而是icmp报文,但是这个报文只有请求,没有回复。

是什么原因呢?此时pod-1与host的对话应该是这样

pod-1:我给你发送了icmp报文,你为什么不回复我?

host:我收到了报文,但是我不知道怎么回复你,我这里没有看到10.1.10.10的路由,所以我交给了默认网关

默认网关:这条报文的目标地址我不知道,我把它丢了

pod-1: ......

还是路由问题,我们在主机上添加直连路由

# ip route add  10.1.10.10 dev  cali-001 scope link 

然后,我们再次ping host

# ip netns exec pod-1 ping -c 1  192.168.11.126
PING 192.168.11.126 (192.168.11.126) 56(84) bytes of data.
64 bytes from 192.168.11.126: icmp_seq=1 ttl=64 time=0.024 ms

--- 192.168.11.126 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.024/0.024/0.024/0.000 ms

终于通了!!!!

还没完,这时候我们执行以下命令,开启反向校验

# echo 1  >  /proc/sys/net/ipv4/conf/cali-001/rp_filter

再次尝试ping host,发现仍然可以ping通,为什么会这样呢?

我们先来看内核参数rp_filter 的说明

含义
0关闭反向路由校验
1开启严格的反向路径校验。对每个进来的数据包校验其反向路径是否是最佳路径接收报文的网卡和回数据的网卡是否是同一张网卡。如果反向路径不是最佳路径则直接丢弃该数据包。
2开启松散的反向路径校验。对每个进来的数据包校验其源地址是否可达即反向路径是否能通通过任意网卡如果反向路径不通则直接丢弃

现在我们删除路由信息,并把rp_filter修改为1,看看发生了什么

# ip route del 10.1.10.10 dev cali-001
# echo 1 > /proc/sys/net/ipv4/conf/cali-001/rp_filter

要观察内核发生了什么,我们还需要打印syslog

# sysctl -w net.ipv4.conf.all.log_martians=1

该参数用于打印是否存在火星包(丢包)

一切准备就绪,还是在pod-1中ping host

# ip netns exec pod-1 ping -c 1  -I eth0 192.168.11.126
PING 192.168.11.126 (192.168.11.126) from 10.1.10.10 eth0: 56(84) bytes of data.

--- 192.168.11.126 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms


# dmesg
[85071.926604] IPv4: martian source 169.254.1.1 from 10.1.10.10, on dev cali-001
[85071.926609] ll header: 00000000: ff ff ff ff ff ff a2 37 06 54 4f f6 08 06        .......7.TO...

# netstat -s | grep IPReversePathFilter
IPReversePathFilter: 10

上面展示了三条命令,第一条是pod-1 ping host,第二个是查看syslog信息,第三个是查看反向过滤拦截数。

从syslog日志中可以看到,出现了火星包,这个日志大意是:

cali-001上收到了src=10.0.10.10,dst=192.254.1.1的包,但是按照本机的路由设置对10.0.10.10进行路由计算,得出的out dev不是cali-001,主机路由如下:

# ip route
default via 192.168.11.2 dev ens33 proto static metric 100 
192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.126 metric 100 

因为主机没有配置10.0.10.10路由,就会从默认路由,即ens33出去,所以校验失败,内核丢弃该包,所以pod-1内收不到Arp响应

当我们配置路由后,主机路由中就有了10.0.10.10的路由,并且这个路由对应的网卡就是cali-001,满足过滤条件,内核就不会丢弃该包,而是应答Arp请求。

总结

总结以下,上文我们创建了一对虚拟网卡cali-001/eth0,并将eth0分配给pod-1这个network namespace,然后在容器中发送了一个ICMP报文,数据流转如下:

  1. 在用户态中执行ping命令,通过socket调用给到pod-1协议栈
  2. pod-1协议栈准备发起ICMP报文,查找路由表,获知从eth0出去,下一跳是169.254.1.1
  3. pod-1协议栈查找Arp表,没有169.254.1.1的MAC地址,于是先发起Arp请求
  4. cali-001收到pod-1中eth0发来的Arp请求,因为配置了proxy_arp,于是响应自己的MAC地址
  5. pod-1协议栈收到响应,组装ICMP报文,发送给cali-001
  6. cali-001收到ICMP报文,交给自己的协议栈,即host协议栈
  7. host协议栈处理完ICMP报文,准备回复
  8. host协议栈先查自己的路由表,获知应该从cali-001出去
  9. host将响应报文从cali-001发出
  10. cali-001和eth0是一对veth pair,所以eth0收到响应报文
  11. eth0将报文交给pod-1的协议栈

后续

读完本文,你可能还有疑问,同宿主机上不同network namespace应该如何通信,以及跨主机不同network namespace应该如何通信,关于这些问题,在后续文章中将会展开介绍

  • 19
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Docker和Kubernetes是两个在容器化应用领域非常流行的工具。 Docker是一种容器化平台,它可以将应用程序及其依赖项打包到一个可移植的容器中,以便在不同的环境中运行。Docker使用了Linux内核的容器技术,通过隔离进程、文件系统和网络等资源,实现了高度可移植、可扩展且隔离的应用运行环境。Docker容器可以在任何支持Docker的操作系统上运行,而不受底层操作系统的限制。 Kubernetes是一个开源的容器编排和管理平台,它可以帮助我们自动化容器的部署、扩展和管理。Kubernetes提供了一个集群管理的框架,可以将多个Docker容器组织成一个弹性、可伸缩的应用。Kubernetes通过自动化调度、负载均衡、容错恢复等机制,实现了高可用性和高性能的容器集群。 基本原理上,Docker通过使用Linux内核的容器技术实现了应用程序与底层操作系统的隔离。它使用了命名空间、控制组、文件系统等技术,确保每个容器拥有独立的运行环境。 Kubernetes则是建立在Docker之上的容器编排和管理平台。它通过使用API来管理容器集群,提供了自动化的容器编排、服务发现、负载均衡、水平扩展、滚动升级等功能。Kubernetes利用标签和选择器机制,可以方便地对容器进行管理和操作。 总的来说,Docker提供了容器化的运行环境,而Kubernetes则提供了容器集群的编排和管理能力,使得我们可以更方便地部署和管理容器化应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值