循序渐进地学习kubernetes网络

 


目录

收起

导语

1. linux网络命名空间

2. 不同网络命名空间的通信

2.1 多个网络命名空间的通信

3. pod内部容器的网络通信

4. 同一个node,不同pod间的网络通信

5. 不同node,不同pod间的网络通信

6. Flannel扁平网络

7. Pod如何对外暴露服务

7.1 hostNetwork: true模式

7.2 hostPort模式

7.3 NodePort模式

7.4 LoadBalancer模式

7.5 Ingress与Ingress Controller模式

8. port、containerPort、nodePort、hostPort、targetPort的区别

9. Pod的dns域名解析流程

9.1 如何为Pod创建dns记录

参考文档

导语

  之前对kubernetes网络通信的认识都比较摸棱两可,本文从linux网络空间开始循序渐进地整理kubernetes网络的知识脉络,意在彻底理解其网络通信原理。文中所用的图片均来源于网络,由于比较分散,就没有将参考文档一一列举出来。

1. linux网络命名空间

  network namespace 是实现网络虚拟化的重要功能,它能创建多个隔离的网络空间,它们有独自的网络栈信息。不管是虚拟机还是容器,运行的时候仿佛自己就在独立的网络中。对于每个 network namespace 来说,它会有自己独立的网卡、路由表、arp表、iptables 等和网络相关的资源。相关的主要命令如下:

ip netns add net1
ip netns show
ip netns exec net1 ip addr

2. 不同网络命名空间的通信

  为了实现不同 network namespace 之间的网络通信,linux提供了veth pair,可以将veth pair当做一对虚拟网卡,这对虚拟网卡连通着不同的网命名络空间。创建veth pair实现不同网络命名空间通信的步骤如下:

// 创建2个网络命名空间
[jettchen@k8s-master01 ~]$ sudo ip netns add net1
[jettchen@k8s-master01 ~]$ sudo ip netns add net2

// 创建veth pair
[jettchen@k8s-master01 ~]$ sudo ip link add name veth1 type veth peer name veth2

// 将veth pair的2个虚拟接口挪到新创建的2个网络命名空间中
[jettchen@k8s-master01 ~]$ sudo ip link set veth1 netns net1
[jettchen@k8s-master01 ~]$ sudo ip link set veth2 netns net2

// 启动虚拟接口,并给veth pair配置ip地址,注意这里2个虚拟接口的ip必须是同网段的
[jettchen@k8s-master01 ~]$ sudo ip netns exec net1 ip link set veth1 up
[jettchen@k8s-master01 ~]$ sudo ip netns exec net2 ip link set veth2 up
[jettchen@k8s-master01 ~]$ sudo ip netns exec net1 ip addr add 20.1.1.1/24 dev veth1
[jettchen@k8s-master01 ~]$ sudo ip netns exec net2 ip addr add 20.1.1.2/24 dev veth2

// veth pair接口配置的网络测试结果
[jettchen@k8s-master01 ~]$ sudo ip netns exec net1 ping -c 3 20.1.1.2
PING 20.1.1.2 (20.1.1.2) 56(84) bytes of data.
64 bytes from 20.1.1.2: icmp_seq=1 ttl=64 time=0.033 ms
64 bytes from 20.1.1.2: icmp_seq=2 ttl=64 time=0.028 ms
64 bytes from 20.1.1.2: icmp_seq=3 ttl=64 time=0.033 ms

2.1 多个网络命名空间的通信

  不同namespace之间,直接通过veth pair相连,如下所示。这种方式的缺点就是:当namespace很多时,veth pair数量呈n^2趋势增长,总数量为n*(n - 1) / 2。

循序渐进地学习kubernetes网络_Pod

  通过linux bridge,相当于用一个交换机中转两个 namespace 的流量,如下所示。这种方式就可以将veth pair的数量控制在n,n为网络命名空间的数量。

循序渐进地学习kubernetes网络_命名空间_02

3. pod内部容器的网络通信

  在 k8s中,每个 Pod 有一个 pause 容器,这个pause容器会创建独立的网络命名空间,在 Pod 内启动其他 Docker 容器的时候使用 –net=container 就可以让当前 Docker 容器加入到 Pod 拥有的网络命名空间(pause 容器)。由于Pod内的所有容器共享同一个网络空间,所以它们可以直接通过自身网络空间的loopback接口通信。这也是在规划 k8s 调度的时候,尽量把关系紧密的服务放到同一个 pod 内的原因,这样网络请求的耗时就可以忽略,因为容器之间的通信在一个网络空间内,就像 local 本地通信一样。

循序渐进地学习kubernetes网络_Pod_03

4. 同一个node,不同pod间的网络通信

  不同的Pod,其网络命名空间也就不同,因此这个时候的通信就相当于前面讲的不同网络空间的通信,如下所示。大致实现是:通过虚拟网桥与各个Pod间建立veth pair。各个Pod的IP是由虚拟网桥分配的,这些Pod都连在同一个网桥上,在同一个网段内,它们可以进行IP寻址和互通。

循序渐进地学习kubernetes网络_命名空间_04

5. 不同node,不同pod间的网络通信

循序渐进地学习kubernetes网络_nginx_05

  Node1 中的 Pod1 与 Node2 的 Pod4 进行通信的流程:

  1. 首先 Pod1 通过自己的以太网设备 eth0 把数据包发送到关联到 root 命名空间的 veth0 上。
  2. 然后数据包被 Node1 上的网桥设备接收到,网桥查找转发表发现找不到 Pod4 的 Mac 地址,则会把包转发到默认路由(root 命名空间的 eth0 设备)。
  3. 然后数据包经过 eth0 就离开了 Node1,被发送到网络。
  4. 数据包到达 Node2 后,首先会被 root 命名空间的 eth0 设备接收。
  5. 然后通过网桥把数据路由到虚拟设备 veth1,最终数据包会被流转到与 veth1 配对的另外一端(Pod4 的 eth0)。

6. Flannel扁平网络

  对于扁平网络,kubernetes本身没有具体实现,而是通过提供CNI接口供开源社区实现。这里介绍下CoreOS的Flannel在kubernetes上实现CNI的大致原理,如下所示。

循序渐进地学习kubernetes网络_Pod_06

  Flannel0作为overlay网络的设备,用来进行 数据报文的封包和解包。不同node之间的pod数据流量都从overlay设备以隧道的形式发送到对端。

  Flanneld作为agent进程运行在每个主机上,它会为所在主机从集群的网络地址空间中,获取一个小的网段subnet,本主机内所有Pod的IP地址都将从中分配,这样就可以保证集群内各个Pod的IP是唯一的。同时Flanneld监听K8s集群的数据库etcd,为Flannel0设备提供封装数据时必要的mac、ip等网络数据信息。数据包的具体转发流程是:

  1. Pod中产生数据,根据Pod的路由信息,将数据发送到docker0网桥。
  2. docker0网桥根据节点的路由表,将数据发送到隧道设备Flannel0。
  3. Flannel0查看数据包的目的ip,从Flanneld获得对端隧道设备的必要信息,封装数据包。
  4. Flannel0将数据包发送到对端设备。对端节点的网卡接收到数据包,发现数据包为overlay数据包,解开外层封装,并发送内层封装到Flannel0设备。
  5. Flannel0设备查看数据包,根据路由表匹配,将数据发送给docker0网桥。
  6. docker0网桥匹配路由表,发送数据给网桥上对应的端口。

7. Pod如何对外暴露服务

7.1 hostNetwork: true模式

  这种网络模式 ,相当于 docker run --net=host,采用宿主机的网络命名空间。此时,Pod 的ip 为 node 节点的ip,Pod的端口需要保持不与宿主机上的port 端口发生冲突。

循序渐进地学习kubernetes网络_nginx_07

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      hostNetwork: true
      containers:
        - name: nginx
          image: nginx:stable
          command: ["nginx", "-g", "daemon off;"]

[jettchen@k8s-master01 ~/pubgm/k8s]$ sudo ps aux| grep nginx
root     20126  0.0  0.0  10640  3512 ?        Ss   10:17   0:00 nginx: master process nginx -g daemon off;
101      20138  0.0  0.0  11044  1524 ?        S    10:17   0:00 nginx: worker process
101      20139  0.0  0.0  11044  1524 ?        S    10:17   0:00 nginx: worker process
101      20140  0.0  0.0  11044  1524 ?        S    10:17   0:00 nginx: worker process
101      20141  0.0  0.0  11044  1524 ?        S    10:17   0:00 nginx: worker process

[jettchen@k8s-master01 ~/pubgm/k8s]$ sudo netstat -lpn| grep ":80 "
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      20126/nginx: master

  如上配置,可以直接通过curl {node_ip}/index.html访问nginx,并且由于Pod采用的是宿主机的网络命名空间,因此宿主机可以直接看到nginx master进程正在监听80端口。

7.2 hostPort模式

  hostPort是将容器端口与宿主节点上的端口建立映射关系,这样用户就可以通过宿主机的 IP 加上来访问 Pod 了。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:stable
          command: ["nginx", "-g", "daemon off;"]
          ports: 
            - containerPort: 80
              hostPort: 81

[jettchen@k8s-master01 ~/pubgm/k8s/tlog]$ kube get pod -o wide|grep nginx
nginx-deployment-ff549cb9d-5t4bp        1/1     Running   0          5m8s    10.244.0.40   k8s-master01   <none>           <none>

[jettchen@k8s-master01 ~/pubgm/k8s/tlog]$ sudo iptables -L -nv -t nat| grep "0.244.0.40:80"
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:81 to:10.244.0.40:80

  如上配置,既可以通过curl 10.244.0.40/index.html访问nginx,也可以通过curl {node_ip}:81/index.html访问nginx。可以看到iptables会添加一条DNAT规则,将宿主机的81端口映射到Pod的80端口。

7.3 NodePort模式

  NodePort 在 Kubernetes 里是一个广泛应用的服务暴露方式。Kubernetes 中的 service 默认情况下都是使用的ClusterIP这种类型,这样的 service 会产生一个 ClusterIP,这个 IP 只能在集群内部访问,要想让外部能够直接访问 service,需要将 service type 修改为nodePort

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      nodeSelector:
        nginx: ok
      containers:
        - name: nginx
          image: nginx:stable
          command: ["nginx", "-g", "daemon off;"]
	  
kind: Service
apiVersion: v1
metadata:
  name: nginx-svc
spec:
  type: NodePort
  ports:
    - port: 80
      nodePort: 30001
  selector:
    name: nginx
	
[jettchen@k8s-master01 ~/pubgm/k8s]$ kube get svc -o wide
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE    SELECTOR
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        176d   <none>
nginx-svc    NodePort    10.110.21.50   <none>        80:30001/TCP   26s    app=nginx

[jettchen@k8s-master01 ~/pubgm/k8s]$ kube get pod -o wide
NAME                                    READY   STATUS    RESTARTS   AGE   IP            NODE           NOMINATED NODE   READINESS GATES
clubproxy-deployment-55c958bf67-cg424   1/1     Running   0          11h   10.244.0.56   k8s-master01   <none>           <none>
nginx-deployment-c9fd6b54f-bdr8l        1/1     Running   0          18s   10.244.0.58   k8s-master01   <none>           <none>

[jettchen@k8s-master01 ~/pubgm/k8s]$ sudo netstat -lpn | grep 30001
tcp        0      0 0.0.0.0:30001           0.0.0.0:*               LISTEN      15954/kube-proxy

  如上配置,可以通过{pod_ip}/index.html、{cluster_ip}/index.html、{node_ip}:30001/index.html访问nginx。node节点上可以看到kube-proxy进程在监听30001端口,用于接收从主机网络进入的外部流量。最后,通过iptables规则将{node_ip}:30001映射到{pod_ip}:容器端口。

7.4 LoadBalancer模式

kind: Service
apiVersion: v1
metadata:
  name: nginx-svc
spec:
  type: LoadBalancer
  ports:
    - port: 80
      nodePort: 30001
  selector:
    app: nginx
	
[jettchen@k8s-master01 ~/pubgm/k8s]$ kube get svc -o wide
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE     SELECTOR
kubernetes   ClusterIP      10.96.0.1      <none>        443/TCP        176d    <none>
nginx-svc    LoadBalancer   10.97.214.72   <pending>     80:30001/TCP   2m30s   app=nginx

LoadBalancer只能在 service 上定义。这是公有云提供的负载均衡器,如果成功分配了EXTERNAL-IP(这是一个 VIP,是云供应商提供的负载均衡器 IP),则外部可以通过EXTERNAL-IP:30001来访问nginx。当然,此时也可以通过{pod_ip}/index.html、{cluster_ip}/index.html、{node_ip}:30001/index.html访问nginx。

7.5 Ingress与Ingress Controller模式

  将 k8s 集群中服务暴露给集群外访问,最简单的方式莫过于使用 NodePort,定义 NodePort 类型的 Service 后,即可通过集群中任意节点的 IP 加 nodePort 指定的端口访问到 k8s 集群中的服务。但随着服务的增多,使用 NodePort 访问的问题也会逐渐显现出来:可用作 NodePort 的端口是一个有限的范围、不容易记忆、不好管理。可以在集群内部署一个 Nginx 服务,NodePort 暴露 Nginx 的端口,再由 Nginx 代理访问集群内的服务。在 k8s 中也有这样的一个资源,能够起到与这个 Nginx 类似的作用,即Ingress。Ingress的资源配置文件如下所示。这个 Ingress 资源表示www.tchua.top域名下的请求都会转发给名为myapp-svc,端口为80的服务。可以将 Ingress 狭义的理解为Nginx 中的配置文件nginx.conf。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-myapp
  namespace: default
  annotations: 
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: www.tchua.top 
    http:
      paths:
      - path: /
        backend:
          serviceName: myapp-svc
          servicePort: 80

  很显然,只有 Nginx 的配置文件,是起不到转发请求的作用的,必须还要有 Nginx 程序。同样,仅创建 Ingress 资源本身是没有任何作用的,还需要部署 Ingress Controller。Ingress Controller 的作用就相当于是 Nginx 服务,实际上,k8s 官方支持和维护的三个 Ingress Controller 里,就有基于 nginx 实现的 ingress-nginx,另外两个是 AWS 和 GCE

  不同类型的 Ingress Controller 对应的 Ingress 配置通常也是不同的,当集群中存在多于一个的 Ingress Controller 时,就需要为 Ingress 指定对应的 Controller 是哪个。Kubernetes 从1.18版本开始提供了一个 IngressClass资源,以此关联对应的 Ingress Controller。然后,在Ingress 资源文件中通过 spec.ingressClassName 属性用来指定对应的 IngressClass,进而由 IngressClass 关联到对应的 Ingress Controller。

  除了可能会有多个不同类型的 Ingress Controller 之外,还可能存在多个相同类型的 Ingress Controller,比如部署了两个 Nginx Ingress Controller,一个负责处理外网访问,一个负责处理内网访问。此时也可以通过上面的方式,为每个 Controller 设定唯一的一个Ingress Class。

8. port、containerPort、nodePort、hostPort、targetPort的区别

  • port是暴露在cluster ip上的端口,port提供了集群内部访问service的入口,即 clusterIP:port
  • containerPort是在pod控制器中定义的、pod中的容器需要暴露的端口。
  • nodePort 提供了集群外部访问 Service 的一种方式,外部可通过nodeIP:nodePort访问集群内的服务。
  • hostPort是直接将宿主机的端口映射到容器端口,这样外部就可以通过宿主机的IP:hostPort访问Pod了。
  • targetPort是pod上的端口,从port/nodePort上来的数据,经过kube-proxy流入到后端pod的targetPort上,最后进入容器。
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: NodePort            // 配置NodePort,外部流量可访问k8s中的服务
  ports:
  - port: 30080             // 服务访问端口,集群内部访问的端口
    targetPort: 80          // pod控制器中定义的端口(应用访问的端口)
    nodePort: 30001         // NodePort,外部客户端访问的端口
  selector:
    name: nginx-pod

9. Pod的dns域名解析流程

  在Pod内部,域名解析依赖配置文件/etc/resolv.conf。如下所示,Pod的dns服务器指向kube-dns这个service。

[jettchen@k8s-master01 ~/pubgm/k8s]$ kube get svc -n kube-system
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   176d

[jettchen@k8s-master01 ~/pubgm/k8s]$ kube exec -it nginx-deployment-c9fd6b54f-bdr8l -- /bin/sh
# cat /etc/resolv.conf 
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

  Kubernetes中,域名的全称必须是 service-name.namespace.svc.cluster.local 这种模式,其中service-name字段值就是Kubernetes中 Service 的名称。比如我们执行curl svc-app命令的时候,会按配置文件中search配置项中的 svc-app.default.svc.cluster.local -> svc-app.svc.cluster.local -> svc-app.cluster.local 顺序解析域名,如果解析不到则将svc-app当作全域名再解析一遍。

  ndots:5,表示:如果查询的域名包含的点 '.',不到5个,那dns解析的时候会先走search配置项流程,解析不到再走全域名解析。如果你查询的域名包含点数大于等于5,那么dns解析的时候先走全域名解析,解析不到才走search配置项流程。

  有一种可以绕过search配置项流程的办法是:输入的service-name以 '.' 结尾,比如svc-app.。

9.1 如何为Pod创建dns记录

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      nodeSelector:
        nginx: ok
      hostname: nginx-host
      subdomain: nginx-subdomain
      containers:
        - name: nginx
          image: nginx:stable
          command: ["nginx", "-g", "daemon off;"]
		  
kind: Service
apiVersion: v1
metadata:
  name: nginx-subdomain
spec:
  type: ClusterIP
  ports:
    - port: 80
      name: nginx
  selector:
    app: nginx

  当Pod配置hostname和subdomain,并且service的metadata.name的字段值与Pod的subdomain一致的时候,此时就不仅会针对service生成一条dns记录,还会针对Pod生成一条dns记录。结果如下:

[jettchen@k8s-master01 ~/pubgm/k8s/clubproxy]$ kube get pod -o wide
NAME                                    READY   STATUS    RESTARTS   AGE    IP            NODE           NOMINATED NODE   READINESS GATES
nginx-deployment-5f6778cf77-7sv8p       1/1     Running   0          15h    10.244.0.68   k8s-master01   <none>           <none>


[jettchen@k8s-master01 ~/pubgm/k8s/clubproxy]$ kube get svc -o wide
NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE    SELECTOR
nginx-subdomain   ClusterIP   10.97.33.219   <none>        80/TCP    15h    app=nginx

// 10.96.0.10为kube-dns这个service的cluster_ip
[jettchen@k8s-master01 ~/pubgm/k8s/clubproxy]$ nslookup nginx-host.nginx-subdomain.default.svc.cluster.local 10.96.0.10 
Server:         10.96.0.10
Address:        10.96.0.10#53

Name:   nginx-host.nginx-subdomain.default.svc.cluster.local
Address: 10.244.0.68

[jettchen@k8s-master01 ~/pubgm/k8s/clubproxy]$ nslookup nginx-subdomain.default.svc.cluster.local 10.96.0.10 
Server:         10.96.0.10
Address:        10.96.0.10#53

Name:   nginx-subdomain.default.svc.cluster.local
Address: 10.97.33.219

  另外,如果service配置成Headless Service,那么对于service的dns域名解析,会直接解析到Pod的ip。