1.Service

Service是Kubernetes的核心资源类型之一,通常可看作为服务的一种实现。事实上它是一种抽象:通过规则定义出由多个Pod对象组合而成的逻辑集合,以及访问这组Pod的策略。Service关联Pod资源的规则要借助于标签选择器来完成,这一点类似于Pod控制器。

1.1 Service资源概述

由Deployment等控制器管理的Pod对象中断后会由新建的资源对象所取代,而扩缩容后的应用则会带来Pod对象群体的变动,随之变化的还有Pod的IP地址接口等,这也是编排系统之上的应用程序必然面临的问题。

Service资源基于标签选择器将一组Pod定义成一个逻辑组合,并通过自己的IP地址和端口调度代理请求至组内的Pod对象之上,它向客户端隐藏了真实的、处理用户请求的Pod资源,使得客户端的请求看上去就像是由Service直接处理并进行响应的一样。

Service对象的IP地址也称为Cluster IP,它位于为k8s集群配置指定专用IP地址的范围之内,而且是一种虚拟IP地址,它在Service对象创建后即保持不变,并且能够被同意集群中的Pod资源所访问。Service端口用于接收客户端请求并将其转发至后端的Pod中应用的相应端口之上,因此,这种代理机制也称为端口代理(port proxy)或四层代理,它工作与TCP/IP协议栈的传输层。

通过其标签选择器匹配到的后端Pod资源不止一个时,Service资源能够以负载均衡的方式进行流量调度,实现了请求流量的分发机制。Service与Pod对象之间的关联关系通过标签选择器以松耦合的方式建立,它可以先于Pod对象创建而不会发生错误,于是,创建Service与Pod资源的任务可由不同的用户分别完成。

Service资源会通过API Server持续监视者标签选择器匹配到的后端Pod对象,并实时跟踪对象的变动,例如,IP变动、对象增加或减少等。不过,需要特别说明的是,Service并不直接链接至Pod对象,它们之间还有一个中间层—Endpoints资源对象,它是一个由IP地址和端口组成的列表,这些IP和端口则来自于由Service的标签选择器匹配到的Pod资源。这也是很多场景中会使用“Service的后端端点(Endpoint)”这一术语的原因。默认情况下,创建Service资源对象时,其关联的Endpoints对象会自动创建。

1.2 虚拟IP和服务代理

简单来说,一个Service对象就是工作节点上的一些iptables或ipvs规则,用于将到达Service对象IP地址的流量调度转发至相应的Endpoints对象指向的IP地址和端口之上。工作于每个工作节点的kube-proxy组件通过API Server持续监控着各Service及其关联的Pod对象,并将其创建或变动实时反映至当前工作节点上相应的iptables或ipvs规则上。

kube-proxy将请求代理至相应端点的方式有三种:userspace(用户空间)、iptables和ipvs:

1.2.1 userspace代理模型

userspace是指linux操作系统的用户空间。这种模型中,kube-proxy负责跟踪API Server上Service和Endpoints对象的变动,并据此调整Service资源的定义。对于每个Service对象,它会随机打开一个本地端口(运行于用户空间的kube-proxy进程负责监听),任何到达此代理端口的连接请求都将被代理至当前Service资源后端的各Pod对象上,至于挑中哪个Pod对象取决于Service资源的调度方式,默认是轮询(round-robin),此类的Service对象还会创建iptables规则以捕获任何到达ClusterIP和端口的流量。

这种代理模型中,请求流量到达内核空间后经由套接字送往用户空间的kube-proxy,而后再由它送回内核空间,并调度至后端Pod。这种方式中,请求在内核空间和用户空间来回转发必然会导致效率不高。

1.2.2 iptables代理模型

在创建Service资源时,集群中每个节点上的kube-proxy都会收到通知并将其定义为当前节点上的iptables规则,用于转发工作接口接收到的与此Service资源的ClusterIP和端口的相关流量。客户端发来的请求被相关的iptables规则进行调度和目标地址转发(DNAT)后再转发至集群内的Pod对象之上。

