Service资源及其实现模型
Service资源基于标签选择器将一组Pod定义成一个逻辑组合,并通过自己的IP地址和端口调度代理请求至组内的Pod对象之上,它向客户端隐藏了真实的、处理用户请求的Pod资源,使得客户端的请求看上去就像是由Service直接处理并进行响应的一样
Service对象的IP地址也称为Cluster IP,它位于为Kubernetes集群配置指定专用IP地址的范围之内,并且是一种虚拟IP地址,它在Service对象创建后即保持不变,并且能够被同一集群中的Pod资源所访问。Service端口用于接收客户端请求并将其转发到其后端的Pod中应用的相应端口之上,因此,这种代理机制也称为“端口代理”(port proxy)或四层代理,它工作于TCP/IP协议栈的传输层。
通过其标签选择器匹配到的后端Pod资源不止一个时,Service资源能够以负载均衡的方式进行流量调度,实现了请求流量的分发机制。Service与Pod对象之间的关联关系通过标签选择器以松耦合的方式建立,它可以先于Pod对象创建而不会发生错误
Service资源会通过API Server持续监视着(watch)标签选择器匹配到的后端Pod对象,并实时跟踪各对象的变动,例如,IP地址变动、对象增加或减少等。不过,需要特别说明的是,Service并不直接链接至Pod对象,它们之间还有一个中间层—Endpoints资源对象,它是一个由IP地址和端口组成的列表,这些IP地址和端口则来自于由Service的标签选择器匹配到的Pod资源。这也是很多场景中会使用“Service的后端端点”(Endpoints)这一术语的原因。默认情况下,创建Service资源对象时,其关联的Endpoints对象会自动创建。
简单来讲,一个Service对象就是工作节点上的一些iptables或ipvs规则,用于将到达Service对象IP地址的流量调度转发至相应的Endpoints对象指向的IP地址和端口之上。
工作于每个工作节点的kube-proxy组件通过APIServer持续监控着各Service及与其关联的Pod对象,并将其创建或变动实时反映至当前工作节点上相应的iptables或ipvs规则上。
Service IP事实上是用于生成iptables或ipvs规则时使用的IP地址,它仅用于实现Kubernetes集群网络的内部通信,并且仅能够将规则中定义的转发服务的请求作为目标地址予以响应,这也是它被称为虚拟IP的原因之一。kube-proxy将请求代理的相应端点的方式有三种:userspace(用户空间)、iptables和ipvs。
1.userspace(用户空间)
2.iptables
3.ipvs
Service资源的基础应用
Service资源本身并不提供任何服务,真正处理并响应客户端请求的是后端的Pod资源。
因此Service资源通常要与控制器资源(最为常用的控制器之一是Deployment)协同使用以完成应用的创建和对外发布。
创建Service资源
创建Service对象的常用方法有两种:一是直接使用“kubectl expose”命令,另一个是使用资源配置文件。
定义Service资源对象时,spec的两个较为常用的内嵌字段分别为selector和ports,分别用于定义使用的标签选择器和要暴露的端口。下面的配置清单是一个Service资源示例:
kind: Service
apiVersion: v1
metadata:
name: myapp-svc
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 80
~]$ kubectl get svc myapp-svc
~]$ kubectl get endpoints myapp-svc
Service资源myapp-svc通过标签选择器关联至标签为“app=myapp”的各Pod对象,它会自动创建名为myapp-svc的Endpoints资源对象,并自动配置一个ClusterIP,暴露的端口由port字段进行指定,后端各Pod对象的端口则由targetPort给出,也可以使用同port字段的默认值。
也可以不为Service资源指定.spec.selector属性值,其关联的Pod资源可由用户手动创建Endpoints资源进行定义。
Service对象创建完成后即可作为服务被各客户端访问,但要真正响应这些请求,还是要依赖于各后端的资源对象。
向Service对象请求服务
Service资源的默认类型为ClusterIP,它仅能接收来自于集群中的Pod对象中的客户端程序的访问请求。
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秒:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: <integer>
服务发现 (Service Discovery)
服务发现⽅式:环境变量
创建Pod资源时,kubelet会将其所属名称空间内的每个活动的Service对象以一系列环境变量的形式注入其中。它支持使用Kubernetes Service环境变量以及与Docker的links兼容的变量。
1.Kubernetes Service环境变量
Kubernetes为每个Service资源生成包括以下形式的环境变量在内的一系列环境变量,在同一名称空间中创建的Pod对象都会自动拥有这些变量。
- {SVCNAME}_SERVICE_HOST
- {SVCNAME}_SERVICE_PORT
如果SVCNAME中使用了连接线,那么Kubernetes会在定义为环境变量时将其转换为下划线。
2.Docker Link形式的环境变量
Docker使用"--link"选项实现容器连接时所设置的环境变量形式,在创建Pod对象时,Kubernetes也会将与此形式兼容的一系列环境变量注入Pod对象中。
示例说明:例如,在Service资源myapp-svc创建后创建的Pod对象中查看可用的环境变量,其中以MYAPP_SVC_SERVICE开头的表示Kubernetes Service环境变量,名称中不包含“SERVICE”字符串的环境变量为Docker Link形式的环境变量。
基于环境变量的服务发现其功能简单、易用,但存在一定的局限,例如,仅有那些与创建的Pod对象在同一名称空间中且事先存在的Service对象的信息才会以环境变量的形式注入,那些处于不同命名称空间,或者是在Pod资源创建之后才创建的Service对象的相关环境变量则不会被添加。幸而,基于DNS的发现机制并不存在此类限制。
ClusterDNS和服务发现
Kubernetes系统之上用于名称解析和服务发现的ClusterDNS是集群的核心附件之一,集群中创建的每个Service对象,都会由其自动生成相关的资源记录。默认情况下,集群内各Pod资源会自动配置其作为名称解析服务器,并在其DNS搜索列表中包含它所属名称空间的域名后缀。
1.拥有ClusterIP的Service资源,需要具有以下类型的资源记录。
- A记录:
<service>.<ns>.svc.<zone>. <ttl> IN A <cluster-ip>
- SRV记录:
_<port>._<proto>.<service>.<ns>.svc.<zone>. <ttl> IN SRV <weight> <priority> <port-number> <service>.<ns>.svc.<zone>
- PTR记录:
<d>.<c>.<b>.<a>.in-addr.arpa. <ttl> IN PTR <service>.<ns>.svc.<zone>
2.Headless类型的Service资源,需要具有以下类型的资源记录。
- A记录:
<service>.<ns>.svc.<zone>. <ttl> IN A <endpoint-ip>
- SRV记录:
_<port>._<proto>.<service>.<ns>.svc.<zone>. <ttl> IN SRV <weight> <priority> <port-number> <hostname>.<service>.<ns>.svc.<zone>
- PTR记录:
<d>.<c>.<b>.<a>.in-addr.arpa. <ttl> IN PTR <hostname>.<service>.<ns>.svc.<zone>
3.ExternalName类型的Service资源,需要具有CNAME类型的资源记录。
- CNAME记录:
<service>.<ns>.svc.<zone>. <ttl> IN CNAME <extname>
服务发现方式:DNS
创建Service资源对象时,ClusterDNS会为它自动创建资源记录用于名称解析和服务注册,于是,Pod资源可直接使用标准的DNS名称来访问这些Service资源。每个Service对象相关的DNS记录包含如下两个。
{SVCNAME}.{NAMESPACE}.{CLUSTER_DOMAIN}
{SVCNAME}.{NAMESPACE}.svc.{CLUSTER_DOMAIN}
"--cluster-dns"指定了集群DNS服务的工作地址,"--cluster-domain"定义了集群使用的本地域名,因此,系统初始化时默认会将“cluster.local.”和主机所在的域作为DNS的本地域使用,这些信息会在Pod创建时以DNS配置的相关信息注入它的/etc/resolv.conf
配置文件中。
-
{NAMESPACE}.svc.{CLUSTER_DOMAIN}
:如 default.svc.cluster.local。 -
svc.{CLUSTER_DOMAIN}
:如svc.cluster.local
基于DNS的服务发现不受Service资源所在的名称空间和创建时间的限制。
服务暴露
Service的IP地址仅在集群内可达,然而,总会有些服务需要暴露到外部网络中接受各类客户端的访问,此时,就需要在集群的边缘为其添加一层转发机制,以实现将外部请求流量接入到集群的Service资源之上,这种操作也称为发布服务到外部网络中。
Service类型
Kubernetes的Service共有四种类型:ClusterIP、NodePort、LoadBalancer和ExternalName。
-
ClusterIP:通过集群内部IP地址暴露服务,此地址仅在集群内部可达,无法被集群外部的客户端访问,此为默认的Service类型。
-
NodePort:这种类型建立在ClusterIP类型之上,其在每个节点的IP地址的某静态端口(NodePort)暴露服务,因此,它依然会为Service分配集群IP地址,并将此作为NodePort的路由目标。简单来说,NodePort类型就是在工作节点的IP地址上选择一个端口用于将集群外部的用户请求转发至目标Service的ClusterIP和Port,因此,这种类型的Service既可如ClusterIP一样受到集群内部客户端Pod的访问,也会受到集群外部客户端通过套接字
<NodeIP>: <NodePort>
进⾏的请求。
- LoadBalancer:这种类型建构在NodePort类型之上,其通过cloud provider提供的负载均衡器将服务暴露到集群外部,因此LoadBalancer一样具有NodePort和ClusterIP。简而言之,一个LoadBalancer类型的Service会指向关联至Kubernetes集群外部的、切实存在的某个负载均衡设备,该设备通过工作节点之上的NodePort向集群内部发送请求流量。此类型的优势在于,它能够把来自于集群外部客户端的请求调度至所有节点(或部分节点)的NodePort之上,而不是依赖于客户端自行决定连接至哪个节点,从而避免了因客户端指定的节点故障而导致的服务不可用。
- ExternalName:其通过将Service映射至由externalName字段的内容指定的主机名来暴露服务,此主机名需要被DNS服务解析至CNAME类型的记录。换言之,此种类型并非定义由Kubernetes集群提供的服务,而是把集群外部的某服务以DNS CNAME记录的形式映射到集群内,从而让集群内的Pod资源能够访问外部的Service的一种实现方式。因此,这种类型的Service没有ClusterIP和NodePort,也没有标签选择器用于选择Pod资源,因此也不会有Endpoints存在。
NodePort类型的Service资源
NodePort即节点Port,通常在安装部署Kubernetes集群系统时会预留一个端口范围用于NodePort,默认为30000~32767之间的端口。与ClusterIP类型的可省略.spec.type
属性所不同的是,定义NodePort类型的Service资源时,需要通过此属性明确指定其类型名称。
kind: Service
apiVersion: v1
metadata:
name: myapp-svc-nodeport
spec:
type: NodePort
selector:
app: myapp
ports:
- protocol: TCP
port: 80 # Cluster IP的端口
targetPort: 80 # pod中容器暴漏访问端口
nodePort: 32223 # NodePort
实践中,并不鼓励用户自定义使用的节点端口,除非事先能够明确知道它不会与某个现存的Service资源产生冲突。无论如何,只要没有特别需求,留给系统自动动配置总是较好的选择。
NodePort类型的Service资源依然会被配置ClusterIP,事实上,它会作为节点从NodePort接⼊流量后转发的目标地址,目标端口则是与Service资源对应的spec.ports.port属性中定义的端口
因此,对于集群外部的客户端来说,它们可经由任何一个节点的节点IP及端口访问NodePort类型的Service资源,而对于集群内的Pod客户端来说,依然可以通过ClusterIP对其进行访问。
LoadBalancer类型的Service资源
NodePort类型的Service资源虽然能够于集群外部访问得到,但外部客户端必须得事先得知NodePort和集群中⾄至少一个节点的IP地址,且选定的节点发生故障时,客户端还得自行选择请求访问其他的节点。另外,集群节点很可能是某IaaS云环境中使用私有IP地址的VM,或者是IDC中使用私有地址的物理机,这类地址对互联网客户端不可达,因此,一般还应该在集群之外创建一个具有公网IP地址的负载均衡器,由它接入外部客户端的请求并调度至集群节点相应的NodePort之上。
需要注意的是,有些环境中可能还需要为Service资源的配置定义添加Annotations。
进一步地,在IaaS环境支持手动指定IP地址时,用户还可以使用.spec.loadBalancerIP
指定创建的负载均衡器使用的IP地址,并可使用.spec.loadBalancerSourceRanges
指定负载均衡器允许的客户端来源的地址范围。
ExternalName Service
ExternalName类型的Service资源用于将集群外部的服务发布到集群中以供Pod中的应用程序访问,因此,它不需要使用标签选择器关联任何的Pod对象,但必须要使用spec.externalName属性定义一个CNAME记录用于返回外部真正提供服务的主机的别名,而后通过CNAME记录值获取到相关主机的IP地址。
kind: Service
apiVersion: v1
metadata:
name: external-redis-svc
namespace: default
spec:
type: ExternalName
externalName: redis.ilinux.io
ports:
- protocol: TCP
port: 6379
targetPort: 6379
nodePort: 0
selector: {}
待Service资源external-redis-svc创建完成后,各Pod对象即可通过external-redis-svc或其FQDN格式的名称external-redis-svc.default.svc.cluster.local访问相应的服务。ClusterDNS会将此名称以CNAME格式解析为.spec.externalName字段中的名称,而后通过DNS服务将其解析为相应的主机的IP地址。
由于ExternalName类型的Service资源实现于DNS级别,客户端将直接接入外部的服务而完全不需要服务代理,因此,它也无须配置ClusterIP,此种类型的服务也称为Headless Service。
Headless类型的Service资源
Service对象隐藏了各Pod资源,并负责将客户端的请求流量调度至该组Pod对象之上。不过,偶尔也会存在这样一类需求:客户端需要直接访问Service资源后端的所有Pod资源,这时就应该向客户端暴露每个Pod资源的IP地址,而不再是中间层Service对象的ClusterIP,这种类型的Service资源便称为Headless Service。
Headless Service对象没有ClusterIP,至于如何为此类Service资源配置IP地址,则取决于它的标签选择器的定义。
- 具有标签选择器:端点控制器(Endpoints Controller)会在API中为其创建Endpoints记录,并将ClusterDNS服务中的A记录直接解析到此Service后端的各Pod对象的IP地址上。
- 没有标签选择器:端点控制器(Endpoints Controller)不会在API中为其创建Endpoints记录,ClusterDNS的配置分为两种情形,对ExternalName类型的服务创建CNAME记录,对其他三种类型来说,为那些与当前Service共享名称的所有Endpoints对象创建⼀条记录。
创建Headless Service资源
配置Service资源配置清单时,只需要将ClusterIP字段的值设置为“None”即可将其定义为Headless类型。下面是一个Headless Service资源配置清单示例,它拥有标签选择器:
kind: Service
apiVersion: v1
metadata:
name: myapp-headless-svc
spec:
clusterIP: None
selector:
app: myapp
ports:
- name: httpport
port: 80
targetPort: 80
使用资源创建命令“kubectl create”或“kubectl apply”完成资源创建后,使用相关的查看命令获取Service资源的相关信息便可以看出,它没有ClusterIP,不过,如果标签选择器能够匹配到相关的Pod资源,它便拥有Endpoints记录,这些Endpoints对象会作为DNS资源记录名称myapp-headless-svc查询时的A记录解析结果
~]$ kubectl describe svc myapp-headless-svc
……
Endpoints: 10.244.1.113:80,10.244.2.13:80,10.244.3.104:80
……
Pod资源发现
根据Headless Service的工作特性可知,它记录于ClusterDNS的A记录的相关解析结果是后端Pod资源的IP地址,这就意味着客户端通过此Service资源的名称发现的是各Pod资源.
解析结果是Headless Service通过标签选择器关联到的所有Pod资源的IP地址。于是,客户端向此Service对象发起的请求将直接接入到Pod资源中的应用之上,而不再由Service资源进行代理转发,它每次接入的Pod资源则是由DNS服务器接收到查询请求时以轮询(roundrobin)的方式返回的IP地址。
Ingress资源
Kubernetes提供了两种内建的云端负载均衡机制(cloud load balancing)用于发布公共应用,一种是工作于传输层的Service资源,它实现的是“TCP负载均衡器”,另一种是Ingress资源,它实现的是“HTTP(S)负载均衡器”。
Ingress和Ingress Controller
Ingress是Kubernetes API的标准资源类型之一,它其实是一组基于DNS名称(host)或URL路径把请求转发至指定的Service资源的规则,用于将集群外部的请求流量转发至集群内部完成服务发布。然而,Ingress资源自身并不能进行流量穿透”,它仅是一组路由规则的集合,这些规则要想真正发挥作用还需要其他功能的辅助,如监听某套接字,然后根据这些规则的匹配机制路由请求流量。这种能够为Ingress资源监听套接字并转发流量的组件称为Ingress控制器(Ingress Controller)。
Ingress控制器是Kubernetes集群的一个重要附件,类似于CoreDNS,需要在集群上单独部署。
Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,如Nginx、Envoy、HAProxy、Vulcand和Traefik等。Ingress控制器自身也是运行于集群中的Pod资源对象,它与被代理的运行为Pod资源的应用运行于同一网络中。
使用Ingress资源进行流量分发时,Ingress控制器可基于某Ingress资源定义的规则将客户端的请求流量直接转发至与Service对应的后端Pod资源之上,这种转发机制会绕过Service资源,从而省去了由kube-proxy实现的端口代理开销。
创建Ingress资源
Ingress资源是基于HTTP虚拟主机或URL的转发规则,它在资源配置清单的spec字段中嵌套了rules、backend和tls等字段进行定义。下面的示例中定义了一个Ingress资源,它包含了一个转发规则,把发往www.k8s.com的请求代理给名为myapp-svc的Service资源:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: www.k8s.com
http:
paths:
- backend:
serviceName: myapp-svc
servicePort: 80
资源清单中的annotations用于识别其所属的Ingress控制器的类别,这一点在集群上部署有多个Ingress控制器时尤为重要。Ingress Spec中的字段是定义Ingress资源的核心组成部分,它主要嵌套如下三个字段。
- rules
- backend
- tls
backend对象的定义由两个必选的内嵌字段组成:serviceName和servicePort,分别用于指定流量转发的后端目标Service资源的名称和端口。
rules对象由一系列配置Ingress资源的host规则组成,这些host规则用于将一个主机上的某个URL路径映射至相关的后端Service对象,它的定义格式如下:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: jdd-parking-cloud-ui
spec:
ingressClassName: nginx-ingress-congtoller
rules:
- host: test.jdd966.cn
http:
paths:
- backend:
serviceName: jdd-parking-cloud-ui
servicePort: 80
path: /mgr
pathType: Prefix
tls:
- hosts:
- test.jdd966.cn
secretName: https-cn
.spec.rules.host
属性值目前不支持使用IP地址,也不支持后跟“:PORT”格式的端口号,且此字段值留空表示通配所有的主机名。
tls对象由两个内嵌字段组成,仅在定义TLS主机的转发规则时才需要定义此类对象。
hosts:包含于使用的TLS证书之内的主机名称字符串列表,因此,此处使用的主机名必须匹配tlsSecret中的名称。
secretName:用于引用SSL会话的secret对象名称,在基于SNI实现多主机路由的场景中,此字段为可选。
Ingress资源类型
基于HTTP暴露的每个Service资源均可发布于一个独立的FQDN主机名之上,如“www.ik8s.io”;也可发布于某主机的URL路径之上,从而将它们整合到同一个Web站点,如“www.ik8s.io/grafana”。
1.单Service资源型Ingress
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-ingress
spec:
backend:
serviceName: my-svc
servicePort: 80
Ingress控制器会为其分配一个IP地址接入请求流量,并将它们转至示例中的my-svc后端。
2.基于URL路径进行流量分发
垂直拆分或微服务架构中,每个小的应用都有其专用的Service资源暴露服务,但在对外开放的站点上,它们可能是财经、新闻、电商、无线端或API接口等一类的独立应用,可通过主域名的URL路径(path)分别入。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
annotations:
ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: www.ilinux.io
http:
paths:
- path: /wap
backend:
serviceName: wap
servicePort: 80
- path: /api
backend:
serviceName: api
servicePort: 80
3.基于主机名称的虚拟主机
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
spec:
rules:
- host: api.ilinux.io
http:
paths:
- path:
backend:
serviceName: api
servicePort: 80
- host: wap.ilinux.io
http:
paths:
- path:
backend:
serviceName: wap
servicePort: 80
4.TLS类型的Ingress资源
这种类型用于以HTTPS发布Service资源,基于一个含有私钥和证书的Secret对象即可配置TLS协议的Ingress资源,目前来说,Ingress资源仅支持单TLS端口,并且还会卸载TLS会话。在Ingress资源中引用此Secret即可让Ingress控制器加载并配置为HTTPS服务。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: no-rules-map
spec:
tls:
- secretName: ikubernetesSecret
backend:
serviceName: homesite
servicePort: 80
TLS Secret中包含的证书必须以tls.crt作为其键名,私钥文件必须以tls.key为键名,在Ingress控制器上配置HTTPS主机时,不能直接使用私钥和证书文件,而是要使用Secret资源对象来传递相关的数据。
部署Ingress控制器(Nginx)
Ingress控制器自身是运行于Pod中的容器应用,一般是Nginx或Envoy一类的具有代理及负载均衡功能的守护进程,它监视着来自于API Server的Ingress对象状态,并以其规则生成相应的应用程序专有格式的配置文件并通过重载或重启守护进程而使新配置生效。
对于Nginx来说,Ingress规则需要转换为Nginx的配置信息。简单来说,Ingress控制器其实就是托管于Kubernetes系统之上的用于实现在应用层发布服务的Pod资源,它将跟踪Ingress资源并实时生成配置规则。
那么,同样运行为Pod资源的Ingress控制器进程是如何接⼊外部的请求流量呢?常用的解决方案有如下两种。
- 以Deployment控制器管理Ingress控制器的Pod资源,并通过NodePort或LoadBalancer类型的Service对象为其接入集群外部的请求流量,这就意味着,定义一个Ingress控制器时,必须在其前端定义一个专用的Service资源
- 借助于DaemonSet控制器,将Ingress控制器的Pod资源各自以单一实例的方式运行于集群的所有或部分工作节点之上,并配置这类Pod对象以hostPort或hostNetwork的方式在当前节点接入外部流量。