K8S Service 原理、案例

一、理论介绍

1.1、3W 法则

1、是什么?

  • Service 是一种为一组功能相同的 pod 提供单一不变的接入点的资源。当 Service 存在时,它的IP地址和端口不会改变。客户端通过IP地址和端口号与 Service 建立连接,这些连接会被路由到提供该 Service 的任意一个pod上。通过这种方式,客户端不需要知道每个单独的pod的地址,这样这些pod就可以在集群中随时被创建或移除。

2、为什么需要?

  • Pod 的 IP 地址经常变化。
  • Pod 的 IP 在集群外无法访问。
  • Pod 实例之间的负载均衡。

3、局限性

  • Service 是一种四层代理
  • 所谓四层,是针对 OSI 七层网络模型来说的。四层对应的是TCP/UDP协议,也就常说的IP+端口。
  • 因此,所谓四层代理就是基于IP+端口的负载均衡;七层就是基于URL等应用层信息的负载均衡。

1.2、基础信息

bash 复制代码
kubectl explain svc
# svc 是 service 的缩写
  • apiVersion:当前资源使用的 api 版本,与 VERSION 一致。
  • kind:资源类型,跟 KIND 保持一致。
  • metadata:元数据。定义资源名称、标签、注解等。
  • spec:规范、规约。
  • status:最近观察到的 Service 状态。由系统填充。只读。

1.3、ServiceSpec 规约

bash 复制代码
kubectl explain svc.spec

|-------------------------------------------------|-------|
| allocateLoadBalancerNodePorts <boolean> | |
| clusterIP <string> | |
| clusterIPs <[]string> | |
| externalIPs <[]string> | |
| externalName <string> | |
| externalTrafficPolicy <string> | |
| healthCheckNodePort <integer> | |
| internalTrafficPolicy <string> | |
| ipFamilies <[]string> | |
| ipFamilyPolicy <string> | |
| loadBalancerClass <string> | |
| loadBalancerIP <string> | |
| loadBalancerSourceRanges <[]string> | |
| ports <[]ServicePort> | 端口 |
| publishNotReadyAddresses <boolean> | |
| selector <map[string]string> | 标签选择器 |
| sessionAffinity <string> | |
| sessionAffinityConfig <SessionAffinityConfig> | |
| trafficDistribution <string> | |
| type <string> | 类型 |

1.4、Service 类型

bash 复制代码
kubectl explain svc.spec.type

type 类型有四种:

  • ClusterIP:虚拟集群IP。 通过集群的内部 IP 暴露服务,选择该值时服务只能够在集群内部访问。默认类型。
  • NodePort:**节点端口。**通过每个节点上的 IP 和静态端口(NodePort)暴露服务。 NodePort 服务会路由到自动创建的 ClusterIP 服务。 通过请求 <节点 IP>:<节点端口>,你可以从集群的外部访问一个 NodePort 服务。
  • ExternalName:**外部命名空间。**通过返回 CNAME 和对应值,可以将服务映射到 externalName 字段的内容(例如,foo.bar.example.com)。 无需创建任何类型代理。
  • LoadBalancer:**负载均衡。**使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上。Kubernetes 不直接提供负载均衡组件; 你必须提供一个,或者将你的 Kubernetes 集群与某个云平台集成。

其中 ClusterIP 为默认方式,只能集群内部访问。NodePort、LoadBalancer 则是向外暴露服务的同时将流量路由到 ClusterIP服务。ExternalName 则是CNAME方式进行服务映射。

1.5、Service 端口

bash 复制代码
kubectl explain svc.spec.ports

|-----------------------------|-----------------------------------------------------------------------------------------------|
| appProtocol <string> | |
| name <string> | |
| nodePort <integer> | service 在节点映射的端口。 type 类型是 NodePort 或 LoadBalancer 时才指定。 通常是系统分配,也可以自己指定,范围在 30000-32767。 |
| port <integer> -required- | Service 将公开的端口。 |
| protocol <string> | 协议。协议类型有 SCTP, TCP, UDP。默认 TCP。 |
| targetPort <IntOrString> | pod 端口 |