相对于用户空间模型来说,iptables模型无须将流量在用户空间和内核空间来回切换,因而更加高效和可靠。不过,其缺点是iptables不会在被挑中的后端Pod资源无影响时自动进行重定向,而userspace模型则可以。

1.2.3 ipvs代理模型

kube-proxy跟踪API Server上Service和Endpoints对象的变动,据此来调用netlink接口创建ipvs规则,并确保与API Server中的变动保持同步。它与iptables规则的不同之处仅在于其请求流量的调度功能由ipvs实现,余下的其他功能仍由iptables完成。

类似于iptables模型,ipvs构建于netfilter的钩子函数上,但它使用hash表作为底层数据结构并工作于内核空间,因此具有流量转发速度快、规则同步性能好的特性。ipvs还支持众多调度算法,例如rr、lc、dh、sh、sed和nq等。

1.3 Service资源的基础应用

Service资源本身并不提供服务,真正处理并响应客户端请求的是后端的Pod资源,因此Service资源通常要与控制器资源(最为常用的之一是Deployment)协同使用以完成应用的创建和对外发布。

1.3.1 创建Service资源

Service示例:

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: myapp-svc
  5. spec:
  6. selector:
  7. app: myapp
  8. ports:
  9. - protocol: TCP
  10. port: 80
  11. targetPort: 80

Service资源myapp-svc通过标签选择器关联至标签为”app=myapp”的各Pod对象,它会自动创建名为myapp-svc的Endpoints,并自动配置一个ClusterIP,暴露的端口由port字段进行指定,后端各Pod对象的端口则由targetPort给出,也可以使用同port字段的默认值。

# kubectl get svc myapp-svc
NAME        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
myapp-svc   ClusterIP   10.106.76.254   <none>        80/TCP    4m43s

上面命令中的结果显示,myapp-svc的类型为默认的ClusterIP,其使用的地址自动配置为10.106.76.254。此类型的Service对象仅能通过此IP地址接受来自于集群内的客户端Pod中的请求。若集群上存在标签为”app=myapp”的Pod资源,则它们会被关联和创建,作为此Service对象的后端Endpoint对象,并负责接收相应的请求流量。

# kubectl get endpoints myapp-svc
NAME        ENDPOINTS                                               AGE
myapp-svc   10.244.167.131:80,10.244.167.190:80,10.244.167.191:80   7m24s

1.3.2 Service会话粘性

Service资源还支持Session affinity(粘性会话)机制,它能够将来自同一个客户端的请求始终转发至同一个后端的Pod对象。当客户端访问Pod中的应用程序时,如果有基于客户端身份保存某些私有信息,并基于这些私有信息追踪用户的活动等一类的需求时,那么应该启用session affinity机制。

Session affinity的效果仅会在一定期限内生效,默认为10800秒,超出此时长之后,客户端的再次访问会被调度算法重新调度。另外,Service资源的Session affinity机制仅能基于客户端IP识别客户端身份,它会把经由同一个NAT服务器进行源地址转换的所有客户端识别为同一个客户端,调度粒度粗燥且效果不佳,因此,实践中不推荐使用此种实现粘性会话。仅介绍其功能及实现。

Service通过.spec.sessionAffinity和.spec.sessionAffinityConfig两个字段配置粘性会话。.spec.sessionAffinity字段用于定义要使用的粘性会话的类型,它仅支持使用None和ClientIP两种:

  • None: 不实用sessionAffinity,默认值;
  • ClientIP: 基于客户端IP地址识别客户端身份,把来自同一个源Ip地址的请求始终调度至同一个Pod对象。

.spec.sessionAffinityConfig配置其会话保持时长,范围为”1 ~ 86400”,默认为10800秒:

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: <integer>

1.4 服务发现

1.4.1 服务发现方式:环境变量

