kubernetes 简介:kube-proxy 和 service

penghc 2年前
   <h2>简介</h2>    <p>在 kubernetes 集群中,网络是非常基础也非常重要的一部分。对于大规模的节点和容器来说,要保证网络的连通性、网络转发的高效,同时能做的 ip 和 port 自动化分配和管理,并让用户用直观简单的方式来访问需要的应用,这是需要复杂且细致设计的。</p>    <p>kubernetes 在这方面下了很大的功夫,它通过 service 、 dns 、 ingress 等概念,解决了服务发现、负载均衡的问题,也大大简化了用户的使用和配置。</p>    <p>这篇文章就讲解如何配置 kubernetes 的网络,最终从集群内部和集群外部都能访问应用。</p>    <h2>跨主机网络配置:flannel</h2>    <p>一直以来,kubernetes 并没有专门的网络模块负责网络配置,它需要用户在主机上已经配置好网络。kubernetes 对网络的要求是:容器之间(包括同一台主机上的容器,和不同主机的容器)可以互相通信,容器和集群中所有的节点也能直接通信。</p>    <p>至于具体的网络方案,用户可以自己选择,目前使用比较多的是 flannel,因为它比较简单,而且刚好满足 kubernetes 对网络的要求。我们会使用 flannel vxlan 模式,具体的配置我在博客之前有文章介绍过,这里不再赘述。</p>    <p>以后 kubernetes 网络的发展方向是希望通过插件的方式来集成不同的网络方案, <a href="/misc/goto?guid=4959746403357197681" rel="nofollow,noindex">CNI</a> 就是这一努力的结果,flannel 也能够通过 CNI 插件的形式使用。</p>    <h2>kube-proxy 和 service</h2>    <p>配置好网络之后,集群是什么情况呢?我们可以创建 pod,也能通过 ReplicationController 来创建特定副本的 pod(这是更推荐也是生产上要使用的方法,即使某个 rc 中只有一个 pod 实例)。可以从集群中获取每个 pod ip 地址,然后也能在集群内部直接通过 podIP:Port 来获取对应的服务。</p>    <p>但是还有一个问题: <strong>pod 是经常变化的,每次更新 ip 地址都可能会发生变化</strong> ,如果直接访问容器 ip 的话,会有很大的问题。而且进行扩展的时候,rc 中会有新的 pod 创建出来,出现新的 ip 地址,我们需要一种更灵活的方式来访问 pod 的服务。</p>    <h3>Service 和 cluster IP</h3>    <p>针对这个问题,kubernetes 的解决方案是“服务”(service),每个服务都一个固定的虚拟 ip(这个 ip 也被称为 cluster IP),自动并且动态地绑定后面的 pod,所有的网络请求直接访问服务 ip,服务会自动向后端做转发。Service 除了提供稳定的对外访问方式之外,还能起到负载均衡(Load Balance)的功能,自动把请求流量分布到后端所有的服务上,服务可以做到对客户透明地进行水平扩展(scale)。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/0af85d635c05b8f03a0d1551f81e4399.jpg"></p>    <p>而实现 service 这一功能的关键,就是 kube-proxy。kube-proxy 运行在每个节点上,监听 API Server 中服务对象的变化,通过管理 iptables 来实现网络的转发。</p>    <p>NOTE: kube-proxy 要求 NODE 节点操作系统中要具备 /sys/module/br_netfilter 文件,而且还要设置 bridge-nf-call-iptables=1,如果不满足要求,那么 kube-proxy 只是将检查信息记录到日志中,kube-proxy 仍然会正常运行,但是这样通过 Kube-proxy 设置的某些 iptables 规则就不会工作。</p>    <p>kube-proxy 有两种实现 service 的方案:userspace 和 iptables</p>    <ul>     <li>userspace 是在用户空间监听一个端口,所有的 service 都转发到这个端口,然后 kube-proxy 在内部应用层对其进行转发。因为是在用户空间进行转发,所以效率也不高</li>     <li>iptables 完全实现 iptables 来实现 service,是目前默认的方式,也是推荐的方式,效率很高(只有内核中 netfilter 一些损耗)。</li>    </ul>    <p>这篇文章通过 iptables 模式运行 kube-proxy,后面的分析也是针对这个模式的,userspace 只是旧版本支持的模式,以后可能会放弃维护和支持。</p>    <h3>kube-proxy 参数介绍</h3>    <p>kube-proxy 的功能相对简单一些,也比较独立,需要的配置并不是很多,比较常用的启动参数包括:</p>    <table>     <thead>      <tr>       <th>参数</th>       <th>含义</th>       <th>默认值</th>      </tr>     </thead>     <tbody>      <tr>       <td>–alsologtostderr</td>       <td>打印日志到标准输出</td>       <td>false</td>      </tr>      <tr>       <td>–bind-address</td>       <td>HTTP 监听地址</td>       <td>0.0.0.0</td>      </tr>      <tr>       <td>–cleanup-iptables</td>       <td>如果设置为 true,会清理 proxy 设置的 iptables 选项并退出</td>       <td>false</td>      </tr>      <tr>       <td>–healthz-bind-address</td>       <td>健康检查 HTTP API 监听端口</td>       <td>127.0.0.1</td>      </tr>      <tr>       <td>–healthz-port</td>       <td>健康检查端口</td>       <td>10249</td>      </tr>      <tr>       <td>–iptables-masquerade-bit</td>       <td>使用 iptables 进行 SNAT 的掩码长度</td>       <td>14</td>      </tr>      <tr>       <td>–iptables-sync-period</td>       <td>iptables 更新频率</td>       <td>30s</td>      </tr>      <tr>       <td>–kubeconfig</td>       <td>kubeconfig 配置文件地址</td>       <td> </td>      </tr>      <tr>       <td>–log-dir</td>       <td>日志文件目录/路径</td>       <td> </td>      </tr>      <tr>       <td>–masquerade-all</td>       <td>如果使用 iptables 模式,对所有流量进行 SNAT 处理</td>       <td>false</td>      </tr>      <tr>       <td>–master</td>       <td>kubernetes master API Server 地址</td>       <td> </td>      </tr>      <tr>       <td>–proxy-mode</td>       <td>代理模式, userspace 或者 iptables , 目前默认是 iptables ,如果系统或者 iptables 版本不够新,会 fallback 到 userspace 模式</td>       <td>iptables</td>      </tr>      <tr>       <td>–proxy-port-range</td>       <td>代理使用的端口范围, 格式为 beginPort-endPort ,如果没有指定,会随机选择</td>       <td>0-0</td>      </tr>      <tr>       <td>–udp-timeout</td>       <td>UDP 空连接 timeout 时间,只对 userspace 模式有用</td>       <td>250ms</td>      </tr>      <tr>       <td>–v</td>       <td>日志级别</td>       <td>0</td>      </tr>     </tbody>    </table>    <p>kube-proxy 的工作模式可以通过 --proxy-mode 进行配置,可以选择 userspace 或者 iptables 。</p>    <h3>实例启动和测试</h3>    <p>我们可以在终端上启动 kube-proxy ,也可以使用诸如 systemd 这样的工具来管理它,比如下面就是一个简单的 kube-proxy.service 配置文件</p>    <pre>  [root@localhost]# cat /usr/lib/systemd/system/kube-proxy.service  [Unit]  Description=Kubernetes Proxy Service  Documentation=http://kubernetes.com  After=network.target  Wants=network.target    [Service]  Type=simple  EnvironmentFile=-/etc/sysconfig/kube-proxy  ExecStart=/usr/bin/kube-proxy \      --master=http://172.17.8.100:8080 \      --v=4 \      --proxy-mode=iptables  TimeoutStartSec=0  Restart=on-abnormal    [Install]  WantedBy=multi-user.target</pre>    <p>为了方便测试,我们创建一个 rc,里面有三个 pod。这个 pod 运行的是 cizixs/whoami 容器 ,它是一个简单的 HTTP 服务器,监听在 3000 端口,访问它会返回容器的 hostname。</p>    <pre>  [root@localhost ~]# cat whoami-rc.yml  apiVersion: v1  kind: ReplicationController  metadata:    name: whoami  spec:    replicas: 3    selector:      app: whoami    template:      metadata:        name: whoami        labels:          app: whoami          env: dev      spec:        containers:        - name: whoami          image: cizixs/whoami:v0.5          ports:          - containerPort: 3000          env:            - name: MESSAGE              value: viola</pre>    <p>我们为每个 pod 设置了两个 label: app=whoami 和 env=dev ,这两个标签很重要,也是后面服务进行绑定 pod 的关键。</p>    <p>为了使用 service,我们还要定义另外一个文件,并通过 kubectl create -f ./whoami-svc.yml 来创建出来对象:</p>    <pre>  apiVersion: v1  kind: Service  metadata:    labels:      name: whoami    name: whoami  spec:    ports:      - port: 3000        targetPort: 3000        protocol: TCP    selector:      app: whoami      env: dev</pre>    <p>其中 selector 告诉 kubernetes 这个 service 和后端哪些 pod 绑定在一起,这里包含的键值对会对所有 pod 的 labels 进行匹配,只要完全匹配,service 就会把 pod 作为后端。也就是说,service 和 rc 并不是对应的关系,一个 service 可能会使用多个 rc 管理的 pod 作为后端应用。</p>    <p>ports 字段指定服务的端口信息:</p>    <ul>     <li>port :虚拟 ip 要绑定的 port,每个 service 会创建出来一个虚拟 ip,通过访问 vip:port 就能获取服务的内容。这个 port 可以用户随机选取,因为每个服务都有自己的 vip,也不用担心冲突的情况</li>     <li>targetPort :pod 中暴露出来的 port,这是运行的容器中具体暴露出来的端口,一定不能写错</li>     <li>protocol :提供服务的协议类型,可以是 TCP 或者 UDP</li>    </ul>    <p>创建之后可以列出 service ,发现我们创建的 service 已经分配了一个虚拟 ip (10.10.10.28),这个虚拟 ip 地址是不会变化的(除非 service 被删除)。查看 service 的详情可以看到它的 endpoints 列出,对应了具体提供服务的 pod 地址和端口。</p>    <pre>  [root@localhost ~]# kubectl get svc  NAME         CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE  kubernetes   10.10.10.1    <none>        443/TCP    19d  whoami       10.10.10.28   <none>        3000/TCP   1d    [root@localhost ~]# kubectl describe svc whoami  Name:                   whoami  Namespace:              default  Labels:                 name=whoami  Selector:               app=whoami  Type:                   ClusterIP  IP:                     10.10.10.28  Port:                   <unset> 3000/TCP  Endpoints:              10.11.32.6:3000,10.13.192.4:3000,10.16.192.3:3000  Session Affinity:       None  No events.</pre>    <p>默认的 service 类型是 ClusterIP ,这个也可以从上面输出看出来。在这种情况下,只能从集群内部访问这个 IP,不能直接从集群外部访问服务。如果想对外提供服务,我们后面会讲解决方案。</p>    <p>测试一下,访问 service 服务的时候可以看到它会随机地访问后端的 pod,给出不同的返回:</p>    <pre>  [root@localhost ~]# curl http://10.10.10.28:3000  viola from whoami-8fpqp  [root@localhost ~]# curl http://10.10.10.28:3000  viola from whoami-c0x6h  [root@localhost ~]# curl http://10.10.10.28:3000  viola from whoami-8fpqp  [root@localhost ~]# curl http://10.10.10.28:3000  viola from whoami-dc9ds</pre>    <p>默认情况下,服务会随机转发到可用的后端。如果希望保持会话(同一个 client 永远都转发到相同的 pod),可以把 service.spec.sessionAffinity 设置为 ClientIP 。</p>    <p>NOTE: 需要注意的是,服务分配的 cluster IP 是一个虚拟 ip,如果你尝试 ping 这个 IP 会发现它没有任何响应,这也是刚接触 kubernetes service 的人经常会犯的错误。实际上,这个虚拟 IP 只有和它的 port 一起的时候才有作用,直接访问它,或者想访问该 IP 的其他端口都是徒劳。</p>    <h3>外部能够访问的服务</h3>    <p>上面创建的服务只能在集群内部访问,这在生产环境中还不能直接使用。如果希望有一个能直接对外使用的服务,可以使用 NodePort 或者 LoadBalancer 类型的 Service。我们先说说 NodePort ,它的意思是在所有 worker 节点上暴露一个端口,这样外部可以直接通过访问 nodeIP:Port 来访问应用。</p>    <p>我们先把刚才创建的服务删除:</p>    <pre>  [root@localhost ~]# kubectl delete rc whoami  replicationcontroller "whoami" deleted    [root@localhost ~]# kubectl delete svc whoami  service "whoami" deleted    [root@localhost ~]# kubectl get pods,svc,rc  NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE  kubernetes   10.10.10.1   <none>        443/TCP   14h</pre>    <p>对我们原来的 Service 配置文件进行修改,把 spec.type 写成 NodePort 类型:</p>    <pre>  [root@localhost ~]# cat whoami-svc.yml  apiVersion: v1  kind: Service  metadata:    labels:      name: whoami    name: whoami  spec:    ports:      - port: 3000        protocol: TCP        # nodePort: 31000    selector:      app: whoami    type: NodePort</pre>    <p>因为我们的应用比较简单,只有一个端口。如果 pod 有多个端口,也可以在 spec.ports 中继续添加,只有保证多个 port 之间不冲突就行。</p>    <p>重新创建 rc 和 svc:</p>    <pre>  [root@localhost ~]# kubectl create -f ./whoami-svc.yml  service "whoami" created  [root@localhost ~]# kubectl get rc,pods,svc  NAME        DESIRED   CURRENT   READY     AGE  rc/whoami   3         3         3         10s    NAME              READY     STATUS    RESTARTS   AGE  po/whoami-8zc3d   1/1       Running   0          10s  po/whoami-mc2fg   1/1       Running   0          10s  po/whoami-z6skj   1/1       Running   0          10s    NAME             CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE  svc/kubernetes   10.10.10.1     <none>        443/TCP          14h  svc/whoami       10.10.10.163   <nodes>       3000:31647/TCP   7s</pre>    <p>需要注意的是,因为我们没有指定 nodePort 的值,kubernetes 会自动给我们分配一个,比如这里的 31647 (默认的取值范围是 30000-32767)。当然我们也可以删除配置中 # nodePort: 31000 的注释,这样会使用 31000 端口。</p>    <p>nodePort 类型的服务会在所有的 worker 节点(运行了 kube-proxy)上统一暴露出端口对外提供服务,也就是说外部可以任意选择一个节点进行访问。比如我本地集群有三个节点: 172.17.8.100 、 172.17.8.101 和 172.17.8.102 :</p>    <pre>  [root@localhost ~]# curl http://172.17.8.100:31647  viola from whoami-mc2fg  [root@localhost ~]# curl http://172.17.8.101:31647  viola from whoami-8zc3d  [root@localhost ~]# curl http://172.17.8.102:31647  viola from whoami-z6skj</pre>    <p>有了 nodePort ,用户可以通过外部的 Load Balance 或者路由器把流量转发到任意的节点,对外提供服务的同时,也可以做到负载均衡的效果。</p>    <p>nodePort 类型的服务并不影响原来虚拟 IP 的访问方式,内部节点依然可以通过 vip:port 的方式进行访问。</p>    <p>LoadBalancer 类型的服务需要公有云支持,如果你的集群部署在公有云(GCE、AWS等)可以考虑这种方式。</p>    <h2>service 原理解析</h2>    <p>目前 kube-proxy 默认使用 iptables 模式,上述展现的 service 功能都是通过修改 iptables 实现的。</p>    <p>我们来看一下从主机上访问 service:port 的时候发生了什么(通过 iptables-save 命令打印出来当前机器上的 iptables 规则)。</p>    <p>所有发送出去的报文会进入 KUBE-SERVICES 进行处理</p>    <pre>  *nat  -A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES</pre>    <p>KUBE-SERVICES 每条规则对应了一个 service,它告诉继续进入到某个具体的 service chain 进行处理,比如这里的 KUBE-SVC-OQCLJJ5GLLNFY3XB</p>    <pre>  -A KUBE-SERVICES -d 10.10.10.28/32 -p tcp -m comment --comment "default/whoami: cluster IP" -m tcp --dport 3000 -j KUBE-SVC-OQCLJJ5GLLNFY3XB</pre>    <p>更具体的 chain 中定义了怎么转发到对应 endpoint 的规则,比如我们的 rc 有三个 pods,这里也就会生成三个规则。这里利用了 iptables 随机和概率转发的功能</p>    <pre>  -A KUBE-SVC-OQCLJJ5GLLNFY3XB -m comment --comment "default/whoami:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-VN72UHNM6XOXLRPW  -A KUBE-SVC-OQCLJJ5GLLNFY3XB -m comment --comment "default/whoami:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-YXCSPWPTUFI5WI5Y  -A KUBE-SVC-OQCLJJ5GLLNFY3XB -m comment --comment "default/whoami:" -j KUBE-SEP-FN74S3YUBFMWHBLF</pre>    <p>我们来看第一个 chain,这个 chain 有两个规则,第一个表示给报文打上 mark;第二个是进行 DNAT(修改报文的目的地址),转发到某个 pod 地址和端口。</p>    <pre>  -A KUBE-SEP-VN72UHNM6XOXLRPW -s 10.11.32.6/32 -m comment --comment "default/whoami:" -j KUBE-MARK-MASQ  -A KUBE-SEP-VN72UHNM6XOXLRPW -p tcp -m comment --comment "default/whoami:" -m tcp -j DNAT --to-destination 10.11.32.6:3000</pre>    <p>因为地址是发送出去的,报文会根据路由规则进行处理,后续的报文就是通过 flannel 的网络路径发送出去的。</p>    <p>nodePort 类型的 service 原理也是类似的,在 KUBE-SERVICES chain 的最后,如果目标地址不是 VIP 则会通过 KUBE-NODEPORTS :</p>    <pre>  Chain KUBE-SERVICES (2 references)   pkts bytes target     prot opt in     out     source               destination               0     0 KUBE-NODEPORTS  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL</pre>    <p>而 KUBE-NODEPORTS chain 和 KUBE-SERVICES chain 其他规则一样,都是转发到更具体的 service chain,然后转发到某个 pod 上面。</p>    <pre>  -A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami:" -m tcp --dport 31647 -j KUBE-MARK-MASQ  -A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami:" -m tcp --dport 31647 -j KUBE-SVC-OQCLJJ5GLLNFY3XB</pre>    <h2>不足之处</h2>    <p>看起来 service 是个完美的方案,可以解决服务访问的所有问题,但是 service 这个方案(iptables 模式)也有自己的缺点。</p>    <p>首先,如果转发的 pod 不能正常提供服务,它不会自动尝试另一个 pod,当然这个可以通过 readiness probes 来解决。每个 pod 都有一个健康检查的机制,当有 pod 健康状况有问题时,kube-proxy 会删除对应的转发规则。</p>    <p>另外, nodePort 类型的服务也无法添加 TLS 或者更复杂的报文路由机制。</p>    <h2>参考资料</h2>    <ul>     <li><a href="/misc/goto?guid=4959746403441710900" rel="nofollow,noindex">Kubernetes 1.2 如何使用 iptables</a></li>     <li><a href="/misc/goto?guid=4959746403536259747" rel="nofollow,noindex">Kubernetes User Guide: Service</a></li>     <li><a href="/misc/goto?guid=4959746403614480960" rel="nofollow,noindex">Kubernetes User Guide: Debugging Services</a></li>     <li><a href="/misc/goto?guid=4959746403698113654" rel="nofollow,noindex">Kubernetes Services and Ingress Under X-ray</a></li>     <li><a href="/misc/goto?guid=4959746403779368708" rel="nofollow,noindex">CoreOS documentation: Overview of a Service</a></li>    </ul>    <p> </p>    <p>来自:http://cizixs.com/2017/03/30/kubernetes-introduction-service-and-kube-proxy</p>    <p> </p>