【Kubernetes】ClusterIp Service

664 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 22 天,点击查看活动详情

一、 ClusterIp Service

ClusterIp Service 可分为两种:普通 ServiceHeadless Service.

默认创建的都是普通 Service

  • ClusterIP Service :只能用于集群内部,不能提供外部的访问。
  • NodePort Service :可以让集群中每个Node都开放一个固定端口,客户端通过连接任意一个 NodeIP + NodePort即可访问集群内部的服务。

ClusterIP 简单理解如图:

2022-04-2313-35-26.png

(1)Normal Service

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 # 外部负载均衡器的主机名

服务负载分发策略 & 多端口服务 & 端口命名

Kubernetes 集群中有两种负载均衡分发策略,也可以称之为服务的会话亲和性,如下所示:

  • RoundRobin:轮询模式,这是默认配置。即定义 YAML 文件时不设置 spec.sessionAffinity 或设置 spec.sessionAffinity=None。它表示的是将请求分发到后端各个 Pod 上,没有固定某个 Pod 对请求进行响应。
  • SessionAffinity:会话保持模式,需要在定义 YAML 文件时设置 spec.sessionAffinity=ClientIP。当某个客户端第一次请求转发到后端的某个 Pod 上,那么之后这个客户端的请求也一直由相同的 Pod 进行响应。

多端口服务:

有的时候 Pod 会开放多个端口,这样的情况下 Service 也可以开放多个端口与 Pod 的端口形成一一对应的关系。 在集群中使用一个服务将多个端口暴露出来。

Tips: 创建多端口服务的时候必须给每个端口都命名。

比如 Pod 监听两个端口,其中的 8000 端口用于 HTTP 服务,8888 端口用于 HTTPS 服务,那么 Service 可以使用 80 端口转发到 Pod 的 8000 端口、使用 88 端口转发到 Pod 的 8888 端口,其定义的 YAML 文件如下所示:

apiVersion: v1
kind: Service
metadata:
  name: multi-ports-service
spec:
  ports:
    - name: http
      port: 80 # 将 Pod 的 8000 端口映射到 Service 的 80 端口
      targetPort: 8000
    - name: https
      port: 88 # 将 Pod 的 8080 端口映射到 Service 的 88 端口
      targetPort: 8080
  selector:
    app: test

也可以在定义 Pod 端口时直接给这些端口命名,然后在 Service 中直接引用这些 Pod 名称、而不是端口号,这样的好处在于如果 Pod 端口经常变动的情况下,只需要修改 Pod 的端口号,而不需要再去修改相关服务的端口号(因为引用的是端口名)。

依然用上面的例子,那么 PodYAML 文件定义为:

---
kind: Pod
spec:
  containers:
    - name: test-pod
      ports:
        - name: http #0 端口被命名为 http 端口
          containerPort: 8000
        - name: https #8 端口被命名为 https 端口
          containerPort: 8888

引用端口命名的 ServiceYAML 文件定义为:

apiVersion: v1
kind: Service
metadata:
  name: multi-ports-service
spec:
  ports:
    - name: http
      port: 80 # 将 Service 的 80 端口映射到 Pod 名为 http 的端口
      targetPort: http
    - name: https
      port: 88 # 将 Service 的 88 端口映射到 Pod 名为 https 的端口
      targetPort: https
  selector:
    app: test

举个栗子

/home/shiyanlou 目录下新建 tomcat-deployment.yaml 文件,并向其中写入如下代码:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: multi-ports
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
        - name: tomcat
          image: tomcat
          ports:
            - name: service # 将 8080 端口命名为 service
              containerPort: 8080
            - name: shutdown # 将 8005 端口命名为 shutdown
              containerPort: 8005

执行创建:

$ kubectl create -f tomcat-deployment.yaml

查看创建的详细情况:

$ kubectl get deploy
NAME          READY   UP-TO-DATE   AVAILABLE   AGE
multi-ports   2/2     2            2           61s
$ kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
multi-ports-797bbf966b-9rh46   1/1     Running   0          94s
multi-ports-797bbf966b-tdrsz   1/1     Running   0          94s

/home/shiyanlou 目录下新建 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-service 服务有两个端口,分别为 tcp 88 端口和 tcp 85 端口
$ kubectl get svc
NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP         85d
multi-ports-service   ClusterIP   10.99.212.184   <none>        88/TCP,85/TCP   7s


