Kubernetes Service详解

为何须要service

Kubernetes能够方便的为容器应用提供了一个持续运行且方便扩展的环境,可是,应用最终是要被用户或其余应用访问、调用的。要访问应用pod,就会有如下两个问题:node

  1. pod是有生命周期的。它会根据集群的指望状态不断的在建立、删除、更新,因此pod的ip也在不断变化,如何访问到不断变化的pod?
  2. 一般一个应用不会单只有一个pod,而是由多个相同功能的pod共同提供服务的。那么对这个应用的访问,如何在多个pod中负载均衡?

service主要就是用来解决这两个问题的。简单来讲,它是一个抽象的api对象,用来表示一组提供相同服务的pod及对这组pod的访问方式。web

service的实现

service做为一个相似中介的角色,对内,它要能代理访问到不断变换的一组后端Pod;对外,它要能暴露本身给集群内部或外部的其余资源访问。咱们分别来看下具体是怎么实现的。segmentfault

后端代理

以前的文章kubeadm部署最后的测试部分,建立了一组pod及服务来验证业务,继续以这个例子来讲明:后端

集群中已经有以下一组pod:api

NAME                     READY   STATUS       IP            NODE     APP
goweb-55c487ccd7-5t2l2   1/1     Running     10.244.1.15   node-1   goweb
goweb-55c487ccd7-cp6l8   1/1     Running     10.244.3.9    node-2   goweb
goweb-55c487ccd7-gcs5x   1/1     Running     10.244.1.17   node-1   goweb
goweb-55c487ccd7-pp6t6   1/1     Running     10.244.3.10   node-2   goweb

pod都带有app:goweb标签,对外暴露8000端口,访问/info路径会返回主机名。架构

建立service

建立一个servcie有两种方式app

  • 命令式
$ kubectl expose deployment goweb --name=gowebsvc --port=80  --target-port=8000
  • 声明式
# 定义服务配置文件
# svc-goweb.yaml
apiVersion: v1
kind: Service
metadata:
  name: gowebsvc
spec:
  selector:
    app: goweb
  ports:
  - name: default
    protocol: TCP
    port: 80
    targetPort: 8000
  type: ClusterIP
# 建立服务
$ kubectl apply -f svc-goweb.yaml

咱们来看下配置文件中几个重点字段:负载均衡

  • selector指定了app: goweb标签。说明该svc代理全部包含有"app: goweb"的pod
  • port字段指定了该svc暴露80端口
  • targetPort指定改svc代理对应pod的8000端口
  • type定义了svc的类型为ClusterIP,这也是svc的默认类型

经过apply建立服务后,来查看一下服务状态less

$ kubectl  get svc gowebsvc  -o wide
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE   SELECTOR
gowebsvc   ClusterIP   10.106.202.0   <none>        80/TCP    3d   app=goweb

能够看到,Kubernetes自动为服务分配了一个CLUSTER-IP。经过这个访问这个IP的80端口,就能够访问到"app: goweb"这组pod的8000端口,而且能够在这组pod中负载均衡。dom

[root@master-1 ~]# curl http://10.106.202.0/info
Hostname: goweb-55c487ccd7-gcs5x
[root@master-1 ~]# curl http://10.106.202.0/info
Hostname: goweb-55c487ccd7-cp6l8
[root@master-1 ~]# curl http://10.106.202.0/info
Hostname: goweb-55c487ccd7-pp6t6

请求代理转发

cluster-ip是一个虚拟的ip地址,并非某张网卡的真实地址。那具体的请求代理转发过程是怎么实现的呢? 答案是iptables。咱们来看下iptables中与cluster-ip相关的规则

[root@master-1 ~]# iptables-save | grep 10.106.202.0
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-SVC-SEG6BTF25PWEPDFT

能够看到,目的地址为CLUSTER-IP、目的端口为80的数据包,会被转发到KUBE-MARK-MASQ与KUBE-SVC-SEG6BTF25PWEPDFT链上。其中,KUBE-MARK-MASQ链的做用是给数据包打上特定的标记(待验证),重点来看下KUBE-SVC-SEG6BTF25PWEPDFT链:

-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-5ZXTVLEM4DKNW7T2
-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-EBFXI7VOCPDT2QU5
-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-C3PKSXKMO2M43SPF
-A KUBE-SVC-SEG6BTF25PWEPDFT -j KUBE-SEP-2GQCCNJGO65Z5MFS

能够看到,KUBE-SVC-SEG6BTF25PWEPDFT链经过设置--probability,将请求等几率转发到4条链上,查看其中一条转发链:

[root@master-1 ~]# iptables-save | grep  "A KUBE-SEP-5ZXTVLEM4DKNW7T2" 
-A KUBE-SEP-5ZXTVLEM4DKNW7T2 -s 10.244.1.15/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-5ZXTVLEM4DKNW7T2 -p tcp -m tcp -j DNAT --to-destination 10.244.1.15:8000

发现KUBE-SEP-5ZXTVLEM4DKNW7T2这条规则对请求的目的地址做了DNAT到10.244.1.15:8000,这正是goweb组中goweb-55c487ccd7-5t2l2这个pod的ip地址。这样,对svc的CLUSTER-IP的请求,就会经过iptables规则转发到相应的pod。

可是,还有个问题,svc是怎么跟踪pod的ip变化的?
注意到前面的nat规则,第一次转发的链名称是KUBE-SVC-xxx,第二次转发给具体pod的链名称是KUBE-SEP-xxx,这里的SEP实际指的是kubernetes另外一个对象endpoint,咱们能够经过vkubectl get ep命令来查看:

[root@master-1 ~]# kubectl  get ep gowebsvc
NAME         ENDPOINTS 
gowebsvc     10.244.1.15:8000,10.244.1.17:8000,10.244.3.10:8000 + 1 more...   35d

在svc建立的时候,kube-proxy组件会自动建立同名的endpoint对象,动态地跟踪匹配selector的一组pod当前ip及端口,并生成相应的iptables KUBE-SVC-xxx规则。

请求代理的三种方式

上面说的请求代理转发的方式,是kubernetes目前版本的默认方式,实际上,service的代理方式一共有三种:

Userspace 模式

在这种模式下,kube-proxy为每一个服务都打开一个随机的端口,全部访问这个端口的请求都会被转发到服务对应endpoints指定的后端。最后,kube-proxy还会生成一条iptables规则,把访问cluster-ip的请求重定向到上面说的随机端口,最终转发到后端pod。整个过程以下图所示:

clipboard.png

Userspace模式的代理转发主要依靠kube-proxy实现,工做在用户态。因此,转发效率不高。较为不推荐用该种模式。

iptables 模式

iptables模式是目前版本的默认服务代理转发模式,上两小节作过详细说明的就是这种模式,来看下请求转发的示意图:

clipboard.png

与userspace模式最大的不一样点在于,kube-proxy只动态地维护iptables,而转发彻底靠iptables实现。因为iptables工做在内核态,不用在用户态与内核态切换,因此相比userspace模式更高效也更可靠。可是每一个服务都会生成若干条iptables规则,大型集群iptables规则数会很是多,形成性能降低也不易排查问题。

ipvs 模式

在v1.9版本之后,服务新增了ipvs转发方式。kube-proxy一样只动态跟踪后端endpoints的状况,而后调用netlink接口来生成ipvs规则。经过ipvs来转发请求:

clipboard.png

ipvs一样工做在内核态,并且底层转发是依靠hash表实现,因此性能比iptables还要好的多,同步新规则也比iptables快。同时,负载均衡的方式除了简单rr还有多种选择,因此很适合在大型集群使用。而缺点就是带来了额外的配置维护操做。

集群内部服务发现

在集群内部对一个服务的访问,主要有2种方式,环境变量与DNS。

环境变量方式

当一个pod建立时,集群中属于同个namespace下的全部service对象信息都会被做为环境变量添加到pod中。随便找一个pod查看一下:

$ kubectl exec goweb-55c487ccd7-5t2l2 'env' | grep GOWEBSVC
GOWEBSVC_PORT_80_TCP_ADDR=10.106.202.0
GOWEBSVC_SERVICE_PORT=80
GOWEBSVC_SERVICE_PORT_DEFAULT=80
GOWEBSVC_PORT_80_TCP=tcp://10.106.202.0:80
GOWEBSVC_PORT_80_TCP_PROTO=tcp
GOWEBSVC_PORT_80_TCP_PORT=80
GOWEBSVC_PORT=tcp://10.106.202.0:80
GOWEBSVC_SERVICE_HOST=10.106.202.0

能够看到,pod经过{SVCNAME}_SERVICE_HOST/PORT就能够方便的访问到某个服务。这种访问方式简单易用,能够用来快速测试服务。但最大的问题就是,服务必须先于pod建立,后建立的服务是不会添加到现有pod的环境变量中的。

DNS方式