创建Pod资源时,kubelet会将其所属名称空间内的每个活动的Service对象以一系列环境变量的形式注入其中。它支持使用k8s Service环境变量以及与Docker的links兼容的变量。

K8s为每个Service资源生成包括以下形式的环境变量在内的一系列环境变量,在同一名称空间中创建的Pod对象都会自动拥有这些变量。

  • {SVCNAME}_SERVICE_HOST
  • {SVCNAME}_SERVICE_PORT

注意:如果SVCNAME中使用了连接线,那么k8s会在定义环境变量时将其转换为下划线。

1.4.2 ClusterDNS和服务发现

k8s系统之上用于名称解析和服务发现的ClusterDNS是集群的核心附件之一,集群中创建的每个Service对象,都会由其自动生成相关的资源记录。默认情况下,集群内各Pod资源会自动配置其作为名称解析服务器,并在其DNS搜索列表中包含它所属名称空间的域名后缀。

无论是使用kubeDNS还是CoreDNS,它们提供的基于DNS的服务发现解决方案都会负责解析以下资源记录类型以实现服务发现。

(1)拥有ClusterIP的Service资源:

  • A 记录: ..svc.. IN A
  • SRV 记录
  • PTR 记录

(2)Headless类型的Service资源:

  • A 记录: ..svc.. IN A
  • SRV 记录
  • PTR 记录

(3)ExternalName类型的Service资源:

  • CNAME 记录: ..svc.. IN CNAME

1.4.3 服务发现方式:DNS

创建Service资源对象时,ClusterDNS会为它自动创建资源记录用于名称解析和服务注册,Pod资源可直接使用标准的DNS名称来访问这些Service资源。每个Service对象相关的DNS记录包含如下两个:

  • {SVCNAME}.{NAMESPACE}.{CLUSTER_DOMAIN}
  • {SVCNAME}.{NAMESPACE}.svc.{CLUSTER_DOMAIN}

另外,部署参数中,”—cluster-dns”指定了集群DNS服务的工作地址,”—cluster-domain”定义了集群使用的本地域名,因此,系统初始化时默认会将”cluster.local.”和主机所在的域”ilinux.io”作为DNS的本地域使用,这些信息会在Pod创建时以DNS配置的相关信息注入它的/etc/resolv.conf配置文件中。

# kubectl exec -it myapp-deploy-7866747958-rkzwk -- /bin/sh
/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

上述search参数中指定的DNS各搜索域,是以次序指定的几个域名后缀:

  • {NAMESPACE}.svc.{CLUSTER_DOMAIN}
  • svc.{CLUSTER_DOMAIN}
  • {CLUSTER_DOMAIN}
  • {WORK_NODE_DOMAIN}

1.5 服务暴露

1.5.1 Service 类型

  • ClusterIP:通过集群内部IP地址暴露服务,此地址仅在集群内部可达,而无法被集群外部的客户端访问,此为默认的Service类型;
  • NodePort:这种类型构建在ClusterIP类型之上,NodoPort类型是在工作节点的IP地址上选择一个端口用于将集群外部的用户请求转发目标Service的ClusterIP和Port,这种类型的Service即可受到集群内部客户端Pod的访问,也会受到集群外部客户端通过套接字:进行的请求。
  • LoadBalancer: 这种类型构建在NodePort类型之上,一个LoadBalancer类型的Service会指向关联至k8s集群外部的、切实存在的某个负载均衡设备,该设备通过工作节点之上的NodePort向集群内不发送请求流量。此类型的优势在于,它能够把来自于集群外部客户端的请求调度至所有节点(或部分节点)的NodePort上,而不是依赖与客户端自行决定连接至哪个节点,从而避免了因客户端指定的节点故障而导致的服务不可用。
  • ExternalName: 通过将Service映射至由ExternalName字段的内容指定的主机名来暴露服务,此主机名需要被DNS服务解析至CNAME类型的记录。此种类型并非定义由k8s集群提供的服务,而是把集群外部的某服务以DNS CNAME记录的方式映射到集群内,从而让集群内的Pod资源能够访问外部的Service的一种实现方式。

