K8s学习笔记——4.Service
半野

1.Service

K8s创建Pod生命周期一般比较短暂,其IP的也是动态的。我们一般会通过Deployment运行应用,一个服务同时会存在多个Pod实例,因此还会存在负载均衡的问题。

在传统的架构上,为了解决这个问题,我们一般会在后端的流量入口处加一个Nginx做反向代理或者是使用LVS,保证服务对外提供的IP的稳定的同时,又实现了负载均衡。在K8s的架构中,实现这个功能的是Service对象。

Service是K8s的一个网络资源,在创建一个Service对象后,K8s会给其分配一个唯一静态IP地址,然后Service再去对后端Pod集合进行管理。当客户端访问Service的时候,Service会根据负载均衡的策略,转发给具体的Pod。

2.EndPoints & Endpoint Slices

2.1.EndPoints

Service和Pod之间并不是直接关联的,而是通过EndPoints对象进行的连通。

Pod存在有四种类型:

  1. PodScheduled:Pod 是否已经被调度到某节点;
  2. ContainersReady:Pod 中所有容器是否都已就绪;
  3. Initialized:所有的 Init 容器 是否都已成功完成;
  4. Ready:Pod 是否可以为请求提供服务,并且应该被添加到对应服务的负载均衡池中。

当Pod状态进入到Ready阶段,并且Status为True的时候,EndPoints控制器就会将该Pod的IP地址和Port暴露给Service,允许外部流量的进入。如果Pod在Ready阶段,但是Status为False或者Unknown的时候,EndPoints控制器就会移除该Pod的IP地址和Port。

总的来说,只有处于Running状态,并且Ready状态是True(readinessProbe 健康检查通过)的 Pod 才有资格成为 EndPoint。

EndPoints为Service屏蔽了不可用的Pod,使得Service访问到的Pod均是可用的。一般我们不需要自己创建EndPoints资源,在创建Service的时候,K8s会自动 的创建。

2.2.Endpoint Slices

如果一个微服务很重要,访问量很高可能会有数千上万的Pod,那么当一个Pod发生了变更,则EndPoints整体都会发生一次变化,当EndPoints的结果下发给所有Node的时候,无疑是一个很消耗资源的行为。为此,K8s又提出了一个新的Api——Endpoint Slices。通过将EndPoints进行切片存储,当某个Pod发生变化的时候,只需要下发对应切片的变更,即可大大减少资源的消耗。

在K8s v1.19 中,该功能默认启用。

3.健康检查

提到Pod状态,就必须说下为什么最好给Pod配置健康检查。

K8s的健康检查探针会返回三种结果:

  • Success:容器通过了诊断。
  • Failure:容器未能通过诊断。
  • Unknown:诊断失败(不应采取任何措施,kubelet 将进行进一步检查)。

在K8s中,存在三种探针,用于进行应用的健康检查:

  1. livenessProbe:指示容器是否正在运行。如果 liveness 探测失败,kubelet 会杀死容器,然后会根据重启策略,判断是否进行重启。如果容器不提供活性探测,则默认状态为Success。
  2. readinessProbe:指示容器是否准备好响应请求。EndPoints控制器会根据此结果控制Pod是否向Service暴露。如果容器不提供就绪探测,则默认状态为Success。
  3. startupProbe:指示容器内的应用程序是否已启动。如果提供了startupProbe,则所有其他探针都将被禁用,直到其成功为止。如果启动探测失败,kubelet 会杀死容器,然后会根据重启策略,判断是否进行重启。如果容器不提供启动探测,则默认状态为Success.

如果不配置健康检查,那么在Ready阶段,默认的readinessProbe默认会返回Success,这样Status就会为True。事实上,程序在启动后,并不是立即就能接受请求的,需要许多初始化的东西,如果此时请求打到了该Pod,就会有不可用。

4.kube-proxy

Kube-proxy 是 K8s 工作节点上的一个网络代理组件,作用是使发往 Service 的流量(通过ClusterIP和端口)负载均衡到正确的后端Pod。Kube-proxy 会监听 Service、Endpoint (Endpointslices)、Node等资源的变化,保证 Service 的可用性。kube-proxy的工位示意图(来自K8s官网)如下:

image

5.Yaml模板

Service的创建与Pod、Deployment等资源不同,它是通过kubectl expose建立的。前面提到,Service是一种网络资源,expose就很容易理解了,就是对一个微服务的IP和端口资源进行暴露。

获取 Service Yaml 模板的命令如下,可以通过 Deployment Name 获取,也可以通过 Deployment 的 yaml 文件进行获取:

1
`# 直接通过 Deployment Name 获取模板 kubectl expose deploy deploy-name --port=80 --target-port=8080 --dry-run=client -o yaml # 通过 Deployment yaml 获取模板 kubectl expose -f deploy.yml --port=80 --target-port=8080 --dry-run=client -o yaml `

其中 port 为 Service 对外暴露的端口号,target-port 为 Pod 的的端口号,即:用 port 代理 Pod 的 target-port。

使用下面这个 Deployment,我们创建一个应用 hostname-app。hostname-app 的目的是用于获取 Pod 的 hostname。

1
`apiVersion: apps/v1 kind: Deployment metadata: labels: app: hostname-app-deploy name: hostname-app-deploy spec: replicas: 3 selector: matchLabels: app: hostname-app template: metadata: labels: app: hostname-app spec: containers: - name: hostname image: k8s.gcr.io/serve_hostname ports: - containerPort: 9376 protocol: TCP `

hostname-app 对应的 Service 的 yaml 模板大致如下:

1
`apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: hostname-app-deploy name: hostname-app-deploy spec: ports: - port: 80 protocol: TCP targetPort: 9376 selector: app: hostname-app status: loadBalancer: {} `

6.关键字段

Service 的定义非常简单,在“spec”里只有两个关键字段,selectorports

  1. spec.selector

selector 和 Deployment 里的作用是一样的,用来过滤出要对外暴露的 Pod,这里就是对应着 Deployment 中 spec.template 中的lebels 的标签。

  1. spec.ports

ports 就很好理解了,port、protocol、targetPort 三个字段分别表示Service对外暴露的外部端口、使用的协议和 Pod中的端口。

imageimage

7.操作Service

7.1.修改Yaml

首先对生成的yaml进行下修改,并写入到hostname-service.yml文件中:

1
`apiVersion: v1 kind: Service metadata: labels: app: hostname-app-svc name: hostname-app-svc spec: ports: - port: 80 protocol: TCP targetPort: 9376 selector: app: hostname-app `

7.2.C: 创建 Service

1
`kubectl apply -f hostname-service.yml `

7.3.R: 查看 Service

1
`$ kubectl get svc hostname-app-svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE hostname-app-svc ClusterIP 10.43.52.211 80/TCP 28s `

查看Service的详细信息:

1
`$ kubectl describe svc hostname-app-svc Name: hostname-app-svc Namespace: default Labels: app=hostname-app-svc Annotations: Selector: app=hostname-app Type: ClusterIP IP Family Policy: SingleStack IP Families: IPv4 IP: 10.43.52.211 IPs: 10.43.52.211 Port: 80/TCP TargetPort: 9376/TCP Endpoints: 10.42.0.118:9376,10.42.0.123:9376,10.42.0.129:9376 Session Affinity: None Events: `

查看Service管理的Pod:

1
`kubectl get pod -l app=hostname-app -o wide `

可以看到 Service 的 Endpoints 的 IP、端口是和 Pod 的 IP地址是对应的。

image

8.访问 Service

通过kubectl get svc我们可以看到Service分配的IP是10.43.52.211。我们在WSL中,通过curl访问这个IP的80端口,可以发现每次响应都不相同,Service对请求进行了负载均衡处理。对于 hostname-app ,我们存在3个副本,我们访问了4次,才实现了每个Pod至少访问一次的目的,可见 Service的负载均衡不是标准的轮询。

image

9.命名服务

通过IP调用服务固然可行,但是人类天生就不擅长记忆没有规律的数字,不然也不会有域名的出现了。为了解决这个问题,K8s通过DNS插件,为Service绑定了一个域名。

