Kubernetes权威指南——深入掌握Service(一)

深入掌握Service

1、Service定义详解

YAML格式的Service定义文件的完整内容如下:

yaml 复制代码
apiVersion: v1             # 必填项
kind: Service              # 必填项
metadata:                  # 必填项
  name: string             # 必填项:Service 名称
  namespace: string        # 命名空间
  labels:                  # 标签:必须是 key: value 键值对格式
    app: string
  annotations:             # 注解:必须是 key: value 键值对格式
    key: string
spec:                      # 必填项
  selector:                # 标签选择器:必须是 key: value 键值对格式
    app: string
  type: string             # Service 类型(如 ClusterIP, NodePort, LoadBalancer 等)
  clusterIP: string        # 集群内部 IP(可选)
  sessionAffinity: string  # 会话保持类型(可选,如 ClientIP)
  ports:
  - name: string           # 端口名称
    protocol: string       # 协议(如 TCP, UDP, SCTP)
    port: int              # Service 暴露的端口
    targetPort: int        # 后端容器监听的端口
    nodePort: int          # 节点端口(当 type 为 NodePort 或 LoadBalancer 时使用)
  status: #当 spec.type=LoadBalancer 时,设置外部负载均衡器的地址,用于公有云环境。
  	loadBalancer: # 外部负载均衡器的容器对象。
  		ingress: # 外部负载均衡器的 ingress 列表。
  			ip: # 外部负载均衡器分配的 IP 地址(若云厂商通过 IP 对外暴露)。
  			hostname: # 外部负载均衡器分配的 主机名/域名(若云厂商通过 DNS 对外暴露)。

对各属性的说明如表所示。

2、Service的基本用法

一般来说,对外提供服务的应用程序需要通过某种机制来实现,对于容器应用最简便的方式就是通过TCP/IP机制及监听IP和端口号来实现。例如,定义一个提供Web服务的RC,由两个Tomcat容器副本组成,每个容器都通过containerPort设置提供服务的端口号为8080:

yaml 复制代码
apiVersion: v1
kind: ReplicationController     # 资源类型:RC 控制器(负责确保指定数量的 Pod 副本在任何时候都在运行)
metadata:
  name: webapp                  # ReplicationController 的名称
spec:
  replicas: 2                   # 副本数:期望始终运行 2 个该 Pod 的实例
  template:
    metadata:
      name: webapp
      labels:
        app: webapp             # Pod 的标签(RC 通过该标签来识别和管理哪些 Pod 属于它)
    spec:
      containers:
      - name: webapp            # 容器名称
        image: tomcat           # 使用的容器镜像(这里是 Tomcat)
        ports:
        - containerPort: 8080   # 容器内部暴露的端口

创建该RC:

bash 复制代码
# kubectl create -f webapp-rc.yaml
replicationcontroller "webapp" created

获取Pod的IP地址:

bash 复制代码
# kubectl get pods -l app=webapp -o yaml | grep podIP
    podIP: 172.17.1.4
    podIP: 172.17.1.3

可以直接通过这两个Pod的IP地址和端口号访问Tomcat服务:

bash 复制代码
# curl 172.17.1.3:8080
<!DOCTYPE html>
<html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Apache Tomcat/8.0.35</title>
......
# curl 172.17.1.4:8080
<!DOCTYPE html>
<html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Apache Tomcat/8.0.35</title>
......

直接通过Pod的IP地址和端口号可以访问到容器应用内的服务,但是Pod的IP地址是不可靠的,例如当Pod所在的Node发生故障时,Pod将被Kubernetes重新调度到另一个Node,Pod的IP地址将发生变化。更重要的是,如果容器应用本身是分布式的部署方式,通过多个实例共同提供服务,就需要在这些实例的前端设置一个负载均衡器来实现请求的分发。Kubernetes中的Service就是用于解决这些问题的核心组件。

以前面创建的webapp应用为例,为了让客户端应用访问到两个Tomcat Pod实例,需要创建一个Service来提供服务。Kubernetes提供了一种快速的方法,即通过kubectlexpose命令来创建Service:

bash 复制代码
# kubectl expose rc webapp
service "webapp" exposed

查看新创建的Service,可以看到系统为它分配了一个虚拟的IP地址(ClusterIP)​,Service所需的端口号则从Pod中的containerPort复制而来:

bash 复制代码
# kubectl get svc
NAME         CLUSTER-IP         EXTERNAL-IP    PORT(S)     AGE
webapp      169.169.235.79    <none>          8080/TCP    3s

