深入理解kubernetes(k8s)网络原理之一-pod连接主机
对于刚接触k8s的人来说,最令人懵逼的应该就是k8s的网络了,如何访问部署在k8s的应用,service的几种类型有什么区别,各有什么使用场景,服务的负载均衡是如何实现的,与haproxy/nginx转发有什么区别,网络策略为什么不用限制serviceIP等等
本文将站在一个初学者的角度,采用一边讲解一边实践的方式把k8s的网络相关的原理慢慢剖析清楚,并且用普通的linux命令把pod/serviceIP/nodePort等场景都模拟出来;
本文比较适合刚接触k8s,对docker有一些了解,有一定计算机基础的童鞋,在浏览本文时,各位童鞋可以准备个centos7.6的环境,安装docker和iproute2工具包,一边看一边操作,加深理解。
OK,走起!
一些关于linux网络的知识
在开始之前,有一些关于linux网络的知识你需要先知道,就像做数学题之前,先得理解公式一样。
每当linux需要向外发送一个数据包时,总是会执行以下步骤:
- 查找到该数据包的目的地的路由信息,如果是直连路由(不知道什么是直连路由?没关系,后面会解释),则在邻居表中查找该目的地的MAC地址
- 如果非直连路由,则在邻居表中找下一跳的MAC地址
- 如果找不到对应的路由,则报“network is unreachable”
- 如果在邻居表中没有查找到相应的MAC信息,则向外发送ARP请求询问
- 发送出去的数据帧,源MAC地址为发送网卡的MAC地址,目标MAC则是下一跳的MAC,只要不经过NAT,那么源目地IP是全程不会变化的,而MAC地址则每一跳都会变化
每当linux收到一个数据帧时,总会执行以下步骤:
- 如果数据帧目标MAC地址不是收包网卡的MAC,也不是ff:ff:ff:ff:ff:ff(ARP广播),且网卡未开启混杂模式,则拒绝收包;
- 如果数据帧目标MAC为ff:ff:ff:ff:ff:ff,则进入arp请求处理流程;
- 如果数据帧目标MAC地址是收包网卡的MAC,且是IP包,则:
- 目标IP地址在本机,则上送到上一层协议继续处理;
- 目标IP地址不在本机,则看net.ipv4.ip_forward是否为1,如果为1,则查找目标IP的路由信息,进行转发;
- 目标IP地址不在本机,且net.ipv4.ip_forward为0,则丢弃
一些常用的命令
代码语言:txt复制ip link ##查看网卡信息
ip addr ##查看网卡IP地址
ip route ##查看路由信息
ip neigh ##查看邻居表信息
##上面的命令均可简化,就是第二个单词的首字母,
##例如ip link可以简化为ip l,ip addr可以简化为ip a,以此类推……
iptables-save ##查看所有iptables规则
然后,我们就正式开始了,因为k8s的网络主要都是要解决怎么访问pod和pod怎么访问外面的问题,所以先来了解一下什么是pod
pod是什么
现在的服务器一般配置都比较高,64核256G的配置,如果一台服务器只用来跑一个java程序,显然就太浪费了,如果想资源利用率高一些,可以用qemu-kvm或vmware等软件进行虚拟化,让多个java进程分别运行在虚拟机里,这样可以相互不受影响;
但虚拟化难免会带来一些资源损耗,而且要先拉起一台虚拟机再在里面启动一个java进程也会比直接在裸金属服务上启动一个java进程要耗费更多的时间,只是从运行几个java进程的角度来说,虚拟化并非资源利用的最优解;
如果不用虚拟化,直接同时在裸金属服务上运行多个java进程,就要解决各个进程CPU内存资源占用、端口冲突、文件系统冲突等几个问题,否则就会出现:
- 一个进程消耗了大量的内存和CPU,而另一个更重要的进程却得不到资源;
- 80端口只有一个,进程A用了,进程B就用不了
- 多个进程同时操作相同的目录或读写相同的文件,造成异常
因为需要让多个进程都能高效地相互不受影响地运行,所以容器技术出现了,其中又以docker最为流行,容器解决了多进程间的环境隔离:
- 资源隔离,使用linux control group(简称:cgroup)解决各进程cpu和内存、io的资源分配问题
- 网络隔离,使用linux network namespace(下面开始简称:ns)使各个进程运行在独立的网络命名空间,使各类网络资源相互隔离(网卡、端口、防火墙规则、路由规则等)
- 文件系统隔离,使用union fs,例如:overlay2/aufs等,让各个进程运行在独立的根文件系统中
而所谓的pod,就是共享一个ns的多个容器
但是,什么叫“共享一个ns的多个容器”?
每当我们用docker运行一个容器,默认情况下,会给这个新的容器创建一个独立的ns,多个容器间相互访问只能使用对方IP地址
代码语言:txt复制docker run -itd --name=pause busybox
docker run --name=nginx -d nginx
此时要在pause中访问nginx,先查找一下nginx容器的IP地址:
代码语言:txt复制docker inspect nginx|grep IPAddress
"IPAddress": "172.17.0.8",
然后在pause容器中用刚查到的IP访问:
代码语言:txt复制docker exec -it pause curl 172.17.0.8
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
....
</body>
</html>
但其实是可以让nginx容器加入pause容器的ns,用下面的命令可以模拟:
代码语言:txt复制docker run -itd --name=pause busybox
docker run --name=nginx --network=container:pause -d nginx
此时pause容器和nginx容器在相同的ns中,相互间就可以用localhost访问对方了,可以用下面的命令验证:
代码语言:txt复制docker exec -it pause curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
....
</body>
</html>
pause容器和nginx容器就是“共享一个ns的两个容器”,所以,pause容器和nginx容器加起来,就是k8s的pod
在k8s集群的节点中使用docker ps,总是会发现一堆名为pause的容器,就是这个原因,pause是为多个业务容器提供共享的ns的
可以用下面的命令进入之前用docker创建的pause容器的ns
先获取pause容器的pid
代码语言:txt复制docker inspect pause|grep Pid
"Pid": 3083138,
用nsenter
命令进入指定pid的ns
nsenter --net=/proc/3083138/ns/net
此时我们已经在pause容器的ns中了,可以查看该ns的网卡,路由表,邻居表等信息
代码语言:txt复制ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
605: eth0@if606: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.7/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
代码语言:txt复制ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2
从网络的角度看,一个pod就是一个独立的ns,解决了ns的网络进出问题,就解决了pod的网络问题,所以下面的讨论就都以ns为主,不再频繁创建docker容器,后面的描述不管是pod还是ns,请统一理解为ns就好
认识ns
对于一台linux主机来说,影响网络方面的配置主要有以下几个:
- 网卡:启动时初始化,后期可以添加虚拟设备;
- 端口:1到65535,所有进程共用
- iptables规则:配置进出主机的防火墙策略和NAT规则
- 路由表:到目标地址的路由信息
- 邻居表:与主机在同个二层网络(什么是同个二层网络?大概可以理解为在一台交换机上,彼此之间通过ARP找到对方的)的其它主机的MAC地址与IP地址的映射关系
对于每一个ns来说,这几个配置都是独立的,所以从网络的角度来说,当你创建一个新的ns,其实就相当于拥有了一台新的主机
用下面的命令创建新的ns
代码语言:txt复制ip netns add ns1 ##创建ns
然后我们可以用ip netns exec ns1
为前缀来执行命令,这样显示的结果就都是ns1的网络相关配置:
ip netns exec ns1 ip link show
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
可以看到,我们用ip命令创建的ns除了一张lo网卡外,其它都是空白,这样一来她里面运行的程序访问不了外面,外面也找不到她,需要我们一点点地去组织,这就是k8s的cni要做的事情
上面我们用docker启动的容器中,网卡,IP,路由都是docker网络管理(cnm)给组织好的,从单节点的网络管理来看,cni和cnm做的事情还是挺像的
下面我们就一步步来完成k8s的cni做的事情,第一步是让主机和pod能相互访问
主机与pod相互访问
首先给ns1增加一张与主机连接的网卡,这里会用到linux虚拟网络设备veth网卡对,对于veth,基本上可以理解为两张网卡中间连着线,一端发送会触发另一端接收,用下面的命令创建:
代码语言:txt复制ip link add ns1-eth0 type veth peer name veth-ns1 ## 增加一对veth网卡,名为ns1-eth0和veth-ns1
ip link set ns1-eth0 netns ns1 ## 其中一端挪到刚才创建的ns1中,另一端留在主机端,这样主机和ns就连接起来了
ip link set veth-ns1 up ##启动主机端的网卡veth-ns1
ip netns exec ns1 ip addr add 172.20.1.10/24 dev ns1-eth0 ##此时ns1-eth0已经在ns1中了,所以要进去ns1中去执行命令设置网卡IP
ip netns exec ns1 ip link set ns1-eth0 up ##启动ns1端的网卡ns1-eth0
笔者使用的主机的IP是192.168.6.160,此时尝试从ns1中ping主机,能通了吗?
代码语言:txt复制ip netns exec ns1 ping 192.168.6.160
connect: Network is unreachable
ping不通主机,此时的情况就是上面提到的linux知识点中关于发送数据包的第3点,因为没有到目的地的路由,ns1不知如何去192.168.6.160,在这里我们要给ns1增加一条默认路由,所有没有显式声明路由的数据包都会走默认网关:
代码语言:txt复制ip netns exec ns1 ip route add default via 172.20.1.1 dev ns1-eth0
去看一下ns1中的路由表,已经有两条路由信息
代码语言:txt复制ip netns exec ns1 ip route
default via 172.20.1.1 dev ns1-eth0 ##这是一条非直连路由,意思是默认流量走ns1-eth0网卡,下一跳为172.20.1.1
172.20.1.0/24 dev ns1-eth0 proto kernel scope link src 172.20.1.10 ## 这是一条直连路由,没有via的就是直连路由,这是我们给网卡设置IP时系统自动增加的
此时能通了吗?
代码语言:txt复制ip netns exec ns1 ping 192.168.6.160
PING 192.168.6.160 (192.168.6.160) 56(84) bytes of data.
还是不行,定在那里半天没反应,又卡在哪了呢?还是linux知识点中关于发送数据包的第2点,如果是非直连路由,会先去拿下一跳的MAC地址,下一跳是172.20.1.1,能获取到它的MAC地址吗?用下面的命令查一下邻居表:
代码语言:txt复制ip netns exec ns1 ip neigh
172.20.1.1 dev ns1-eth0 FAILED
很明显,获取不到,因为网关IP地址确实是个不存在的地址,但其实这个地址不需要是个存在的地址(所以你会看到在calico中,容器的默认网关是个168.254开头的地址),因为这个地址其实是用不到的,网关的IP是不会出现在pod发送的数据包中的,真正需要用到的是网关的mac地址,我们的目的是要得到主机端veth-ns1的mac地址,有两个方法:
- 设置对端的网卡的arp代答,ns1-eth0的对端是主机上的veth-ns1网卡
echo 1 > /proc/sys/net/ipv4/conf/veth-ns1/proxy_arp
这样就开启了veth-ns1的arp代答,只要收到arp请求,不管目标IP是什么,veth-ns1网卡都会把自己MAC地址回复回去
- 把网关地址设置在对端的网卡上
在这里我们用第一种方式,设置后再查一下邻居表:
代码语言:txt复制ip netns exec ns1 ip neigh
172.20.1.1 dev ns1-eth0 lladdr b6:58:7b:0e:35:b3 REACHABLE
可以看到,已经拿到网关的MAC地址了,这个地址也确实就是主机端veth-ns1的地址:
代码语言:txt复制 ip link show veth-ns1
607: veth-ns1@if608: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether b6:58:7b:0e:35:b3 brd ff:ff:ff:ff:ff:ff link-netns ns1
这下总该行了吧?兴高采烈地ping一下,发现依旧不行
oh shit!!
什么垃圾!!
入门到放弃!!!
先别急着崩溃,主机委屈说:数据包我收到了,只是我不知道怎么回给你,因为我这里没有到172.20.1.10的路由,所以我就把回包给了我的默认网关……
坚持住,只需最后一步,在主机上添加到pod的直联路由
代码语言:txt复制ip route add 172.20.1.10 dev veth-ns1 ## 这也是一条直连路由,注意是添加主机的路由,所以不用ip netns exec ns1开头了
此时从ns1中ping主机:
代码语言:txt复制ip netns exec ns1 ping -c 5 192.168.6.160
PING 192.168.6.160 (192.168.6.160) 56(84) bytes of data.
64 bytes from 192.168.6.160: icmp_seq=1 ttl=64 time=0.025 ms
64 bytes from 192.168.6.160: icmp_seq=2 ttl=64 time=0.017 ms
64 bytes from 192.168.6.160: icmp_seq=3 ttl=64 time=0.015 ms
64 bytes from 192.168.6.160: icmp_seq=4 ttl=64 time=0.017 ms
64 bytes from 192.168.6.160: icmp_seq=5 ttl=64 time=0.017 ms
--- 192.168.6.160 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 135ms
rtt min/avg/max/mdev = 0.015/0.018/0.025/0.004 ms
从主机ping ns1
代码语言:txt复制ping -c 5 172.20.1.10
PING 172.20.1.10 (172.20.1.10) 56(84) bytes of data.
64 bytes from 172.20.1.10: icmp_seq=1 ttl=64 time=0.026 ms
64 bytes from 172.20.1.10: icmp_seq=2 ttl=64 time=0.015 ms
64 bytes from 172.20.1.10: icmp_seq=3 ttl=64 time=0.016 ms
64 bytes from 172.20.1.10: icmp_seq=4 ttl=64 time=0.015 ms
64 bytes from 172.20.1.10: icmp_seq=5 ttl=64 time=0.016 ms
--- 172.20.1.10 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 138ms
rtt min/avg/max/mdev = 0.015/0.017/0.026/0.006 ms
OK,成功完成第一步,pod与主机互通,此时在pod里能访问百度吗?
答案是不行,172.20.1.10这个地址是我们随便模拟出来的,除了当前这台主机谁也不认识,所以如果他要访问外网,需要做源地址转换,这个场景跟我们办公室的PC上外网原理是一样的,当你用办公室内网的电脑去访问百度时,数据包到达百度的服务器上时源地址肯定不是你电脑的内网网卡地址,而是你的办公网络出外网的出口IP加一个随机端口,这个源地址转换是你的办公室网络出口路由器自动帮你完成的,我们在主机上也要配置针对刚才创建的pod的源地址转换规则
pod访问外网
- 首先第一步要打开本机的ip转发功能
echo 1 > /proc/sys/net/ipv4/ip_forward
- 然后是设置snat规则
iptables -A POSTROUTING -t nat -s 172.20.1.10 -j MASQUERADE
此时再试一下已经可以ping通百度
代码语言:txt复制ip netns exec ns1 ping -c 5 www.baidu.com
PING www.a.shifen.com (14.215.177.38) 56(84) bytes of data.
64 bytes from 14.215.177.38 (14.215.177.38): icmp_seq=1 ttl=54 time=9.47 ms
64 bytes from 14.215.177.38 (14.215.177.38): icmp_seq=2 ttl=54 time=9.42 ms
64 bytes from 14.215.177.38 (14.215.177.38): icmp_seq=3 ttl=54 time=9.42 ms
64 bytes from 14.215.177.38 (14.215.177.38): icmp_seq=4 ttl=54 time=9.33 ms
64 bytes from 14.215.177.38 (14.215.177.38): icmp_seq=5 ttl=54 time=9.58 ms
--- www.a.shifen.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 9ms
rtt min/avg/max/mdev = 9.332/9.443/9.582/0.102 ms
至于上面的iptables规则和为何要开启转发,下一章解释serviceIP时再详细介绍。