为什么需要service

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

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

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

service的实现

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

后端代理

举这个例子来说明:
集群中已经有如下一组pod:

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

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

创建service

创建一个servcie有两种方式

  • 命令式

    1. $ kubectl expose deployment goweb --name=gowebsvc --port=80 --target-port=8000
  • 声明式

    1. # 定义服务配置文件
    2. # svc-goweb.yaml
    3. apiVersion: v1
    4. kind: Service
    5. metadata:
    6. name: gowebsvc
    7. spec:
    8. selector:
    9. app: goweb
    10. ports:
    11. - name: default
    12. protocol: TCP
    13. port: 80
    14. targetPort: 8000
    15. type: ClusterIP
    16. # 创建服务
    17. $ 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创建服务后,来查看一下服务状态

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

可以看到,Kubernetes自动为服务分配了一个CLUSTER-IP。通过这个访问这个IP的80端口,就可以访问到”app: goweb”这组pod的8000端口,并且可以在这组pod中负载均衡。

  1. [root@master-1 ~]# curl http://10.106.202.0/info
  2. Hostname: goweb-55c487ccd7-gcs5x
  3. [root@master-1 ~]# curl http://10.106.202.0/info
  4. Hostname: goweb-55c487ccd7-cp6l8
  5. [root@master-1 ~]# curl http://10.106.202.0/info
  6. Hostname: goweb-55c487ccd7-pp6t6

请求代理转发

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

  1. [root@master-1 ~]# iptables-save | grep 10.106.202.0
  2. -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
  3. -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链:

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

可以看到,KUBE-SVC-SEG6BTF25PWEPDFT链通过设置—probability,将请求等概率转发到4条链上,查看其中一条转发链:

  1. [root@master-1 ~]# iptables-save | grep "A KUBE-SEP-5ZXTVLEM4DKNW7T2"
  2. -A KUBE-SEP-5ZXTVLEM4DKNW7T2 -s 10.244.1.15/32 -j KUBE-MARK-MASQ
  3. -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命令来查看:

  1. [root@master-1 ~]# kubectl get ep gowebsvc
  2. NAME ENDPOINTS
  3. 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。整个过程如下图所示:
Kubernetes Service详解 - 图1
Userspace模式的代理转发主要依靠kube-proxy实现,工作在用户态。所以,转发效率不高。较为不推荐用该种模式。

iptables 模式

iptables模式是目前版本的默认服务代理转发模式,上两小节做过详细说明的就是这种模式,来看下请求转发的示意图:
Kubernetes Service详解 - 图2
与userspace模式最大的不同点在于,kube-proxy只动态地维护iptables,而转发完全靠iptables实现。由于iptables工作在内核态,不用在用户态与内核态切换,所以相比userspace模式更高效也更可靠。但是每个服务都会生成若干条iptables规则,大型集群iptables规则数会非常多,造成性能下降也不易排查问题。

ipvs 模式

在v1.9版本以后,服务新增了ipvs转发方式。kube-proxy同样只动态跟踪后端endpoints的情况,然后调用netlink接口来生成ipvs规则。通过ipvs来转发请求:
Kubernetes Service详解 - 图3
ipvs同样工作在内核态,而且底层转发是依靠hash表实现,所以性能比iptables还要好的多,同步新规则也比iptables快。同时,负载均衡的方式除了简单rr还有多种选择,所以很适合在大型集群使用。而缺点就是带来了额外的配置维护操作。

集群内部服务发现

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

环境变量方式

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

  1. $ kubectl exec goweb-55c487ccd7-5t2l2 'env' | grep GOWEBSVC
  2. GOWEBSVC_PORT_80_TCP_ADDR=10.106.202.0
  3. GOWEBSVC_SERVICE_PORT=80
  4. GOWEBSVC_SERVICE_PORT_DEFAULT=80
  5. GOWEBSVC_PORT_80_TCP=tcp://10.106.202.0:80
  6. GOWEBSVC_PORT_80_TCP_PROTO=tcp
  7. GOWEBSVC_PORT_80_TCP_PORT=80
  8. GOWEBSVC_PORT=tcp://10.106.202.0:80
  9. 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。这样,集群外部通过:就可以访问到对应的服务。举个例子:

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

LoadBalance:

LoadBalance方式主要是给公有云服务使用的,通过配置LoadBalance,可以触发公有云创建负载均衡器,并把node节点作为负载的后端节点。每个公有云的配置方式不同,具体可以参考各公有云的相关文档。

ExternalName:

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

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: dbserver
  5. namespace: default
  6. spec:
  7. type: ExternalName
  8. externalName: database.abc.com

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

几种特殊的service

除了上面这些通常的service配置,还有几种特殊情况:

Multi-Port Services

service可以配置不止一个端口,比如官方文档的例子:

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: my-service
  5. spec:
  6. selector:
  7. app: MyApp
  8. ports:
  9. - name: http
  10. protocol: TCP
  11. port: 80
  12. targetPort: 9376
  13. - name: https
  14. protocol: TCP
  15. port: 443
  16. 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的请求时,会把请求转发到对应服务。参考官方文档的例子:
    1. apiVersion: v1
    2. kind: Service
    3. metadata:
    4. name: my-service
    5. spec:
    6. selector:
    7. app: MyApp
    8. ports:
    9. - name: http
    10. protocol: TCP
    11. port: 80
    12. targetPort: 9376
    13. externalIPs:
    14. - 80.11.12.10
    这里的80.11.12.10就是一个不由kubernetes维护的公网IP地址,通过80.11.12.10:80就可以访问到服务对应的pod。
    简单总结下,service对象实际上解决的就是一个分布式系统的服务发现问题,把相同功能的pod做成一个服务集也能很好的对应微服务的架构。在目前的kubernetes版本中,service还只能实现4层的代理转发,并且要搭配好DNS服务才能真正满足生产环境的需求。