DNS组件是k8s集群的可选组件,它会不停监控k8s API,在有新服务建立时,自动建立相应的DNS记录。。以gowebsvc为例,在服务建立时,会建立一条gowebsvc.default.svc.cluster.local的dns记录指向服务。并且dns记录做用域是整个集群,不局限在namespace。
虽然是可选组件,但DNS生产环境能够说是必备的组件了。这里先简单说明,后面打算专门开篇文章来详细介绍。

集群外部的服务暴露

服务发现解决了集群内部访问pod问题,但不少时候,pod提供的服务也是要对集群外部来暴露访问的,最典型的就是web服务。k8s中的service有多种对外暴露的方式,能够在部署Service时经过ServiceType字段来指定。默认状况下,ServiceType配置是只能内部访问的ClusterIP方式,前面的例子都是这种模式,除此以外,还能够配置成下面三种方式:

NodePort方式:

该方式把服务暴露在每一个Node主机IP的特定端口上,同一个服务在全部Node上端口是相同的,并自动生成相应的路由转发到ClusterIP。这样,集群外部经过<NodeIP>:<NodePort>就能够访问到对应的服务。举个例子:

## 建立svc,经过Nodeport方式暴露服务
$ kubectl expose deployment goweb --name=gowebsvc-nodeport --port=80  --target-port=8000  --type=NodePort 
## 查看svc,能够看到NodePort随机分配的端口为32538
$ kubectl get svc gowebsvc-nodeport 
NAME                TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
gowebsvc-nodeport   NodePort   10.101.166.252   <none>        80:32538/TCP   86s
## 随便访问一个nodeip的32538端口,均可以访问到gowebsvc-nodeport服务对应的pod
$ curl 172.16.201.108:32538/info
Hostname: goweb-55c487ccd7-pp6t6
$ curl 172.16.201.109:32538/info
Hostname: goweb-55c487ccd7-5t2l2

LoadBalance:

LoadBalance方式主要是给公有云服务使用的,经过配置LoadBalance,能够触发公有云建立负载均衡器,并把node节点做为负载的后端节点。每一个公有云的配置方式不一样,具体能够参考各公有云的相关文档。

ExternalName:

当ServiceType被配置为这种方式时,该服务的目的就不是为了外部访问了,而是为了方便集群内部访问外部资源。举个例子,假如目前集群的pod要访问一组DB资源,而DB是部署在集群外部的物理机,尚未容器化,能够配置这么一个服务:

apiVersion: v1
kind: Service
metadata:
  name: dbserver
  namespace: default
spec:
  type: ExternalName
  externalName: database.abc.com

这样,集群内部的pod经过dbserver.default.svc.cluster.local这个域名访问这个服务时,请求会被cname到database.abc.com来。事后,假如db容器化了,不须要修改业务代码,直接修改service,加上相应selector就能够了。

几种特殊的service

除了上面这些一般的service配置,还有几种特殊状况:

Multi-Port Services

service能够配置不止一个端口,好比官方文档的例子:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  - name: https
    protocol: TCP
    port: 443
    targetPort: 9377

这个service保留了80与443端口,分别对应pod的9376与9377端口。这里须要注意的是,pod的每一个端口必定指定name字段(默认是default)。

Headless services

Headless services是指一个服务没有配置了clusterIP=None的服务。这种状况下,kube-proxy不会为这个服务作负载均衡的工做,而是交予DNS完成。具体又分为2种状况:

  • 有配置selector: 这时候,endpoint控制器会为服务生成对应pod的endpoint对象。service对应的DNS返回的是endpoint对应后端的集合。
  • 没有配置selector:这时候,endpoint控制器不会自动为服务生成对应pod的endpoint对象。若服务有配置了externalname,则生成一套cnmae记录,指向externalname。若是没有配置,就须要手动建立一个同名的endpoint对象。dns服务会建立一条A记录指向endpoint对应后端。

External IPs

若是有个非node本地的IP地址,能够经过好比外部负载均衡的vip等方式被路由到任意一台node节点,那就能够经过配置service的externalIPs字段,经过这个IP地址访问到服务。集群以这个IP为目的IP的请求时,会把请求转发到对应服务。参考官方文档的例子:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  externalIPs:
  - 80.11.12.10

这里的80.11.12.10就是一个不禁kubernetes维护的公网IP地址,经过80.11.12.10:80就能够访问到服务对应的pod。

简单总结下,service对象实际上解决的就是一个分布式系统的服务发现问题,把相同功能的pod作成一个服务集也能很好的对应微服务的架构。在目前的kubernetes版本中,service还只能实现4层的代理转发,而且要搭配好DNS服务才能真正知足生产环境的需求。

相关文章
相关标签/搜索