【k8s系列】Kubernetes Service 深度解析:从基础到实战

一、前言

在当今的云原生世界中,Kubernetes 已经成为容器编排和管理的事实标准。它提供了一种强大的方式来部署、扩展和管理容器化应用。然而,随着应用规模的扩大和复杂性的增加,如何有效地暴露和管理这些应用的网络服务成为了一个关键问题。Kubernetes Service 正是解决这一问题的利器。

Kubernetes Service 是一种抽象,它定义了一组逻辑 Pod 集合和访问它们的策略。通过 Service,开发者可以轻松地将应用暴露给集群内部或外部的用户,而无需关心 Pod 的具体位置和数量。这种抽象不仅简化了网络配置,还提供了负载均衡、服务发现和稳定网络标识等关键功能。

本文将分享一些笔者在这块的知识点学习过程,便于感兴趣的小伙伴可以快速理解Kubernetes组件的基础应用。

二、Kubernetes Service 简介

(1)Service 的基本概念和作用

Kubernetes Service 是一种抽象,它定义了一组逻辑 Pod 集合和访问它们的策略。Service 通过一个稳定的 IP 地址和端口,将流量路由到后端的 Pod 集合。这种抽象使得应用的网络配置变得简单和一致,无论 Pod 的具体位置和数量如何变化。Service 具有以下关键特征:

  • 唯一指定的名字:每个 Service 都有一个唯一的名字,例如 mysql-server。这个名字在集群内部可以被用作 DNS 名称,方便服务发现。

  • 虚拟IP和端口号:Service 被分配了一个虚拟 IP 地址(Cluster IP)和一个端口号。这个虚拟 IP 地址是稳定的,不会随着 Pod 的变化而改变。

  • 远程服务能力:Service 提供了某种远程服务能力,例如数据库服务、缓存服务或 Web 服务。

  • 映射到容器应用:Service 被映射到提供这种服务能力的一组容器应用(Pod)上。

Service 在 Kubernetes 中扮演着至关重要的角色,其主要作用包括:

  • 服务发现:Service 提供了一种机制,使得集群内的其他组件和服务可以发现和访问它。

  • 负载均衡:Service 可以将流量均匀地分发到后端的多个 Pod 上,从而实现负载均衡。

  • 稳定的网络标识:Service 提供了一个稳定的 IP 地址和 DNS 名称,即使后端的 Pod 发生变化,客户端也可以通过这个标识稳定地访问服务。

(2)Service与Pod的关系

【k8s系列】Kubernetes Service 深度解析:从基础到实战_第1张图片

在 Kubernetes 中,Service 定义了一个服务的访问入口地址,前端应用(Pod)通过这个入口地址访问背后的一组由 Pod 副本组成的集群。Service 与后端的 Pod 副本集群通过 Label Selector 实现“无缝对接”。而 其中Replication Controller(RC)的作用是确保 Service 的服务能力和服务质量达到预期标准。

通过将系统中的所有服务建模为 Kubernetes Service,我们的系统由多个提供不同业务能力且彼此独立的微服务单元组成。这些服务之间通过 TCP/IP 进行通信,从而拥有了强大的分布式能力、弹性扩展能力和容错能力。

每个 Pod 都会被分配一个单独的 IP 地址,并且每个 Pod 提供一个独立的 Endpoint(Pod IP + ContainerPort)供客户端访问。多个 Pod 副本组成一个集群来提供服务。

此外,Kubernetes 在每个节点上安装 kube-proxy。kube-proxy 进程实际上是一个智能的软件负载均衡器,负责将对 Service 的请求转发到后端的某个 Pod 实例上,并在内部实现服务的负载均衡和会话保持机制。

Kubernetes 在这块使用了一个非常巧妙的设计方法:每个 Service 被分配了一个全局唯一的虚拟 IP 地址,称为 Cluster IP。这样,每个服务就变成了具备唯一 IP 地址的“通信节点”,服务调用变成了最基础的 TCP 网络通信问题。

Pod 的 Endpoint 地址会随着 Pod 的销毁和重新创建而改变,因为新的 Pod 地址与之前的不同。而 Service 一旦被创建,Kubernetes 就会自动为它分配一个可用的 Cluster IP,并且在 Service 的整个生命周期内,它的 Cluster IP 不会发生改变。因此,只需将 Service 的名称与 Service 的 Cluster IP 地址做一个 DNS 域名映射即可解决问题。

(3)Service的定义

Kubernetes 中的 Service 是一个对象(与 Pod 或 ConfigMap 类似)。我们可以使用 Kubernetes API 创建、查看或修改 Service 定义。 通常我们会使用 kubectl 这类工具来替我们发起这些 API 调用。