二、镜像准备

2.1、镜像准备

bash 复制代码
docker pull mirrorgooglecontainers/serve_hostname:latest
docker pull alpine:latest
docker pull curlimages/curl

2.2、镜像导出

bash 复制代码
docker save -o serve_hostname.tar.gz mirrorgooglecontainers/serve_hostname:latest
docker save -o alpine.tar.gz alpine:latest
docker save -o curl.tar.gz curlimages/curl

2.3、镜像导入工作节点 containerd

bash 复制代码
# k8s31node1 执行
[root@k8s31node1 ~]# ctr -n=k8s.io images import serve_hostname.tar.gz
[root@k8s31node1 ~]# ctr -n=k8s.io images ls|grep serve_hostname
[root@k8s31node1 ~]# ctr -n=k8s.io images import alpine.tar.gz
[root@k8s31node1 ~]# ctr -n=k8s.io images ls|grep alpine
[root@k8s31node1 ~]# ctr -n=k8s.io images import curl.tar.gz
[root@k8s31node1 ~]# ctr -n=k8s.io images ls|grep curl

# k8s31node2 执行
[root@k8s31node2 ~]# ctr -n=k8s.io images import serve_hostname.tar.gz
[root@k8s31node2 ~]# ctr -n=k8s.io images ls|grep serve_hostname
[root@k8s31node2 ~]# ctr -n=k8s.io images import alpine.tar.gz
[root@k8s31node2 ~]# ctr -n=k8s.io images ls|grep alpine
[root@k8s31node2 ~]# ctr -n=k8s.io images import curl.tar.gz
[root@k8s31node2 ~]# ctr -n=k8s.io images ls|grep curl

2.4、环境准备

假设有如下三个节点的 K8S 集群:

k8s31master 是控制节点

k8s31node1、k8s31node2 是工作节点

容器运行时是 containerd

三、实践

3.1、创建 ClusterIP 类型 Service

假设有这么一个部署:

objectivec 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostnames
spec:
  selector:
    matchLabels:
      app: hostnames
  replicas: 3
  template:
    metadata:
      labels:
        app: hostnames
    spec:
      containers:
      - name: hostnames
        image: mirrorgooglecontainers/serve_hostname
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 9376
          protocol: TCP

mirrorgooglecontainers/serve_hostname 这个应用的作用,就是每次访问 9376 端口时,返回它自己的 hostname。

3.1.1、在不同的节点上访问 podIP:port

返回了各个 pod 自己的 hostname。

可以看到,在集群内的不同节点上, Pod IP 都能访问。

3.1.2、在不同的容器内访问 podIP:port

假设现在新起一个 pod:

bash 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: curl-tools
spec:
  containers:
  - name: curl-tools
    image: curlimages/curl:latest
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh", "-c", "while true; do echo 'Hello from curl-tools'; sleep 30; done"]
  • curlimages/curl 是一个 curl 调试工具。
  • command:容器启动后执行的命令,这里使用一个无限循环,每隔 30 秒输出一次 Hello from curl-tools。
  • 像 Alpine 镜像,或者基于 Alpine 制作的工具镜像,容器内没有运行服务,需要启动后运行一个无限循环,防止容器被 K8S 杀掉。

进入容器访问 hostnames 服务:

bash 复制代码
kubectl exec -it curl-tools -- curl 10.244.165.57:9376

可以看到,在集群内的容器之间,Pod IP 都能访问。

3.1.3、在集群外访问 podIP:port

我们再起一台虚拟机 docker1,IP 地址跟 K8S 集群在一个网段。

访问 hostnames 服务:

可以看到,即使 docker1 的 IP 地址跟 K8S 集群在一个网段,但 docker1 没有用类似 kubeadm join 加入过集群,Pod IP 是不能访问的。

3.1.4、误删一个 pod

bash 复制代码
kubectl delete pod hostnames-d9d7674f5-2djvf

可以看到,K8S 又帮我们重新拉起了一个新 pod:hostnames-d9d7674f5-n7mtn,以维持我们 Deployment 控制器希望的副本数 replicas: 3。但是这个新 pod 的 IP,跟原来旧 pod 的 IP 是不一样的。

倘若我们是调用这些 pod 服务的客户端,在 pod 扩缩容期间,维护这些 pod IP 的代价是非常大的。所以我们需要一个稳定的接入层,它的 IP 地址、端口不变,让它来代理后端的一组 pod,而我们程序只需要跟这个接入层打交道就可以。这个接入层,就是 Service。

3.1.5、新建一个 ClusterIP 类型 Service

bash 复制代码
apiVersion: v1
kind: Service
metadata:
  name: hostnames-svc
spec:
  type: ClusterIP
  selector:
    app: hostnames
  ports:
  - port: 80
    protocol: TCP
    targetPort: 9376
  • spec.selector:Service 通过标签选择器来查找 app=hostnames 标签的 Pod。
  • port: 80 表示该服务的可用端口。
  • targetPort: 9376 表示服务将连接转发的 Pod 端口。
  • port 跟 targetPort 配合起来表示 这个 Service 的 80 端口,代理的是 Pod 的 9376 端口。
  • 查看 service
bash 复制代码
kubectl get svc
  • 访问 service

连续三次不断地访问 Service 的 CLUSTER-IP 和 端口 80:

  • 依次返回了三个 Pod 的 hostname。
  • 请求 Service IP:port 跟直接访问 Pod IP:port 的结果一样,这说明 Service 可以把请求代理到它所关联的后端 Pod。
  • 这也印证了 Service 提供的是 Round Robin (轮询) 方式的负载均衡。
  • 查看 endpoints

K8S 创建 Service 的时候,如果 Service 带有 selector 选择器,则 K8S 会创建一个与 Service 同名的 Endpoints 对象。selector 选中的 Pod 的 IP 和 端口,都会记录在 Endpoints 中。当一个新的 Pod 被创建并且它的标签匹配了某个 Service 的选择器时,该 Pod 的 IP 和端口会被添加到对应的 Endpoints 对象中;同样地,当 Pod 被删除时,它也会从 Endpoints 中移除。Endpoints 通常由 Service 引用, 以定义可以将流量发送到哪些 Pod。

bash 复制代码
kubectl get ep hostnames-svc
# ep endpoints 缩写
kubectl get ep hostnames-svc -oyaml

需要注意的是,只有处于 Running 状态,且 readinessProbe 检查通过的 Pod,才会出现在 Service 的 Endpoints 列表里。并且,当某一个 Pod 出现问题时,Kubernetes 会自动把它从 Service 里摘除掉。

3.1.6、集群外访问 Service ClusterIP:port

在集群外的虚拟机 192.168.40.111 访问 10.103.168.44:80

可以看到,访问不到 Service。如果希望集群外的机器能访问,需要创建 NodePort 类型的 Service 或 LoadBalancer 类型的 Service。

3.1.7、通过 FQDN 访问服务

在 Kubernetes (K8S) 环境中,FQDN(Fully Qualified Domain Name,完全限定域名)是指集群内部服务的完整域名,它包含了服务名、命名空间以及可选的服务后缀,用于唯一标识集群中的一个服务。

当你在 Kubernetes 中创建一个服务时,Kubernetes 会为该服务分配一个 FQDN。这个 FQDN 的格式通常是:

java 复制代码
<service-name>.<namespace>.svc.cluster.local
  • service-name 是你给服务起的名字。
  • namespace 是服务所在的命名空间,默认是 default,除非你在创建服务时指定了不同的命名空间。
  • svc 是固定的,代表这是一个服务。
  • cluster.local 是集群的默认域,大多数情况下是这个值,但如果你的集群配置了不同的域名,则会有所不同。

例如,我们上面 hostnames-svc 服务,它的完整域名是:

java 复制代码
hostnames-svc.default.svc.cluster.local

我们在集群的其他 pod 中,就可以使用这个域名访问服务:

可以省略命名空间和 svc.cluster.local,因为已经帮我们解析了。

在节点上不能解析这个域名,Kubernetes 的 DNS 服务通常只服务于集群中的 Pod,而节点本身并不自动配置为使用它:

使用 FQDN 可以让 Kubernetes 集群中的其他服务通过 DNS 解析来找到并访问你的服务,而不需要知道服务的具体 IP 地址。这有助于实现服务发现和服务间的通信,并且提高了服务部署的灵活性和可移植性。在 Kubernetes 中,CoreDNS 通常被用来提供这种 DNS 服务发现的功能。

3.1.8、Service 原理

我们以 3.1.2 小节 curl-tools 容器(假设运行在 node2 上)访问 Service Cluster_IP : port 为例,来讲解 Service 的工作原理。

首先介绍一下几个组件:

  • kube - proxy:是 Kubernetes 集群中每个节点上运行的一个组件。它负责在节点上维护网络规则,实现了 Service 的代理和负载均衡功能,确保客户端可以通过 Service 的 IP 和端口访问到对应的后端 Pod。kube-proxy 会监视 API Server 中 Service 和 Endpoints 对象的变化。当有新的 Service 或 Endpoints 对象创建、更新或删除时,kube-proxy 会收到通知,并相应地更新节点上的网络规则(iptables)。kube-proxy 有 iptables 模式跟 ipvs 模式。
  • Service:是 Kubernetes 提供的一种抽象层,它定义了一组 Pod 的逻辑集合以及访问这些 Pod 的策略。Service 为 Pod 提供了一个稳定的 IP 地址(ClusterIP)和端口号,使得客户端可以通过这个稳定的地址来访问后端的 Pod,而不需要关心具体 Pod 的 IP 地址和生命周期。
  • Endpoints:是 Kubernetes 中的一个资源对象,它记录了 Service 对应的所有后端 Pod 的 IP 地址和端口信息。每当 Pod 的数量或状态发生变化时,Endpoints 对象会自动更新。

curl-tools 容器内访问 Service IP : port 流程:

  1. node2 上的 kube-proxy 监视 API Server 中 Service 和 Endpoints 对象的变化,更新节点上的网络规则(iptables)。
  2. 客户端 curl-tools 请求 Service IP : port,请求包目的地 Destination 初始设置为服务的IP和端口(10.103.168.44:80)。发送到网络之前,node2 的内核会根据配置在该节点上的 iptables 规则处理数据包。内核会检查数据包是否匹配任何这些 iptables 规则。其中有个规则规定如果有任何数据包的目的地IP等于10.103.168.44、目的地端口等于80,那么数据包的目的地IP和端口应该被替换为随机选中的 hostnames pod的IP和端口。
  3. 本例中的数据包满足规则,故而它的IP:端口被改变了。假设 pod hostnames-d9d7674f5-tjwzp 被轮询算法随机选中了,所以数据包的目的地IP变更为 10.244.165.56,端口改为9376(Service中定义的目标端口)。就好像是客户端 curl-tools 直接发送数据包给 hostnames-d9d7674f5-tjwzp 而不是通过 Service。

3.1.9、iptables Or IPVS

kube-proxy 通过 iptables 处理 Service 的过程,其实需要在宿主机上设置相当多的 iptables 规则。而且,kube-proxy 还需要在控制循环里不断地刷新这些规则来确保它们始终是正确的。

当宿主机上有大量 Pod 的时候,成百上千条 iptables 规则不断地被刷新,很明显会影响到整体性能。

IPVS 模式的工作原理,其实跟 iptables 模式类似。区别只在于当我们创建了前面的 Service 之后,kube-proxy 首先会在宿主机上创建一个虚拟网卡(叫作:kube-ipvs0),并为它分配 Service VIP 作为 IP 地址,如下所示:

而接下来,kube-proxy 就会通过 Linux 的 IPVS 模块,为这个 IP 地址设置三个 IPVS 虚拟主机,并设置这三个虚拟主机之间使用轮询模式 (rr) 来作为负载均衡策略。我们可以通过 ipvsadm 查看到这个设置,如下所示:

bash 复制代码
ipvsadm -ln
-l:这是 ipvsadm 命令的一个选项,代表 "list",即列出当前 IPVS 规则。使用该选项可以查看已经配置的虚拟服务器(Virtual Server)及其对应的真实服务器(Real Server)信息。
-n:同样是 ipvsadm 命令的选项,代表 "numeric",表示以数字形式显示地址和端口,而不是将 IP 地址解析为域名、端口号解析为服务名。使用这个选项可以避免 DNS 解析和服务名查找的过程,更直观地显示规则信息。

可以看到,这三个 IPVS 虚拟主机的 IP 地址和端口,对应的正是三个被代理的 Pod。rr 表示轮询 round robin。

这时候,任何发往 10.103.168.44:80 的请求,就都会被 IPVS 模块转发到某一个后端 Pod 上了。 而相比于 iptables,IPVS 在内核中的实现其实也是基于 Netfilter 的 NAT 模式,所以在转发这一层上,理论上 IPVS 并没有显著的性能提升。但是,IPVS 并不需要在宿主机上为每个 Pod 设置 iptables 规则,而是把对这些"规则"的处理放到了内核态,从而极大地降低了维护这些规则的代价。

不过需要注意的是,IPVS 模块只负责上述的负载均衡和代理功能。而一个完整的 Service 流程正常工作所需要的包过滤、SNAT 等操作,还是要靠 iptables 来实现。

3.1.10、Endpoints 与 readinessProbe 就绪探针

  • 编写服务 svc-hellok8s.yaml
bash 复制代码
apiVersion: v1
kind: Service
metadata:
  name: hellok8s-svc
spec:
  type: ClusterIP
  selector:
    app: hellok8s
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  • 执行并监控
bash 复制代码
kubectl apply -f svc-hellok8s.yaml
# -w 表示持续监控,注意这个时候不要关闭终端
kubectl get ep hellok8s-svc -w
  • 编写部署 deploy-hellok8s.yaml
bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
      version: "1.0"
  template:
    metadata:
      labels:
        app: hellok8s
        version: "1.0"
    spec:
      containers:
      - name: hellok8s
        image: hellok8s:1.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10

initialDelaySeconds: 表示容器启动后延迟多少秒,开始就绪探测。这里特意设置成 15 秒,为了观察就绪探针没有完成时, Endpoints 的列表里,会不会有 pod 的 IP。

  • 在另一个终端运行部署
bash 复制代码
kubectl apply -f deploy-hellok8s.yaml

可以看到,在 pod 还没就绪前,endpoints 列表里面是不会有 pod IP 的。

  • 删除部署
bash 复制代码
kubectl delete -f deploy-hellok8s.yaml

可以看到 pod 被删除,endpoints 列表也会移除 pod IP。

3.1.11、原理图

3.2、创建 NodePort类型 Service

假设我们现在有如下 三个 pod:

java 复制代码
kubectl get pod -l app=hostnames -owide

IP 分别是 10.244.9.57、10.244.9.59、10.244.165.3

3.2.1、编写服务

XML 复制代码
apiVersion: v1
kind: Service
metadata:
  name: hostnames-nodeport
spec:
  type: NodePort
  selector:
    app: hostnames
  ports:
  - port: 80
    protocol: TCP
    targetPort: 9376
    nodePort: 32000
  • spec.type:NodePort 通过每个节点上的 IP 和静态端口(NodePort)暴露服务。
  • spec.ports.nodePort:指定节点上暴露的端口 32000。

3.2.2、访问服务

在浏览器中、或者任何能访问到集群三个节点的机器上,访问 节点IP:32000

bash 复制代码
curl 192.168.40.10:32000
curl 192.168.40.20:32000
curl 192.168.40.30:32000

