k8s的host网络模型

一, docker网络模型

一)网络模式介绍

查看显示的三种网络模式,其实还有一种是容器模式。一共4种

docker network ls

bridge模式:使用–net =bridge指定,默认设置;
host模式:使用–net =host指定;
none模式:使用–net =none指定;
container模式:使用–net =container:NAMEorID指定。

(二)bridge模式(docker默认的网络模式)

  • ①介绍

在默认情况下,docker 会在 host 机器上新创建一个 docker0 的 bridge:可以把它想象成一个虚拟的交换机,所有的容器都是连到这台交换机上面的。docker 会从私有网络中选择一段地址来管理容器,比如 172.17.0.1/16,这个地址根据你之前的网络情况而有所不同。

 

sudo yum install net-tools
ifconfig -a

  • ②数据流程
  1. 容器内部发送一条报文,查看路由规则,默认转发到 172.17.0.1(如果是同一个网段,会直接把源地址标记为 172.17.0.2 进行发送)
  2. 通过 eth0 发送的报文,会在 vethXXX 被接收,因为它直接连在 docker0 上,所以默认路由到 docker0
  3. 这个时候报文已经来到了主机上,查询主机的路由表,发现报文应该通过 eth0 从默认网关发送出去,那么报文就被转发给 eth0(就是前面提到的要打开 linux 系统的自动转发配置)
  4. 匹配机器上的 iptables,发现有一条 -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE,也就是 SNAT 规则,那么 linux 内核会修改 ip 源地址为 eth0 的地址,维护一条 NAT 规则记录,然后把报文转发出去。(也就是说对于外部来说,报文是从主机 eth0 发送出去的,无法感知容器的存在)

  • ③流程演示方便理解

启动两个容器

docker run --name a1 -d busybox /bin/sh -c "while true;do echo hello docker;sleep 10;done"
docker run --name a2 -d busybox /bin/sh -c "while true;do echo hello docker;sleep 10;done"

容器互相通信

 

docker exec -it a1 /bin/sh 
ifconfig
#查看到a1的ip是172.17.0.2
exit
docker exec -it a2 /bin/sh 
ifconfig
#查看到a2的ip是172.17.0.3
#在a2容器内可以ping通172.17.0.2
ping 172.17.0.2

#在a1容器内尝试ping下a2的ip 172.17.0.3
#在a1容器内可以ping通172.17.0.2
ping 172.17.0.3

记录ip太麻烦了,可以通过link的方式直接让容器包含起来
这样只是单向的,还需要删除a1,重新创建才能添加link a2,

docker run --name a2 --link a1 -d busybox /bin/sh -c "while true;do echo hello docker;sleep 10;done"
docker exec -it a2 /bin/sh
ping a1

  • ④自定义网络

但是建议大家不要使用link的方式,如果容器千千万都link,人就受不了了。还是自定网络比较靠谱。下面说说自定义网络

docker network create -d bridge net-test

测试网络通信,创建容器,进行通信
不需要ip的方式两个容器都是通的。自定义网络牛Xclass

docker run --name test3 --network net-test -d busybox /bin/sh -c "while true;do echo hello docker;sleep 10;done"
docker run --name test4 --network net-test -d busybox /bin/sh -c "while true;do echo hello docker;sleep 10;done"
docker exec -it test3 /bin/sh
ping test4
exit
docker exec -it test4 /bin/sh
ping test3
exit

自定义网络的IP段看下。172.18网段。

 


docker network inspect net-test

container 里面包括test3 和test4

(三)host模式(共享主机的网络模式)

docker 不会为容器创建单独的网络 namespace,而是共享主机的 network namespace,也就是说:容器可以直接访问主机上所有的网络信息。在实际微服务的环境中不建议使用这种。

#network 更换成host
 docker run --name test5_host --network host -d busybox /bin/sh -c "while true;do echo hello docker;sleep 10;done"  
docker exec -it test5_host  /bin/sh
ifconfig

跟宿主机是一样的