# 查看 endpoints
$ kubectl get endpoints
NAME                  ENDPOINTS                                                     AGE
kubernetes            10.192.0.2:6443                                               85d
# 这里省略了一个 IP 地址+端口号 未显示,应该为:10.244.3.3:8080
multi-ports-service   10.244.2.4:8005,10.244.3.3:8005,10.244.2.4:8080 + 1 more...   18s


$ kubectl get endpoints -o yaml
...
  subsets:
  - addresses:
    - ip: 10.244.2.4 # 可以看到 10.244.2.4 的 IP 地址是 kubernetes-worker 节点的
      nodeName: kubernetes-worker
      targetRef:
        kind: Pod
        name: multi-ports-797bbf966b-tdrsz
        namespace: default
        resourceVersion: "2077"
        uid: f6106369-b920-4a76-a7cd-f342d287bfd6
    - ip: 10.244.3.3 # 可以看到 10.244.3.3 的 IP 地址是 kubernetes-worker2 节点的
      nodeName: kubernetes-worker2
      targetRef:
        kind: Pod
        name: multi-ports-797bbf966b-9rh46
        namespace: default
        resourceVersion: "2069"
        uid: 1be2227f-702a-4131-b767-7f1e0d852def
    ports:
    - name: shutdown-port
      port: 8005 # 对应的容器 shutdown 端口号为 8005
      protocol: TCP
    - name: service-port
      port: 8080 # 对应的容器 service 端口号为 8080
      protocol: TCP
...

进行验证:

# 任意进入一个 Node 节点
$ docker exec -it kubernetes-worker bash
root@kubernetes-worker:/# curl 10.99.212.184:88
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Apache Tomcat/8.5.47</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>
...

需要着重理解的是 ServiceCluster IP,是 Kubernetes 集群内部的虚拟 IP,是伪造的 IP 网络,它有如下 4 个特点:

  • Cluster IP 只能用于 Service 对象,由 KubernetesCluster IP 池中进行分配和管理
  • Cluster IP 不能被 ping 通,因为没有“实体网络对象”进行响应,所以在集群内部只能使用 curl 而不能使用 ping
  • Cluster IP 只能和 Service Port 一并使用构成通信端口,单独的 Cluster IP 不具备 TCP/IP 通信功能;如果集群外的节点想要访问这个通信端口,还需要额外的操作。
  • 在集群内部,Node IP 网络、Pod IP 网络和 Cluster IP 之间的通信采用的是 Kubernetes 自己设计的独特的路由规则,这和普通的 IP 路由不一样。

(2)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 ControllerapiService 中会创建 Endpoints 记录,并且修改 DNS 配置返回 A 记录,这样就可以获取到 Headless Service 对应的所有 Pod 的 IP 地址。
  • 没有设置 Selector:不会有 Endpoints 记录。

所以 Headless Service 自定义负载均衡的实现逻辑是:通过标签选择器获取到所有符合标签的 PodIP 地址列表,然后自定义服务响应的方式。

举个栗子

/home/shiyanlou 目录下新建 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.7.9
          ports:
            - containerPort: 80 # 指定开放 80 端口

然后创建一个普通的、有 ClusterIP 的服务,在 /home/shiyanlou 目录下新建 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,在 /home/shiyanlou 目录下新建 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
kubectl create -f normal-service.yaml
kubectl create -f headless-service.yaml
​
​
$ kubectl get all
# 有 3 个新创建的 nginx Pod
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-5754944d6c-5v6wq   1/1     Running   0          2m13s
pod/nginx-5754944d6c-q5mxh   1/1     Running   0          2m13s
pod/nginx-5754944d6c-vzjb9   1/1     Running   0          2m13s
​
# headless-service 的 CLUSTER-IP 为 None,而 normal-service 有一个固定的 CLUSTER-IP 为 10.107.154.5
NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/headless-service   ClusterIP   None           <none>        8080/TCP   90s
service/kubernetes         ClusterIP   10.96.0.1      <none>        443/TCP    85d
service/normal-service     ClusterIP   10.107.154.5   <none>        8080/TCP   97s
​
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx   3/3     3            3           2m13s
​
NAME                               DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-5754944d6c   3         3         3       2m13s