Service 对应的域名完整形式是“ServiceName.命名空间.svc.cluster.local”,但是大多数情况下,直接使用“ServiceName. 命名空间”甚至“ServiceName”等也可以访问,默认会使用当前对象所在的命名空间。

通过kubectl get ns命令,可以查看K8s的所有命名空间,kube开头的命名空间,是K8s内部使用的命名空间,例如apiserver、etcd等组件的Pod,就放在kube-system下。我们在创建资源的时候,如果没有指定命名空间,则会使用默认使用的命名空间default。

1
`$ kubectl get ns NAME STATUS AGE default Active 4d kube-system Active 4d kube-public Active 4d kube-node-lease Active 4d `

对于我们创建的hostname-app-svc Service,其对应的完整域名为hostname-app-svc.default.svc.cluster.local

创建的Service的Type默认是ClusterIP,即只能在集群内部访问的。为了测试Service的命名访问,我们在之前创建的nginx-app的Pod内,进行下测试(因为k8s.gcr.io/serve_hostname镜像内部是没有shell的)。

首先,需要进入Pod:

1
`kubectl exec -it ngx-app-deploy-76f6b8586b-6kmlj -- sh `

准确地说,是进入Pod的某一个容器,在不指定的情况下,默认是进入第一个容器。

然后直接通过域名(服务名)访问服务,可以看到,正确的获取到了服务端的响应:

1
`$ kubectl exec -it ngx-app-deploy-76f6b8586b-6kmlj -- sh / # curl hostname-app-svc.default.svc.cluster.local hostname-app-deploy-57f4cf499b-wr8cf / # curl hostname-app-svc hostname-app-deploy-57f4cf499b-wr8cf / # curl hostname-app-svc hostname-app-deploy-57f4cf499b-wr8cf / # curl hostname-app-svc hostname-app-deploy-57f4cf499b-qr4tk / # curl hostname-app-svc hostname-app-deploy-57f4cf499b-wb7rl / # `

image

因此,在K8s中,我们为一个微服务应用绑定一个Service,就能够直接通过这个微服务的服务名进行应用的访问,甚至于不再需要注册中心。

10.Service 对外暴露

上述创建的服务,我们只能在 K8s 通过IP:Port 访问到,这种 Service 为 ClusterIP 类型。Service 总共有四种类型:

  1. ClusterIP:只能在集群内部访问。
  2. NodePort:将Service直接对集群外暴露,会在宿主机Node上开个端口,将流量转发到 port。类似Docker的-p。
  3. ExternalName:将 Service 绑定到一个远程的域名(通过spec.externalName指定的),而不是绑定到Pod,类似DNS的CNAME机制,多数情况下,由云服务商提供。
  4. LoadBalancer:LoadBalancer也可以实现集群外部访问服务,它是在集群外部进行负载均衡,多适用于支持外部负载均衡器的云服务提供商。

创建一个 NodePort 类型的Service,需要在yaml文件下添加 spec.type=NodePort,并需要在主机映射的端口(否则K8s会随机映射一个,范围在 30000-32767)。

1
`... spec: ports: - port: 80 protocol: TCP targetPort: 9376 + nodePort: 30000 + type: NodePort `

在获取 Service yaml 模板的时候,也可以通过 --type=NodePort 获取 NodePort 类型的模板。

通过kubectl apply更新 hostname-app-svc 后,这时候再通过 kubectl get svc 命令,可以看到 Service 的 TYPE 变为了NodePort,PORT(S)中也多了个宿主机的 nodePort: 30000。

1
`# ClusterIP $ kubectl get svc hostname-app-svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE hostname-app-svc ClusterIP 10.43.52.211 80/TCP 28s # NodePort $ kubectl get svc hostname-app-svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE hostname-app-svc NodePort 10.43.52.211 80:30000/TCP 30h `

现在,通过127.0.0.1即可访问我们部署到应用:

image

由 Hexo 驱动 & 主题 Keep
总字数 105.7k 访客数 访问量