直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题,Docker host 上已经使用的端口就不能再用了。Docker host 的另一个用途是让容器可以直接配置 host 网路。比如某些跨 host 的网络解决方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。容器中,对这些设备有全部的访问权限。因此docker提示我们,这种方式是不安全的。如果在隔离良好的环境中(比如租户的虚拟机中)使用这种方式,问题不大。

(四)container 模式(容器之前的共享模式,学习k8s这个很重要)

一个容器直接使用另外一个已经存在容器的网络配置:ip 信息和网络端口等所有网络相关的信息都是共享的。需要注意的是:这两个容器的计算和存储资源还是隔离的。

 

# test7_container 依赖a1的网络模式
 docker run --name test7_container --network container:a1 -d busybox /bin/sh -c "while true;do echo hello docker;sleep 10;done" 
# 分别进入test7_container 和a1查看ifconfig 发现两个是一样的
docker exec -it test7_container /bin/sh
ifconfig
exit
docker exec -it a1 /bin/sh
ifconfig
exit

kubernetes 的 pod 就是用这个实现的,同一个 pod 中的容器共享一个 network namespace。


 

二, k8s 网络模型

Pods

Pods中的多个container共享一个网络栈,这是基础了。Pods的网络namespace和宿主机的物理网络的namespace不是一个,之间通过docker bridge相连。

在pods的namespace中,pods的虚拟网络接口为veth0;在宿主机上,物理网络的网络接口为eth0。docker bridge作为veth0的默认网关,用于和宿主机网络的通信。

所有pods的veth0所能分配的IP是一个独立的IP地址范围,来自于创建cluster时候kubeadm的--pod-network-cidr参数设定的CIDR,这里看起来像是172.17.0.0/24,是一个B类局域网IP地址段;所有宿主机的网络接口eth0所能分配的IP是实际物理网络的设定,一般来自于实际物理网络中的路由器通过DHCP分配的,这里看起来是10.100.0.0/24,是一个A类局域网IP地址段。

docker bridge建立起pod和其宿主机之间的通信,但是并没有解决完问题,事实上,直到这里还属于Docker自身的范畴,K8s的pods还要能够与其它宿主机节点上的pods通信,这怎么做到呢?通过添加路由信息!如下图所示:

注意上图中,docker0的名字被改成了cbr0,意思是custom bridge,没错,K8s使用custom bridge代替了docker bridge。由此,如果左侧的pod想访问右侧的pod,则IP包会通过bridge cbr0来到左侧宿主机的eth0,然后查询宿主机上新增的路由信息,继而将IP包送往右侧的宿主机的eth0,继而再送往右侧的bridge cbr0,最后送往右侧的pod。

上面说了IP层相通的逻辑基础,就像前文说的那样,这个IP就是Pod的IP,是pods的veth0所能分配的IP是一个独立的IP地址范围,来自于创建cluster时候kubeadm的--pod-network-cidr参数设定的CIDR;而端口就是容器中服务实实在在listen端口,我们称之为containerPort。到目前为止,我们就可以通过pod ip + containerPort的方式来访问pod的服务了。

访问是可以访问了,但是问题就来了,而且还很多,以至于根本没法在实际项目中使用:

  1. 一个服务经常会起多个pod,你到底访问那个pod的ip呢?
  2. pod经常会因为各种原因被调度,调度后一个pod的ip会发生变化...
  3. pod的ip是虚拟的且局域的,在集群内部访问没有问题,但是从k8s集群的外部如何访问pod的ip呢?

解决1、2问题的答案就是反向代理和负载均衡,这是由来已久的惯例,由此我们引入了K8s的service。

services

我们可以使用service封装多个pods的一类服务,对外提供同一的service的ip和port,岂不解决了上述的问题?

