kubernetes 网络

前言

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

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

扫描二维码关注公众号,回复: 9663381 查看本文章
$ 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,及其作用

      set name members usage
      KUBE-CLUSTER-IP All service IP + port Mark-Masq for cases that masquerade-all=true or clusterCIDR specified
      KUBE-LOOP-BACK All service IP + port + IP masquerade for solving hairpin purpose
      KUBE-EXTERNAL-IP service external IP + port masquerade for packages to external IPs
      KUBE-LOAD-BALANCER load balancer ingress IP + port masquerade for packages to load balancer type service
      KUBE-LOAD-BALANCER-LOCAL LB ingress IP + port with externalTrafficPolicy=local accept packages to load balancer with externalTrafficPolicy=local
      KUBE-LOAD-BALANCER-FW load balancer ingress IP + port with loadBalancerSourceRanges package filter for load balancer with loadBalancerSourceRanges specified
      KUBE-LOAD-BALANCER-SOURCE-CIDR load balancer ingress IP + port + source CIDR package filter for load balancer with loadBalancerSourceRanges specified
      KUBE-NODE-PORT-TCP nodeport type service TCP port masquerade for packets to nodePort(TCP)
      KUBE-NODE-PORT-LOCAL-TCP nodeport type service TCP port with externalTrafficPolicy=local accept packages to nodeport service with externalTrafficPolicy=local
      KUBE-NODE-PORT-UDP nodeport type service UDP port masquerade for packets to nodePort(UDP)
      KUBE-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

      2. 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

猜你喜欢

转载自www.cnblogs.com/gaofeng-henu/p/12442522.html