深入掌握Service
-
- 1、Service定义详解
- 2、Service的基本用法
- [3、Headless Service](#3、Headless Service)
1、Service定义详解
YAML格式的Service定义文件的完整内容如下:
yaml
apiVersion: v1 # 必填项
kind: Service # 必填项
metadata: # 必填项
name: string # 必填项:Service 名称
namespace: string # 命名空间
labels: # 标签:必须是 key: value 键值对格式
app: string
annotations: # 注解:必须是 key: value 键值对格式
key: string
spec: # 必填项
selector: # 标签选择器:必须是 key: value 键值对格式
app: string
type: string # Service 类型(如 ClusterIP, NodePort, LoadBalancer 等)
clusterIP: string # 集群内部 IP(可选)
sessionAffinity: string # 会话保持类型(可选,如 ClientIP)
ports:
- name: string # 端口名称
protocol: string # 协议(如 TCP, UDP, SCTP)
port: int # Service 暴露的端口
targetPort: int # 后端容器监听的端口
nodePort: int # 节点端口(当 type 为 NodePort 或 LoadBalancer 时使用)
status: #当 spec.type=LoadBalancer 时,设置外部负载均衡器的地址,用于公有云环境。
loadBalancer: # 外部负载均衡器的容器对象。
ingress: # 外部负载均衡器的 ingress 列表。
ip: # 外部负载均衡器分配的 IP 地址(若云厂商通过 IP 对外暴露)。
hostname: # 外部负载均衡器分配的 主机名/域名(若云厂商通过 DNS 对外暴露)。
对各属性的说明如表所示。


2、Service的基本用法
一般来说,对外提供服务的应用程序需要通过某种机制来实现,对于容器应用最简便的方式就是通过TCP/IP机制及监听IP和端口号来实现。例如,定义一个提供Web服务的RC,由两个Tomcat容器副本组成,每个容器都通过containerPort设置提供服务的端口号为8080:
yaml
apiVersion: v1
kind: ReplicationController # 资源类型:RC 控制器(负责确保指定数量的 Pod 副本在任何时候都在运行)
metadata:
name: webapp # ReplicationController 的名称
spec:
replicas: 2 # 副本数:期望始终运行 2 个该 Pod 的实例
template:
metadata:
name: webapp
labels:
app: webapp # Pod 的标签(RC 通过该标签来识别和管理哪些 Pod 属于它)
spec:
containers:
- name: webapp # 容器名称
image: tomcat # 使用的容器镜像(这里是 Tomcat)
ports:
- containerPort: 8080 # 容器内部暴露的端口
创建该RC:
bash
# kubectl create -f webapp-rc.yaml
replicationcontroller "webapp" created
获取Pod的IP地址:
bash
# kubectl get pods -l app=webapp -o yaml | grep podIP
podIP: 172.17.1.4
podIP: 172.17.1.3
可以直接通过这两个Pod的IP地址和端口号访问Tomcat服务:
bash
# curl 172.17.1.3:8080
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/8.0.35</title>
......
# curl 172.17.1.4:8080
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/8.0.35</title>
......
直接通过Pod的IP地址和端口号可以访问到容器应用内的服务,但是Pod的IP地址是不可靠的,例如当Pod所在的Node发生故障时,Pod将被Kubernetes重新调度到另一个Node,Pod的IP地址将发生变化。更重要的是,如果容器应用本身是分布式的部署方式,通过多个实例共同提供服务,就需要在这些实例的前端设置一个负载均衡器来实现请求的分发。Kubernetes中的Service就是用于解决这些问题的核心组件。
以前面创建的webapp应用为例,为了让客户端应用访问到两个Tomcat Pod实例,需要创建一个Service来提供服务。Kubernetes提供了一种快速的方法,即通过kubectlexpose命令来创建Service:
bash
# kubectl expose rc webapp
service "webapp" exposed
查看新创建的Service,可以看到系统为它分配了一个虚拟的IP地址(ClusterIP),Service所需的端口号则从Pod中的containerPort复制而来:
bash
# kubectl get svc
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp 169.169.235.79 <none> 8080/TCP 3s
接下来就可以通过Service的IP地址和Service的端口号访问该Service了:
bash
# curl 169.169.235.79:8080
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/8.0.35</title>
......
这里,对Service地址169.169.235.79:8080的访问被自动负载分发到了后端两个Pod之一:172.17.1.3:8080或172.17.1.4:8080。
除了使用kubectl expose命令创建Service,我们也可以通过配置文件定义Service,再通过kubectl create命令进行创建。例如对于前面的webapp应用,我们可以设置一个Service,代码如下:
yaml
apiVersion: v1
kind: Service
metadata:
name: webapp
spec:
ports:
- port: 8081
targetPort: 8080
selector:
app: webapp
Service定义中的关键字段是ports和selector。本例中ports定义部分指定了Service所需的虚拟端口号为8081,由于与Pod容器端口号8080不一样,所以需要再通过targetPort来指定后端Pod的端口号。selector定义部分设置的是后端Pod所拥有的label:app=webapp。
创建该Service并查看其ClusterIP地址:
bash
# kubectl create -f webapp-svc.yaml
service "webapp" created
# kubectl get svc
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp 169.169.28.190 <none> 8081/TCP 3s
通过Service的IP地址和Service的端口号进行访问:
bash
# curl 169.169.28.190:8081
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/8.0.35</title>
......
同样,对Service地址169.169.28.190:8081的访问被自动负载分发到了后端两个Pod之一:172.17.1.3:8080或172.17.1.4:8080。目前Kubernetes提供了两种负载分发策略:RoundRobin和SessionAffinity,具体说明如下。
RoundRobin:轮询模式,即轮询将请求转发到后端的各个Pod上。SessionAffinity:基于客户端IP地址进行会话保持的模式,即第1次将某个客户端发起的请求转发到后端的某个Pod上,之后从相同的客户端发起的请求都将被转发到后端相同的Pod上。
在默认情况下,Kubernetes采用RoundRobin模式对客户端请求进行负载分发,但我们也可以通过设置service.spec.sessionAffinity=ClientIP来启用SessionAffinity策略。这样,同一个客户端IP发来的请求就会被转发到后端固定的某个Pod上了。
通过Service的定义,Kubernetes实现了一种分布式应用统一入口的定义和负载均衡机制。Service还可以进行其他类型的设置,例如设置多个端口号、直接设置为集群外部服务,或实现为Headless Service(无头服务)模式(将在4.3节介绍)。
2.1、多端口Service
有时一个容器应用也可能提供多个端口的服务,那么在Service的定义中也可以相应地设置为将多个端口对应到多个应用服务。在下面的例子中,Service设置了两个端口号,并且为每个端口号都进行了命名:
bash
apiVersion: v1
kind: Service
metadata:
name: webapp # 服务名称,集群内其他 Pod 可以通过此名称进行通信
labels:
app: webapp # 标签,用于标识该 Service 归属的应用
spec:
type: ClusterIP # 服务类型:ClusterIP(默认类型),仅在集群内部提供访问
ports:
- name: web # 端口名称,用于区分不同的监听端口
port: 8080 # Service 自身对外暴露的端口(集群内其他应用访问时使用的端口)
targetPort: 8080 # 后端 Pod 容器内部实际监听的端口
- name: management # 第二个端口的名称(例如用于管理或监控)
port: 8005 # 第二个端口对外暴露的端口
targetPort: 8005 # 后端 Pod 容器内部的对应管理端口
selector:
app: webapp # 标签选择器:精确匹配带有 `app: webapp` 标签的 Pod,将流量导入其中
另一个例子是两个端口号使用了不同的4层协议---TCP和UDP:
yaml
apiVersion: v1
kind: Service
metadata:
name: kube-dns # 服务名称
namespace: kube-system # 所在的命名空间(系统核心组件通常部署在 kube-system)
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
kubernetes.io/name: "KubeDNS"
spec:
selector:
k8s-app: kube-dns # 标签选择器,用于匹配 CoreDNS / KubeDNS 的 Pod
clusterIP: 169.169.0.100 # 指定集群内部的固定虚拟 IP(通常由 Kubernetes 自动分配,此处为自定义固定 IP)
ports:
- name: dns # DNS 服务端口(UDP 协议,用于常规 DNS 查询)
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp # DNS 服务端口(TCP 协议,用于大响应包或区域传输)
port: 53
protocol: TCP
targetPort: 53
2.2、外部服务Service
在某些环境中,应用系统需要将一个外部数据库作为后端服务进行连接,或将另一个集群或Namespace中的服务作为服务的后端,这时可以通过创建一个无LabelSelector的Service来实现:
yaml
apiVersion: v1
kind: Service
metadata:
name: my-service # 服务名称
spec:
type: ClusterIP # 服务类型:ClusterIP(默认类型),仅在集群内部提供访问
ports:
- name: http # 端口名称(建议加上,方便多端口或监控时区分)
protocol: TCP # 传输层协议(支持 TCP、UDP、SCTP,默认为 TCP)
port: 80 # Service 自身对外暴露的端口
targetPort: 80 # 后端 Pod 容器内部实际监听的端口
selector:
app: MyApp # 标签选择器:精准匹配带有 `app: MyApp` 标签的 Pod(注意:此处需根据实际情况配置)
通过该定义创建的是一个不带标签选择器的Service,即无法选择后端的Pod,系统不会自动创建Endpoint,因此需要手动创建一个和该Service同名的Endpoint,用于指向实际的后端访问地址。创建Endpoint的配置文件内容如下:
yaml
apiVersion: v1
kind: Endpoints
metadata:
name: my-service # 端点名称(通常需与同名的 Service 名称保持一致)
subsets:
- addresses:
- ip: 1.2.3.4 # 后端实际提供服务的 Pod 或外部服务器的 IP 地址(注意:字段名为小写的 ip)
ports:
- port: 80 # 后端服务实际监听的端口号
protocol: TCP # 传输层协议(可选,默认为 TCP)
如图所示,访问没有标签选择器的Service和带有标签选择器的Service一样,请求将会被路由到由用户手动定义的后端Endpoint上。

3、Headless Service
在某些应用场景中,开发人员希望自己控制负载均衡的策略,不使用Service提供的默认负载均衡的功能,或者应用程序希望知道属于同组服务的其他实例。Kubernetes提供了Headless Service来实现这种功能,即不为Service设置ClusterIP(入口IP地址),仅通过LabelSelector将后端的Pod列表返回给调用的客户端。例如:
yaml
apiVersion: v1
kind: Service
metadata:
name: nginx # 服务名称
labels:
app: nginx # 标签,用于标识该 Service 归属的应用
spec:
clusterIP: None # 设置为 None 表示无头服务 (Headless Service),不分配集群虚拟 IP,常用于 StatefulSet
ports:
- name: http # 端口名称
port: 80 # Service 自身对外暴露的端口
targetPort: 80 # 后端 Pod 容器内部实际监听的端口
selector:
app: nginx # 标签选择器:精准匹配带有 `app: nginx` 标签的 Pod
这样,Service就不再具有一个特定的ClusterIP地址,对其进行访问将获得包含Label"app=nginx"的全部Pod列表,然后客户端程序自行决定如何处理这个Pod列表。例如,StatefulSet就是使用Headless Service为客户端返回多个服务地址的。
对于"去中心化"类的应用集群,Headless Service将非常有用。下面以搭建Cassandra集群为例,看看如何通过对Headless Service的巧妙使用,自动实现应用集群的创建。
Apache Cassandra是一套开源分布式NoSQL数据库系统,主要特点为它不是单个数据库,而是由一组数据库节点共同构成的一个分布式的集群数据库。由于Cassandra使用的是"去中心化"模式,所以在集群里的一个节点启动之后,需要一个途径获知集群中新节点的加入。Cassandra使用了Seed(种子)来完成集群中节点之间的相互查找和通信。
通过对Headless Service的使用,实现了Cassandra各节点之间的相互查找和集群的自动搭建。主要步骤包括:自定义SeedProvider;通过Headless Service自动查找后端Pod;自动添加新Cassandra节点。
3.1、自定义SeedProvider
在本例中使用了一个自定义的SeedProvider类来完成新节点的查询和添加,类名为io.k8s.cassandra.KubernetesSeedProvider。
KubernetesSeedProvider.java类的源代码节选如下:
java
......
public List<InetAddress> getSeeds() {
List<InetAddress> list = new ArrayList<InetAddress>();
String host = "https://kubernetes.default.cluster.local";
String serviceName = getEnvOrDefault("CASSANDRA_SERVICE", "cassandra");
String podNamespace = getEnvOrDefault("POD_NAMESPACE", "default");
String path = String.format("/api/v1/namespaces/%s/endpoints/", podNamespace);
......
public static void main(String[] args) {
SeedProvider provider = new KubernetesSeedProvider(new HashMap<String,
String>());
System.out.println(provider.getSeeds());
}
}
定制的KubernetesSeedProvider类将使用REST API来访问Kubernetes Master,然后通过查询name=cassandra的服务(Headless Service将返回Pod列表)完成对其他"节点"的查找。
3.2、通过Service动态查找Pod
在KubernetesSeedProvider类中,通过查询环境变量CASSANDRA_SERVICE的值来获得服务的名称。这样就要求Service在Pod之前创建。如果我们已经创建好DNS服务(详见4.5节的说明),那么也可以直接使用服务的名称而无须使用环境变量。
回顾一下Service的概念。Service通常用作一个负载均衡器,供Kubernetes集群中其他应用(Pod)对属于该Service的一组Pod进行访问。由于Pod的创建和销毁都会实时更新Service的Endpoint数据,所以可以动态地对Service的后端Pod进行查询。Cassandra的"去中心化"设计使得Cassandra集群中的一个Cassandra实例(节点)只需要查询到其他节点,即可自动组成一个集群,正好可以使用Service的这个特性查询到新增的节点。下图描述了Cassandra新节点加入集群的过程。

在Kubernetes系统中,首先需要为Cassandra集群定义一个Service:
yaml
apiVersion: v1
kind: Service
metadata:
name: cassandra # 服务名称
labels:
name: cassandra # 标签,用于标识该 Service 归属的应用
spec:
ports:
- name: cql # 端口名称(建议加上,方便后续多端口扩展或监控)
port: 9042 # Service 自身对外暴露的端口(Cassandra 默认的 CQL 客户端连接端口)
targetPort: 9042 # 后端 Pod 容器内部实际监听的端口
selector:
name: cassandra # 标签选择器:精准匹配带有 `name: cassandra` 标签的 Pod
在Service的定义中指定Label Selector为name=cassandra。
使用kubectl create命令创建这个Service:
bash
$ kubectl create -f cassandra-service.yaml
service "cassandra" created
然后,创建一个Cassandra Pod:
bash
apiVersion: apps/v1
kind: Deployment
metadata:
name: cassandra # Deployment 名称
labels:
name: cassandra
spec:
replicas: 1 # 副本数量
selector:
matchLabels:
name: cassandra # 标签选择器(Deployment 要求必须使用 matchLabels)
template:
metadata:
labels:
name: cassandra
spec:
containers:
- name: cassandra # 容器名称
image: gcr.io/google_containers/cassandra:v5 # 镜像地址
command:
- /run.sh # 启动命令
ports:
- name: cql
containerPort: 9042 # CQL 客户端连接端口
- name: thrift
containerPort: 9160 # Thrift 端口
resources:
limits:
cpu: 0.5 # 资源限制:CPU 最大使用 0.5 核
env:
- name: MAX_HEAP_SIZE
value: 512M # JVM 最大堆内存
- name: HEAP_NEWSIZE
value: 100M # JVM 新生代内存
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace # 通过 Downward API 获取当前 Pod 所在的命名空间
volumeMounts:
- name: data
mountPath: /cassandra_data # 容器内数据挂载路径
volumes:
- name: data
emptyDir: {} # 使用临时空目录卷作为存储卷
bash
$ kubectl create -f cassandra-rc.yaml
replicationcontroller "cassandra" created
现在,一个Cassandra Pod运行起来了,但还没有组成Cassandra集群。
3.3、Cassandra集群中新节点的自动添加
现在,我们使用Kubernetes提供的Scale(扩容和缩容)机制对Cassandra集群进行扩容:
bash
$ kubectl scale rc cassandra --replicas=2
replicationcontroller "cassandra" scaled
查看Pod,可以看到RC创建并启动了一个新的Pod:
bash
$ kubectl get pods -l="name=cassandra"
NAME READY STATUS RESTARTS AGE
cassandra 1/1 Running 0 5m
cassandra-g52t3 1/1 Running 0 50s
使用Cassandra提供的nodetool工具对任一cassandra实例(Pod)进行访问来验证Cassandra集群的状态。下面的命令将访问名为cassandra的Pod(访问cassandra-g52t3也能获得相同的结果):
bash
$ kubectl exec -ti cassandra -- nodetool status
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns (effective) Host ID Rack
UN 10.1.20.16 51.58 KB 256 100.0% 1625c65d-b5b6-40f4-a794-
6f5a12322d86 rack1
UN 10.1.10.11 51.51 KB 256 100.0% cdfcbf1a-795c-4412-9d3f-
e8fe50bb8deb rack1
可以看到在Cassandra集群中有两个节点处于正常运行状态(Up and Normal,UN)。该结果中的两个IP地址为两个Cassandra Pod的IP地址。
内部的过程为:每个Cassandra节点(Pod)都通过API访问Kubernetes Master,查询名为cassandra的Service的Endpoint(即Cassandra节点),若发现有新节点加入,就进行添加操作,最后成功组成一个Cassandra集群。
我们再增加两个Cassandra实例:
bash
$ kubectl scale rc cassandra --replicas=4
用nodetool工具查看Cassandra集群的状态:
bash
$ kubectl exec -ti cassandra -- nodetool status
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns (effective) Host ID Rack
UN 10.1.20.16 51.58 KB 256 50.5% 1625c65d-b5b6-40f4-a794-
6f5a12322d86 rack1
UN 10.1.10.12 52.03 KB 256 47.0% 8bcc1c3e-44ec-46a7-b981-
4090b206f14e rack1
UN 10.1.20.17 68.05 KB 256 50.6% 579b6493-e92a-47f5-91f2-
9313198a24c9 rack1
UN 10.1.10.11 51.51 KB 256 51.9% cdfcbf1a-795c-4412-9d3f-
e8fe50bb8deb rack1
可以看到4个Cassandra节点都加入Cassandra集群中了。
另外,可以通过查看Cassandra Pod的日志来看到新节点加入集群的记录:
bash
$ kubectl logs cassandra-g52t3
......
INFO 18:05:36 Handshaking version with /10.1.20.17
INFO 18:05:36 Node /10.1.20.17 is now part of the cluster
INFO 18:05:36 InetAddress /10.1.20.17 is now UP
INFO 18:05:38 Handshaking version with /10.1.10.12
INFO 18:05:39 Node /10.1.10.12 is now part of the cluster
INFO 18:05:39 InetAddress /10.1.10.12 is now UP
本例描述了一种通过API查询Service来完成动态Pod发现的应用场景。对于类似于Cassandra的去中心化集群类应用,都可以使用Headless Service查询后端Endpoint这种巧妙的方法来实现对应用集群(属于同一Service)中新加入节点的查找。