在上图中,我们将右边pod的服务抽象为了service,service的ip段来自于创建集群时kubeadm的-service-cluster-ip-range指定的CIDR,在本案例中该service分配了10.3.241.152的ip。但是,如果通过该service的ip访问目标pod时,如何确保ip包能够到达这个莫名其妙的ip呢?birdge cbr0 不认识,只管往上扔,eth0还是不认识,一看路由表,也不知道怎么走,那这条路不就断了吗?答案就是kube-proxy通过用户空间的iptable来和内核中的netfilter进行转发规则的实时同步,这样当一个ip包匹配上了iptable规则中的某一条规则时,就会被转发到对应的新地址上,从而做到了将ip包送往目标pod。没错,netfilter就是内核空间的ip包的proxy:Netfilter规则是路由规则,工作在3层上。路由规则决定packet的行进路径靠的是packet中本身包含的信息,一般来说就是这个packet从哪里来要到哪里去。 以上述案例来说,每个来到宿主机节点eth0网口的要去往10.3.241.152:80的包都将会被netfilter处理, 进而packet会被送往一个服务正常的pod上。

要值得注意的是,netfilter的规则是用来匹配目的地地址为service ip的packet的。

前面已经说过了service的ip,而service的port(对,就叫port,不像pod的containerPort那样叫servicePort)范围由--service-node-port-range参数指定,默认值是30000-32767,再通过targetPort指定pod的containerPort,从而建立起service端口到pod端口的映射关系。由此,service IP + port的组合开始提供访问服务,并且还是很稳定的,不会随着pod的变动而变动。那么service封装pod来提供服务究竟可靠吗?我们重温一遍:

  1. kube-proxy作为一个systemd的服务在运行,所以及时异常也会被重启,所以提供的服务还是比较可靠的;
  2. kube-proxy接收k8s master API服务的通知以响应集群中的变动,当收到更新通知后,kube-proxy通过iptables来同步netfilter中的规则;
  3. 当一个新的service及其endpoints被创建后,kube-proxy收到通知,通过iptables在netfilter中新建必备的匹配规则;当一个service被删除后,同理该规则会被kube-proxy删除;
  4. 每个宿主机节点上运行的kubelet组件会对各个endpoints进行健康检查,当某个endpoint被发现不正常后,kubelet会通过master的api server来通知kube-proxy,然后kube-proxy会修改netfilter的规则来去除这个不正常的endpoint;当endpoint重新恢复正常后,规则又再次被编辑来将该endpoint添加回来。

到目前为止,service已经能够应付上一章节最后三问中的前两问,也就是多个pod的负载均衡被解决了,pod因为各种原因更换ip也被service封装在内部解决了,但是第三个问题还没有解决:

  1. pod的ip是虚拟的且局域的,在集群内部访问没有问题,但是从k8s集群的外部如何访问pod的ip呢?

要解决这个问题,k8s提供了以下几种方法:

  1. 带nodePort的service;
  2. 带externalIPs的service;
  3. ingress;
  4. LoadBalancer,需要底层的基础设施支持。目前 Google Cloud Platform 和 AWS平台上支持这种类型;比如在GCP上,当创建一个LB时,GCP会创建一个external IP、forwarding rule、target proxy、backend service、instance group。 当这个external IP创建好后,你可以配置域名指向它,然后将域名告诉客户端即可;

带nodePort的service

nodePort是kubernetes提供给K8s cluster外部客户访问service入口的一种方式,你在本文的前面已经见过了。这个会将service的端口mapping到每个Node上,然后你在cluster外部可以使用<NodeIP>:<NodePort> 来访问该服务。

当kubernetes创建一个NodePort service时,kube-proxy从端口范围30000–32767 中分配一个,并且打开这个端口侦听在每个宿主机节点的eth0网口上,所有通往这个端口的packet都被转发到对应的service的(cluster)ip上 。

上图中客户端通过公网IP连接到LB,LB然后分发到一个宿主机node上,也就是10.100.0.3:32213,kube-proxy收到后将其转发到service所在的cluster IP 10.3.241.152:80像前文所说的,“netfilter的规则是用来匹配目的地地址为service ip的packet的“,这个时候就匹配上了netfilter规则,然后被转发到了服务所在的pod,也就是10.0.2.2:8080

这种类型的service目前看起来有如下的缺点:

  1. 端口都是乱七八糟的端口,不是标准端口,如80;
  2. 端口数量有限,才2768个;
  3. 端口随机。

带externalIPs的service

首先K8s cluster的Node上有外网可以访问的IP,那么像下面这样的配置就可以使用80.11.12.10:80来访问civilnet-service;

kind: Service
apiVersion: v1
metadata:
  name: civilnet-service
spec:
  selector:
    app: Gemfield
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  externalIPs:
  - 222.128.5.170

ingress

ingress是个资源类型,要在k8s上管理这种类型的资源,你需要安装ingress controller。目前用的最多的ingress controller就是nginx实现的:

这种ingress的使用体验,就相当于把nginx的配置规则和k8s的configMap进行了结合。

值得注意的是,ingress的这种proxy主要针对的还是http,想象一下普通的Nginx服务是怎么工作的就清楚多了。如果你要进行其它协议的proxy(比如rtmp流媒体协议,比如mqtt物联网协议等),先确保目前的ingress controller是否支持。

如果有时候你使用kubectl get ing看状态,你会发现刚创建的ingress资源的ADDRESS是空的:

gemfield@master:~$ kubectl get ing
NAME          HOSTS     ADDRESS   PORTS     AGE
web-ingress   *                   80        8s

这是因为你部署ingress的时候,使用的是NodePort类型的(官网上在BareMetal上默认安装的方式),NodePort类型的ingress没有显式的IP,这种情况下,你仍然可以使用任一node的NodePort来访问ingress;那么如果你想要显式的IP,你需要修改上述官方的ingress service的yaml文件(从NodePort类型改为:

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app: ingress-nginx
  externalIPs: 
    - 192.168.1.188

重新部署ingress service后,然后再次创建ingress resource,然后等几十秒,然后使用kubectl get ing看看状态,你会发现ADDRESS已经有值了:

gemfield@master:~$ kubectl get ing
NAME          HOSTS     ADDRESS         PORTS     AGE
web-ingress   *         192.168.1.188   80        2m

 

三, k8s的host网络模型

1、Pod的网络
  每个Pod都会默认启动一个pod-infrastructure(或pause)的容器,作为共享网络的基准容器。其他业务容器在启动之后,会将自己的网络模式指定为“"NetworkMode": "container:pause_containerID”。这样就能做到Pod中的所有容器网络都是共享的,一个Pod中的所有容器中的网络是一致的,它们能够通过本地地址(localhost)访问其他用户容器的端口。在Kubernetes的网络模型中,每一个Pod都拥有一个扁平化共享网络命名空间的IP,称为PodIP。通过PodIP,Pod就能够跨网络与其他物理机和容器进行通信。

  也可以设置Pod为Host网络模式,即直接使用宿主机的网络,不进行网络虚拟化隔离。这样一来,Pod中的所有容器就直接暴露在宿主机的网络环境中,这时候,Pod的PodIP就是其所在Node的IP。从原理上来说,当设定Pod的网络为Host时,是设定了Pod中pod-infrastructure(或pause)容器的网络为Host,Pod内部其他容器的网络指向该容器。

2. 一个demo
  对于同Deployment下的Host模式启动的Pod,每个node上只能启动一个。也就是说,Host模式的Pod启动副本数不可以多于“目标node”的数量,“目标node”指的是在启动Pod时选定的node,若未选定(没有指定nodeSelector),“目标node”的数量就是集群中全部的可用的node的数量。当副本数大于“目标node”的数量时,多出来的Pod会一直处于Pending状态,因为schedule已经找不到可以调度的node了。

  以下示例中,集群只有4个node,当设置副本数量为5时,最后一个Pod状态会一直处于Pending状态。

[root@k8s-master yaml]# kubectl get pod -o wide
NAME                         READY     STATUS    RESTARTS   AGE       IP             NODE
test-host-1108333573-11wbl   1/1       Running   0          17s       10.0.251.153   k8s-node-1
test-host-1108333573-2k35s   1/1       Running   0          17s       10.0.251.146   k8s-node-3
test-host-1108333573-lnlpy   1/1       Running   0          17s       10.0.251.222   k8s-node-4
test-host-1108333573-t6izr   1/1       Running   0          17s       10.0.251.155   k8s-node-2
test-host-1108333573-tf4mc   0/1      Pending   0          17s       <none>         

Pod的PodIP就是其所在Node的IP,具体如下:

[root@k8s-master yaml]# kubectl get pod -o wide
NAME                         READY     STATUS    RESTARTS   AGE       IP             NODE
test-host-1108333573-11wbl   1/1       Running   0          2h        10.0.251.153   k8s-node-1
test-host-1108333573-2k35s   1/1       Running   0          2h        10.0.251.146   k8s-node-3
test-host-1108333573-lnlpy   1/1       Running   0          2h        10.0.251.222   k8s-node-4
test-host-1108333573-t6izr   1/1       Running   0          2h        10.0.251.155   k8s-node-2
虽然PodIP是NodeIP,但也可以通过service的方式加到Kubernetes的虚拟化网络中被使用。创建成功之后,集群内部的应用可通过虚拟网络中的clusterIP:10.254.127.198:8080来访问后端服务,集群外部的应用(如浏览器)可以通过nodeIP+NodePort来访问后端服务。

3. 端口占用
若同集群中,用host模式启动的deployment(或RC)有多个,若这些 deployment中定义的containerPort有相同的值,那么,Kubernetes会校验出端口资源的冲突。
[root@k8s-master yaml]#  kubectl get pod -o wide
NAME                         READY     STATUS    RESTARTS   AGE       IP             NODE
test-host-1108333573-9oo55   1/1       Running   0          19m       10.0.251.155   k8s-node-2
test-host-1108333573-uc2v2   1/1       Running   0          19m       10.0.251.146   k8s-node-3
test-host-1108333573-vxezq   1/1       Running   0          19m       10.0.251.153   k8s-node-1
test-host1-6476903-q8e6o     0/1       Pending   0          19m       <none>         
test-host1-6476903-zmk5l     1/1       Running   0          19m       10.0.251.222   k8s-node-4
  实验集群下,可用的node数量只有4个,而两个deployment——test-host、test-host1对外暴露额端口是相同的——8080。两个deployment一共要启动的副本数量为5,这时,最后启动的那个Pod就会一直处于Pending状态。

 宿主机端口占用: 当Host模式的Deployment(或RC)声明一个端口时,比如8080,若宿主机上有非Kubernetes控制的程序占用了8080这个端口,这时Kubernetes是无法校验到的。也就是说,schedule仅仅会记录Kubernetes集群中的端口占用信息,并在调度时做相关的校验工作。但schedule不会校验宿主机上真正的端口占用信息。这其实是非常合理的,集群中的node通常成千上万,被当做一台台单纯的提供计算能力的资源,计算什么由中心节点来决定。没有必要,也没有场景需要在node上额外的跑其他程序。

在使用Host模式网络时,需注意的是,每个应用(部署成一个deployment)都要有自己固定的、和其他应用不同的端口,比如编码器永远固定成9525、源服务器永远固定成9537等。且宿主机在做了Kubernetes集群中的node之后,尽量不对非本集群应用提供服务。

4. 镜像制作要求
  必须用Host模式启动的Pod,在镜像制作时要求端口唯一、且单一。

  一般Pod中只会存在一个业务主镜像,该业务镜像在制作时,应该只放一种应用,这个应用只对外开放一个接口。例如,编码器主控节点这个应用,主要有两方面的功能:1)接收组播流,并控制处理节点,占用端口9525;2)可视化操控界面,占用端口8080。其中接收组播流这块,需要使用Host模式的网络。拆分建议为两个业务镜像,部署时部署两个deployment。即接收组播流拆成一个镜像,固定端口9525,使用Host模式启动;可视化界面拆成一个镜像,用Kubernetes默认网络模式启动。
 

posted @ 2023-02-22 22:41  車輪の唄  阅读(438)  评论(0编辑  收藏  举报  来源