10.1 ClusterIp Service 简介
创建服务时默认的就是 ClusterIp 类型的服务,这类服务的特点是只能用于集群内部通信,不能用于外部通信。
它的简化理解图如下所示:

Normal Service
这里我们先介绍普通 Service 的 YAML 文件模板,以及定义时需要注意的事项。
YAML 文件模板
Service 完整的 YAML 配置文件格式如下所示:
apiVersion: v1
kind: Service
metadata: # 元数据
name: string
namespace: string # 如果不填写的话,默认为 default
labels: # 自定义标签属性列表
- name: string
annotations: # 自定义注解属性列表
- name: string
spec: # 详细描述
selector: [] # 标签选择器
type: string # 可选有:ClusterIP(默认)/NodePort/LoadBalancer(外接负载均衡器时选择这个)
clusterIP: string # 不指定的话系统自动分配 IP,当 type=LoadBalancer 时必须指定
sessionAffinity: string # 是否支持 Session,默认值为空,可选值为 ClientIP,ClientIP 表示将同一个客户端的访问请求都转发到同一个后端 Pod
ports: # 需要暴露的端口列表
- name: string # 端口名称
protocols: string # 端口协议,支持 TCP 和 UDP,默认值为 TCP
port: int # 服务监听的端口号
targetPort: int # 需要转发到后端 Pod 的端口号
nodePort: int # 指定映射到 Node 节点的端口号
status: # 当 spec.type=LoadBalancer 时,设置外部负载均衡器的地址,用于公有云环境
loadBalancer:
ingress: # 外部负载均衡器
ip: string # 外部负载均衡器的 IP 地址
hostname: string # 外部负载均衡器的主机名
10.2 服务负载分发策略 & 多端口服务 & 端口命名
服务负载分发策略
在 Kubernetes 集群中有两种负载均衡分发策略,也可以称之为服务的会话亲和性,如下所示:
- RoundRobin:轮询模式,这是默认配置。即定义 YAML 文件时不设置 spec.sessionAffinity 或设置 spec.sessionAffinity=None。它表示的是将请求分发到后端各个 Pod 上,没有固定某个 Pod 对请求进行响应。
- SessionAffinity:会话保持模式,需要在定义 YAML 文件时设置 spec.sessionAffinity=ClientIP。当某个客户端第一次请求转发到后端的某个 Pod 上,那么之后这个客户端的请求也一直由相同的 Pod 进行响应。
多端口服务
有的时候 Pod 会开放多个端口,这样的情况下 Service 也可以开放多个端口与 Pod 的端口形成一一对应的关系。在集群中使用一个服务将多个端口暴露出来。
需要注意的是:创建多端口服务的时候必须给每个端口都命名。
比如 Pod 监听两个端口,其中的 8080 端口用于 HTTP 服务,6443 端口用于 HTTPS 服务,那么 Service 可以使用 80 端口转发到 Pod 的 8080 端口、使用 443 端口转发到 Pod 的 6443 端口,其定义的 YAML 文件如下所示:
apiVersion: v1
kind: Service
metadata:
name: multi-ports-service
spec:
ports:
- name: http
port: 80 # 将 Pod 的 8080 端口映射到 Service 的 80 端口
targetPort: 8080
- name: https
port: 443 # 将 Pod 的 6443 端口映射到 Service 的 443 端口
targetPort: 6443
selector:
app: test
使用命名的端口
也可以在定义 Pod 端口时直接给这些端口命名,然后在 Service 中直接引用这些 Pod 名称、而不是端口号,这样的好处在于如果 Pod 端口经常变动的情况下,只需要修改 Pod 的端口号,而不需要再去修改相关服务的端口号(因为引用的是端口名)。
依然用上面的例子,那么 Pod 的 YAML 文件定义为:
---
kind: Pod
spec:
containers:
- name: test-pod
ports:
- name: http # 端口被命名为 http 端口
containerPort: 8080
- name: https # 端口被命名为 https 端口
containerPort: 6443
引用端口命名的 Service 的 YAML 文件定义为:
apiVersion: v1
kind: Service
metadata:
name: multi-ports-service
spec:
ports:
- name: http
port: 80 # 将 Service 的 80 端口映射到 Pod 名为 http 的端口
targetPort: http
- name: https
port: 443 # 将 Service 的 443 端口映射到 Pod 名为 https 的端口
targetPort: https
selector:
app: test
新建 tomcat-multi-ports-deployment.yaml 文件,并向其中写入如下代码:
apiVersion: apps/v1
kind: Deployment
metadata:
name: multi-ports
spec:
selector:
matchLabels:
app: tomcat-multi-ports
replicas: 2
template:
metadata:
labels:
app: tomcat-multi-ports
spec:
containers:
- name: tomcat
image: kubeguide/tomcat-app:v1
ports:
- name: service # 将 8080 端口命名为 service
containerPort: 8080
- name: shutdown # 将 8005 端口命名为 shutdown
containerPort: 8005
执行创建:
$ kubectl create -f tomcat-multi-ports-deployment.yaml
deployment.apps/multi-ports created
查看创建的详细情况:
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
multi-ports 2/2 2 2 23s
$ kubectl get pods -l app=tomcat-multi-ports
NAME READY STATUS RESTARTS AGE
multi-ports-7ff6f74c7d-sdwcc 1/1 Running 0 96s
multi-ports-7ff6f74c7d-xr4x6 1/1 Running 0 96s
新建 multi-ports-service.yaml 文件,并向其中写入如下代码:
apiVersion: v1
kind: Service
metadata:
name: multi-ports-service
spec:
sessionAffinity: ClientIP # 设置 service 为会话保持模式
ports:
- name: service-port
port: 88 # 将名为 service-port Service 的 88 端口映射到 Pod 名为 service 的端口
targetPort: service
- name: shutdown-port
port: 85 # 将名为 shutdown-port Service 的 85 端口映射到 Pod 名为 shutdown 的端口
targetPort: shutdown
selector:
app: tomcat-multi-ports
执行创建:
$ kubectl create -f multi-ports-service.yaml
service/multi-ports-service created
# 可以看到 multi-ports-service 服务有两个端口,分别为 tcp 88 端口和 tcp 85 端口
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
multi-ports-service ClusterIP 10.96.37.247 <none> 88/TCP,85/TCP 31s
# 查看 endpoints
$ kubectl get endpoints
NAME ENDPOINTS AGE
multi-ports-service 10.20.138.27:8005,10.20.177.111:8005,10.20.138.27:8080 + 1 more... 60s
$ kubectl get endpoints -o yaml
...
subsets:
- addresses:
- ip: 10.20.138.27 # 可以看到 10.20.138.27 IP是 kubesphere02 节点的
nodeName: kubesphere02
targetRef:
kind: Pod
name: multi-ports-7ff6f74c7d-sdwcc
namespace: user-test
resourceVersion: "841755"
uid: 85d7ce56-7694-4de8-a10b-4068ae470e6a
- ip: 10.20.177.111 # 可以看到 10.20.177.111 IP是 kubesphere03 节点的
nodeName: kubesphere03
targetRef:
kind: Pod
name: multi-ports-7ff6f74c7d-xr4x6
namespace: user-test
resourceVersion: "841752"
uid: 7479c39e-19c5-4018-8af5-6901375e83fb
ports:
- name: shutdown-port
port: 8005
protocol: TCP
- name: service-port
port: 8080
protocol: TCP
...
进行验证:
$ curl http://10.96.37.247:88
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/8.0.35</title>
<link href="favicon.ico" rel="icon" type="image/x-icon" />
<link href="favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="tomcat.css" rel="stylesheet" type="text/css" />
</head>
...
只要理解清楚了服务实现的基本原理,在 Kubernetes 集群中创建基本的 Service 还是比较容易的。
需要着重理解的是 Service 的 Cluster IP,是 Kubernetes 集群内部的虚拟 IP,是伪造的 IP 网络,它有如下 4 个特点:
- Cluster IP 只能用于 Service 对象,由 Kubernetes 从 Cluster IP 池中进行分配和管理
- Cluster IP 不能被 ping 通,因为没有“实体网络对象”进行响应,所以在集群内部只能使用 curl 而不能使用 ping
- Cluster IP 只能和 Service Port 一并使用构成通信端口,单独的 Cluster IP 不具备 TCP/IP 通信功能;如果集群外的节点想要访问这个通信端口,还需要额外的操作
- 在集群内部,Node IP 网络、Pod IP 网络和 Cluster IP 之间的通信采用的是 Kubernetes 自己设计的独特的路由规则,这和普通的 IP 路由不一样。
因此 Service 的 Cluster IP 只是属于集群内部的地址。
10.3 Headless Service
"Headless Service" 翻译过来就是“无头服务”,它表示的是创建的 Service 没有设置 Cluster IP。它的创建非常简单,只需要设置 service.spec.clusterIP=None 即可。
它属于一种特殊类型的集群服务,通常应用于以下两种情况中:
- 自定义负载均衡策略,即:不使用 Service 默认的负载均衡策略(默认的策略是通过服务转发连接到符合要求的任一一个 Pod 上)。
- 获取属于相同标签选择器下的所有 Pod 列表
所以通过 Headless Service 可以获取到所有符合相关要求的 Pod 列表,然后可以通过自定义负载均衡器让客户端的连接转发到一个、多个、或是所有的 Pod 上,典型的应用就是:StatefulSet。
Headless Service 的特点如下:
- 在集群内部没有一个特定的 Cluster IP 地址
- kube-proxy 不会处理 Headless Service
- 没有负载均衡和路由功能
- 根据服务是否有标签选择器进行 DNS 配置
是否定义标签选择器主要影响 DNS 配置:
- 设置了 Selector:Endpoints Controller 在 apiService 中会创建 Endpoints 记录,并且修改 DNS 配置返回 A 记录,这样就可以获取到 Headless Service 对应的所有 Pod 的 IP 地址。
- 没有设置 Selector:不会有 Endpoints 记录。
所以 Headless Service 自定义负载均衡的实现逻辑是:通过标签选择器获取到所有符合标签的 Pod 的 IP 地址列表,然后自定义服务响应的方式。
新建 nginx-deployment.yaml 文件,并向其中写入如下内容:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 3 # 创建 3 个 nginx Pod 副本
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.19
ports:
- containerPort: 80 # 指定开放 80 端口
然后创建一个普通的、有 ClusterIP 的服务,新建 normal-service.yaml 文件,并向其中写入如下内容:
apiVersion: v1
kind: Service
metadata:
name: normal-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 8080 # 将服务的 8080 端口映射到 nginx 容器的 80 端口
targetPort: 80
接着创建一个 headless 类型的 Service,新建 headless-service.yaml 文件,并向其中写入如下内容:
apiVersion: v1
kind: Service
metadata:
name: headless-service
spec:
clusterIP: None # 注意:这里一定要设置为 None
selector:
app: nginx
ports:
- protocol: TCP
port: 8080
targetPort: 80
执行创建:
$ kubectl create -f nginx-deployment.yaml
deployment.apps/nginx created
$ kubectl create -f normal-service.yaml
service/normal-service created
$ kubectl create -f headless-service.yaml
service/headless-service created
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/nginx-687548bb8c-2n9fs 1/1 Running 0 45s
pod/nginx-687548bb8c-xtg5v 1/1 Running 0 45s
pod/nginx-687548bb8c-xxfh6 1/1 Running 0 45s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/headless-service ClusterIP None <none> 8080/TCP 29s
service/normal-service ClusterIP 10.96.168.255 <none> 8080/TCP 35s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx 3/3 3 3 45s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nginx-687548bb8c 3 3 3 45s
查询创建的 3 个 Pod 的 IP 地址:
$ kubectl get pods -l app=nginx -o yaml|grep podIP
podIP: 10.20.177.109
podIP: 10.20.177.113
podIP: 10.20.177.114
进入容器内部通过 DNS 查找 podIP,这里单独运行一个使用 utils 镜像创建的 pod 执行命令(镜像中已经安装了 dnsutils):
$ kubectl run --generator=run-pod/v1 --rm utils -it --image tutum/dnsutils bash
If you don't see a command prompt, try pressing enter.
root@utils:/# nslookup normal-service
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: normal-service.user-test.svc.cluster.local
Address: 10.96.168.255
root@utils:/# nslookup headless-service
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: headless-service.default.svc.cluster.local
Address: 10.20.177.114
Name: headless-service.default.svc.cluster.local
Address: 10.20.177.113
Name: headless-service.default.svc.cluster.local
Address: 10.20.177.109
访问 normal-service 获得的是服务在集群中的 ClusterIP,而访问 headless-service 获取到的是符合标签选择器的所有 Pod 的 IP 地址列表。
Headless Service 可以让我们直接连接到所有的 Pod,而不需要使用作为负载均衡的 Service 或是 kube-proxy,这类特殊的服务有其特定的用途,比如经常和 StatefulSet 搭配使用。
10.4 无 Selector 的服务
它也属于一种特殊类型的集群服务。在上一节我们提到过如果没有标签选择器,就不会有 Endpoints 记录。但是可以创建 Endpoints 对象,在 Endpoints 对象中手动指定需要映射到的 IP 地址和端口号。
虽然 Service 服务通常都会被用来代理对于 Pod 的访问,但是也可以代理其它的后端类型,只要我们自定义 Endpoints 记录就可以了。这些情况通常包括:
- 一个集群在不同场景下使用不同的数据库。比如在生产环境中使用外部数据库,而在测试环境中使用集群内的数据库;
- 服务被其它命名空间或是其它集群上的服务调用;
- 当迁移应用时,一些后端在集群内部运行,一些后端在集群外部运行。
需要注意的是:如果只定义一个没有标签选择器的服务,那么创建的服务在集群内有 VIP,只是没有 Endpoints 而已。
下面来看一个示例。
新建 no-selector-svc.yaml 文件,并向其中写入如下内容:
apiVersion: v1
kind: Service
metadata:
name: no-selector-svc
spec:
ports:
- protocol: TCP
port: 80 # 指定 ClusterIP 对应的端口为 80
targetPort: 9376 # 指定对应 Pod 的端口为 9376
执行创建:
$ kubectl create -f no-selector-svc.yaml
service/no-selector-svc created
# 定义的 no-selector-svc 依然是有 ClusterIP 的,为:10.96.215.87
kubectl get svc no-selector-svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
no-selector-svc ClusterIP 10.96.215.87 <none> 80/TCP 30s
新建 test-endpoints.yaml 文件,并向其中写入如下内容:
apiVersion: v1
kind: Endpoints
metadata:
name: no-selector-svc # 这里的名称一定要与需要绑定的 Service 相同
subsets:
- addresses:
- ip: 10.20.177.114 # Endpoints 需要映射到的 IP 地址
ports:
- port: 9376 # Endpoints 映射的 IP 地址对应的端口
执行创建:
$ kubectl create -f test-endpoints.yaml
endpoints/no-selector-svc created
查看新创建好的 Endpoints:
$ kubectl get endpoints no-selector-svc
NAME ENDPOINTS AGE
no-selector-svc 10.20.177.114:9376 28s
在设置 Endpoints 的 subsets.addresses.ip 字段时,需要注意它们不是以下的任意一种:
- 环回地址,比如:IPv4 的 127.0.0.0/8,IPv6 的 ::1/128
- 本地连接,比如:IPv4 的 169.254.0.0/16 和 224.0.0.0/24,IPv6 的 fe80::/64
- Service 的 ClusterIP
访问没有 selector 的 Service,与有标签选择器的服务一样,依然是将请求路由到用户自定义的 Endpoints,在上面的例子中就是地址 10.20.177.114:9376。
Endpoints 的缺点是只能指定 IP,如果想要指定网址可以使用 ExternalName 类型的 Service。
10.5 NodePort Service 简介及实例
在开发阶段或是临时应用,如果想要外部客户端访问集群服务,最常用的方式就是 NodePort Service。
NodePort Service 是在 ClusterIP Service 的基础上,为 Service 在集群所有 Node 节点上绑定一个端口,外部客户端通过 nodeIP:nodePort 就可以访问服务。
在定义 YAML 文件时需要:
- spec.type: 设置值为 NodePort
- spec.ports.nodePort: 设置在 Node 节点上开发的端口号
新建 nginx-deployment.yaml 文件,并向其中写入如下代码:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.19
ports:
- containerPort: 80 # 指定开放 80 端口
执行创建:
$ kubectl create -f nginx-deployment.yaml
deployment.apps/nginx created
新建 nginx-service-nodeport.yaml 文件,并向其中写入如下内容:
apiVersion: v1
kind: Service
metadata:
name: nginx-service-nodeport
spec:
type: NodePort
ports:
- port: 80 # 设置 ClusterIP 对应的端口为 80
targetPort: 80 # Pod 开放的端口为 80
nodePort: 31000 # 设置在 Node 上开放的端口为 31000
selector:
app: nginx
注意:nodePort 的默认范围为 30000-32767,我们设置的时候也需要在这个范围之内。
执行创建:
$ kubectl create -f nginx-service-nodeport.yaml
service/nginx-service-nodeport created
# CLUSTER-IP 的值为 10.96.152.188,PORT(S) 表示集群 IP(80 端口)和 node 端口(31000)
$ kubectl get svc nginx-service-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-service-nodeport NodePort 10.96.152.188 <none> 80:31000/TCP 24s
现在外部客户端可以通过 nodeIP:nodePort 访问服务:
$ curl 192.168.0.31:31000
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...
</body>
</html>
也可以在集群内部使用 clusterIP:port 访问服务:
curl 10.96.152.188:80
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...
</body>
</html>
在使用 nodePort Service 时,需要注意:
- node 每个端口只能提供一个服务
- 如果 node IP 地址发生变化,访问的地址也要随之发生改变
10.6 扩展:客户端直接访问 Pod
前面介绍了服务 Service 直接被外部客户端访问,其实 Pod 也可以直接被外部客户端访问。主要通过两种方式实现,在定义 Pod YAML 文件时设置:
- spec.ports.hostPort: 容器级别的设置,将容器应用的端口号映射到 Node 节点上;
- spec.hostNetwork: Pod 级别的设置,Pod 中所有容器的端口号都被直接映射到 Node 节点上;
还有一种方式是使用命令 kubectl port-forward 实现数据转发,kubectl 会主动监听本地的某个端口,然后将对于本地端口的请求转发到 Pod 容器端口上。
下面逐个看一下这些方式的使用。
hostPort
hostPort 是将容器应用的端口号映射到所在 Node 节点上,通过 NodeIP + hostPort 可以直接访问容器应用。
新建 pod-hostport.yaml 文件,并向其中写入如下代码:
apiVersion: v1
kind: Pod
metadata:
name: pod-hostport
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.19
ports:
- containerPort: 80
hostPort: 8888
执行创建:
$ kubectl create -f pod-hostport.yaml
pod/pod-hostport created
$ kubectl get pods pod-hostport -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-hostport 1/1 Running 0 33s 10.20.177.119 kubesphere03 <none> <none>
可以看到 pod-hostport 在 kubesphere03 节点上,然后可以直接通过 kubesphere03 IP:hostPort 直接访问容器:
$ curl 192.168.0.33:8888
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...
</body>
</html>
需要注意的是:当 Pod 被重新调度以后,所在的 Node 节点也会发生变化。
hostNetwork
hostNetwork 用于直接定义 Pod 网络,将 Pod 中所有容器的端口号直接映射到 Node 节点上。通过 spec.hostNetwork=true 设置,并且 spec.ports.containerPort 和 spec.ports.hostPort 必须相同:
- 当不指定 spec.ports.hostPort 时,默认 hostPort 与 containerPort 相同
- 当指定 spec.ports.hostPort 时,hostPort 必须与 containerPort 相同
新建 pod-hostnetwork.yaml 文件,并向其中写入如下代码:
apiVersion: v1
kind: Pod
metadata:
name: pod-hostnetwork
labels:
app: nginx
spec:
hostNetwork: true
containers:
- name: nginx
image: nginx:1.19
ports:
- containerPort: 80
hostPort: 80
执行创建:
$ kubectl create -f pod-hostnetwork.yaml
pod/pod-hostnetwork created
$ kubectl get pod pod-hostnetwork -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-hostnetwork 1/1 Running 0 27s 192.168.0.33 kubesphere03 <none> <none>
可以看到 pod-hostnetwork 在 kubesphere03 节点上,并且 IP 地址也是节点的 IP 地址:192.168.0.33,然后可以直接通过 kubesphere03 IP:hostPort 直接访问容器:
$ curl 192.168.0.33:80
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...
</body>
</html>
需要注意的是:启动 Pod 后它被调度分配到的节点可能都是不同的,所以想要访问 Pod 的 nodeIP 地址是不固定的,并且需要注意 Pod 的端口不能与 Node 节点的端口有冲突。
Port Forward
Port Forward是直接将本地端口与 Pod 端口绑定,通常用于调试服务。
当客户端发送请求到 kubectl 监听的本地端口后,kubectl 会将数据发送给 apiServer,apiServer 与 kubelet 建立连接,然后通过目标 Pod 端口以及 Pod 内部指定的容器端口,最终将请求送达。
直接使用命令创建一个 nginx Pod 进行查看:
$ kubectl run --generator=run-pod/v1 nginx --image nginx:1.19 --port=80
pod/nginx created
查看 nginx Pod 监听的端口:
$ kubectl get pods nginx --template='{{(index (index .spec.containers 0).ports 0).containerPort}}{{"\n"}}'
80
将本地的 3000 端口转发到 nginx Pod 的 80 端口:
$ kubectl port-forward nginx 3000:80
Forwarding from 127.0.0.1:3000 -> 80
Forwarding from [::1]:3000 -> 80
新开一个终端访问 127.0.0.1:3000 地址:
$ curl 127.0.0.1:3000
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...
</body>
</html>
评论区