例如,假定有一组 Pod,每个 Pod 都在侦听 TCP 端口 9376,并且它们还被打上 app.kubernetes.io/name=MyApp 标签。我们可以定义一个 Service 来发布该 TCP 侦听器。

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app.kubernetes.io/name: MyApp
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 9376

参数解析:

参数 说明
apiVersion 指定 API 版本,对于 Service 通常是 v1
kind 指定资源类型,这里是 Service
metadata 包含 Service 的元数据,如名称 name
spec 定义 Service 的详细规格。
selector 选择器,用于指定与 Service 关联的 Pod 的标签。
ports 定义 Service 的端口配置。
protocol 协议,通常是 TCP 或 UDP。
port Service 的端口号。
targetPort Pod 的端口号,流量将被转发到这个端口。
type Service 的类型,可以是 ClusterIP、NodePort、LoadBalancer 或 ExternalName。

因此上面的service表示系统将创建一个名为 "my-service" 的、 服务类型默认为 ClusterIP 的 Service。 该 Service 指向带有标签 app.kubernetes.io/name: MyApp 的所有 Pod 的 TCP 端口 9376。

Kubernetes 为该 Service 分配一个 IP 地址(称为 “集群 IP”),供虚拟 IP 地址机制使用。

需要说明的是:Service 能够将任意入站 port 映射到某个 targetPort。 默认情况下,出于方便考虑,targetPort 会被设置为与 port 字段相同的值。

除此以外,在Service中也能引用Pod中定义的端口名程:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app.kubernetes.io/name: proxy
spec:
  containers:
  - name: nginx
    image: nginx:stable
    ports:
      - containerPort: 80
        name: http-web-svc
​
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app.kubernetes.io/name: proxy
  ports:
  - name: name-of-service-port
    protocol: TCP
    port: 80
    targetPort: http-web-svc

即使在 Service 中混合使用配置名称相同的多个 Pod,各 Pod 通过不同的端口号支持相同的网络协议, 此机制也可以工作。这一机制为 Service 的部署和演化提供了较高的灵活性。 例如,我们可以在后端软件的新版本中更改 Pod 公开的端口号,但不会影响到客户端。

Service 的默认协议是 TCP; 我们还可以使用其他受支持的任何协议。

由于许多 Service 需要公开多个端口,所以 Kubernetes 为同一 Service 定义多个端口。 每个端口定义可以具有相同的 protocol,也可以具有不同协议。

(4)Service的类型

