Kubernetes网络简析之二:Services

前言

上一篇文章我们对k8s的pod、node以及cluster网络模型进行了分析:Kubernetes 网络简析之一:Pods。通过分析,我们理解了pod内部的container的交互方式,以及同一个cluster内部pod(包括跨node的pod)之间的交互方式。所以在一个k8s cluster里,一个pod是可以直接访问另外一个pod的。但是仔细想一想,pod之间的交互仍然还是有一些疑问的。
我们知道k8s的最小操作单位是pod。pod对于k8s而言永远是暂态的,它可以被创建销毁重建,而重建之后的pod再也不是以前的那个pod了。这意味着一个pod以及分配给它的IP并不能保证一直存在并保持映射关系不变。所以当我们想让一个pod内的服务去访问另外一个pod的时候,这种情况可能会导致我们的服务处于一种不稳定的状态。那k8s针对于这种case有没有什么解决方案呢?
答案是有的,这就是services需要做的事情。

其实上面描述的这种问题在非k8s的传统环境里面也会遇到,而在传统环境里我们的解决方案是增加一个反向代理或者负载均衡服务来实现。这种服务必须满足三个条件:

  • 该服务本身必须保持稳定并且有主备容灾功能
  • 必须有一个或者多个业务服务提供给该服务来分发请求
  • 对于这些业务服务该服务需要能够探测他们的运行健康状态以保证他们能够正常处理请求

而k8s service通过一种优雅的方式利用Linux底层系统功能即完成了上述的要求。

service示例

接下来我们以实际操作为例进行分析说明。
首先我们构造两个相同的处理http请求的服务:

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: service-test 
spec:
  replicas: 2 # 构造两个服务来处理请求
  selector:
    matchLabels:
      app: service_test_pod
  template:
    metadata:
      labels:
        app: service_test_pod
    spec:
      containers:
      - name: simple-http
        image: python:2.7
        imagePullPolicy: IfNotPresent
        command: ["/bin/bash"]
        args: ["-c", "echo \"<p>Hello from $(hostname)</p>\" > index.html; python -m SimpleHTTPServer 8080"]
        ports:
        - name: http
          containerPort: 8080 # 当有请求访问当前pod的8080端口时,会返回 Hello from $(hostname)

我们在k8s上创建这两个业务服务

$ kubectl apply -f test-deployment.yaml
deployment "service-test" created

$ kubectl get pods
service-test-6ffd9ddbbf-kf4j2    1/1    Running    0    15s
service-test-6ffd9ddbbf-qs2j6    1/1    Running    0    15s

$ kubectl get pods --selector=app=service_test_pod -o jsonpath='{.items[*].status.podIP}'
10.0.1.2 10.0.2.2

如上所示,我们构造了两个监听8080端口的http请求的服务。这两个服务pod的IP为10.0.1.2和10.0.2.2
假如我们想构造一个客户端去访问这两个pod的服务的话,我们必须在客户端里指定pod具体的IP。但是如果我们想让客户端访问两个pod的服务同时不受pod状态变化影响的话,我们就需要构造service了

kind: Service
apiVersion: v1
metadata:
  name: service-test
spec:
  selector:
    app: service_test_pod # 对应提供服务的pod
  ports:
  - port: 80 # 监听请求的端口
    targetPort: http # 监听请求类型
$ kubectl get service service-test
NAME           CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service-test   10.3.241.152   <none>        80/TCP    11s

我们为两个带 app: service_test_pod 标签的pod构造了一个service。这个service监听10.3.241.152:80的http请求,并把请求平均分配到每个pod的8080端口。
为了验证,我们构造了一个客户端去请求这个service

apiVersion: v1
kind: Pod
metadata:
  name: service-test-client2
spec:
  restartPolicy: Never
  containers:
  - name: test-client2
    image: alpine
    command: ["/bin/sh"]
    args: ["-c", "echo 'GET / HTTP/1.1\r\n\r\n' | nc service-test 80"]
$ kubectl logs service-test-client2
HTTP/1.0 200 OK
<!-- blah -->
<p>Hello from service-test-6ffd9ddbbf-kf4j2</p>

如果多次尝试我们会发现得到的请求结果会在两个pod上平均分配。

service网络模型

让我们先来看看service的详细参数:

$ kubectl describe services service-test
Name:                   service-test
Namespace:              default
Labels:                 <none>
Selector:               app=service_test_pod
Type:                   ClusterIP
IP:                     10.3.241.152
Port:                   http    80/TCP
Endpoints:              10.0.1.2:8080,10.0.2.2:8080
Session Affinity:       None
Events:                 <none>

当前service的type是ClusterIP(ClusterIP表示当前service所分配的IP可以被cluster内部所有的Pod都访问到,其他的type会在下一篇解释)。我们可以看到service的IP和pod的IP差别比较大,而且根本不在同一个网段上:

ResourceIpnetwork
pod110.0.1.210.0.0.0/14
pod210.0.2.210.0.0.0/14
service10.3.241.15210.3.240.0/20