创建Service默认为ClusterIP类型,它仅能接收来自于集群中的Pod对象中的客户端程序的访问请求。如若需要将Service资源发布至网络外部,应该将其配置为NodePort或LoadBalancer类型,而若要把外部的服务发布于集群内供Pod对象使用,则需要定义一个ExternalName类型的Service资源。

1.5.2 NodePort类型

NodePort即节点Port,通常在安装部署k8s集群系统时预留一个端口范围用于NodePort,默认为30000~32767之间的端口。与ClusterIP类型的可省略.spec.type属性所不同的是,定义NodePort类型的Service资源时,需要通过此属性明确指定其类型名称。例如,下边创建一个NodePort类型,且人为指定其节点端口为32223:

apiVersion: v1
kind: Service
metadata:
  name: myapp-svc-nodeport
spec:
  type: NodePort
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 32223

注意:并不鼓励用户自定义使用的节点端口,除非事先能够明确知道它不会与某个现存的Service资源产生冲突,无论如何,只要没有特别需求,留给系统自动配置总是较好的选择。

请求流量转发过程:External Clients —> NodeIP:NodePort —> ServiceIP:ServicePort —> PodIP:PodPort

因此,对于集群外部的客户端来说,它们可经由任何一个节点的IP及端口访问NodePort类型的Service资源,而对于集群内的Pod客户端来说,依然可以通过ClusterIP对其进行访问。

1.5.3 LoadBalancer类型

NodePort类型的Service资源虽然能够于集群外部访问得到,但外部客户端必须得事先得知NodePort和集群中至少一个节点的IP地址,且选定的节点发生故障时,客户端还得自己选择请求访问其他的节点。因此,一般还应该在集群之外创建一个具有公网IP地址的负载均衡器,由它接入外部客户端的请求并调度至集群节点相应的NodePort上。

IaaS云计算环境通常提供了LBaaS(Load Balancer as a Service)服务,它允许租户动态地在自己的网络中创建一个负载均衡设备。那么部署于此类环境之上的k8s集群在创建Service资源时可以直接调用此接口按需创建出一个软负载均衡器,而具有这种功能的Service资源即为LoadBalancer类型。不够,如果k8s部署于裸的物理服务器之上,系统管理员也可以自行手动部署一个负载均衡器(推荐使用冗余配置),并配置其将请求流量调度至个节点的NodePort之上即可。

下面是一个LoadBalancer配置清单,需要注意的是,有些环境还需要为Service资源定义添加Annotations:

apiVersion: v1
kind: Service
metadata:
  name: myapp-svc-lb
spec:
  type: LoadBalancer
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targerPort: 80
    nodePort: 32223

进一步地,在IaaS环境支持手动指定IP地址时,用户还可以使用.spec.loadBalancerIP指定创建的负载均衡使用的IP地址,并可使用.spec.loadBalancerSourceRanges指定负载均衡器允许的客户端来源的地址范围。

1.5.4 ExternalName类型

ExternalName类型用于将集群外部的服务发布到集群中以供Pod中的应用程序访问,因此,它不需要使用标签选择器关联任何的Pod对象,但必须要使用.spec.externalName属性定义一个CNAME记录用于返回外部真正提供服务的主机的别名,而后通过CNAME记录值获取到相关主机的IP地址。

apiVersion: v1
kind: Service
metadata:
  name: external-redis-svc
spec:
  type: ExternalName
  externalName: redis.ilinux.io
  ports:
  - protocol: TCP
    port: 6379
    nodePort: 0
  selector: {}

各Pod对象即可通过external-redis-svc或其FQDN格式的名称external-redis-svc.default.svc.cluster.local访问相应的服务。ClusterDNS会将此名称以CNAME格式解析为.spec.externalName字段中的名称,而后通过DNS服务将其解析为相应的主机的IP地址。