为了便于后面的验证,先查询创建的 3 个 PodIP 地址:

$ kubectl get pods -l app=nginx -o yaml|grep podIP
    podIP: 10.244.2.5
    podIP: 10.244.2.4
    podIP: 10.244.3.3

现在可以进入容器内部通过 DNS 查找 podIP,这里单独运行一个使用 utils 镜像创建的 pod 执行命令(镜像中已经安装了 dnsutils):

$ kubectl run --rm utils -it --image registry-vpc.cn-hangzhou.aliyuncs.com/chenshi-kubernetes/utils:latest bash
​
root@utils:/# nslookup normal-service
Server:   10.96.0.10
Address:  10.96.0.10#53Name: normal-service.default.svc.cluster.local
Address:  10.107.154.5
​
root@utils:/# nslookup headless-service
Server:   10.96.0.10
Address:  10.96.0.10#53Name: headless-service.default.svc.cluster.local
Address:  10.244.3.3
Name: headless-service.default.svc.cluster.local
Address:  10.244.2.4
Name: headless-service.default.svc.cluster.local
Address:  10.244.2.5

通过上面的输出也就证实了,访问 normal-service 获得的是服务在集群中的 ClusterIP,而访问 headless-service 获取到的是符合标签选择器的所有 PodIP 地址列表。

Headless Service 可以让我们直接连接到所有的 Pod,而不需要使用作为负载均衡的 Service 或是 kube-proxy,这类特殊的服务有其特定的用途,比如经常和 StatefulSet 搭配使用。

(3)无 Selector 的服务

它也属于一种特殊类型的集群服务。在上一节我们提到过如果没有标签选择器,就不会有 Endpoints 记录。但是可以创建 Endpoints 对象,在 Endpoints 对象中手动指定需要映射到的 IP 地址和端口号。

虽然 Service 服务通常都会被用来代理对于 Pod 的访问,但是也可以代理其它的后端类型,只要我们自定义 Endpoints 记录就可以了。这些情况通常包括:

  • 一个集群在不同场景下使用不同的数据库。比如在生产环境中使用外部数据库,而在测试环境中使用集群内的数据库;
  • 服务被其它命名空间或是其它集群上的服务调用;
  • 当迁移应用时,一些后端在集群内部运行,一些后端在集群外部运行。

需要注意的是:如果只定义一个没有标签选择器的服务,那么创建的服务在集群内有 VIP,只是没有 Endpoints 而已。

举个栗子

/home/shiyanlou 目录下新建 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.100.179.62
$ kubectl get svc
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes        ClusterIP   10.96.0.1       <none>        443/TCP   87d
no-selector-svc   ClusterIP   10.100.179.62   <none>        80/TCP    22s

然后在 /home/shiyanlou 目录下新建 test-endpoints.yaml 文件,并向其中写入如下内容:

apiVersion: v1
kind: Endpoints
metadata:
  name: no-selector-svc # 这里的名称一定要与需要绑定的 Service 相同
subsets:
  - addresses:
      - ip: 192.0.2.42 # Endpoints 需要映射到的 IP 地址
    ports:
      - port: 9376 # Endpoints 映射的 IP 地址对应的端口号

执行创建:

$ kubectl create -f test-endpoints.yaml
endpoints/no-selector-svc created

查看新创建好的 Endpoints

# 可以发现自定义的与 no-selector-svc 服务相关联的 Endpoints 映射已经建立好了
$ kubectl get endpoints
NAME              ENDPOINTS         AGE
kubernetes        10.192.0.2:6443   87d
no-selector-svc   192.0.2.42:9376   24s

在设置 Endpointssubsets.addresses.ip 字段时,需要注意它们不是以下的任意一种:

  • 环回地址,比如:IPv4127.0.0.0/8IPv6::1/128
  • 本地连接,比如:IPv4169.254.0.0/16224.0.0.0/24IPv6fe80::/64
  • ServiceClusterIP

访问没有 selectorService,与有标签选择器的服务一样,依然是将请求路由到用户自定义的 Endpoints,在上面的例子中就是地址 192.0.2.42:9376

Endpoints 的缺点是只能指定 IP,如果想要指定网址可以使用 ExternalName 类型的 Service

\