由此可见,service是有自己的网段的。所有创建的service,其IP都会在这个网段里面。
但是当我们登录到cluster的node主机上,通过诸如 ifconfig之类的命令查看主机上所有的网桥和网卡接口时,我们会发现并没有一个网桥或者接口指向service的IP。甚至于我们去检查连接所有node的网关时,也不会在路由表里找到跟service IP 相关的映射关系。
这么说service并不是实实在在存在的?!
但是为什么它又能发挥代理和均衡的作用呢?!
带着如上的疑问,我们来尝试跟踪一下一个pod之间网络报文的走向。
我们先将如上示例中的server和client pod图形化:
service pod 图例
如图,我们有两个node通过网关进行连接,构造的两个server pod则分别部署在了两个node上面(这里用server1和server2表示),而构造的一个client pod则部署在了和server1相同的node上面。
我们知道IP网络的一个基本特性就是,当请求报文对应的目标IP在本地无法找到对应的设备时,报文会被向上转发,直到找到为止。
当client pod发送请求时,报文路径为:

  1. 当client pod去请求service 对应的DNS名字 “service-test”时,k8s的cluster DNS 解析系统会将其解析为10.3.241.152。这个时候client pod会尝试把报文发送给这个IP。

  2. client pod本身的网段是10.0.0.0/14,它不知道10.3.241.152对应着哪个设备,所以它只能把请求向上转发到网桥cbr0。

  3. cbr0本身也只有转发功能,所以它又把请求转发给了eth0。

  4. eth0自身的网段是10.100.0.0/24,它也不知道10.3.241.152意味着啥。

按理说这个时候eth0会把请求向上转发到网关,让网关去尝试寻找对应的设备。但是这个时候神奇的事情发生了:

  • eth0将报文直接重定向给了一个server pod (10.0.2.2) !!! (相当于service的代理和均衡功能在这里就直接完成了!!!)
  • 接下来的过程和我们上一篇描述的一样,报文经过网关的解析和转发到达了另外一个node上面的server2
    node间报文转发过程
    这是怎么回事?报文为什么突然在eth0就知道了自己的实际目的地是server2?
    其实这要归功于k8s内部kube-proxy的神奇功能。

kube-proxy

根据上面的示例我们知道service并不是一个真实存在的设备。在k8s里面,一切都是资源,而service这种资源也仅仅是其内部数据库中存储的关于如何配置其他软件的记录。而其中最重要的配置就是对于kubeproxy的配置。
从名字看,我们可能认为kube-proxy和传统的haproxy等一样,先分别和client和server建立连接,然后进行数据的中转分发。其实不然。它是通过利用Linux内核特性netfilter和用户空间的iptables来完成上述报文的重定向的。

  • netfilter是linux内核中的回调函数集,它允许向各个网络栈注册回调函数。当网络栈中的报文满足条件时就会触发回调函数。而对应着当前的情况,我们可以认为它是一个基于一定规则的包处理引擎。它在内核中运行,然后监视每一个报文,当它发现当前报文满足一定的转发规则时就会将报文重定向。所以它其实就是一个内核中的proxy。
  • iptables就是提供给netfilter转发规则的表结构。它存储的记录包含了特定报文的匹配规则以及最后的转发目的地址。

在k8s的发展历程中,kube-proxy的结构其实一直在发生着变化:

  • 在早期的k8s 1.1及之前版本,kube-proxy是一个单纯的用户空间或者用户和内核空间混合的代理。它主要是由kube-proxy在node的网络接口上监听一个端口(如下图的10400),同时设置netfilter的iptables路由规则将所有发给service的报文都转到自己的端口下,然后再将报文转发给某一个pod。这种做法其实很不优雅,特别是以为netfilter在内核空间,而kube-proxy在用户空间,那么一个报文就会在内核空间和用户空间转换,导致效率低下。
    user space 模式

  • 到了k8s 1.2 版本,这种结构得到了改善:kube-proxy将重定向的功能也让netfilter来完成,而它自己只负责维护和同步匹配service IP以及重定向的iptables规则。这样数据的匹配和转发只会在内核空间进行,大大提高了效率。但是这种模式下kube-proxy就需要实时地通过probe探测service对应的pod的状态,并实时地更新iptables规则。
    iptables 模式

  • 到了k8s 1.9版本,kube-proxy采用了性能更好的Ipvs来设置路由规则。相比于iptables,Ipvs采用hashtable作为数据结构,同时还放在内核空间中。这就意味着它拥有更快的重定向速度和更好的同步性能。而且它还提供更为丰富的均衡算法,比如rr, lc, dh等等。

我们可以看到随着k8s的发展,kube-proxy的性能变得越来越好。而且获得改善的不仅仅是性能。我们可以看到在k8s 1.1及之前,kube-proxy还是service核心功能的所在,虽然它也是作为系统性的功能存在,可以在挂掉后马上重启,但是它终究还是一个单节点而非集群,所以对k8s的稳定性有一定影响。但是切换到iptables或者ipvs模式之后,service的核心功能则被netfilter接管了,而netfilter作为Linux内核的功能,只有node保持正常,它就能保持正常工作。这对于k8s的稳定性来说有了很大的提升。kube-proxy只负责配置工作,它通过kubelet来获取pod的状态信息来实时地对netfilter规则进行增删改,从而保证service功能的正常运行。
这篇文章以及上一篇文章我们只讲述了cluster内部的网络通信模型。那么cluster和外部的网络交互模型是什么样的呢?让我们在下一篇讲 Kubernetes网络简析之三:Ingress 的时候来慢慢分析。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值