在 Kubernetes 中,Service 是一组 Pod 的逻辑集合和访问方式的抽象。 对于一组 Pod,Service 以统一的形式对外暴露成一个服务,它利用运行在内核空间的 iptables(或 ipvs)转发来自节点内部和外部的流量。 本文不会深入分析 Service 的实现原理 1,而是通过循序渐进的方式给出访问业务 Pod 的实践过程。
1 准备工作
笔者拥有一个包含 11 个节点的集群,这些节点(私网 IP 地址为 192.168.23.160
~192.168.23.170
)相互之间可以免密登录,且均不具备公网 IP。
在本机(macOS 10.15,私网 IP 为 192.168.18.254
)通过 ssh
登录到节点 ubuntu 上获取各节点信息:
k8s@ubuntu:~$ k get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ubuntu Ready control-plane,master 3d v1.22.2 192.168.23.160 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-large-2 Ready <none> 2d19h v1.22.2 192.168.23.161 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-large-3 Ready <none> 2d19h v1.22.2 192.168.23.162 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-medium-1 Ready <none> 2d19h v1.22.2 192.168.23.163 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-medium-2 Ready <none> 2d19h v1.22.2 192.168.23.164 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-medium-3 Ready <none> 2d19h v1.22.2 192.168.23.165 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-medium-4 Ready <none> 2d19h v1.22.2 192.168.23.166 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-small-1 Ready <none> 2d18h v1.22.2 192.168.23.167 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-small-2 Ready <none> 2d18h v1.22.2 192.168.23.168 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-small-3 Ready <none> 2d18h v1.22.2 192.168.23.169 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
worker-small-4 Ready <none> 2d18h v1.22.2 192.168.23.170 <none> Ubuntu 18.04.5 LTS 4.15.0-112-generic docker://20.10.9
2 部署 Deployment
部署一个如下的 Deployment——该 Deployment 通过 ReplicaSet 控制三个 nginx Pod 副本,每个 nginx Pod 暴露自身的 80
端口。
# clusterIP-example.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-1
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: nginx-dp-1
replicas: 3
template:
metadata:
labels:
app: nginx-dp-1
spec:
containers:
- name: nginx
image: nginx
ports:
# 每一个 nginx Pod 暴露自身的 80 端口
- containerPort: 80
通过 apply
命令进行部署:
k8s@ubuntu:~/learn-k8s/09-service$ k apply -f clusterIP-example.yaml
deployment.apps/nginx-deployment-1 created
k8s@ubuntu:~/learn-k8s/09-service$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-1-7bdf85bc86-72jfj 1/1 Running 0 11s 10.244.2.52 worker-large-3 <none> <none>
nginx-deployment-1-7bdf85bc86-9b5fh 1/1 Running 0 11s 10.244.1.43 worker-large-2 <none> <none>
nginx-deployment-1-7bdf85bc86-sl2f8 1/1 Running 0 11s 10.244.1.44 worker-large-2 <none> <none>
可以发现,这三个 Pod 分别被分配了一个集群内的私网 IP:10.244.2.52
、10.244.1.43
、10.244.1.44
,其中有两个被调度到了节点 worker-large-2 上,有一个被调度到了 worker-large-3 上。
在 ** 集群内 ** 的任意节点上,均可以通过这些 IP 地址访问到对应的 nginx Pod。
例如,在节点 ubuntu 上通过 curl
访问 nginx-deployment-1-7bdf85bc86-9b5fh
:
k8s@ubuntu:~/learn-k8s/09-service$ curl -v 10.244.1.43
* Rebuilt URL to: 10.244.1.43/
* Trying 10.244.1.43...
* TCP_NODELAY set
* Connected to 10.244.1.43 (10.244.1.43) port 80 (#0)
> GET / HTTP/1.1
> Host: 10.244.1.43
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.21.3
< Date: Fri, 15 Oct 2021 07:25:42 GMT
< Content-Type: text/html
< Content-Length: 615
< Last-Modified: Tue, 07 Sep 2021 15:21:03 GMT
< Connection: keep-alive
< ETag: "6137835f-267"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
...
接下来,我们登录到节点 worker-medium-4 上访问 nginx-deployment-1-7bdf85bc86-sl2f8
:
k8s@worker-medium-4:~$ curl 10.244.1.44
<!DOCTYPE html>
<html>
...
<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>
...
以上操作表明在集群内任意节点上均可以直接访问这些 nginx Pod。实际上,只要是集群内的 “机器”(包含节点、在节点之上运行的虚拟机和容器),均可以直接访问这些 nginx Pod。
接下来,我们部署一个 centos Pod,并在该 Pod 内发起对 nginx-deployment-1-7bdf85bc86-9b5fh
的访问:
k8s@ubuntu:~/learn-k8s/09-service$ k run -it centos --image=centos -- /bin/sh
sh-4.4# curl 10.244.1.43 # 访问 nginx-deployment-1-7bdf85bc86-9b5fh
<!DOCTYPE html>
<html>
...
<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>
sh-4.4# exit
exit
Session ended, resume using 'kubectl attach centos -c centos -i -t' command when the pod is running
因为本机(macOS)不在该集群内,因此在本机的浏览器 URL 栏中输入这些 nginx Pod 的 IP 地址并不能访问到它们。
3 部署 Service
Service 是一组 Pod 的逻辑集合和访问方式的抽象。 当这组 Pod 发生变化的时候(通过伸缩操作被创建或销毁),相应的 Pod IP 也会变化。 Service 会监听这种变化,在更新自身的同时对外维持一个固定不变的访问方式,这就是为什么要使用 Service 的原因。 本质上,对于某个业务,Service 为访问者提供了固定的访问方式,而无序关心这些业务的实际运行状态。
Service 有四种类型,分别为 ClusterIP、NodePort、LoadBalancer 和 ExternalName,图 1 给出了相关概念的可视化展示。 接下来将依次介绍它们。
3.1 部署 ClusterIP 类型的 Service
我们将 clusterIP-example.yaml 文件更新如下:
# clusterIP-example.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-1
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: nginx-dp-1
replicas: 3
template:
metadata:
labels:
app: nginx-dp-1
spec:
containers:
- name: nginx
image: nginx
ports:
# 每一个 nginx Pod 暴露自身的 80 端口
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-svc-clusterip
spec:
# ClusterIP 适用于集群内部访问
type: ClusterIP
selector:
app: nginx-dp-1 # 通过 selector 建立起本 Service 和对应的资源类型的绑定
ports:
- name: http1
port: 80 # 本 svc 要暴露的端口
targetPort: 80 # 本 svc 所指向的 Pod 所暴露的端口
使用 apply
命令创建名为 nginx-svc-clusterip
的 Service。可以看到:
k8s@ubuntu:~/learn-k8s/09-service$ k get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d2h <none>
nginx-svc-clusterip ClusterIP 10.97.103.102 <none> 80/TCP 87m app=nginx-dp-1
ClusterIP 类型的 Service 只能在集群内部访问。因此,集群内的 “机器” 均可通过 10.97.103.102
访问到该 Service。
该 Service 会将发往自身的流量通过某种策略转发给背后的某个具体的 nginx Pod。
下面分别展示了在节点 ubuntu、centos Pod 和节点 worker-medium-3 访问 nginx-svc-clusterip
:
# 在节点 ubuntu 上访问该 svc
k8s@ubuntu:~/learn-k8s/09-service$ curl 10.97.103.102
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
k8s@ubuntu:~/learn-k8s/09-service$ k run -it centos --image=centos -- /bin/sh
# 在 centos Pod 内访问该 svc
sh-4.4# curl 10.97.103.102
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
sh-4.4# exit
exit
Session ended, resume using 'kubectl attach centos -c centos -i -t' command when the pod is running
# 在节点 worker-medium-3 上访问该 svc
k8s@worker-medium-3:~$ curl 10.97.103.102
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
Service 还提供服务发现的功能。我们只需要通过 SVC_NAME.default.svc.cluster.local
这种域名的方式即可访问该 Service,而无需记住其 IP 2。
在该域名中,default
是本 Service 所处的命名空间,cluster.local
意为 “本地集群”。
为了展示服务发现,我们创建一个 busybox Pod,进入其中,并使用 nslookup
查看名为 nginx-svc-clusterip
的 Service 是否存在:
# 我们即将创建的 busybox Pod
k8s@ubuntu:~/learn-k8s/09-service$ cat ../03-pod/basic-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: default
spec:
containers:
- name: busybox
# busybox:latest 的 nslookup 命令有 bug,因此此处指定版本为 1.28.3
image: busybox:1.28.3
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
restartPolicy: Always
k8s@ubuntu:~/learn-k8s/09-service$ k apply -f ../03-pod/basic-pod.yaml
pod/busybox created
k8s@ubuntu::~$ k get svc -o wide -A # 注意观察 kube-dns 的 IP
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d2h <none>
default nginx-svc-clusterip ClusterIP 10.97.103.102 <none> 80/TCP 107m app=nginx-dp-1
kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 3d2h k8s-app=kube-dns
k8s@ubuntu:~/learn-k8s/09-service$ k exec -it busybox -- /bin/sh
/ # cat /etc/resolv.conf # kube-dns 是集群内的 DNS 服务器,因此被记录在了集群内任意 Pod 的 `/etc/resolv.conf` 文件中
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
/ # nslookup nginx-svc-clusterip # 可以找到该服务,其地址为 10.97.103.102
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: nginx-svc-clusterip
Address 1: 10.97.103.102 nginx-svc-clusterip.default.svc.cluster.local
/ # nslookup nginx-svc-clusterip.default.svc # 默认是. default.svc,因此不用写全也可发现该服务
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: nginx-svc-clusterip.default.svc
Address 1: 10.97.103.102 nginx-svc-clusterip.default.svc.cluster.local
/ # ping nginx-svc-clusterip # Service 的 IP 是虚拟 IP,可以访问但是无法 ping 通
PING nginx-svc-clusterip (10.97.103.102): 56 data bytes
^C
--- nginx-svc-clusterip ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
/ # exit
command terminated with exit code 1
因为本机(macOS)不在该集群内,因此在本机的浏览器 URL 栏中输入 nginx-svc-clusterip
的 IP 地址并不能访问到该 Service。
将请求到 Service 的流量转发到后端的某个 Pod,这叫做负载均衡。Kubernetes 默认提供了两个策略:
- 如果不在 Service 的资源文件指定,则使用 kube-proxy 的策略,如轮询、随机等;
- 在 Service 的资源文件
spec
内添加spec.sessionAffinity: ClientIP
,则是基于客户端地址的会话保持模式,即来自同一个客户端发起的所有请求都会转发到固定的一个 Pod 上。
Kubernetes 还提供了一种所谓的 “Headless Service”,这种 Service 除了不会被分配 ClusterIP,其余和 ClusterIP 类型的 Service 并无区别。
如果想要访问该 Service,只能通过域名进行查询(可以通过设定 svc.spec.ClusterIP
为 None
来创建它)。Headless Service 的存在为了方便开发者自己来控制负载均衡策略。
对于 Headless Service,通过 dig @DNS 服务器 IP HEADLESS-SVC.ns-name.svc.cluster.local
对其进行查询,可发现该 Service 仍然指向后端的某个(些)Pod。
此处不再展示。
3.2 部署 NodePort 类型的 Service
首先,根据如下的 yaml 文件创建一组 Pod 和对应的 Service,类型选择 NodePort:
# nodePort-example.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-2
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: nginx-dp-2
replicas: 3
template:
metadata:
labels:
app: nginx-dp-2
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-svc-nodeport
spec:
# NodePort 适用于对外访问
# 通过 "任意节点 IP: 分配的端口号" 均可访问到该服务
type: NodePort
selector:
app: nginx-dp-2
ports:
- name: http2
# 最好让集群自动为我们指定 nodePort,而非手动指定
port: 80
targetPort: 80
类似地,nginx-svc-nodeport
通过 app: nginx-dp-2
这个 Label 建立 Pod 和自身的关联。
k8s@ubuntu:~/learn-k8s/09-service$ k apply -f nodePort-example.yaml
deployment.apps/nginx-deployment-2 created
service/nginx-svc-nodeport created
k8s@ubuntu:~/learn-k8s/09-service$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-1-7bdf85bc86-72jfj 1/1 Running 0 130m 10.244.2.52 worker-large-3 <none> <none>
nginx-deployment-1-7bdf85bc86-9b5fh 1/1 Running 0 130m 10.244.1.43 worker-large-2 <none> <none>
nginx-deployment-1-7bdf85bc86-sl2f8 1/1 Running 0 130m 10.244.1.44 worker-large-2 <none> <none>
nginx-deployment-2-974f55497-4j2fr 1/1 Running 0 7m26s 10.244.2.65 worker-large-3 <none> <none>
nginx-deployment-2-974f55497-nnrwc 1/1 Running 0 7m26s 10.244.2.66 worker-large-3 <none> <none>
nginx-deployment-2-974f55497-xpm7v 1/1 Running 0 7m26s 10.244.2.64 worker-large-3 <none> <none>
k8s@ubuntu:~/learn-k8s/09-service$ k get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d2h <none>
nginx-svc-clusterip ClusterIP 10.97.103.102 <none> 80/TCP 123m app=nginx-dp-1
nginx-svc-nodeport NodePort 10.96.192.33 <none> 80:31608/TCP 23s app=nginx-dp-2
NodePort 是在 ClusterIP 的基础之上,还额外提供了 “映射到集群内每一个节点的特定端口号” 的功能。
这意味着,首先,我们可以在集群内任意 “机器” 上通过访问 10.96.192.33
来访问该 Service;
此外,还可以在子网内任意 “机器” 上通过 集群内任意节点私网 IP:31608
访问该 Service。
下面依次展示了在集群内任意节点上、集群内任意 Pod 上、子网内任意节点(指本机)上访问该 Service:
# 在 worker-large-3 上访问 worker-small-4 上的该服务
k8s@worker-large-3:~$ curl 192.168.23.170:31608
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
# 在某个 Pod 内访问 worker-medium-3 上的该服务
k8s@ubuntu:~/learn-k8s/09-service$ k exec -it nginx-deployment-1-7bdf85bc86-72jfj -- /bin/sh
# curl 192.168.23.165:31608
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
# exit
# 在本机 macOS 上访问(或通过浏览器访问)
(base) ➜ ~ curl 192.168.23.169:31608
<!DOCTYPE html>
<html>
<head>
<<title>Welcome to nginx!</title>
...
</html>
</html>
实际上,如果这些节点具有公网 IP,那么已经可以直接在子网外部通过 集群内任意节点公网 IP:31608
的方式访问 nginx 服务了。
NodePort 是引导外部流量到集群内部服务的最原始方式。
3.3 部署 LoadBalancer 类型的 Service
首先,根据如下的 yaml 文件创建一组 Pod 和对应的 Service,类型选择 LoadBalancer:
# loadbalancer-example.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-3
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: nginx-dp-3
replicas: 3
template:
metadata:
labels:
app: nginx-dp-3
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-svc-lb
namespace: default
labels:
app: nginx-dp-3
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
externalTrafficPolicy: Local
ports:
- name: http
port: 80
protocol: TCP
selector:
app: nginx-dp-3
type: LoadBalancer
使用 apply
命令进行部署。然后可以看到:
k8s@ubuntu:~/learn-k8s/09-service$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-1-7bdf85bc86-72jfj 1/1 Running 0 4h2m 10.244.2.52 worker-large-3 <none> <none>
nginx-deployment-1-7bdf85bc86-9b5fh 1/1 Running 0 4h2m 10.244.1.43 worker-large-2 <none> <none>
nginx-deployment-1-7bdf85bc86-sl2f8 1/1 Running 0 4h2m 10.244.1.44 worker-large-2 <none> <none>
nginx-deployment-2-974f55497-4j2fr 1/1 Running 0 120m 10.244.2.65 worker-large-3 <none> <none>
nginx-deployment-2-974f55497-nnrwc 1/1 Running 0 120m 10.244.2.66 worker-large-3 <none> <none>
nginx-deployment-2-974f55497-xpm7v 1/1 Running 0 120m 10.244.2.64 worker-large-3 <none> <none>
nginx-deployment-3-574484cd44-866gj 1/1 Running 0 14s 10.244.1.47 worker-large-2 <none> <none>
nginx-deployment-3-574484cd44-9jzmh 1/1 Running 0 14s 10.244.1.48 worker-large-2 <none> <none>
nginx-deployment-3-574484cd44-tt99r 1/1 Running 0 14s 10.244.2.67 worker-large-3 <none> <none>
k8s@ubuntu:~/learn-k8s/09-service$ k get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d4h <none>
nginx-svc-clusterip ClusterIP 10.97.103.102 <none> 80/TCP 4h2m app=nginx-dp-1
nginx-svc-lb LoadBalancer 10.101.101.122 <pending> 80:30299/TCP 19s app=nginx-dp-3
nginx-svc-nodeport NodePort 10.96.192.33 <none> 80:31608/TCP 120m app=nginx-dp-2
LoadBalancer 是暴露服务到外网的推荐方式。因为在笔者的实验环境中并没有真正的 Load Balancer 在使用,因此 EXTERNAL-IP
这一列显示的是 <pending>
。
LoadBalancer 类型的 Service 是外部流量进入集群内网的入口。外部流量通过该 Service 进入集群内部,该 Service 将流量转发集群内部的一个 “隐藏的 Service”(此处称之为 A),
由 A 将流量再转发到对应的节点的端口号上(本实验中,是 worker-large-2 和 worker-large-3 的 30299
端口,这是因为对应的 endpoints 被调度到了 worker-large-2 和 worker-large-3 上)。
因此,对于任意 “机器”,该 Service 的访问方式有如下几种:
- 若该 “机器” 在子网内,可通过
192.168.23.161:30299
和192.168.23.162:30299
访问该 Service(其余节点的该端口号不可访问); - 若该 “机器” 在集群内,还可通过访问
10.101.101.122
来访问该 Service; - 若该 “机器” 在外网,则只能通过对应的 Load Balancer 的
公网 IP:30299
进行访问。
下面分别展示了在本机上通过方式 1、在集群内的 Pod 上通过方式 2 访问该 Service:
# 在本机上通过方式 1 访问(或从浏览器访问)
(base) ➜ ~ curl 192.168.23.162:30299
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
(base) ➜ ~ curl 192.168.23.163:30299 # 无响应
^C
# 在集群内的 Pod 上通过方式 2 访问
k8s@ubuntu:~/learn-k8s/09-service$ k exec -it nginx-deployment-1-7bdf85bc86-sl2f8 -- /bin/sh
# curl 10.101.101.122
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
# exit
3.4 部署 ExternalName 类型的 Service
ExternalName 和以上 Service 的工作目的不同,它是为集群外部的服务创建一个 Service,使得集群内的 “机器” 可以通过该 Service 访问到这个外部服务。
首先,创建这种类型的 Service,外部服务为百度搜索:
apiVersion: v1
kind: Service
metadata:
name: baidu-svc-externalname
spec:
type: ExternalName
externalName: www.baidu.com # 指向的是百度搜索这个服务
对应的 Service 已成功部署:
k8s@ubuntu:~/learn-k8s/09-service$ k get svc -o wide baidu-svc-externalname
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
baidu-svc-externalname ExternalName <none> www.baidu.com <none> 7m5s <none>
此时,通过 dig
命令应该可以发现对应的 DNS 记录已经存在:
k8s@ubuntu:~/learn-k8s/09-service$ dig @10.96.0.10 baidu-svc-externalname.default.svc.cluster.local
; <<>> DiG 9.11.3-1ubuntu1.12-Ubuntu <<>> @10.96.0.10 baidu-svc-externalname.default.svc.cluster.local
; (1 server found)
;; global options: +cmd
;; Got answer:
;; WARNING: .local is reserved for Multicast DNS
;; You are currently testing what happens when an mDNS query is leaked to DNS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6645
;; flags: qr aa rd; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: c56168861bc3f318 (echoed)
;; QUESTION SECTION:
;baidu-svc-externalname.default.svc.cluster.local. IN A
# DNS 查询结果
;; ANSWER SECTION:
baidu-svc-externalname.default.svc.cluster.local. 5 IN CNAME www.baidu.com.
www.baidu.com. 5 IN CNAME www.a.shifen.com.
www.a.shifen.com. 5 IN A 220.181.38.149
www.a.shifen.com. 5 IN A 220.181.38.150
;; Query time: 1 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Sat Nov 06 15:23:53 CST 2021
;; MSG SIZE rcvd: 271
3.5 总结
Service 是一个概念性质的资源对象,真正起作用的,其实是 kube-proxy 服务进程。 集群内每个节点上都运行着一个 kube-proxy 服务进程,当创建 Service 的时候会通过 api-server 向 etcd 写入创建的 service 的信息, 而 kube-proxy 会基于监听的机制发现这种 Service 的变动,然后它会将最新的 Service 信息转换成对应的访问规则。
kube-proxy 支持三种工作模式:
- userspace 模式。 该模式下,kube-proxy 会为每一个 Service 创建一个监听端口,发向 ClusterIP 的请求被 Iptables 规则重定向到 kube-proxy 监听的端口上,kube-proxy 选择一个提供服务的 Pod 并和其建立链接,以将请求转发到 Pod 上。该模式下,kube-proxy 本质上是一个工作在 OSI 网络模型第 4 层的负载均衡器。由于 kube-proxy 运行在 userspace 中,在进行转发处理时会增加内核和用户空间之间的数据拷贝,因此虽然稳定,但效率较低。
- iptables 模式。 该模式下,kube-proxy 为 Service 后端的每个 Pod 创建对应的 iptables 规则,直接将发向 ClusterIP 的请求重定向到一个 Pod IP。该模式下 kube-proxy 不承担四层负责均衡器的角色,只负责创建 iptables 规则。该模式的优点是效率更高,但不能提供灵活的负载均衡策略,当后端 Pod 不可用时也无法进行重试。
- ipvs 模式。 和 iptables 类似,kube-proxy 监控 Pod 的变化并创建相应的 ipvs 规则。ipvs 相对 iptables 转发效率更高,且支持更多的负载均衡策略。
4 部署 Ingress
根据上一章节的描述可以总结,Service 对外暴露服务的方式主要是 NodePort 和 LoadBalancer。细细一想,这二者都有一定的缺点:
- NodePort 的缺点是会占用集群内节点的很多端口,当集群内服务变多时,这个缺点就愈发明显;
- LoadBalancer 的缺点是每个 Service 都需要一个外部的负载均衡器,浪费且麻烦。
为了解决这些问题,Kubernetes 引入了名为 Ingress 的资源对象,Ingress 只需要一个 NodePort 或一个 LoadBalancer 就可以同时暴露多个集群内服务。 Ingress 的工作原理如图 2 所示。Ingress 相当于一个工作在 OSI 模型第 7 层的负载均衡器,是 Kubernetes 对反向代理的一个抽象。 它的工作原理类似于 Nginx,本质上是在 Ingress 里建立诸多映射规则,并通过 Ingress Controller 监听这些配置规则并转化成 Nginx 的反向代理配置,从而对外部提供服务。 在上述描述中,Ingress 的作用是定义 “请求如何转发到 Service” 的具体规则,而 Ingress Controller 则是具体实现反向代理及负载均衡的程序,它通过对 Ingress 定义的规则进行解析,根据配置实现请求转发,实现方式有 Nginx、Contour 以及 Haproxy 等。
Ingress 的工作流程可以总结如下(以 Nginx 作为实现方式):
- 用户编写 Ingress 规则,指明哪个域名对应哪个 Service;
- Ingress Controller 动态感知 Ingress 服务规则的变化,然后生成一段对应的 Nginx 反向代理配置;
- Ingress Controller 将生成的 Nginx 配置写入到一个运行着的 Nginx 实例中,并动态更新;
- 该 Nginx 实例稳定运行中。其内部配置了用户定义的请求转发规则。
4.1 部署 Ingress Controller
为了使用 Ingress,我们首先需要将 Ingress Controller 启动。笔者选择 Nginx 作为实现方式。 我们根据 ingress-nginx 指南 3 部署相应的 controller:
k8s@ubuntu:~/install-k8s$ k apply -f ingress-nginx-deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
configmap/ingress-nginx-controller created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
service/ingress-nginx-controller-admission created
service/ingress-nginx-controller created
deployment.apps/ingress-nginx-controller created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
serviceaccount/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
# 记住 10.244.2.168 这个 IP 地址,后面还会提到它
k8s@ubuntu:~/install-k8s$ k get po -n ingress-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-admission-create--1-cf26z 0/1 Completed 0 2m56s 10.244.1.116 worker-large-2 <none> <none>
ingress-nginx-admission-patch--1-554gq 0/1 Completed 1 2m56s 10.244.2.167 worker-large-3 <none> <none>
ingress-nginx-controller-69bb54574d-fnlmp 1/1 Running 0 2m56s 10.244.2.168 worker-large-3 <none> <none>
k8s@ubuntu:~/install-k8s$ k get svc -o wide -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
ingress-nginx-controller LoadBalancer 10.111.69.58 <pending> 80:32125/TCP,443:32763/TCP 3m5s app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
ingress-nginx-controller-admission ClusterIP 10.110.129.22 <none> 443/TCP 3m5s app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
可以发现相应的 Pod 和 Service 已成功部署。为了检验 ingress-nginx-controller
是否可用,可以从各种角度测试一下:
# 直接访问 ingress-nginx-controller 的 IP
k8s@ubuntu:~/install-k8s$ curl 10.111.69.58:80 # http
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
k8s@ubuntu:~/install-k8s$ curl 10.111.69.58:443 # https
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx</center>
</body>
</html>
# 查看 ivps 规则
k8s@ubuntu:~/learn-k8s/13-ingress$ sudo ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
...
# 以下两条规则表明发往 10.111.69.58:80 的流量会被转发到 10.244.2.168:80、发往 10.111.69.58:443 的流量会被转发到 10.244.2.168:443
# 注意,根据前面的输出可以发现,10.244.2.168 就是 ingress-nginx-controller-69bb54574d-fnlmp、也就是 ingress-nginx-controller 背后真正在运行的 Nginx server 的 IP 地址!
TCP 10.111.69.58:80 rr
-> 10.244.2.168:80 Masq 1 0 0
TCP 10.111.69.58:443 rr
-> 10.244.2.168:443 Masq 1 0 0
...
# 注意,只能访问节点 worker-large-3,因为这是 Pod ingress-nginx-controller-69bb54574d-fnlmp 所在节点
k8s@ubuntu:~/install-k8s$ curl 192.168.23.162:32125 # http
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
k8s@ubuntu:~/install-k8s$ curl 192.168.23.162:32763 # https
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx</center>
</body>
</html>
4.2 创建 Ingress 资源对象
本实验将创建两个 Service,分别为 nginx-svc 和 tomcat-svc,每个 Service 背后有三个 endpoints,分别对应 nginx Pod 和 tomcat Pod。
# svcs.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx-pod
template:
metadata:
labels:
app: nginx-pod
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat-deployment
spec:
replicas: 3
selector:
matchLabels:
app: tomcat-pod
template:
metadata:
labels:
app: tomcat-pod
spec:
containers:
- name: tomcat
image: tomcat:8.5-jre10-slim
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
selector:
app: nginx-pod
type: ClusterIP
clusterIP: None
ports:
- port: 80
targetPort: 80
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: tomcat-svc
spec:
selector:
app: tomcat-pod
type: ClusterIP
clusterIP: None
ports:
- port: 8080
targetPort: 8080
protocol: TCP
成功部署后,可以看到:
k8s@ubuntu:~/learn-k8s/13-ingress$ k get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 27d <none>
nginx-svc ClusterIP None <none> 80/TCP 8m40s app=nginx-pod
tomcat-svc ClusterIP None <none> 8080/TCP 8m40s app=tomcat-pod
k8s@ubuntu:~/learn-k8s/13-ingress$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-5c767764f5-h5brh 1/1 Running 0 8m51s 10.244.2.174 worker-large-3 <none> <none>
nginx-deployment-5c767764f5-jjjf9 1/1 Running 0 8m51s 10.244.1.122 worker-large-2 <none> <none>
nginx-deployment-5c767764f5-v6sxr 1/1 Running 0 8m51s 10.244.2.173 worker-large-3 <none> <none>
tomcat-deployment-7db86c59b7-9kblx 1/1 Running 0 8m51s 10.244.1.121 worker-large-2 <none> <none>
tomcat-deployment-7db86c59b7-lpzpr 1/1 Running 0 8m51s 10.244.2.172 worker-large-3 <none> <none>
tomcat-deployment-7db86c59b7-rdzn7 1/1 Running 0 8m51s 10.244.1.120 worker-large-2 <none> <none>
接下来,以 http 的方式将上述 Service 添加到 Ingress 规则中:
# ingress-http.yaml (以下写法适用于 v1.22)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-http
annotations:
# 新版本要求这个 annotation 必须被添加,否则路由规则无法被正确建立!
kubernetes.io/ingress.class: "nginx"
spec:
rules:
# 规则 1: 将 nginx.svc.com 关联到 nginx-svc:80
- host: nginx.svc.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-svc
port:
number: 80
# 规则 2: tomcat.svc.com 关联到 tomcat-svc:8080
- host: tomcat.svc.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tomcat-svc
port:
number: 8080
通过 apply
命令进行部署,可以看到部署结果:
k8s@ubuntu:~/learn-k8s/13-ingress$ k apply -f ingress-http.yaml
ingress.networking.k8s.io/ingress-http created
k8s@ubuntu:~/learn-k8s/13-ingress$ k get ing
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-http <none> nginx.svc.com,tomcat.svc.com 80 8m59s
k8s@ubuntu:~/learn-k8s/13-ingress$ k describe ing ingress-http
Name: ingress-http
Namespace: default
Address:
Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
Host Path Backends
---- ---- --------
nginx.svc.com # 匹配到 nginx-svc 的三个 endpoints
/ nginx-svc:80 (10.244.1.122:80,10.244.2.173:80,10.244.2.174:80)
tomcat.svc.com # 匹配到 tomcat-svc 的三个 endpoints
/ tomcat-svc:8080 (10.244.1.120:8080,10.244.1.121:8080,10.244.2.172:8080)
Annotations: kubernetes.io/ingress.class: nginx
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 9m33s nginx-ingress-controller Scheduled for sync # 规则已更新
我们可以查看 ingress-nginx-controller-69bb54574d-fnlmp
的日志,观察以上两条规则是否被建立:
# 查看 ingress-nginx-controller 日志
k8s@ubuntu:~/learn-k8s/13-ingress$ k logs ingress-nginx-controller-69bb54574d-fnlmp -n ingress-nginx
...
I1108 07:14:02.354428 7 controller.go:152] "Configuration changes detected, backend reload required"
I1108 07:14:02.432951 7 controller.go:169] "Backend successfully reloaded"
I1108 07:14:02.433248 7 event.go:282] Event(v1.ObjectReference{Kind:"Pod", Namespace:"ingress-nginx", Name:"ingress-nginx-controller-69bb54574d-fnlmp", UID:"439aa017-52d9-44ca-9a7f-be7f084cd7a5", APIVersion:"v1", ResourceVersion:"5126060", FieldPath:""}): type:'Normal'reason:'RELOAD' NGINX reload triggered due to a change in configuration
...
我们还可以进入 ngress-nginx-controller-69bb54574d-fnlmp
内部,查看 /etc/nginx/nginx.conf
文件是否被更新:
# 进入到 ingress-nginx-controller-69bb54574d-fnlmp 内部,发现已经存在 nginx.svc.com 和 tomcat.svc.com 两条规则
k8s@ubuntu:~/learn-k8s/13-ingress$ k exec -it ingress-nginx-controller-69bb54574d-fnlmp -n ingress-nginx -- /bin/bash
# 查看 Nginx server 的配置信息,发现 nginx.svc.com 和 tomcat.svc.com 已成功被注册
bash-5.1$ cat /etc/nginx/nginx.conf | grep com
## start server nginx.svc.com
server_name nginx.svc.com ;
# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
## end server nginx.svc.com
## start server tomcat.svc.com
server_name tomcat.svc.com ;
# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
## end server tomcat.svc.com
bash-5.1$ exit
exit
我们还可以查看 ipvs 规则是否被建立:
# 登录 worker-large-3 节点,即 ingress-nginx-controller-69bb54574d-fnlmp 所在节点
k8s@worker-large-3:~$ sudo ipvsadm -Ln
[sudo] password for k8s:
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
...
# 以下两条规则表明发往本节点 32125 和 32763 的流量会被转发到 ingress-nginx-controller-69bb54574d-fnlmp 上
TCP 192.168.23.162:32125 rr
-> 10.244.2.168:80 Masq 1 0 0
TCP 192.168.23.162:32763 rr
-> 10.244.2.168:443 Masq 1 0 0
...
# 登录 ubuntu 节点,发现发往本节点 32125 和 32763 的流量不会被处理!这是因为 ingress-nginx-controller 是以 LoadBalancer 类型的 Service 运行的
k8s@ubuntu:~/learn-k8s/13-ingress$ sudo ipvsadm -Ln
[sudo] password for k8s:
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
...
TCP 192.168.23.160:32125 rr
TCP 192.168.23.160:32763 rr
...
因为这两个域名并未通过正常流程在运营商进行注册,所以是无法正常解析的。
我们需要手动将 nginx.svc.com
和 tomcat.svc.com
映射到机器的 IP 地址上。注意,ingress-nginx-controller
以 LoadBalancer 的方式运行,
而 ingress-nginx-controller-69bb54574d-fnlmp
运行在 worker-large-3 上(内网地址 192.168.23.162
),
因此我们需要在子网内任意想要访问 nginx.svc.com
和 tomcat.svc.com
的 /etc/hosts
文件中添加如下两行:
192.168.23.162 nginx.svc.com
192.168.23.162 tomcat.svc.com
下面展示了在节点 ubuntu 和本机(macOS)上添加上述内容之后的效果:
# 在节点 ubuntu 上
k8s@ubuntu:~/learn-k8s/13-ingress$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 ubuntu
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
...
192.168.23.162 nginx.svc.com
192.168.23.162 tomcat.svc.com
# 在本机 macOS 上
(base) ➜ ~ cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
...
# Added by hliangzhao for testing ingress
192.168.23.162 nginx.svc.com
192.168.23.162 tomcat.svc.com
终于,我们可以正常访问这两个域名了!以下展示了在节点 ubuntu 上访问 nginx.svc.com
和 tomcat.svc.com
的效果:
# 访问方式为 “域名”+“ingress-nginx-controller 映射的 http 端口”
k8s@ubuntu:~/learn-k8s/13-ingress$ curl nginx.svc.com:32125
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
<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>
...
k8s@ubuntu:~/learn-k8s/13-ingress$ curl tomcat.svc.com:32125
<!DOCTYPE html>
...
<h1>Apache Tomcat/8.5.35</h1>
</div>
<div id="upper" class="curved container">
<div id="congrats" class="curved container">
<h2>If you're seeing this, you've successfully installed Tomcat. Congratulations!</h2>
</div>
...
下面展示的是在本机(macOS)浏览器访问 nginx.svc.com
的效果:
下面展示的是在本机(macOS)浏览器访问 tomcat.svc.com
的效果:
我们还可以以 https 的方式创建 ingress。此时需要预先创建证书和 TLS 密钥,并在 yaml 文件中指定 tls.hosts.secretName
。
其余步骤与 http 版本并无差别。创建完毕之后,将通过 nginx.svc.com:32763
和 tomcat.svc.com:32763
访问。这里 32763
是 ingress-nginx-controller
的对 443
的端口映射。
此处不再展示相关实验细节。
总结
本质上,Service 是通过规则定义出的、由多个 Pod 对象组合而成的逻辑组合以及访问这组 Pod 的策略。 Service 为 Pod 提供了一个固定、统一的访问入口及负载均衡的能力,并支持基于 DNS 的服务发现。但是当 Service 众多时,会大量消耗节点端口号,因此引入了 Ingress 的概念, 只需要特定域名和一个端口号即可访问所有 Service。
转载申请
本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接。您必须给出适当的署名,并标明是否对本文作了修改。
-
关于 Service 的实现原理参见 Kubernetes Service 的实现原理。 ↩︎
-
实际上,Pod 也会被创建对应的 DNS。具体地,Kubernetes 会创建形如
POD_NAME.SVC_NAME.default.svc.cluster.local
的域名,SVC_NAME
是为POD_NAME
所暴露的服务名称。 ↩︎