本文介绍Kubernetes中的基本概念和术语 — RC、Deployment、HPA、StatefulSet、Service、Job。
1. RC
RC是K8s系统中的核心概念之一,简单来说,它其实定义了一个期望的场景。即声明某种Pod的副本数量在任意时刻都符合某个预期值。RC包括以下几个部分:
- Pod期待的副本数
- 用于筛选目标Pod的Label Selector。
- 当Pod的副本数量小于预期数量时,用于创建新的Pod模板(template)
一个完整的RC定义的例子:
1 | apiVersion: v1 |
在k8s 1.2中,RC升级为另外一个新的概念 — Replica Set (RS),RS也是用于保证与label selector匹配的pod数量维持在期望状态。两者的区别如下:
RC只支持基于等式的selector(env=dev或environment!=qa),但RS还支持新的基于集合的selector(version in (v1.0, v2.0)或env not in (dev, qa)),这对复杂的运维管理很方便。
升级方式
RS不能使用kubectl rolling-update进行升级。 kubectl rolling-update专用于rc。 RS升级使用deployment或者kubectl replace命令。
一个完整的RS定义的例子:
1 | apiVersion: extensions/v1beta1 |
RC相关命令:
1 | kubectl scale rc rc_name --replicas=3 # 修改RC的副本数量为3 |
需要注意的是,删除RC并不会影响通过该RC已创建好的Pod。为了删除所有Pod,可以设置replicas的数量为0,然后更新该RC。另外,kubectl提供了stop和delete命令来一次性删除RC和RC控制的全部Pod。
总结一下RC的特性和作用:
- 在大多数情况下,我们通过定义一个RC实现pod的创建过程及副本数量的自动控制。
- RC里包括完整的Pod定义模板。
- RC通过label selector机制实现对pod副本的自动控制。
- 通过改变RC的pod副本数量,可以实现pod的扩容或缩容。
- 通过改变RC中Pod模板的镜像版本,可以实现Pod的滚动升级功能。
2. Deployment
Deployment同样为k8s的一个核心内容,主要职责同样是为了保证pod的数量和健康,90%的功能与Replication Controller完全一样,可以看做新一代的Replication Controller。但是,它又具备了Replication Controller之外的新特性:
- Replication Controller全部功能:Deployment继承了上面描述的Replication Controller全部功能。
- 事件和状态查看:可以查看Deployment的升级详细进度和状态。
- 回滚:当升级pod镜像或者相关参数的时候发现问题,可以使用回滚操作回滚到上一个稳定的版本或者指定的版本。
- 版本记录: 每一次对Deployment的操作,都能保存下来,给予后续可能的回滚使用。
- 暂停和启动:对于每一次升级,都能够随时暂停和启动。
- 多种升级方案:Recreate:删除所有已存在的pod,重新创建新的; RollingUpdate:滚动升级,逐步替换的策略,同时滚动升级时,支持更多的附加参数,例如设置最大不可用pod数量,最小升级间隔时间等等。
一个完整的Deployment定义:
1 | apiVersion: extensions/v1beta1 |
几个重要参数的说明:
maxSurge与maxUnavailable
maxSurge: 1 表示滚动升级时会先启动1个pod
maxUnavailable: 1 表示滚动升级时允许的最大Unavailable的pod个数
由于replicas为3,则整个升级完成后,pod个数在2-4个之间terminationGracePeriodSeconds
k8s将会给应用发送SIGTERM信号,可以用来正确、优雅地关闭应用,默认为30秒。
如果需要更优雅地关闭,则可以使用k8s提供的pre-stop lifecycle hook 的配置声明,将会在发送SIGTERM之前执行。
livenessProbe与readinessProbe
livenessProbe是kubernetes认为该pod是存活的,不存在则需要kill掉,然后再新启动一个,以达到replicas指定的个数。
readinessProbe是kubernetes认为该pod是启动成功的,这里根据每个应用的特性,自己去判断,可以执行command,也可以进行httpGet。比如对于使用java web服务的应用来说,并不是简单地说tomcat启动成功就可以对外提供服务的,还需要等待spring容器初始化,数据库连接连接上等等。对于spring boot应用,默认的actuator带有/health接口,可以用来进行启动成功的判断。
其中readinessProbe.initialDelaySeconds可以设置为系统完全启动起来所需的最少时间,livenessProbe.initialDelaySeconds可以设置为系统完全启动起来所需的最大时间+若干秒。
Deployment相关命令
1 | kubectl create -f review-demo.yaml # 通过yaml文件创建Deployment |
说明
在新版本的Kubernetes中建议使用ReplicaSet来取代ReplicationController。ReplicaSet跟ReplicationController没有本质的不同,只是名字不一样,并且ReplicaSet支持集合式的selector。
虽然ReplicaSet可以独立使用,但一般还是建议使用 Deployment 来自动管理ReplicaSet,这样就无需担心跟其他机制的不兼容问题(比如ReplicaSet不支持rolling-update但Deployment支持)。
附
Pod的管理对象,除了RC和Deloyment之外,还包括ReplicaSet、DaemonSet、StatefulSet和Job等。
3. HPA(Horizontal Pod Autoscaler)
通过手动执行kubectl scale命令,可以通过RC实现pod扩容。但并不满足谷歌对k8s的定位模板-自动化。
HPA,pod横向自动扩容,实现原理是通过追踪分析RC控制的所有目标Pod的负载变化情况,来确定是否需要针对性地调整目标pod的副本数。
有两种方式作为pod负载的度量指标。
- CPU utilization percentage
- 应用程序自定义的度量指标,比如服务在每秒内的相应的请求数。
CPU utilization percentage是一个算术平均值,目标pod所有副本自身的CPU利用率的平均值。一个Pod自身的CPU利用率是该Pod当前CPU使用量除以它的Pod request的值。比如当我们定义一个Pod的pod request为0.4,而当前pod的cpu使用量为0.2,则使用率为50%。如此可以得出一个平均值,如果某一个时刻CPU utilization percentage超过80%,则表示当前副本数不够,需要进行扩容。
CPU utilization percentage计算过程使用到的Pod的CPU使用量通常是1分钟的平均值。
一个完整的HPA定义例子:
1 | apiVersion: autoscaling/v2beta1 |
根据如上定义,我们可知这个HPA控制的目标对象为一个名为tomcat-shopxx的Deployment里的Pod副本,当这些Pod副本的CPUUtilizationPercentage超过90%时,会触发自动动态扩容行为,在扩容时必须满足的一个约束条件是Pod的副本数为1~10。
关于HPA的apiVersion说明:
k8s 1.2 中HPA被升级为稳定版本:apiVersion: autoscaling/v1
但仍然保留了旧版本:apiVersion: extensions/v1beta1
k8s 1.6 版本以后,API版本为:apiVersion: autoscaling/v2alpha1
注意:
除了通过直接定义yaml文件并且调用kubectl create 的命令来创建一个HPA资源对象的方式外,还可以通过如下简单的命令直接创建等价的HPA对象:
1 | kubectl autoscale deployment tomcat-shopxx --cpu-percent=90 --min=1 --max =10 |
4. StatefulSet
在k8s系统中,Pod的管理对象RC、Deployment、DaemonSet和Job都是面向无状态的服务,它们所管理的Pod的IP、名字,启停顺序等都是随机的。而StatefulSet,顾名思义,有状态的集合,管理所有有状态的服务,比如MySQL、MongoDB集群等。
StatefulSet本质
StatefulSet本质上是Deployment的一种变体,在v1.9版本中已成为GA版本,它为了解决有状态服务的问题,它所管理的Pod拥有固定的Pod名称,启停顺序,在StatefulSet中,Pod名字称为网络标识(hostname),还必须要用到共享存储。
在Deployment中,与之对应的服务是service,而在StatefulSet中与之对应的headless service,headless service,即无头服务,与普通service的区别就是它没有Cluster IP,解析它的名称时将返回该Headless Service对应的全部Pod的Endpoint列表。
除此之外,StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为:
$(podname).(headless server name)
比如一个三个节点的Kafka的StatefulSet集群对应的Headless Service的名称为kafka,StatefulSet的名称为kafka,则StatefulSet中创建的3个Pod的DNS名称分别为:kafka-0.kafka,kafka-1.kafka,kafka-2.kafka。这些DNS名称可以直接在集群的配置文件中固定下来。
如下是一个完整的StatefulSet的定义:
1 | apiVersion: v1 |
通过该配置文件,可看出StatefulSet的三个组成部分:
- Headless Service:名为nginx,用来定义Pod网络标识( DNS domain)。
- StatefulSet:定义具体应用,名为Nginx,有三个Pod副本,并为每个Pod定义了一个域名。
- volumeClaimTemplates: 存储卷申请模板,创建PVC,指定pvc名称大小,将自动创建pvc,且pvc必须由存储类供应。
附录:PV和PVC
PersistentVolume(PV)是集群中由管理员配置的一段网络存储。 它是集群中的资源,就像节点是集群资源一样。 PV是容量插件,如Volumes,但其生命周期独立于使用PV的任何单个pod。 此API对象捕获存储实现的详细信息,包括NFS,iSCSI或特定于云提供程序的存储系统。
PersistentVolumeClaim(PVC)是由用户进行存储的请求。 它类似于pod。 Pod消耗节点资源,PVC消耗PV资源。Pod可以请求特定级别的资源(CPU和内存)。声明可以请求特定的大小和访问模式(例如,可以一次读/写或多次只读)。
两个问题
为什么需要 headless service 无头服务?
在用Deployment时,每一个Pod名称是没有顺序的,是随机字符串,因此是Pod名称是无序的,但是在statefulset中要求必须是有序 ,每一个pod不能被随意取代,pod重建后pod名称还是一样的。而pod IP是变化的,所以是以Pod名称来识别。pod名称是pod唯一性的标识符,必须持久稳定有效。这时候要用到无头服务,它可以给每个Pod一个唯一的名称 。
为什么需要volumeClaimTemplate?
对于有状态的副本集都会用到持久存储,对于分布式系统来讲,它的最大特点是数据是不一样的,所以各个节点不能使用同一存储卷,每个节点有自已的专用存储,但是如果在Deployment中的Pod template里定义的存储卷,是所有副本集共用一个存储卷,数据是相同的,因为是基于模板来的 ,而statefulset中每个Pod都要自已的专有存储卷,所以statefulset的存储卷就不能再用Pod模板来创建了,于是statefulSet使用volumeClaimTemplate,称为卷申请模板,它会为每个Pod生成不同的pvc,并绑定pv, 从而实现各pod有专用存储。这就是为什么要用volumeClaimTemplate的原因。
创建:
1 | kubectl create -f nginx.yaml |
看下这三个Pod创建过程:
1 | 第一个是创建web-0 |
根据volumeClaimTemplates自动创建的PVC
1 | kubectl get pvc |
如果集群中没有StorageClass的动态供应PVC的机制,也可以提前手动创建多个PV、PVC,手动创建的PVC名称必须符合之后创建的StatefulSet命名规则:(volumeClaimTemplates.name)-(pod_name)
Statefulset名称为web 三个Pod副本: web-0,web-1,web-2,volumeClaimTemplates名称为:www,那么自动创建出来的PVC名称为www-web[0-2],为每个Pod创建一个PVC。
规律总结
- 匹配Pod name(网络标识)的模式为:$(statefulset名称)-$(序号),比如上面的示例:web-0,web-1,web-2。
- StatefulSet为每个Pod副本创建了一个DNS域名,这个域名的格式为: $(podname).(headless server name),也就意味着服务间是通过Pod域名来通信而非Pod IP,因为当Pod所在Node发生故障时,Pod会被飘移到其它Node上,Pod IP会发生变化,但是Pod域名不会有变化。
- StatefulSet使用Headless服务来控制Pod的域名,这个域名的FQDN为:$(service name).$(namespace).svc.cluster.local,其中,“cluster.local”指的是集群的域名。
- 根据volumeClaimTemplates,为每个Pod创建一个pvc,pvc的命名规则匹配模式:(volumeClaimTemplates.name)-(pod_name),比如上面的volumeMounts.name=www, Pod name=web-[0-2],因此创建出来的PVC是www-web-0、www-web-1、www-web-2。
- 删除Pod不会删除其pvc,手动删除pvc将自动释放pv。
Statefulset的启停顺序:
- 有序部署:部署StatefulSet时,如果有多个Pod副本,它们会被顺序地创建(从0到N-1)并且,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态。
- 有序删除:当Pod被删除时,它们被终止的顺序是从N-1到0。
- 有序扩展:当对Pod执行扩展操作时,与部署一样,它前面的Pod必须都处于Running和Ready状态。
Statefulset Pod管理策略:
在v1.7以后,通过允许修改Pod排序策略,同时通过.spec.podManagementPolicy字段确保其身份的唯一性。
- OrderedReady:上述的启停顺序,默认设置。
- Parallel:告诉StatefulSet控制器并行启动或终止所有Pod,并且在启动或终止另一个Pod之前不等待前一个Pod变为Running and Ready或完全终止。
StatefulSet使用场景:
- 稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现。
- 稳定的网络标识符,即Pod重新调度后其PodName和HostName不变。
- 有序部署,有序扩展,基于init containers来实现。
- 有序收缩。
更新策略:
在Kubernetes 1.7及更高版本中,通过.spec.updateStrategy字段允许配置或禁用Pod、labels、source request/limits、annotations自动滚动更新功能。
- OnDelete:通过.spec.updateStrategy.type 字段设置为OnDelete,StatefulSet控制器不会自动更新StatefulSet中的Pod。用户必须手动删除Pod,以使控制器创建新的Pod。
- RollingUpdate:通过.spec.updateStrategy.type 字段设置为RollingUpdate,实现了Pod的自动滚动更新,如果.spec.updateStrategy未指定,则此为默认策略。
StatefulSet控制器将删除并重新创建StatefulSet中的每个Pod。它将以Pod终止(从最大序数到最小序数)的顺序进行,一次更新每个Pod。在更新下一个Pod之前,必须等待这个Pod Running and Ready。- Partitions:通过指定 .spec.updateStrategy.rollingUpdate.partition 来对 RollingUpdate 更新策略进行分区,如果指定了分区,则当 StatefulSet 的 .spec.template 更新时,具有大于或等于分区序数的所有 Pod 将被更新。
具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas,则其 .spec.template 的更新将不会传播到 Pod。在大多数情况下,不需要使用分区。
5. Service
Service也是k8s里面最核心的资源对象之一,k8s里面的每个service其实就是我们经常提起的微服务架构中的一个“微服务”。下图是Pod、RC和Service之间的关系:
从图中可以看到, Kubernetes 的 Service 定义了一个服务的访问入口地址,前端的应用 ( Pod ) 通过这个入口地址访问其背后的一组由 Pod 副本组成的集群实例, Service 与其后端 Pod 副本集群之间则是通过 Label Selector 来实现“无缝对接”的。而 RC 的作用实际上是保证 Service 的服务能力和服务质量始终处于预期的标准。
Service 一旦被创建, Kubernetes 就会自动为它分配个可用的 Cluster IP ,而且在 Service 的整个生命周期 内,它的 Cluster IP 不会发生改变。
如下是一个Service的定义示例:
1 | apiVersion: v1 |
上述定义了一个名为“tomcat-service”的Service,它的服务端口为8080,拥有”tier=frontend”这个Label的所有Pod实例都属于它,运行如下命令进行Service的创建:
1 | kubectl create -f tomcat-server.yml |
查看Service的Endpoint列表:
1 | kubectl get endpoints |
查看Service对应的Cluster IP及更多的信息:
1 | kubectl get svc tomcat-service -o yaml |
在spec.ports 的定义中, targetPort 属性用来确定提供该服务的容器所暴露( EXPOSE )的端 口号,即具体业务进程在容器内的 targetPort 上提供 TCP/IP 接入,而 port 属性则定义了 Service 的虚端口。前面我们定义 tomcat 服务时,没有指定 targetPort ,则默认 targetPort 与port相同。
Service的多端口问题
很多服务都存在多个端口的问题,通常一个端口提供业务服务,另外一个端口提供管理服 务, 比如 Mycat 、Codis 等常见中间件。 Kubernetes Service 支持多个 Endpoint ,在存在多个 Endpoint 的情况下,要求每个 Endpoint 定义一个名字来区分。 下面是 Tomcat 多端口的 Service 定义样例:
1 | apiVersion : v1 |
从上述定义可以看出,多端口时需要给每个端口都定义一个名称,为什么要这样做?原因和k8s的服务发现机制有关。
k8s的服务发现机制
最早时 Kubernetes 采用了 Linux 环境变量的方式解决这个问题,即每个 Service 生成一些对应的 Linux 环境变量 (ENV ),并在每个 Pod 的容器在启动时,自动注入这些环境变量。这样就可以通过代码访问系统环境变量的方 式得到所需的信息,实现服务调用。
后来 Kubernetes 通过 Add-On 增值包的方式引入了 DNS 系统,把服务名作为 DNS 域名,这样一来, 程序就可以直接使用服务名来建立通信连接了。目前 Kubernetes 上的大部分应用都己经采用了 DNS 这些新兴的服务发现机制。
外部系统访问Service的问题
Kubernetes的 “三种IP”:
Node IP: Node 节点的 IP 地址。
Pod IP: Pod IP 地址 。
Cluster IP: Service 的IP地址。
Node IP
Node IP 是Kubernetes 集群中每个节点的物理网卡的 IP 地址,这是个真实存在的物理网络,所有属于这个网络的服务器之间都能通过这个网络直接通信,不管它们中是否有部分节点不属于这个 Kubernetes 集群。这表明了 Kubernetes 集群之外的节点访问 Kubernetes 集群之内的某个节点或者 TCP/IP 务时,必须要通过 Node IP行通信。
Pod IP
Pod IP 是每个 Pod IP 地址,它是 Docker Engine 根据 docker0 网桥的地址段进行分配的,通常是一个虚拟的二层网络,前面我们说过, Kubernetes 要求位于不同 Node 上的Pod 能够彼此直接通信,所以 Kubernetes 里一个 Pod 里的容器访问另外一个 Pod 里的容器,就是通过 Pod IP所在的虚拟二层网络进行通信的,而真实的 TCP/IP 流量则是通过 Node IP所在的物理网卡流出的。
Cluster IP
它也是个虚拟的 IP ,但更像是一个 “伪造”的 IP 网络。
- Cluster 仅仅作用 Kubernetes Service 这个对象,并由 Kubernetes 管理和分配 IP 地址(来源于 Cluster 地址池)。
- Cluster 无法被 Ping ,因为没有一个“实体网络对象”来响应 。
- Cluster IP只能结合 Service Port 组成一个具体的通信端口,单独的 Cluster IP不具备 TCP/IP 通信的基础 ,并且它们属于 Kubernetes 集群这样一个封闭的空间, 集群之外的节点如果要访问这个通信端口 ,则需要做一些额外的工作 。
- 在Kubernetes 集群之内, Node IP网、 Pod IP 网与 Cluster IP网之间的通信采用的是 Kubernetes 自己设计的一种编程方式的特殊的路由规则,与我们所熟知的路由有很大的不同。
结合上述分析,可以有如下总结:
Service Cluster IP 属于 Kubernetes 集群内部的地址,无法在集群外部直接使用这个地址。
那么矛盾来了:实际上我们开发的业务系统中肯定多少有一部分服务是要提供给 Kubernetes 集群外部的应用或者用户来使用的,典型的例子就是Web 端的服务模块,比如上面 tomcat-service ,那么用户怎么访问它?
采用 NodePort 是解决上述问题的最直接、最有效、最常用的做法。具体做法如下,以 tomcat-service 为例,我们在 Service 的定义里做如下扩展即可:
1 | apiVersion: v1 |
nodePort:31002 这个属性表明我们手动指定 tomcat-service 的 NodePort 为 31002 ,否 则Kubernetes 会自动分配 个可用的端口 。接下来 ,我们在浏览器里访问 http://<nodePort IP> :31002/,就可以 看到Tomcat 的欢迎界面了。
NodePort
NodePort 实现方式是在 Kubernetes 集群里的每个 Node 上为需要外部访问的 Service 开启一个对应的 TCP 监听端口,外部系统只要用任意 Node IP 地址+具体的 NodePort 端口号 即可访问此服务,在任意一个Node 上运行 netstat 命令,我们就可以看到有 NodePort 端口被监听:
1 | netstat -tlp | grep 31002 |
但是NodePort 还没有完全解决外部访问 Service 的所有问题,比如负载均衡问题,假如我们的集群中有 10 个Node ,则此时最好有一个负载均衡器,外部的请求只需访问此负载均衡器的 IP 地址,由负载均衡器负责转发流量到后面某个 Node 的 NodePort 上。如下图所示:
图中的 Load balancer 组件独立于 Kubernetes 集群之外,通常是个硬件的负载均衡器, 或者是以软件方式实现的,例如 HAProxy 或者 Nginx 。对于每个 Service ,我们通常需要配置一个对应的 Load balancer 实例来转发流量到后端的 Node 上,这的确增加了工作量及出错的概率。 于是 Kubernetes 提供了自动化的解决方案,如果我们的集群运行在谷歌的 GCE 公有云上,那么 只要我们把 Service 的 type=NodePort 改为 type=LoadBalancer ,此时 Kubernetes 会自动创建一个对应的 Load balancer 实例并返回它的 IP 地址供外部客户端使用。
6. Job
Job负责批量处理短暂的一次性任务 (short lived one-off tasks),即仅执行一次的任务,它保证批处理任务的一个或多个Pod成功结束。Kubernetes支持以下几种Job:
- 非并行Job:通常创建一个Pod直至其成功结束
- 固定结束次数的Job:设置.spec.completions,创建多个Pod,直到.spec.completions个Pod成功结束
- 带有工作队列的并行Job:设置.spec.Parallelism但不设置.spec.completions,当所有Pod结束并且至少一个成功时,Job就认为是成功
根据.spec.completions和.spec.Parallelism的设置,可以将Job划分为以下几种pattern:
Job类型 | 使用示例 | 行为 | completions | Parallelism |
---|---|---|---|---|
一次性Job | 数据库迁移 | 创建一个Pod直至其成功结束 | 1 | 1 |
固定结束次数的Job | 处理工作队列的Pod | 依次创建一个Pod运行直至completions个成功结束 | 2+ | 1 |
固定结束次数的并行Job | 多个Pod同时处理工作队列 | 依次创建多个Pod运行直至completions个成功结束 | 2+ | 2+ |
并行Job | 多个Pod同时处理工作队列 | 创建一个或多个Pod直至有一个成功结束 | 1 | 2+ |
Job Controller
Job Controller负责根据Job Spec创建Pod,并持续监控Pod的状态,直至其成功结束。如果失败,则根据restartPolicy(只支持OnFailure和Never,不支持Always)决定是否创建新的Pod再次重试任务。
Job Spec格式
- spec.template格式同Pod
- RestartPolicy仅支持Never或OnFailure
- 单个Pod时,默认Pod成功运行后Job即结束
- .spec.completions标志Job结束需要成功运行的Pod个数,默认为1
- .spec.parallelism标志并行运行的Pod的个数,默认为1
- spec.activeDeadlineSeconds标志失败Pod的重试最大时间,超过这个时间不会继续重试
一个Job的定义、创建示例:
1 | apiVersion: batch/v1 |
1 | kubectl create -f ./job.yaml |
固定结束次数的Job示例
1 | apiVersion: batch/v1 |
Bare Pods
所谓Bare Pods是指直接用PodSpec来创建的Pod(即不在ReplicaSets或者ReplicationCtroller的管理之下的Pods)。这些Pod在Node重启后不会自动重启,但Job则会创建新的Pod继续任务。所以,推荐使用Job来替代Bare Pods,即便是应用只需要一个Pod。