在前面,我讨论了两个API对象,Deployment和DaemonSet。它们都是在线服务,但使用不同的策略部署应用程序。Deployment可以创建任意数量的实例,而Daemon为每个节点只会创建一个实例。
这两个API对象可以部署各种形式的应用程序,在云原生时代,微服务无疑是应用程序中最主流的形式。
为了更好地支持微服务(microservices)和服务网格(service meshes)等应用程序架构,K8S专门定义了一个新的对象:Service,它是集群内的负载均衡机制,用于解决服务发现的关键问题。
为什么是K8S Service
有了Deployment和DaemonSets,我们在集群中发布应用程序的工作就容易多了。凭借k8强大的自动运维能力,我们可以将应用更新发布频率从以前的每月和每周提高到每天和每小时,将服务质量提升到更高的水平。
然而,当应用程序在版本快速迭代时,另一个问题也逐渐出现了,即服务发现。
K8s集群中的Pod的生命周期相对较短。虽然Deployment和DaemonSet可以保持Pod总体数量的稳定,但是在运行过程中不可避免会出现Pod被删除、重建的情况,这将导致Pod状态不是固定的(注:每个Pod重启后ip会发生改变)。
这种动态性(pod的Ip变化)对当前流行的微服务架构是非常致命的。试想一下,后台Pod的IP地址总是在变化,客户端该如何访问它呢?如果不处理这个问题,Pod的部署和DaemonSet管理是毫无价值的。
其实,这个问题并不难。对于这种不稳定的后端服务,业界已经有了解决方案,即负载平衡。典型的应用程序包括LVS、Nginx等。
它们在前端和后端之间增加了一个中间层,屏蔽后端变化,为前端提供稳定的服务。
但是LVS和Nginx毕竟不是云原生技术,所以K8s根据这个想法定义了一个新的API对象:Service。
所以你可以想象,Service的工作原理与LVS和Nginx类似。K8s将为它分配一个静态IP地址,然后它将自动管理和维护稍后动态更改的pod集。当客户端访问服务时,会按照一定的策略将流量转发到Pod中。
下图清楚地显示了Service在k8中的工作原理:
和你想的一样,Service在这里使用了iptables技术。每个节点上的kube-proxy组件自动维护iptables规则,这样客户端就不用再关心Pod的具体地址。
当Client只需要访问Service的固定IP地址,Service就会按照iptables规则转发。当维护多个pod时,这种管理就是典型的负载平衡架构。
然而,Service并不仅仅使用iptables来实现负载均衡。它有另一个性能更好的实现(注:因为iptables使用是一条一条的规则,当规则条数越多时,性能就呈线性下降),叫做ipvs。
Service 例子
让我们做一个快速的服务示例来帮助您更好地理解。在下面的YAML文件中,我定义了一个典型的K8s Service:
apiVersion: v1
kind: Service
metadata:
name: hostnames
spec:
selector:
app: hostnames
ports:
- name: default
protocol: TCP
port: 80
targetPort: 9376
在上面的例子中,我使用selector字段声明这个Service只代理带有app=hostname标签的Pods。此外,该服务的80端口正在代理Pod的9376端口(映射)。
应用程序的Pod YAML看起来就像这样:
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: k8s.gcr.io/serve_hostname
ports:
- containerPort: 9376
protocol: TCP
该应用程序每次访问端口9376时返回自己的主机名。选择器选择的Pods称为Serivce的端点,可以使用kubectl get ep命令查看它们,如下所示:
$ kubectl get endpoints hostnames
NAME ENDPOINTS
hostnames 10.244.0.5:9376,10.244.0.6:9376,10.244.0.7:9376
应该注意的是,只有处于运行状态并通过readinessProbe检查的Pods才会出现在服务的端点列表中。而且,当Pod出现问题时,K8s会自动将其从服务中移除。
此时,通过服务10.0.1.175的VIP地址,可以访问正在代理的Pod。
$ kubectl get svc hostnames
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hostnames ClusterIP 10.0.1.175 <none> 80/TCP 5s$ curl 10.0.1.175:80
hostnames-0uton$ curl 10.0.1.175:80
hostnames-yp2kp$ curl 10.0.1.175:80
hostnames-bvc05
此VIP地址由k8自动为Service分配的。和上面一样,通过连续三次访问服务的VIP地址和代理端口80,它依次为我们返回三个pod的主机名。这也证实了Service提供的负载平衡是轮询方法。对于这个方法,我们称之为:Service中的 ClusterIP模式。
使用Yaml来描述一个Service
了解了Service的基本工作原理后,让我们来看看如何为Service编写YAML描述文件。
和往常一样,我们仍然可以使用kubectl api-resources命令查看它的基本信息,我们可以知道它的缩写是svc, apiVersion是v1。
注意,这意味着,与Pod一样,它属于K8s的核心对象,不与业务应用程序相关联,这与Job和Deployment不同。让我们来看一个实际的例子。
假设我们想要为ngx-dep应用程序生成一个Service,命令如下所示:
$ kubectl expose deploy ngx-dep --port=80 --target-port=80 --dry-run=client -o yaml
结果如下:
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: ngx-dep
name: ngx-dep
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
status:
loadBalancer: {}
你会发现Service的定义非常简单,在spec中只有两个关键字段,selector和ports。
选择器的作用与在Deployment/DaemonSet中的作用相同,该作用是用于过滤掉要代理的这些Pod。因为我们指定了代理部署,所以K8S会自动为我们填写NGX-DEP标签,并且将选择此部署对象部署的所有POD。
从这里你也可以看到,虽然K8s的标签机制很简单,但它非常强大有效,很容易和部署的Pod关联起来。
端口很容易理解。其中的三个字段表示外部端口、内部端口和使用的协议。这里,内部和外部都使用80端口,协议是TCP。
当然,您也可以将端口更改为其他端口,例如8080,以便外部服务看到Service给出的端口,而不知道Pod暴露的真实端口。
下面的图片应该可以帮助您清楚地看到服务和Pod之间的关系:
当我们定义一个服务时,我们可以指定我们需要的服务类型。如果不指定,则默认为ClusterIP类型。
Service 类型如下:
ClusterIP: 通过集群内部IP公开服务。该服务只能在集群内访问,默认为集群内访问。
NodePort: 通过每个Node节点的IP和静态端口(NodePort)公开服务。NodePort服务被路由到自动创建的ClusterIP服务。通过请求NodeIp:NodePort,可以从集群外部访问NodePort服务。
LoadBalancer: 使用云提供商的负载均衡器向外网公开服务。外部负载均衡器可以路由到NodePort服务和ClusterIP服务,这些服务需要与特定的云供应商一起操作。
ExternalName: 通过返回CNAME及其值,服务可以映射到字段内容的ExternalName(例如,foo.bar.example.com)。
总结
在这篇文章中,我们讨论了K8s Service的基础知识,接下来我们将讨论K8s Service是如何工作的,以及iptables和ipvs之间的区别。