zoukankan      html  css  js  c++  java
  • kubernetes(十九) 网络

    前言

    本文通过两个简单的服务之间的访问,结合tcpdump抓包,详细分析下在IPVS模式下,kubernetes实现通过服务名称访问NodePort、ClusterIp类型的service的原理。

    当然kubernetes网络实现牵扯到很多知识,特别是对Linux低层的模块的各种调用,如果对Linux中的网络命名空间、eth设备对、网桥等模块不熟悉的话,可以先参考下另一篇文章[Docker 网络](Docker 网络.md),之后也可以看下另一篇文件[Kubernetes kube-proxy](Kubernetes kube-proxy详解.md)来了解下kube-proxy的IPVS模式

    本文环境基于flannel网络插件,具体搭建参考kubernetes安装-二进制

    集群环境

    角色 系统 CPU Core 内存 主机名称 ip 安装组件
    master 18.04.1-Ubuntu 4 8G master 192.168.0.107 kubectl,kube-apiserver,kube-controller-manager,kube-scheduler,etcd,flannald,kubelet,kube-proxy
    slave 18.04.1-Ubuntu 4 4G slave 192.168.0.114 docker,flannald,kubelet,kube-proxy,coredns

    拓扑图

    节点路由信息

    1. master节点

      $ route -n -v
      内核 IP 路由表
      目标            网关            子网掩码        标志  跃点   引用  使用 接口
      0.0.0.0         192.168.0.1     0.0.0.0         UG    600    0        0 wlp3s0
      169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 wlp3s0
      172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 br-471858815e83
      172.30.22.0     0.0.0.0         255.255.255.0   U     0      0        0 docker0
      172.30.78.0     172.30.78.0     255.255.255.0   UG    0      0        0 flannel.1
      192.168.0.0     0.0.0.0         255.255.255.0   U     600    0        0 wlp3s0
      	
      
    2. slave节点

      route -v -n
      内核 IP 路由表
      目标            网关            子网掩码        标志  跃点   引用  使用 接口
      0.0.0.0         192.168.0.1     0.0.0.0         UG    600    0        0 wlo1
      169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 wlo1
      172.30.22.0     172.30.22.0     255.255.255.0   UG    0      0        0 flannel.1
      172.30.78.0     0.0.0.0         255.255.255.0   U     0      0        0 docker0
      192.168.0.0     0.0.0.0         255.255.255.0   U     600    0        0 wlo1
      	
      

    镜像准备

    1. web镜像

      用spring boot启动了一个web服务,监听8080端口,里面提供一个方法 /header/list,调用这个方法后,会把调用者地址相关信息输出出来

      @RequestMapping("/header/list")
      public String listHeader(HttpServletRequest request) {
      	
      	log.info("host is" + request.getHeader("host"));
      	
      	log.info("remoteAddr is " + request.getRemoteHost());
      	
      	log.info("remotePort is " + request.getRemotePort());
      	
      	return "OK";
      }
      
      
    2. curl镜像

      基于 alpine镜像,只安装了一个curl命令,使我们可以通过这个命令访问web服务

      FROM alpine:latest
      RUN apk update
      RUN apk add --upgrade curl
      
      

    为节点添加label

    为了控制pod启动到指定的节点完成下面的分析,给两个节点分别添加不同的label

    $ kubectl label nodes master sample=master
    node/master labeled
    	
    $ kubectl label nodes slave sample=slave
    node/slave labeled
    	
    

    例子1

    web服务的curl服务对应的pod都在master节点上,由拓扑图可知,此次访问通信只用经过master 节点上的docker0网桥即可实现

    1. 编写web服务启动文件

      $ cat > web.yml <<EOF
      apiVersion: v1
      kind: Service
      metadata:
        name: clientip
      spec:
              #type: NodePort
        selector:
          app: clientip
        ports:
        - name: http
          port: 8080
          targetPort: 8080
          #nodePort: 8086
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: clientip-deployment
      spec:
        selector:
          matchLabels:
            app: clientip
        replicas: 1
        template:
          metadata:
            labels:
              app: clientip
          spec:
            nodeSelector:
              sample: master
            containers:
            - name: clientip
              image: 192.168.0.107/k8s/client-ip-test:0.0.2
              ports:
              - containerPort: 8080
            
      EOF
           
      
    2. 编写启动 curl pod的文件

      $ cat > pod_curl.yml <<EOF
      apiVersion: v1
      kind: Pod
      metadata:
        name: curl
      spec:
        containers:
        - name: curl
          image: 192.168.0.107/k8s/curl:1.0
          command:
            - sleep
            - "3600"
        nodeSelector:
          sample: master
      EOF
      	
      
    3. 启动服务

      $ kubectl create -f web.yml -f pod_curl.yml
      	
      $ kubectl get pod -o wide
      NAME                                   READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
      clientip-deployment-5d8b5dcb46-qprps   1/1     Running   0          4s    172.30.22.4   master   <none>           <none>
      curl                                   1/1     Running   0          9s    172.30.22.3   master   <none>           <none>
      	
      $ kubectl get svc
      NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
      clientip     ClusterIP   10.254.0.30   <none>        8080/TCP   51s
      kubernetes   ClusterIP   10.254.0.1    <none>        443/TCP    25d
      
      

      可以看到,两个服务服务都正常启动起来,并启动在master节点上

    4. 启动监听master节点上docker0、flannel.1设备

      $ tcpdump -n -vv -i docker0
      	
      $ tcpdump -n -vv -i flannel.1 
      	
      
      1. 在curl 容器中访问clientip 这个web服务
      $ kubectl exec -it curl curl http://clientip:8080/header/list
      OK
      	
      
    5. 监控日志分析

      1. web 服务日志

        2020-03-06 08:29:05.447  INFO 6 --- [nio-8080-exec-1] c.falcon.clientip.ClientIpController     : host isclientip:8080
        2020-03-06 08:29:05.447  INFO 6 --- [nio-8080-exec-1] c.falcon.clientip.ClientIpController     : remoteAddr is 172.30.22.3
        2020-03-06 08:29:05.447  INFO 6 --- [nio-8080-exec-1] c.falcon.clientip.ClientIpController     : remotePort is 42000
        	
        
        • 请求remoteAddr IP 172.30.22.3对应curl pod的IP地址
      2. docker0网络监控(只摘录了主要流程的日志)

        172.30.22.3.47980 > 10.254.0.2.53: [bad udp cksum 0xcd6e -> 0xdae6!] 22093+ A? clientip.default.svc.cluster.local. (52) 
        ...
        	
        10.254.0.2.53 > 172.30.22.3.47980: [udp sum ok] 22093*- q: A? clientip.default.svc.cluster.local. 1/0/0 clientip.default.svc.cluster.local. A 10.254.0.30 (102)
        	
        ...
        	
        172.30.22.3.42000 > 10.254.0.30.8080: Flags [P.], cksum 0xcdbb (incorrect -> 0x95b1), seq 0:88, ack 1, win 507, options [nop,nop,TS val 3200284558 ecr 1892112994], length 88: HTTP, length: 88
        GET /header/list HTTP/1.1
        Host: clientip:8080
        User-Agent: curl/7.67.0
        Accept: */*
           
        ...
        172.30.22.3.42000 > 172.30.22.4.8080: Flags [P.], cksum 0x84c2 (incorrect -> 0xdeaa), seq 1:89, ack 1, win 507, options [nop,nop,TS val 3200284558 ecr 1892112994], length 88: HTTP, length: 88
        GET /header/list HTTP/1.1
        Host: clientip:8080
        User-Agent: curl/7.67.0
        Accept: */*
            
        ...
           
        172.30.22.4.8080 > 172.30.22.3.42000: Flags [P.], cksum 0x84dd (incorrect -> 0xe64b), seq 1:116, ack 89, win 502, options [nop,nop,TS val 1892113104 ecr 3200284558], length 115: HTTP, length: 115
        HTTP/1.1 200
        Content-Type: text/plain;charset=UTF-8
        Content-Length: 2
        Date: Fri, 06 Mar 2020 08:29:05 GMT
        
        OK[!http]
        
        
        • 第一条 通过47980端口向DNS服务器发起解析域名clientip.default.svc.cluster.local的请求
        • 第二条 DNS服务解析出clientip.default.svc.cluster.local对应的IP是10.254.0.30
        • 第三条通过42000端口 向10.254.0.30:8080 发出请求
        • 请求10.254.0.30:8080在input链上被IPVS匹配,因为10.254.0.30是service的ClusterIp,IPVS匹配成功,采用NAT机制将目的地址转换成172.30.22.4.8080,进入postrouting,master节点上的路由信息发现发往172.30.22.4的请求还是通过docker0网络设备发送,所以在docker0上又收到了第四条记录,即向真实服务172.30.22.4.8080发起的请求
        • 第五条记录 真实的web服务172.30.22.4在完成处理后直接将结果返回到了172.30.22.3中,没有经过IPVS的mssq
      3. 观察flannel.1设备的输出,此时是不会出现和请求172.30.22.4相关的信息,此处略去

    例子2

    curl服务对应的pod在master节点上,web服务对应的pod在slave节点上,由拓扑图可知,这时要完成从curl的pod内部访问到web服务依次要经过 master.docker0->master.flannel.1->master.wlp3s0->slave.wlo1->slave.flannel.1->slave.docker0

    1. 修改web的启动文件,将nodeSelector的值修改成sample=slave,重新启动web应用

      $ kubectl get pod -o wide
      NAME                                   READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
      clientip-deployment-68c57b7965-pmwp2   1/1     Running   0          33s   172.30.78.3   slave    <none>           <none>
      curl                                   1/1     Running   0          48m   172.30.22.3   master   <none>           <none>
      
      $ kubectl get svc
      NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
      clientip     ClusterIP   10.254.167.63   <none>        8080/TCP   94s
      kubernetes   ClusterIP   10.254.0.1      <none>        443/TCP    25d
      
      
    2. 日志监控

      1. 监控web服务的日志

        $ kubectl logs -f clientip-deployment-68c57b7965-pmwp2
        
        
      2. 监控master各个网络设备的日志

        $ tcpdump -n -vv -i docker0
        $ tcpdump -n -vv -i flannel.1
        $ tcpdump -n -vv -i wlp3s0
        
        
      3. 监控slave节点各个网络设备日志

        
        $ tcpdump -n -vv -i docker0
        $ tcpdump -n -vv -i flannel.1
        $ tcpdump -n -vv -i wlo1
        
        
        
    3. 监控日志分析

      1. web日志

        2020-03-07 11:13:22.384  INFO 6 --- [nio-8080-exec-3] c.falcon.clientip.ClientIpController     : host isclientip:8080
        2020-03-07 11:13:22.384  INFO 6 --- [nio-8080-exec-3] c.falcon.clientip.ClientIpController     : remoteAddr is 172.30.22.3
        2020-03-07 11:13:22.384  INFO 6 --- [nio-8080-exec-3] c.falcon.clientip.ClientIpController     : remotePort is 51596
        
        
        • 对应的远端IP是172.30.22.3,就是我们发起请求的curl pod对应的IP
      2. master网络设备的日志分析(只展示主要流程,tcp握手过程略去)

        1. docker0设备

          ...
          11:13:22.346481 IP (tos 0x0, ttl 64, id 28047, offset 0, flags [DF], proto UDP (17), length 80)
          	172.30.22.3.35482 > 10.254.0.2.53: [bad udp cksum 0xcd6e -> 0x55df!] 3111+ A? clientip.default.svc.cluster.local. (52)
          ...
          11:13:22.355447 IP (tos 0x0, ttl 62, id 34179, offset 0, flags [DF], proto UDP (17), length 130)
              10.254.0.2.53 > 172.30.22.3.35482: [udp sum ok] 3111*- q: A? clientip.default.svc.cluster.local. 1/0/0 clientip.default.svc.cluster.local. A 10.254.167.63 (102)
          ...
          11:13:22.359009 IP (tos 0x0, ttl 64, id 23895, offset 0, flags [DF], proto TCP (6), length 140)
              172.30.22.3.51596 > 10.254.167.63.8080: Flags [P.], cksum 0x74dd (incorrect -> 0x0f66), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88
                  GET /header/list HTTP/1.1
                  Host: clientip:8080
                  User-Agent: curl/7.67.0
                  Accept: */*
          ...
          11:13:22.372907 IP (tos 0x0, ttl 62, id 63303, offset 0, flags [DF], proto TCP (6), length 167)
              10.254.167.63.8080 > 172.30.22.3.51596: Flags [P.], cksum 0x077c (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115
                  HTTP/1.1 200
                  Content-Type: text/plain;charset=UTF-8
                  Content-Length: 2
                  Date: Sat, 07 Mar 2020 03:13:22 GMT
          
                  OK[!http]
          
          
          • 第一条 向DNS服务器发起解析域名clientip.default.svc.cluster.local的请求
          • 第二条 DNS服务解析出clientip.default.svc.cluster.local对应的IP是10.254.167.63
          • 第三条 向10.254.167.63:8080 发出请求
          • 第四条记录 从10.254.167.63:8080返回的信息传递给了172.30.22.3.51596

          返回信息是从10.254.167.63:8080发回来的,和发出去的路径是一致的,在返回时IPVS的masq(SNAT),将真实服务器地址转换成了虚拟地址

        2. flannel.1网络设备

          11:13:22.359020 IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140)
              172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xbcc1 (incorrect -> 0xc781), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88
                  GET /header/list HTTP/1.1
                  Host: clientip:8080
                  User-Agent: curl/7.67.0
                  Accept: */*
          ...
          11:13:22.372887 IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167)
              172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbf97 (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115
                  HTTP/1.1 200
                  Content-Type: text/plain;charset=UTF-8
                  Content-Length: 2
                  Date: Sat, 07 Mar 2020 03:13:22 GMT
          
                  OK[!http]        
          
          
          • 第一条172.30.22.3发出的请求,通过51596端口,向真实的服务器172.30.78.3.8080发起请求,
          • 第二条,真实的服务器返回响应信息给172.30.22.3.51596
        3. wlp3s0网卡(物理网卡)

          ...
          11:13:22.359026 IP (tos 0x0, ttl 64, id 22491, offset 0, flags [none], proto UDP (17), length 190)
              192.168.0.107.33404 > 192.168.0.114.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1
          IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140)
              172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88
                  GET /header/list HTTP/1.1
                  Host: clientip:8080
                  User-Agent: curl/7.67.0
                  Accept: */* 
          ...
          11:13:22.372815 IP (tos 0x0, ttl 64, id 57065, offset 0, flags [none], proto UDP (17), length 217)
              192.168.0.114.43021 > 192.168.0.107.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1
          IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167)
              172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbf97 (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115
                  HTTP/1.1 200
                  Content-Type: text/plain;charset=UTF-8
                  Content-Length: 2
                  Date: Sat, 07 Mar 2020 03:13:22 GMT
          
                  OK[!http]
          
          
          • 第一条 172.30.22.3.51596向172.30.78.3.8080发出的请求,封装到了udp数据包的内部,通过物理网卡通道192.168.0.107.33404 > 192.168.0.114.8472进行传输
          • 第二条 从172.30.78.3.8080向172.30.22.3.51596的返回信息,封装到了udp数据包的内部,通过物理网卡通道192.168.0.114.43021 > 192.168.0.107.8472进行传输
      3. master节点上数据传输总结(通过抓包中的时间分析出数据到达各个设备的先后顺序,红色方块curl开始)

        1. 发送流程
          1. 发起请求的pod向DNS服务器发出请求,查找clientip对应的IP地址
          2. 找到IP后向对应的地址发送真实的请求
          3. 因为这个IP地址是一个service的ClusterIP,会绑定到kubernetes为每一台节点机器创建的dummy设备kube-ipvs0上,所以宿主机会认为这是一个本机IP,进入内核的input链
          4. IPVS在input链上对这个ClusterIP进行判断,发现是一个集群服务,会执行DNAT,找到一个真实的后端服务(172.30.78.3.8080),将请求目的地址转换成这个真实服务,之后将请求跳转到内核的POSTROUTING链上
          5. 在路由选择阶段,根据master节点的路由规则,发现发往172.30.78.0/24的请求要经过flannel.1网络设备,所以flannel.1网络设备中有了172.30.22.3.51596 > 172.30.78.3.8080的请求信息
          6. flannel1.1网络设备,通过flanneld,查找到172.30.78.3.8080所在的物理节点,将数据包重新包装成,追加outerIp、port信息(192.168.0.114.8472)
          7. 此时再根据路由规则,发往192.168.0.114的请求需经过wlp3s0网卡,所以wlp3s0上收到了192.168.0.107.33404 > 192.168.0.114.8472的请求包,请求正式发送出去
        2. 接收流程
          1. wlp3s0网卡上接收到192.168.0.114.43021返回的信息
          2. 因为是一个vxlan格式的数据包,所以会丢个flanneld处理,将outerIp、Port信息去除,得到内部的tcp请求信息172.30.78.3.8080 > 172.30.22.3.51596
          3. 之后flanneld发请求信息转发送给flannel1.1网络设备,所以flannel1.1网络设备上我们能监听到172.30.78.3.8080 > 172.30.22.3.51596的数据包。当数据到达INPUT时,IPVS开始工作,此时IPVS判断出此报文是之前发出请求的响应,继而进行SNAT(在IPVS源码块的handle_response,对于tcp协议是tcp_snat_handler函数中处理),将返回请求的源地址转换成真实服务对应的虚拟服务地址,即10.254.167.63.8080,之后使用函数ip_vs_route_me_harder进行重新路由,
          4. 根据路由规则,发往172.30.22.3.51596的数据包,要经过docker0网络设备,所以我们在docker0设备上看到了10.254.167.63.8080 > 172.30.22.3.51596的数据包
    4. slave节点上网络设备的日志分析(只展示主要流程,tcp握手过程略去)

      1. docker0设备

        ...
        11:13:22.379401 IP (tos 0x0, ttl 62, id 23895, offset 0, flags [DF], proto TCP (6), length 140)
            172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88
                GET /header/list HTTP/1.1
        ...
        11:13:22.389173 IP (tos 0x0, ttl 64, id 63303, offset 0, flags [DF], proto TCP (6), length 167)
            172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbcdc (incorrect -> 0xbf97), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115
                HTTP/1.1 200
        
        
        
        • 第一条172.30.22.3发出的请求,通过51596端口,向真实的服务器172.30.78.3.8080发起请求,
        • 第二条,真实的服务器返回响应信息给172.30.22.3.51596
      2. flannel.1设备

        11:13:22.379392 IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140)
            172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88
                GET /header/list HTTP/1.1
                Host: clientip:8080
                User-Agent: curl/7.67.0
                Accept: */*
         ...
         
        11:13:22.389192 IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167)
            172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbcdc (incorrect -> 0xbf97), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115
                HTTP/1.1 200
                Content-Type: text/plain;charset=UTF-8
                Content-Length: 2
                Date: Sat, 07 Mar 2020 03:13:22 GMT
        
                OK[!http]
        
        • 第一条172.30.22.3发出的请求,通过51596端口,向真实的服务器172.30.78.3.8080发起请求,
        • 第二条,真实的服务器返回响应信息给172.30.22.3.51596
      3. wlo1网卡

        11:13:22.379300 IP (tos 0x0, ttl 64, id 22491, offset 0, flags [none], proto UDP (17), length 190)
            192.168.0.107.33404 > 192.168.0.114.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1
        IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140)
            172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88
                GET /header/list HTTP/1.1
                Host: clientip:8080
                User-Agent: curl/7.67.0
                Accept: */*
                
        ...
        11:13:22.389223 IP (tos 0x0, ttl 64, id 57065, offset 0, flags [none], proto UDP (17), length 217)
            192.168.0.114.43021 > 192.168.0.107.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1
        IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167)
            172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbf97 (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115
                HTTP/1.1 200
                Content-Type: text/plain;charset=UTF-8
                Content-Length: 2
                Date: Sat, 07 Mar 2020 03:13:22 GMT
        
                OK[!http] 
        ...
        
        
        • 第一条 172.30.22.3.51596向172.30.78.3.8080发出的请求,封装到了udp数据包的内部,通过物理网卡通道192.168.0.107.33404 > 192.168.0.114.8472进行传输
        • 第二条 从172.30.78.3.8080向172.30.22.3.51596的返回信息,封装到了udp数据包的内部,通过物理网卡通道192.168.0.114.43021 > 192.168.0.107.8472进行传输
      4. slave节点上响应请求过程(通过抓包中的时间分析出数据到达各个设备的先后顺序,红色方块请求进入为起始点)

        1. wlo1网卡上接收到192.168.0.107.33404的请求信息
        2. 因为是一个vxlan格式的数据包,所以会丢个flanneld处理,将outerIp、Port信息去除,得到内部的tcp请求信息172.30.22.3.51596 > 172.30.78.3.8080
        3. 之后flanneld发请求信息转发送给flannel1.1网络设备,所以flannel1.1网络设备上我们能监听到172.30.22.3.51596 > 172.30.78.3.8080的数据包,
        4. flannel1.1进行路由选择,根据路由规则,发送给172.30.78.3.8080的数据包要从docker0设备进入,将请求数据包转发到docker0设备,所以在docker0设备上监听到了172.30.22.3.51596 > 172.30.78.3.8080的请求数据包
        5. 172.30.78.3对应的pod响应请求,并构造response返回172.30.22.3.51596,在docker0设备上有了172.30.78.3.8080 > 172.30.22.3.51596的响应信息
        6. 根据slave上的路由规则,发往172.30.22.3.51596的数据包,要经过flannel1.1网络设备,所以flannel.1网络设备中有了172.30.78.3.8080 > 172.30.22.3.51596的响应信息
        7. flannel1.1网络设备,将数据发送给flanneld,flanneld查找到172.30.22.3.51596所在的物理节点,将数据包重新包装成,追加outerIp、port信息(192.168.0.107.8472),之后通过路由规则,发往192.168.0.107.8472的数据包从wlo1走,所以在wlo1网卡上出现192.168.0.114.43021 > 192.168.0.107.8472的数据包

    例子3

    只在slave上启动一个web服务,type设定成NodePort,对应的nodePort设置成8086,从master宿主机上使用curl http://slaveIp:8088/header/list 访问web服务(直接从slave上访问,数据不需要传输,无法看到slave机器上物理网卡上的数据包,所以为了分析,我们从master上访问)

    原理

    当创建NodePort类型的service时,Kubernetes会从API Server指定的参数--service-node-port-range中选择一个port分配给service,也可以自己通过.spec.ports[*].nodePort自己指定。之后kubernetes会在集群的每个node上监听对应的port。

    除了在所有节点节点上监听port外,kubernetes会自动给我们创建一个ClusterIP类型的service,所以创建NodePort的service后,也可以像上个例子一样在集群内部通过 service Name+ service Port的形式访问

    此时数据包不需要在集群内pod中跨主机流转,所以数据包不会经过flannel.1,数据包处理流程: master.wlp3s0->.slave.wlo1->slave.docker0->slave.docker0->slave.wlo1-> master.wlp3s0

    启动服务
    1. 修改web启动文件

      $cat > web.yml <<EOF
      apiVersion: v1
      kind: Service
      metadata:
        name: clientip
      spec:
        type: NodePort
        selector:
          app: clientip
        ports:
        - name: http
          port: 8080
          targetPort: 8080
          nodePort: 8086
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: clientip-deployment
      spec:
        selector:
          matchLabels:
            app: clientip
        replicas: 1
        template:
          metadata:
            labels:
              app: clientip
          spec:
            nodeSelector:
              sample: slave
            containers:
            - name: clientip
              image: 192.168.0.107/k8s/client-ip-test:0.0.2
              ports:
              - containerPort: 8080
      EOF
      
      
    2. 启动服务

      $ kubectl create -f web.yml
      service/clientip created
      deployment.apps/clientip-deployment created
      
      $ kubectl get pod -o wide
      NAME                                   READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
      clientip-deployment-68c57b7965-28w4t   1/1     Running   0          10s   172.30.78.3   slave   <none>           <none>
      $ kubectl get svc -o wide
      NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)         AGE   SELECTOR
      clientip     NodePort    10.254.85.24   <none>        8080:8086/TCP   17s   app=clientip
      kubernetes   ClusterIP   10.254.0.1     <none>        443/TCP         27d   <none>
      
      
    日志监控
    1. 监控web服务的日志

      $ kubectl logs -f clientip-deployment-68c57b7965-28w4t
      
      
    2. 监控master wlp3s0网卡的日志

      $ tcpdump -n -vv -i wlp3s0
      
      
    3. 监控slave节点各个网络设备日志

      
      $ tcpdump -n -vv -i docker0
      $ tcpdump -n -vv -i flannel.1
      $ tcpdump -n -vv -i wlo1
      
      
      
    日志分析(只展示主要流程,tcp握手过程略去)
    1. web日志

      2020-03-08 10:15:01.498  INFO 6 --- [nio-8080-exec-2] c.falcon.clientip.ClientIpController     : host is192.168.0.114:8086
      2020-03-08 10:15:01.499  INFO 6 --- [nio-8080-exec-2] c.falcon.clientip.ClientIpController     : remoteAddr is 172.30.78.1
      2020-03-08 10:15:01.499  INFO 6 --- [nio-8080-exec-2] c.falcon.clientip.ClientIpController     : remotePort is 38362
      
      
      • 主意remoteAddr对应的值是172.30.78.1,并不是我们的宿主机IP,原因参考请求过程
    2. slave 日志

      1. docker0设备日志

        ...
        10:15:01.494019 IP (tos 0x0, ttl 63, id 41431, offset 0, flags [DF], proto TCP (6), length 145)
            172.30.78.1.38362 > 172.30.78.3.8080: Flags [P.], cksum 0x171b (correct), seq 0:93, ack 1, win 502, options [nop,nop,TS val 670876057 ecr 1109116899], length 93: HTTP, length: 93
                GET /header/list HTTP/1.1
                Host: 192.168.0.114:8086
                User-Agent: curl/7.58.0
                Accept: */*
        
        ...
        
        10:15:01.503806 IP (tos 0x0, ttl 64, id 34492, offset 0, flags [DF], proto TCP (6), length 167)
            172.30.78.3.8080 > 192.168.0.107.38362: Flags [P.], cksum 0xbbce (incorrect -> 0x0f9e), seq 1:116, ack 94, win 502, options [nop,nop,TS val 1109116911 ecr 670876057], length 115: HTTP, length: 115
                HTTP/1.1 200
                Content-Type: text/plain;charset=UTF-8
                Content-Length: 2
                Date: Sun, 08 Mar 2020 02:15:01 GMT
        
                OK[!http]
        ...
        
        
        • 第一条,请求从docker0进入172.30.78.3.8080,注意此时的请求是从172.30.78.1.38362过来的,就是我们在web容器中看到的remoteAddr,原因参考请求过程

        • 第二条,请求处理后从docker0返回,这时对应的响应返回的地址又变成了实际发出访问的地址192.168.0.107.38362

      2. flnanel.1设备日志

        tcpdump -n -vv -i flannel.1
        tcpdump: listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes
        
                                   
        
        • 说明没有相关数据包经过
      3. wlo1物理网卡日志

        ...
        10:15:01.493998 IP (tos 0x0, ttl 64, id 41431, offset 0, flags [DF], proto TCP (6), length 145)
            192.168.0.107.38362 > 192.168.0.114.8086: Flags [P.], cksum 0x8928 (correct), seq 1:94, ack 1, win 502, options [nop,nop,TS val 670876057 ecr 1109116899], length 93
        
        ...
        10:15:01.503827 IP (tos 0x0, ttl 63, id 34492, offset 0, flags [DF], proto TCP (6), length 167)
            192.168.0.114.8086 > 192.168.0.107.38362: Flags [P.], cksum 0x489f (correct), seq 1:116, ack 94, win 502, options [nop,nop,TS val 1109116911 ecr 670876057], length 115
        ...
        
        
        • 第一条,slave主机收到192.168.0.107.38362发过来的请求
        • 第二条,slave将web容器响应的内容返回给192.168.0.107.38362
    3. master wlp3s0网卡日志

      ...
      10:15:01.447172 IP (tos 0x0, ttl 64, id 41431, offset 0, flags [DF], proto TCP (6), length 145)
          192.168.0.107.38362 > 192.168.0.114.8086: Flags [P.], cksum 0x82b1 (incorrect -> 0x8928), seq 1:94, ack 1, win 502, options [nop,nop,TS val 670876057 ecr 1109116899], length 93
      ...
      10:15:01.460324 IP (tos 0x0, ttl 63, id 34492, offset 0, flags [DF], proto TCP (6), length 167)
          192.168.0.114.8086 > 192.168.0.107.38362: Flags [P.], cksum 0x489f (correct), seq 1:116, ack 94, win 502, options [nop,nop,TS val 1109116911 ecr 670876057], length 115
      ...
      
      
      • 第一条向192.168.0.114.8086发送请求
      • 第二条从192.168.0.114.8086接收到响应
    4. slave上响应请求过程总结(红色方块请求进入为起始点)

      从master到slave的请求过程和普通请求一样,此处不在描述

      1. wlo1物理网卡收到请求,发现是访问自己机器的IP,进入Netfilter的INPUT链

      2. IPVS在input链上判断访问的地址192.168.0.114.8086是一个集群服务(为什么能判断出来,参考ipvs判断原理),从自己的hash表中选择一个真实的服务172.30.78.3.8080,并做DNAT,将请求的目的地址换成这个真实的服务器地址,进入POSTROUTING阶段

      3. 在POSTROUTING阶段,按照IPTABLES的规则会进行masquerade(为什么执行,参考执行masquerade的原因),之后进行路由选择,根据slave的路由规则表,发往172.30.78.3.8080的数据需要经过docker0,根据masquerade的原理,在发送时将源地址变成了docker0网络设备的地址

        $ ip addr
        6: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
            link/ether 02:42:0d:ab:b0:60 brd ff:ff:ff:ff:ff:ff
            inet 172.30.78.1/24 brd 172.30.78.255 scope global docker0
               valid_lft forever preferred_lft forever
            inet6 fe80::42:dff:feab:b060/64 scope link
               valid_lft forever preferred_lft forever
        
        

        对应的地址是172.30.78.1,这就是为什么我们在web日志,以及在docker0网络上看到请求是172.30.78.1的原因

      4. 之后请求转发到docker0网络,从docker0网络进入到web容器内

      5. web容器处理完请求构成响应体,在返回时发现这个请求是经过masquerade进来的,返回时查找masquerade前的真实请求发起者,将数据返回地址设置为192.168.0.107.38362,之后根据路由规则,发送给192.168.0.107.38362的数据包,需要从物理网卡wlo1发送,所以数据转发给了wlo1网卡,在进入之前,会执行IPVS的masquerade,将源地址修改成192.168.0.114,并通过wlo1网卡发送给master

    Slave上数据流转原理
    1. IPVS判断出192.168.0.114.8086是集群服务原理

      我们知道IPVS根据自己的hash表中的内容进行判断,所以kubernetes只需要把集群服务相关的信息存入到IPVS的hash表中就能实现了。利用ipvsadm工具查看当启动一个NodePort的service后,kubernetes会在这个hash表中存入哪些内容(下面命令输出中略去了不相干的记录)

      $ ipvsadm --list
      IP Virtual Server version 1.2.1 (size=4096)
      Prot LocalAddress:Port Scheduler Flags
        -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
      TCP  localhost:8086 rr
        -> 172.30.78.3:http-alt         Masq    1      0          0
      TCP  slave:8086 rr
        -> 172.30.78.3:http-alt         Masq    1      0          0
      TCP  promote.cache-dns.local:http rr
        -> 172.30.78.3:http-alt         Masq    1      0          0
      ...	
      
      
      • 可以看到kubernetes不仅将自动创建的ClusterIP对应的记录插入到hash表中,针对NodePort服务,还会多插入两条记录localhost和hostname对应的规则,这样当我们访问192.168.0.114.8086时,能匹配到slave:8086,所以IPVS判断出这访问的是一个集群服务,会进行DNAT
    2. 请求在POSTROUTING阶段执行masquerade的原理

      1. 首先看下采用IPVS模式时,kubernetes给我们创建的ipset,及其作用

        name members usage
        -CLUSTER-IP All service IP + port Mark-Masq for cases that masquerade-all=true or clusterCIDR specified
        -LOOP-BACK All service IP + port + IP masquerade for solving hairpin purpose
        -EXTERNAL-IP service external IP + port masquerade for packages to external IPs
        -LOAD-BALANCER load balancer ingress IP + port masquerade for packages to load balancer type service
        -LOAD-BALANCER-LOCAL LB ingress IP + port with externalTrafficPolicy=local accept packages to load balancer with externalTrafficPolicy=local
        -LOAD-BALANCER-FW load balancer ingress IP + port with loadBalancerSourceRanges package filter for load balancer with loadBalancerSourceRanges specified
        -LOAD-BALANCER-SOURCE-CIDR load balancer ingress IP + port + source CIDR package filter for load balancer with loadBalancerSourceRanges specified
        -NODE-PORT-TCP nodeport type service TCP port masquerade for packets to nodePort(TCP)
        -NODE-PORT-LOCAL-TCP nodeport type service TCP port with externalTrafficPolicy=local accept packages to nodeport service with externalTrafficPolicy=local
        -NODE-PORT-UDP nodeport type service UDP port masquerade for packets to nodePort(UDP)
        -NODE-PORT-LOCAL-UDP nodeport type service UDP port with externalTrafficPolicy=local accept packages to nodeport service with externalTrafficPolicy=local
        • 其中KUBE-NODE-PORT-TCP 里面存储的是需要进行masquerade的本机端口号
      2. 其次,需要知道kubernetes是如何利用这些ipset的,再看下kubernetes为我们在iptables中追加的规则

      下面的输出内容是在kube-proxy启动参数:iptables.masqueradeAll=false;clusterCIDR=172.30.0.0/16时的结果,配置成其他的KUBE-SERVICES的规则链会稍有不同,输出进行了精简只包含了和kubernetes相关的规则

       ```
       $ iptables -n -L -t nat
       
       Chain PREROUTING (policy ACCEPT)
       target     prot opt source               destination
       KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
       
       Chain OUTPUT (policy ACCEPT)
       target     prot opt source               destination
       KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
       
       Chain POSTROUTING (policy ACCEPT)
       target     prot opt source               destination
       KUBE-POSTROUTING  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
       
       Chain KUBE-FIREWALL (0 references)
       target     prot opt source               destination
       KUBE-MARK-DROP  all  --  0.0.0.0/0            0.0.0.0/0
       
       Chain KUBE-KUBELET-CANARY (0 references)
       target     prot opt source               destination
       
       Chain KUBE-LOAD-BALANCER (0 references)
       target     prot opt source               destination
       KUBE-MARK-MASQ  all  --  0.0.0.0/0            0.0.0.0/0
       
       Chain KUBE-MARK-DROP (1 references)
       target     prot opt source               destination  
       
       Chain KUBE-MARK-MASQ (2 references)
       target     prot opt source               destination
       MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x4000
       
       Chain KUBE-NODE-PORT (1 references)
       target     prot opt source               destination
       KUBE-MARK-MASQ  tcp  --  0.0.0.0/0            0.0.0.0/0            /* Kubernetes nodeport TCP port for masquerade purpose */ match-set KUBE-NODE-PORT-TCP dst
       
       Chain KUBE-POSTROUTING (1 references)
       target     prot opt source               destination
       MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000
       MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            match-set KUBE-LOOP-BACK dst,dst,src
       
       Chain KUBE-SERVICES (2 references)
       target     prot opt source               destination
       KUBE-MARK-MASQ  all  -- !172.30.0.0/16        0.0.0.0/0            /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
       KUBE-NODE-PORT  all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
       ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            match-set KUBE-CLUSTER-IP dst,dst
      
       ```
       
       1. PREROUTING阶段
       
       	* 在PREROUTING阶段,所有请求会jump到KUBE-SERVICES规则链
       	* 在KUBE-SERVICES规则链里,根据我们访问的地址salveIp:8086,第一条不满足,会匹配到第二条即访问的地址是本机,所以jump到KUBE-NODE-PORT规则链
       	* 在KUBE-NODE-PORT规则链中会判断请求目的端口号是否在KUBE-NODE-PORT-TCP这个ipset中,是的话跳转到KUBE-MARK-MASQ,看下我们启动NodePort service后这个ipset中的值
       		
       		```
       		$ ipset  --list KUBE-NODE-PORT-TCP
       		Name: KUBE-NODE-PORT-TCP
       		Type: bitmap:port
       		Revision: 3
       		Header: range 0-65535
       		Size in memory: 8268
       		References: 1
       		Number of entries: 1
       		Members:
       		8086
       		
       		```
       		kubernetes的确把我们创建的服务对应的node port值存入这个里面了
       	* KUBE-MARK-MASQ规则链对进入这个规则链的所有请求都打上一个标签0x4000
       	
       1. POSTROUTING阶段
       	* 在POSTROUTING阶段,所有的请求都jump到KUBE-POSTROUTING规则链中
       	* 在KUBE-POSTROUTING规则链中,根据第一条规则,当进来的数据包有0x4000标记时进行MASQUERADE,根据在PREROUTING阶段中的处理,访问salveIp:8086的请求会满足条规则,所以会对我们的请求进行MASQUERADE
      

    总结

    这样,本文用三个例子,通过用tcpdump对各个网络设备上数据包的分析,阐述了不同情况下kubernetes的网络请求过程。最后一个例子结合kubernetes给我创建的ipset、iptables规则讲述了kubernetes实现服务访问的原理。前面两个例子读者也可以采用这样的方式结合下iptables中的规则链,来验证下数据的流转流程。

    另外最后一个例子,还可以通过集群中master节点的8086来访问web服务,这时数据包还会经过两个节点的flannel.1网络设备,但不会经过master.docker0设备,并且web中收到请求的remoteAddr也会不一样,下面只给出请求过程不再给出具体的tcpdump日志信息

    请求:
    master.wlp3s0->master.flannel.1->master.wlp3s0->slave.wlo1->slave.flannel.1->slave.docker0

    响应:
    slave.docker0->slave.flannel.1->slave.wlo1->master.wlp3s0->master.flannel.1-> master.wlp3s0

    读者朋友可以自行试下,结合tcpdump工具和iptables中的规则对数据包流转过程进行分析。

    题外话

    额外的一点思考,为啥kubernetes要设计的这么复杂对通过node port的请求进行masquerade呢,这是因为当创建一个NodePort服务后,kubernetes不只是让服务对应的endpoint所在的节点上能够提供服务,而是让集群中所有的节点都可以在对应的port上提供服务,这样我们从外部通过node port访问集群服务时,有可能访问的服务对应的pod不在我们访问的节点上,这样要是不经过masquerade,真实的endpoint处理完请求后在响应时看到的也是真实的clientIP,数据就不会先返回到client一开始请求的node上,而是直接返回给了client,这样client收到结果发现是和请求的地址不一样的服务器给了响应,会认为这是不合法的的响应体。所以为了让client能从请求的节点上拿到响应体,所以需要对外部访问node port的请求统一做masquerade,这样数据返回时,会首先返回到client请求的节点上,再由此节点返回给client。如果因为业务需求,如一些审计什么的,必须要获取到client的真实IP,可以考虑下面三种方式:

    1. 在集群外层再加一个代理(ingress方式),在代理里面获取client IP,存入约定好的header 头中,在集群内的服务通过这个header信息来获取
    2. POD直接设置成hostNetwork
    3. 通过将服务设置{"externalTrafficPolicy":"Local"}},这时如果pod不在对应的节点上时,是无法提供服务的

    kubernetes的官网对此的探讨:
    Using Source IP

  • 相关阅读:
    数据仓库中的几种数据模型
    数据仓库为什么要分层
    数据仓库的两种建模方法
    数据仓库之架构发展
    数据仓库是什么
    ETL 自动化测试框架
    大数据测试之ETL测试工具和面试常见的问题及答案
    Hadoop面试链接
    Hadoop 面试总结
    Spark 基本架构及原理
  • 原文地址:https://www.cnblogs.com/gaofeng-henu/p/12442522.html
Copyright © 2011-2022 走看看