Service 主要用于提供网络服务,通过Servicel的定义,能够 为客户端应用提供稳定的访问地址(域名或IP地址)和负载均衡功能,以及屏蔽后端Endpoint的变化,是Kubernetes实现微服务的核心资源。
本文详细讲解下Service的相关概念及原理。
下面演示在没有Service之前,是如何访问一个多副本的应用容器组提供的服务。
以Tomcat容器为例,其Deployment资源文件定义如下:
- apiVersion: apps/v1
- kind: Deployment
- metadata:
- name: webapp
- spec:
- replicas: 2
- selector:
- matchLabels:
- app: webapp
- template:
- metadata:
- labels:
- app: webapp
- spec:
- containers:
- - name: webapp
- image: kubeguide/tomcat-app:v1
- ports:
- - containerPort: 8080
创建完成后,查看每个pod的ip地址:

客户端应用可以直接通过这两个Pod的IP地址和端口号8080访问Web服务,例如:curl 10.0.95.22:8080
但是,提供服务的容器应用通常是分布式的,通过多个Pod副本共同提供服务(还需要考虑扩缩容的问题),要实现动态感知服务后端实例的变化,会大大增加客户端系统实现的复杂度,为了解决这个问题,Kubernetes引入 Service资源类型 。
Service实现的是微服务架构中的几个核心功能:全自动的服务注册、服务发现、 服务负载均衡等。
命令如下:
- $ kubectl expose deployment webapp
- service/webapp exposed
查看新创建的Service,可以看到系统为它分配了一个虚拟IP地址(ClusterIP 地址),Service的端口号则从Pod中的containerPort复制而来:

通过curl 169.169.140.242:8080 也是可以访问的。访问时,会被自动负载分发到了后端两个Pod之一:10.0.95.22:8080或10.0.95.23:8080。
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- ports:
- - protocol: TCP
- port: 8080
- targetPort: 8080
- selector:
- app: webapp
Service定义中的关键字段是ports和selector:
使用kubectl create命令创建后,能看到和使用kubectl expose命令创建Service的效果一样。
一个Service对应的 “后端” 由Pod的IP和容器端口号组成,这在k8s系统中称为Endpoint。
可以通过kubectl descirbe svc 命令查看Endpoint列表,如:

Kubernetes自动创建了与Service关联的Endpoint资源对象,这可以通过查询Endpoint对象讲行查看:

当一个 Service 对象在 Kubernetes 集群中被定义出来时,集群内的客户端应用就可以通过服务IP访问到具体的Pod容器提供的服务了。
从服务IP到后端Pod的负载均衡机制,则是由每个Node上的kube-proxy负责实现的。通过Service的负载均衡机制,Kubernetes实现了一种分布式应用的统一入口,免去了客户端应用获知后端服务实例列表和变化的复杂度。。
kube-proxy代理模式参考:Kubernetes基础(四)-Kube-proxy_kubectl proxy-CSDN博客
Service支持通过设置sessionAffinity实现基于客户端IP的会话保持机制,即:首次将某个客户端来源IP发起的请求转发到后端的某个Pod上,之后从相同的客户端 IP发起的请求都将被转发到相同的后端Pod上。
配置参数为 service.spec.sessionAffinity,也可以设置会话保持的最长时间(service.spec.sessionAffinityConfig.clientIP.timeoutSeconds),例如下面的服务将会话保持时间设置为10800s(3h):
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- sessionAffinity: ClientIP
- sessionAffinityConfig:
- clientIP:
- timeoutSecondes: 10080
- ports:
- - protocol: TCP
- port: 8080
- targetPort: 8080
- selector:
- app: webapp
一个容器应用可以提供多个端口的服务,在Service的定义中也可以相应地设置多个端口号。
在下面的例子中,Service设置了两个端口号来分别提供不同的服务,如web服务和management服务(下面为每个端口号都进行了命名,以便区分):
- apiversion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- ports:
- - port: 8080
- targetPort: 8080
- name: web
- - port: 8005
- targetPort: 8005
- name: management
- selector:
- app: webapp
另一个例子是同一个端口号使用的协议不同,如TCP和UDP,也需要设置为多个端口号来提供不同的服务:
- apiVersion: v1
- kind: Service
- metadata:
- name: kube-dns
- namespace: kube-system
- labels:
- k8s-app: kube-dns
- kubernetes.io/cluster-service: "true"
- kubernetes.io/name: "KubeDNS"
- spec:
- selector:
- k8s-app: kube-dns
- clusterIP: 169.169.0.100
- ports:
- - name: dns
- port: 53
- protocol: UDP
- - name: dns-tcp
- port: 53
- protocol: TCP
普通的Service通过Label Selector对后端Endpoint列表进行了一次抽象,如果后端的Endpoint不是由Pod副本集提供的,则Service还可以抽象定义任意其他服务,将一个Kubernetes集群外部的已知服务定义为Kubernetes内的一个Service, 供集群内的其他应用访问。
常见的应用场景包括:
Service指向外部服务如下图所示:

对于这种应用场景,用户在创建Service资源对象时不设置Label Selector(后端Pod也不存在),同时再定义一个与Service关联的Endpoint资源对象,在Endpoint中设置外部服务的IP地址和端口号,例如:
- apiVersion: v1
- kind: Service
- metadata:
- name: my-service
- spec:
- ports:
- - protocol: TCP
- port: 80
- targetPort: 80
-
- -----------
- apiversion: v1
- kind: Endpoints
- metadata:
- name: my-service
- subsets:
- - addresses:
- - IP: 1.2.3.4
- ports:
- - port: 80
Kubernetes为Service创建的ClusterIP地址是对后端Pod列表的一层抽象,对于集群外部来说并没有意义,但有许多Service是需要对集群外部提供服务的,Kubernetes提供了多种机制将Service暴露出去,供集群外部的客户端访问。
这可以通过Service资源对象的类型字段“type”进行设置。
Kubernetes 服务有四种类型——ClusterIP、NodePort、LoadBalancer 和 ExternalName。 服务spec中的 type 属性决定了服务如何暴露给网络。
集群内的服务间通信。 例如应用程序的前端(front-end)和后端(back-end)组件之间的通信。
举例
- apiVersion: v1
- kind: Service
- metadata:
- name: my-backend-service
- spec:
- type: ClusterIP # Optional field (default)
- clusterIP: 10.10.0.1 # within service cluster ip range
- ports:
- - name: http
- protocol: TCP
- port: 80
- targetPort: 8080
举例
- apiVersion: v1
- kind: Service
- metadata:
- name: my-frontend-service
- spec:
- type: NodePort
- selector:
- app: web
- ports:
- - name: http
- protocol: TCP
- port: 80
- targetPort: 8080
- nodePort: 30000 # 30000-32767, Optional field
当用户使用云厂商来托管Kubernetes 集群时。
举例
- apiVersion: v1
- kind: Service
- metadata:
- name: my-frontend-service
- spec:
- type: LoadBalancer
- clusterIP: 10.0.171.123
- loadBalancerIP: 123.123.123.123
- selector:
- app: web
- ports:
- - name: http
- protocol: TCP
- port: 80
- targetPort: 8080
举例
- apiVersion: v1
- kind: Service
- metadata:
- name: my-service
- spec:
- type: ExternalName
- externalName: my.database.example.com
Kubernetes为Service创建的ClusterIP地址是对后端Pod列表的一层抽象,对于集群外部来说并没有意义,但有许多Service是需要对集群外部提供服务的,Kubernetes提供了多种机制将Service暴露出去,供集群外部的客户端访问。
外部访问方式参考:Kubernetes基础(三)-Service外部网络访问方式-CSDN博客
目前Service支持的网络协议如下.
| 协议 | 描述 |
| TCP | Service的默认网络协议,可用于所有类型的Service。 |
| UDP | 可用于大多数类型的Service,LoadBalancer类型取决于云服务商对UDP的支持。 |
| HTTP | 取决于云服务商是否支持HTTP和实现机制。 |
| PROXY | 取决于云服务商是否支持HTTP和实现机制。 |
| SCTP | 从Kubernetes1.12版本引入,到1.19版本时达到Beta阶段,默认启用,如需关闭该特性,则需要设置kube-apiserver的启动参数–feature- gates=-SCTPSupport=-false进行关闭。 |
Kubernetes从1.17版本开始,可以为Service和Endpoint资源对象设置一个新的段"AppProtocol",用于标识后端服务在某个端口号上提供的应用层协议类型,例如HTTP、HTTPS、SSL、DNS等。
要使用AppProtocol,需要设置kube-apiserver的启动参数--feature-gates=ServiceAppProtocol=true进行开启,然后在Service或Endpoint的定义中设置AppProtocol字段指定应用层协议的类型,例如:
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- ports:
- - port: 8080
- targetPort: 8080
- AppProtocol: HTTP
- selector:
- app: webapp
服务发现机制指客户端应用在一个Kubernetes集群中如何获知后端服务的访问地址,一共有两种方式。
在一个Pod运行起来的时候,系统会自动为其容器运行环境注入所有集群中有效Service的信息。
Service的相关信息包括服务IP、服务端口号、各端口号相关的协议等,通过{SVCNAME_SERVICE_HOST}和{SVCNAME_SERVICE_PORT}格式进行设置。
其中SVCNAME的命名规则为:将Service的name字符串转换为全大写字母,将中横线“”替换为下画线 “_”,以webapp服务为例:
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- ports:
- - protocol: TCP
- port: 8080
- targetPort: 8080
- selector:
- app: webapp
在一个新创建的Pod(客户端应用)中,可以看到系统自动设置的环境变量如下:
- WEBAPP_SERVICE_HOST=169.169.81.175
- WEBAPP_SERVICE_PORT=8080
- WEBAPP_P0RT=tcp://169.169.81.175:8080
- WEBAPP_P0RT_8080_TCP=tcp://169.169.81.175:8080
- WEBAPP_PORT_8080_TCP_PROTO=tcp
- WEBAPP_PORT_8080_TCP_PORT=8080
- WEBAPP_PORT_8080_TCP_ADDR=169.169.81.175
然后客户端应用就能够根据Service相关环境变量的命名规则,从环境变量中获取需要访问的目标服务的地址了,例如:
curl http://{WEBAPP_SERVICE_HOST}:${WEBAPP_SERVICE_HOST}
Service在Kubernetes系统中遵循DNS命名规范,Service的DNS域名表示方法 为
另外,Service定义中的端口号如果设置了名称(name),则该端口号也会拥有一个DNS域名,在DNS服务器中以SRV记录的格式保存:_
当Service以DNS域名形式进行访问时,就需要在Kubernetes集群中存在一个DNS服务器来完成域名到ClusterIP地址的解析工作了,经过多年的发展,目前由CoreDNS作为Kubernetes集群的默认DNS服务器提供域名解析服务。
以webapp服务为例,将其端口号命名为“http”:
- apiversion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- ports:
- - protocol: TCP
- port: 8080
- targetPort: 8080
- name: http
- selector:
- app: webapp
解析名为 “http” 端口的DNS SRV记录“_http._tcp.webapp.default.svc.cluster.local'”,可以查询到其端口号的值为8080。
在某些应用场景中,客户端应用不需要通过Kubernetes内置Service实现的负载均衡功能,或者需要自行完成对服务后端各实例的服务发现机制,或者需要自行实现负载均衡功能,此时可以通过创建一种特殊的名为 “Headless‘”的服务来实现。headless介绍参考: Kubernetes基础(二)-Headless Service_alden_ygq的博客-CSDN博客
Service的后端是一组Endpoint列表,为客户端应用提供了极大的便利。但随着集群规模的扩大及Service数量的增加,特别是Service后端Endpoint数量的增加,kube-proxy需要维护的负载分发规则(例如iptables规则侧或 ipvs规则)的数量也会急剧增加,导致后续对Service后端Endpoint的添加、删除 等更新操作的成本急剧上升。
假设在Kubernetes集群中有10000个Endpoint运行在大约5000个Node上,则对单个Pod的更新将需要总计约5GB的数据传输,这不仅对集群内的网络带宽浪费巨大,而且对Master的冲击非常大,会影响Kubernetes集群的整体性能,在Deployment不断进行滚动升级操作的情况下尤为突出。这种情况下K8s设计了一种端点分片(Endpoint Slices)机制来解决。
EndpointSlice通过对Endpoint进行分片管理来实现降低Master和各Node之间的网络传输数据量及提高整体性能的目标。对于Deployment的滚动升级,可以实现仅更新部分Node上的Endpoint信息,Master与Node之间的数据传输量可以减少100倍左右,能够大大提高管理效率。

