带你玩转kubernetes-k8s(第45篇:深入分析k8s网络原理[Pod和Service])

       Docker给我们带来了不同的网络模式,Kubernetes也以一种不同的方式来解决这些网络模式的挑战,但其方式有些难以理解,特别是对于刚开始接触Kubernetes的网络的开发者来说。我们在前面学习了Kubernetes、Docker的理论,本节将通过一个完整的实验,从部署一个Pod开始,一步一步地部署那些Kubernetes的组件,来剖析Kubernetes在网络层是如何实现及工作的。
      这里使用虚拟机来完成实验。如果要部署在物理机器上或者云服务商的环境中,则涉及的网络模型很可能稍微有所不同。不过,从网络角度来看,Kubernetes的机制是类似且一致的。

     好了,来看看我们的实验环境:

        

     Kubernetes的网络模型要求每个Node上的容器都可以相互访问。
     默认的Docker网络模型提供了一个IP地址段是172.17.0.0/16的docker0网桥。每个容器都会在这个子网内获得IP地址,并且将docker0网桥的IP地址(172.17.42.1)作为其默认网关。需要注意的是,Docker宿主机外面的网络不需要知道任何关于这个172.17.0.0/16的信息或者知道如何连接到其内部,因为Docker的宿主机针对容器发出的数据,在物理网卡地址后面都做了IP伪装MASQUERADE(隐含NAT)。也就是说,在网络上看到的任何容器数据流都来源于那台Docker节点的物理IP地址。这里所说的网络都指连接这些主机的物理网络。

      这个模型便于使用,但是并不完美,需要依赖端口映射的机制。

     在Kubernetes的网络模型中,每台主机上的docker0网桥都是可以被路由到的。也就是说,在部署了一个Pod时,在同一个集群内,各主机都可以访问其他主机上的Pod IP,并不需要在主机上做端口映射。综上所述,我们可以在网络层将Kubernetes的节点看作一个路由器。如果将实验环境改画成一个网络图,那么它看起来如下图所示:

 

为了支持Kubernetes网络模型,我们采取了直接路由的方式来实现,在每个Node上都配置相应的静态路由项,例如在node1这个Node上配置了两个路由项:

route add -net 10.1.20.0 netmask 255.255.255.0 gw 20.0.40.55
route add -net 10.1.30.0 netmask 255.255.255.0 gw 20.0.40.56

node2:

route add -net 10.1.10.0 netmask 255.255.255.0 gw 20.0.40.54
route add -net 10.1.30.0 netmask 255.255.255.0 gw 20.0.40.56

node3:

route add -net 10.1.10.0 netmask 255.255.255.0 gw 20.0.40.54
route add -net 10.1.20.0 netmask 255.255.255.0 gw 20.0.40.55

    这意味着,每一个新部署的容器都将使用这个Node(docker0的网桥IP)作为它的默认网关。而这些Node(类似路由器)都有其他docker0的路由信息,这样它们就能够相互连通了。
接下来通过一些实际的案例,来看看Kubernetes在不同的场景下其网络部分到底做了什么。

第1步:部署一个RC/Pod

 

 部署的RC/Pod描述文件如下(frontend-controller.yaml):

apiVersion: v1
kind: ReplicationController
metadata:
  name: frontend
  labels:
    name: frontend
spec:
  replicas: 1
  selector:
    name: frontend
  template:
    metadata:
      labels:
        name: frontend
    spec:
      containers:
      - name: tomcat
        image: tomcat
        env:
        - name: GET_HOSTS_FROM
          value: env
        ports:
        - containerPort: 18080
          hostPort: 18080

   为了便于观察,我们假定在一个空的Kubernetes集群上运行,提前清理了所有Replication Controller、Pod和其他Service.(可以不清除掉蛤)

检查一下此时某个Node上的网络接口有哪些。

Node1的状态是:

[root@k8s-node1 ~]# ifconfig 
datapath: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1376
        inet6 fe80::3012:c2ff:fe6c:37e4  prefixlen 64  scopeid 0x20<link>
        ether 32:12:c2:6c:37:e4  txqueuelen 1000  (Ethernet)
        RX packets 5148  bytes 145336 (141.9 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 648 (648.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:fa:49:9c:b9  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ens192: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 20.0.40.54  netmask 255.255.255.0  broadcast 20.0.40.255
        inet6 fe80::d2a8:dff9:79af:81ad  prefixlen 64  scopeid 0x20<link>
        ether 00:50:56:94:06:d7  txqueuelen 1000  (Ethernet)
        RX packets 27940383  bytes 9781889476 (9.1 GiB)
        RX errors 0  dropped 159  overruns 0  frame 0
        TX packets 17282068  bytes 5207858422 (4.8 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1  (Local Loopback)
        RX packets 1774557  bytes 228015453 (217.4 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1774557  bytes 228015453 (217.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vethwe-bridge: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1376
        inet6 fe80::6468:58ff:fe34:ace9  prefixlen 64  scopeid 0x20<link>
        ether 66:68:58:34:ac:e9  txqueuelen 0  (Ethernet)
        RX packets 5218  bytes 231248 (225.8 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 164  bytes 45548 (44.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vethwe-datapath: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1376
        inet6 fe80::200f:f1ff:fe14:9a20  prefixlen 64  scopeid 0x20<link>
        ether 22:0f:f1:14:9a:20  txqueuelen 0  (Ethernet)
        RX packets 1774557  bytes 228015453 (217.4 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1774557  bytes 228015453 (217.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vethwepl3ff9d10: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1376
        inet6 fe80::983e:53ff:fe53:f9ea  prefixlen 64  scopeid 0x20<link>
        ether 9a:3e:53:53:f9:ea  txqueuelen 0  (Ethernet)
        RX packets 75549  bytes 10251363 (9.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 61748  bytes 14056132 (13.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vethweplac9b1dd: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1376
        inet6 fe80::4402:85ff:fe12:4b06  prefixlen 64  scopeid 0x20<link>
        ether 46:02:85:12:4b:06  txqueuelen 0  (Ethernet)
        RX packets 75549  bytes 10251363 (9.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 61748  bytes 14056132 (13.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vxlan-6784: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 65470
        inet6 fe80::8c83:3eff:febe:3cc8  prefixlen 64  scopeid 0x20<link>
        ether 8e:83:3e:be:3c:c8  txqueuelen 1000  (Ethernet)
        RX packets 885161  bytes 1210978136 (1.1 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 880280  bytes 1210873748 (1.1 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

weave: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1376
        inet 10.43.128.0  netmask 255.240.0.0  broadcast 10.47.255.255
        inet6 fe80::fccd:a6ff:fed5:d30b  prefixlen 64  scopeid 0x20<link>
        ether fe:cd:a6:d5:d3:0b  txqueuelen 1000  (Ethernet)
        RX packets 75549  bytes 10251363 (9.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 61748  bytes 14056132 (13.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

可以看出,有一个docker0网桥和一个本地地址的网络端口。现在部署一下我们在前面准备的RC/Pod配置文件,看看发生了什么:

可以看到一些有趣的事情。Kubernetes为这个Pod找了一个主机Node3来运行它。另外,这个Pod获得了一个在Node3的docker0网桥上的IP地址。我们登录Node3查看正在运行的容器:

    在Node3上现在运行了两个容器,在我们的RC/Pod定义文件中仅仅包含了一个,那么这第2个是从哪里来的呢?第2个看起来运行的是一个叫作/pause:3.1的镜像,而且这个容器已经有端口映射到它上面了,为什么是这样呢?让我们深入容器内部看一下具体原因。使用Docker的inspect命令来查看容器的详细信息,特别要关注容器的网络模型:

     有趣的结果是,在查看完每个容器的网络模型后,我们可以看到这样的配置:我们检查的第1个容器是运行了“pause:3.1”镜像的容器,它使用了Docker默认的网络模型 bridge;而我们检查的第2个容器,也就是在RC/Pod中定义运行的tomcat容器,使用了非默认的网络配置和映射容器的模型,指定了映射目标容器为“pause:3.1”。

    一起来仔细思考这个过程,为什么Kubernetes要这么做呢?

      首先,一个Pod内的所有容器都需要共用同一个IP地址,这就意味着一定要使用网络的容器映射模式。然而,为什么不能只启动第1个Pod中的容器,而将第2个Pod中的容器关联到第1个容器呢?我们认为Kubernetes是从两方面来考虑这个问题的:首先,如果在Pod内有多个容器的话,则可能很难连接这些容器;其次,后面的容器还要依赖第1个被关联的容器,如果第2个容器关联到第1个容器,且第1个容器死掉的话,第2个容器也将死掉。启动一个基础容器,然后将Pod内的所有容器都连接到它上面会更容易一些。因为我们只需要为基础的这个Google_containers/pause容器执行端口映射规则,这也简化了端口映射的过程。

所以我们启动Pod后的网络模型类似下图:

  

在这种情况下,实际Pod的IP数据流的网络目标都是这个google_containers/pause容器。上图有点儿取巧地显示了是google_containers/pause容器将端口18080的流量转发给了相关的容器.而pause容器只是看起来转发了网络流量,但它并没有真的这么做。实际上,应用容器直接监听了这些端口,和google_containers/pause容器共享了同一个网络堆栈。这就是为什么在Pod内部实际容器的端口映射都显示到pause容器上了。我们可以使用docker port命令来检验一下。

 

综上所述,google_containers/pause容器实际上只是负责接管这个Pod的Endpoint,并没有做更多的事情。那么Node呢?它需要将数据流传给pause容器吗?

iptables-save

如果您是一个空的kubernetes来做这个实验:你会发现上的这些规则,并没有被应用到我们刚刚定义的Pod上。当然,Kubernetes会给每一个Kubernetes节点都提供一些默认的服务,上面的规则就是Kubernetes的默认服务所需要的。关键是,我们没有看到任何IP伪装的规则,并且没有任何指向Pod 10.36.0.1内部的端口映射。

第二步:发布一个服务

   我们已经了解了Kubernetes如何处理最基本的元素即Pod的连接问题,接下来看一下它是如何处理Service的。Service允许我们在多个Pod之间抽象一些服务,而且服务可以通过提供在同一个Service的多个Pod之间的负载均衡机制来支持水平扩展。我再次将环境初始化,删除刚创建的rc或pod来确保集群是空的:

   然后准备一个名为frontend的Service配置文件:

apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    name: frontend
spec:
  ports:
  - port: 80
#    nodePort: 38080
  selector:
    name: frontend
#  type:
 #   NodePort

接着在Kubernetes集群中定义这个服务:

    

kubecatl applf -f frontend-service.yaml
kubecatl get svc

    在服务正确创建后,可以看到Kubernetes集群已经为这个服务分配了一个虚拟IP地址10.110.75.165,这个IP地址是在Kubernetes的Portal Network中分配的。而这个Portal Network的地址范围是我们在Kubmaster上启动API服务进程时,使用--service-cluster-ip-range=xx命令行参数指定的:

     这个IP段可以是任何段,只要不和docker0或者物理网络的子网冲突就可以。选择任意其他网段的原因是这个网段将不会在物理网络和docker0网络上进行路由。这个PortalNetwork对每一个Node都有局部的特殊性,实际上它存在的意义是让容器的流量都指向默认网关(也就是docker0网桥)。

    在继续实验前,先登录到Node1上看一下在我们定义服务后发生了什么变化。首先检查一下iptables或Netfilter的规则:

(ps:书上的图片,由于我没有清空kubernetes,没有做成桥接,就拿书上的图片来解决了)

第1行是挂在PREROUTING链上的端口重定向规则,所有进入的流量如果满足20.1.244.75: 80,则都会被重定向到端口33761。第2行是挂在OUTPUT链上的目标地址NAT,做了和上述第1行规则类似的工作,但针对的是当前主机生成的外出流量。所有主机生成的流量都需要使用这个DNAT规则来处理。简而言之,这两个规则使用了不同的方式做了类似的事情,就是将所有从节点生成的发送给20.1.244.75:80的流量重定向到本地的33761端口。

至此,目标为Service IP地址和端口的任何流量都将被重定向到本地的33761端口。

这个端口连到哪里去了呢?

    这就到了kube-proxy发挥作用的地方了。这个kube-proxy服务给每一个新创建的服务都关联了一个随机的端口号,并且监听那个特定的端口,为服务创建相关的负载均衡对象。在我们的实验中,随机生成的端口刚好是33761。通过监控Node1上的Kubernetes-Service的日志,在创建服务时可以看到下面的记录:

     现在我们知道,所有流量都被导入kube-proxy中了。我们现在需要它完成一些负载均衡的工作,创建Replication Controller并观察结果,下面是Replication Controller的配置文件

apiVersion: v1
kind: ReplicationController
metadata:
  name: frontend
  labels:
     name: frontend
spec:
  replicas: 3
  selector:
    name: frontend
  template:
    metadata:
      labels:
        name: frontend
    spec:
      containers:
      - name: nginx
        image: nginx
        imagePullPolicy: IfNotPresent
        env:
        - name: GET_HOST_FROM
          value: env
        ports:    
        - containerPort: 80
          hostPort: 38080

     在集群发布上述配置文件后,等待并观察,确保所有Pod都运行起来了:

 

    现在所有的Pod都运行起来了,Service将会把客户端请求负载分发到包含“name=frontend”标签的所有Pod上。

     Kubernetes的kube-proxy看起来只是一个夹层,但实际上它只是在Node上运行的一个服务。上述重定向规则的结果就是针对目标地址为服务IP的流量,将Kubernetes的kube-proxy变成了一个中间的夹层。

    为了查看具体的重定向动作,我们会使用tcpdump来进行网络抓包操作。

    首先,安装tcpdump:

   

yum install -y tcpdump

   安装完成后,登录Node1,运行tcpdump命令:

tcpdump -nn -q -i   port 80

 需要捕获物理服务器以太网接口的数据包,Node1机器上的以太网接口名字叫作ens192。

 

     再打开第1个窗口运行第2个tcpdump程序,不过我们需要一些额外的信息去运行它,即挂接在docker0桥上的虚拟网卡Veth的名称。我们看到只有一个frontend容器在Node1主机上运行,所以可以使用简单的“ip addr”命令来查看最后一个的Veth网络接口:

好了,我们已经在同时捕获两个接口的网络包了。这时再启动第3个窗口,运行一个“docker exec "命令来连接到我们的frontend容器内部(你可以先执行docker ps来获得这个容器的ID):

这些信息说明了什么问题呢?

     总而言之,Kubernetes的kube-proxy作为一个全功能的代理服务器管理了两个独立的TCP连接:一个是从容器到kube-proxy:另一个是从kube-proxy到负载均衡的目标Pod。

 

小结:

     本节内容到此结束,谢谢大家的浏览,多多点关注蛤。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值