Kubernetes 提供了四种主要的 Service 类型:

  • ClusterIP

    ClusterIP 是默认的 Service 类型。它为 Service 分配一个集群内部的虚拟 IP 地址,使得集群内的其他组件和服务可以访问它。这种类型适用于集群内部的服务发现和通信。我们可以使用 Ingress或者 Gateway API向公共互联网公开服务。

    其他几种 Service 类型在 ClusterIP 类型的基础上进行构建。如果我们定义的 Service 将 .spec.clusterIP 设置为 "None",则 Kubernetes 不会为其分配 IP 地址。

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      selector:
        app: my-app
      ports:
        - protocol: TCP
          port: 80
          targetPort: 9376
      type: ClusterIP

    在创建 Service 的请求中,我们可以通过设置 spec.clusterIP 字段来指定自己的集群 IP 地址。我们所选择的 IP 地址必须是合法的 IPv4 或者 IPv6 地址,并且这个 IP 地址在 API 服务器上所配置的 service-cluster-ip-range CIDR 范围内。 如果我们尝试创建一个带有非法 clusterIP 地址值的 Service,API 服务器会返回 HTTP 状态码 422, 表示值不合法。

  • NodePort

    NodePort 类型是在每个节点上开放一个端口,通过这个端口将流量转发到 Service。这种类型适用于需要从集群外部访问服务的场景。 为了让 Service 可通过节点端口访问,Kubernetes 会为 Service 配置集群 IP 地址, 相当于我们请求了 type: ClusterIP 的 Service。

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      selector:
        app: my-app
      ports:
        - protocol: TCP
          port: 80
          targetPort: 9376
          nodePort: 30007
      type: NodePort

    如果我们将 type 字段设置为 NodePort,则 Kubernetes 控制平面将在 --service-node-port-range 标志所指定的范围内分配端口(默认值:30000-32767)。 每个节点将该端口(每个节点上的相同端口号)上的流量代理到我们的 Service。 我们的 Service 在其 .spec.ports[*].nodePort 字段中报告已分配的端口。

    使用 NodePort 可以让我们自由设置自己的负载均衡解决方案, 配置 Kubernetes 不完全支持的环境, 甚至直接公开一个或多个节点的 IP 地址。

    对于 NodePort 类型 Service,Kubernetes 额外分配一个端口(TCP、UDP 或 SCTP 以匹配 Service 的协议)。 集群中的每个节点都将自己配置为监听所分配的端口,并将流量转发到与该 Service 关联的某个就绪端点。 通过使用合适的协议(例如 TCP)和适当的端口(分配给该 Service)连接到任何一个节点, 我们就能够从集群外部访问 type: NodePort 服务。

  • LoadBalancer

    使用云平台的负载均衡器将流量分发到 Service。Kubernetes 不直接提供负载均衡组件; 我们必须提供一个,或者将我们的 Kubernetes 集群与某个云平台集成。这种类型适用于需要外部负载均衡器的场景。

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      selector:
        app.kubernetes.io/name: MyApp
      ports:
        - protocol: TCP
          port: 80
          targetPort: 9376
      clusterIP: 10.0.171.239
      type: LoadBalancer
    status:
      loadBalancer:
        ingress:
        - ip: 192.0.2.127

    来自外部负载均衡器的流量将被直接重定向到后端各个 Pod 上,云平台决定如何进行负载平衡。要实现 type: LoadBalancer 的服务,Kubernetes 通常首先进行与请求 type: NodePort 服务类似的更改。cloud-controller-manager 组件随后配置外部负载均衡器, 以将流量转发到所分配的节点端口。

  • ExternalName

    将服务映射到 externalName 字段的内容(例如,映射到主机名 api.test.com)。 该映射将集群的 DNS 服务器配置为返回具有该外部主机名值的 CNAME 记录。 集群不会为之创建任何类型代理。这种类型适用于需要将集群内部的服务映射到外部服务的场景。

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      type: ExternalName
      externalName: my.database.com

    服务 API 中的 type 字段被设计为层层递进的形式 - 每层都建立在前一层的基础上。 但是,这种层层递进的形式有一个例外。 我们可以在定义 LoadBalancer Service 时禁止负载均衡器分配 NodePort

    通过设置 Service 的 spec.allocateLoadBalancerNodePortsfalse,我们可以对 LoadBalancer 类型的 Service 禁用节点端口分配操作。 这仅适用于负载均衡器的实现能够直接将流量路由到 Pod 而不是使用节点端口的情况。 默认情况下,spec.allocateLoadBalancerNodePortstrue,LoadBalancer 类型的 Service 也会继续分配节点端口。如果某已有 Service 已被分配节点端口,如果将其属性 spec.allocateLoadBalancerNodePorts 设置为 false,这些节点端口不会被自动释放。 我们必须显式地在每个 Service 端口中删除 nodePorts 项以释放对应的端口。

(5)Service与kube-proxy

在 Kubernetes 中,kube-proxy 是一个关键的组件,它运行在每个节点上,负责维护节点上的网络规则,使得从集群内部或外部的流量能够正确地路由到 Service 及其后端的 Pod。

当一个 Service 被创建时,kube-proxy 会监听到这个事件,并根据 Service 的配置在节点上创建相应的网络规则。这些网络规则通常包括 iptables 规则或 IPVS 规则,用于将流量从 Service 的虚拟 IP 地址(Cluster IP)转发到后端的 Pod。比如当一个nginx的 Service 被创建时,kube-proxy 会在每个节点上创建相应的 iptables 规则,将发往service 的 Cluster IP 和端口 80 的流量转发到后端的 Pod。

kube-proxy 的工作原理

  1. 分布式代理:每个 Node 节点上都会运行一个 kube-proxy 服务进程。kube-proxy 通过查询和监听 API Server 中 Service 与 Endpoints 的变化,为每个 Service 都建立一个“服务代理对象”,并自动同步。

  2. 服务代理对象:服务代理对象是 kube-proxy 程序内部的一种架构,它包括一个用于监听此服务请求的 SocketServer。SocketServer 的端口是随机选择一个本地空闲端口。此外,kube-proxy 内部创建了一个负载均衡器 LoadBalancer。

  3. 负载均衡:对于每个 TCP 类型的 Kubernetes Service,kube-proxy 都会在本地 Node 节点上建立一个 SocketServer 来负责接收请求,然后均匀发送到后端某个 Pod 的端口上。这个过程默认采用 Round Robin (rr) 负载均衡算法。

  4. 动态更新:kube-proxy 通过持续监控 API Server 中 Service 与 Endpoints 的变化,针对发生变化的 Service 列表,kube-proxy 会逐个处理。如果没有设置集群 IP,则不做任何处理;否则,kube-proxy 会为该 Service 的所有端口定义列表分配服务代理对象,并为该 Service 创建相关的 iptables 规则,更新负载均衡组件中对应 Service 的转发地址列表。

  5. 会话保持:在某些情况下,kube-proxy 还可以实现会话保持(Session Affinity),即确保来自同一个客户端的请求总是被转发到同一个后端 Pod。这对于需要保持会话状态的应用非常有用。

    比如咱们希望 my-service 实现客户端 IP 会话保持,可以在 Service 的 YAML 定义中添加 sessionAffinity 字段:

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      selector:
        app: my-app
      ports:
        - protocol: TCP
          port: 80
          targetPort: 9376
      type: ClusterIP
      sessionAffinity: ClientIP

    这样,kube-proxy 会根据客户端的 IP 地址将请求转发到同一个后端 Pod。

kube-proxy在启动时和监听到Service或Endpoint的变化后,会在本机Iptables的NAT表中添加4条规则链。

  • KUBE-PORTABLS-CONTAINER: 从容器中通过Cluster IP和端口号访问service

  • KUBE-PORTALS-HOST: 从主机中通过Cluster IP和端口号访问service

  • KUBE-NODEPORT-CONTAINER: 从容器中通过NODE IP和端口号访问service

  • KUBE-NODEPORT-HOST: 从主机中通过Node IP和端口号访问service

三、Kubernetes Service的基础使用

手动创建一个Service的配置文件,并配置上外部访问:

root@master01:/opt/cri-docker-file# vi redis-service.yaml
root@master01:/opt/cri-docker-file# cat redis-service.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: redis-pod
  labels:
    app: redis
spec:
  containers:
    - name: redis
      image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/redis:7.0.14
      ports:
        - containerPort: 6379
          name: redis-pod
---        
apiVersion: v1
kind: Service
metadata:
  name: redis-service
  labels:
    app: redis
spec:
  selector:
    app: redis
  ports:
    - protocol: TCP
      port: 6379
      targetPort: redis-pod
      nodePort: 30079
  type: NodePort

这里为了方便redis服务的应用直接将pod部分也一起写在同一个yaml文件下了。然后可以查看一下pod的执行情况以及service的信息:

#创建pod和service
root@master01:/opt/cri-docker-file# kubectl apply -f redis-service.yaml 
pod/redis-pod created
service/redis-service created
#查看pod创建情况,容器正在创建
root@master01:/opt/cri-docker-file# kubectl get pods -n default
NAME        READY   STATUS              RESTARTS   AGE
redis-pod   0/1     ContainerCreating   0          13s
#查看所有service信息
root@master01:/opt/cri-docker-file# kubectl get services
NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
kubernetes      ClusterIP   10.1.0.1               443/TCP          15d
redis-service   NodePort    10.1.241.126           6379:30079/TCP   56s
#根据label名称查看其中存在的service
root@master01:/opt/cri-docker-file# kubectl get service -l app=redis
NAME            TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
redis-service   NodePort   10.1.241.126           6379:30079/TCP   3m7s
#查看指定service的具体信息
root@master01:/opt/cri-docker-file# kubectl describe svc redis-service
Name:                     redis-service
Namespace:                default
Labels:                   app=redis
Annotations:              
Selector:                 app=redis
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.1.241.126
IPs:                      10.1.241.126
Port:                       6379/TCP
TargetPort:               redis-pod/TCP
NodePort:                   30079/TCP
Endpoints:                
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   
#重新查看pod的运行情况,已经正常。
root@master01:/opt/cri-docker-file# kubectl get pods -n default
NAME        READY   STATUS    RESTARTS   AGE
redis-pod   1/1     Running   0          2m25s

此时就可以在内外部访问redis服务了:

#内部访问
root@master01:/opt/cri-docker-file# kubectl exec -it redis-pod -- /bin/bash
root@redis-pod:/data# redis-cli
127.0.0.1:6379> ping
PONG

外部访问连接也OK:

【k8s系列】Kubernetes Service 深度解析:从基础到实战_第2张图片

最后如果不需要使用该服务了,就可以进行删除操作:

root@master01:/opt/cri-docker-file# kubectl delete service redis-service
service "redis-service" deleted

如果涉及配置更新操作,基本与pod相似,需要修改yaml配置文件后重新应用。

四、总结

笔者看来,其实这块组要还是理解pod与service之间的关系比较重要,包括创建Kubernetes服务的过程,应用起来其实相对简单。Kubernetes Service 的核心价值一直都在于其简化了网络配置和管理,提供了强大的服务发现、负载均衡和故障恢复机制,使得开发者能够更加高效地构建和运维云原生应用。随着 Kubernetes 的不断发展和完善,相信Service 将继续在云原生领域发挥其重要作用,推动应用架构和运维模式的持续创新~

如有分析不对的地方欢迎指正~

你可能感兴趣的:(服务器与运维,kubernetes,容器,云原生)