Endpoint Slices要实现的第2个目标是为基于Node拓扑的服务路由提供支持,这需要与服务拓扑(Service Topology)机制共同实现。
kubernetes从1.19版本开始,EndpointSplice机制以及EndpointSliceProxying是默认开启的:
以一个3副本的webapp服务为例,Pod列表如下:

服务和Endpoint的信息如下:

查看EndpointSlice,可以看到系统自动创建了一个名称前缀为“webapp-”的EndpointSlice:

查看其详情信息,可以看到3个Endpoint的IP地址和端口信息,同时为Endpoint设置了Topology相关信息:

10.1.1 参数默认情况下,在由EndpointSlice控制器创建的EndpointSlice中最多包含100个Endpoint,如需修改,则可以通过kube-controller-manager服务的启动参数-- max-endpoints-per-slice设置,但上限不能超过1000。
EndpointSlice的关键信息如下:
| 配置项 | 描述 |
| 关联的服务名称 | 将EndpointSlice与Service的关联信息设置为一个标签kubernetes.io/service-name=webapp,该标签标明了服务名称 |
| 地址类型AddressType | 包括以下3种取值类型:
|
| 每个Endpoint的信息 | 在Endpoints列表中列出的每个Endpoint的信息:
目前EndpointSlice控制器自动设置的拓扑信息如下:
在大规模集群中,管理员应对不同地域或不同区域的Node设置相关的 topology标签,用于为Node设置拓扑信息. |
| EndpointSlice的管理控制器 | 通过endpointslice.kubernetes.io/managed-by标签进行设置,用于存在多个管理控制器的应用场景中,例如某个Service Mesh管理工具也可以对EndpointSlice进行管理。为了支持多个管理工具对EndpointSlice同时进行管理并且互不干扰,可以通过endpointslice.kubernetes.io/managed--by标签设置管理控制器的名称,Kubernetes内置的EndpointSlice控制器自动设置该标签的值为endpointslice-controller.k8s.io,其他管理控制器应设置唯一名称用于标识. |
EndpointSlice复制(Mirroring)功能:应用程序有时可能会创建自定义的Endpoint资源,为了避免应用程序在创建Endpoint资源时再去创建EndpointSlice资源,Kubernetes控制平面会 自动完成 将Endpoint资源复制为EndpointSlice资源的操作。
以下几种情况下,不会执行自动复制操作:
一个Endpoint资源同时存在IPv4和IPv6地址类型时,会被复制为多个EndpointSlice资源,每种地址类型最多会被复制为1000个EndpointSlice资源。
如上例所示,可以看到每个EndpointSlice资源都包含一组作用于全部Endpoint的端口号(Ports)。如果Service定义中的端口号使用了字符串名称,则对于相同name的端口号,目标Pod 的targetPort可能是不同的,结果是EndpointSlice资源将会不同。这与Endpoint 资源设置子集(subset)的逻辑是相同的。
Kubernetes控制平面对于EndpointSlice中数据的管理机制是尽可能填满,但不会在多个EndpointSlice数据不均衡衡的情况下主动执行重新平衡(rebalance)操作,其背后的逻辑也很简单,步骤如下:
重要的是,第3步优先考虑创建新的EndpointSlice而不是更新原EndpointSlice。例如,如果要添加l0个新的Endpoint,则当前有两个EndpointSlice各有5个剩余空间可用于填充,系统也会创建一个新的EndpointSlice用来填充这10个新Endpoint。换句话说,单个EndpointSlice的创建优于对多个EndpointSlice的更新。
以上主要是由于在每个节点上运行的kube-proxy都会持续监控EndpointSlice的变化,对EndpointSlice每次更新成本都很高,因为每次更新都需要Master将更新数据发送到每个kube-proxy。
上述管理机制旨在限制需要发送到每个节点的更新数据量,即使可能导致最终有许多EndpointSlice资源未能填满。实际上,这种不太理想的数据分布情况应该是罕见的。
Master的EndpointSlice控制器处理的大多数更新所带来的数据量都足够小,使得对已存在 (仍有空余空间)EndpointSlice的数据填充都没有问题,如果实在无法填充,则无论如何都需要创建新的EndpointSlice资源。
此外,对Deployment执行滚动升级操作时,由于后端Pod列表和相关Endpoint列表全部会发生变化,所以也会很自然地对EndpointSlice资源的内容全部进行更新。
在默认情况下,发送到一个Service的流量会被均匀转发到每个后端Endpoint,但无法根据更复杂的拓扑信息设置复杂的路由策略。服务拓扑机制的引入就是为了实现基于Node拓扑的服务路由,允许Service创建者根据来源Node和目标Node的标签来定义流量路由策略。
通过对来源Node和目标Node标签的匹配,用户可以根据业务需求对Node进行分组,设置有意义的指标值来标识 “较近” 或者 “较远” 的属性:
例如对于公有云环境来说,通常有区域(Zone或Region)的划分,云平台倾向于把服务流量限制在同一个区域内,这通常是因为跨区域网络流量会收取额外的费用。另一个例子是把流量路由到由DaemonSet管理的当前Node的Pod 上。又如希望把流量保持在相同机架内的Node上,以获得更低的网络延时。
服务拓扑机制需要通过设置kube-apiserver和kube-proxy服务的启动参数--feature-gates-=“ServiceTopology=true,EndpointSlice=true“进行启用(需要同时启用EndpointSlice功能),然后就可以在Service资源对象上通过定义 topologyKeys字段来控制到Service的流量路由了。
对于需要使用服务拓扑机制的集群,管理员需要为Node设置相应的拓扑标签,包括kubernetes.io/hostname、topology.kubernetes.io/zone 和topology.kubernetes.io/region。
然后为Service设置topologyKeys的值,就可以实现如下流量路由策略:
目前使用服务拓扑有以下几个约束条件:
下面通过Service的YAML文件对几种常见的服务拓扑应用实例进行说明。
1)只将流量路由到相同Node的Endpoint上,如果Node没有可用的Endpoint,则将请求丢弃:
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- selector:
- app: webapp
- ports:
- - port: 8080
- topologykeys:
- - "kubernetes.io/hostname"
2)优先将流量路由到相同Node的Endpoint上,如果Node没有可用的Endpoint,则将请求路由到任意可用的Endpoint:
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- selector:
- app: webapp
- ports:
- - port:8080
- topologyKeys:
- - "kubernetes.io/hostname"
- - "*"
3)只将流量路由到相同zone或同region的Endpoint上,如果没有可用的Endpoint,则将请求丢弃:
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- selector:
- app: webapp
- ports:
- - port:8080
- topologyKeys:
- - "topology.kubernetes.io/zone"
- - "topology.kubernetes.io/region"
4)按同Node、同zone、同region的优先级顺序路由流量,如果Node、 zone、region都没有可用的Endpoint,则将请求路由到集群内任意可用的Endpoint.上:
- apiVersion: v1
- kind: Service
- metadata:
- name: webapp
- spec:
- selector:
- app: webapp
- ports:
- - port:8080
- topologyKeys:
- - "kubernetes.io/hostname"
- - "topology.kubernetes.io/zone"
- - "topology.kubernetes.io/region"
- - "*"