Kubernetes现已成为在私有云,公共云以及混合云环境中大规模部署容器化应用程序的事实标准。业内最大的几家公有云平台AWS,Google Cloud,Azure,IBM Cloud以及Oracle Cloud现已提供Kubernetes托管服务。
Kubernetes 这个单词来自于希腊语,含义是 舵手 或 领航员 。其词根是 governor 和 cybernetic。 K8s 是它的缩写,用 8 字替代了“ubernete”。
Kubernetes的主要功能(如容器分组,覆盖网络,4层路由,secret管理等)整合到容器平台DC/OS中。DC/OS还将Kubernetes作为一个和Marathon相似的容器调度器集成进来
Kubernetes整体上的架构,其应用程序的部署模型,服务发现和负载平衡,内部/外部路由分离,持久卷的使用,在节点上部署守护进程,部署有状态的分布式系统,运行后台作业,部署数据库,配置管理,凭据管理, 滚动更新,自动伸缩以及包管理。
Kubernetes架构
这个接近完美的集群管理器采取的基本设计决策之一是能够部署在虚拟机上运行的现有应用程序,而无需对应用程序代码做出任何修改。从整体上来说,任何在虚拟机上运行的应用程序都可以通过简单地容器化其组件的方式部署在Kubernetes上。这是通过其核心功能实现的:容器分组,容器编排,覆盖网络,基于4层虚拟IP路由系统的容器间路由,服务发现,对运行守护程序的支持,部署有状态的应用程序组件,而且最重要的是,它还能够通过扩展容器编排器以支持复杂的编排要求。
宏观的层面来讲,Kubernetes提供了一组动态可扩展的主机,用于承载运行工作负载的容器,并使用一组称为master的管理主机,提供用于管理整个容器基础设施的API。工作负载可能包括长期运行的服务,批处理作业和运行在特定容器主机上的守护程序。所有容器主机使用覆盖网络连接在一起以提供容器间的路由。部署在Kubernetes上的应用程序在集群网络是可以动态发现的,而且可以通过传统的负载均衡器暴露到外部网络。集群管理器的状态存放在高度分布式组织的k/v存储里,该存储运行在master实例上。
使用 Kubernetes,你可以快速、高效地满足用户以下的需求:
快速精准地部署应用程序
即时伸缩你的应用程序
无缝展现新特征
限制硬件用量仅为所需资源
Kubernetes的优势:
可移动: 公有云、私有云、混合云、多态云
可扩展: 模块化、插件化、可挂载、可组合
自修复: 自动部署、自动重启、自动复制、自动伸缩
为什么选择 Kubernetes,它的作用?
Kubernetes 能在实体机或虚拟机集群上调度和运行程序容器。而且,Kubernetes 也能让开发者斩断联系着实体机或虚拟机的“锁链”,从以主机为中心的架构跃至以容器为中心的架构。该架构最终提供给开发者诸多内在的优势和便利。Kubernetes 提供给基础架构以真正的以容器为中心的开发环境。
Kubernetes 优势
诸如:
协调辅助进程,协助应用程序整合,维护一对一“程序 – 镜像”模型。
挂载存储系统
分布式机密信息
检查程序状态
复制应用实例
使用横向荚式自动缩放
命名与发现
负载均衡
滚动更新
资源监控
访问并读取日志
程序调试
提供验证与授权
以上兼具平台即服务(PaaS)的简化和基础架构即服务(IaaS)的灵活,并促进了在平台服务提供商之间的迁移。
Kubernetes缺点
Kubernetes 不是传统的、全包容的平台即服务(Paas)系统。它尊重用户的选择,这很重要。
并不限制支持的程序类型。它并不检测程序的框架 (例如,Wildfly),也不限制运行时支持的语言集合 (比如, Java、Python、Ruby),也不仅仅迎合 12 因子应用程序,也不区分 应用 与 服务 。Kubernetes 旨在支持尽可能多种类的工作负载,包括无状态的、有状态的和处理数据的工作负载。如果某程序在容器内运行良好,它在 Kubernetes 上只可能运行地更好。
不提供中间件(例如消息总线)、数据处理框架(例如 Spark)、数据库(例如 mysql),也不把集群存储系统(例如 Ceph)作为内置服务。但是以上程序都可以在 Kubernetes 上运行。
没有“点击即部署”这类的服务市场存在。
不部署源代码,也不编译程序。持续集成 (CI) 工作流程是不同的用户和项目拥有其各自不同的需求和表现的地方。所以,Kubernetes 支持分层 CI 工作流程,却并不监听每层的工作状态。
允许用户自行选择日志、监控、预警系统。( Kubernetes 提供一些集成工具以保证这一概念得到执行)
不提供也不管理一套完整的应用程序配置语言/系统(例如 jsonnet)。
不提供也不配合任何完整的机器配置、维护、管理、自我修复系统。
另一方面,大量的 PaaS 系统运行在 Kubernetes 上,诸如 Openshift、Deis,以及 Eldarion。你也可以开发你的自定义PaaS,整合上你自选的CI系统,或者只在 Kubernetes 上部署容器镜像。
因为 Kubernetes 运营在应用程序层面而不是在硬件层面,它提供了一些 PaaS 所通常提供的常见的适用功能,比如部署、伸缩、负载平衡、日志和监控。然而,Kubernetes 并非铁板一块,这些默认的解决方案是可供选择,可自行增加或删除的。
Kubernetes scheduler将始终确保每个应用程序组件都是经过健康检查的,提供高可用的服务,当副本数量设置为多个时,每个实例在多个主机上进行调度,并且如果其中一个主机变为不可用时,所有在该台主机上运行的容器都会被调度到剩余的其他主机。Kubernetes提供的一项迷人功能是两级自动扩缩。首先,通过使用一个叫做Horizontal Pod Autoscaler(Pod自动水平扩缩器)的资源,它可以为用户提供容器的自动扩缩功能,该资源将会监视资源的消耗并相应地扩展所需的容器数量。其次,它可以根据资源需求添加或删除主机来扩展容器集群本身。 此外,随着集群联盟(cluster federation)功能的引入,它甚至可以使用单个API端点管理一组Kubernetes集群,这些集群甚至可能跨越多个数据中心。
应用部署模型
上图展示的是Kubernetes宏观层面的应用程序部署模型。它使用一个叫做ReplicaSet的资源来编排容器。将ReplicaSet视为基于YAML或基于JSON的一个元数据文件,该文件里定义了容器镜像,端口,副本数,激活运行状况检查(health check),存活状况检查(liveness health check),环境变量,挂载卷以及创建和管理容器所需的一些安全规则。在Kubernetes上,容器总是以所谓Pods的形式成组创建,它是Kubernetes的一个元数据定义或者说是一个资源。每个pod允许在使用了Linux namespace,cgroup和其他内核功能的容器之间共享文件系统,网络接口和操作系统用户。ReplicaSet可以由另外一个名为Deployments的高级资源管理,它被设计用于提供滚动更新和处理回滚等功能。
通过执行下面这样一条简单的CLI命令,用户便可以通过定义一个deployment,在Kubernetes上部署一个容器化的应用程序:
kubectl run <application-name> --image=<container-image> --port=<port-no>
执行上述CLI命令后,它将使用给定的容器镜像创建一个Deployment声明,一个副本集以及一个采用了指定容器镜像的Pod;添加一个应用名的选择器标签。根据当前的设计,由此创建的每个pod将会拥有两个容器,一个用于指定的应用程序组件,另外一个即所谓的pause容器,用于连接网络接口。
服务发现 & 负载均衡
Kubernetes的一个关键特性便是由SkyDNS和4层虚拟IP路由系统提供的服务发现和内部路由模型。这些功能可以为面向service的应用请求提供内部路由。可以使用集群网络里的service对通过副本集创建的一组pod提供负载平衡。service使用选择器标签连接到pod。每个service都将会分配一个唯一的IP地址,一个由它的名称派生出的主机名,并且将会以round robin的方式路由pods的请求。这些service甚至可以为可能需要支持会话亲和性的应用程序提供基于IP哈希的路由机制。service可以指定一组端口,而针对指定的service定义的一系列属性将会以相同的形式同样应用到所有端口上。因此,如果只是某个给定端口需要支持会话亲和性,而所有的其他端口只需要round robin的方式路由的情况下,可能需要用到多个service。
service内部工作原理
Kubernetes service背后是通过一个叫做kube-proxy的组件实现。kube-proxy实例运行在每个节点上,并提供了三种代理模式:userspace,iptables和IPVS。目前的默认值是iptables。
在第一种代理模式下,userspace,kube-proxy本身将充当代理服务器的角色,并且将被一条iptable规则接受的请求代理到后端pod。在这种模式下,kube-proxy将在用户空间中运行,并且将会在消息流上额外增加一跳(hop)。在iptables中,kube-proxy将创建一个iptable规则集合,用于将来自客户端的入口请求在网络层面直接转发到后端pod的端口上,而不会在中间额外增加一跳。这种代理模式比第一种模式快得多,因为它是在内核空间中操作而不是在中间增加一台额外的代理服务器。
第三种代理模式是在Kubernetes v1.8中添加的,它和第二种代理模式非常相似,该模式使用的是基于IPVS的虚拟服务器来路由请求,而无需用到iptable规则。IPVS是一个传输层的负载均衡功能,可以在基于Netfilter的Linux内核中使用,并且提供了一组负载均衡算法。在iptables上使用IPVS的主要原因是在使用iptables时同步代理规则带来的性能开销。当创建数千个service时,更新iptable规则需要相当长的时间,相比之下,使用IPVS只需几毫秒。此外,IPVS使用哈希表线性的扫描iptables来查找代理规则。有关介绍IPVS代理模式的更多信息,请参阅华为在KubeCon 2017上的“Scaling Kubernetes to Support 50,000 Services”[1]演示文稿。
内外路由分离
Kubernetes service可以通过两种主流方式暴露给外部网络。第一种是使用节点端口(node port),通过暴露节点上的动态端口,这些端口会将流量转发到service端口。第二种是使用一个ingress controller配置的负载均衡器,它将会连接到同一个覆盖网络并将请求代理给service。ingress controller是一个后台进程,它可以跑在容器里,该容器将会监听Kubernetes API,根据指定的一组ingress动态配置并重新加载给定的负载均衡器。一个ingress定义了一组基于service的主机名及上下文路径的路由规则。
一旦通过执行kubectl run命令,在Kubernetes上跑起来应用后,我们可以通过一个负载均衡器将其暴露给外部网络,如下所示:
kubectl expose deployment <application-name> --type=LoadBalancer --name=<service-name>
上述命令将创建一个负载均衡器类型的service,并且使用在POD创建之初建立的选择器标签映射到相同的一组pod。这样一来,根据Kubernetes集群不同的配置方式,一个负载均衡器类型的service将会在底层基础设施上创建出来,通过service或直接路由的形式将请求转发到指定的一组pod。
持久卷用法
需要在文件系统上持久化存储数据的应用程序可以使用卷将存储设备挂载到临时容器,这和虚拟机的使用方式类似。Kubernetes巧妙地重新设计了这一概念,通过引入一个所谓的持久卷声明(PVC)的中间资源,在物理存储设备和容器之间做了解耦。一个PVC定义了磁盘大小,磁盘类型(ReadWriteOnce,ReadOnlyMany,ReadWriteMany),并将存储设备动态链接到在pod中定义了的卷。该绑定过程可以使用PV这样一个静态的形式,也可以使用一个持久化存储的provider动态实现。在这两种方法里,卷将一对一地链接到PV,并且取决于其配置,给定的数据即便pod被终止也将会被保留。根据使用的磁盘类型,多个pod将能够连接到同一磁盘并进行读/写。
支持ReadWriteOnce的磁盘将只能连接到单个pod,并且无法同时在多个pod之间共享。但是,支持ReadOnlyMany的磁盘将能够在只读模式下同时在多个pod之间共享。相反地,顾名思义,支持ReadWriteMany的磁盘可以连接到多个pod,以便在读写模式下共享数据。 Kubernetes提供了一系列的卷插件,用于支持公有云平台上可用的存储服务,例如AWS EBS,GCE Peristent Disk,Azure File,Azure Disk和许多其他众所周知的存储系统,如NFS,Glusterfs,Cinder等。
在节点上部署守护程序
Kubernetes提供了一个名为DaemonSets的资源,用于在每个Kubernetes节点上以守护进程的形式运行pod的副本。DaemonSets的一些用例如下:
需要部署到每个节点上提供持久化存储的集群存储守护程序(如glusterd,ceph)。
需要在每个节点上运行的监控容器宿主机的节点监控守护程序,例如Prometheus Node Exporter。
需要在每个节点上运行的,用作采集容器及Kubernetes组件日志的日志采集守护程序,例如fluentd或是logstash。
需要在一组节点上运行的提供外部路由的ingress controll pod。
部署有状态的分布式系统
容器化应用程序最艰巨的任务之一莫过于设计有状态分布式组件部署架构的流程。无状态组件可以很容易地进行容器化,因为它们可能没有预定的启动顺序,集群要求,点对点的TCP连接,唯一的网络标识符,优雅的启动和终止需求等。像数据库,大数据分析系统,分布式k/v存储以及消息代理这样的系统,可能拥有需要支持上述这些功能的复杂分布式架构。Kubernetes引入了StatefulSets资源来解决这些复杂的需求。
从整体上来说,StatefulSets类似于ReplicaSet,除了提供处理pod的启动顺序的能力,唯一地标识每个pod以保留其状态之外,它还同时提供以下特性:
稳定,唯一的网络标识符。
稳定,持久化的存储。
有序,优雅的部署和扩容。
有序,优雅的删除和终止。
有序,自动地滚动更新。
这里面的“稳定”指的是在跨pod重新调度时它将会保留网络标识符和持久化存储。如上图所示,唯一的网络标识符可以通过使用headless service提供。Kubernetes提供了一些StatefulSets的示例,包括以分布式的形式部署Cassandra以及Zookeeper。
执行后台任务
除了ReplicaSet和StatefulSets之外,Kubernetes还提供了两个额外的控制器,用于在后台运行称为Jobs和CronJobs的工作负载。Jobs和CronJobs之间的区别在于Jobs执行一次即终止,而CronJobs会按照与标准Linux cron作业类似的给定时间间隔定期地执行。
部署数据库
由于存在对集群,点对点连接,复制同步,灰度,备份管理等需求,在容器平台上部署数据库用于生产环境将会比部署应用程序稍微困难一些。正如之前所提到的那样,Statefulsets专门为解决此类复杂需求而设计,而如今已经有一些在Kubernetes上运行PostgreSQL和MongoDB集群的方案。YouTube的数据库集群系统Vitess现如今已经是一个CNCF项目,对于在Kubernetes上大规模灰度运行MySQL是一个很好的选择。说是如此,我们最好注意一下,这些方案目前仍然处于非常早期的开发阶段,而如果现有的生产级别数据库系统,仍然可用于给定的基础架构,例如AWS上的RDS,GCP上的Cloud SQL,或是内部按需部署的数据库集群,考虑到安装的复杂性以及维护成本,选择这其中的一种方案可能更合适些。
配置管理
容器通常使用环境变量来参数化它们的运行时配置。但是,常见的企业应用程序往往使用大量的配置文件为一个指定的部署提供所需的静态配置。Kubernetes则提供了一个绝妙的办法,使用名为ConfigMaps的一种简单资源来管理此类配置文件,而无需将它们打包到容器镜像里。可以通过执行以下CLI命令,使用目录,文件或文本值创建ConfigMaps:
kubectl create configmap <map-name> <data-source>
# map-name: name of the config map
# data-source: directory, file or literal value
创建ConfigMap后,可以通过卷的形式将其挂载到pod。通过这种松耦合的架构,一个已经在运行的系统的配置可以通过更新相关的ConfigMaps的方式无缝更新,而其滚动更新的执行流程这块我将在下一节中详细说明。值得一提的是,ConfigMaps现在不支持嵌套的目录结构;因此,如果应用程序的嵌套目录结构中存放有配置文件的话,则需要为每一个目录层级创建一个ConfigMap。
凭证管理
与ConfigMaps类似,Kubernetes提供了另一种名为Secrets的宝贵资源,用于管理密码,OAuth令牌和ssh密钥等敏感信息。否则,在已运行的系统上更新该信息可能需要重建容器镜像。
可以使用以下方式创建用于管理基本身份验证凭据的密钥:
# write credentials to two files $ echo -n 'admin' > ./username.txt $ echo -n '1f2d1e2e67df' > ./password.txt # create a secret $ kubectl create secret generic app-credentials --from-file=./username.txt --from-file=./password.txt
创建secret后,pod可以使用环境变量或挂载卷的方式来读取它。类似地,可以使用相同的方法将任何其他类型的敏感信息注入到pod中。
滚动更新
上面这个动画描述了如何使用蓝/绿部署的方法为已经运行的应用程序发布应用程序更新,而无需任何宕机成本。这是Kubernetes提供的另一个重磅功能,它允许应用程序不费吹灰之力即可无缝地发布安全更新和向后兼容的变更。如果变更不向后兼容,则可能需要使用单独的部署定义手动执行蓝/绿部署。
这一方案允许通过一条简单的CLI命令,发起一个部署以更新容器镜像:
$ kubectl set image deployment/<application-name> <container-name>=<container-image-name>:<new-version>
一旦发起部署,可以通过如下方式检查部署进度的状态:
$ kubectl rollout status deployment/<application-name>Waiting for rollout to finish: 2 out of 3 new replicas have been updated...deployment "<application-name>" successfully rolled out
使用相同的CLI命令kubectl set image deployment,可以让部署更新回滚到之前的状态。
自动扩缩
Kubernetes允许使用ReplicaSet或Deployments手动调整pod数量。这可以通过执行如下CLI命令来实现:
kubectl scale --replicas=<desired-instance-count> deployment/<application-name>
如上图所示,可以通过向Deployment添加另一个名为Horizontal Pod Autoscaler(HPA)的资源来扩展此功能,以便根据实际资源使用情况动态扩缩容器。HPA将通过资源指标的API监视每个pod的资源使用情况,并通知Deployment相应地更改ReplicaSet的副本数。Kubernetes使用高档延迟(upscale delay)和缩减延迟(downscale delay)来避免某些情况下频繁的资源使用波动而可能导致的颠簸。目前,HPA仅支持基于CPU的使用情况进行扩展。如果有必要的话,还可以通过Custom Metrics API加入自定义指标,这具体视应用程序的自然属性而定。
包管理
Kubernetes社区发起了一个单独的项目,为Kubernetes实现了一个称为Helm的包管理器。它允许用户使用一个名为Chart的资源模板化并打包Kubernetes资源,比如Deployment,Service,ConfigMap,Ingress等,并允许在安装时使用输入参数配置它们。更重要的是,它允许在使用依赖项实现包的安装时复用现有图表。Helm存储库可以托管在公有云或私有云环境中,用于管理应用程序的Chart。Helm提供了一个CLI,用于从给定的Helm存储库里安装应用程序到指定的Kubernetes环境中。
一些众所周知的软件应用程序的各种稳定Helm图表可以在它的Github存储库[2]中找到,也可以在中心化的Helm服务器中找到:Kubeapps Hub[3]。
下图为开源自动化运维体系
链:
1、cobbler实现自动装机
2、saltstack实现工程自动化配置
3、kubernetes实现容器自动化编排
4、zabbix实现自动化监控
5、elastic实现应用日志自动化收集
6、jenkins实现开发持续化交付
一、Kubernetes
2014年出现的kubernetes(又叫k8s)更加炙手可热。为啥kubernetes又叫做k8s?据说是因为kubernetes这个单词太长,不好记,而首字母和尾字母中间有8个字母,所以就简写成了k8s。Kubernetes翻译成中文,意思是“舵手”。
K8s简单说,是用来实现容器集群管理的系统,用于自动部署、扩展和管理容器。它是由Google公司开发,其原型为Google内部容器管理系统Borg。
Borg经过十多年地优化、改进,其功能和效率不言而喻,k8s项目的目的就是把Borg最精华的部分提取出来,使现在的开发者能够更简单、直接地应用。K8s自诞生之日起就注定了它的不平凡之路。
几个概念:
node 一个物理机器,或一个虚拟机(KVM类型,而不是容器类型);将虚拟机作为node一般是历史原因,或是为了彻底隔绝杜绝安全问题。
cluster 一组node需要被集中管理,统一叫一个cluster。一个cluster有一个master和多个node。每个node里面有一个kubelet用来服从master调度并管理node本身。
app containers 一个node里面可以有一个或多个容器化的应用程序,即app container。可以简单认为就是docker容器。
pod 多个app containers之间可能需要共享硬盘,或共享同一个ip,这样一组app containers合起来叫一个pod。典型应用如:一个容器不停产生日志到本地硬盘,另一个容器不停读本地硬盘并上传日志到日志服务器。
service 当一个node挂了时,上面的pod及pod里面的container也自然都挂了。为了死不掉,需要有个pod上层的抽象,pod挂了,service还在。service通过如下几种方式暴露出来。
ClusterIP (default),cluser的内网ip,只能此cluster内可见
NodePort,端口NAT到cluster外面
LoadBalancer,在cluster外面搞个LB并分配个外面可见的固定IP给LB
ExternalName,类似CNAME方式
deployment 配置yaml格式,存在master上,当机器故障或需要横向scale时或需要更新binary时,master根据配置搞定一切。
二、安装
entos 7.2版本,我们将master和node安装在一台机器上测试,如不同机器上,注意IP配置即可。
ip:192.168.1.101
master安装:
yum -y install kubernetes-master etcd
node安装:
yum -y install kubernetes-node etcd flannel docker
配置:
more /etc/etcd/etcd.conf
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.1.101:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.101:2379,http://127.0.0.1:2379"
ETCD_MAX_SNAPSHOTS="5"
ETCD_NAME="etcd1"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.101:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.101:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://192.168.1.101:2380"
more /etc/kubernetes/apiserver
KUBE_API_ADDRESS="--address=0.0.0.0"
KUBE_API_PORT="--port=8080"
KUBELET_PORT="--kubelet-port=10250"
KUBE_ETCD_SERVERS="--etcd-servers=http://127.0.0.1:2379"
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=192.168.0.0/16"
KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,ResourceQuota"
KUBE_API_ARGS=""
more /etc/kubernetes/config
KUBE_LOGTOSTDERR="--logtostderr=true"
KUBE_LOG_LEVEL="--v=0"
KUBE_ALLOW_PRIV="--allow-privileged=false"
KUBE_MASTER="--master=http://192.168.1.101:8080"
三、启动
master服务启动:
systemctl start etcd.service
systemctl start kube-apiserver
systemctl start kube-controller-manager
systemctl start kube-scheduler
systemctl enable etcd.service
systemctl enable kube-apiserver
systemctl enable kube-controller-manager
systemctl enable kube-scheduler
node服务启动:
systemctl start kubelet
systemctl start kube-proxy
systemctl enable kubelet
systemctl enable kube-proxy
四、创建服务测试
我们使用yaml配置一个nginx的服务进行测试。
docker 下载两个镜像
docker pull registry.access.redhat.com/rhel7/pod-infrastructure:latest
docker pull nginx
more nginx.yaml
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
sessionAffinity: ClientIP
selector:
app: nginx
ports:
- port: 80
nodePort: 30080
创建一个服务
kubectl create -f nginx.yaml
打开web输入http://192.168.1.101:30080
Kubernetes 资源管理概述
Kubernetes 从创建之初的核心模块之一就是资源调度。想要在生产环境使用好 Kubernetes,必须对它的资源模型以及资源管理非常了解。
Kubernetes 资源简介
什么是资源?
在 Kubernetes 中,有两个基础但是非常重要的概念:Node 和 Pod。Node 翻译成节点,是对集群资源的抽象;Pod 是对容器的封装,是应用运行的实体。Node 提供资源,而 Pod 使用资源,这里的资源分为计算(CPU、Memory、GPU)、存储(Disk、SSD)、网络(Network Bandwidth、IP、Ports)。这些资源提供了应用运行的基础,正确理解这些资源以及集群调度如何使用这些资源,对于大规模的 Kubernetes 集群来说至关重要,不仅能保证应用的稳定性,也可以提高资源的利用率。
CPU 可分配的是使用时间,也就是操作系统管理的时间片,每个进程在一定的时间片里运行自己的任务(另外一种方式是绑核,也就是把 CPU 完全分配给某个 Pod 使用,但这种方式不够灵活会造成严重的资源浪费,Kubernetes 中并没有提供);而对于内存,系统提供的是内存大小。
CPU 的使用时间是可压缩的,换句话说它本身无状态,申请资源很快,也能快速正常回收;而内存大小是不可压缩的,因为它是有状态的(内存里面保存的数据),申请资源很慢(需要计算和分配内存块的空间),并且回收可能失败(被占用的内存一般不可回收)。
把资源分成可压缩和不可压缩,是因为在资源不足的时候,它们的表现很不一样。对于不可压缩资源,如果资源不足,也就无法继续申请资源(内存用完就是用完了),并且会导致 Pod 的运行产生无法预测的错误(应用申请内存失败会导致一系列问题);而对于可压缩资源,比如 CPU 时间片,即使 Pod 使用的 CPU 资源很多,CPU 使用也可以按照权重分配给所有 Pod 使用,虽然每个人使用的时间片减少,但不会影响程序的逻辑。
在 Kubernetes 集群管理中,有一个非常核心的功能:就是为 Pod 选择一个主机运行。调度必须满足一定的条件,其中最基本的是主机上要有足够的资源给 Pod 使用。
资源除了和调度相关之外,还和很多事情紧密相连
Kubernetes 资源的表示
用户在 Pod 中可以配置要使用的资源总量,Kubernetes 根据配置的资源数进行调度和运行。目前主要可以配置的资源是 CPU 和 Memory,对应的配置字段是 spec.containers[].resource.limits/request.cpu/memory
。
需要注意的是,用户是对每个容器配置 Request 值,所有容器的资源请求之和就是 Pod 的资源请求总量,而我们一般会说 Pod 的资源请求和 Limits。
Limits
和 Requests
的区别我们下面会提到,这里先说说比较容易理解的 CPU 和 Memory。
CPU
一般用核数来标识,一核 CPU 相对于物理服务器的一个超线程核,也就是操作系统 /proc/cpuinfo
中列出来的核数。因为对资源进行了池化和虚拟化,因此 Kubernetes 允许配置非整数个的核数,比如 0.5
是合法的,它标识应用可以使用半个 CPU 核的计算量。CPU 的请求有两种方式,一种是刚提到的 0.5
,1
这种直接用数字标识 CPU 核心数;另外一种表示是 500m
,它等价于 0.5
,也就是说 1 Core = 1000m
。
内存比较容易理解,是通过字节大小指定的。如果直接一个数字,后面没有任何单位,表示这么多字节的内存;数字后面还可以跟着单位, 支持的单位有 E
、P
、T
、G
、M
、K
,前者分别是后者的 1000
倍大小的关系,此外还支持 Ei
、Pi
、Ti
、Gi
、Mi
、Ki
,其对应的倍数关系是 2^10 = 1024
。比如要使用 100M 内存的话,直接写成 100Mi
即可。
节点可用资源
理想情况下,我们希望节点上所有的资源都可以分配给 Pod 使用,但实际上节点上除了运行 Pods 之外,还会运行其他的很多进程:系统相关的进程(比如 SSHD、Udev等),以及 Kubernetes 集群的组件(Kubelet、Docker等)。我们在分配资源的时候,需要给这些进程预留一些资源,剩下的才能给 Pod 使用。预留的资源可以通过下面的参数控制:
--kube-reserved=[cpu=100m][,][memory=100Mi][,][ephemeral-storage=1Gi]
:控制预留给 Kubernetes 集群组件的 CPU、Memory 和存储资源。--system-reserved=[cpu=100mi][,][memory=100Mi][,][ephemeral-storage=1Gi]
:预留给系统的 CPU、Memory 和存储资源。
这两块预留之后的资源才是 Pod 真正能使用的,不过考虑到 Eviction 机制(下面的章节会提到),Kubelet 会保证节点上的资源使用率不会真正到 100%,因此 Pod 的实际可使用资源会稍微再少一点。主机上的资源逻辑分配图如下所示:
NOTE:需要注意的是,Allocatable 不是指当前机器上可以分配的资源,而是指能分配给 Pod 使用的资源总量,一旦 Kubelet 启动这个值是不会变化的。
Allocatable 的值可以在 Node 对象的 status
字段中读取,比如下面这样:
status:
allocatable:
cpu: "2"
ephemeral-storage: "35730597829"
hugepages-2Mi: "0"
memory: 3779348Ki
Pods: "110"
capacity:
cpu: "2"
ephemeral-storage: 38770180Ki
hugepages-2Mi: "0"
memory: 3881748Ki
Pods: "110"
Kubernetes 资源对象
介绍 Kubernetes 中提供的让我们管理 Pod 资源的原生对象。
请求(Requests)和上限(Limits)
前面说过用户在创建 Pod 的时候,可以指定每个容器的 Requests 和 Limits 两个字段,下面是一个实例:
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
Requests
是容器请求要使用的资源,Kubernetes 会保证 Pod 能使用到这么多的资源。请求的资源是调度的依据,只有当节点上的可用资源大于 Pod 请求的各种资源时,调度器才会把 Pod 调度到该节点上(如果 CPU 资源足够,内存资源不足,调度器也不会选择该节点)。
注意,调度器只关心节点上可分配的资源,以及节点上所有 Pods 请求的资源,而不关心节点资源的实际使用情况,换句话说,如果节点上的 Pods 申请的资源已经把节点上的资源用满,即使它们的使用率非常低,比如说 CPU 和内存使用率都低于 10%,调度器也不会继续调度 Pod 上去。
Limits
是 Pod 能使用的资源上限,是实际配置到内核 cgroups 里面的配置数据。对于内存来说,会直接转换成 docker run
命令行的 --memory
大小,最终会配置到 cgroups 对应任务的 /sys/fs/cgroup/memory/……/memory.limit_in_bytes
文件中。
NOTE:如果 Limit 没有配置,则表明没有资源的上限,只要节点上有对应的资源,Pod 就可以使用。
使用 Requests 和 Limits 概念,我们能分配更多的 Pod,提升整体的资源使用率。但是这个体系有个非常重要的问题需要考虑,那就是怎么去准确地评估 Pod 的资源 Requests?如果评估地过低,会导致应用不稳定;如果过高,则会导致使用率降低。这个问题需要开发者和系统管理员共同讨论和定义。
Limit Range(默认资源配置)
为每个 Pod 都手动配置这些参数是挺麻烦的事情,Kubernetes 提供了 LimitRange
资源,可以让我们配置某个 Namespace 默认的 Request 和 Limit 值,比如下面的实例:
apiVersion: "v1" kind: "LimitRange" metadata:
name: you-shall-have-limits
spec:
limits:
- type: "Container"
max:
cpu: "2"
memory: "1Gi"
min:
cpu: "100m"
memory: "4Mi"
default:
cpu: "500m"
memory: "200Mi"
defaultRequest:
cpu: "200m"
memory: "100Mi"
如果对应 Namespace 创建的 Pod 没有写资源的 Requests 和 Limits 字段,那么它会自动拥有下面的配置信息:
内存请求是 100Mi,上限是 200Mi
CPU 请求是 200m,上限是 500m
当然,如果 Pod 自己配置了对应的参数,Kubernetes 会使用 Pod 中的配置。使用 LimitRange 能够让 Namespace 中的 Pod 资源规范化,便于统一的资源管理。
资源配额(Resource Quota)
资源管理和调度可以认为 Kubernetes 把这个集群的资源整合起来,组成一个资源池,每个应用(Pod)会自动从整个池中分配资源来使用。默认情况下只要集群还有可用的资源,应用就能使用,并没有限制。Kubernetes 本身考虑到了多用户和多租户的场景,提出了 Namespace 的概念来对集群做一个简单的隔离。
基于 Namespace,Kubernetes 还能够对资源进行隔离和限制,这就是 Resource Quota 的概念,翻译成资源配额,它限制了某个 Namespace 可以使用的资源总额度。这里的资源包括 CPU、Memory 的总量,也包括 Kubernetes 自身对象(比如 Pod、Services 等)的数量。通过 Resource Quota,Kubernetes 可以防止某个 Namespace 下的用户不加限制地使用超过期望的资源,比如说不对资源进行评估就大量申请 16核 CPU 32G 内存的 Pod。
下面是一个资源配额的实例,它限制了 Namespace 只能使用 20 核 CPU 和 1G 内存,并且能创建 10 个 Pod、20 个 RC、5 个 Service,可能适用于某个测试场景。
apiVersion: v1 kind: ResourceQuota metadata:
name: quota
spec:
hard:
cpu: "20"
memory: 1Gi
Pods: "10"
replicationcontrollers: "20"
resourcequotas: "1"
services: "5"
Resource Quota 能够配置的选项还很多,比如 GPU、存储、Configmaps、PersistentVolumeClaims 等等,更多信息可以参考官方文档。
Resource Quota 要解决的问题和使用都相对独立和简单,但是它也有一个限制:那就是它不能根据集群资源动态伸缩。一旦配置之后,Resource Quota 就不会改变,即使集群增加了节点,整体资源增多也没有用。Kubernetes 现在没有解决这个问题,但是用户可以通过编写一个 Controller 的方式来自己实现。
应用优先级
QoS(服务质量)
Requests 和 Limits 的配置除了表明资源情况和限制资源使用之外,还有一个隐藏的作用:它决定了 Pod 的 QoS 等级。
上一节我们提到了一个细节:如果 Pod 没有配置 Limits ,那么它可以使用节点上任意多的可用资源。这类 Pod 能灵活使用资源,但这也导致它不稳定且危险,对于这类 Pod 我们一定要在它占用过多资源导致节点资源紧张时处理掉。优先处理这类 Pod,而不是处理资源使用处于自己请求范围内的 Pod 是非常合理的想法,而这就是 Pod QoS 的含义:根据 Pod 的资源请求把 Pod 分成不同的重要性等级。
Kubernetes 把 Pod 分成了三个 QoS 等级:
Guaranteed:优先级最高,可以考虑数据库应用或者一些重要的业务应用。除非 Pods 使用超过了它们的 Limits,或者节点的内存压力很大而且没有 QoS 更低的 Pod,否则不会被杀死。
Burstable:这种类型的 Pod 可以多于自己请求的资源(上限由 Limit 指定,如果 Limit 没有配置,则可以使用主机的任意可用资源),但是重要性认为比较低,可以是一般性的应用或者批处理任务。
Best Effort:优先级最低,集群不知道 Pod 的资源请求情况,调度不考虑资源,可以运行到任意节点上(从资源角度来说),可以是一些临时性的不重要应用。Pod 可以使用节点上任何可用资源,但在资源不足时也会被优先杀死。
Pod 的 Requests 和 Limits 是如何对应到这三个 QoS 等级上的,用下面一张表格概括:
问题:如果不配置 Requests 和 Limits,Pod 的 QoS 竟然是最低的。没错,所以推荐大家理解 QoS 的概念,并且按照需求一定要给 Pod 配置 Requests 和 Limits 参数,不仅可以让调度更准确,也能让系统更加稳定。
NOTE:按照现在的方法根据 Pod 请求的资源进行配置不够灵活和直观,更理想的情况是用户可以直接配置 Pod 的 QoS,而不用关心具体的资源申请和上限值。但 Kubernetes 目前还没有这方面的打算。
Pod 的 QoS 还决定了容器的 OOM(Out-Of-Memory)值,它们对应的关系如下:
QoS 越高的 Pod OOM 值越低,也就越不容易被系统杀死。对于 Bustable Pod,它的值是根据 Request 和节点内存总量共同决定的:
oomScoreAdjust := 1000 - (1000*memoryRequest)/memoryCapacity
其中 memoryRequest
是 Pod 申请的资源,memoryCapacity
是节点的内存总量。可以看到,申请的内存越多,OOM 值越低,也就越不容易被杀死。
QoS 的作用会在后面介绍 Eviction 的时候详细讲解。
Pod 优先级(Priority)
除了 QoS,Kubernetes 还允许我们自定义 Pod 的优先级,比如:
apiVersion: scheduling.k8s.io/v1alpha1 kind: PriorityClass metadata:
name: high-priority
value: 1000000 globalDefault: false description: "This priority class should be used for XYZ service Pods only."
优先级的使用也比较简单,只需要在 Pod.spec.PriorityClassName
指定要使用的优先级名字,即可以设置当前 Pod 的优先级为对应的值。
Pod 的优先级在调度的时候会使用到。首先,待调度的 Pod 都在同一个队列中,启用了 Pod priority 之后,调度器会根据优先级的大小,把优先级高的 Pod 放在前面,提前调度。
如果在调度的时候,发现某个 Pod 因为资源不足无法找到合适的节点,调度器会尝试 Preempt 的逻辑。简单来说,调度器会试图找到这样一个节点:找到它上面优先级低于当前要调度 Pod 的所有 Pod,如果杀死它们,能腾足够的资源,调度器会执行删除操作,把 Pod 调度到节点上。更多内容可以参考:Pod Priority and Preemption - Kubernetes。
驱逐(Eviction)
讲述的都是理想情况下 Kubernetes 的工作状况,我们假设资源完全够用,而且应用也都是在使用规定范围内的资源。
在管理集群的时候我们常常会遇到资源不足的情况,在这种情况下我们要保证整个集群可用,并且尽可能减少应用的损失。保证集群可用比较容易理解,首先要保证系统层面的核心进程正常,其次要保证 Kubernetes 本身组件进程不出问题;但是如何量化应用的损失呢?首先能想到的是如果要杀死 Pod,要尽量减少总数。另外一个就和 Pod 的优先级相关了,那就是尽量杀死不那么重要的应用,让重要的应用不受影响。
Pod 的驱逐是在 Kubelet 中实现的,因为 Kubelet 能动态地感知到节点上资源使用率实时的变化情况。其核心的逻辑是:Kubelet 实时监控节点上各种资源的使用情况,一旦发现某个不可压缩资源出现要耗尽的情况,就会主动终止节点上的 Pod,让节点能够正常运行。被终止的 Pod 所有容器会停止,状态会被设置为 Failed。
驱逐触发条件
目前主要有三种情况:实际内存不足、节点文件系统的可用空间(文件系统剩余大小和 Inode 数量)不足、以及镜像文件系统的可用空间(包括文件系统剩余大小和 Inode 数量)不足。
下面这图是具体的触发条件:
有了数据的来源,另外一个问题是触发的时机,也就是到什么程度需要触发驱逐程序?Kubernetes 运行用户自己配置,并且支持两种模式:按照百分比和按照绝对数量。比如对于一个 32G 内存的节点当可用内存少于 10% 时启动驱逐程序,可以配置 memory.available<10%
或者 memory.available<3.2Gi
。
NOTE:默认情况下,Kubelet 的驱逐规则是
memory.available<100Mi
,对于生产环境这个配置是不可接受的,所以一定要根据实际情况进行修改。
软驱逐(Soft Eviction)和硬驱逐(Hard Eviction)
因为驱逐 Pod 是具有毁坏性的行为,因此必须要谨慎。有时候内存使用率增高只是暂时性的,有可能 20s 内就能恢复,这时候启动驱逐程序意义不大,而且可能会导致应用的不稳定,我们要考虑到这种情况应该如何处理;另外需要注意的是,如果内存使用率过高,比如高于 95%(或者 90%,取决于主机内存大小和应用对稳定性的要求),那么我们不应该再多做评估和考虑,而是赶紧启动驱逐程序,因为这种情况再花费时间去判断可能会导致内存继续增长,系统完全崩溃。
为了解决这个问题,Kubernetes 引入了 Soft Eviction 和 Hard Eviction 的概念。
软驱逐可以在资源紧缺情况并没有哪些严重的时候触发,比如内存使用率为 85%,软驱逐还需要配置一个时间指定软驱逐条件持续多久才触发,也就是说 Kubelet 在发现资源使用率达到设定的阈值之后,并不会立即触发驱逐程序,而是继续观察一段时间,如果资源使用率高于阈值的情况持续一定时间,才开始驱逐。并且驱逐 Pod 的时候,会遵循 Grace Period ,等待 Pod 处理完清理逻辑。和软驱逐相关的启动参数是:
--eviction-soft
:软驱逐触发条件,比如memory.available<1Gi
。--eviction-sfot-grace-period
:触发条件持续多久才开始驱逐,比如memory.available=2m30s
。--eviction-max-Pod-grace-period
:Kill Pod 时等待 Grace Period 的时间让 Pod 做一些清理工作,如果到时间还没有结束就做 Kill。
前面两个参数必须同时配置,软驱逐才能正常工作;后一个参数会和 Pod 本身配置的 Grace Period 比较,选择较小的一个生效。
硬驱逐更加直接干脆,Kubelet 发现节点达到配置的硬驱逐阈值后,立即开始驱逐程序,并且不会遵循 Grace Period,也就是说立即强制杀死 Pod。对应的配置参数只有一个 --evictio-hard
,可以选择上面表格中的任意条件搭配。
设置这两种驱逐程序是为了平衡节点稳定性和对 Pod 的影响,软驱逐照顾到了 Pod 的优雅退出,减少驱逐对 Pod 的影响;而硬驱逐则照顾到节点的稳定性,防止资源的快速消耗导致节点不可用。
软驱逐和硬驱逐可以单独配置,不过还是推荐两者都进行配置,一起使用。
驱逐哪些 Pods?
上面已经整体介绍了 Kubelet 驱逐 Pod 的逻辑和过程。牵涉到一个具体的问题:要驱逐哪些 Pod?驱逐的重要原则是尽量减少对应用程序的影响。
如果是存储资源不足,Kubelet 会根据情况清理状态为 Dead 的 Pod 和它的所有容器,以及清理所有没有使用的镜像。如果上述清理并没有让节点回归正常,Kubelet 就开始清理 Pod。
一个节点上会运行多个 Pod,驱逐所有的 Pods 显然是不必要的,因此要做出一个抉择:在节点上运行的所有 Pod 中选择一部分来驱逐。虽然这些 Pod 乍看起来没有区别,但是它们的地位是不一样的,
系统组件的 Pod 要比普通的 Pod 更重要,另外运行数据库的 Pod 自然要比运行一个无状态应用的 Pod 更重要。Kubernetes 又是怎么决定 Pod 的优先级的呢?这个问题的答案就藏在我们之前已经介绍过的内容里:Pod Requests 和 Limits、优先级(Priority),以及 Pod 实际的资源使用。
简单来说,Kubelet 会根据以下内容对 Pod 进行排序:Pod 是否使用了超过请求的紧张资源、Pod 的优先级、然后是使用的紧缺资源和请求的紧张资源之间的比例。具体来说,Kubelet 会按照如下的顺序驱逐 Pod:
使用的紧张资源超过请求数量的
BestEffort
和Burstable
Pod,这些 Pod 内部又会按照优先级和使用比例进行排序。紧张资源使用量低于 Requests 的
Burstable
和Guaranteed
的 Pod 后面才会驱逐,只有当系统组件(Kubelet、Docker、Journald 等)内存不够,并且没有上面 QoS 比较低的 Pod 时才会做。执行的时候还会根据 Priority 排序,优先选择优先级低的 Pod。
防止波动
波动有两种情况,第一种。驱逐条件出发后,如果 Kubelet 驱逐一部分 Pod,让资源使用率低于阈值就停止,那么很可能过一段时间资源使用率又会达到阈值,从而再次出发驱逐,如此循环往复……为了处理这种问题,我们可以使用 --eviction-minimum-reclaim
解决,这个参数配置每次驱逐至少清理出来多少资源才会停止。
另外一个波动情况是这样的:Pod 被驱逐之后并不会从此消失不见,常见的情况是 Kubernetes 会自动生成一个新的 Pod 来取代,并经过调度选择一个节点继续运行。如果不做额外处理,有理由相信 Pod 选择原来节点的可能性比较大(因为调度逻辑没变,而它上次调度选择的就是该节点),之所以说可能而不是绝对会再次选择该节点,是因为集群 Pod 的运行和分布和上次调度时极有可能发生了变化。
无论如何,如果被驱逐的 Pod 再次调度到原来的节点,很可能会再次触发驱逐程序,然后 Pod 再次被调度到当前节点,循环往复…… 这种事情当然是我们不愿意看到的,虽然看似复杂,但这个问题解决起来非常简单:驱逐发生后,Kubelet 更新节点状态,调度器感知到这一情况,暂时不往该节点调度 Pod 即可。--eviction-pressure-transition-period
参数可以指定 Kubelet 多久才上报节点的状态,因为默认的上报状态周期比较短,频繁更改节点状态会导致驱逐波动。
使用了上面多种参数的驱逐配置实例:
–eviction-soft=memory.available<80%,nodefs.available<2Gi \
–eviction-soft-grace-period=memory.available=1m30s,nodefs.available=1m30s \
–eviction-max-Pod-grace-period=120 \
–eviction-hard=memory.available<500Mi,nodefs.available<1Gi \
–eviction-pressure-transition-period=30s \
--eviction-minimum-reclaim="memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi"
碎片整理和重调度
Kubernetes 的调度器在为 Pod 选择运行节点的时候,只会考虑到调度那个时间点集群的状态,经过一系列的算法选择一个当时最合适的节点。但是集群的状态是不断变化的,用户创建的 Pod 也是动态的,随着时间变化,原来调度到某个节点上的 Pod 现在看来可能有更好的节点可以选择。比如考虑到下面这些情况:
调度 Pod 的条件已经不再满足,比如节点的 Taints 和 Labels 发生了变化。
新节点加入了集群。如果默认配置了把 Pod 打散,那么应该有一些 Pod 最好运行在新节点上。
节点的使用率不均匀。调度后,有些节点的分配率和使用率比较高,另外一些比较低。
节点上有资源碎片。有些节点调度之后还剩余部分资源,但是又低于任何 Pod 的请求资源;或者 Memory 资源已经用完,但是 CPU 还有挺多没有使用。
想要解决上述的这些问题,都需要把 Pod 重新进行调度(把 Pod 从当前节点移动到另外一个节点)。但是默认情况下,一旦 Pod 被调度到节点上,除非给杀死否则不会移动到另外一个节点的。
Kubernetes 社区孵化了一个称为 Descheduler
的项目,专门用来做重调度。重调度的逻辑很简单:找到上面几种情况中已经不是最优的 Pod,把它们驱逐掉(Eviction)。
Descheduler 不会决定驱逐的 Pod 应该调度到哪台机器,而是假定默认的调度器会做出正确的调度抉择。也就是说,之所以 Pod 目前不合适,不是因为调度器的算法有问题,而是因为集群的情况发生了变化。如果让调度器重新选择,调度器现在会把 Pod 放到合适的节点上。这种做法让 Descheduler 逻辑比较简单,而且避免了调度逻辑出现在两个组件中。
Descheduler 执行的逻辑是可以配置的,目前有几种场景:
RemoveDuplicates
:RS、Deployment 中的 Pod 不能同时出现在一台机器上。LowNodeUtilization
:找到资源使用率比较低的 Node,然后驱逐其他资源使用率比较高节点上的 Pod,期望调度器能够重新调度让资源更均衡。RemovePodsViolatingInterPodAntiAffinity
:找到已经违反 Pod Anti Affinity 规则的 Pods 进行驱逐,可能是因为反亲和是后面加上去的。RemovePodsViolatingNodeAffinity
:找到违反 Node Affinity 规则的 Pods 进行驱逐,可能是因为 Node 后面修改了 Label。
当然,为了保证应用的稳定性,Descheduler 并不会随意地驱逐 Pod,还是会尊重 Pod 运行的规则,包括 Pod 的优先级(不会驱逐 Critical Pod,并且按照优先级顺序进行驱逐)和 PDB(如果违反了 PDB,则不会进行驱逐),并且不会驱逐没有 Deployment、RS、Jobs 的 Pod 不会驱逐,Daemonset Pod 不会驱逐,有 Local storage 的 Pod 也不会驱逐。
Descheduler 不是一个常驻的任务,每次执行完之后会退出,因此推荐使用 CronJob 来运行。
总的来说,Descheduler 是对原生调度器的补充,用来解决原生调度器的调度决策随着时间会变得失效,或者不够优化的缺陷。
资源动态调整
动态调整的思路:应用的实际流量会不断变化,因此使用率也是不断变化的,为了应对应用流量的变化,我们应用能够自动调整应用的资源。比如在线商品应用在促销的时候访问量会增加,我们应该自动增加 Pod 运算能力来应对;当促销结束后,有需要自动降低 Pod 的运算能力防止浪费。
运算能力的增减有两种方式:改变单个 Pod 的资源,以及增减 Pod 的数量。这两种方式对应了 Kubernetes 的 HPA 和 VPA。
Horizontal Pod AutoScaling(横向 Pod 自动扩展)
横向 Pod 自动扩展的思路是这样的:Kubernetes 会运行一个 Controller,周期性地监听 Pod 的资源使用情况,当高于设定的阈值时,会自动增加 Pod 的数量;当低于某个阈值时,会自动减少 Pod 的数量。自然,这里的阈值以及 Pod 的上限和下限的数量都是需要用户配置的。
一个重要的信息:HPA 只能和 RC、Deployment、RS 这些可以动态修改 Replicas 的对象一起使用,而无法用于单个 Pod、Daemonset(因为它控制的 Pod 数量不能随便修改)等对象。
目前官方的监控数据来源是 Metrics Server 项目,可以配置的资源只有 CPU,但是用户可以使用自定义的监控数据(比如:Prometheus)。其他资源(比如:Memory)的 HPA 支持也已经在路上了。
Vertical Pod AutoScaling
和 HPA 的思路相似,只不过 VPA 调整的是单个 Pod 的 Request 值(包括 CPU 和 Memory)。VPA 包括三个组件:
Recommander:消费 Metrics Server 或者其他监控组件的数据,然后计算 Pod 的资源推荐值。
Updater:找到被 VPA 接管的 Pod 中和计算出来的推荐值差距过大的,对其做 Update 操作(目前是 Evict,新建的 Pod 在下面 Admission Controller 中会使用推荐的资源值作为 Request)。
Admission Controller:新建的 Pod 会经过该 Admission Controller,如果 Pod 是被 VPA 接管的,会使用 Recommander 计算出来的推荐值。
可以看到,这三个组件的功能是互相补充的,共同实现了动态修改 Pod 请求资源的功能。相对于 HPA,目前 VPA 还处于 Alpha,并且还没有合并到官方的 Kubernetes Release 中,后续的接口和功能很可能会发生变化。
Cluster Auto Scaler
随着业务的发展,应用会逐渐增多,每个应用使用的资源也会增加,总会出现集群资源不足的情况。为了动态地应对这一状况,我们还需要 CLuster Auto Scaler,能够根据整个集群的资源使用情况来增减节点。
对于公有云来说,Cluster Auto Scaler 就是监控这个集群因为资源不足而 Pending 的 Pod,根据用户配置的阈值调用公有云的接口来申请创建机器或者销毁机器。对于私有云,则需要对接内部的管理平台。
目前 HPA 和 VPA 不兼容,只能选择一个使用,否则两者会相互干扰。而且 VPA 的调整需要重启 Pod,这是因为 Pod 资源的修改是比较大的变化,需要重新走一下 Apiserver、调度的流程,保证整个系统没有问题。目前社区也有计划在做原地升级,也就是说不通过杀死 Pod 再调度新 Pod 的方式,而是直接修改原有 Pod 来更新。
理论上 HPA 和 VPA 是可以共同工作的,HPA 负责瓶颈资源,VPA 负责其他资源。比如对于 CPU 密集型的应用,使用 HPA 监听 CPU 使用率来调整 Pods 个数,然后用 VPA 监听其他资源(Memory、IO)来动态扩展这些资源的 Request 大小即可。当然这只是理想情况,
总结
集群的资源使用并不是静态的,而是随着时间不断变化的,目前 Kubernetes 的调度决策都是基于调度时集群的一个静态资源切片进行的,动态地资源调整是通过 Kubelet 的驱逐程序进行的,HPA 和 VPA 等方案也不断提出,相信后面会不断完善这方面的功能,让 Kubernetes 更加智能。
资源管理和调度、应用优先级、监控、镜像中心等很多东西相关,是个非常复杂的领域。在具体的实施和操作的过程中,常常要考虑到企业内部的具体情况和需求,做出针对性的调整,并且需要开发者、系统管理员、SRE、监控团队等不同小组一起合作。但是这种付出从整体来看是值得的,提升资源的利用率能有效地节约企业的成本,也能让应用更好地发挥出作用。
本文转自开源中国-自动化运维Kubernetes