3.2.3、原理

每一个节点的防火墙规则里面,都有一条 节点IP:32000 的转发规则。转发到三个 pod上。

创建 NodePort 类型的 Service 会默认帮我们创建 Cluster_IP

它的数据转发方式,跟 3.1 节讲的是一样的,走 kube-ipvs0 虚拟网桥。

如果节点上有安装 docker,NodePort 不会走节点IP端口,会默认走 docker0 网桥,然后数据再通过 docker0 转发给 pod。

3.3、创建 ExternalName 类型 Service

3.3.1、场景分析

Service,是无法代理到不同名称空间下的 Pod 的。

假设我们默认名称空间下,有三个这样的 Pod:

bash 复制代码
kubectl get pod -l app=hostnames -A --show-labels

在名称空间 external-demo 下,有这样一个 Service:

XML 复制代码
apiVersion: v1
kind: Service
metadata:
  name: external-svc-a
  namespace: external-demo
spec:
  type: ClusterIP
  selector:
    app: hostnames
  ports:
  - port: 80
    protocol: TCP
    targetPort: 9376

Service 在 external-demo 名称空间下,它的标签选择器选择了上面的三个 Pod。

查看 Service 详情:

XML 复制代码
kubectl describe svc external-svc-a -n=external-demo

可以看到它并没有代理到任何 Pod。

此时,不管是在节点上直接访问 Service ClusterIP 10.100.211.127:80

还是在 external-demo 名称空间下的 Pod:

XML 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: curl-tools
  namespace: external-demo
spec:
  containers:
  - name: curl-tools
    image: curlimages/curl:latest
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh", "-c", "while true; do echo 'Hello from curl-tools'; sleep 30; done"]
bash 复制代码
kubectl exec -it -n=external-demo curl-tools -- /bin/sh

都访问不到任何 Pod。

那么,external-demo 名称空间下的 Pod 要如何通过 Service 访问 default 名称空间下的 Pod 呢?

3.3.2、ExternalName 类型 Service

在 external-demo 名称空间下新建一个 Service:

XML 复制代码
apiVersion: v1
kind: Service
metadata:
  name: external-svc-b
  namespace: external-demo
spec:
  type: ExternalName
  externalName: hostnames-svc.default.svc.cluster.local
  selector:
    app: hostnames
  ports:
  - port: 80
    protocol: TCP
  • type: ExternalName。
  • externalName:指定 default 名称空间下的完全限定服务名。

相当于给 hostnames-svc 服务创建了一个软连接。

  • targetPort:在这种情况下可以忽略。
bash 复制代码
kubectl get svc -n=external-demo

此时,在 external-demo 名称空间下的 curl-tools 就可以直接访问这个服务,请求会被代理到 default 名称空间下的 Pod:

四、参考资料

官网:虚拟 IP 和服务代理

Kubernetes教程(五)---Service 的几种访问方式

相关推荐
❀͜͡傀儡师1 小时前
Docker部署搜索引擎SearXNG
运维·docker·容器·searxng
泡沫冰@1 小时前
K8S集群管理(3)
云原生·容器·kubernetes
麦兜*2 小时前
MongoDB 常见错误解决方案:从连接失败到主从同步问题
java·数据库·spring boot·redis·mongodb·容器
LQ深蹲不写BUG3 小时前
微服务事务管理利器:Seata 核心原理与实践指南
微服务·云原生·架构
nathan05294 小时前
Kubernetes 实战练习指南
云原生·容器·kubernetes
無名之輩5 小时前
Nvidia Device Plugin入门二之envvar策略
kubernetes
云和数据.ChenGuang6 小时前
微服务技术栈
微服务·云原生·架构
syty20206 小时前
K8s是什么
容器·kubernetes·dubbo
江团1io08 小时前
微服务雪崩问题与系统性防御方案
微服务·云原生·架构
Evan Wang8 小时前
使用Terraform管理阿里云基础设施
阿里云·云原生·terraform