接下来就可以通过Service的IP地址和Service的端口号访问该Service了:

bash 复制代码
# curl 169.169.235.79:8080
<!DOCTYPE html>
<html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Apache Tomcat/8.0.35</title>
......

这里,对Service地址169.169.235.79:8080的访问被自动负载分发到了后端两个Pod之一:172.17.1.3:8080或172.17.1.4:8080。

除了使用kubectl expose命令创建Service,我们也可以通过配置文件定义Service,再通过kubectl create命令进行创建。例如对于前面的webapp应用,我们可以设置一个Service,代码如下:

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: webapp
spec:
  ports:
  - port: 8081
    targetPort: 8080
  selector:
    app: webapp

Service定义中的关键字段是ports和selector。本例中ports定义部分指定了Service所需的虚拟端口号为8081,由于与Pod容器端口号8080不一样,所以需要再通过targetPort来指定后端Pod的端口号。selector定义部分设置的是后端Pod所拥有的label:app=webapp。

创建该Service并查看其ClusterIP地址:

bash 复制代码
# kubectl create -f webapp-svc.yaml
service "webapp" created

# kubectl get svc
NAME         CLUSTER-IP         EXTERNAL-IP    PORT(S)     AGE
webapp      169.169.28.190    <none>          8081/TCP    3s

通过Service的IP地址和Service的端口号进行访问:

bash 复制代码
# curl 169.169.28.190:8081
<!DOCTYPE html>
<html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Apache Tomcat/8.0.35</title>
......

同样,对Service地址169.169.28.190:8081的访问被自动负载分发到了后端两个Pod之一:172.17.1.3:8080或172.17.1.4:8080。目前Kubernetes提供了两种负载分发策略:RoundRobin和SessionAffinity,具体说明如下。

  • RoundRobin:轮询模式,即轮询将请求转发到后端的各个Pod上。
  • SessionAffinity:基于客户端IP地址进行会话保持的模式,即第1次将某个客户端发起的请求转发到后端的某个Pod上,之后从相同的客户端发起的请求都将被转发到后端相同的Pod上。

在默认情况下,Kubernetes采用RoundRobin模式对客户端请求进行负载分发,但我们也可以通过设置service.spec.sessionAffinity=ClientIP来启用SessionAffinity策略。这样,同一个客户端IP发来的请求就会被转发到后端固定的某个Pod上了。

通过Service的定义,Kubernetes实现了一种分布式应用统一入口的定义和负载均衡机制。Service还可以进行其他类型的设置,例如设置多个端口号、直接设置为集群外部服务,或实现为Headless Service(无头服务)模式(将在4.3节介绍)​。

2.1、多端口Service

有时一个容器应用也可能提供多个端口的服务,那么在Service的定义中也可以相应地设置为将多个端口对应到多个应用服务。在下面的例子中,Service设置了两个端口号,并且为每个端口号都进行了命名:

bash 复制代码
apiVersion: v1
kind: Service
metadata:
  name: webapp          # 服务名称,集群内其他 Pod 可以通过此名称进行通信
  labels:
    app: webapp         # 标签,用于标识该 Service 归属的应用
spec:
  type: ClusterIP       # 服务类型:ClusterIP(默认类型),仅在集群内部提供访问
  ports:
  - name: web           # 端口名称,用于区分不同的监听端口
    port: 8080          # Service 自身对外暴露的端口(集群内其他应用访问时使用的端口)
    targetPort: 8080    # 后端 Pod 容器内部实际监听的端口
  - name: management    # 第二个端口的名称(例如用于管理或监控)
    port: 8005          # 第二个端口对外暴露的端口
    targetPort: 8005    # 后端 Pod 容器内部的对应管理端口
  selector:
    app: webapp         # 标签选择器:精确匹配带有 `app: webapp` 标签的 Pod,将流量导入其中

另一个例子是两个端口号使用了不同的4层协议---TCP和UDP:

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: kube-dns          # 服务名称
  namespace: kube-system  # 所在的命名空间(系统核心组件通常部署在 kube-system)
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "KubeDNS"
spec:
  selector:
    k8s-app: kube-dns     # 标签选择器,用于匹配 CoreDNS / KubeDNS 的 Pod
  clusterIP: 169.169.0.100 # 指定集群内部的固定虚拟 IP(通常由 Kubernetes 自动分配,此处为自定义固定 IP)
  ports:
  - name: dns             # DNS 服务端口(UDP 协议,用于常规 DNS 查询)
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp         # DNS 服务端口(TCP 协议,用于大响应包或区域传输)
    port: 53
    protocol: TCP
    targetPort: 53

