发展历史
云计算
在我上学期间,为了方便学习 Spark ,自己在台式机和笔记本通过 VMware 分别搭建了一个伪分布 Spark 环境,从组网开始,部署操作系统、数据库、运行时环境, Spark 集群 ,最后提交一个简单的 wordCount 到 Spark 集群上运行,大功告成。然而这两个 Spark 集群导致自己经常把大把的时间浪费在了同步数据上,最后忍无可忍,租了一个阿里云基础服务器,因为服务器已经提供了操作系统,网络等底层环境,自己仅仅部署了数据库、运行时环境、 Spark 集群。
套用云计算的3种服务模式, IaaS ( Infrastructure-as-a-Service )、 PaaS ( Platform-as-a-Service )、 SaaS ( Software-as-a-Service ),自己从石器时代终于踏步进入了 Iaas 阶段。在 IaaS 之前,部署 Applications ,首先需要组网,然后部署存储、购买硬件、安装操作系统、部署数据库、安全组件、运行时环境,最后部署应用; IaaS 阶段,基础服务设施即服务,只需向供应商购买相应的基础设施服务,基础设置包含了网络、虚拟化、操作系统等服务,然后在其上面部署数据库、安全组件、运行时环境、最后部署应用即可。 PaaS 阶段,平台即服务,平台服务包含了基本的网络、磁盘、操作系统、数据库、运行环境等,只需将应用部署到上面即可。 SaaS 阶段,软件设施即服务,例如Office,我们不需要经过一小时的安装,只需通过浏览器访问到其服务器端即可完成我们的需求。其发展如下图所示。
PasS
PaaS 经过了好几代的更替,最开始的时候是人工的构建方式,比如我向供应商打一个电话,说”我需要一个 LAMP 的环境, Apache 要求 2.3 版本, PhP 要求 5.3 版本, MySQL 需要 5.5 版本“,那么供应商需要人工或者通过脚本的方式去生成相应的运行平台,然后再将这个地址给我,我远程登陆即可,后来由 OpenStack 一直发展到以 Colud Foundry 为代表的开源 PaaS 项目,成为了当时云计算技术中的一股清流。
PaaS 项目被大家接纳的一个主要原因,就是它提供了一种名叫 应用托管 的能力。之前用户租赁的服务器或者平台服务,用户难免会遇到云端虚拟机和本地环境不一致的问题,所以当时的云计算,比的就是谁能更好地模拟本地服务器环境,能带来更好的“上云”体验。 PaaS 项目是当时解决这个问题的一个最佳方案。事实上,像 Cloud Foundry 这样的 PaaS 项目,最核心的组件就是一套应用的打包和分发机制。 Cloud Foundry 定义了一种打包方式,将用户的可执行文件和启动脚本打进一个压缩包内,上传至云端,接着, Cloud Foundry 会通过调度策略选择一个可以运行这个应用的虚拟机,然后通知这个机器将应用压缩包下载下来并启动。
这时候关键来了,由于需要在一个虚拟机上启动很多个来自不同用户的应用, Cloun Foundry 会调用操作系统的 Cgroups 和 Namespace 机制为每一个应用单独创建一个称作“沙盒”的隔离环境,然后在这个“沙盒”中启动这些应用程序。这样,就实现了把多个用户的应用互不干涉地在虚拟机里批量地、自动地运行起来的目的。这正式 PasS 项目最核心的能力。而这些“沙盒“,就是所谓的”容器“。
Docker
在这股 PaaS 热潮中,当时还名叫 dotCloud 的 Docker 公司,也是其中的一份子,长期以来无人问津。眼看就要被如火如荼的 PaaS 风潮抛弃, dotCloud 公司决定开发自己的容器项目 Docker 。然而,短短几个月, Docker 项目就迅速崛起。 Docker 项目确实与 Cloud Foundry 的容器在大部分功能和实现原理上都是一样的,可偏偏就是这剩下的一小部分不一样的功能,成了 Docker 项目接下来”呼风唤雨“的不二法宝。这个功能就是 Docker 镜像。
Docker 镜像从根本上解决了打包这个问题。所谓 Docker 镜像,其实就是个压缩包。但是这个压缩包里面的内容,比 PaaS 的应用可执行文件+启停脚本的组合就要丰富多了。实际上,大多数 Docker 镜像是直接由一个完整操作系统的所有文件和目录构成的,所以这个压缩包里的内容跟你本地开发和测试环境用的操作系统是完全一样的。即该镜像不论在哪儿运行,都可以得到和你本地测试时一样的环境。这正式 Docker 镜像的精髓。
所以, Docker 项目给 Paas 世界带来了“降维打击”,其实是提供了一种非常便利的打包机制。这种机制直接打包了应用运行所需要的整个操作系统,从而保证了本地环境和云端环境的高度一致,避免了用户通过“试错“来匹配两种不同运行环境之间差异的痛苦过程。
解决了应用打包这个根本性的问题,同开发者与生俱来的亲密关系,再加上 PaaS 概念已经深入人心的完美契机,成为 Docker 这个技术上看似平淡无奇的项目一举走红的重要原因。
kubernetes
但是容器化也带了一个问题,例如我需要部署一个 JavaWeb 项目,用到4台物理机, A 、 B 、 C 、 D , A 服务器部署 Nginx 、 B 、 C 部署 Tomcat 、 D 服务器部署 MySQL 服务器,他们之间的网络联通通过基本的 TCP 就可以访问。但是如果一旦容器化之后,例如我在 A 服务器上部署 Docker-Nginx 并将容器例如80端口映射到物理机80端口、 B 、 C 服务器上部署 Docker-Tomcat 并将容器例如8080端口映射到物理机8080端口、 D 服务器上部署 MySQL 服务器并将端口3306端口映射到物理机3306端口,并且我们一般容器化就是最大化利用资源,一台物理机上会部署多个 Docker 容器,你会发现仅端口管理就让人头大,更不用说集群的容错恢复等,那么怎么解决这个问题呢?也就是说容器的集群化有没有好的方案呢?有需求就会有产品,这个产品叫资源管理器。
Docker 公司也意识到这个问题,在2014年12月的 DockerCon 上发布 Swarm ,之后,大量围绕着 Docker 项目的网络、存储、监控、 CI/CD ,甚至 UI 项目纷纷出台,也涌现出了很多 Rancher 、 Tutum 这样在开源与商业上均取得了巨大成功的创业公司。这令人兴奋的繁荣背后,却浮现出了更多的担忧。很多从业者意识到 Docker 项目此时已经成为 Docker 公司一个商业产品,而开源,只是 Docker 公司吸引开发者群体的一个重要手段,更重要的是, Docker 公司在 Docker 开源项目的发展上,始终保持着绝对的权威和发言权,并在多个场合用实际行动挑战到了其他玩家(比如, CoreOS 、 RedHat ,甚至谷歌和微软)的切身利益。
所以这次, Google 、 RedHat 等开源基础设施领域玩家们,共同牵头发起了一个名为 CNCF ( Cloud Native Computing Foundation )的基金会。这个基金会的目的其实很容易理解:它希望,以 Kubernetes 项目为基础,建立一个由开源基础设施领域厂商主导的、按照独立基金会方式运营的平台级社区,来对抗以 Docker 公司为核心的容器商业生态。
Kubernetes 项目,并不是几个工程师突然“拍脑袋”想出来的东西,而是 Google 公司在容器化基础设施领域多年来实践经验的沉淀与升华,另一方面, kubernetes 整个社区推进“民主化”架构,即:从 API 到容器运行时的每一层, Kubernetes 项目都为开发者暴露出了可以扩展的插件机制,鼓励用户通过代码的方式介入到 Kubernetes 项目的每一个阶段。
Kubernetes 项目的这个变革的效果立竿见影,很快在整个容器社区中催生出了大量的、基于 Kubernetes API 和扩展接口的二次创新工作,比如:
目前热度极高的微服务治理项目 Istio ;
被广泛采用的有状态应用部署框架 Operator ;
还有像 Rook 这样的开源创业项目,它通过 Kubernetes 的可扩展接口,把 Ceph 这样的重量级产品封装成了简单易用的容器存储插件。
就这样,在这种鼓励二次创新的整体氛围当中, Kubernetes 社区在2016 年之后得到了空前的发展。更重要的是,不同于之前局限于“打包、发布”这样的 PaaS 化路线,这一次容器社区的繁荣,是一次完全以 Kubernetes 项目为核心的“百花争鸣”。
容器简介
对于进程,它的静态表现就是程序,平常都安安静静地待在磁盘上;而一旦运行起来,它就变成了计算机里的数据和状态的总和,这就是它的动态表现。 而容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界” 。对于 Docker 等大多数 Linux 容器来说, Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。
Namespace
首先,让我们创建一个简单的 Docker 容器
$ docker run --it redis /bin/sh/ #
这个命令是告诉 Docker 帮我们启动一个 redis 容器,并在容器启动之后,帮我们分配一个文本输入/输出环境。这个样子,我的电脑就变成了一个宿主机,在其上运行着一个 redis 容器。
然后我们在容器中执行
/ # psPID USER TIME COMMAND1 root 0:00 /bin/sh...
按照常理来说,每当我们在宿主机上运行了一个 /bin/sh 程序,操作系统都会给它分配一个进程编号,比如 PID =100,而1号进程是系统最初的进程,而现在,我们发现在容器中 PID 也为1,其实, Docker 在内部给这个 PID =100的进程施了一个“障眼法”,让他永远看不到前面其他99个进程,让它错误的以为自己就是 PID =1.
这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进只能看到重新计算过的进程编号,例如 PID =1。可实际上,它们在宿主机的操作系统里,还是原来的第100号进程。这种技术,就是 Linux 里面的 Namespace 机制。 Linux 操作系统提供了多种 Namespace ,例如 Mount 、 UTS 、 IPC 、 Network 、 User 等,用来对各种不同的进程上下文进行“障眼法”操作。例如, Mount Namespace ,用来让被隔离进程只看到当前 Namespace 里的挂载点信息; Network Namespace ,用来让被隔离进程只能看到当前 Namespace 里的网络设备和配置。这就是 Linxu 容器最基本的实现原理了。
所以, Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
所以说,容器,其实是一种特殊的进程而已,即:容器是一个“单进程”模型。这意味着,用户运行在容器里的应用进程,跟宿主机上的其他进程一样,都由宿主机操作系统统一管理,只不过这些被隔离的进程拥有额外设置过的 Namespace 参数。而 Docker 项目在这里扮演的角色,更多的是旁路式的辅助和管理工作。
CGroups
Namesapce 实现了对容器的“隔离”,我们再来谈谈容器的“限制”问题。你可以能好奇,我们不是已经通过 Namespace 创建了一个容器吗?为什么还要对容器进行“限制”呢?
接着上述的例子,虽然容器内的第1号进程在“障眼法”的干扰下只能看到容器里的情况,但是宿主机上,它作为第100号进程与其他所有进程之间依然还是平等的竞争关系。这就意味着,虽然第100号进程表面上被隔离了起来,但是它所能使用到的资源(比如 CPU 、内存),却是可以随时被宿主机上的其他进程(或者其他容器)占用的。当然,这个100号进程自己也可能把所有资源吃光。这些情况,显然都不是一个“沙盒”应该表现出来的合理行为。
而 Linux CGroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。 Linux CGroups 的全程是 Linux Control Group 。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU 、内存、磁盘、网络宽带等等。
我们知道 Linux 系统配置都是保存在文件中,所以对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把相应的配置,例如 CPU,Memory 等信息填进去就可以了。
镜像
前面所说, Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。这么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,可是,这个房间四周虽然有了墙,但是如果容器进程低头一看地面,又会是怎样的一幅景象呢?即,容器里的进程看到的文件系统又是怎么样呢?
通过对 Namesapce 的了解,我们理所当然的想到了 Mount Namesapce , Mount Nameapsce 实际上修改了容器进程对文件系统的“挂载点”的认知。当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂在一个完整的操作系统的文件系统,例如 Ubuntu 的 ISO 。这个样子,在容器启动之后,我们在容器里通过执行 # ls / 查看根目录下的内容,就是 Ubuntu 的所有目录和文件。而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它有一个更为专业的名字,叫做: rootfs (根文件系统)。
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。
小结
现在我们可以知道,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
1. 启用 Linux Namesapce 配置;
2. 设置指定的 Cgroups 参数。
这样一个完整的容器就诞生了。我们也可以从另外一个角度,“一分为二”的看待一个正在运行的容器:
1. 一组 rootfs ,这一部分我们成为“容器镜像”,是容器的静态视图;
2. 一个由 Namesapce + Cgroups 构成的隔离环境,这一部分我们称为”容器运行时“,是容器的动态视图。
更进一步地说,作为一名开发者,我并不关心容器运行时的差异。因为,在整个”开发-测试-发布“的流程中,真正承载着容器信息进行传递的,是容器镜像,而不是容器运行时。
kubernetes
整体架构
Kubernetes 主要由 Master 、 Node 两部分组成。Master节点上运行着集群管理相关的一组进程 etcd 、 API Server 、 Controller Manager 、 Scheduler ,后三个组件构成了 Kubernetes 的总控中心,这些进程实现了整个集群的资源管理、 Pod 调度、弹性伸缩、安全控制、系统监控和纠错等管理功能,并且全都是自动完成。在每个 Node 上运行 Kubelet 、 Proxy、Docker daemon 三个组件,负责对本节点上的 Pod 的生命周期进行管理,以及实现服务代理的功能。
- etcd :用于持久化存储集群中所有的资源对象,如 Node 、 Service 、 Pod 、 RC 、 Namespace 等;
- scheduler :集群中的调度器,负责 Pod 在集群节点中的调度分配。
- API Service :提供了资源对象的唯一操作入口,其他所有组件都必须通过它提供的API来操作资源数据,通过对相关的资源数据“全量查询”+“变化监听”,这些组件可以很“实时”地完成相关的业务功能。
- Contoller Manager :集群内部的管理控制中心,其主要目的是实现 Kubernetes 集群的故障检测和恢复的自动化工作,比如根据 Controller Manger 的定义完成 Pod 的复制或移除,以确保 Pod 实例数符合 Controller Manager 副本的定义;根据 Service 与 Pod 的管理关系,完成服务的 Endpoints 对象的创建和更新;其他诸如 Node 的发现、管理和状态监控、死亡容器所占磁盘空间及本地缓存的镜像文件的清理等工作也是由 Controller Manager 完成的.
- kubelet :负责本 Node 节点上的 Pod 的创建、修改、监控、删除等全生命周期管理,同时 Kubelet 定时“上报”本 Node 的状态信息到 API Server 里。
- proxy :实现了 Service 的代理与软件模式的负载均衡器。
Pod
在 Docker 中,我在一台服务器上创建了两个容器 A 、 B ,因为容器之间通过 Namespace 进行隔离,容器 A 、 B 都有自己的网络空间, Ip 地址等等,假设容器 A 和 B 容器需要通过网络通讯进行交换数据, Docker 允许我们将容器端口暴露给主机,通过端口映射到主机以此来达到两个容器网络通讯的目的,当然这里只介绍了一种方式,我们也可以通过共享网络栈等方式进行容器之间的通讯,我们发现这些配置比较繁琐,从另一个角度考虑,我们想有些容器就应该在一起,并且它们之间应该能够见面,也就是通过 localhost 的方式可以访问到,但是如果我们采用标准的容器方案的话,我们不可以这样做,除非你把两个不同的进程封装到统一个容器中,或者是容器 A 采用容器 B 的网络栈,但是这样机会存在安全方面的隐患,因此, Kubernetes 给我们建立了一个新的概念,叫做 Pod 。
那么 Pod 是怎么解决上述问题的呢?如下图所示。
从上图可以, Pod 是一组容器的集合,管理多个容器,他也是 kubernetes 资源管理的最小单位,例如上图的容器 A 、 B ,并且每个 Pod 中都存在一个特殊的容器 pause ,其作为 Pod 启动的时候启动的第一个容器,其他容器共享该容器的网络栈和 Volumn 挂载卷。
- 公用网络栈也就因为这该 Pod 中容器都没有自己独立的 Ip 地址,它们拥有的都是 pause 的 Ip 地址,即它们之间的访问就可以通过 localhost 的方式进行访问,也就意味着在同一个 Pod 中,不同容器之间的端口不能冲突。
- 公用 Volumn 挂载卷,也就是说如果该 pause 挂载了一个网络存储,那么其他容器都可以访问该网络存储。
创建第一个pod
# vim pod. yaml #我们使用k8s哪个版本的apiapiVersion: v1#声明我们要创建一个Podkind: Pod#设置以下Pod中包含的元数据信息metadata: name: myapp-pod#设置该Pod特有的东西spec: #设置容器 containers: # 该pod第一个容器的相关信息,名字叫做container1,镜像是tomcat - name: container1 image: tomcat imagePullPolicy: IfNotPresent # 该pod第二个容器的相关信息,名字叫做container2,镜像是nginx - name: container2 image: nginx imagePullPolicy: IfNotPresent
- 运行
# kubectl apply -f pod.yamlpod/myapp-pod created
- 查看运行的状态
# kubectl get podNAME READY STATUS RESTARTS AGEmyapp-pod 2/2 Running 0 14s
- 验证 Pod 中包含的容器
# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS 94605c6c68e3 540a289bab6c "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 52fb0c36bd76 882487b8be1d "catalina.sh run" 2 minutes ago Up 2 minutes615e37eab4d7 k8s.gcr.io/pause:3.1 "/pause" 2 minutes ago Up 2 minutes
- 进入 Tomcat 访问8080端口 因为本地无法联通 kubernetes 为每个pod分配的虚拟 Ip ,因此进入容器本身进行测试。当然可以采用 Service 资源管理器进行本地访问(后面章节)。
# kubectl exec myapp-pod -c container1 -it -- /bin/bashroot@myapp-pod:/usr/local/tomcat#curl localhost:8080<!DOCTYPE html><!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <title>Apache Tomcat/8.5.47</title> ......
第一个 Pod 创建完成,如果这个时候,该 Pod 所在的节点宕机了,即该 Pod 被杀死了。
# kubectl delete pod myapp-podpod "myapp-pod" deleted# kubectl get podNo resources found.
我们发现我们创建的 Pod 没有了,但是有时候我们的想法是这个样子的:给我部署一个 Tamcat 服务器,如果该 Tomcat 挂掉了,给我重新启动一个,这个时候怎么办呢?其实,在 Kubernetes 中给我们提供了多种部署 Pod 的方式,主要分为两种:
- 自主式 Pod :即在 yaml 中指定 Kind = Pod 的方式。
- 控制器管理 Pod : Kubernetes 内置了多种控制器管理 Pod ,例如 Replication Controller ,我们可以直接指定 Kind = Replication Controller 即可,我们可以设定副本数=1,那么 Kubernets 会自动创建/删除 Pod 维持我们的期望 Pod 数目。下图所示了 Kubernetes 常用的控制器。
控制器
Replication Controller
Replication Controller (RC)用来确保容器应用的副本数始终保持在用户定义的副本数,即如果有容器异常退出,会自动创建新的 Pod 来代替;而异常多出来的容器也会自动回收。所以RC的定义包含以下几个部分:
- Pod 期待的数目( replicas )。
- 用户筛选目标 Pod 的 Label Selector 。
Label 即标签的意思,可以附加到各种资源上,例如 Node 、 Pod 、 Rc 、 Serice 等,一个资源对象可以定义任意数量的 Label ,同一个 Label 也可以被添加到任意数量的资源上去。随后可以通过 Label Selector (标签选择器)查询和筛选有某些 Label 的资源对象。 不同控制器内部就是通过 Label Selector 来筛选要监控的 Pod ,例如 RC 通过 Label Selector 来选择要监控 Pod 的副本数。所以我们在编写不同的控制器的时候一般都需要配置 Label Selector 。 * 当 Pod 的副本数小于预期数目的时候,用户创建新的 Pod 的模板( template )
创建第一个RC
Pod包含一个容器Tomcat,Pod通过RC控制器管理,维持副本数为3 * 编写资源清单
# vim rc.yaml#我们使用k8s哪个版本的apiapiVersion: v1#声明我们要创建一个RCkind: ReplicationController#设置以下RC中包含的元数据信息metadata: name: myapp-rc#设置该RC特有的东西spec: #副本数为3 replicas: 3 #选择器,该RC只管理label_1_key=label_1_value的Pod,如果不存在通过template创建 selector: label_1_key: label_1_value template: metadata: labels: #该Pod上添加 label_1_key=label_1_value label_1_key: label_1_value spec: #设置容器 containers: # 该pod第一个容器的相关信息,名字叫做container1,镜像是tomcat - name: container1 image: tomcat
需要注意的是 spec.template.metadata.labels 指定了该 Pod 的标签,这里的标签需要和 spec.selector 相匹配,否则此 RC 每次创建一个无法匹配的 Label 的 Pod ,就会不断地尝试创建新的 Pod ,最终陷入“只为他人做嫁衣“的悲惨世界中,永无翻身之时。
- 运行
# kubectl apply -f rc.yamlreplicationcontroller/myapp-rc created
- 查看运行状态
# kubectl get rcNAME DESIRED CURRENT READY AGEmyapp-rc 3 3 3 4s# kubectl get podNAME READY STATUS RESTARTS AGEmyapp-rc-9ddjv 1/1 Running 0 13smyapp-rc-j6w6m 1/1 Running 0 13smyapp-rc-qv9tx 1/1 Running 0 13s
- 删除名为 myapp-rc-9ddjv 的 Pod ,验证 RC 时候是否会自动创建新的 Pod 。
# kubectl delete pod myapp-rc-9ddjvpod "myapp-rc-9ddjv" deleted
- 查看运行状态
# kubectl get rcNAME DESIRED CURRENT READY AGEmyapp-rc 3 3 3 15m# kubectl get podNAME READY STATUS RESTARTS AGEmyapp-rc-j6w6m 1/1 Running 0 15mmyapp-rc-p5rbv 1/1 Running 0 61smyapp-rc-qv9tx 1/1 Running 0 15m
我们发现RC帮我们创建了一个新的Pod:myapp-rc-p5rbv
- 将名为myapp-rc-j6w6m的Pod的Label重命名为label_2_key=label_2_value
# kubectl edit pod myapp-rc-j6w6mlabels: 10 label_2_key: label_2_valuepod/myapp-rc-j6w6m edited
- 查看运行状态
# kubectl get rcNAME DESIRED CURRENT READY AGEmyapp-rc 3 3 3 36m# kubectl get pod --show-labelsNAME READY STATUS RESTARTS AGE LABELSmyapp-rc-j6w6m 1/1 Running 0 40m label_2_key=label_2_valuemyapp-rc-lkt4p 1/1 Running 0 85s label_1_key=label_1_valuemyapp-rc-p5rbv 1/1 Running 0 26m label_1_key=label_1_valuemyapp-rc-qv9tx 1/1 Running 0 40m label_1_key=label_1_value
由于 myapp-rc-j6w6m 标签修改,导致该 Pod 不受 RC 管理,所以 RC 为了维持副本数为3,为我们重新创建了一个新的 Pod 。
Replication Set
Replication Set ( RS )跟 Replication Controller 没有本质的不同,只是名字不一样,并且 Replication Set 支持集合式的 selector 。
RS 的定义与 RC 的定义很类似,除了 API 和 Kind 类型有所区别:
apiVersion: extensions/v1beat1kind: ReplicaSet
Deployment
Deployment 为 Pod 和 RS 提供了一个声明时定义方法,用来替代以前 RC 来方便的管理应用,其提供了 Pod 的滚动升级和回滚特性,主要应用场景包括。
- 定义 Deployment 来创建 Pod 和 RS 。
- 回滚升级和回滚应用。
- 扩容和缩容。
- 暂停和继续 Deployment 。
Deployment 的定义与 RS 的定义很类似,除了 API 和 Kind 类型有所区别:
apiVersion: extensions/v1beta1 kind: Deployment
创建第一个Deployment
- 编写资源清单
# vim deployment.yaml#我们使用k8s哪个版本的apiapiVersion: extensions/v1beta1#声明我们要创建一个Deploymentkind: Deployment#设置以下Deployment中包含的元数据信息metadata: name: myapp-deployment#设置该RC特有的东西spec: #副本数为3 replicas: 3 #选择器,该Deployment只管理label_1_key=label_1_value的Pod,如果不存在通过template创建 selector: matchLabels: label_1_key: label_1_value template: metadata: labels: #该Pod上添加 label_1_key=label_1_value label_1_key: label_1_value spec: #设置容器 containers: # 该pod第一个容器的相关信息,名字叫做container1,镜像是tomcat - name: container1 image: tomcat imagePullPolicy: IfNotPresent
- 运行并查看运行状态
# kubectl apply -f deployment.yamldeployment.extensions/myapp-deployment created# kubectl get deploymentNAME READY UP-TO-DATE AVAILABLE AGEmyapp-deployment 3/3 3 3 6s
Horizontal Pod Autoscaling
Horizontal Pod Authscaler 简成 HPA ,意思是 Pod 自动横向扩容,它也是一种资源对象。通过追踪分析所有控制Pod的负载变化情况,来确定是否需要针对性地调整目标 Pod 的副本数,这是 HPA 的实现原理。当前, HPA 可以有以下另种方式作为 Pod 负载的度量指标。
- CPUUtilizationPrecentage 。
- 应用程序自定义的度量指标,比如服务在每秒内的相应的请求数( TPS 或者 QPS )。
创建第一个HPA
- 编写资源清单
vim hpa.yamlapiVersion: autoscaling/v1kind: HorizontalPodAutoscalermetadata: name: myapp-hpaspec: maxReplicas: 10 minReplicas: 1 scaleTargetRef: kind: Deployment name: myapp-deployment targetCPUUtilizationPercentage: 90
- 运行并查看运行状态
# kubectl apply -f hpa.yamlhorizontalpodautoscaler.autoscaling/myapp-hpa created# kubectl get hpaNAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGEmyapp-hpa Deployment/myapp-deployment <unknown>/90% 1 10 0 7s
StatefullSet
StatefullSet 是为了解决有状态服务的问题(对应 Deployment 和 RS 是为无状态服务而设计),其应用场景包括:
- 稳定的持久化存储,即 Pod 重新调度后还是能访问到相同的持久化数据,基于 PVC 来实现。
- 稳定的网络标志,即 Pod 重新调度后其 PodName 和 hostName 不变,基于 Headless Service (即没有 Cluster IP 的 Service )来实现。
- 有序部署,有序扩展,即 Pod 是有顺序的,在部署或者扩展的时候要依据定义的顺序依次进行,基于 init containers 来实现。
- 有序收缩,有序删除。
DaemonSet
DaemonSet 确保全部(或者一些) Node 上运行一个 Pod 副本。当有 Node 加入集群时,也会为它们创建一个新的 Pod 。当有 Node 从集群中移除的时,这些 Pod 也会被回收。删除 DaemonSet 将会删除它创建的所有 Pod 。 使用 DemonSet 的一些典型用法:
- 运行集群存储 Daemon ,例如在每个 Node 上运行 gluster、ceph 。
- 在每个 Node 上运行日志收集 daemon ,例如 fluentd 、 logstash 。
- 在每个 Node 上运行监控 daemon ,例如 Prometheus Node Exporter 。
Job
Job 负责批处理任务,即仅执行一次的任务,它确保批处理任务的一个或者多个 Pod 成功运行。
Cron Job
Cron Job 管理基于时间的 Job 。即
- 在指定时间点只运行一次。
- 周期性地在给定时间点运行。
Service
假设 kubernetes 集群中运行了好多 pod , kubernetes 在创建每个 Pod 的时候会为每个 Pod 分配了一个虚拟的 Pod Ip 地址, Pod Ip 是一个虚拟的二层网络,集群之间不同机器之间 Pod 的通讯,其真实的 TCP/IP 流量是通过 Node 节点所在的物理网卡流出的( Node IP )。由于 Pod Ip 是 kubernetes 集群内部的一些私有 Ip 地址,因此 kubernetes 集群内部的程序才可以访问 Pod , kubernetes 集群之外的程序没有办法访问。
然而我们部署的许多应用都需要提供给外部客户端访问,因此,我们可以可以通过 kubernetes 的服务发现( Service ),将这些服务暴露给我们的客户端,那么客户端就可以通过 Ip + Port 的方式访问至我们的多个 Pod 。为什么说是多个 Pod 呢?因为Service为我们提供了复杂均衡机制。例如我们通过 Deployment 部署了一个 Tomcat , replicas 设置为3, Service 提供了多种负载均衡策略,针对不同的请求路由到不同的 Pod 上。如下图所示。
采用微服务架构时,作为服务所有者,除了实现业务逻辑外,我们也需要考虑应该怎样发布我们的服务,例如发布的服务中哪些服务不需要暴露给客户端,仅仅在服务内部之间使用,哪些服务我们又需要暴露出去,因此, kubernetes 给我们提供了多种灵活的服务发布方式,主要包括: ClusterIp 、 NodePort 、 LoadBalancer 等,其关系如下图所示。
ClusrterIP
当我们发布服务的时候, Kubernetes 会为我们的服务默认分配一个虚拟的 IP ,即 ClusterIp ,这也是Service默认的类型。 ClusterIp 更像是一个“伪造”的 IP 网络,原因有以下几点:
- Cluster IP 仅仅作用于 Kubernetes Service 这个对象,并有 Kubernetes 管理和分配IP地址(来源于 ClusterIP 地址池)。
- Cluster IP 无法被 Ping ,因为没有一个“实体网络对象”来响应。
- Cluster IP 只能结合 Service Port 组成一个具体的通讯端口,单独的 Cluster IP 不具备 TCP/IP 通讯的基础,并且它们属于 Kubernetes 集群这个一个封闭的空间,集群之外的节点如果要访问这个通讯端口,则需要做一些额外的工作。
- 在 Kubernetes 集群之外, Node Ip 网、 Pod IP 网于 Cluster IP 网之间的通讯,采用的是 Kubernetes 自己设计的一种编程方式的特殊的路由规则,与我们所熟知的 IP 路由有很大的不同。
创建第一个ClusterIP
通过 Deployment 部署一个 Tomcat , replcas 设置为3,并通过ClusterIP的方式注册tomcat为 Service ,另外我们创建一个 Nginx Pod ,通过 Nginx Pod 测试是否能访问该服务,另外我们通过本机是否能 Ping 通该服务。其结构如下。
- 创建 TomcatService
# vim myapp-deploy-tomcat.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: myapp-deploy-tomcat namespace: defaultspec: replicas: 3 selector: matchLabels: app: myapp-tomcat template: metadata: labels: app: myapp-tomcat spec: containers: - name: myapp-tomcat image: tomcat imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: myapp-service-tomcat namespace: defaultspec: type: ClusterIP selector: app: myapp-tomcat ports: - name: http port: 8080 targetPort: 8080
- 创建 LinuxPod
vim myapp-pod-tomca.yaml#我们使用k8s哪个版本的apiapiVersion: v1#声明我们要创建一个Podkind: Pod#设置以下Pod中包含的元数据信息metadata: name: myapp-pod-tomcat#设置该Pod特有的东西spec: #设置容器 containers: - name: myapp-pop-tomcat image: tomcat imagePullPolicy: IfNotPresent
- LinuxPod 中访问 Service
# kubectl get pod NAME READY STATUS RESTARTS AGEmyapp-deploy-tomcat-bdc87ddf7-69nsg 1/1 Running 0 31mmyapp-deploy-tomcat-bdc87ddf7-8xz6f 1/1 Running 0 31mmyapp-deploy-tomcat-bdc87ddf7-wpt7s 1/1 Running 0 31mmyapp-pod-tomcat 1/1 Running 0 2m9s# kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes ClusterIP 10.96.0.1 <none> 443/TCP 35mmyapp-service-tomcat ClusterIP 10.103.250.70 <none> 8080/TCP 32m#kubectl exec myapp-pod-tomcat -it -- /bin/bashroot@myapp-pod-tomcat:/usr/local/tomcat# curl 10.103.250.70:8080<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <title>......
- 本机访问 TomcatService
# curl localhost:8080curl: (7) Failed to connect to localhost port 8080
NodePort
根据上述的分析和总结,我们基本明白了: Service 的 Cluster IP 属于 Kubernetes 集群内部的地址,无法在集群外部直接使用这个地址。那么矛盾来了,实际上我们开发的许多业务中肯定有一部分服务是要提供给 Kubernetes 集群外部的应用或者用户来访问的,典型的就是 Web 端的服务模块,比如上面的 tomcat-server ,因此我们在发布 Service 的时候,可以采用 NodePort 的方式。
创建第一个NodePort
NodePort 的实现方式是在 Kubernetes 集群中的每个 Node 上为需要外部访问的 Service 开启一个对应的 TCP 监听端口,外部系统只要使用任意一个 Node 的 Ip +具体的 NodePort 端口号即可访问此服务。
- 创建 TomcatService
# vim myapp-node-port-tomcat.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: myapp-deploy-tomcat namespace: defaultspec: replicas: 3 selector: matchLabels: app: myapp-tomcat template: metadata: labels: app: myapp-tomcat spec: containers: - name: myapp-tomcat image: tomcat imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: myapp-service-tomcat namespace: defaultspec: type: NodePort selector: app: myapp-tomcat ports: - name: http port: 8080 targetPort: 8080
- 本地测试
# kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes ClusterIP 10.96.0.1 <none> 443/TCP 47mmyapp-service-tomcat NodePort 10.96.174.55 <none> 8080:30038/TCP 5m45s# curl localhost:30038<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <title>Apache Tomcat/8.5.47</title> <link href="f......
LoadBalancer
但 NodePort 还没有完全解决外部访问 Service 的所有问题,比如负载均衡问题,假如我们的额集群中有10个 Node ,则此时最好有一个负载均衡器,外部的请求只需访问此负载均衡器的 IP 地址,又负载均衡器负责转发流量到后面某个 Node 的 NodePort 上,如下图所示。
上图中的 Load Balancer 组件独立于 Kubernetes 集群之外,通常是一个硬件的负载均衡器,或者是以软件方式实现的,例如 HAProxy 或者 Nginx 。对于每个 Service ,我们通常需要一个对应的 Load Balancer 实例来转发流量到后端的 Node 上,这的确增加了工作量和出错的概率。于是 Kubernetes 提出了自动化的方案,如果我们的集群运行在谷歌的 GCE 公有云上,那么我们只要把 Service 的 type=NodePort 改成 Node=LoadBalancer ,此时 Kubernetes 会自动创建一个对应的 Load Balancer 实例并返回它的 IP 地址供外部客户端使用。其他公有云提供商只要实现了支持此特性的驱动,则也可以达到上述目的。此外,裸机上的类似机制( Bare Meta Service Load Balancers )也正在被开发中。
存储
对于服务,我们经常将其分为两大类:有状态服务、无状态服务。有状态服务常见的例如 DBMS 等,有状态服务常见的例如调度器, Apache 等。对于 Docker 来说,其更适应于无状态服务,但是 Kubernetes 的目标是作为未来基础设施的平台,其必须要攻克有状态服务,那有状态服务有些数据需要持久化,需要保存起来,因此, kubernetes 引入了多种存储,例如 ConfigMap (专门用来存储配置文件,就像配置文件中心)、 Secret (存储一些需要加密的数据)、 Volumn (用来存储一些数据)、 PV ( Persistent Volumn ,一个动态的存储)。
ConfigMap
ConfigMap 功能在 Kubernetes 1.2版本中引入,许多应用程序会从配置文件、命令行参数或者环境变量中读取配置信息。 ConfigMap API 给我们提供了想容器中注入配置信息的机制, ConfigMap 可以被用来保存单个属性,也可以用来保存整个配置文件或者 JSON 二进制大对象。
ConfigMap的创建
ConfigMap 可以使用目录创建、使用文件创建、使用字面值创建。
- 使用目录创建
# ls ./configmapa.propertiesb.properties# cat a.propertiesa1=a1a2=a2# cat b.propertiesb1=b1b2=b2# kubectl creatte configmap my-config --from-file=./configmap
-from-file 指定在目录下的所有文件都会在 ConfigMap 里面创建一个键值对,键的名字就是文件名,值就是文件的内容 *
- 使用文件创建
只要指定一个文件就可以从单个文件中创建 ConfigMap
# kubectl create configmap my-config --from-file=./configmap/a.properties
-from-file 这个参数可以使用多次,你可以使用两次分别制定上个例子中的那两个配置文件,效果就跟指定整个目录是一样的。
- 使用字面值创建
使用字面值创建,利用 -from-litteral 参数传递配置信息,该参数可以使用多次,格式如下
# kubectl create configmap my-config --from-literal=log.level=warn
Pod中使用ComfigMap
- 使用 ConfigMap 来代替环境变量
# vim configmap-env.yamlapiVersion: v1kind: ConfigMapmetadata: name: special -config namespace: defaultdata: special.how: very special.type: charm---apiVersion: v1kind: ConfigMapmetadata: name: env-config namespace: defaultdata: log_level: INFO---apiVersion: v1kind: Podmetadata: name: configmap-testspec: containers: - name: configmap-test image: tomcat command: ["/bin/sh","-c","env"] env: - name: SPECIAL_LEVLE_KEY valueFrom: configMapKeyRef: name: special-config key: special.how - name: SPECIAL_TYPE_KEY valueFrom: configMapKeyRef: name: special-config key: special.type envFrom: - configMapRef: name: env-config restartPolicy: Never# kubectl get cmNAME DATA AGEenv-config 1 3m24sspecial-config 2 3m24s# kubectl get podNAME READY STATUS RESTARTS AGEconfigmap-test 0/1 Completed 0 3m27s# kubectl log configmap-testlog_level=INFOSPECIAL_TYPE_KEY=charmSPECIAL_LEVLE_KEY=very
- 通过数据卷插件来使用 ConfigMap
# vim configmap-volumn.yamlapiVersion: v1kind: ConfigMapmetadata: name: special-config namespace: defaultdata: special.how: very special.type: charm---apiVersion: v1kind: Podmetadata: name: configmap-volumnspec: containers: - name: configmap-volumn-test image: tomcat command: ["/bin/sh","-c","sleep 6000s"] volumeMounts: - name: config-volumn mountPath: /etc/config volumes: - name: config-volumn configMap: name: special-config restartPolicy: Never# kubectl exec configmap-volumn -it -- /bin/bashroot@configmap-volumn:/etc/config# ls /etc/configspecial.how special.typeroot@configmap-volumn:/etc/config# cat /etc/config/special.howvery
Secret
Secret 解决了密码、 token 、密钥等敏感数据的配置,而不需要把这些敏感数据暴露到镜像或者 Pod Spec 中。
Secret 可以以 Volumn 或者环境变量的方式使用
Secret 有三种类型:
- ServiceAccount :用来访问 Kubernetes API ,由 Kubernetes 自动创建,并且会自动挂载到 Pod 的 /run/secrets/kubernetes.io/serviceaccount 目录中;
- Opaque : base64 编码格式的 Secret ,用来存储密码、密钥等;
- kubernetes.io/dockerconfigjson :用来存储私有 docker registry 的认证信息。
Opaque Secret
- 创建及使用
Opque 类型的数据是一个 map 类型,要求 value 是 base64 编码格式:
# echo -n "admin"|base64YWRtaW4=# echo -n "guoyuan_password"|base64Z3VveXVhbl9wYXNzd29yZA==# vim secret.yamlapiVersion: v1kind: Secretmetadata: name: mysecrettype: Opaquedata: username: YWRtaW4= password: Z3VveXVhbl9wYXNzd29yZA==---apiVersion: v1kind: Podmetadata: name: secret-testspec: volumes: - name: secrets secret: secretName: mysecret containers: - image: tomcat name: db volumeMounts: - name: secrets mountPath: "/etc/secrets" readOnly: true#kubectl exec secret-test -it -- /bin/bashroot@secret-test:/usr/local/tomcat# cat /etc/secrets/usernameadminroot@secret-test:/usr/local/tomcat# cat /etc/secrets/passwordguoyuan_password
Volumn
容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃时, Kubelet 会重启它,但是容器中的文件将丢失–容器以干净的状态(镜像最初的状态)重新启动。其次,在 Pod 中同时运行多个容器时,这些容器之间通常需要共享文件。 Kubernetes 中的 Volumn 抽象就很好的解决了这些问题。目前, Kubernetes 支持多种类型的 Volumn ,例如 GlusterFs 、 Ceph 等先进的分布式文件系统。
Volumn 的使用也比较简单,在大多数情况下,我们现在Pod上声明一个 Volumn ,然后在容器里引用该 Volumn 并 Mount 到容器里的某个目录上。举例来说,我们要给之前 Tomcat Pod 增加一个名字为 dataVol 的 Volumn ,并且 Mount 到容器的 /mydata-data 目录下,则只要对 Pod 的定义文件做如下修改即可
spec: volumns: - name: datavol emptyDir: {} containers: - name: tomcat iamge: tomcat volumeMounts: - mountPath: /mydata-data name: datavol
上面例子中 emptyDir 是 Volumn 的一种类型, Kubernets 提供了丰富的 Volumn 类型。
emptyDir
一个 emptyDir Volumn 是在 Pod 分配到 Node 时创建的,从它的名字可以看出,他的初始化内容为空,并且无需指定宿主机上对应的目录文件,因为这是 Kubernetes 自动分配的一个目录,当 Pod 从 Node 上移除时, emptyDir 中的数据也会被永久删除。
hostPath
hostPath 为在 Pod 上挂载宿主机上的文件或目录。在下面的例子中使用宿主机的 /data 定义了一个 hostPath 类型的 Volumn :
volumes:- name: "persistent-storage" hostPath: path: "/data"
Persistent Volumn
之前我们提到的 Volumn 是定义在 Pod 上的,属于“计算资源”的一部分,而实际上,“网络存储”是相对独立于“计算资源”而存在的一种实体资源。比如在使用虚拟机的情况下,我们通常会先定义一个网络存储,然后从中划出一个“网盘”并挂在到虚拟机上。 Persistent Volumn (简称 PV )和与之关联的 Persistent Volumn Clain (简称 PVC )也起到了类似的作用。
PV 可以理解成 Kubernetes 集群中的某个网络存储中对应的一块存储,它与 Volumn 很类似,但有以下区别。
- PV 只能是网络存储,不属于任何 Node ,但可以在每个 Node 上访问。
- PV 并不是定义在 Pod 上的,而是独立于 Pod 之外定义。
- PV 目前只有一种类型: GCE Persistent Disks 、 NFS 、 RBD 、 iSCSCI 、 AWS ElasticBlockStore 、 CluserFS 等。
下面给出了 NFS 类型 PV 的一个 yaml 定义声明,声明了需要 5Gi 的存储空间:
apiVersion: v1kind: PersistentVolumnmetadata: name: pv0003spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce nfs: path: /somepath server: 172.17.0.2
如果某个 Pod 想申请某种条件的 PV ,则首先需要定义一个 PersistentVolumnClaim ( PVC )对象:
kind: PersistentVolumnClaimapiVersion: v1metadata: name: myclaimspec: accessModes: - ReadWriteOnce resources: requests: storage: 8Gi
然后在 Pod 的 Volumn 定义中引用上述 PVC 即可:
volumes:- name: mypd persistentVolumeClaim: claimName: myclaim