从 Service 到 Ingress · K8S 实践 01

Hailiang Zhao | 2021/10/15

Categories: practice Tags: Kubernetes Service Deployment Ingress


在 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.5210.244.1.4310.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 给出了相关概念的可视化展示。 接下来将依次介绍它们。

图 1 Kubernetes Service、IP 以及 Port。

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 默认提供了两个策略:

  1. 如果不在 Service 的资源文件指定,则使用 kube-proxy 的策略,如轮询、随机等;
  2. 在 Service 的资源文件 spec 内添加 spec.sessionAffinity: ClientIP,则是基于客户端地址的会话保持模式,即来自同一个客户端发起的所有请求都会转发到固定的一个 Pod 上。

Kubernetes 还提供了一种所谓的 “Headless Service”,这种 Service 除了不会被分配 ClusterIP,其余和 ClusterIP 类型的 Service 并无区别。 如果想要访问该 Service,只能通过域名进行查询(可以通过设定 svc.spec.ClusterIPNone 来创建它)。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 的访问方式有如下几种:

  1. 若该 “机器” 在子网内,可通过 192.168.23.161:30299192.168.23.162:30299 访问该 Service(其余节点的该端口号不可访问);
  2. 若该 “机器” 在集群内,还可通过访问 10.101.101.122 来访问该 Service;
  3. 若该 “机器” 在外网,则只能通过对应的 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 支持三种工作模式:

  1. userspace 模式。 该模式下,kube-proxy 会为每一个 Service 创建一个监听端口,发向 ClusterIP 的请求被 Iptables 规则重定向到 kube-proxy 监听的端口上,kube-proxy 选择一个提供服务的 Pod 并和其建立链接,以将请求转发到 Pod 上。该模式下,kube-proxy 本质上是一个工作在 OSI 网络模型第 4 层的负载均衡器。由于 kube-proxy 运行在 userspace 中,在进行转发处理时会增加内核和用户空间之间的数据拷贝,因此虽然稳定,但效率较低。
  2. iptables 模式。 该模式下,kube-proxy 为 Service 后端的每个 Pod 创建对应的 iptables 规则,直接将发向 ClusterIP 的请求重定向到一个 Pod IP。该模式下 kube-proxy 不承担四层负责均衡器的角色,只负责创建 iptables 规则。该模式的优点是效率更高,但不能提供灵活的负载均衡策略,当后端 Pod 不可用时也无法进行重试。
  3. ipvs 模式。 和 iptables 类似,kube-proxy 监控 Pod 的变化并创建相应的 ipvs 规则。ipvs 相对 iptables 转发效率更高,且支持更多的负载均衡策略。

4 部署 Ingress

根据上一章节的描述可以总结,Service 对外暴露服务的方式主要是 NodePort 和 LoadBalancer。细细一想,这二者都有一定的缺点:

  1. NodePort 的缺点是会占用集群内节点的很多端口,当集群内服务变多时,这个缺点就愈发明显;
  2. 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 等。

图 2 Kubernetes Ingress 的工作原理。

Ingress 的工作流程可以总结如下(以 Nginx 作为实现方式):

  1. 用户编写 Ingress 规则,指明哪个域名对应哪个 Service;
  2. Ingress Controller 动态感知 Ingress 服务规则的变化,然后生成一段对应的 Nginx 反向代理配置;
  3. Ingress Controller 将生成的 Nginx 配置写入到一个运行着的 Nginx 实例中,并动态更新;
  4. 该 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.comtomcat.svc.com 映射到机器的 IP 地址上。注意,ingress-nginx-controller 以 LoadBalancer 的方式运行, 而 ingress-nginx-controller-69bb54574d-fnlmp 运行在 worker-large-3 上(内网地址 192.168.23.162), 因此我们需要在子网内任意想要访问 nginx.svc.comtomcat.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.comtomcat.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 的效果:

图 3 在 macOS 上访问 nginx.svc.com。

下面展示的是在本机(macOS)浏览器访问 tomcat.svc.com 的效果:

图 4 在 macOS 上访问 tomcat.svc.com。

我们还可以以 https 的方式创建 ingress。此时需要预先创建证书和 TLS 密钥,并在 yaml 文件中指定 tls.hosts.secretName。 其余步骤与 http 版本并无差别。创建完毕之后,将通过 nginx.svc.com:32763tomcat.svc.com:32763 访问。这里 32763ingress-nginx-controller 的对 443 的端口映射。 此处不再展示相关实验细节。

总结

本质上,Service 是通过规则定义出的、由多个 Pod 对象组合而成的逻辑组合以及访问这组 Pod 的策略。 Service 为 Pod 提供了一个固定、统一的访问入口及负载均衡的能力,并支持基于 DNS 的服务发现。但是当 Service 众多时,会大量消耗节点端口号,因此引入了 Ingress 的概念, 只需要特定域名和一个端口号即可访问所有 Service。

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接。您必须给出适当的署名,并标明是否对本文作了修改。


  1. 关于 Service 的实现原理参见 Kubernetes Service 的实现原理。 ↩︎

  2. 实际上,Pod 也会被创建对应的 DNS。具体地,Kubernetes 会创建形如 POD_NAME.SVC_NAME.default.svc.cluster.local 的域名,SVC_NAME 是为 POD_NAME 所暴露的服务名称。 ↩︎

  3. https://kubernetes.github.io/ingress-nginx/deploy/ ↩︎