2.2、外部服务Service

在某些环境中,应用系统需要将一个外部数据库作为后端服务进行连接,或将另一个集群或Namespace中的服务作为服务的后端,这时可以通过创建一个无LabelSelector的Service来实现:

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: my-service        # 服务名称
spec:
  type: ClusterIP         # 服务类型:ClusterIP(默认类型),仅在集群内部提供访问
  ports:
  - name: http            # 端口名称(建议加上,方便多端口或监控时区分)
    protocol: TCP         # 传输层协议(支持 TCP、UDP、SCTP,默认为 TCP)
    port: 80              # Service 自身对外暴露的端口
    targetPort: 80        # 后端 Pod 容器内部实际监听的端口
  selector:
    app: MyApp            # 标签选择器:精准匹配带有 `app: MyApp` 标签的 Pod(注意:此处需根据实际情况配置)

通过该定义创建的是一个不带标签选择器的Service,即无法选择后端的Pod,系统不会自动创建Endpoint,因此需要手动创建一个和该Service同名的Endpoint,用于指向实际的后端访问地址。创建Endpoint的配置文件内容如下:

yaml 复制代码
apiVersion: v1
kind: Endpoints
metadata:
  name: my-service        # 端点名称(通常需与同名的 Service 名称保持一致)
subsets:
- addresses:
  - ip: 1.2.3.4           # 后端实际提供服务的 Pod 或外部服务器的 IP 地址(注意:字段名为小写的 ip)
    ports:
  - port: 80              # 后端服务实际监听的端口号
    protocol: TCP         # 传输层协议(可选,默认为 TCP)

如图所示,访问没有标签选择器的Service和带有标签选择器的Service一样,请求将会被路由到由用户手动定义的后端Endpoint上。

3、Headless Service

在某些应用场景中,开发人员希望自己控制负载均衡的策略,不使用Service提供的默认负载均衡的功能,或者应用程序希望知道属于同组服务的其他实例。Kubernetes提供了Headless Service来实现这种功能,即不为Service设置ClusterIP(入口IP地址)​,仅通过LabelSelector将后端的Pod列表返回给调用的客户端。例如:

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: nginx             # 服务名称
  labels:
    app: nginx          # 标签,用于标识该 Service 归属的应用
spec:
  clusterIP: None       # 设置为 None 表示无头服务 (Headless Service),不分配集群虚拟 IP,常用于 StatefulSet
  ports:
  - name: http          # 端口名称
    port: 80            # Service 自身对外暴露的端口
    targetPort: 80      # 后端 Pod 容器内部实际监听的端口
  selector:
    app: nginx          # 标签选择器:精准匹配带有 `app: nginx` 标签的 Pod

这样,Service就不再具有一个特定的ClusterIP地址,对其进行访问将获得包含Label"app=nginx"的全部Pod列表,然后客户端程序自行决定如何处理这个Pod列表。例如,StatefulSet就是使用Headless Service为客户端返回多个服务地址的。

对于"去中心化"类的应用集群,Headless Service将非常有用。下面以搭建Cassandra集群为例,看看如何通过对Headless Service的巧妙使用,自动实现应用集群的创建。

Apache Cassandra是一套开源分布式NoSQL数据库系统,主要特点为它不是单个数据库,而是由一组数据库节点共同构成的一个分布式的集群数据库。由于Cassandra使用的是"去中心化"模式,所以在集群里的一个节点启动之后,需要一个途径获知集群中新节点的加入。Cassandra使用了Seed(种子)来完成集群中节点之间的相互查找和通信。

通过对Headless Service的使用,实现了Cassandra各节点之间的相互查找和集群的自动搭建。主要步骤包括:自定义SeedProvider;通过Headless Service自动查找后端Pod;自动添加新Cassandra节点。

3.1、自定义SeedProvider

在本例中使用了一个自定义的SeedProvider类来完成新节点的查询和添加,类名为io.k8s.cassandra.KubernetesSeedProvider。

KubernetesSeedProvider.java类的源代码节选如下:

java 复制代码
......
    public List<InetAddress> getSeeds() {
      List<InetAddress> list = new ArrayList<InetAddress>();
      String host = "https://kubernetes.default.cluster.local";
      String serviceName = getEnvOrDefault("CASSANDRA_SERVICE", "cassandra");
      String podNamespace = getEnvOrDefault("POD_NAMESPACE", "default");
      String path = String.format("/api/v1/namespaces/%s/endpoints/", podNamespace);
......
    public static void main(String[] args) {
      SeedProvider provider = new KubernetesSeedProvider(new HashMap<String,
String>());
      System.out.println(provider.getSeeds());
    }
}

完整的源代码可以从https://github.com/kubernetes/kubernetes/blob/master/examples/storage/cassandra/java/src/main/java/io/k8s/cassandra/KubernetesSeedProvider.java获取。

定制的KubernetesSeedProvider类将使用REST API来访问Kubernetes Master,然后通过查询name=cassandra的服务(Headless Service将返回Pod列表)完成对其他"节点"的查找。

3.2、通过Service动态查找Pod

在KubernetesSeedProvider类中,通过查询环境变量CASSANDRA_SERVICE的值来获得服务的名称。这样就要求Service在Pod之前创建。如果我们已经创建好DNS服务(详见4.5节的说明)​,那么也可以直接使用服务的名称而无须使用环境变量。

回顾一下Service的概念。Service通常用作一个负载均衡器,供Kubernetes集群中其他应用(Pod)对属于该Service的一组Pod进行访问。由于Pod的创建和销毁都会实时更新Service的Endpoint数据,所以可以动态地对Service的后端Pod进行查询。Cassandra的"去中心化"设计使得Cassandra集群中的一个Cassandra实例(节点)只需要查询到其他节点,即可自动组成一个集群,正好可以使用Service的这个特性查询到新增的节点。下图描述了Cassandra新节点加入集群的过程。

在Kubernetes系统中,首先需要为Cassandra集群定义一个Service:

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: cassandra           # 服务名称
  labels:
    name: cassandra         # 标签,用于标识该 Service 归属的应用
spec:
  ports:
  - name: cql             # 端口名称(建议加上,方便后续多端口扩展或监控)
    port: 9042            # Service 自身对外暴露的端口(Cassandra 默认的 CQL 客户端连接端口)
    targetPort: 9042      # 后端 Pod 容器内部实际监听的端口
  selector:
    name: cassandra       # 标签选择器:精准匹配带有 `name: cassandra` 标签的 Pod

在Service的定义中指定Label Selector为name=cassandra。

使用kubectl create命令创建这个Service:

bash 复制代码
$ kubectl create -f cassandra-service.yaml
service "cassandra" created

然后,创建一个Cassandra Pod:

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cassandra           # Deployment 名称
  labels:
    name: cassandra
spec:
  replicas: 1               # 副本数量
  selector:
    matchLabels:
      name: cassandra       # 标签选择器(Deployment 要求必须使用 matchLabels)
  template:
    metadata:
      labels:
        name: cassandra
    spec:
      containers:
      - name: cassandra     # 容器名称
        image: gcr.io/google_containers/cassandra:v5 # 镜像地址
        command:
        - /run.sh           # 启动命令
        ports:
        - name: cql
          containerPort: 9042 # CQL 客户端连接端口
        - name: thrift
          containerPort: 9160 # Thrift 端口
        resources:
          limits:
            cpu: 0.5        # 资源限制:CPU 最大使用 0.5 核
        env:
        - name: MAX_HEAP_SIZE
          value: 512M       # JVM 最大堆内存
        - name: HEAP_NEWSIZE
          value: 100M       # JVM 新生代内存
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace # 通过 Downward API 获取当前 Pod 所在的命名空间
        volumeMounts:
        - name: data
          mountPath: /cassandra_data # 容器内数据挂载路径
      volumes:
      - name: data
        emptyDir: {}        # 使用临时空目录卷作为存储卷
bash 复制代码
$ kubectl create -f cassandra-rc.yaml
replicationcontroller "cassandra" created

现在,一个Cassandra Pod运行起来了,但还没有组成Cassandra集群。

3.3、Cassandra集群中新节点的自动添加

现在,我们使用Kubernetes提供的Scale(扩容和缩容)机制对Cassandra集群进行扩容:

bash 复制代码
$ kubectl scale rc cassandra --replicas=2
replicationcontroller "cassandra" scaled

查看Pod,可以看到RC创建并启动了一个新的Pod:

bash 复制代码
$ kubectl get pods -l="name=cassandra"
NAME                 READY    STATUS     RESTARTS   AGE
cassandra           1/1       Running   0            5m
cassandra-g52t3    1/1       Running   0            50s

使用Cassandra提供的nodetool工具对任一cassandra实例(Pod)进行访问来验证Cassandra集群的状态。下面的命令将访问名为cassandra的Pod(访问cassandra-g52t3也能获得相同的结果)​:

bash 复制代码
$ kubectl exec -ti cassandra -- nodetool status
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address     Load       Tokens  Owns (effective)  Host ID              Rack
UN  10.1.20.16 51.58 KB   256     100.0%            1625c65d-b5b6-40f4-a794-
6f5a12322d86  rack1
UN  10.1.10.11 51.51 KB   256     100.0%            cdfcbf1a-795c-4412-9d3f-
e8fe50bb8deb  rack1

可以看到在Cassandra集群中有两个节点处于正常运行状态(Up and Normal,UN)​。该结果中的两个IP地址为两个Cassandra Pod的IP地址。

内部的过程为:每个Cassandra节点(Pod)都通过API访问Kubernetes Master,查询名为cassandra的Service的Endpoint(即Cassandra节点)​,若发现有新节点加入,就进行添加操作,最后成功组成一个Cassandra集群。

我们再增加两个Cassandra实例:

bash 复制代码
$ kubectl scale rc cassandra --replicas=4

用nodetool工具查看Cassandra集群的状态:

bash 复制代码
$ kubectl exec -ti cassandra -- nodetool status
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address     Load       Tokens  Owns (effective)  Host ID               Rack
UN  10.1.20.16 51.58 KB   256     50.5%             1625c65d-b5b6-40f4-a794-
6f5a12322d86  rack1
UN  10.1.10.12 52.03 KB   256     47.0%             8bcc1c3e-44ec-46a7-b981-
4090b206f14e  rack1
UN  10.1.20.17 68.05 KB   256     50.6%             579b6493-e92a-47f5-91f2-
9313198a24c9  rack1
UN  10.1.10.11 51.51 KB   256     51.9%             cdfcbf1a-795c-4412-9d3f-
e8fe50bb8deb  rack1

可以看到4个Cassandra节点都加入Cassandra集群中了。

另外,可以通过查看Cassandra Pod的日志来看到新节点加入集群的记录:

bash 复制代码
$ kubectl logs cassandra-g52t3
......
INFO  18:05:36 Handshaking version with /10.1.20.17
INFO  18:05:36 Node /10.1.20.17 is now part of the cluster
INFO  18:05:36 InetAddress /10.1.20.17 is now UP
INFO  18:05:38 Handshaking version with /10.1.10.12
INFO  18:05:39 Node /10.1.10.12 is now part of the cluster
INFO  18:05:39 InetAddress /10.1.10.12 is now UP

本例描述了一种通过API查询Service来完成动态Pod发现的应用场景。对于类似于Cassandra的去中心化集群类应用,都可以使用Headless Service查询后端Endpoint这种巧妙的方法来实现对应用集群(属于同一Service)中新加入节点的查找。

相关推荐
Z21-X2 小时前
云原生运维学习笔记:容器技术的演进、底层原理与 Docker 全生命周期管理
运维·笔记·学习·云原生
java_logo10 小时前
Docker 部署 Liquibase:轻松搭建数据库变更管理
数据库·docker·容器·数据库管理·liquibase·轩辕镜像·数据库变更
阿里云云原生17 小时前
HiRoute 新版本发布:结合自己的真实工作选择最佳模型
云原生
IT利刃出鞘18 小时前
Docker Compose--解决修改配置后不生效的问题
运维·docker·容器
java_logo18 小时前
Docker 部署 CloudBeaver:轻松搭建网页数据库管理平台
数据库·docker·容器·数据库管理·dbeaver·轩辕镜像·cloudbeaver
好用斋19 小时前
拾光坞安装网盘聚合LitePan:把Docker命令翻译成图形界面
运维·docker·容器
对象存储与RustFS20 小时前
Velero 接 RustFS 实操:K8s 集群备份落到自建对象存储
后端·docker·云原生
weirong1 天前
图解 client-go Informer:手写一个 mini-Informer 才敢说懂
kubernetes
.冰块.1 天前
流量大了自动加 Pod!K8s HPA 扩缩容与 ResourceQuota/LimitRange 资源管控实战
linux·云原生·容器·kubernetes·hpa·quota·limitrange