1.6 Headless类型

Service对象隐藏了各Pod资源,并负责将客户端的请求流量调度至该组Pod对象之上。不过,偶尔也会存在这样一类需求:客户端需要直接访问Service后端的所有Pod,这时就应该向客户端暴露每个Pod资源的IP地址,而不再是中间层Service对象的ClusterIP,这种类型的Service资源便成为Headless Service。

Headless Service对象没有ClusterIP,于是kube-proxy便无须处理此类请求,也就更没有了负载均衡或代理它的需要。至于如何为此类Service资源配置IP地址,则取决于它的标签选择器的定义。

  • 具有标签选择器:端点控制器(Endpoints Controller)会在API中为其创建Endpoints记录,并将ClusterDNS服务中的A记录直接解析到此Service后端的各Pod对象的IP地址上。
  • 没有标签选择器:端点控制器(Endpoints Controller)不会在API中为其创建Endpoints记录,ClusterDNS的配置分为两种情形,对ExternalName类型的服务创建CNAME记录,对其他三种类型来说,为那些与当前Service共享名称的所有Endpoints对象创建一条记录。

1.6.1 创建Headless Service资源

配置Service资源配置清单时,只需要将ClusterIP字段设置为”None”即可将其定义为Headless类型。下面是一个示例:

apiVersion: v1
kind: Service
metadata:
  name: myapp-headless-svc
spec:
  clusterIp: None
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 80
    name: httpport

创建后,它没有ClusterIP,不过,如果标签选择器能够匹配到相关的Pod资源,它便拥有Endpoints记录,这些Endpoints对象会作为DNS资源记录名称myapp-headless-svc查询时的A记录解析结果。

1.6.2 Pod资源发现

根据Headless Service的工作特性可知,它记录于ClusterDNS的A记录的相关解析结果是后端Pod资源的IP地址,这就意味着客户端通过此Service资源的名称发现的各Pod资源。

# kubectl run cirros-$RANDOM --rm -it --image=cirros -- sh
/ # nslookup myapp-headless-svc
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name: myapp-headless-svc
Address 1: 10.244.2.13
...

2.Ingress资源

K8s提供了两种内建的云端负载均衡机制(cloud load balancing)用于发布公共应用,一种是工作于传输层的Service资源,它实现的是TCP负载均衡器,另一种是Ingress资源,它实现的是HTTP(S)负载均衡器。

  • TCP负载均衡器:无论是iptables还是ipvs模型的Service都配置于Linux内核中的Netfilter之上进行四层调度,是一种类型更为通用的调度器,支持调度HTTP、MySQL等应用层服务。不过,也正是由于工作于传输层从而使得它无法做到类似卸载HTTPS中的SSL会话等一类操作,也不支持基于URL的请求调度机制,k8s也不支持为此类负载均衡器配置任何类型的健康状态检查机制。
  • HTTP(S)负载均衡器:应用层负载均衡机制的一种,支持根据环境做出更好的调度决策。与传输层调度器相比,它提供了诸如可自定义URL映射和TLS卸载等功能,并支持多种类型的后端服务器健康状态检查机制。

2.1 Ingress和Ingress Controller

Ingress是Kubernetes API的标准资源类型之一,它其实就是一组基于DNS名称或URL路径把请求转发至指定的Service资源的规则,用于将集群外部的请求流量转发至集群内部完成服务发布。然后,Ingress资源自身并不能进行“流量穿透”,它仅是一组路由规则的集合,这些规则要想真正发挥作用还需要其他功能的辅助,如监听某套接字,然后根据这些规则的匹配机制路由请求流量。这种能够为Ingress资源监听套接字并转发流量的组件称为Ingress控制器(Ingress Controller)。

