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存在有四种类型:
PodScheduled:Pod 是否已经被调度到某节点;ContainersReady:Pod 中所有容器是否都已就绪;Initialized:所有的 Init 容器 是否都已成功完成;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中,存在三种探针,用于进行应用的健康检查:
- livenessProbe:指示容器是否正在运行。如果 liveness 探测失败,kubelet 会杀死容器,然后会根据重启策略,判断是否进行重启。如果容器不提供活性探测,则默认状态为Success。
- readinessProbe:指示容器是否准备好响应请求。EndPoints控制器会根据此结果控制Pod是否向Service暴露。如果容器不提供就绪探测,则默认状态为Success。
- 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官网)如下:

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”里只有两个关键字段,selector 和 ports。
- spec.selector
selector 和 Deployment 里的作用是一样的,用来过滤出要对外暴露的 Pod,这里就是对应着 Deployment 中 spec.template 中的lebels 的标签。
- spec.ports
ports 就很好理解了,port、protocol、targetPort 三个字段分别表示Service对外暴露的外部端口、使用的协议和 Pod中的端口。


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地址是对应的。

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

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 / # ` |

因此,在K8s中,我们为一个微服务应用绑定一个Service,就能够直接通过这个微服务的服务名进行应用的访问,甚至于不再需要注册中心。
10.Service 对外暴露
上述创建的服务,我们只能在 K8s 通过IP:Port 访问到,这种 Service 为 ClusterIP 类型。Service 总共有四种类型:
- ClusterIP:只能在集群内部访问。
- NodePort:将Service直接对集群外暴露,会在宿主机Node上开个端口,将流量转发到 port。类似Docker的-p。
- ExternalName:将 Service 绑定到一个远程的域名(通过spec.externalName指定的),而不是绑定到Pod,类似DNS的CNAME机制,多数情况下,由云服务商提供。
- 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即可访问我们部署到应用:
