相信很多朋友在学习K8S的时候,能够借助yaml文档把自己的应用部署到K8S集群上,但是对于K8S内部的技术细节和实现原理并不了解,而这恰恰正是我们作为开发者提升技术所欠缺的东西。那么今天我们就来简单总结一下K8S的基本架构和其中的各个组件的概念和原理。
在开始正式介绍K8S之前,我们首先要搞明白一个问题:K8S是用来干什么的?
首先,熟悉网购的朋友可能都知道,每年的双十一期间,会有无数的订单传往淘宝,这对阿里的服务器系统的运算能力和承受能力是一个巨大的挑战。毫无疑问,哪怕是一台超级计算机,面对PB级别的访问业务也会应接不暇。
云计算技术的日益成熟弥补了企业计算能力的不足。所谓云计算,就是把许多台单独的计算机的计算能力集中起来,承担起单台计算机无法承受的业务。实现云计算,我们不得不提到“虚拟化”技术,也就是使不同系统不同配置的机器能够提供相同环境的技术。
(欢迎移步我的另一片关于云计算虚拟化的入门介绍博客)
而容器技术则是轻量化的虚拟技术。它不需要虚拟出整个操作系统,只需要虚拟一个小规模的环境。2013年3月,dotCloud公司的创始人之一,28岁的Solomon Hykes正式决定,将Docker项目开源,这使得Docker容器引擎受到了广大企业和开发者青睐。
平常使用过Docker等容器技术的朋友可能会有这样的感受:当我们把应用部署在一个或几个容器之中的时候,我们需要完成拉取镜像、启动容器、解决不同容器的通信问题、终止容器进程等等一系列操作,实在是非常不方便。也就是说,怎么实现多台计算机之间的业务调度和资源管理,是我们必须要解决的问题。
那么,有没有一款容器集群管理工具,能够帮我解决这些底层的东西,让我专注于业务本身呢?
答案当然是有的,那就是我们今天的主角——Kubernetes,又被称作K8S。
Kubernetes源于希腊语,有“舵”或“飞行员”的意思。而K8S,则是由Kubernetes中间的八个字母缩写为数字8得来的。Google采用这个名字的深意就是:docker把自己定位成驮着集装箱在大海上遨游的鲸鱼,那么Kubernetes就是鲸鱼的掌舵者,鲸鱼必须按照其设定的路线巡游。从上边图片中的土拨鼠也能看得出来,Kubernetes是使用GO语言开发的。
Kubernetes是一个完备的分布式系统支撑平台,具有完备的集群管理能力,多扩多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和发现机制、內建智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制以及多粒度的资源配额管理能力。同时Kubernetes提供完善的管理工具,涵盖了包括开发、部署测试、运维监控在内的各个环节。
从K8S的定义中我们可以知道,所谓Kubernetes的核心特点,就是能够自主地管理容器来保证云平台中的容器按照用户的期望状态运行(比如用户想让apache一直运行,用户不需要关心怎么去做,Kubernetes会自动去监控,然后去重启,新建,总之,让apache一直提供服务),同时,Kubernetes也提升了工具以及人性化方面,让用户能够方便的部署自己的应用。
总而言之,Kubernetes的出现使得用户能够轻松地把自己的应用或业务部署在一个K8S集群上,而避免了处理集群中可能存在的调度、监控、运维、网络等等诸多问题。那么K8S究竟是怎么做到这些的呢?接下来我们就来正式学习一下K8S集群的基本架构。
在正式介绍K8S的架构之前,请各位读者先思考一个问题:如果让你来设计一个由50台计算机组成的集群,这个集群可以提供一台超级计算机的计算能力和负载能力,那么你会怎么设计?
首先,我们总得有几台机器是专门负责对整个集群进行管理和调度的吧?如果这几台计算机能够提供对外界的接口,那就太好不过了。在实际的K8S集群中,我们往往会分出几台计算机专门处理这样的管理事务,它们被称作“Master”节点。
其次,我们必须要有大量的机器负责处理实际的业务。因此,我们就可以把剩下的机器都用于提供计算能力,它们应当具有正常的运算能力和通信能力。实际中这些节点被称作“Node”节点,而在每一个Node节点中,都运行着若干Pod进程,处理实际的业务。
K8S集群的管理节点,负责管理集群,提供集群的资源数据访问入口。它拥有etcd进行存储,运行Api Server进程,Controller Manager服务进程及Scheduler服务进程,关联工作节点Node。
kube-apiserver:是整个k8s集群对外提供服务的唯一接口,它提供请求过滤、访问控制等机制,是各组件的协调者,此API是声明式的(简单说就是用户想要什么规格的容器直接跟kube-apiserver说就行了,过程不用你管)。用户的合法请求会被API放行,然后存入etcd中。
是否合法指的是:etcd就好像公司领导,kuber-apiserver就是门口保安,领导规定,必须什么样的人你能放进来。k8s将etcd所能接受的数据规格范式加以封装定义在了kube-apiserver中,符合规格才能放行。
kube-scheduler:资源调度器。kube-apiserver收到新建Pod的请求,识别其合法并存入etcd,然后kube-scheduler去watch kube-apiserver知道此需求,根据预定的调度策略评估出一个最合适Node节点来运行Pod,如果没有最合适,那就随机,最后会把调度的结果记录在etcd中。关于scheduler的更多原理,请移步我的博客。
kube-controller:控制器。就好比人类的大脑一样,负责维护集群的状态、故障检测与恢复、自动扩展、节点状态等等。kube-controller有一个control loop的机制,它会循环检测集群中Pod的状态,假如Nginx启的不是预期的80端口,那就由kube-controller来控制容器重启、重建,直到达到预期效果为止。
etcd:是一个键值对(key:value)格式的存储系统,保存应用程序配置信息。
总而言之,如果把ApiServer比作“接待大厅”的话,那么Controller就是负责掌控Api对象运行周期的“控制室”,Scheduler就是负责提供调度策略的“调度室”,而etcd则是存储着所有资源对象信息的“存储室”。
比方说,用户发送新建Nginx容器的请求给kube-apiserver,kube-apiserver识别其合法后以键值对的方式存入etcd,kube-scheduler和kube-controller通过watch kube-apiserver知道此需求,然后kube-scheduler负责资源分配并决定容器运行在哪个Node上,至于运行时所需的镜像及运行的健康状态的维护都由kube-controller来负责,kube-controller会循环将当前容器的状态与watch到的用户预期的需求做对比,看是否匹配。
Node是Kubernetes集群架构中运行Pod的服务节点(亦叫agent或minion)。Node是Kubernetes集群操作的单元,用来承载被分配Pod的运行,是Pod运行的宿主机。关联Master管理节点,拥有名称和IP、系统资源信息。运行docker eninge服务,守护进程kunelet及负载均衡器kube-proxy。
Node节点可以在运行期间动态增加到Kubernetes集群中,默认情况下,kubelet会想master注册自己,这也是Kubernetes推荐的Node管理方式,kubelet进程会定时向Master汇报自身情报,如操作系统、Docker版本、CPU和内存,以及有哪些Pod在运行等等,这样Master可以获知每个Node节点的资源使用情况,冰实现高效均衡的资源调度策略。
我们简单分析一下K8S集群部署业务的流程,并简单看一看各个组件的作用。
准备包含应用程序的Deployment的yaml文件,然后通过kubectl客户端工具发送给ApiServer。
ApiServer接收到客户端的请求并将资源内容存储到数据库(etcd)中。
Controller组件(包括scheduler、replication、endpoint)监控资源变化并作出反应。
ReplicaSet检查数据库变化,创建期望数量的pod实例。
Scheduler再次检查数据库变化,发现尚未被分配到具体执行节点(node)的Pod,然后根据一组相关规则将pod分配到可以运行它们的节点上,并更新数据库,记录pod分配情况。
Kubelete监控数据库变化,管理后续pod的生命周期,发现被分配到它所在的节点上运行的那些pod。如果找到新pod,则会在该节点上运行这个新pod。
API对象是K8s集群中的管理操作单元。K8s集群系统每支持一项新功能,引入一项新技术,一定会新引入对应的API对象,支持对该功能的管理操作。例如副本集Replica Set对应的API对象是RS。
每个API对象都有3大类属性:元数据metadata、规范spec和状态status。
所有Kubernetes中的资源,比如Pod,都通过一个叫URI的东西来区分,这个URI有一个UID,URI的重要组成部分是:对象的类型(比如pod),对象的名字,对象的命名空间,对于特殊的对象类型,在同一个命名空间内,所有的名字都是不同的,在对象只提供名称,不提供命名空间的情况下,这种情况是假定是默认的命名空间。UID是时间和空间上的唯一。
那么总体介绍完了API对象的定义和特性之后,我们就来简单了解一下几种重要的K8S集群中常见的API对象。
K8s有很多技术概念,同时对应很多API对象,最重要的也是最基础的是微服务Pod。从之前的架构图像中我们也能看出,每一个Node上都运行着若干个Pod。Pod是在K8s集群中运行部署应用或服务的最小单元。Pod的设计理念是支持多个容器在一个Pod中共享网络地址和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。
Pod对多容器的支持是K8s最基础的设计理念。比如你运行一个操作系统发行版的软件仓库,一个Nginx容器用来发布软件,另一个容器专门用来从源仓库做同步,这两个容器的镜像不太可能是一个团队开发的,但是他们一块儿工作才能提供一个微服务;这种情况下,不同的团队各自开发构建自己的容器镜像,在部署的时候组合成一个微服务对外提供服务。
Pod是K8s集群中所有业务类型的基础,可以看作运行在K8s集群中的小机器人,不同类型的业务就需要不同类型的小机器人去执行。目前K8s中的业务主要可以分为长期伺服型(long-running)、批处理型(batch)、节点后台支撑型(node-daemon)和有状态应用型(stateful application),分别对应的小机器人控制器为Deployment、Job、DaemonSet和PetSet,本文后面会挑重点进行介绍。
RC是K8s集群中最早的保证Pod高可用的API对象。通过监控运行中的Pod来保证集群中运行指定数目的Pod副本。指定的数目可以是多个也可以是1个;少于指定数目,RC就会启动运行新的Pod副本;多于指定数目,RC就会杀死多余的Pod副本。即使在指定数目为1的情况下,通过RC运行Pod也比直接运行Pod更明智,因为RC也可以发挥它高可用的能力,保证永远有1个Pod在运行。RC是K8s较早期的技术概念,只适用于长期伺服型的业务类型,比如控制小机器人提供高可用的Web服务。
RS是新一代RC,提供同样的高可用能力,区别主要在于RS后来居上,能支持更多种类的匹配模式。副本集对象一般不单独使用,而是作为Deployment的理想状态参数使用。
部署表示用户对K8s集群的一次更新操作。部署是一个比RS应用模式更广的API对象,可以是创建一个新的服务,更新一个新的服务,也可以是滚动升级一个服务。滚动升级一个服务,实际是创建一个新的RS,然后逐渐将新RS中副本数增加到理想状态,将旧RS中的副本数减小到0的复合操作;这样一个复合操作用一个RS是不太好描述的,所以用一个更通用的Deployment来描述。以K8s的发展方向,未来对所有长期伺服型的的业务的管理,都会通过Deployment来管理。
RC、RS和Deployment只是保证了支撑服务的微服务Pod的数量,但是没有解决如何访问这些服务的问题。一个Pod只是一个运行服务的实例,随时可能在一个节点上停止,在另一个节点以一个新的IP启动一个新的Pod,因此不能以确定的IP和端口号提供服务。要稳定地提供服务需要服务发现和负载均衡能力。服务发现完成的工作,是针对客户端访问的服务,找到对应的的后端服务实例。在K8s集群中,客户端需要访问的服务就是Service对象。每个Service会对应一个集群内部有效的虚拟IP,集群内部通过虚拟IP访问一个服务。
Job是K8s用来控制批处理型任务的API对象。批处理业务与长期伺服业务的主要区别是批处理业务的运行有头有尾,而长期伺服业务在用户不停止的情况下永远运行。Job管理的Pod根据用户的设置把任务成功完成就自动退出了。成功完成的标志根据不同的spec.completions策略而不同:单Pod型任务有一个Pod成功就标志完成;定数成功型任务保证有N个任务全部成功;工作队列型任务根据应用确认的全局成功而标志成功。
还有很多的API对象,这里就不一一介绍了。如果没个实例,说得太多,相信大家也是懵逼的。
在前几部分中,我们了解了K8S的基本架构和相关资源对象的概念。那么在K8S集群中,不同pod、不同node之间的网络是什么样的呢?让我们来简单看一看。
k8s的网络模型设计的基础原则是:每个pod都拥有一个独立的IP,并且假定所有pod都在一个可以直接连通的扁平的网络空间中,因此,不管他们是否在同一个node中,都能够通过ip进行互相访问。用户就可以不用考虑如何在pod之间建立联系,也不用考虑容器端口映射到主机端口等问题。
但是在实际中,IP是以pod为单位进行分配的,一个pod中的所有容器共享一个网络堆栈(即共享一个网络命名空间,包括IP、网络设备、配置等等都是共享的)。按照这个网络原则抽象出来的一个pod一个ip的模型也被叫做ip-per-pod模型。
容器之间的网络,其实也就是CNI。CNI(Container Network Interface)是由一组用于配置Linux容器的网络接口的规范和库组成,同时还包含了一些插件。CNI仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。
熟悉docker的同学会知道,docker创建的容器之间会通过一张docker0的网卡相互通信。而k8s社区为了方便用户适应各自的网络情况,暴露出一套api接口------cni,用户只要实现了这套接口就可以自定义容器网络配置策略。
k8s定义了一种资源Service,Kubernetes Service 定义了这样一种抽象:一个 Pod 的逻辑分组,一种可以访问它们的策略 —— 通常称为微服务。 这一组 Pod 能够被 Service 访问到,通常是通过 Label Selector实现的。
对 Kubernetes 集群中的应用,Kubernetes 提供了简单的 Endpoints API,只要 Service 中的一组 Pod发生变更,应用程序就会被更新。 对非 Kubernetes 集群中的应用,Kubernetes 提供了基于 VIP(虚拟IP) 的网桥的方式访问 Service,再由 Service 重定向到相应的 Pod。
在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy(服务代理) 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式,而不是 ExternalName 的形式。
服务代理模式主要分三种,分别为userspace代理模式、iptables代理模式、ipvs代理模式。这里仅介绍userspace代理模式。
Ingress 是从Kubernetes集群外部访问集群内部服务的入口。
通常情况下,service和pod仅可在集群内部网络中通过IP地址访问。所有到达边界路由器的流量或被丢弃或被转发到其他地方。Ingress是授权入站连接到达集群服务的规则集合。
关于如何在K8S中部署项目,我在之前的博客中已经举过把Mysql镜像部署在K8S集群上并且远程访问的例子。这里就不再赘述如何把项目部署在K8S上了,我们的重点在于探究当我们使用kubectl创建一个Pod时,它在集群中经历了什么样的过程。
用户提交创建Pod的请求,可以通过API Server的REST API ,也可用Kubectl命令行工具,支持Json和Yaml两种格式;
API Server 处理用户请求,存储Pod数据到Etcd;
Schedule通过和 API Server的watch机制,查看到新的pod,尝试为Pod绑定Node;
过滤主机:调度器用一组规则过滤掉不符合要求的主机,比如Pod指定了所需要的资源,那么就要过滤掉资源不够的主机;
主机打分:对第一步筛选出的符合要求的主机进行打分,在主机打分阶段,调度器会考虑一些整体优化策略,比如把一个Replication Controller的副本分布到不同的主机上,使用最低负载的主机等;
选择主机:选择打分最高的主机,进行binding操作,结果存储到Etcd中;
kubelet根据调度结果执行Pod创建操作: 绑定成功后,会启动container, docker run, scheduler会调用API Server的API在etcd中创建一个bound pod对象,描述在一个工作节点上绑定运行的所有pod信息。运行在每个工作节点上的kubelet也会定期与etcd同步bound pod信息,一旦发现应该在该工作节点上运行的bound pod对象没有更新,则调用Docker API创建并启动pod内的容器。
Kubernetes作为容器集群管理工具,于2015年7月22日迭代到 v 1.0并正式对外公布,这意味着这个开源容器编排系统可以正式在生产环境使用。Kubernetes项目凝结了Google过去十年间在生产环境的经验和教训,从Borg的多任务Alloc资源块到Kubernetes的多副本Pod,在Docker等高级引擎带动容器技术兴起和大众化的同时,为容器集群管理提供独了到见解和新思路。
作为开发人员,我们除了要能够熟练掌握Kubernetes中的各种命令行、具备部署扩容排错等运维能力,还应该时刻注意对K8S各构件和底层原理的学习。只有打好K8S项目的原理基础,我们才能提升个人在就业或者工作方面的竞争力,而不至于对发生的问题束手无策。