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 的几种访问方式

相关推荐
诡异森林。2 小时前
Docker--Docker网络原理
网络·docker·容器
angushine4 小时前
让Docker端口映射受Firewall管理而非iptables
运维·docker·容器
孔令飞5 小时前
Go:终于有了处理未定义字段的实用方案
人工智能·云原生·go
玄明Hanko6 小时前
Quarkus+Docker最全面完整教程:手把手搞定Java云原生
后端·docker·云原生
SimonLiu0096 小时前
清理HiNas(海纳斯) Docker日志并限制日志大小
java·docker·容器
高峰君主9 小时前
Docker容器持久化
docker·容器·eureka
能来帮帮蒟蒻吗9 小时前
Docker安装(Ubuntu22版)
笔记·学习·spring cloud·docker·容器
言之。13 小时前
别学了,打会王者吧
java·python·mysql·容器·spark·php·html5
秦始皇爱找茬17 小时前
docker部署Jenkins工具
docker·容器·jenkins