注意:Ingress是k8s集群的一个重要附件,类似于CoreDNS,需要在集群上单独部署。

Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,如Nginx、Envoy、HAProxy、Vulcand和Traefik等。Ingress控制器自身也是运行于集群中的Pod对象,它与被代理的运行为Pod的应用运行于同一网络中。

2.2 创建Ingress资源

Ingress资源是基于HTTP虚拟主机或URL的转发规则,它在资源配置清单的spec字段中嵌套了rules、backend和tls等字段进行定义。

下面示例中定义了一个Ingress资源,它包含了一个转发规则,把发往www.ilinux.io的请求代理给名为myapp-svc的Service资源:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - host: www.ilinux.io
    http:
      paths:
      - backend:
        serviceName: myapp-svc
        servicePort: 80

Ingress Spec主要嵌套如下三个字段:

  • rules : 用于定义当前Ingress资源的转发规则列表;未由rules定义规则或者没有匹配到任何规则时,所有流量都会转发到由backend定义的默认后端。
  • backend : 默认的后端用于服务那些没有匹配到任何规则的请求;定义Ingress资源时,至少应该定义backend或rules两者之一;此字段用于让负载均衡器指定一个全局默认的后端。
  • tls : TLS配置,目前仅支持通过默认端口443提供服务;如果要配置指定的列表成员指向了不同的主机,则必须通过SNI TLS扩展机制来支持此功能。

    backend对象的定义由两个必选的内嵌字段组成:ServiceName和ServicePort,分别用于指定流量转发的后端目标Service资源的名称和端口。

    rules对象由一系列配置Ingress资源的Host规则组成,这些host规则用于将一个主机上的某个URL路径映射至相关的后端Service对象:

    spec:
      rules:
      - host: <String>
        http:
          paths:
            backend:
              serviceName: <String>
              servicePort: <String>
            path: <String>
    

    注意:.spec.rules.host属性值不支持使用IP地址,也不支持后跟”:PORT”格式的端口号,且此字段值留空表示统配所有的主机名。

    tls对象由两个内嵌字段组成,仅在定义TLS主机的转发规则时才需要定义此类对象。

    • hosts: 包含于使用的TLS证书之内的主机名称字符串列表,此处使用的主机名必须匹配tlsSecret中的名称。
    • secretName:用于引用SSL会话的secret对象名称,在基于SNI实现多主机路由的场景中,此字段为可选。

    2.3 Ingress资源类型

    基于HTTP暴露的每个Service资源均可发布于一个独立的FQDN主机名之上,如”www.ik8s.io”;也可发布于某主机的URL路径之上,从而将它们整合到同一个Web站点,如”www.ik8s.io/grafana”。

    2.3.1 单Service资源型Ingress

    暴露单个服务,使用Ingress时,只需要为Ingress指定”default backend”即可。例如:

    apiVersion: extensions/v1beta1
    kind: Ingress
    metadata:
      name: my-ingress
    spec:
      backend:
        serviceName: my-svc
        servicePort: 80
    

    Ingress控制器会为其分配一个IP地址接入请求流量,并将它们转至示例中的my-svc后端。

    2.3.2 基于URL路径进行流量分发

    例如,使用Ingress资源将对www.ilinux.io/api的请求转发至API Service资源,将对www.ilinux.io/wap的请求转发至WAP Service资源:

    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    metadata:
      name: simple-fanout-example
      annotations:
        nginx.ingress.kubernetes.io/rewrite-target: /
    spec:
      rules:
      - host: www.ilinux.io
        http:
          paths:
          - path: /api
            backend:
              serviceName: api
              servicePort: 80
          - path: /wap
            backend:
              serviceName: wap
              servicePort: 80
    

    2.3.3 基于主机名称的虚拟主机

    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    metadata:
      name: name-virtual-host-ingress
    spec:
      rules:
      - host: api.ik8s.io
        http:
          paths:
          - backend:
              serviceName: api
              servicePort: 80
      - host: wap.ik8s.io
        http:
          paths:
          - backend:
              serviceName: wap
              servicePort: 80