Open WangShuXian6 opened 5 years ago
集群的 Master 包含着几个重要的组成部分,比如 API Server, Controller Manager 等。
而 Node 上,则运行着三个必要的组件 kubelet, container runtime (一般是 Docker), kube-proxy 。
通过所有组件的分工协作,最终实现了 K8S 对容器的编排和调度。
Node 用于工作的服务器,它有一些状态和信息,当这些条件都满足一些条件判断时,Node 便处于 Ready 状态,可用于执行后续的工作。 可以是一台物理机,也可以是虚拟机
Deployment 对期望状态的描述,能提供对 index.html 访问的服务
Pod 作为集群中可调度的最小单元,Nginx 和 index.html 这个组合 Pod 可以是一组容器(也可以包含存储卷)
Docker 是我们选择的容器运行时,可运行我们构建的服务镜像,减少在环境方面所做的重复工作,并且也非常便于部署。
Container Runtime
K8S 整体上遵循 C/S 架构,
左侧是一个官方提供的名为 kubectl 的 CLI (Command Line Interface)工具,用于使用 K8S 开放的 API 来管理集群和操作对象等。
右侧则是 K8S 集群的后端服务及开放出的 API 等。根据上一节的内容,我们知道 Node 是用于工作的机器,而 Master 是一种角色(Role),表示在这个 Node 上包含着管理集群的一些必要组件。
当然在这里,只画出了一个 Master,在生产环境中,为了保障集群的高可用,我们通常会部署多个 Master 。
整个 K8S 集群的“大脑”
- 接收:外部的请求和集群内部的通知反馈
- 发布:对集群整体的调度和管理
- 存储:存储
这些功能,也通过一些组件来共同完成,通常情况下称为 control plane
Cluster state store API Server Controller Manager Scheduler Node Kubelet Container runtime Kube Proxy
存储集群所有需持久化的状态,并且提供 watch 的功能支持,可以快速的通知各组件的变更等操作。
因为目前 Kubernetes 的存储层选择是 etcd ,所以一般情况下,大家都直接以 etcd 来代表集群状态存储服务。即:将所有状态存储到 etcd 实例中。
整个集群的入口,接收外部的信号和请求,并将一些信息写入到 etcd 中。
处理逻辑
- 请求 API Server :“嗨,我有些东西要放到 etcd 里面”
- API Server 收到请求:“你是谁?我为啥要听你的”
- 从请求中,拿出自己的身份凭证(一般是证书):“是我啊,你的master,给我把这些东西放进去” 这时候就要看是些什么内容了,如果这些内容 API Server 能理解,那就放入 etcd 中 “好的 master 我放进去了”;如果不能理解,“抱歉 master 我理解不了”
提供了认证相关的功能,用于判断是否有权限进行操作。 支持多种认证方法,不过一般情况下,我们都使用 x509 证书进行认证。 API Server 的目标是成为一个极简的 server,只提供 REST 操作,更新 etcd ,并充当着集群的网关。至于其他的业务逻辑之类的,通过插件或者在其他组件中完成。
在后台运行着许多不同的控制器进程,用来调节集群的状态。 当集群的配置发生变更,控制器就会朝着预期的状态开始工作。
集群的调度器,它会持续的关注集群中未被调度的 Pod ,并根据各种条件,比如资源的可用性,节点的亲和性或者其他的一些限制条件,通过绑定的 API 将 Pod 调度/绑定到 Node 上。
在这个过程中,调度程序一般只考虑调度开始时, Node 的状态,而不考虑在调度过程中 Node 的状态变化 ,比如节点亲和性等
加入集群中的机器
运行在 Node 上的几个核心组件
Kubelet 实现了集群中最重要的关于 Node 和 Pod 的控制功能,如果没有 Kubelet 的存在,那 Kubernetes 很可能就只是一个纯粹的通过 API Server CRUD 的应用程序。
K8S 原生的执行模式是操作应用程序的容器,而不像传统模式那样,直接操作某个包或者是操作某个进程。基于这种模式,可以让应用程序之间相互隔离,互不影响。此外,由于是操作容器,所以应用程序可以说和主机也是相互隔离的,毕竟它不依赖于主机,在任何的容器运行时(比如 Docker)上都可以部署和运行。
Pod 可以是一组容器(也可以包含存储卷),K8S 将 Pod 作为可调度的基本单位, 分离开了构建时和部署时的关注点:
- 构建时,重点关注某个容器是否能正确构建,如何快速构建
- 部署时,关心某个应用程序的服务是否可用,是否符合预期,依赖的相关资源是否都能访问到
这种隔离的模式,可以很方便的将应用程序与底层的基础设施解耦,极大的提高集群扩/缩容,迁移的灵活性。
Master 节点的 Scheduler 组件,它会调度未绑定的 Pod 到符合条件的 Node 上,而至于最终该 Pod 是否能运行于 Node 上,则是由 Kubelet 来裁定的。
容器运行时最主要的功能是下载镜像和运行容器,我们最常见的实现可能是 Docker , 目前还有其他的一些实现,比如 rkt, cri-o。
K8S 提供了一套通用的容器运行时接口 CRI (Container Runtime Interface), 凡是符合这套标准的容器运行时实现,均可在 K8S 上使用。
每个 Pod 在创建后都会有一个虚拟 IP,
K8S 中有一个抽象的概念,叫做 Service ,kube-proxy 便是提供一种代理的服务,让你可以通过 Service 访问到 Pod。
实际的工作原理是在每个 Node 上启动一个 kube-proxy 的进程,通过编排 iptables 规则来达到此效果。
K8S 的整体遵循 C/S 架构,集群的 Master 包含着几个重要的组成部分,比如 API Server, Controller Manager 等。
而 Node 上,则运行着三个必要的组件 kubelet, container runtime (一般是 Docker), kube-proxy 。
通过所有组件的分工协作,最终实现了 K8S 对容器的编排和调度。
自动调度、配置、监管和故障处理
Kubernetes使开发者可以自主部署应用,并且控制部署的频率,完全脱离运维团队的帮助。
Kubernetes同时能让运维团队监控整个系统,并且在硬件故障时重新调度应用。系统管理员的工作重心,从监管应用转移到了监管Kubernetes,以及剩余的系统资源,因为Kubernetes会帮助监管所有的应用。
Kubernetes是希腊语中的“领航员”或“舵手”的意思。
Kubernetes有几种不同的发音方式。许多人把它读成Koo-ber-nay-tace,还有一些人读成Koo-bernetties。
Kubernetes抽象了数据中心的硬件基础设施,使得对外暴露的只是一个巨大的资源池。它让我们在部署和运行组件时,不用关注底层的服务器。使用Kubernetes部署多组件应用时,它会为每个组件都选择一个合适的服务器,部署之后它能够保证每个组件可以轻易地发现其他组件,并彼此之间实现通信。
单体应用中的组件与独立的微服务
单体应用由很多个组件组成,这些组件紧密地耦合在一起,由于它们在同一个操作系统进程中运行,所以在开发、部署、管理的时候必须以同一个实体进行
将复杂的大型单体应用,拆分为小的可独立部署的微服务组件。每个微服务以独立的进程运行,并通过简单且定义良好的接口(API)与其他的微服务通信。
服务之间可以通过类似HTTP这样的同步协议通信,或者通过像AMQP这样的异步协议通信。
这些协议能够被大多数开发者所理解,并且并不局限于某种编程语言。
这意味着任何一个微服务,都可以用最适合的开发语言来实现。
因为每个微服务都是独立的进程,提供相对静态的API,所以独立开发和部署单个微服务成了可能。
只要API不变或者向前兼容,改动一个微服务,并不会要求对其他微服务进行改动或者重新部署。
面向单体系统,扩容针对的是整个系统,而面向微服务架构,扩容却只需要针对单个服务,这意味着你可以选择仅扩容那些需要更多资源的服务而保持其他的服务仍然维持在原来的规模。
当组件数量增加时,部署相关的决定就变得越来越困难。因为不仅组件部署的组合数在增加,而且组件间依赖的组合数也在以更大的因素增加。
微服务以团队形式完成工作,所以需要找到彼此进行交流。部署微服务时,部署者需要正确地配置所有服务来使其作为一个单一系统能正确工作
因为跨了多个进程和机器,使得调试代码和定位异常调用变得困难。幸运的是,这些问题现在已经被诸如Zipkin这样的分布式定位系统解决。
一个微服务架构中的组件不仅被独立部署,也被独立开发
不同的团队开发不同的组件是很正常的事实
因为组件之间依赖的差异性,应用程序需要同一个库的不同版本是不可避免的。
部署动态链接的应用需要不同版本的共享库,或者需要其他特殊环境,在生产服务器部署并管理这种应用很快会成为运维团队的噩梦。
多个应用在同一个主机上运行可能会有依赖冲突
为了减少仅会在生产环境才暴露的问题,最理想的做法是让应用在开发和生产阶段可以运行在完全一样的环境下,它们有完全一样的操作系统、库、系统配置、网络环境和其他所有的条件
在过去,开发团队的任务是创建应用并交付给运维团队,然后运维团队部署应用并使它运行。
但是现在,公司都意识到,让同一个团队参与应用的开发、部署、运维的整个生命周期更好。这意味着开发者、QA和运维团队彼此之间的合作需要贯穿整个流程。这种实践被称为DevOps。
运维团队负责管理生产部署流程及应用所在的硬件设备。他们关心系统安全、使用率,以及其他对于开发者来说优先级不高的东西。
开发者是部署程序本身,不需要知道硬件基础设施的任何情况,也不需要和运维团队交涉,这被叫作NoOps
Kubernetes能让我们实现所有这些想法。通过对实际硬件做抽象,然后将自身暴露成一个平台,用于部署和运行应用程序。它允许开发者自己配置和部署应用程序,而不需要系统管理员的任何帮助,让系统管理员聚焦于保持底层基础设施运转正常的同时,不需要关注实际运行在平台上的应用程序。
系统由一个主节点和若干个工作节点组成。
开发者把一个应用列表提交到主节点,Kubernetes会将它们部署到集群的工作节点。组件被部署在哪个节点对于开发者和系统管理员来说都不用关心。
开发者能指定一些应用必须一起运行,Kubernetes将会在一个工作节点上部署它们。其他的将被分散部署到集群中
Kubernetes可以被当作集群的一个操作系统来看待。
它降低了开发者不得不在他们的应用里实现一些和基础设施相关服务的心智负担。
他们现在依赖于Kubernetes来提供这些服务,包括服务发现、扩容、负载均衡、自恢复,甚至领导者的选举。
应用程序开发者因此能集中精力实现应用本身的功能而不用浪费时间思索怎样集成应用与基础设施。
Kubernetes将你的容器化应用运行在集群的某个地方,并提供信息给应用组件来发现彼此并保证它们的运行。因为你的应用程序不关心它运行在哪个节点上,Kubernetes能在任何时间迁移应用并通过混合和匹配应用来获得比手动调度高很多的资源利用率。
在硬件级别,一个Kubernetes集群由很多节点组成,这些节点被分成以下两种类型:
主节点,它承载着Kubernetes控制和管理整个集群系统的控制面板
工作节点,它们运行用户实际部署的应用
组成一个Kubernetes集群的组件
控制面板用于控制集群并使它工作。它包含多个组件,组件可以运行在单个主节点上或者通过副本分别部署在多个主节点以确保高可用性。这些组件是:
Kubernetes API服务器,你和其他控制面板组件都要和它通信
Scheculer,它调度你的应用(为应用的每个可部署组件分配一个工作节点)
Controller Manager,它执行集群级别的功能,如复制组件、持续跟踪工作节点、处理节点失败等
etcd,一个可靠的分布式数据存储,它能持久化存储集群配置
控制面板的组件持有并控制集群状态,但是它们不运行你的应用程序。这是由工作节点完成的。
工作节点是运行容器化应用的机器。运行、监控和管理应用服务的任务是由以下组件完成的:
Docker、rtk或其他的容器类型
Kubelet,它与API服务器通信,并管理它所在节点的容器
Kubernetes Service Proxy(kube-proxy),它负责组件之间的负载均衡网络流量
为了在Kubernetes中运行应用,首先需要将应用打包进一个或多个容器镜像,再将那些镜像推送到镜像仓库,然后将应用的描述发布到Kubernetes API服务器。
该描述包括诸如容器镜像或者包含应用程序组件的容器镜像、这些组件如何相互关联,以及哪些组件需要同时运行在同一个节点上和哪些组件不需要同时运行等信息。
此外,该描述还包括哪些组件为内部或外部客户提供服务且应该通过单个IP地址暴露,并使其他组件可以发现。
当API服务器处理应用的描述时,调度器调度指定组的容器到可用的工作节点上,
调度是基于每组所需的计算资源,以及调度时每个节点未分配的资源。
然后,那些节点上的Kubelet指示容器运行时(例如Docker)拉取所需的镜像并运行容器。
仔细看图1.10以更好地理解如何在Kubernetes中部署应用程序。应用描述符列出了四个容器,并将它们分为三组(这些集合被称为pod,我们将在第3章中解释它们是什么)。前两个pod只包含一个容器,而最后一个包含两个。这意味着两个容器都需要协作运行,不应该相互隔离。在每个pod旁边,还可以看到一个数字,表示需要并行运行的每个pod的副本数量。在向Kubernetes提交描述符之后,它将把每个pod的指定副本数量调度到可用的工作节点上。节点上的Kubelets将告知Docker从镜像仓库中拉取容器镜像并运行容器。
一旦应用程序运行起来,Kubernetes就会不断地确认应用程序的部署状态始终与你提供的描述相匹配。
例如,如果你指出你需要运行五个web服务器实例,那么Kubernetes总是保持正好运行五个实例。
如果实例之一停止了正常工作,比如当进程崩溃或停止响应时,Kubernetes将自动重启它。
如果整个工作节点死亡或无法访问,Kubernetes将为在故障节点上运行的所有容器选择新节点,并在新选择的节点上运行它们
图1.10 Kubernetes体系结构的基本概述和在它之上运行的应用程序
当应用程序运行时,可以决定要增加或减少副本量,而Kubernetes将分别增加附加的或停止多余的副本。
甚至可以把决定最佳副本数目的工作交给Kubernetes。
它可以根据实时指标(如CPU负载、内存消耗、每秒查询或应用程序公开的任何其他指标)自动调整副本数。
Kubernetes可能需要在集群中迁移你的容器。当它们运行的节点失败时,或者为了给其他容器腾出地方而从节点移除时,就会发生这种情况。如果容器向运行在集群中的其他容器或者外部客户端提供服务,那么当容器在集群内频繁调度时,它们该如何正确使用这个容器?当这些容器被复制并分布在整个集群中时,客户端如何连接到提供服务的容器呢?
为了让客户能够轻松地找到提供特定服务的容器,可以告诉Kubernetes哪些容器提供相同的服务,而Kubernetes将通过一个静态IP地址暴露所有容器,并将该地址暴露给集群中运行的所有应用程序。这是通过环境变量完成的,但是客户端也可以通过良好的DNS查找服务IP。kube-proxy将确保到服务的连接可跨提供服务的容器实现负载均衡。服务的IP地址保持不变,因此客户端始终可以连接到它的容器,即使它们在集群中移动。
如果在所有服务器上部署了Kubernetes,那么运维团队就不需要再部署应用程序。因为容器化的应用程序已经包含了运行所需的所有内容,系统管理员不需要安装任何东西来部署和运行应用程序
由于Kubernetes将其所有工作节点公开为一个部署平台,因此应用程序开发人员可以自己开始部署应用程序,不需要了解组成集群的服务器
所有节点都是一组等待应用程序使用它们的计算资源
通过在服务器上装配Kubernetes,并使用它运行应用程序而不是手动运行它们,你已经将应用程序与基础设施分离开来。当你告诉Kubernetes运行你的应用程序时,你在让它根据应用程序的资源需求描述和每个节点上的可用资源选择最合适的节点来运行你的应用程序。
可以随时在集群中移动应用程序的能力,使得Kubernetes可以比人工更好地利用基础设施。人类不擅长寻找最优的组合
Kubernetes监控你的应用程序组件和它们运行的节点,并在节点出现故障时自动将它们重新调度到其他节点。
这使运维团队不必手动迁移应用程序组件,并允许团队立即专注于修复节点本身,并将其修好送回到可用的硬件资源池中,而不是将重点放在重新定位应用程序上。
如果你的基础设施有足够的备用资源来允许正常的系统运行,即使故障节点没有恢复,运维团队甚至不需要立即对故障做出反应,比如在凌晨3点
运维团队不需要不断地监控单个应用程序的负载,以对突发负载峰值做出反应。
可以告诉Kubernetes监视每个应用程序使用的资源,并不断调整每个应用程序的运行实例数量。
如果Kubernetes运行在云基础设施上,在这些基础设施中,添加额外的节点就像通过云供应商的API请求它们一样简单,那么Kubernetes甚至可以根据部署的应用程序的需要自动地将整个集群规模放大或缩小。
应用程序开发和生产流程中都运行在同一个环境中,这对发现bug有很大的影响。我们都同意越早发现一个bug,修复它就越容易,修复它需要的工作量也就越少。由于是在开发阶段就修复bug,所以这意味着他们的工作量减少了。
开发人员不需要实现他们通常会实现的特性。这包括在集群应用中发现服务和对端。这是由Kubernetes来完成的而不是应用。通常,应用程序只需要查找某些环境变量或执行DNS查询。如果这还不够,应用程序可以直接查询Kubernetes API服务器以获取该信息和其他信息。像这样查询Kubernetes API服务器,甚至可以使开发人员不必实现诸如复杂的集群leader选举机制。
开发者们的信心增加。当他们知道,新版本的应用将会被推出时Kubernetes可以自动检测一个应用的新版本是否有问题,如果是则立即停止其滚动更新,这种信心的增强通常会加速应用程序的持续交付,这对整个组织都有好处。
创建一个简单的应用,把它打包成容器镜像并在远端的Kubernetes集群(如托管在Google Kubernetes Engine中)或本地单节点集群中运行
在Kubernetes中运行应用需要打包好的容器镜像
按照 http://docs.docker.com/engine/installation/ 上的指南安装Docker
Docker hub中有许多随时可用的常见镜像,其中就包括 busybox,可以用来运行简单的 echo"Hello world" 命令。
busybox是一个单一可执行文件,包含多种标准UNIX命令行工具,如:echo、ls、gzip 等
使用Docker运行一个Hello world容器
docker run busybox echo "hello world"
目前的应用是单一可执行文件(busybox),但也可以是一个有许多依赖的复杂应用。整个配置运行应用的过程是完全一致的。同样重要的是应用是在容器内部被执行的,完全独立于其他所有主机上运行的进程。
图 2.1 展示了执行 docker run 命令之后发生的事情。
首先,Docker会检查busybox:latest 镜像是否已经存在于本机。
如果没有,Docker会从http://docker.io的Docker镜像中心拉取镜像。
镜像下载到本机之后,Docker基于这个镜像创建一个容器并在容器中运行命令。
echo 命令打印文字到标准输出流,然后进程终止,容器停止运行。
在一个基于busybox镜像的容器中运行echo “Hello world”
所有的软件包都会更新,所以通常每个包都不止一个版本。
Docker支持同一镜像的多个版本。每一个版本必须有唯一的tag名。
当引用镜像没有显式地指定tag时,Docker会默认指定tag为latest。
如果想要运行别的版本的镜像,需要像这样指定镜像的版本:
docker run <image>:<tag>
构建一个简单的Node.js Web应用,并把它打包到容器镜像中。这个应用会接收HTTP请求并响应应用运行的主机名
构建一个简单的Node.js Web应用,并把它打包到容器镜像中。这个应用会接收HTTP请求并响应应用运行的主机名
一个简单的Node.js应用:app.js
const http = require('http');
const os = require('os');
console.log("Kubia server starting...");
var handler = function(request, response) {
console.log("Received request from " + request.connection.remoteAddress);
response.writeHead(200);
response.end("You've hit " + os.hostname() + "\n");
};
var www = http.createServer(handler);
www.listen(8080);
在8080端口启动了一个HTTP服务器。服务器会以状态码 200 OK 和文字 "You've hit
"来响应每个请求。请求handler会把客户端的IP打印到标准输出,以便日后查看。
为了把应用打包成镜像,首先需要创建一个叫Dockerfile的文件,它包含了一系列构建镜像时会执行的指令。Dockerfile文件需要和app.js文件在同一目录
构建应用容器镜像的Dockerfile
FROM node:7
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]
From 行定义了镜像的起始内容(构建所基于的基础镜像)。这个例子中使用的是 node 镜像的tag 7 版本。
第二行中把app.js文件从本地文件夹添加到镜像的根目录,保持app.js这个文件名。
最后一行定义了当镜像被运行时需要被执行的命令,这个例子中,命令是 node app.js。
有了Dockerfile和app.js文件,这是用来构建镜像的所有文件
构建镜像
docker build -t kubia .
用户告诉Docker需要基于当前目录(注意命令结尾的点)构建一个叫kubia的镜像,Docker会在目录中寻找Dockerfile,然后基于其中的指令构建镜像。
基于Dockerfile构建一个新的容器镜像
构建过程不是由Docker客户端进行的,而是将整个目录的文件上传到Docker守护进程并在那里进行的。
Docker客户端和守护进程不要求在同一台机器上
不要在构建目录中包含任何不需要的文件,这样会减慢构建的速度——尤其当Docker守护进程运行在一个远端机器的时候。
在构建过程中,Docker首次会从公开的镜像仓库(Docker Hub)拉取基础镜像(node:7),除非已经拉取过镜像并存储在本机上了。
镜像不是一个大的二进制块,而是由多层组成的
不同镜像可能会共享分层,这会让存储和传输变得更加高效
所有组成基础镜像的分层只会被存储一次。
拉取镜像的时候,Docker会独立下载每一层。一些分层可能已经存储在机器上了,所以Docker只会下载未被存储的分层。
构建镜像时,Dockerfile中每一条单独的指令都会创建一个新层。镜像构建的过程中,拉取基础镜像所有分层之后,Docker在它们上面创建一个新层并且添加app.js。然后会创建另一层来指定镜像被运行时所执行的命令。最后一层会被标记为kubia:latest
构建完成时,新的镜像会存储在本地
docker run --name kubia-container -p 8080:8080 -d kubia
这条命令告知Docker基于 kubia 镜像创建一个叫 kubia-container 的新容器。
这个容器与命令行分离(-d 标志),这意味着在后台运行。
本机上的8080端口会被映射到容器内的8080端口(-p 8080:8080 选项),所以可以通过http://localhost:8080 访问这个应用。
curl localhost:8080
应用把 44d76963e8e1 作为主机名返回,这并不是宿主机的主机名。这个十六进制数是Docker容器的ID
docker ps
docker inspect kubia-container
由于一个容器里可以运行多个进程,所以总是可以运行新的进程去看看里面发生了什么。如果镜像里有可用的shell二进制可执行文件,也可以运行一个shell。
镜像基于的Node.js镜像包含了bash shell
docker exec -it kubia-container bash
这会在已有的kubia-container容器内部运行bash。bash 进程会和主容器进程拥有相同的命名空间。
-it
选项是下面两个选项的简写:
-i
,确保标准输入流保持开放。需要在shell中输入命令。
-t
,分配一个伪终端(TTY)。如果希望像平常一样使用shell,需要同时使用这两个选项(如果缺少第一个选项就无法输入任何命令。如果缺少第二个选项,那么命令提示符不会显示,并且一些命令会提示 TERM 变量没有设置)。
从容器内列出进程
ps aux
容器内的进程运行在主机操作系统上
ps aux | grep app.js
运行在容器中的进程是运行在主机操作系统上的。如果你足够敏锐,会发现进程的ID在容器中与主机上不同。容器使用独立的PID Linux命名空间并且有着独立的系列号,完全独立于进程树。
容器的文件系统也是独立的
容器拥有完整的文件系统
可以使用 exit 命令来退出容器返回宿主机
通过告知Docker停止 kubia-container 容器来停止应用
docker stop kubia-container
因为没有其他的进程在容器内运行,这会停止容器内运行的主进程。容器本身仍然存在并且可以通过 docker ps-a 来查看。-a 选项打印出所有的容器,包括运行中的和已经停止的
真正地删除一个容器,需要运行 docker rm
docker rm kubia-container
这会删除容器,所有的内容会被删除并且无法再次启动
可以推送镜像到公开可用的Docker Hub(http://hub.docker.com)镜像中心。另外还有其他广泛使用的镜像中心,如Quay.io和Google Container Registry。
在推送之前,需要重新根据Docker Hub的规则标注镜像。Docker Hub允许向以你的Docker Hub ID开头的镜像仓库推送镜像
docker tag kubia yourid/kubia
这不会重命名标签,而是给同一个镜像创建一个额外的标签。可以通过docker images 命令列出本机存储的镜像来加以确认
一个容器镜像可以有多个标签
在向Docker Hub推送镜像之前,先需要使用 docker login 命令和自己的用户ID登录,然后就可以像这样向Docker Hub推送 yourid/kubia 镜像
docker push yourid/kubia
docker run -p 8080:8080 -d yourid/kubia
现在,应用被打包在一个容器镜像中,并通过Docker Hub给大家使用,可以将它部署到Kubernetes集群中,而不是直接在Docker中运行。但是需要先设置集群。
设置一个完整的、多节点的Kubernetes集群
一个适当的Kubernetes安装需要包含多个物理或虚拟机,并需要正确地设置网络,以便在Kubernetes集群内运行的所有容器都可以在相同的扁平网络环境内相互连通。
安装Kubernetes集群的方法有许多。这些方法在http://kubernetes.io的文档中有详细描述
Kubernetes可以在本地的开发机器、自己组织的机器集群或是虚拟机提供商(Google Compute Engine、Amazon EC2、Microsoft Azure等)上运行,或者使用托管的Kubernetes集群,如Google Kubernetes Engine(以前称为Google Container Engine)。
在本地机器上运行单节点Kubernetes集群
运行在Google Kubernetes Engine(GKE)上的托管集群。
使用 kubeadm 工具安装一个集群
在亚马逊的AWS(Amazon Web Services)上安装Kubernetes。为此,可以查看 kops 工具,它是在前面一段提到的 kubeadm 基础之上构建的,可以在http://github.com/kubernetes/kops中找到。它帮助你在AWS上部署生产级、高可用的Kubernetes集群,并最终会支持其他平台(Google Kubernetes Engine、VMware、vSphere等)。
Minikube是一个构建单节点集群的工具,对于测试Kubernetes和本地开发应用都非常有用。
Minikube是一个需要下载并放到路径中的二进制文件。
它适用于OSX、Linux和Windows系统。
最好访问GitHub上的Minikube代码仓库(http://github.com/kubernetes/minikube),按照说明来安装它
下载Minikube并进行设置
在Linux系统中,可以下载另一个版本(将URL中的 “darwin” 替换为“linux”)。在Windows系统中,可以手动下载文件,将其重命名为minikube.exe,并把它加到路径中。Minikube在VM中通过VirtualBox或KVM运行Kubernetes,所以在启动Minikube集群之前,还需要安装VM。
minikue start
启动集群需要花费超过一分钟的时间,所以在命令完成之前不要中断它
要与Kubernetes进行交互,还需要 kubectl CLI客户端。同样,需要做的就是下载它,并放在路径中。例如,OSX系统的最新稳定版本可以通过以下命令下载并安装:
要下载用于Linux或Windows系统的kubectl,用 linux 或 windows 替换URL中的 darwin
kubectl cluster-info
显示了各种Kubernetes组件的URL,包括API服务器和Web控制台。
可以运行 minikube ssh 登录到Minikube VM并从内部探索它。例如,可以查看在节点上运行的进程
探索一个完善的多节点Kubernetes集群,可以使用托管的Google Kubernetes Engine(GKE)集群。这样,无须手动设置所有的集群节点和网络
在设置新的Kubernetes集群之前,需要设置GKE环境。
阅读 https://cloud.google.com/containerengine/docs/before-begin 中的说明后就可以开始了。
过程
1.注册谷歌账户,如果你还没有注册过。
2.在Google Cloud Platform控制台中创建一个项目。
3.开启账单。这会需要你的信用卡信息,但是谷歌提供了为期12个月的免费试用。而且在免费试用结束后不会自动续费。
4.开启Kubernetes Engine API。
5.下载安装Google Cloud SDK(这包含 gcloud 命令行工具,需要创建一个Kubernetes集群)。
6.使用 gcloud components install kubectl 安装 kubectl 命令行工具。
注意 某些操作(例如步骤2中的操作)可能需要几分钟才能完成
在GKE上创建一个三节点集群
每个节点运行着Docker、Kubelet和kube-proxy。可以通过 kubectl 命令行客户端向运行在主节点上的Kubernetes API服务器发出REST请求以与集群交互。
如何与三节点Kubernetes集群进行交互
使用kubectl列出集群节点
kubectl get 命令可以列出各种Kubernetes对象
kubectl get nodes
可以使用 gcloud compute ssh
登录到其中一个节点,查看节点上运行了什么。
kubectl describe node gke-kubia-xxx-node-xxx
输出显示了节点的状态、CPU和内存数据、系统信息、运行容器的节点等。
在前面的 kubectl describe 示例中,显式地指定了节点的名称,但也可以执行一个简单的 kubectl describe node 命令,而无须指定节点名,它将打印出所有节点的描述信息。
kubectl 会被经常使用
为kubectl 设置别名和tab命令补全可让使用变得简单
将下面的代码添加到 ~/.bashrc 或类似的文件中:
alias k=kubectl
如果你已经在用 gcloud 配置集群,就已经有可执行文件 k 了
即使使用短别名k,仍然需要输入许多内容
kubectl命令还可以配置bash和zsh shell的代码补全。tab补全不仅可以补全命令名,还能补全对象名。
例如,无须在前面的示例中输入整个节点名,只需输入
kubectl desc<TAB> no<TAB> gke-ku<TAB>
需要先安装一个叫作 bashcompletion 的包来启用bash中的tab命令补全,然后可以运行接下来的命令(也需要加到 ~/.bashrc 或类似的文件中):
tab命令行补全只在使用完整的 kubectl 命令时会起作用(当使用别名 k 时不会起作用)。需要改变 kubectl completion 的输出来修复:
别名的shell命令补全在MacOS系统上并不起作用。如果需要使用命令行补全,就需要使用完整的 kubectl 命令
通常,需要准备一个JSON或YAML,包含想要部署的所有组件描述的配置文件,但是因为还没有介绍可以在Kubernetes中创建的组件类型,所以这里将使用一个简单的单行命令来运行应用。
部署应用程序最简单的方式是使用 kubectl run 命令,
该命令可以创建所有必要的组件而无需JSON或YAML文件。
试着运行之前创建、推送到Docker Hub的镜像。
下面是在Kubernetes中运行的代码
创建了一个名为kubia的ReplicationController
kubectl run kubia --image=luksa/kubia --port=8080 --generator=run/v1
--image=luksa/kubia
显示的是指定要运行的容器镜像,
--port=8080
选项告诉Kubernetes应用正在监听8080端口。
--generator
通常并不会使用到它,它让 Kubernetes 创建一个 ReplicationController,而不是 Deployment
Kubernetes 不直接处理单个容器。相反,它使用多个共存容器的理念。这组容器就叫作pod。
一个pod是一组紧密相关的容器,它们总是一起运行在同一个工作节点上,以及同一个Linux命名空间中。
每个pod就像一个独立的逻辑机器,拥有自己的IP、主机名、进程等,运行一个独立的应用程序。
应用程序可以是单个进程,运行在单个容器中,也可以是一个主应用进程或者其他支持进程,每个进程都在自己的容器中运行。
一个pod的所有容器都运行在同一个逻辑机器上,
而其他pod中的容器,即使运行在同一个工作节点上,也会出现在不同的节点上。
容器、pod及物理工作节点之间的关系
每个pod都有自己的IP,并包含一个或多个容器,每个容器都运行一个应用进程。pod分布在不同的工作节点上。
不能列出单个容器,因为它们不是独立的Kubernetes对象,但是可以列出pod
使用 kubectl 列出pod
kubectl get pods
pod仍然处于挂起状态,pod的单个容器显示为还未就绪的状态(这是 READY列中的 0/1的含义)。
pod还没有运行的原因是:该pod被分配到的工作节点正在下载容器镜像,完成之后才可以运行。下载完成后,将创建pod的容器,然后pod会变为运行状态
再次列出pod查看pod的状态是否变化
要查看有关pod的更多信息,还可以使用 kubectl describe pod 命令,就像之前查看工作节点一样。
如果pod停留在挂起状态,那么可能是Kubernetes无法从镜像中心拉取镜像。
如果你正在使用自己的镜像,确保它在Docker Hub上是公开的。
为了确保能够成功地拉取镜像,可以试着在另一台机器上使用 docker pull命令手动拉取镜像。
在Kubernetes中运行容器镜像所必需的两个步骤。
首先,构建镜像并将其推送到Docker Hub。这是必要的,因为在本地机器上构建的镜像只能在本地机器上可用,但是需要使它可以访问运行在工作节点上的Docker守护进程。
当运行 kubectl 命令时,它通过向Kubernetes API服务器发送一个REST HTTP请求,在集群中创建一个新的ReplicationController对象。然后,ReplicationController创建了一个新的pod,调度器将其调度到一个工作节点上。Kubelet看到pod被调度到节点上,就告知Docker从镜像中心中拉取指定的镜像,因为本地没有该镜像。下载镜像后,Docker创建并运行容器。
展示另外两个节点是为了显示上下文。它们没有在这个过程中扮演任何角色,因为pod没有调度到它们上面。
调度(scheduling)的意思是将pod分配给一个节点。pod会立即运行,而不是将要运行。
在Kubernetes中运行luksa/kubia容器镜像
如何访问正在运行的pod?
每个pod都有自己的IP地址,但是这个地址是集群内部的,不能从集群外部访问。
要让pod能够从外部访问,需要通过服务对象公开它,要创建一个特殊的 LoadBalancer 类型的服务。
因为如果你创建一个常规服务(一个 ClusterIP 服务),比如pod,它也只能从集群内部访问。
通过创建 LoadBalancer 类型的服务,将创建一个外部的负载均衡,可以通过负载均衡的公共IP访问pod。
要创建服务,需要告知Kubernetes对外暴露之前创建的ReplicationController:
kubectl expose rc kubia --type=LoadBalancer --name kubia-http
缩写
这里用的是 replicationcontroller 的缩写 rc。大多数资源类型都有这样的缩写,所以不必输入全名(例如,pods 的缩写是 po,service 的缩写是 svc,等等)
expose 命令的输出中提到一个名为kubian-http 的服务。
服务是类似于pod和Node的对象,因此可以通过运行 kubectl get services 命令查看新创建的服务对象
kubectl get services
该列表显示了两个服务。暂时忽略 kubernetes 服务,
仔细查看创建的kubian-http 服务。它还没有外部IP地址,因为Kubernetes运行的云基础设施创建负载均衡需要一段时间。
负载均衡启动后,应该会显示服务的外部IP地址。让我们等待一段时间并再次列出服务
现在有外部IP了,应用就可以从任何地方通过http://104.155.74.57:8080 访问。
注意 Minikube不支持 LoadBalancer 类型的服务,因此服务不会有外部IP。但是可以通过外部端口访问服务。在下一节的提示中将介绍这是如何做到的。
现在可以通过服务的外部IP和端口向pod发送请求:
提示 使用Minikube的时候,可以运行 minikube service kubia-http获取可以访问服务的IP和端口。
应用将pod名称作为它的主机名。如前所述,每个pod都像一个独立的机器,具有自己的IP地址和主机名。尽管应用程序运行在工作节点的操作系统中,但对应用程序来说,它似乎是在一个独立的机器上运行,而这台机器本身就是应用程序的专用机器,没有其他的进程一同运行。
到目前为止,主要介绍了系统实际的物理组件。三个工作节点是运行Docker和Kubelet的VM,还有一个控制整个系统的主节点。
正如前面解释过的,没有直接创建和使用容器。相反,Kubernetes的基本构件是pod。
但是,你并没有真的创建出任何pod,至少不是直接创建。
通过运行kubectl run 命令,创建了一个ReplicationController,它用于创建pod实例。
为了使该pod能够从集群外部访问,需要让Kubernetes将该ReplicationController管理的所有pod由一个服务对外暴露
由ReplicationController、pod和服务组成的系统
在你的系统中最重要的组件是pod。它只包含一个容器,但是通常一个pod可以包含任意数量的容器。
容器内部是Node.js进程,该进程绑定到8080端口,等待HTTP请求。
pod有自己独立的私有IP地址和主机名。
下一个组件是 kubia ReplicationController。它确保始终存在一个运行中的pod实例。
通常,ReplicationController用于复制pod(即创建pod的多个副本)并让它们保持运行。
示例中没有指定需要多少pod副本,所以ReplicationController创建了一个副本。
如果你的pod因为任何原因消失了,那么ReplicationController将创建一个新的pod来替换消失的pod。
系统的第三个组件是 kubian-http 服务。
要理解为什么需要服务,需要学习有关pod的关键细节。
pod的存在是短暂的,一个pod可能会在任何时候消失,或许因为它所在节点发生故障,或许因为有人删除了pod,或者因为pod被从一个健康的节点剔除了。
当其中任何一种情况发生时,如前所述,消失的pod将被ReplicationController替换为新的pod。
新的pod与替换它的pod具有不同的IP地址。这就是需要服务的地方——解决不断变化的pod IP地址的问题,以及在一个固定的IP和端口对上对外暴露多个pod。
当一个服务被创建时,它会得到一个静态的IP,在服务的生命周期中这个IP不会发生改变。
客户端应该通过固定IP地址连接到服务,而不是直接连接pod。
服务会确保其中一个pod接收连接,而不关心pod当前运行在哪里(以及它的IP地址是什么)。
服务表示一组或多组提供相同服务的pod的静态地址。
到达服务IP和端口的请求将被转发到属于该服务的一个容器的IP和端口。
现在有了一个正在运行的应用,由ReplicationController监控并保持运行,并通过服务暴露访问
pod由一个ReplicationController管理
kubectl get replicationcontrollers
该列表显示了一个名为kubia 的单个ReplicationController。DESIRED 列显示了希望ReplicationController保持的pod副本数,而 CURRENT 列显示当前运行的pod数。在示例中,希望pod副本为1,而现在就有一个副本正在运行。
为了增加pod的副本数,需要改变ReplicationController期望的副本数
kubectl scale rc kubia --replicas=3
现在已经告诉Kubernetes需要确保pod始终有三个实例在运行。
注意,你没有告诉Kubernetes需要采取什么行动,也没有告诉Kubernetes增加两个pod,只设置新的期望的实例数量并让Kubernetes决定需要采取哪些操作来实现期望的状态。
这是Kubernetes最基本的原则之一。不是告诉Kubernetes应该执行什么操作,而是声明性地改变系统的期望状态,并让Kubernetes检查当前的状态是否与期望的状态一致。在整个Kubernetes世界中都是这样的。
kubectl get rc
由于pod的实际数量已经增加到三个(从 CURRENT 列中可以看出),列出所有的pod时显示的应该是三个而不是一个: 有三个pod而不是一个。两个已经在运行,一个仍在挂起中,一旦容器镜像下载完毕并启动容器,挂起的pod会马上运行。
应用本身需要支持水平伸缩。
Kubernetes并不会让你的应用变得可扩展,它只是让应用的扩容或缩容变得简单。
为现在应用的多个实例在运行
请求随机地切换到不同的pod。当pod有多个实例时Kubernetes服务就会这样做。服务作为负载均衡挡在多个pod前面。当只有一个pod时,服务为单个pod提供一个静态地址。无论服务后面是单个pod还是一组pod,这些pod在集群内创建、消失,这意味着它们的IP地址会发生变化,但服务的地址总是相同的。这使得无论有多少pod,以及它们的地址如何变化,客户端都可以很容易地连接到pod
由同一ReplicationController管理并通过服务IP和端口暴露的pod的三个实例
每个pod都有自己的IP,并且可以与任何其他pod通信,不论其他pod是运行在同一个节点上,还是运行在另一个节点上。
每个pod都被分配到所需的计算资源,因此这些资源是由一个节点提供还是由另一个节点提供,并没有任何区别。
-o wide
可以使用-o wide 选项请求显示其他列。在列出pod时,该选项显示pod的IP和所运行的节点
kubectl get pods -o wide
使用kubectl describe描述一个pod
kubectl describe pod kubia-hczji
这展示pod的一些其他信息,pod调度到的节点、启动的时间、pod使用的镜像,以及其他有用的信息
探索Kubernetes集群的另一种方式
图形化的web用户界面
如果更喜欢图形化的web用户界面,Kubernetes也提供了一个不错的(但仍在开发迭代的)web dashboard
dashboard可以列出部署在集群中的所有pod、ReplicationController、服务和其他部署在集群中的对象,以及创建、修改和删除它们
如果你正在使用Google Kubernetes Engine,可以通过 kubectl clusterinfo 命令找到dashboard的URL
kubectl clusterinfo | grep dashboard
如果在浏览器中打开这个URL,将会显示用户名和密码提示符。可以运行以下命令找到用户名和密码
gcloud container clusters describe kubia | grep -E "(username | password):"
minikube dashboard
dashboard将在默认浏览器中打开。与GKE不同的是,不需要输入任何凭证来访问它
pod是Kubernetes中最为重要的核心概念,而其他对象仅仅是在管理、暴露pod或被pod使用
pod是一组并置的容器,代表了Kubernetes中的基本构建模块。
在实际应用中我们并不会单独部署容器,更多的是针对一组pod的容器进行部署和操作。
然而这并不意味着一个pod总是要包含多个容器——实际上只包含一个单独容器的pod也是非常常见的。
当一个pod包含多个容器时,这些容器总是运行于同一个工作节点上——一个pod绝不会跨越多个工作节点
一个pod的所有容器都运行在同一个节点上;一个pod绝不跨越两个节点
在Kubernetes中,我们经常在容器中运行进程,由于每一个容器都非常像一台独立的机器,此时你可能认为在单个容器中运行多个进程是合乎逻辑的,然而在实践中这种做法并不合理。
容器被设计为每个容器只运行一个进程(除非进程本身产生子进程)。
如果在单个容器中运行多个不相关的进程,那么保持所有进程运行、管理它们的日志等将会是我们的责任。
例如,我们需要包含一种在进程崩溃时能够自动重启的机制。
同时这些进程都将记录到相同的标准输出中,而此时我们将很难确定每个进程分别记录了什么。
综上所述,我们需要让每个进程运行于自己的容器中,而这就是Docker和Kubernetes期望使用的方式
由于不能将多个进程聚集在一个单独的容器中,我们需要另一种更高级的结构来将容器绑定在一起,并将它们作为一个单元进行管理,这就是pod背后的根本原理。
在包含容器的pod下,我们可以同时运行一些密切相关的进程,并为它们提供(几乎)相同的环境,此时这些进程就好像全部运行于单个容器中一样,同时又保持着一定的隔离。
这样一来,我们便能全面地利用容器所提供的特性,同时对这些进程来说它们就像运行在一起一样,实现两全其美。
容器之间彼此是完全隔离的,但此时我们期望的是隔离容器组,而不是单个容器,并让每个容器组内的容器共享一些资源,而不是全部(换句话说,没有完全隔离)
Kubernetes通过配置Docker来让一个pod内的所有容器共享相同的Linux命名空间,而不是每个容器都有自己的一组命名空间。
由于一个pod中的所有容器都在相同的network和UTS命名空间下运行(在这里我们讨论的是Linux命名空间),所以它们都共享相同的主机名和网络接口
同样地,这些容器也都在相同的IPC命名空间下运行,因此能够通过IPC进行通信。
在最新的Kubernetes和Docker版本中,它们也能够共享相同的PID命名空间,但是该特征默认是未激活的。
注意 当同一个pod中的容器使用单独的PID命名空间时,在容器中执行ps aux就只会看到容器自己的进程。
但当涉及文件系统时,情况就有所不同。由于大多数容器的文件系统来自容器镜像,因此默认情况下,每个容器的文件系统与其他容器完全隔离。
但我们可以使用名为Volume的Kubernetes资源来共享文件目录
由于一个pod中的容器运行于相同的Network命名空间中,因此它们共享相同的IP地址和端口空间。
这意味着在同一pod中的容器运行的多个进程需要注意不能绑定到相同的端口号,否则会导致端口冲突,
但这只涉及同一pod中的容器。由于每个pod都有独立的端口空间,对于不同pod中的容器来说则永远不会遇到端口冲突。此外,一个pod中的所有容器也都具有相同的loopback网络接口,因此容器可以通过localhost与同一pod中的其他容器进行通信。
Kubernetes集群中的所有pod都在同一个共享网络地址空间中,这意味着每个pod都可以通过其他pod的IP地址来实现相互访问。
这也表示它们之间没有NAT(网络地址转换)网关。
当两个pod彼此之间发送网络数据包时,它们都会将对方的实际IP地址看作数据包中的源IP。
每个pod获取可路由的IP地址,其他pod都可以在该IP地址下看到该pod
因此,pod之间的通信其实是非常简单的。
不论是将两个pod安排在单一的还是不同的工作节点上,同时不管实际节点间的网络拓扑结构如何,这些pod内的容器都能够像在无NAT的平坦网络中一样相互通信,就像局域网(LAN)上的计算机一样。
此时,每个pod都有自己的IP地址,并且可以通过这个专门的网络实现pod之间互相访问。
这个专门的网络通常是由额外的软件基于真实链路实现的。
pod是逻辑主机,其行为与非容器世界中的物理主机或虚拟机非常相似。
此外,运行在同一个pod中的进程与运行在同一物理机或虚拟机上的进程相似,只是每个进程都封装在一个容器之中。
将pod视为独立的机器,其中每个机器只托管一个特定的应用。
过去我们习惯于将各种应用程序塞进同一台主机,但是pod不是这么干的。
由于pod比较轻量,我们可以在几乎不导致任何额外开销的前提下拥有尽可能多的pod。
与将所有内容填充到一个pod中不同,我们应该将应用程序组织到多个pod中,而每个pod只包含紧密相关的组件或进程。
同一pod的所有容器总是运行在一起,但对于Web服务器和数据库来说,它们真的需要在同一台计算机上运行吗?答案显然是否定的,它们不应该被放到同一个pod中。
那假如你非要把它们放在一起,有错吗?某种程度上来说,是的。
如果前端和后端都在同一个容器中,那么两者将始终在同一台计算机上运行。如果你有一个双节点Kubernetes集群,而只有一个单独的pod,那么你将始终只会用一个工作节点,而不会充分利用第二个节点上的计算资源(CPU和内存)。
因此更合理的做法是将pod拆分到两个工作节点上,允许Kubernetes将前端安排到一个节点,将后端安排到另一个节点,从而提高基础架构的利用率。
另一个不应该将应用程序都放到单一pod中的原因就是扩缩容。
pod也是扩缩容的基本单位,对于Kubernetes来说,它不能横向扩缩单个容器,只能扩缩整个pod。
这意味着如果你的pod由一个前端和一个后端容器组成,那么当你扩大pod的实例数量时,比如扩大为两个,最终会得到两个前端容器和两个后端容器。
通常来说,前端组件与后端组件具有完全不同的扩缩容需求,所以我们倾向于分别独立地扩缩它们
更不用说,像数据库这样的后端服务器,通常比无状态的前端web服务器更难扩展
将多个容器添加到单个pod的主要原因是应用可能由一个主进程和一个或多个辅助进程组成
pod应该包含紧密耦合的容器组(通常是一个主容器和支持主容器的其他容器)
例如,pod中的主容器可以是一个仅仅服务于某个目录中的文件的Web服务器,而另一个容器(所谓的sidecar容器)则定期从外部源下载内容并将其存储在Web服务器目录中。
我们将看到在这种情况下需要使用Kubernetes Volume,并将其挂载到两个容器中。
sidecar容器的其他例子包括日志轮转器和收集器、数据处理器、通信适配器等
它们需要一起运行还是可以在不同的主机上运行?
它们代表的是一个整体还是相互独立的组件?
它们必须一起进行扩缩容还是可以分别进行?
基本上,我们总是应该倾向于在单独的pod中运行容器,除非有特定的原因要求它们是同一pod的一部分。
容器不应该包含多个进程,pod也不应该包含多个并不需要运行在同一主机上的容器
pod和其他Kubernetes资源通常是通过向Kubernetes REST API提供JSON或YAML描述文件来创建的。此外还有其他更简单的创建资源的方法,比如在前一章中使用的kubectl run命令,但这些方法通常只允许你配置一组有限的属性。另外,通过YAML文件定义所有的Kubernetes对象之后,还可以将它们存储在版本控制系统中,充分利用版本控制所带来的便利性。
创建对象时还应参考 http://kubernetes.io/docs/reference/ 中的Kubernetes API参考文档
kubectl get po xx -o yaml
使用带有-o yaml选项的kubectl get命令来获取pod的整个YAML定义
已部署pod的完整YAML
创建一个新的pod时,需要写的YAML相对来说则要短得多。
pod定义由这么几个部分组成:
首先是YAML中使用的Kubernetes API版本和YAML描述的资源类型;
其次是几乎在所有Kubernetes资源中都可以找到的三大重要部分:
metadata 包括名称、命名空间、标签和关于该容器的其他信息。
spec包含pod内容的实际说明,例如pod的容器、卷和其他数据。
status 包含运行中的pod的当前信息,例如pod所处的条件、每个容器的描述和状态,以及内部IP和其他基本信息
代码清单3.1展示了一个正在运行的pod的完整描述,其中包含了它的状态。status部分包含只读的运行时数据,该数据展示了给定时刻的资源状态。而在创建新的pod时,永远不需要提供status部分。 上述三部分展示了Kubernetes API对象的典型结构。正如你将在整本书中看到的那样,其他对象也都具有相同的结构,这使得理解新对象相对来说更加容易。
一个基本的pod manifest:kubia-manual.yaml
apiVersion: v1
kind: Pod
metadata:
name: kubia-manual
spec:
containers:
- image: luksa/kubia
name: kubia
ports:
- containerPort: 8080
protocol: TCP
该文件遵循Kubernetes API的v1版本。
我们描述的资源类型是pod,名称为kubia-manual;
该pod由基于luksa/kubia镜像的单个容器组成。
此外我们还给该容器命名,并表示它正在监听8080端口。
在pod定义中指定端口纯粹是展示性的(informational)。
忽略它们对于客户端是否可以通过端口连接到pod不会带来任何影响。
如果容器通过绑定到地址0.0.0.0的端口接收连接,那么即使端口未明确列出在pod spec中,其他pod也依旧能够连接到该端口。
但明确定义端口仍是有意义的,在端口定义下,每个使用集群的人都可以快速查看每个pod对外暴露的端口。
明确定义端口还允许你为每个端口指定一个名称,这样一来更加方便我们使用。
在准备manifest时,可以转到 http://kubernetes.io/docs/api 上的Kubernetes参考文档查看每个API对象支持哪些属性,也可以使用kubectl explain命令。
当从头创建一个pod manifest时,可以从请求kubectl 来解释pod开始:
kubectl explani pods
Kubectl打印出对象的解释并列出对象可以包含的属性,接下来就可以深入了解各个属性的更多信息。例如,可以这样查看spec属性:
kubectl explain pod.spec
使用kubectl create命令从YAML文件创建pod:
kubectl create -f kubia-manual.yaml
kubectl create-f命令用于从YAML或JSON文件创建任何资源(不只是pod)
kubectl get po kubia-manual -o yaml
也可以让kubectl返回JSON格式而不是YAML格式(显然,即使你使用YAML创建pod,同样也可以获取JSON格式的描述文件):
kubectl get po kubia-manual -o json
列出pod来查看它们的状态
kubectl get pods
这里可以看到kubia-manual这个pod,状态显示它正在运行
小型Node.js应用将日志记录到进程的标准输出。
容器化的应用程序通常会将日志记录到标准输出和标准错误流,而不是将其写入文件,这就允许用户可以通过简单、标准的方式查看不同应用程序的日志。
容器运行时(在我们的例子中为Docker)将这些流重定向到文件,并允许我们运行以下命令来获取容器的日志:
docker logs <container id>
使用ssh命令登录到pod正在运行的节点,并使用docker logs命令查看其日志,
但Kubernetes提供了一种更为简单的方法。
为了查看pod的日志(更准确地说是容器的日志),只需要在本地机器上运行以下命令(不需要ssh到任何地方)
kubectl logs kubia-manual
在我们向Node.js应用程序发送任何Web请求之前,日志只显示一条关于服务器启动的语句
每天或者每次日志文件达到10MB大小时,容器日志都会自动轮替。
kubectl logs命令仅显示最后一次轮替后的日志条目。
-c <容器名称>
如果我们的pod包含多个容器,在运行kubectl logs命令时则必须通过包含-c <容器名称>选项来显式指定容器名称。在kubia-manual pod中,我们将容器的名称设置为kubia,所以如果该pod中有其他容器,可以通过如下命令获取其日志:
kubectl logs kubia-manual -c kubia
只能获取仍然存在的pod的日志。
当一个pod被删除时,它的日志也会被删除。
如果希望在pod删除之后仍然可以获取其日志,我们需要设置中心化的、集群范围的日志系统,将所有日志存储到中心存储中。
连接到pod以进行测试和调试的方法,其中之一便是通过端口转发。
如果想要在不通过service的情况下与某个特定的pod进行通信(出于调试或其他原因),Kubernetes将允许我们配置端口转发到该pod。
可以通过kubectl port-forward命令完成上述操作。
例如以下命令会将机器的本地端口8888转发到我们的kubia-manual pod的端口8080:
此时端口转发正在运行,可以通过本地端口连接到我们的pod。
kubectl port-forward kubia-manual 8888:8080
在另一个终端中,通过运行在localhost:8888上的kubectl portforward代理,可以使用curl 命令向pod发送一个HTTP请求:
curl localhost:8888
发送请求时的简化视图。实际上,kubectl进程和pod之间还有一些额外的组件
描述使用kubectl port-forward和curl时的简单视图
使用端口转发是一种测试特定pod的有效方法
随着pod数量的增加,将它们分类到子集的需求也就变得越来越明显了
对于微服务架构,部署的微服务数量可以轻松超过20个甚至更多。
这些组件可能是副本(部署同一组件的多个副本)和多个不同的发布版本(stable、beta、canary等)同时运行
多个微服务的pod,包括一些运行多副本集,以及其他运行于同一微服务中的不同版本
微服务架构中未分类的pod
通过标签来组织pod和所有其他Kubernetes对象。
标签是一种简单却功能强大的Kubernetes特性,不仅可以组织pod,也可以组织所有其他的Kubernetes资源。
标签是可以附加到资源的任意键值对,用以选择具有该确切标签的资源(这是通过标签选择器完成的)。
只要标签的key在资源内是唯一的,一个资源便可以拥有多个标签。
通常在我们创建资源时就会将标签附加到资源上,但之后我们也可以再添加其他标签,或者修改现有标签的值,而无须重新创建资源。
此时每个pod都标有两个标签:
app,它指定pod属于哪个应用、组件或微服务。
rel,它显示在pod中运行的应用程序版本是stable、beta还是canary。
金丝雀发布是指在部署新版本时,先只让一小部分用户体验新版本以观察新版本的表现,然后再向所有用户进行推广,这样可以防止暴露有问题的版本给过多的用户。
通过添加这两个标签基本上可以将pod组织为两个维度(基于应用的横向维度和基于版本的纵向维度)。
创建一个带有两个标签的新pod
带标签的pod: kubia-manual-with-labels.yaml
apiVersion: v1
kind: Pod
metadata:
name: kubia-manual-v2
labels:
creation_method: manual
env: prod
spec:
containers:
- image: luksa/kubia
name: kubia
ports:
- containerPort: 8080
protocol: TCP
metadata.labels 部分已经包含了creation_method=manual 和env=prod标签
创建该pod
kubectl create -f kubia-manual-with-labels.yaml
--show-labels
kubectl get pods命令默认不会列出任何标签,但我们可以使用--showlabels选项来查看
kubectl get po --show-labels
-L
kubectl get po -L creation_method,env
由于pod kubia-manual也是手动创建的,所以为其添加creation_method=manual标签:
kubectl label po kubia-manual creation_method=manual
将kubia-manual-v2 pod上的env=prod标签更改为env=debug
kubectl label po kubia-manual-v2 env=debug --overwrite
将标签附加到资源上是一项令人难以置信的强大功能
标签要与标签选择器结合在一起。
标签选择器允许我们选择标记有特定标签的pod子集,并对这些pod执行操作。
可以说标签选择器是一种能够根据是否包含具有特定值的特定标签来过滤资源的准则。
标签选择器根据资源的以下条件来选择资源:
包含(或不包含)使用特定键的标签
包含具有特定键和值的标签
包含具有特定键的标签,但其值与我们指定的不同
列出手动创建的所有pod(用creation_method=manual标记了它们)
kubectl get po -l creation_method=manual
列出包含env标签的所有pod,无论其值如何
kubectl get po -l env
列出没有env标签的pod:
kubectl get po -l '!env'
确保使用单引号来圈引
!env
,这样bash shell才不会解释感叹号(译者注:感叹号在bash中有特殊含义,表示事件指示器)。可以将pod与以下标签选择器进行匹配
creation_method!=manual
选择带有creation_method标签,并且值不等于manual的pod
env in(prod,devel)
选择带有env标签且值为prod或devel的pod
env notin(prod,devel)
选择带有env标签,但其值不是prod或devel的pod使用标签选择器app=pc(如图3.8所示)选择属于product catalog微服务的所有pod。
在包含多个逗号分隔的情况下,可以在标签选择器中同时使用多个条件,此时资源需要全部匹配才算成功匹配了选择器。
例如,如果我们只想选择product catalog微服务的beta版本pod,可以使用以下选择器:
app=pc,rel=beta
标签选择器不仅帮助我们列出pod,在对一个子集中的所有pod都执行操作时也具有重要意义。
例如使用标签选择器来实现一次删除多个pod。
此外标签选择器不只是被kubectl使用,在后续内容中我们也将看到它们在内部也被使用过。
默认 创建的所有pod都是近乎随机地调度到工作节点上的
这恰恰是在Kubernetes集群中工作的正确方式
某些情况下,我们希望对将pod调度到何处持一定发言权,你的硬件基础设施并不是同质便是一个很好的例子。如果你的某些工作节点使用机械硬盘,而其他节点使用固态硬盘,那么你可能想将一些pod调度到一组节点,同时将其他pod调度到另一组节点。另外,当需要将执行GPU密集型运算的pod调度到实际提供GPU加速的节点上时,也需要pod调度。
我们不会特别说明pod应该调度到哪个节点上,因为这将会使应用程序与基础架构强耦合,从而违背了Kubernetes对运行在其上的应用程序隐藏实际的基础架构的整个构想。但如果你想对一个pod应该调度到哪里拥有发言权,那就不应该直接指定一个确切的节点,而应该用某种方式描述对节点的需求,使Kubernetes选择一个符合这些需求的节点。这恰恰可以通过节点标签和节点标签选择器完成。
标签可以附加到任何Kubernetes对象上,包括节点。
通常来说,当运维团队向集群添加新节点时,他们将通过附加标签来对节点进行分类,这些标签指定节点提供的硬件类型,或者任何在调度pod时能提供便利的其他信息。
kubectl label node xxx-node-xxx gpu=true
gpu=true
的节点:kubectl get nodes -l gpu=true
kubectl get nodes -L gpu
假设我们想部署一个需要GPU来执行其工作的新pod。
为了让调度器只在提供适当GPU的节点中进行选择,我们需要在pod的YAML文件中添加一个节点选择器
创建一个名为kubia-gpu.yaml的文件,然后使用kubectl create-f kubia-gpu.yaml命令创建该pod。
使用标签选择器将pod调度到特定节点:kubia-gpu.yaml
apiVersion: v1
kind: Pod
metadata:
name: kubia-gpu
spec:
nodeSelector:
gpu: "true"
containers:
- image: luksa/kubia
name: kubia
我们只是在spec部分添加了一个nodeSelector字段。
当我们创建该pod时,调度器将只在包含标签gpu=true的节点中选择
可以将pod调度到某个确定的节点
由于每个节点都有一个唯一标签,其中键为kubernetes.io/hostname,值为该节点的实际主机名
因此我们也可以将pod调度到某个确定的节点
但如果节点处于离线状态,通过hostname标签将nodeSelector设置为特定节点可能会导致pod不可调度。我们绝不应该考虑单个节点,而是应该通过标签选择器考虑符合特定标准的逻辑节点组。
除标签外,pod和其他对象还可以包含注解。
注解也是键值对,所以它们本质上与标签非常相似。
但与标签不同,注解并不是为了保存标识信息而存在的,它们不能像标签一样用于对对象进行分组。当我们可以通过标签选择器选择对象时,就不存在注解选择器这样的东西。
注解可以容纳更多的信息,并且主要用于工具使用。Kubernetes也会将一些注解自动添加到对象,但其他的注解则需要由用户手动添加。
向Kubernetes引入新特性时,通常也会使用注解。一般来说,新功能的alpha和beta版本不会向API对象引入任何新字段,因此使用的是注解而不是字段,一旦所需的API更改变得清晰并得到所有相关人员的认可,就会引入新的字段并废弃相关注解。
大量使用注解可以为每个pod或其他API对象添加说明,以便每个使用该集群的人都可以快速查找有关每个单独对象的信息。例如,指定创建对象的人员姓名的注解可以使在集群中工作的人员之间的协作更加便利。
为了查看注解,我们需要获取pod的完整YAML文件或使用kubectl describe命令
pod的注解
kubectl get po kubia-xxx -o yaml
kubernetes.io/created-by注解保存了创建该pod的对象的一些JSON数据,而没有涉及太多细节,因此注解并不会是我们想要放入标签的东西。相对而言,标签应该简短一些,而注解则可以包含相对更多的数据(总共不超过256KB)。
kubernetes.io/created-by注解在版本1.8中已经废弃,将会在版本1.9中删除,所以在YAML文件中不会再看到该注解。
kubectl annotate pod kubia-manual mycompany.com/someannotation="foo bar"
将注解mycompany.com/someannotation添加为值foo bar。
使用这种格式的注解键来避免键冲突是一个好方法。
当不同的工具或库向对象添加注解时,如果它们不像我们刚刚那样使用唯一的前缀,可能会意外地覆盖对方的注解。
kubectl describe
kubectl describe pod kubia-manual
将对象分割成完全独立且不重叠的组
每次只想在一个小组内进行操作,因此Kubernetes也能将对象分组到命名空间中
这和用于相互隔离进程的Linux命名空间不一样,Kubernetes命名空间简单地为对象名称提供了一个作用域。此时我们并不会将所有资源都放在同一个命名空间中,而是将它们组织到多个命名空间中,这样可以允许我们多次使用相同的资源名称(跨不同的命名空间)。
在使用多个namespace的前提下,我们可以将包含大量组件的复杂系统拆分为更小的不同组,这些不同组也可以用于在多租户环境中分配资源,将资源分配为生产、开发和QA环境,或者以其他任何你需要的方式分配资源。
资源名称只需在命名空间内保持唯一即可,因此两个不同的命名空间可以包含同名的资源。
虽然大多数类型的资源都与命名空间相关,但仍有一些与它无关,其中之一便是全局且未被约束于单一命名空间的节点资源
kubectl get ns
kubectl get ns
当使用kubectl get命令列出资源时,我们从未明确指定命名空间,因此kubectl总是默认为default命名空间,只显示该命名空间下的对象
也可以使用-n来代替--namespace
kubectl get po --namespace kkube-system
如果有多个用户或用户组正在使用同一个Kubernetes集群,并且它们都各自管理自己独特的资源集合,那么它们就应该分别使用各自的命名空间。
这样一来,它们就不用特别担心无意中修改或删除其他用户的资源,也无须关心名称冲突。如前所述,命名空间为资源名称提供了一个作用域。
除了隔离资源,命名空间还可用于仅允许某些用户访问某些特定资源,甚至限制单个用户可用的计算资源数量。
命名空间是一种和其他资源一样的Kubernetes资源,因此可以通过将YAML文件提交到Kubernetes API服务器来创建该资源。
可以通过向API服务器提交YAML manifest来实现创建、读取、更新和删除。
namespace的YAML定义:custom-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: custom-namespace
使用kubectl将文件提交到Kubernetes API服务器
kubectl create -f custom-namespace.yaml
kubectl create namespace custom-namespace
尽管大多数对象的名称必须符合RFC 1035(域名)中规定的命名规范,这意味着它们可能只包含字母、数字、横杠(-)和点号,但命名空间(和另外几个)不允许包含点号。
要在刚创建的命名空间中创建资源,可以选择在metadata字段中添加一个namespace: custom-namespace属性,
也可以在使用kubectl create命令创建资源时指定命名空间:
kukbectl create -f kubia-manual.yaml -n custom-namespace
在列出、描述、修改或删除其他命名空间中的对象时,需要给kubectl命令传递--namespace(或-n)选项。
如果不指定命名空间,kubectl将在当前上下文中配置的默认命名空间中执行操作。
而当前上下文的命名空间和当前上下文本身都可以通过kubectl config命令进行更改
尽管命名空间将对象分隔到不同的组,只允许你对属于特定命名空间的对象进行操作,但实际上命名空间之间并不提供对正在运行的对象的任何隔离。
例如,你可能会认为当不同的用户在不同的命名空间中部署pod时,这些pod应该彼此隔离,并且无法通信,但事实却并非如此。命名空间之间是否提供网络隔离取决于Kubernetes所使用的网络解决方案。当该解决方案不提供命名空间间的网络隔离时,如果命名空间foo中的某个pod知道命名空间 bar中pod的IP地址,那它就可以将流量(例如HTTP请求)发送到另一个pod。
还可以通过指定多个空格分隔的名称来删除多个pod(例如:kubectl delete po pod1 pod2)
按名称删除kubia-gpu pod
kubectl delete po kubia-gpu
在删除pod的过程中,实际上我们在指示Kubernetes终止该pod中的所有容器。
Kubernetes向进程发送一个SIGTERM信号并等待一定的秒数(默认为30),使其正常关闭。
如果它没有及时关闭,则通过SIGKILL终止该进程。
因此,为了确保你的进程总是正常关闭,进程需要正确处理SIGTERM信号。
kubectl delete po -l creation_method=manual
通过指定rel=canary标签选择器(如图3.10所示),可以一次删除所有金丝雀pod:
kubectl delete po -l rel=canary
删除整个命名空间(pod将会伴随命名空间自动删除
删除custom-namespace
kubectl delete ns custom-namespace
--all
使用--all选项告诉Kubernetes删除当前命名空间中的所有pod:
kubectl delete po --all
我们看到,在kubia-zxzij pod 正在终止时,却出现一个之前并没有出现过的叫作kubia-09as0的新pod
使用kubectl run命令不会直接创建pod,而是创建一个ReplicationCcontroller,然后再由ReplicationCcontroller创建pod。
因此只要删除由该ReplicationCcontroller创建的pod,它便会立即创建一个新的pod。
如果想要删除该pod,我们还需要删除这个ReplicationCcontroller。
通过使用单个命令删除当前命名空间中的所有资源,可以删除ReplicationCcontroller和pod,以及我们创建的所有service:
kubectl delete all --all
命令中的第一个all指定正在删除所有资源类型,而--all选项指定将删除所有资源实例,而不是按名称指定它们
使用all关键字删除所有内容并不是真的完全删除所有内容。一些资源(比如Secret)会被保留下来,并且需要被明确指定删除。
删除资源时,kubectl将打印它删除的每个资源的名称。
kubectl delete all--all命令也会删除名为kubernetes的Service,但它应该会在几分钟后自动重新创建。
pod代表了Kubernetes中的基本部署单元
在实际的用例里,你希望你的部署能自动保持运行,并且保持健康,无须任何手动干预。
要做到这一点,你几乎不会直接创建pod,而是创建ReplicationController或Deployment这样的资源,接着由它们来创建并管理实际的pod。
当你创建未托管的pod时,会选择一个集群节点来运行pod,然后在该节点上运行容器。Kubernetes接下来会监控这些容器,并且在它们失败的时候自动重新启动它们。但是如果整个节点失败,那么节点上的pod会丢失,并且不会被新节点替换,除非这些pod由前面提到的ReplicationController或类似资源来管理
只要将pod调度到某个节点,该节点上的Kubelet就会运行pod的容器,从此只要该pod存在,就会保持运行。如果容器的主进程崩溃,Kubelet将重启容器。如果应用程序中有一个导致它每隔一段时间就会崩溃的bug,Kubernetes会自动重启应用程序,所以即使应用程序本身没有做任何特殊的事,在Kubernetes中运行也能自动获得自我修复的能力。
即使进程没有崩溃,有时应用程序也会停止正常工作。例如,具有内存泄漏的Java应用程序将开始抛出OutOfMemoryErrors,但JVM进程会一直运行
Kubernetes可以通过存活探针(liveness probe)检查容器是否还在运行。
可以为pod中的每个容器单独指定存活探针。
如果探测失败,Kubernetes将定期执行探针并重新启动容器。
HTTP GET探针对容器的IP地址(你指定的端口和路径)执行HTTP GET请求。如果探测器收到响应,并且响应状态码不代表错误(换句话说,如果HTTP响应状态码是2xx或3xx),则认为探测成功。如果服务器返回错误响应状态码或者根本没有响应,那么探测就被认为是失败的,容器将被重新启动。
TCP套接字探针尝试与容器指定端口建立TCP连接。如果连接成功建立,则探测成功。否则,容器重新启动。
Exec探针在容器内执行任意命令,并检查命令的退出状态码。如果状态码是0,则探测成功。所有其他状态码都被认为失败。
为你的Node.js应用添加一个存活探针
添加一个存活探针来检查其Web服务器是否提供请求是有意义的
创建一个包含HTTP GET存活探针的新pod
将存活探针添加到pod:kubia-liveness-probe.yaml
apiVersion: v1
kind: Pod
metadata:
name: kubia-liveness
spec:
containers:
- image: luksa/kubia-unhealthy
name: kubia
livenessProbe:
httpGet:
path: /
port: 8080
该pod的描述文件定义了一个httpGet存活探针,该探针告诉Kubernetes定期在端口8080路径上执行HTTP GET请求,以确定该容器是否健康。这些请求在容器运行后立即开始。
经过五次这样的请求(或实际的客户端请求)后,你的应用程序开始返回HTTP状态码500,Kubernetes会认为探测失败并重启容器。
RESTARTS列显示pod的容器已被重启一次
当你想知道为什么前一个容器终止时,你想看到的是前一个容器的日志,而不是当前容器的。可以通过添加--previous选项来完成:
kubectl logs mypod --previous
可以通过查看kubectl describe的内容来了解 为什么必须重启容器
可以看到容器现在正在运行,但之前由于错误而终止。退出代码为137,这有特殊的含义 —— 表示该进程由外部信号终止。数字137是两个数字的总和:128+x,其中x是终止进程的信号编号。在这个例子中,x等于9,这是SIGKILL的信号编号,意味着这个进程被强行终止。
在底部列出的事件显示了容器为什么终止 ——Kubernetes发现容器不健康,所以终止并重新创建。
当容器被强行终止时,会创建一个全新的容器——而不是重启原来的容器。
kubectl describe 还显示关于存活探针的附加信息
除了明确指定的存活探针选项,还可以看到其他属性,例如delay(延迟)、timeout(超时)、period(周期)等。
delay=0s部分显示在容器启动后立即开始探测。
timeout仅设置为1秒,因此容器必须在1秒内进行响应,不然这次探测记作失败。
每10秒探测一次容器(period=10s),
并在探测连续三次失败(#failure=3)后重启容器。
定义探针时可以自定义这些附加参数
例如,要设置初始延迟,请将initialDelaySeconds属性添加到存活探针的配置中
具有初始延迟的存活探针:kubia-liveness-probe-initial-delay.yaml
如果没有设置初始延迟,探针将在启动时立即开始探测容器,这通常会导致探测失败,因为应用程序还没准备好开始接收请求。如果失败次数超过阈值,在应用程序能正确响应请求之前,容器就会重启。
务必记得设置一个初始延迟来说明应用程序的启动时间。
很多场合都会看到这种情况,用户很困惑为什么他们的容器正在重启。但是如果使用kubectl describe,他们会看到容器以退出码137或143结束,并告诉他们该pod是被迫终止的。此外,pod事件的列表将显示容器因liveness探测失败而被终止。如果你在pod启动时看到这种情况,那是因为未能适当设置initialDelaySeconds。
退出代码137表示进程被外部信号终止,退出代码为128+9(SIGKILL)。同样,退出代码143对应于128+15(SIGTERM)。
对于在生产中运行的pod,一定要定义一个存活探针。没有探针的话,Kubernetes无法知道你的应用是否还活着。只要进程还在运行,Kubernetes会认为容器是健康的。
简易的存活探针仅仅检查了服务器是否响应。虽然这看起来可能过于简单,但即使是这样的存活探针也可以创造奇迹
为了更好地进行存活检查,需要将探针配置为请求特定的URL路径(例如/health),并让应用从内部对内部运行的所有重要组件执行状态检查,以确保它们都没有终止或停止响应。
请确保/health HTTP端点不需要认证,否则探测会一直失败,导致你的容器无限重启。
一定要检查应用程序的内部,而没有任何外部因素的影响。
例如,当服务器无法连接到后端数据库时,前端Web服务器的存活探针不应该返回失败。如果问题的底层原因在数据库中,重启Web服务器容器不会解决问题。由于存活探测将再次失败,你将反复重启容器直到数据库恢复。
存活探针不应消耗太多的计算资源,并且运行不应该花太长时间。
默认情况下,探测器执行的频率相对较高,必须在一秒之内执行完毕。
一个过重的探针会大大减慢你的容器运行。
探针的CPU时间计入容器的CPU时间配额,因此使用重量级的存活探针将减少主应用程序进程可用的CPU时间。
如果你在容器中运行Java应用程序,请确保使用HTTP GET存活探针,而不是启动全新JVM以获取存活信息的Exec探针。任何基于JVM或类似的应用程序也是如此,它们的启动过程需要大量的计算资源
探针的失败阈值是可配置的,并且通常在容器被终止之前探针必须失败多次。但即使你将失败阈值设置为1,Kubernetes为了确认一次探测的失败,会尝试若干次。因此在探针中自己实现重试循环是浪费精力。
Kubernetes会在你的容器崩溃或其存活探针失败时,通过重启容器来保持运行。这项任务由承载pod的节点上的Kubelet执行 —— 在主服务器上运行的Kubernetes Control Plane组件不会参与此过程。
但如果节点本身崩溃,那么Control Plane必须为所有随节点停止运行的pod创建替代品。它不会为你直接创建的pod执行此操作。这些pod只被Kubelet管理,但由于Kubelet本身运行在节点上,所以如果节点异常终止,它将无法执行任何操作。
为了确保你的应用程序在另一个节点上重新启动,需要使用ReplicationController或类似机制管理pod,我们将在本章其余部分讨论该机制。
ReplicationController是一种Kubernetes资源,可确保它的pod始终保持运行状态。
如果pod因任何原因消失(例如节点从集群中消失或由于该pod已从节点中逐出),则ReplicationController会注意到缺少了pod并创建替代pod。
一般而言,ReplicationController旨在创建和管理一个pod的多个副本(replicas)。这就是ReplicationController名字的由来。
节点故障时,只有ReplicationController管理的pod被重新创建
ReplicationController会持续监控正在运行的pod列表,并保证相应“类型”的pod的数目与期望相符。
如正在运行的pod太少,它会根据pod模板创建新的副本。
如正在运行的pod太多,它将删除多余的副本
你可能会对有多余的副本感到奇怪。这可能有几个原因: 有人会手动创建相同类型的pod。 有人更改现有的pod的“类型”。 有人减少了所需的pod的数量,等等。 笔者已经使用过几次pod“类型”这种说法,但这是不存在的。ReplicationController不是根据pod类型来执行操作的,而是根据pod是否匹配某个标签选择器
ReplicationController的工作是确保pod的数量始终与其标签选择器匹配。
如果不匹配,则ReplicationController将根据所需,采取适当的操作来协调pod的数量
一个ReplicationController的协调流程
一个ReplicationController有三个主要部分:
label selector(标签选择器),用于确定ReplicationController作用域中有哪些pod
replica count(副本个数),指定应运行的pod数量
pod template(pod模板),用于创建新的pod副本
ReplicationController的三个关键部分(pod选择器、副本个数和pod模板)
ReplicationController的副本个数、标签选择器,甚至是pod模板都可以随时修改,但只有副本数目的变更会影响现有的pod。
更改标签选择器和pod模板对现有pod没有影响。
更改标签选择器会使现有的pod脱离ReplicationController的范围,因此控制器会停止关注它们。
在创建pod后,ReplicationController也不关心其pod的实际“内容”(容器镜像、环境变量及其他)。
因此,该模板仅影响由此ReplicationController创建的新pod。
可以将其视为创建新pod的曲奇切模(cookie cutter)。
确保一个pod(或多个pod副本)持续运行,方法是在现有pod丢失时启动一个新pod。
集群节点发生故障时,它将为故障节点上运行的所有pod(即受ReplicationController控制的节点上的那些pod)创建替代副本。
它能轻松实现pod的水平伸缩 —— 手动和自动都可以
pod实例永远不会重新安置到另一个节点。相反,ReplicationController会创建一个全新的pod实例,它与正在替换的实例无关。
可以通过上传JSON或YAML描述文件到Kubernetes API服务器来创建ReplicationController。
ReplicationController的YAML定义:kubia-rc.yaml
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3
selector:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ports:
- containerPort: 8080
上传文件到API服务器时,Kubernetes会创建一个名为kubia的新ReplicationController,它确保符合标签选择器app=kubia的pod实例始终是三个。当没有足够的pod时,根据提供的pod模板创建新的pod。
模板中的pod标签显然必须和ReplicationController的标签选择器匹配,否则控制器将无休止地创建新的容器。因为启动新pod不会使实际的副本数量接近期望的副本数量。为了防止出现这种情况,API服务会校验ReplicationController的定义,不会接收错误配置。
根本不指定选择器也是一种选择。在这种情况下,它会自动根据pod模板中的标签自动配置。
定义ReplicationController时不要指定pod选择器,让Kubernetes从pod模板中提取它。这样YAML更简短。
创建ReplicationController,请使用已知的kubectl create命令
kubectl create -f kubia-rc.yaml
一旦创建了ReplicationController,它就开始工作
由于没有任何pod有app=kubia标签,ReplicationController会根据pod模板启动三个新的pod。
列出pod以查看ReplicationController是否完成了它应该做的事情
它确实创建了三个pod。现在ReplicationController正在管理这三个pod
手动删除其中一个pod,以查看ReplicationController如何立即启动新容器,从而将匹配容器的数量恢复为三:
kubectl delete pod kubia-53thy
重新列出pod会显示四个,因为你删除的pod已终止,并且已创建一个新的pod:
ReplicationController再次完成了它的工作。这是非常有用的。
通过kubectl get命令显示的关于ReplicationController的信息:
使用rc作为replicationcontroller的简写。
kubectl get rc
会看到三列显示了所需的pod数量,实际的pod数量,以及其中有多少pod已准备就绪
kubectl describe rc kubia
当前的副本数与所需的数量相符,因为控制器已经创建了一个新的pod。它显示了四个正在运行的pod,因为被终止的pod仍在运行中,尽管它并未计入当前的副本个数中。底部的事件列表显示了ReplicationController的行为—— 它到目前为止创建了四个pod。
控制器通过创建一个新的替代pod来响应pod的删除操作(见图4.4)。从技术上讲,它并没有对删除本身做出反应,而是针对由此产生的状态 —— pod数量不足。
虽然ReplicationController会立即收到删除pod的通知(API服务器允许客户端监听资源和资源列表的更改),但这不是它创建替代pod的原因。该通知会触发控制器检查实际的pod数量并采取适当的措施。
一个三节点Kubernetes集群。你将从网络中断开其中一个节点来模拟节点故障。
如果使用Minikube,则无法做这个练习,因为只有一个节点同时充当主节点和工作节点
如果节点在没有Kubernetes的场景中发生故障,运维人员需要手动将节点上运行的应用程序迁移到其他机器。
而现在,Kubernetes会自动执行此操作。在ReplicationController检测到它的pod已关闭后不久,它将启动新的pod以替换它们。
Kubernetes在重新调度pod之前会等待一段时间(如果节点因临时网络故障或Kubelet重新启动而无法访问)。如果节点在几分钟内无法访问,则调度到该节点的pod的状态将变为Unknown。此时,ReplicationController将立即启动一个新的pod
恢复节点,当节点再次启动时,其状态应该返回到Ready,并且状态为Unknown的pod将被删除
由ReplicationController创建的pod并不是绑定到ReplicationController。在任何时刻,ReplicationController管理与标签选择器匹配的pod。通过更改pod的标签,可以将它从ReplicationController的作用域中添加或删除。它甚至可以从一个ReplicationController移动到另一个。
尽管一个pod没有绑定到一个ReplicationController,但该pod在metadata.ownerReferences字段中引用它,可以轻松使用它来找到一个pod属于哪个ReplicationController。
如果你更改了一个pod的标签,使它不再与ReplicationController的标签选择器相匹配,那么该pod就变得和其他手动创建的pod一样了。它不再被任何东西管理。如果运行该节点的pod异常终止,它显然不会被重新调度。但请记住,当你更改pod的标签时,ReplicationController发现一个pod丢失了,并启动一个新的pod替换它。
添加另一个标签并没有用,因为ReplicationController不关心该pod是否有任何附加标签,它只关心该pod是否具有标签选择器中引用的所有标签。
向ReplicationController管理的pod添加其他标签,它并不关心
从ReplicationController角度而言,没发生任何更改
更改app=kubia标签。这将使该pod不再与ReplicationController的标签选择器相匹配,只剩下两个匹配的pod。因此,ReplicationController会启动一个新的pod,将数目恢复为三
kubectl label pod kubia-dmdck app=foo --overwrite
--overwrite参数是必要的,否则kubectl将只打印出警告,并不会更改标签。这样是为了防止你想要添加新标签时无意中更改现有标签的值。再次列出所有pod时会显示四个pod:
在有四个pod:一个不是由你的ReplicationController管理的,其他三个是。其中包括新建的pod。
更改pod的标签,使得它们不再与ReplicationController的pod选择器匹配时,发生的事情。可以看到三个pod和ReplicationController。在将pod的标签从app=kubia更改为app=foo之后,ReplicationController就不管这个pod了。由于控制器的副本个数设置为3,并且只有两个pod与标签选择器匹配,所以ReplicationController启动kubia-2qneh pod,使总数回到了三。kubiadmdck pod现在是完全独立的,并且会一直运行直到你手动删除它(现在可以这样做,因为你不再需要它)。
当你想操作特定的pod时,从ReplicationController管理范围中移除pod的操作很管用。例如,你可能有一个bug导致你的pod在特定时间或特定事件后开始出问题。如果你知道某个pod发生了故障,就可以将它从Replication-Controller的管理范围中移除,让控制器将它替换为新pod,接着这个pod就任你处置了。完成后删除该pod即可。
修改了ReplicationController的标签选择器
它会让所有的pod脱离ReplicationController的管理,导致它创建三个新的pod
Kubernetes确实允许你更改ReplicationController的标签选择器,但这不适用于其他资源
你永远不会修改控制器的标签选择器,但你会时不时会更改它的pod模板
ReplicationController的pod模板可以随时修改。更改pod模板就像用一个曲奇刀替换另一个。它只会影响你之后切出的曲奇,并且不会影响你已经剪切的曲奇(见图4.6)。
要修改旧的pod,你需要删除它们,并让ReplicationController根据新模板将其替换为新的pod。
kubectl edit rc kubia
这将在你的默认文本编辑器中打开ReplicationController的YAML配置。找到pod模板部分并向元数据添加一个新的标签。保存更改并退出编辑器后,kubectl将更新ReplicationController
现在可以再次列出pod及其标签,并确认它们未发生变化。但是如果你删除了这个pod并等待其替代pod创建,你会看到新的标签
像这样编辑一个ReplicationController,来更改容器模板中的容器图像,删除现有的容器,并让它们替换为新模板中的新容器,可以用于升级pod,但是有更好的方法.
可以通过设置KUBE_EDITOR环境变量来告诉kubectl使用你期望的文本编辑器。例如,如果你想使用nano编辑Kubernetes资源,请执行以下命令(或将其放入~/.bashrc或等效文件中):
export KUBE_EDITOR="/usr/bin/nano"
如果未设置KUBE_EDITOR环境变量,则kubectl edit会回退到使用默认编辑器(通常通过EDITOR环境变量进行配置)。
因为改变副本的所需数量非常简单,所以这也意味着水平缩放pod很简单。
放大或者缩小pod的数量规模就和在ReplicationController资源中更改Replicas字段的值一样简单。更改之后,ReplicationController将会看到存在太多的pod并删除其中的一部分(缩容时),或者看到它们数目太少并创建pod(扩容时)。
命令
kubectl scale rc kubia --replicas=10
以声明的形式编辑ReplicationController的定义对其进行缩放
kubectl edit rc kubia
当文本编辑器打开时,找到spec.replicas字段并将其值更改为10
保存该文件并关闭编辑器,ReplicationController会更新并立即将pod的数量增加到10
如果kubectl scale命令看起来好像是你在告诉Kubernetes要做什么,现在就更清晰了,你是在声明对ReplicationController的目标状态的更改,而不是告诉Kubernetes它要做的事情。
在Kubernetes中水平伸缩pod是陈述式的:“我想要运行x个实例。”你不是告诉Kubernetes做什么或如何去做,只是指定了期望的状态。
这种声明式的方法使得与Kubernetes集群的交互变得容易
如果启用pod水平自动缩放,那么即使是Kubernetes本身也可以完成。
通过kubectl delete删除ReplicationController时,pod也会被删除。
但是由于由ReplicationController创建的pod不是ReplicationController的组成部分,只是由其进行管理,因此可以只删除ReplicationController并保持pod运行
当你最初拥有一组由ReplicationController管理的pod,然后决定用ReplicaSe(t 你接下来会知道)替换ReplicationController时,这就很有用。可以在不影响pod的情况下执行此操作,并在替换管理它们的ReplicationController时保持pod不中断运行。
使用--cascade=false删除ReplicationController使托架不受管理
当使用kubectl delete删除ReplicationController时,可以通过给命令增加--cascade=false选项来保持pod的运行
kubectl delete rc kubia --cascade=false
你已经删除了ReplicationController,所以这些pod独立了,它们不再被管理。但是你始终可以使用适当的标签选择器创建新的ReplicationController,并再次将它们管理起来。
最初,ReplicationController是用于复制和在异常时重新调度节点的唯一Kubernetes组件,
后来又引入了一个名为ReplicaSet的类似资源。
它是新一代的ReplicationController,并且将其完全替换掉
(ReplicationController最终将被弃用)。
从现在起,应该始终创建ReplicaSet而不是ReplicationController。它们几乎完全相同
通常不会直接创建它们,而是在创建更高层级的Deployment资源时自动创建它们。
ReplicaSet的行为与ReplicationController完全相同,但pod选择器的表达能力更强。
虽然ReplicationController的标签选择器只允许包含某个标签的匹配pod,
但ReplicaSet的选择器还允许匹配缺少某个标签的pod,或包含特定标签名的pod,不管其值如何。
单个ReplicationController无法将pod与标签env=production和env=devel同时匹配。它只能匹配带有env=devel标签的pod或带有env=devel标签的pod。但是一个ReplicaSet可以匹配两组pod并将它们视为一个大组。
无论ReplicationController的值如何,ReplicationController都无法仅基于标签名的存在来匹配pod,而ReplicaSet则可以。例如,ReplicaSet可匹配所有包含名为env的标签的pod,无论ReplicaSet的实际值是什么(可以理解为env=*)
先前由ReplicationController创建稍后又被抛弃的无主pod,现在如何被ReplicaSet管理
ReplicaSet的YAML定义:kubia-replicaset.yaml
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
name: kubia
spec:
replicas: 3
selector:
matchLabels:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ReplicaSet不是v1 API的一部分,因此你需要确保在创建资源时指定正确的apiVersion。你正在创建一个类型为ReplicaSet的资源,它的内容与你之前创建的ReplicationController的内容大致相同
唯一的区别在选择器中。不必在selector属性中直接列出pod需要的标签,而是在selector.matchLabels下指定它们。这是在ReplicaSet中定义标签选择器的更简单(也更不具表达力)的方式。之后,你会看到表达力更强的选项。
因为你仍然有三个pod匹配从最初运行的app=kubia选择器,所以创建此ReplicaSet不会触发创建任何新的pod。ReplicaSet将把它现有的三个pod归为自己的管辖范围。
apiVersion属性指定的两件事情:
API组(在这种情况下是apps)
实际的API版本(v1beta2)
某些Kubernetes资源位于所谓的核心API组中,该组并不需要在apiVersion字段中指定(只需指定版本——例如,你已经在定义pod资源时使用过apiVersion:v1)。在后续的Kubernetes版本中引入其他资源,被分为几个API组。
使用kubectl create命令根据YAML文件创建ReplicaSet。之后,可以使用kubectl get和kubectl describe来检查ReplicaSet
rs是replicaset的简写。
kubectl get rs
ReplicaSet与ReplicationController没有任何区别。显示有三个与选择器匹配的副本。如果列出所有pod,你会发现它们仍然是你以前的三个pod。ReplicaSet没有创建任何新的pod。
ReplicaSet相对于ReplicationController的主要改进是它更具表达力的标签选择器。
第一个ReplicaSet示例中,用较简单的matchLabels选择器来确认ReplicaSet与ReplicationController没有区别。
现在,将用更强大的matchExpressions属性来重写选择器
一个matchExpressions选择器:kubia-replicasetmatchexpressions.yaml
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
name: kubia
spec:
replicas: 3
selector:
matchExpressions:
- key: app
operator: In
values:
- kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
可以给选择器添加额外的表达式。如示例,每个表达式都必须包含一个key、一个operator(运算符),并且可能还有一个values的列表(取决于运算符)。你会看到四个有效的运算符:
In:Label的值必须与其中一个指定的values匹配。
NotIn:Label的值与任何指定的values不匹配。
Exists:pod必须包含一个指定名称的标签(值不重要)。使用此运算符时,不应指定values字段。
DoesNotExist:pod不得包含有指定名称的标签。values属性不得指定。
如果你指定了多个表达式,则所有这些表达式都必须为true才能使选择器与pod匹配。
如果同时指定matchLabels和matchExpressions,则所有标签都必须匹配,并且所有表达式必须计算为true以使该pod与选择器匹配。
kubectl delete rc kubia
删除ReplicaSet会删除所有的pod。这种情况下是需要列出pod来确认的。
希望pod在集群中的每个节点上运行
包括pod执行系统级别的与基础结构相关的操作。例如,希望在每个节点上运行日志收集器和资源监控器。另一个典型的例子是Kubernetes自己的kube-proxy进程,它需要运行在所有节点上才能使服务工作。
DaemonSet在每个节点上只运行一个pod副本,而副本则将它们随机地分布在整个集群中
在Kubernetes之外,此类进程通常在节点启动期间通过系统初始化脚本或systemd守护进程启动。在Kubernetes节点上,仍然可以使用systemd运行系统进程,但这样就不能利用所有的Kubernetes特性了
要在所有集群节点上运行一个pod,需要创建一个DaemonSet对象,这很像一个ReplicationController或ReplicaSet,除了由DaemonSet创建的pod,已经有一个指定的目标节点并跳过Kubernetes调度程序。它们不是随机分布在集群上的。
DaemonSet确保创建足够的pod,并在自己的节点上部署每个pod
DaemonSet并没有期望的副本数的概念。它不需要,因为它的工作是确保一个pod匹配它的选择器并在每个节点上运行
如果节点下线,DaemonSet不会在其他地方重新创建pod。但是,当将一个新节点添加到集群中时,DaemonSet会立刻部署一个新的pod实例。如果有人无意中删除了一个pod,那么它也会重新创建一个新的pod。与ReplicaSet一样,DaemonSet从配置的pod模板创建pod。
DaemonSet将pod部署到集群中的所有节点上,除非指定这些pod只在部分节点上运行。这是通过pod模板中的nodeSelector属性指定的,这是DaemonSet定义的一部分(类似于ReplicaSet或ReplicationController中的pod模板)
DaemonSet中的节点选择器定义了DaemonSet必须将其pod部署到的节点。
节点可以被设置为不可调度的,防止pod被部署到节点上。DaemonSet甚至会将pod部署到这些节点上,因为无法调度的属性只会被调度器使用,而DaemonSet管理的pod则完全绕过调度器。这是预期的,因为DaemonSet的目的是运行系统服务,即使是在不可调度的节点上,系统服务通常也需要运行。
假设有一个名为ssd-monitor的守护进程,它需要在包含固态驱动器(SSD)的所有节点上运行。你将创建一个DaemonSet,它在标记为具有SSD的所有节点上运行这个守护进程。集群管理员已经向所有此类节点添加了disk=ssd的标签,因此你将使用节点选择器创建DaemonSet,该选择器只选择具有该标签的节点
使用含有节点选择器的DaemonSet在特定的节点上部署pod
创建一个运行模拟的ssd-monitor监控器进程的DaemonSet,该进程每5秒会将“SSD OK”打印到标准输出
一个DaemonSet的YAML:ssd-monitor-daemonset.yaml
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
name: ssd-monitor
spec:
selector:
matchLabels:
app: ssd-monitor
template:
metadata:
labels:
app: ssd-monitor
spec:
nodeSelector:
disk: ssd
containers:
- name: main
image: luksa/ssd-monitor
你正在定义一个DaemonSet,它将运行一个基于luksa/ssd-monitor容器镜像的单容器pod。该pod的实例将在每个具有disk=ssd标签的节点上创建
创建一个DaemonSet就像从YAML文件创建资源那样:
kubectl create -f ssd-monitor-daemonset.yaml
给节点打上disk=ssd标签
DaemonSet将检测到节点的标签已经更改,并将pod部署到有匹配标签的所有节点
列出节点
kubectl get node
给节点添加disk=ssd标签
kubectl label node minikube disk=ssd
如果你没有使用Minikube,用你的节点名替换minikube。
DaemonSet现在应该已经创建pod了
现在看起来一切正常。如果你有多个节点并且其他的节点也加上了同样的标签,将会看到DaemonSet在每个节点上都启动pod。
修改标签
kubectl label node minikube disk=hdd --overwrite
pod如预期中正在被终止
删除DaemonSet也会一起删除这些pod
到目前为止,我们只谈论了需要持续运行的pod。
你会遇到只想运行完成工作后就终止任务的情况。
ReplicationController、ReplicaSet和DaemonSet会持续运行任务,永远达不到完成态。这些pod中的进程在退出时会重新启动。
但是在一个可完成的任务中,其进程终止后,不应该再重新启动。
Kubernetes通过Job资源提供了对此的支持,这与我们在本章中讨论的其他资源类似,
但它允许你运行一种pod,该pod在内部进程成功结束时,不重启容器。
一旦任务完成,pod就被认为处于完成状态
在发生节点故障时,该节点上由Job管理的pod将按照ReplicaSet的pod的方式,重新安排到其他节点。
如果进程本身异常退出(进程返回错误退出代码时),可以将Job配置为重新启动容器。
例如,Job对于临时任务很有用,关键是任务要以正确的方式结束。可以在未托管的pod中运行任务并等待它完成,但是如果发生节点异常或pod在执行任务时被从节点中逐出,则需要手动重新创建该任务。手动做这件事并不合理 —— 特别是如果任务需要几个小时才能完成。
这样的任务的一个例子是,如果有数据存储在某个地方,需要转换并将其导出到某个地方
由Job管理的pod会一直被重新安排,直到它们成功完成任务
创建Job manifes
Job的YAML定义:batch-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job
spec:
template:
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure
containers:
- name: main
image: luksa/batch-job
Job是batch API组v1 API版本的一部分。YAML定义了一个Job类型的资源,它将运行luksa/batch-job镜像,该镜像调用一个运行120秒的进程,然后退出。
在一个pod的定义中,可以指定在容器中运行的进程结束时,Kubernetes会做什么。这是通过pod配置的属性restartPolicy完成的,默认为Always。Job pod不能使用默认策略,因为它们不是要无限期地运行。因此,需要明确地将重启策略设置为OnFailure或Never。此设置防止容器在完成任务时重新启动(pod被Job管理时并不是这样的)。
在使用kubectl create命令创建此作业后,应该看到它立即启动一个pod:
两分钟过后,pod将不再出现在pod列表中,工作将被标记为已完成。默认情况下,除非使用--show-all(或-a)开关,否则在列出pod时不显示已完成的pod:
kubectl get po -a
完成后pod未被删除的原因是允许你查阅其日志
kubectl logs batch-job-xxx
pod可以被直接删除,或者在删除创建它的Job时被删除。在你删除它之前,让我们再看一下Job资源
作业显示已成功完成
作业可以配置为创建多个pod实例,并以并行或串行方式运行它们。
这是通过在Job配置中设置completions和parallelism属性来完成的。
如果你需要一个Job运行多次,则可以将completions设为你希望作业的pod运行多少次
需要多次完成的Job:multi-completion-batch-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: multi-completion-batch-job
spec:
completions: 5
template:
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure
containers:
- name: main
image: luksa/batch-job
Job将一个接一个地运行五个pod。它最初创建一个pod,当pod的容器运行完成时,它创建第二个pod,以此类推,直到五个pod成功完成。如果其中一个pod发生故障,工作会创建一个新的pod,所以Job总共可以创建五个以上的pod。
不必一个接一个地运行单个Job pod,也可以让该Job并行运行多个pod。可以通过parallelism Job配置属性,指定允许多少个pod并行执行
并行运行Job pod:multi-completion-parallel-batch-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: multi-completion-batch-job
spec:
completions: 5
parallelism: 2
template:
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure
containers:
- name: main
image: luksa/batch-job
通过将parallelism设置为2,Job创建两个pod并行运行它们
只要其中一个pod完成任务,工作将运行下一个pod,直到五个pod都成功完成任务。
可以在Job运行时更改Job的parallelism属性。这与缩放ReplicaSet或ReplicationController类似,可以使用kubectl scale命令完成:
kubectl scale job multi-completion-parallel-batch-job --replicas 3
由于你将parallelism从2增加到3,另一个pod立即启动,因此现在有三个pod在运行。
Job要等待一个pod多久来完成任务?如果pod卡住并且根本无法完成(或者无法足够快完成),该怎么办?
通过在pod配置中设置activeDeadlineSeconds属性,可以限制pod的时间。如果pod运行时间超过此时间,系统将尝试终止pod,并将Job标记为失败。
通过指定Job manifest中的spec.backoffLimit字段,可以配置Job在被标记为失败之前可以重试的次数。如果你没有明确指定它,则默认为6。
Job资源在创建时会立即运行pod。
但是许多批处理任务需要在特定的时间运行,或者在指定的时间间隔内重复运行。
在Linux和类UNIX操作系统中,这些任务通常被称为cron任务。Kubernetes也支持这种任务
Kubernetes中的cron任务通过创建CronJob资源进行配置。运行任务的时间表以知名的cron格式指定
在配置的时间,Kubernetes将根据在CronJob对象中配置的Job模板创建Job资源。创建Job资源时,将根据任务的pod模板创建并启动一个或多个pod副本,如你在前一部分中所了解的那样。
想象一下,你需要每15分钟运行一次前一个示例中的批处理任务。为此,请使用以下规范创建一个CronJob资源。
CronJob资源的YAML:cronjob.yaml
指定了创建Job对象的时间表和模板
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: batch-job-every-fifteen-minutes
spec:
schedule: "0,15,30,45 * * * *"
jobTemplate:
spec:
template:
metadata:
labels:
app: periodic-batch-job
spec:
restartPolicy: OnFailure
containers:
- name: main
image: luksa/batch-job
cron时间表格式
时间表从左到右包含以下五个条目:
分钟
小时
每月中的第几天
月
星期几
在该示例中,你希望每15分钟运行一次任务因此schedule字段的值应该是"0,15,30,45****"这意味着每小时的0、15、30和45分钟(第一个星号),每月的每一天(第二个星号),每月(第三个星号)和每周的每一天(第四个星号)。
相反,如果你希望每隔30分钟运行一次,但仅在每月的第一天运行,则应将计划设置为"0,30 1 ",并且如果你希望它每个星期天的3AM运行,将它设置为"0 3 * 0"(最后一个零代表星期天)
配置Job模板
CronJob通过CronJob规范中配置的jobTemplate属性创建任务资源
CronJob资源会创建Job资源,然后Job创建pod。
可能发生Job或pod创建并运行得相对较晚的情况。
你可能对这项工作有很高的要求,任务开始不能落后于预定的时间过多。
在这种情况下,可以通过指定CronJob规范中的startingDeadlineSeconds字段来指定截止日期
为CronJob指定一个startingDeadlineSeconds
工作运行的时间应该是10:30:00。如果因为任何原因10:30:15不启动,任务将不会运行,并将显示为Failed。
在正常情况下,CronJob总是为计划中配置的每个执行创建一个Job,但可能会同时创建两个Job,或者根本没有创建。
为了解决第一个问题,你的任务应该是幂等的(多次而不是一次运行不会得到不希望的结果)。
对于第二个问题,请确保下一个任务运行完成本应该由上一次的(错过的)运行完成的任何工作。
使用存活探针,让Kubernetes在容器不再健康的情况下立即重启它(应用程序定义了健康的条件)。
不应该直接创建pod,因为如果它们被错误地删除,它们正在运行的节点异常,或者它们从节点中被逐出时,它们将不会被重新创建。
ReplicationController始终保持所需数量的pod副本正在运行。
水平缩放pod与在ReplicationController上更改所需的副本个数一样简单。
pod不属于ReplicationController,如有必要可以在它们之间移动。
ReplicationController将从pod模板创建新的pod。更改模板对现有的pod没有影响。
ReplicationController应该替换为ReplicaSet和Deployment,它们提供相同的能力,但具有额外的强大功能。
ReplicationController和ReplicaSet将pod安排到随机集群节点,而DaemonSet确保每个节点都运行一个DaemonSet中定义的pod实例。
执行批处理任务的pod应通过Kubernetes Job资源创建,而不是直接或通过ReplicationController或类似对象创建。
需要在未来某个时候运行的Job可以通过CronJob资源创建。
尽管特定的pod可以独立地应对外部刺激,现在大多数应用都需要根据外部请求做出响应。例如,就微服务而言,pod通常需要对来自集群内部其他pod,以及来自集群外部的客户端的HTTP请求做出响应。
pod需要一种寻找其他pod的方法来使用其他pod提供的服务,不像在没有Kubernetes的世界,系统管理员要在用户端配置文件中明确指出服务的精确的IP地址或者主机名来配置每个客户端应用,但是同样的方式在Kubernetes中并不适用,因为
pod是短暂的——它们随时会启动或者关闭,无论是为了给其他pod提供空间而从节点中被移除,或者是减少了pod的数量,又或者是因为集群中存在节点异常。 Kubernetes在pod启动前会给已经调度到节点上的pod分配IP地址——因此客户端不能提前知道提供服务的pod的IP地址。
水平伸缩意味着多个pod可能会提供相同的服务——每个pod都有自己的IP地址,客户端无须关心后端提供服务pod的数量,以及各自对应的IP地址。它们无须记录每个pod的IP地址。相反,所有的pod可以通过一个单一的IP地址进行访问。
为了解决上述问题,Kubernetes提供了一种资源类型——服务(service)
Kubernetes服务是一种为一组功能相同的pod提供单一不变的接入点的资源。
当服务存在时,它的IP地址和端口不会改变。
客户端通过IP地址和端口号建立连接,这些连接会被路由到提供该服务的任意一个pod上。
通过这种方式,客户端不需要知道每个单独的提供服务的pod的地址,这样这些pod就可以在集群中随时被创建或移除。
回顾一下有前端web服务器和后端数据库服务器的例子。有很多pod提供前端服务,而只有一个pod提供后台数据库服务。需要解决两个问题才能使系统发挥作用。
外部客户端无须关心服务器数量而连接到前端pod上。
前端的pod需要连接后端的数据库。由于数据库运行在pod中,它可能会在集群中移来移去,导致IP地址变化。当后台数据库被移动时,无须对前端pod重新配置。
通过为前端pod创建服务,并且将其配置成可以在集群外部访问,可以暴露一个单一不变的IP地址让外部的客户端连接pod。同理,可以为后台数据库pod创建服务,并为其分配一个固定的IP地址。尽管pod的IP地址会改变,但是服务的IP地址固定不变。另外,通过创建服务,能够让前端的pod通过环境变量或DNS以及服务名来访问后端服务。系统中所有的元素都在图5.1中展示出来(两种服务、支持这些服务的两套pod,以及它们之间的相互依赖关系)。
服务的后端可以有不止一个pod。服务的连接对所有的后端pod是负载均衡的
如何准确地定义哪些pod属于服务哪些不属于呢?
ReplicationController和其他的pod控制器中使用标签选择器来指定哪些pod属于同一组。服务使用相同的机制
标签选择器决定哪些pod属于服务
创建服务的最简单的方法是通过kubectl expose,在第2章中曾使用这种方法来暴露创建的ReplicationController。像创建ReplicationController时使用的pod选择器那样,利用expose命令和pod选择器来创建服务资源,从而通过单个的IP和端口来访问所有的pod。
现在,除了使用expose命令,可以通过将配置的YAML文件传递到Kubernetes API服务器来手动创建服务。
服务的定义:kubia-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
创建了一个名叫kubia的服务,它将在端口80接收请求并将连接路由到具有标签选择器是app=kubia的pod的8080端口上。
接下来通过使用kubectl create发布文件来创建服务。
在发布完YAML文件后,可以在命名空间下列出来所有的服务资源,并可以发现新的服务已经被分配了一个内部集群IP。
kubectl get svc
列表显示分配给服务的IP地址是10.111.249.153。
因为只是集群的IP地址,只能在集群内部可以被访问。
服务的主要目标就是使集群内部的其他pod可以访问当前这组pod,但通常也希望对外暴露服务
现在,从集群内部使用创建好的服务并了解服务的功能
可以通过以下几种方法向服务发送请求:
显而易见的方法是创建一个pod,它将请求发送到服务的集群IP并记录响应。可以通过查看pod日志检查服务的响应。
使用ssh远程登录到其中一个Kubernetes节点上,然后使用curl命令。
可以通过kubectl exec命令在一个已经存在的pod中执行curl命令。
最后一种方法——如何在已有的pod中运行命令。
可以使用kubectl exec命令远程地在一个已经存在的pod容器上执行任何命令。
这样就可以很方便地了解pod的内容、状态及环境。
用kubectl get pod命令列出所有的pod,并且选择其中一个作为exec命令的执行目标(在下述例子中,选择kubia-7nog1 pod作为目标)。
也可以获得服务的集群IP(比如使用kubectl get svc命令),当执行下述命令时,请确保替换对应pod的名称及服务IP地址。
kkubectl exec kubia-7nog1 -- curl -s http://10.111.249.153
如果之前使用过ssh命令登录到一个远程系统,会发现kubectl exec没有特别大的不同之处
双横杠(--)代表着kubectl命令项的结束。
在两个横杠之后的内容是指在pod内部需要执行的命令。
如果需要执行的命令并没有以横杠开始的参数,横杠也不是必需的。
如果这里不使用横杠号,-s选项会被解析成kubectl exec选项,会导致结果异常和歧义错误。
在一个pod容器上,利用Kubernetes去执行curl命令。
curl命令向一个后端有三个pod服务的IP发送了HTTP请求,Kubernetes服务代理截取的该连接,在三个pod中任意选择了一个pod,然后将请求转发给它。
Node.js在pod中运行处理请求,并返回带有pod名称的HTTP响应。
接着,curl命令向标准输出打印返回值,该返回值被kubectl截取并打印到宕主机的标准输出。
使用kubectl exec通过在一个pod中运行curl命令来测试服务是否连通
在之前的例子中,在pod主容器中以独立进程的方式执行了curl命令。这与容器真正的主进程和服务通信并没有什么区别。
如果多次执行同样的命令,每次调用执行应该在不同的pod上。因为服务代理通常将每个连接随机指向选中的后端pod中的一个,即使连接来自于同一个客户端。
另一方面,如果希望特定客户端产生的所有请求每次都指向同一个pod,可以设置服务的sessionAffinity属性为ClientIP(而不是None,None是默认值),
会话亲和性被设置成ClientIP的服务的例子
kubia-svc-client-ip-session-affinity.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
sessionAffinity: ClientIP
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
这种方式将会使服务代理将来自同一个client IP的所有请求转发至同一个pod上
Kubernetes仅仅支持两种形式的会话亲和性服务:None和ClientIP。
你或许惊讶竟然不支持基于cookie的会话亲和性的选项,但是你要了解Kubernetes 服务不是在HTTP层面上工作。服务处理TCP和UDP包,并不关心其中的载荷内容。
因为cookie是HTTP协议中的一部分,服务并不知道它们,这就解释了为什么会话亲和性不能基于cookie。
比如,你的pod监听两个端口,比如HTTP监听8080端口、HTTPS监听8443端口,可以使用一个服务从端口80和443转发至pod端口8080和8443。
在这种情况下,无须创建两个不同的服务。通过一个集群IP,使用一个服务就可以将多个端口全部暴露出来。
在创建一个有多个端口的服务的时候,必须给每个端口指定名字。
在服务定义中指定多端口 kubia-svc-named-ports.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
app: kubia
标签选择器应用于整个服务,不能对每个端口做单独的配置
可以在服务spec中按名称引用这些端口
在服务中引用命名pod
采用命名端口的方式,最大的好处就是即使更换端口号也无须更改服务spec
在你的pod向新端口更新时,根据pod收到的连接,用户连接将会转发到对应的端口号上。
通过创建服务,现在就可以通过一个单一稳定的IP地址访问到pod。在服务整个生命周期内这个地址保持不变。在服务后面的pod可能删除重建,它们的IP地址可能改变,数量也会增减,但是始终可以通过服务的单一不变的IP地址访问到这些pod。
在pod开始运行的时候,Kubernetes会初始化一系列的环境变量指向现在存在的服务。如果你创建的服务早于客户端pod的创建,pod上的进程可以根据环境变量获得服务的IP地址和端口号。
如果服务的创建晚于pod的创建,那么关于这个服务的环境变量并没有设置,需要删除所有的pod使得ReplicationController创建全新的pod
kubectl delete po --all
通过在容器中运行env来列出所有的环境变量
kubectl exec kubia-xxxx env
在集群中定义了两个服务:kubernetes和kubia
所以,列表中显示了和这两个服务相关的环境变量
在本章开始部分,创建了kubia服务,在和其有关的环境变量中有KUBIA_SERVICE_HOST和KUBIA_SERVICE_PORT,分别代表了kubia服务的IP地址和端口号。
回顾本章开始部分的前后端的例子,当前端pod需要后端数据库服务pod时,可以通过名为backend-database的服务将后端pod暴露出来,然后前端pod通过环境变量BACKEND_DATABASE_SERVICE_HOST和BACKEND_DATABASE_SERVICE_PORT去获得IP地址和端口信息。
服务名称中的横杠被转换为下画线,并且当服务名称用作环境变量名称中的前缀时,所有的字母都是大写的。
环境变量是获得服务IP地址和端口号的一种方式
在kube-system命名空间,其中一个pod被称作kube-dns
这个pod运行DNS服务
在集群中的其他pod都被配置成使用其作为dns(Kubernetes通过修改每个容器的/etc/resolv.conf文件实现)。运行在pod上的进程DNS查询都会被Kubernetes自身的DNS 服务器响应,该服务器知道系统中运行的所有服务。
pod是否使用内部的DNS服务器是根据pod中spec的dnsPolicy属性来决定的。
每个服务从内部DNS 服务器中获得一个DNS条目,客户端的pod在知道服务名称的情况下可以通过全限定域名(FQDN)来访问,而不是诉诸于环境变量。
前端-后端的例子,前端pod可以通过打开以下FQDN的连接来访问后端数据库服务:
backend-database.default.svc.cluster.local
backend-database对应于服务名称,
default表示服务在其中定义的名称空间,
而svc.cluster.local是在所有集群本地服务名称中使用的可配置集群域后缀。
客户端仍然必须知道服务的端口号。如果服务使用标准端口号(例如,HTTP的80端口或Postgres的5432端口),这样是没问题的。如果并不是标准端口,客户端可以从环境变量中获取端口号。
连接一个服务可能比这更简单。如果前端pod和数据库pod在同一个命名空间下,可以省略svc.cluster.local后缀,甚至命名空间。因此可以使用backend-database来指代服务
kubectl exec 命令需要添加–it选项
kubectl exec -it kubia-xxx bash
curl http://kubia.default.svc.cluster.local
curl http://kubia.default
curl http://kubia
在请求的URL中,可以将服务的名称作为主机名来访问服务。
因为根据每个pod容器DNS解析器配置的方式,可以将命名空间和svc.cluster.local后缀省略掉。
查看一下容器中的/etc/resilv.conf文件就明白了。
curl这个服务是工作的,但是却ping不通。
这是因为服务的集群IP是一个虚拟IP,并且只有在与服务端口结合时才有意义
通过Kubernetes服务特性暴露外部服务的情况。不要让服务将连接重定向到集群中的pod,而是让它重定向到外部IP和端口。
可以让你充分利用服务负载平衡和服务发现。在集群中运行的客户端pod可以像连接到内部服务一样连接到外部服务。
服务并不是和pod直接相连的。相反,有一种资源介于两者之间——它就是Endpoint资源
Endpoint资源就是暴露一个服务的IP地址和端口的列表,Endpoint资源和其他Kubernetes资源一样,所以可以使用kubectl info来获取它的基本信息。
kubectl get endpoints kubia
尽管在 spec服务中定义了pod选择器,但在重定向传入连接时不会直接使用它。相反,选择器用于构建IP和端口列表,然后存储在Endpoint资源中。当客户端连接到服务时,服务代理选择这些IP和端口对中的一个,并将传入连接重定向到在该位置监听的服务器。
服务的 endpoint与服务解耦后,可以分别手动配置和更新它们。
如果创建了不包含pod选择器的服务,Kubernetes将不会创建Endpoint资源(毕竟,缺少选择器,将不会知道服务中包含哪些pod)。这样就需要创建Endpoint资源来指定该服务的endpoint列表。
要使用手动配置endpoint的方式创建服务,需要创建服务和Endpoint资源。
不含pod选择器的服务:external-service.yaml
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
ports:
- port: 80
定义一个名为external-service的服务,它将接收端口80上的传入连接。并没有为服务定义一个pod选择器。
Endpoint是一个单独的资源并不是服务的一个属性。由于创建的资源中并不包含选择器,相关的Endpoints资源并没有自动创建,所以必须手动创建
手动创建Endpoint资源:external-service-endpoints.yaml
apiVersion: v1
kind: Endpoints
metadata:
name: external-service
subsets:
- addresses:
- ip: 11.11.11.11
- ip: 22.22.22.22
ports:
- port: 80
Endpoint对象需要与服务具有相同的名称,并包含该服务的目标IP地址和端口列表。服务和Endpoint资源都发布到服务器后,这样服务就可以像具有pod选择器那样的服务正常使用。在服务创建后创建的容器将包含服务的环境变量,并且与其IP:port对的所有连接都将在服务端点之间进行负载均衡。
可以为服务添加选择器,从而对Endpoint进行自动管理。反过来也是一样的——将选择器从服务中移除,Kubernetes将停止更新Endpoints。这意味着服务的IP地址可以保持不变,同时服务的实际实现却发生了改变。
pod关联到具有两个外部endpoint的服务上
除了手动配置服务的Endpoint来代替公开外部服务方法,有一种更简单的方法,就是通过其完全限定域名(FQDN)访问外部服务
要创建一个具有别名的外部服务的服务时,要将创建服务资源的一个type字段设置为ExternalName。
例如,设想一下在api.somecompany.com上有公共可用的API,可以定义一个指向它的服务
ExternalName类型的服务:external-service-externalname.yaml
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ExternalName
externalName: api.somecompany.com
ports:
- port: 80
服务创建完成后,pod可以通过external-service.default.svc.cluster.local域名(甚至是external-service)连接到外部服务,而不是使用服务的实际FQDN。这隐藏了实际的服务名称及其使用该服务的pod的位置,允许修改服务定义,并且在以后如果将其指向不同的服务,只需简单地修改externalName属性,或者将类型重新变回ClusterIP并为服务创建Endpoint——无论是手动创建,还是对服务上指定标签选择器使其自动创建。
ExternalName 服务仅在DNS级别实施——为服务创建了简单的CNAME DNS记录。因此,连接到服务的客户端将直接连接到外部服务,完全绕过服务代理。出于这个原因,这些类型的服务甚至不会获得集群IP。
CNAME记录指向完全限定的域名而不是数字IP地址。
向外部公开某些服务。例如前端web服务器,以便外部客户端可以访问它们
将服务暴露给外部客户端
每个集群节点都会在节点上打开一个端口,对于NodePort服务,每个集群节点在节点本身(因此得名叫NodePort)上打开一个端口,并将在该端口上接收到的流量重定向到基础服务。
该服务仅在内部集群IP和端口上才可访问,但也可通过所有节点上的专用端口访问。
NodePort类型的一种扩展——这使得服务可以通过一个专用的负载均衡器来访问,这是由Kubernetes中正在运行的云基础设施提供的。
负载均衡器将流量重定向到跨所有节点的节点端口。
客户端通过负载均衡器的IP连接到服务。
这是一个完全不同的机制,通过一个IP地址公开多个服务——它运行在HTTP层(网络协议第7层)上,因此可以提供比工作在第4层的服务更多的功能。
将一组pod公开给外部客户端的第一种方法是创建一个服务并将其类型设置为NodePort。通过创建NodePort服务,可以让Kubernetes在其所有节点上保留一个端口(所有节点上都使用相同的端口号),并将传入的连接转发给作为服务部分的pod。
这与常规服务类似(它们的实际类型是ClusterIP),但是不仅可以通过服务的内部集群IP访问NodePort 服务,还可以通过任何节点的IP和预留节点端口访问NodePort 服务。
NodePort服务定义:kubia-svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia-nodeport
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30123
selector:
app: kubia
将类型设置为NodePort并指定该服务应该绑定到的所有集群节点的节点端口。指定端口不是强制性的。如果忽略它,Kubernetes将选择一个随机端口。
kubectl get svc kubia-nodeport
10.11.254.223:80
<1stnode'sIP>:30123
<2ndnode'sIP>:30123
,等等
在第一个节点的端口30123收到的连接,可以被重定向到第一节点个上运行的pod,也可能是第二个节点上运行的pod。
更改防火墙规则,让外部客户端访问我们的NodePort服务
在通过节点端口访问服务之前,需要配置谷歌云平台的防火墙,以允许外部连接到该端口上的节点
可以通过其中一个节点的IP的端口30123访问服务,但是需要首先找出节点的IP
可以在节点的JSON或YAML描述符中找到IP。但并不是在很大的JSON中筛选,而是可以利用kubectl只打印出节点IP而不是整个服务的定义。
通过指定kubectl的JSONPath,使得其只输出需要的信息。你可能已经熟悉XPath,并且知道如何使用XML,JSONPath基本上是JSON的XPath。上例中的JSONPath指示kubectl执行以下操作:
浏览item属性中的所有元素。
对于每个元素,输入status属性。
过滤address属性的元素,仅包含那些具有将type属性设置为ExternalIP的元素。
最后,打印过滤元素的address属性。
要了解有关kubectl使用JSONPath的更多信息,请参阅 http://kubernetes.io/docs/user-guide/jsonpath 上的文档。
使用Minikube时,可以运行minikube sevrvice
[-n ]命令,通过浏览器轻松访问NodePort服务。 现在整个互联网可以通过任何节点上的30123端口访问到你的pod。客户端发送请求的节点并不重要。但是,如果只将客户端指向第一个节点,那么当该节点发生故障时,客户端无法再访问该服务。这就是为什么将负载均衡器放在节点前面以确保发送的请求传播到所有健康节点,并且从不将它们发送到当时处于脱机状态的节点的原因。
如果Kubernetes集群支持它(当Kubernetes部署在云基础设施上时,大多数情况都是如此),那么可以通过创建一个Load Badancer而不是NodePort服务自动生成负载均衡器。
在云提供商上运行的Kubernetes集群通常支持从云基础架构自动提供负载平衡器。所有需要做的就是设置服务的类型为Load Badancer而不是NodePort。负载均衡器拥有自己独一无二的可公开访问的IP地址,并将所有连接重定向到服务。可以通过负载均衡器的IP地址访问服务。
如果Kubernetes在不支持Load Badancer服务的环境中运行,则不会调配负载平衡器,但该服务仍将表现得像一个NodePort服务。这是因为Load Badancer服务是NodePort服务的扩展。可以在支持Load Badancer服务的Google Kubernetes Engine上运行此示例。Minikube没有
Load Badancer类型的服务:kubia-svc-loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia-loadbalancer
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
服务类型设置为LoadBalancer而不是NodePort。如果没有指定特定的节点端口,Kubernetes将会选择一个端口。
创建服务后,云基础架构需要一段时间才能创建负载均衡器并将其IP地址写入服务对象。一旦这样做了,IP地址将被列为服务的外部IP地址:
负载均衡器的IP地址为130.211.53.173,因此现在可以通过该IP地址访问该服务:
curl http://130.211.53.173
这次不需要像以前使用NodePort服务那样来关闭防火墙。
使用kubectl explain,可以再次检查服务的会话亲缘性是否仍然设置为None
不同的浏览器请求不会碰到不同的pod,就像使用curl时那样
浏览器使用keep-alive连接,并通过单个连接发送所有请求,而curl每次都会打开一个新连接。服务在连接级别工作,所以当首次打开与服务的连接时,会选择一个随机集群,然后将属于该连接的所有网络数据包全部发送到单个集群。即使会话亲和性设置为None,用户也会始终使用相同的pod(直到连接关闭)。
外部客户端(可以使用curl)连接到负载均衡器的80端口,并路由到其中一个节点上的隐式分配节点端口。之后该连接被转发到一个pod实例。
如果使用kubectl describe来显示有关该服务的其他信息,则会看到为该服务选择了一个节点端口。如果要为此端口打开防火墙,就像在上一节中对NodePort服务所做的那样,也可以通过节点IP访问服务。
如果使用的是Minikube,尽管负载平衡器不会被分配,仍然可以通过节点端口(位于Minikube VM的IP地址)访问该服务
外部客户端连接一个LoadBalancer服务
当外部客户端通过节点端口连接到服务时(这也包括先通过负载均衡器时的情况),随机选择的pod并不一定在接收连接的同一节点上运行。可能需要额外的网络跳转才能到达pod,但这种行为并不符合期望。
可以通过将服务配置为仅将外部通信重定向到接收连接的节点上运行的pod来阻止此额外跳数。这是通过在服务的spec部分中设置externalTrafficPolicy字段来完成的:
如果服务定义包含此设置,并且通过服务的节点端口打开外部连接,则服务代理将选择本地运行的pod。如果没有本地pod存在,则连接将挂起(它不会像不使用注解那样,将其转发到随机的全局pod)。因此,需要确保负载平衡器将连接转发给至少具有一个pod的节点。
使用这个注解还有其他缺点。通常情况下,连接均匀分布在所有的pod上,但使用此注解时,情况就不再一样了。
想象一下两个节点有三个pod。假设节点A运行一个pod,节点B运行另外两个pod。如果负载平衡器在两个节点间均匀分布连接,则节点A上的pod将接收所有连接的50%,但节点B上的两个pod每个只能接收25%
使用local外部流量策略的服务可能会导致跨pod的负载分布不均衡
通常,当集群内的客户端连接到服务时,支持服务的pod可以获取客户端的IP地址。但是,当通过节点端口接收到连接时,由于对数据包执行了源网络地址转换(SNAT),因此数据包的源IP将发生更改。
后端的pod无法看到实际的客户端IP,这对于某些需要了解客户端IP的应用程序来说可能是个问题。例如,对于Web服务器,这意味着访问日志无法显示浏览器的IP。
上一节中描述的local外部流量策略会影响客户端IP的保留,因为在接收连接的节点和托管目标pod的节点之间没有额外的跳跃(不执行SNAT)。
Ingress资源
Ingress ——进入或进入的行为;进入的权利;进入的手段或地点;入口。
每个LoadBalancer 服务都需要自己的负载均衡器,以及独有的公有IP地址,而Ingress只需要一个公网IP就能为许多服务提供访问。当客户端向Ingress发送HTTP请求时,Ingress会根据请求的主机名和路径决定请求转发到的服务
通过一个Ingress暴露多个服务
Ingress在网络栈(HTTP)的应用层操作,并且可以提供一些服务不能实现的功能,诸如基于cookie的会话亲和性(session affinity)等功能。
只有Ingress控制器在集群中运行,Ingress资源才能正常工作。
不同的Kubernetes环境使用不同的控制器实现,但有些并不提供默认控制器。
Google Kubernetes Engine使用Google Cloud Platform带有的HTTP负载平衡模块来提供Ingress功能。
Minikube包含一个可以启用的附加组件,可以试用Ingress功能
需要确保已启用Ingress附加组件。可以通过列出所有附件来检查Ingress是否已启动:
启用Ingress附加组件,并查看正在运行的Ingress
minikube addons enable ingress
这应该会在另一个pod上运行一个Ingress控制器。控制器pod很可能位于kube-system命名空间中,但也不一定是这样,所以使用--all-namespaces选项列出所有命名空间中正在运行的pod:
kubectl get po --all-namespaces
在输出的底部,会看到Ingress控制器pod。该名称暗示Nginx(一种开源HTTP服务器并可以做反向代理)用于提供Ingress功能。
确认集群中正在运行Ingress控制器,可以创建一个Ingress资源
Ingress资源的定义:kubia-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kubia
spec:
rules:
- host: kubia.example.com
http:
paths:
- path: /
backend:
serviceName: kubia-nodeport
servicePort: 80
定义了一个单一规则的Ingress,确保Ingress控制器收到的所有请求主机kubia.example.com的HTTP请求,将被发送到端口80上的kubia-nodeport服务。
云供应商的Ingress控制器(例如GKE)要求Ingress指向一个NodePort服务。但Kubernetes并没有这样的要求。
要通过http://kubia.example.com访问服务,需要确保域名解析为Ingress控制器的IP。
列出Ingress
kubectl get ingresses
在云提供商的环境上运行时,地址可能需要一段时间才能显示,因为Ingress控制器在幕后调配负载均衡器。
IP在ADDRESS列中显示出来
一旦知道IP地址,通过配置DNS服务器将kubia.example.com解析为此IP地址,
或者在
/ect/hosts
文件(Windows系统为C:\windows\system32\drivers\etc\hosts)
中添加下面一行内容:
192.168.99.100 kubia.example.com
环境都已经建立完毕,可以通过http://kubia.example.com地址访问服务(使用浏览器或者curl命令):
curl http://kubia.example.com
现在已经通过Ingress成功访问了该服务
客户端如何通过Ingress控制器连接到其中一个pod。
客户端首先对kubia.example.com执行DNS查找,DNS服务器(或本地操作系统)返回了Ingress控制器的IP。
客户端然后向Ingress控制器发送HTTP请求,并在Host头中指定kubia.example.com。
控制器从该头部确定客户端尝试访问哪个服务,通过与该服务关联的Endpoint对象查看pod IP,并将客户端的请求转发给其中一个pod
Ingress控制器不会将请求转发给该服务,只用它来选择一个pod。大多数(即使不是全部)控制器都是这样工作的
通过Ingress访问pod
查看Ingress规范,则会看到rules和paths都是数组,因此它们可以包含多个条目。
一个Ingress可以将多个主机和路径映射到多个服务
将不同的服务映射到相同主机的不同paths
在同一个主机、不同的路径上,Ingress暴露出多个服务
在这种情况下,根据请求的URL中的路径,请求将发送到两个不同的服务。
因此,客户端可以通过一个IP地址(Ingress控制器的IP地址)访问两种不同的服务。
可以使用Ingress根据HTTP请求中的主机而不是(仅)路径映射到不同的服务
Ingress根据不同的主机(host)暴露出多种服务
根据请求中的Host头(虚拟主机在网络服务器中处理的方式),控制器收到的请求将被转发到foo服务或bar服务。
DNS需要将foo.example.com和bar.example.com域名都指向Ingress控制器的IP地址。
配置Ingress以支持TLS。
当客户端创建到Ingress控制器的TLS连接时,控制器将终止TLS连接。
客户端和控制器之间的通信是加密的,而控制器和后端pod之间的通信则不是。
运行在pod上的应用程序不需要支持TLS。
例如,如果pod运行web服务器,则它只能接收HTTP通信,并让Ingress控制器负责处理与TLS相关的所有内容。
要使控制器能够这样做,需要将证书和私钥附加到Ingress。
这两个必需资源存储在称为Secret的Kubernetes资源中,然后在Ingress manifest中引用它。
openssl genrsa -out tls.key 2048
openssl req -new -x509 -key tls.key -out tls.cert -days 360 -subj /CN=kubia.example.com
kubectl create secret tls tls-secret --cert=tls.cert --key=tls.key
可以不通过自己签署证书,而是通过创建CertificateSigningRequest(CSR)资源来签署。用户或他们的应用程序可以创建一个常规证书请求,将其放入CSR中,然后由人工操作员或自动化程序批准请求
kubectl certficate approve <name of the CSR>
然后可以从CSR的status.certificate字段中检索签名的证书。
证书签署者组件必须在集群中运行,否则创建CertificateSigningRequest以及批准或拒绝将不起作用。
私钥和证书现在存储在名为tls-secret的Secret中。现在,可以更新Ingress对象,以便它也接收kubia.example.com的HTTPS请求
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kubia
spec:
tls:
- hosts:
- kubia.example.com
secretName: tls-secret
rules:
- host: kubia.example.com
http:
paths:
- path: /
backend:
serviceName: kubia-nodeport
servicePort: 80
可以调用
kubectl apply-f kubia-ingress-tls.yaml
使用文件中指定的内容来更新Ingress资源,而不是通过删除并从新文件重新创建的方式。现在可以使用HTTPS通过Ingress访问服务:
对Ingress功能的支持因不同的Ingress控制器实现而异
Ingress是一个相对较新的Kubernetes功能,因此可以预期将来会看到许多改进和新功能。虽然目前仅支持L7(网络第7层)(HTTP /HTTPS)负载平衡,但也计划支持L4(网络第4层)负载平衡。
不要将请求转发到正在启动的pod中,直到完全准备就绪。
Kubernetes还允许为容器定义准备就绪探针。
就绪探测器会定期调用,并确定特定的pod是否接收客户端请求。当容器的准备就绪探测返回成功时,表示容器已准备好接收请求。
这个准备就绪的概念显然是每个容器特有的东西。Kubernetes只能检查在容器中运行的应用程序是否响应一个简单的GET/请求,或者它可以响应特定的URL路径(该URL导致应用程序执行一系列检查以确定它是否准备就绪)。考虑到应用程序的具体情况,这种确切的准备就绪的判定是应用程序开发人员的责任。
执行进程的地方。容器的状态由进程的退出状态代码确定。
向容器发送HTTP GET请求,通过响应的HTTP状态代码判断容器是否准备好。
它打开一个TCP连接到容器的指定端口。如果连接已建立,则认为容器已准备就绪。
启动容器时,可以为Kubernetes配置一个等待时间,经过等待时间后才可以执行第一次准备就绪检查。
之后,它会周期性地调用探针,并根据就绪探针的结果采取行动。
如果某个pod报告它尚未准备就绪,则会从该服务中删除该pod。
如果pod再次准备就绪,则重新添加pod。
与存活探针不同,如果容器未通过准备检查,则不会被终止或重新启动。这是存活探针与就绪探针之间的重要区别。存活探针通过杀死异常的容器并用新的正常容器替代它们来保持pod正常工作,而就绪探针确保只有准备好处理请求的pod才可以接收它们(请求)。这在容器启动时最为必要,当然在容器运行一段时间后也是有用的。
如果一个容器的就绪探测失败,则将该容器从端点对象中移除。连接到该服务的客户端不会被重定向到pod。这和pod与服务的标签选择器完全不匹配的效果相同。
就绪探针失败的pod从服务的endpoint中移除
设想一组pod(例如,运行应用程序服务器的pod)取决于另一个pod(例如,后端数据库)提供的服务。如果任何一个前端连接点出现连接问题并且无法再访问数据库,那么就绪探针可能会告知Kubernetes该pod没有准备好处理任何请求。如果其他pod实例没有遇到类似的连接问题,则它们可以正常处理请求。就绪探针确保客户端只与正常的pod交互,并且永远不会知道系统存在问题。
在文本编辑器中打开ReplicationController的YAML时,在pod模板中查找容器规格,并将以下就绪探针定义添加到spec.template.spec.containers下的第一个容器
RC创建带有就绪探针的pod:kubia-rc-readinessprobe.yaml
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3
selector:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ports:
- name: http
containerPort: 8080
readinessProbe:
exec:
command:
- ls
- /var/ready
就绪探针将定期在容器内执行ls/var/ready命令。如果文件存在,则ls命令返回退出码0,否则返回非零的退出码。如果文件存在,则就绪探针将成功;否则,它会失败。
定义这样一个奇怪的就绪探针的原因是,可以通过创建或删除有问题的文件来触发结果。
更改ReplicationController的pod模板对现有的pod没有影响。
现有的所有pod仍没有定义准备就绪探针
需要删除pod并让它们通过ReplicationController重新创建。新的pod将进行就绪检查会一直失败,并且不会将其作为服务的端点,直到在每个pod中创建/var/ready文件。
READY列显示出没有一个容器准备好。现在通过创建/var/ready文件使其中一个文件的就绪探针返回成功,该文件的存在可以模拟就绪探针成功:
准备就绪探针会定期检查——默认情况下每10秒检查一次。由于尚未调用就绪探针,因此容器未准备好。但是最晚10秒钟内,该pod应该已经准备就绪,其IP应该列为service的endpoint(运行kubectl get endpoint kubialoadbalancer来确认)。
在实际应用中,应用程序是否可以(并且希望)接收客户端请求,决定了就绪探测应该返回成功或失败。
应该通过删除pod或更改pod标签而不是手动更改探针来从服务中手动移除pod
如果想要从某个服务中手动添加或删除pod,请将enabled=true作为标签添加到pod,以及服务的标签选择器中。当想要从服务中移除pod时,删除标签。
首先,如果没有将就绪探针添加到pod中,它们几乎会立即成为服务端点。如果应用程序需要很长时间才能开始监听传入连接,则在服务启动但尚未准备好接收传入连接时,客户端请求将被转发到该pod。因此,客户端会看到“连接被拒绝”类型的错误。
应该始终定义一个就绪探针,即使它只是向基准URL发送HTTP请求一样简单。
不要将停止pod的逻辑纳入就绪探针中
当一个容器关闭时,运行在其中的应用程序通常会在收到终止信号后立即停止接收连接。因此,可能认为只要启动关机程序,就需要让就绪探针返回失败,以确保从所有服务中删除该pod。但这不是必需的,因为只要删除该容器,Kubernetes就会从所有服务中移除该容器。
要让客户端连接到所有pod,需要找出每个pod的IP。一种选择是让客户端调用Kubernetes API服务器并通过API调用获取pod及其IP地址列表,但由于应始终努力保持应用程序与Kubernetes无关,因此使用API 服务器并不理想。
Kubernetes允许客户通过DNS查找发现pod IP。通常,当执行服务的DNS查找时,DNS服务器会返回单个IP——服务的集群IP。但是,如果告诉Kubernetes,不需要为服务提供集群IP(通过在服务 spec中将clusterIP字段设置为None来完成此操作),则DNS服务器将返回pod IP而不是单个服务IP。
DNS服务器不会返回单个DNS A记录,而是会为该服务返回多个A记录,每个记录指向当时支持该服务的单个pod的IP。客户端因此可以做一个简单的DNS A记录查找并获取属于该服务一部分的所有pod的IP。客户端可以使用该信息连接到其中的一个、多个或全部。
将服务 spec中的clusterIP字段设置为None会使服务成为headless服务,因为Kubernetes不会为其分配集群IP,客户端可通过该IP将其连接到支持它的pod。
一个headless服务:kubia-svc-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia-headless
spec:
clusterIP: None
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
在集群中运行的一个pod中执行DNS查询
可以使用Docker Hub上提供的tutum/dnsutils容器镜像,它包含nslookup和dig二进制文件。
要运行pod,可以完成创建YAML清单并将其传给kubectl create的整个过程。但是太烦琐了,对吗?幸运的是,有一个更快的方法。
这次只想创建一个pod,不需要创建一个ReplicationController来管理pod
kubectl run dnsutils --image=tutum/dnsutils --generator=run-pod/v1 --command -- sleep infinity
诀窍在--generator=run-pod/v1选项中,该选项让kubectl直接创建pod,而不需要通过ReplicationController之类的资源来创建。
使用新创建的pod执行DNS查找:
kubectl exec dnsutils nslookup kubia-headless
DNS服务器为kubia-headless.default.svc.cluster.local FQDN返回两个不同的IP。
这些是报告准备就绪的两个pod的IP。
可以通过使用kubectl get pods-o wide列出pod来确认此问题,该清单显示了pod的IP。
这与常规(非headless服务)服务返回的DNS不同,比如kubia服务,返回的IP是服务的集群IP:
kubectl exec dnsutils nslookup kubia
尽管headless服务看起来可能与常规服务不同,但在客户的视角上它们并无不同。即使使用headless服务,客户也可以通过连接到服务的DNS名称来连接到pod上,就像使用常规服务一样。但是对于headless服务,由于DNS返回了pod的IP,客户端直接连接到该pod,而不是通过服务代理。
headless服务仍然提供跨pod的负载平衡,但是通过DNS轮询机制不是通过服务代理
可以使用DNS查找机制来查找那些未准备好的pod。
要告诉Kubernetes无论pod的准备状态如何,希望将所有pod添加到服务中。必须将以下注解添加到服务中:
注解名称表明了这是一个alpha功能。
Kubernetes Service API已经支持一个名为publishNotReadyAddresses的新服务规范字段,它将替换tolerate-unready-endpoints注解。在Kubernetes 1.9.0版本中,这个字段还没有实现(这个注解决定了未准备好的endpoints是否在DNS的记录中)。检查文档以查看是否已更改。
如果无法通过服务访问pod,应该根据下面的列表进行排查:
首先,确保从集群内连接到服务的集群IP,而不是从外部。
不要通过ping服务IP来判断服务是否可访问(请记住,服务的集群IP是虚拟IP,是无法ping通的)。
如果已经定义了就绪探针,请确保它返回成功;否则该pod不会成为服务的一部分。
要确认某个容器是服务的一部分,请使用kubectl get endpoints来检查相应的端点对象。
如果尝试通过FQDN或其中一部分来访问服务(例如,myservice.mynamespace.svc.cluster.local或myservice.mynamespace),但并不起作用,请查看是否可以使用其集群 IP而不是FQDN来访问服务。
检查是否连接到服务公开的端口,而不是目标端口。
尝试直接连接到pod IP以确认pod正在接收正确端口上的连接。
如果甚至无法通过pod的IP访问应用,请确保应用不是仅绑定到本地主机。
创建Kubernetes服务资源来暴露应用程序中可用的服务,无论每个服务后端有多少pod实例
在一个固定的IP地址和端口下暴露匹配到某个标签选择器的多个pod
服务在集群内默认是可访问的,通过将服务的类型设置为NodePort或LoadBalancer,使得服务也可以从集群外部访问
让pod能够通过查找环境变量发现服务的IP地址和端口
允许通过创建服务资源而不指定选择器来发现驻留在集群外部的服务并与之通信,方法是创建关联的Endpoint资源
为具有ExternalName服务类型的外部服务提供DNS CNAME别名
通过单个Ingress公开多个HTTP服务(使用单个IP)
使用pod容器的就绪探针来确定是否应该将pod包含在服务endpoints内
通过创建headless服务让DNS发现pod IP
修改Google Kubernetes/Compute Engine中的防火墙规则
通过kubectl exec在pod容器中执行命令
在现有容器的容器中运行一个bash shell
通过kubectl apply命令修改Kubernetes资源
使用kubectl run--generator=run-pod/v1运行临时的pod
容器是如何访问外部磁盘存储
pod类似逻辑主机,在逻辑主机中运行的进程共享诸如CPU、RAM、网络接口等资源。人们会期望进程也能共享磁盘,但事实并非如此。,pod中的每个容器都有自己独立的文件系统,因为文件系统来自容器镜像。
每个新容器都是通过在构建镜像时加入的详细配置文件来启动的。将此与pod中容器重新启动的现象结合起来(也许是因为进程崩溃,也许是存活探针向Kubernetes发送了容器状态异常的信号),你就会意识到新容器并不会识别前一个容器写入文件系统内的任何内容,即使新启动的容器运行在同一个pod中。
在某些场景下,我们可能希望新的容器可以在之前容器结束的位置继续运行,比如在物理机上重启进程。可能不需要(或者不想要)整个文件系统被持久化,但又希望能保存实际数据的目录。
Kubernetes通过定义存储卷来满足这个需求,它们不像pod这样的顶级资源,而是被定义为pod的一部分,并和pod共享相同的生命周期。这意味着在pod启动时创建卷,并在删除pod时销毁卷。因此,在容器重新启动期间,卷的内容将保持不变,在重新启动容器之后,新容器可以识别前一个容器写入卷的所有文件。另外,如果一个pod包含多个容器,那这个卷可以同时被所有的容器使用。
ReplicationController(复制控制器)、ReplicaSet(副本服务器)、DaemonSet(守护进程集)、作业和服务
Kubernetes的卷是pod的一个组成部分,因此像容器一样在pod的规范中就定义了。它们不是独立的Kubernetes对象,也不能单独创建或删除。pod中的所有容器都可以使用卷,但必须先将它挂载在每个需要访问它的容器中。在每个容器中,都可以在其文件系统的任意位置挂载卷。
一个容器运行了一个web服务器,该web服务器的HTML页面目录位于
/var/htdocs
,并将站点访问日志存储到/var/logs
目录中。第二个容器运行了一个代理来创建HTML文件,并将它们存放在
/var/html
中,第三个容器处理在
/var/logs
目录中找到的日志(转换、压缩、分析它们或者做其他处理)同一个pod的三个容器没有共享存储
Linux允许在文件树中的任意位置挂载文件系统,当这样做的时候,挂载的文件系统内容在目录中是可以访问的。
通过将相同的卷挂载到两个容器中,它们可以对相同的文件进行操作。
在这个例子中,只需要在三个容器中挂载两个卷,这样三个容器将可以一起工作,并发挥作用。
三个容器共享挂载在不同安装路径的两个卷上
首先,pod有一个名为publicHtml的卷,这个卷被挂载在WebServer容器的/var/htdocs中,因为这是web服务器的服务目录。
在ContentAgent容器中也挂载了相同的卷,但在/var/html中,因为代理将文件写入/var/html中。
通过这种方式挂载这个卷,web服务器现在将为content agent生成的内容提供服务。
pod还拥有一个名为logVol的卷,用于存放日志,此卷在WebServer和LogRotator容器中的/var/log中挂载,
注意,它没有挂载在ContentAgent容器中,这个容器不能访问它的文件,即使容器和卷是同一个pod的一部分,在pod的规范中定义卷是不够的。
如果我们希望容器能够访问它,还需要在容器的规范中定义一个VolumeMount。
在本例中,两个卷最初都是空的,因此可以使用一种名为emptyDir的卷。
Kubernetes还支持其他类型的卷,这些卷要么是在从外部源初始化卷时填充的,要么是在卷内挂载现有目录。
这个填充或装入卷的过程是在pod内的容器启动之前执行的。
卷被绑定到pod的lifecycle(生命周期)中,只有在pod存在时才会存在,但取决于卷的类型,即使在pod和卷消失之后,卷的文件也可能保持原样,并可以挂载到新的卷中。
emptyDir——用于存储临时数据的简单空目录。
hostPath ——用于将目录从工作节点的文件系统挂载到pod中。
gitRepo——通过检出Git仓库的内容来初始化的卷。
nfs——挂载到pod中的NFS共享卷。
gcePersistentDisk(Google高效能型存储磁盘卷)、awsElastic BlockStore(AmazonWeb服务弹性块存储卷)、azureDisk(Microsoft Azure磁盘卷)——用于挂载云服务商提供的特定存储类型。
cinder、cephfs、iscsi、flocker、glusterfs、quobyte、rbd、flexVolume、vsphere-Volume、photonPersistentDisk、scaleIO用于挂载其他类型的网络存储。
configMap、secret、downwardAPI——用于将Kubernetes部分资源和集群信息公开给pod的特殊类型的卷。它们不是用于存储数据,而是用于将Kubernetes元数据公开给运行在pod中的应用程序。
persistentVolumeClaim——一种使用预置或者动态配置的持久存储类型
单个容器可以同时使用不同类型的多个卷
最简单的卷类型是emptyDir卷
顾名思义,卷从一个空目录开始,运行在pod内的应用程序可以写入它需要的任何文件。
因为卷的生存周期与pod的生存周期相关联,所以当删除pod时,卷的内容就会丢失。
一个emptyDir卷对于在同一个pod中运行的容器之间共享文件特别有用。
但是它也可以被单个容器用于将数据临时写入磁盘,例如在大型数据集上执行排序操作时,没有那么多内存可供使用
数据也可以写入容器的文件系统本身,
容器的文件系统甚至可能是不可写的,所以写到挂载的卷可能是唯一的选择
构建一个仅有web服务器容器内容代理和单独HTML的卷的pod。
将使用Nginx作为Web服务器和UNIX fortune命令来生成HTML内容,fortune命令每次运行时都会输出一个随机引用,可以创建一个脚本每10秒调用一次执行,并将其输出存储在index.html中
创建一个名为fortune的新目录,然后在其中创建一个具有以下内容的fortuneloop.sh的shell脚本
fortuneloop.sh
#!/bin/bash
trap "exit" SIGINT
mkdir /var/htdocs
while :
do
echo $(date) Writing fortune to /var/htdocs/index.html
/usr/games/fortune > /var/htdocs/index.html
sleep 10
done
然后,在同一个目录中,创建一个名为Dockerfile的文件
Dockerfile
FROM ubuntu:latest
RUN apt-get update ; apt-get -y install fortune
ADD fortuneloop.sh /bin/fortuneloop.sh
ENTRYPOINT /bin/fortuneloop.sh
该镜像基于ubuntu:latest镜像,默认情况下不包括fortune二进制文件。这就是为什么在Dockerfile的第二行中,需要使用apt-get安装它的原因。
之后,可以向镜像的/bin文件夹中添加fortuneloop.sh脚本。
在Dockerfile的最后一行中,指定镜像启动时执行fortuneloop.sh脚本。
使用以下两个命令(用自己的Docker Hub用户ID替换luksa)构建镜像并上传到Docker Hub:
docker build -t luksa/fortune
docker push liksa/fortune
现在有两个镜像需要运行在pod上,是时候创建pod的manifest了,创建一个名为fortune-pod.yaml的文件
一个pod中有两个共用同一个卷的容器 :fortune-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
containers:
- image: luksa/fortune
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {}
pod包含两个容器和一个挂载在两个容器中的共用的卷,但在不同的路径上。
当html-generator容器启动时,它每10秒启动一次fortune命令输出到/var/htdocs/index.html文件。因为卷是在/var/htdocs上挂载的,所以index.html文件被写入卷中,而不是容器的顶层。
一旦web-server容器启动,它就开始为/usr/share/nginx/html目录中的任意HTML文件提供服务(这是Nginx服务的默认服务文件目录)。
因为我们将卷挂载在那个确切的位置,Nginx将为运行fortune循环的容器输出的index.html文件提供服务。
最终的效果是,一个客户端向pod上的80端口发送一个HTTP请求,将接收当前的fortune消息作为响应。
为了查看fortune消息,需要启用对pod的访问,可以尝试将端口从本地机器转发到pod来实现:
kubectl port-forward fortune 8080:80
还可以通过服务来访问该pod,而不是单纯使用端口转发。
现在可以通过本地计算机的8080端口来访问Nginx服务器。通过执行curl命令:
curl http://localhost:8080
如果等待几秒发送另一个请求,则应该会接收到另一条信息。通过组合两个容器,就创建了一个简单的应用,通过这个应用可以观察到卷是如何将两个容器组合在一起,并分别增强它们各自的功能的。
作为卷来使用的emptyDir,是在承载pod的工作节点的实际磁盘上创建的,因此其性能取决于节点的磁盘类型。
但我们可以通知Kubernetes在tmfs文件系统(存在内存而非硬盘)上创建emptyDir。
因此,将emptyDir的medium设置为Memory:
emptyDir卷是最简单的卷类型,但是其他类型的卷都是在它的基础上构建的,在创建空目录后,它们会用数据填充它
gitRepo卷基本上也是一个emptyDir卷,它通过克隆Git仓库并在pod启动时(但在创建容器之前)检出特定版本来填充数据
gitRepo卷是一个emptyDir卷,最初填充了Git仓库的内容
在创建gitRepo卷后,它并不能和对应repo保持同步。
当向Git仓库推送新增的提交时,卷中的文件将不会被更新。
然而,如果所用的pod是由ReplicationController管理的,删除这个pod将触发新建一个新的pod,而这个新pod的卷中将包含最新的提交。
在创建pod之前,需要有一个包含HTML文件并实际可用的Git仓库,笔者在GitHub创建了一个repo
这次,只需要一个Nginx容器和一个gitRepo卷
使用gitRepo卷的pod: gitrepo-volume-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: gitrepo-volume-pod
spec:
containers:
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
gitRepo:
repository: https://github.com/luksa/kubia-website-example.git
revision: master
directory: .
在创建pod时,首先将卷初始化为一个空目录,然后将制定的Git仓库克隆到其中。
如果没有将目录设置为.(句点),存储库将会被克隆到kubia-website-example示例目录中,这不是我们想要的结果。
我们预期将repo克隆到卷的根目录中。
在设置存储库时,我们还需要指明让Kubernetes切换到master分支所在的版本来创建存储卷修订变更。
在pod运行时,我们可以尝试通过端口转发、服务或在pod(或集群中的任意其他pod)中运行curl命令来访问pod。
每次进行更改时,没必要每次都删除pod,可以运行一个附加进程来使卷与Git仓库保持同步
Git同步进程不应该运行在与Nginx站点服务器相同的容器中,而是在第二个容器:sidecar container。它是一种容器,增加了对pod主容器的操作。可以将一个sidecar添加到pod中,这样就可以使用现有的容器镜像,而不是将附加逻辑填入主应用程序的代码中,这会使它过于复杂和不可复用。
为了找到一个保持本地目录与Git仓库同步的现有容器镜像,转到Docker Hub并搜索“git syc”,可以看到很多可以实现的镜像。然后在示例中,从pod的一个新容器使用镜像,挂载pod现有的gitRepo卷到新容器中,并配置Git同步容器来保持文件与Git repo同步。如果正确设置了所有的内容,应该能看到web服务器正在加载的文件与GitHub repo同步。
另外还有一个原因,使得我们必须依赖于Git sync sidecar容器。我们还没有讨论过是否可以使用对应私有Git repo的gitRepo卷,其实不可行。Kubernetes开发人员的共识是保持gitRepo卷的简单性,而不添加任何通过SSH协议克隆私有存储库的支持,因为这需要向gitRepo卷添加额外的配置选项。 如果想要将私有的Git repo克隆到容器中,则应该使用gitsync sidecar或类似的方法,而不是使用gitRepo卷。
gitRepo容器就像emptyDir卷一样,基本上是一个专用目录,专门用于包含卷的容器并单独使用。当pod被删除时,卷及其内容被删除。然而,其他类型的卷并不创建新目录,而是将现有的外部目录挂载到pod的容器文件系统中。该卷的内容可以保存多个pod实例化
大多数pod应该忽略它们的主机节点,因此它们不应该访问节点文件系统上的任何文件。
但是某些系统级别的pod(切记,这些通常由DaemonSet管理)确实需要读取节点的文件或使用节点文件系统来访问节点设备。
Kubernetes通过hostPath卷实现了这一点。
hostPath卷指向节点文件系统上的特定文件或目录。
在同一个节点上运行并在其hostPath卷中使用相同路径的pod可以看到相同的文件。
hostPath卷将工作节点上的文件或目录挂载到容器的文件系统中
hostPath卷是我们介绍的第一种类型的持久性存储,因为gitRepo和emptyDir卷的内容都会在pod被删除时被删除,而hostPath卷的内容则不会被删除。如果删除了一个pod,并且下一个pod使用了指向主机上相同路径的hostPath卷,则新pod将会发现上一个pod留下的数据,但前提是必须将其调度到与第一个pod相同的节点上。
如果你正在考虑使用hostPath卷作为存储数据库数据的目录,请重新考虑。因为卷的内容存储在特定节点的文件系统中,所以当数据库pod被重新安排在另一个节点时,会找不到数据。这解释了为什么对常规pod使用hostPath卷不是一个好主意,因为这会使pod对预定规划的节点很敏感。
hostPath卷通常用于尝试单节点集群中的持久化存储,譬如Minikube创建的集群
仅当需要在节点上读取或写入系统文件时才使用hostPath,切勿使用它们来持久化跨pod的数据。
当运行在一个pod中的应用程序需要将数据保存到磁盘上,并且即使该pod重新调度到另一个节点时也要求具有相同的数据可用。这就不能使用到目前为止我们提到的任何卷类型,由于这些数据需要可以从任何集群节点访问,因此必须将其存储在某种类型的网络存储(NAS)中。
如果是在Google Kubernetes Engine中运行这些示例,那么由于集群节点是运行在Google Compute Engine(GCE)之上,则将使用GCE持久磁盘作为底层存储机制。
在早期版本中,Kubernetes没有自动配置底层存储,必须手动执行此操作。自动配置现在已经可以实现
首先,我们需要手动配置存储,这样可以让你有机会了解背后发生了什么。
首先创建GCE持久磁盘。我们需要在同一区域的Kubernetes 集群中创建它,如果你不记得是在哪个区域创建了集群,可以通过使用gcloud命令来查看:
gcloud container clusters list
以上输出说明已经在europe-west1-b区域中创建了集群,因此也需要在同一区域中创建GCE持久磁盘。
gcloud compute disks create --size=1GiB --zone=europe-west1-b mongodb
这个命令创建了一个1GiB容量并命名为mongodb的GCE持久磁盘。可以忽略有关磁盘大小的告警,因为我们无须关心用于测试的磁盘性能。
现在我们已经正确设置了物理存储,可以在MongoDB pod的卷中使用它。着手为pod准备YAML
一个使用gce Persistent Disk卷的pod: mongodb-podgcepd.yaml
apiVersion: v1
kind: Pod
metadata:
name: mongodb
spec:
volumes:
- name: mongodb-data
gcePersistentDisk:
pdName: mongodb
fsType: ext4
containers:
- image: mongo
name: mongodb
volumeMounts:
- name: mongodb-data
mountPath: /data/db
ports:
- containerPort: 27017
protocol: TCP
如果要使用Minikube,就不能使用GCE持久磁盘,但是可以部署mongodb-pod-hostpath.yaml,这个使用的是hostpath卷而不是GCE持久磁盘。
apiVersion: v1
kind: Pod
metadata:
name: mongodb
spec:
volumes:
- name: mongodb-data
hostPath:
path: /tmp/mongodb
containers:
- image: mongo
name: mongodb
volumeMounts:
- name: mongodb-data
mountPath: /data/db
ports:
- containerPort: 27017
protocol: TCP
pod包含一个容器和一个卷,被之前创建的GCE持久磁盘支持(如图6.5所示)。因为MongoDB就是在/data/db上存储数据的,所以容器中的卷也要挂载在这个路径上。
带有单个运行Mongodb的容器的pod,该容器挂载引用外部的GCE持久磁盘
在mongodb pod中执行MongoDB shell
kubectl exec -it mongodb mongo
MongoDB允许存储JSON文档,所以我们将存放一个文档,以查看其是否被持久化存储,并且可以在重新创建pod后检索到。使用以下命令插入一个新的JSON文档:
use mystore
db.foo.insert({name:'foo'})
通过find()命令来查看插入的文档:
db.foo.find()
文档现在已经被存储在GCE Persistent Disk中了。
现在可以退出mongodb shell(输入exit并按Enter键),然后删除pod并重建:
kubectl delete pod mongodb
kubectl create -f mongodb-pod-gcepd.yaml
新的pod使用与前一个pod完全相同的GCE Persistent Disk,所以运行在其中的MongoDB容器应该会看到完全相同的数据,即便将pod调度到不同的节点也是一样的。
可以通过执行kubectl get po-owide来查看pod被调度到哪个节点上。
在新pod中检索MongoDB的持久化数据
kubectl exec -it mongodb mongo
use mystore
db.foo.find
符合预期,数据仍然存在,即便删除了pod并重建。这证实了可以使用GCE持久磁盘在多个pod实例中持久化数据。
因为你的Kubernetes集群运行在Google Kubernetes引擎上所以需要创建GCE persistent disk。
当在其他地方运行Kubernetes集群时,应该根据不同的基础设施使用其他类型的卷。
例如,如果你的Kubernetes集群运行在Amazon的AWS EC2上,就可以使用awsElasticBlockStore卷给你的pod提供持久化存储。
如果集群在Microsoft Azure上运行,则可以使用azureFile或者azureDisk卷。
首先,需要创建实际的底层存储,然后在卷定义中设置适当的属性。
要使用AWS弹性块存储(Aws Elastic Block Store)而不是GCE 持久磁盘,只需要更改卷定义
使用awsElastic Block Store卷的pod: mongodb-podaws.yaml
apiVersion: v1
kind: Pod
metadata:
name: mongodb-aws
spec:
volumes:
- name: mongodb-data
awsElasticBlockStore:
volumeID: my-volume
fsType: ext4
containers:
- image: mongo
name: mongodb
volumeMounts:
- name: mongodb-data
mountPath: /data/db
ports:
- containerPort: 27017
protocol: TCP
如果集群是运行在自有的一组服务器上,那么就有大量其他可移植的选项用于在卷内挂载外部存储。
例如,要挂载一个简单的NFS共享,只需指定NFS服务器和共享路径
使用NFS的pod: mongodb-pod-nfs.yaml
apiVersion: v1 kind: Pod metadata: name: mongodb-nfs spec: volumes: - name: mongodb-data nfs: server: 1.2.3.4 path: /some/path containers: - image: mongo name: mongodb volumeMounts: - name: mongodb-data mountPath: /data/db ports: - containerPort: 27017 protocol: TCP
![image](https://user-images.githubusercontent.com/30850497/64483963-36c20100-d23e-11e9-9a22-a4ed020edfd2.png)
##### 使用其他存储技术
>其他的支持选项包括用于挂载ISCSI磁盘资源的iscsi,用于挂载GlusterFS的glusterfs,适用于RADOS块设备的rbd,还有fiexVolume、cinder、cephfs、fiocker、fc(光纤通道)等。rbd如果你不会使用到它们,就不需要知道所有的信息。
>要了解每个卷类型设置需要哪些属性的详细信息,可以转到Kubernetes API引用中的Kubernetes API定义,或者通过第三章展示的通过kubectl explain查找信息。如果你已经熟悉了一种特定的存储技术,那么使用explain命令可以让你轻松地了解如何挂载一个适当类型的卷,并在pod中使用它。
>但是开发人员需要知道所有信息吗?开发人员在创建pod时,应该处理与基础设施相关的存储细节,还是应该留给集群管理员处理?
通过pod的卷来隐藏真实的底层基础设施,不就是Kubernetes存在的意义吗?举个例子,让研发人员来指定NFS服务器的主机名会是一件感觉很糟糕的事情。而这还不是最糟糕的。
将这种涉及基础设施类型的信息塞到一个pod设置中,意味着pod设置与特定的Kubernetes集群有很大耦合度。这就不能在另一个pod中使用相同的设置了。所以使用这样的卷并不是在pod中附加持久化存储的最佳实践
### 从底层存储技术解耦pod
>理想的情况是,在Kubernetes上部署应用程序的开发人员不需要知道底层使用的是哪种存储技术,同理他们也不需要了解应该使用哪些类型的物理服务器来运行pod,与基础设施相关的交互是集群管理员独有的控制领域。
>当开发人员需要一定数量的持久化存储来进行应用时,可以向Kubernetes请求,就像在创建pod时可以请求CPU、内存和其他资源一样。系统管理员可以对集群进行配置让其可以为应用程序提供所需的服务。
#### 持久卷和持久卷声明
>在Kubernetes集群中为了使应用能够正常请求存储资源,同时避免处理基础设施细节,引入了两个新的资源,分别是持久卷和持久卷声明
>持久卷、持久卷声明和真实底层存储
>持久卷由集群管理员提供,并被pod通过持久卷声明来消费
![image](https://user-images.githubusercontent.com/30850497/64484029-1e9eb180-d23f-11e9-8e63-48470f37a041.png)
>研发人员无须向他们的pod中添加特定技术的卷,而是由集群管理员设置底层存储,然后通过Kubernetes API服务器创建持久卷并注册。
>在创建持久卷时,管理员可以指定其大小和所支持的访问模式。
>当集群用户需要在其pod中使用持久化存储时,他们首先创建持久卷声明(PersistentVolumeClaim,简称PVC)清单,指定所需要的最低容量要求和访问模式,
>然后用户将持久卷声明清单提交给Kubernetes API服务器,Kubernetes将找到可匹配的持久卷并将其绑定到持久卷声明。
>持久卷声明可以当作pod中的一个卷来使用,其他用户不能使用相同的持久卷,除非先通过删除持久卷声明绑定来释放。
>重新讨论MongoDB示例
>首先承担集群管理员的角色,并创建一个支持GCE 持久磁盘的 持久卷。
>然后,你将承担应用程序研发人员的角色,首先声明持久卷,然后在pod中使用
##### 设置物理存储,在Kubernetes中创建持久卷
>一个gcePersistentDisk持久卷:mongodb-pv-gcepd.yaml
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mongodb-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
gcePersistentDisk:
pdName: mongodb
fsType: ext4
如果在用Minikube,请用mongodb-pv-hostpath.yaml文件创建PV。
apiVersion: v1 kind: PersistentVolume metadata: name: mongodb-pv spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce - ReadOnlyMany persistentVolumeReclaimPolicy: Retain hostPath: path: /tmp/mongodb
>在创建持久卷时,管理员需要告诉Kubernetes其对应的容量需求,以及它是否可以由单个节点或多个节点同时读取或写入。
>管理员还需要告诉Kubernetes如何处理PersistentVolume(当持久卷声明的绑定被删除时)。
>最后,无疑也很重要的事情是,管理员需要指定持久卷支持的实际存储类型、位置和其他属性。
>如果仔细观察,当直接在pod卷中引用GCE持久磁盘时,最后一部分配置与前面完全相同
##### 在pod卷中引用GCE PD
>在使用kubectl create命令创建持久卷之后,应该可以声明它了
>列出了所有的持久卷:
>pv也用作persistentvolume的简写
```bash
kubectl get pv
持久卷显示为可用,因为你还没创建持久卷声明
持久卷不属于任何命名空间,它跟节点一样是集群层面的资源
和集群节点一样,持久卷不属于任何命名空间,区别于pod和持久卷声明
假设现在需要部署一个需要持久化存储的pod,将要用到之前创建的持久卷,但是不能直接在pod内使用,需要先声明一个。
声明一个持久卷和创建一个pod是相对独立的过程,因为即使pod被重新调度(切记,重新调度意味着先前的pod被删除并且创建了一个新的pod),我们也希望通过相同的持久卷声明来确保可用。
准备一个持久卷声明清单,并通过kubectl create将其发布到Kubernetes API
PersistentColumeClaim: mongodb-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
spec:
resources:
requests:
storage: 1Gi
accessModes:
- ReadWriteOnce
storageClassName: ""
当创建好声明,Kubernetes就会找到适当的持久卷并将其绑定到声明,持久卷的容量必须足够大以满足声明的需求,并且卷的访问模式必须包含声明中指定的访问模式。
在该示例中,声明请求1 GiB的存储空间和ReadWriteOnce访问模式。
之前创建的持久卷符合刚刚声明中的这两个条件,所以它被绑定到对应的声明中
列举出所有的持久卷声明来查看PVC的状态:
使用pvc来代称persistentvolumeclaim
kubectl get pvc
PVC状态显示已与持久卷的mongodb-pv绑定。请留意访问模式的简写
RWO——ReadWriteOnce——仅允许单个节点挂载读写。
ROX——ReadOnlyMany——允许多个节点挂载只读。
RWX——ReadWriteMany——允许多个节点挂载读写这个卷。
注意 RWO、ROX、RWX涉及可以同时使用卷的工作节点的数量而并非pod的数量。
通过使用kubectl get命令,我们还可以看到持久卷现在已经Bound,并且不再是Available
kubectl get pv
持久卷显示被绑定在default/mongodb-pvc的声明上,
这个default部分是声明所在的命名空间(在默认命名空间中创建的声明),
我们之前有提到过持久卷是集群范围的,因此不能在特定的命名空间中创建,
但是持久卷声明又只能在特定的命名空间创建,
所以持久卷和持久卷声明只能被同一命名空间内的pod创建使用。
持久卷现在已经可用了,除非先释放掉卷,否则没有人可以申明相同的卷。
要在pod中使用持久卷,需要在pod的卷中引用持久卷声明名称
使用PVC卷的pod: mongodb-pod-pvc.yaml
apiVersion: v1 kind: Pod metadata: name: mongodb spec: containers: - image: mongo name: mongodb volumeMounts: - name: mongodb-data mountPath: /data/db ports: - containerPort: 27017 protocol: TCP volumes: - name: mongodb-data persistentVolumeClaim: claimName: mongodb-pvc
![image](https://user-images.githubusercontent.com/30850497/64484194-4d1d8c00-d241-11e9-8fd4-4c12d1a7e048.png)
>继续创建pod,现在检查这个pod是否确实在使用相同的持久卷和底层GCE PD。通过再次运行MongoDB shell,应该可以看到之前存储的数据
##### 在已使用PVC和PV的pod中检索MongoDB的持久化数据
```bash
kubectl exec -it mongodb mongo
use mystore
db.foo.find()
符合预期,可以检索之前存储到MongoDB的文档。
pod可以直接使用,或者通过持久卷和持久卷声明,这两种方式使用GCE持久磁盘。
考虑如何使用这种间接方法从基础设施获取存储,对于应用程序开发人员(或者集群用户)来说更加简单。是的,这需要额外的步骤来创建持久卷和持久卷声明,但是研发人员不需要关心底层实际使用的存储技术。
此外,现在可以在许多不同的Kubernetes集群上使用相同的pod和持久卷声明清单,因为它们不涉及任何特定依赖于基础设施的内容。声明说:“我需要x存储量,并且我需要能够支持一个客户端同时读取和写入。”然后pod通过其中一个卷的名称来引用声明。
删除pod和持久卷声明
kubectl delete pod mongodb
kubectl delete pvc mongodb-pvc
通过将persistentVolumeReclaimPolicy设置为Retain从而通知到Kubernetes,我们希望在创建持久卷后将其持久化,让Kubernetes可以在持久卷从持久卷声明中释放后仍然能保留它的卷和数据内容。
手动回收持久卷并使其恢复可用的唯一方法是删除和重新创建持久卷资源。
当这样操作时,你将决定如何处理底层存储中的文件:可以删除这些文件,也可以闲置不用,以便在下一个pod中复用它们。
自动回收持久卷
存在两种其他可行的回收策略:Recycle 和Delete。
第一种删除卷的内容并使卷可用于再次声明,通过这种方式,持久卷可以被不同的持久卷声明和pod反复使用
持久卷和持久卷声明的生命周期,以及在pod中的使用
而另一边,Delete策略删除底层存储。需要注意当前GCE持久磁盘无法使用Recycle选项。这种类型的持久卷只支持Retain和Delete策略,其他类型的持久磁盘可能支持这些选项,也可能不支持这些选项。因此,在创建自己的持久卷之前,一定要检查卷中所用到的特定底层存储支持什么回收策略。
可以在现有的持久卷上更改持久卷回收策略。比如,如果最初将其设置为Delete,则可以轻松地将其更改为Retain,以防止丢失有价值的数据。
使用持久卷和持久卷声明可以轻松获得持久化存储资源,无须研发人员处理下面实际使用的存储技术,但这仍然需要一个集群管理员来支持实际的存储。幸运的是,Kubernetes还可以通过动态配置持久卷来自动执行此任务。
集群管理员可以创建一个 持久卷配置,并定义一个或多个StorageClass对象,从而让用户选择他们想要的持久卷类型而不仅仅只是创建持久卷。用户可以在其持久卷声明中引用StorageClass,而配置程序在配置持久存储时将采用这一点。
与持久卷类似,StorageClass资源并非命名空间。
Kubernetes包括最流行的云服务提供商的置备程序provisioner,所以管理员并不总是需要创建一个置备程序。但是如果Kubernetes部署在本地,则需要配置定制的置备程序。
与管理员预先提供一组持久卷不同的是,它们需要定义一个或两个(或多个)StorageClass,并允许系统在每次通过持久卷声明请求时创建一个新的持久卷。最重要的是,不可能耗尽持久卷(很明显,你可以用完存储空间)。
在用户创建持久卷声明之前,管理员需要创建一个或多个StorageClass资源,然后才能创建新的持久卷
一个StorageClass定义:storageclass-fast-gcepd.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd
如果使用Minikube,请部署文件storageclass-fast-hostpath.yaml。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: k8s.io/minikube-hostpath
parameters:
type: pd-ssd
StorageClass资源指定当久卷声明请求此StorageClass时应使用哪个置备程序来提供持久卷。
StorageClass定义中定义的参数将传递给置备程序,并具体到每个供应器插件。
StorageClass使用GCE持久磁盘的预配置器,这意味着当Kubernetes在GCE中运行时可供使用。
对于其他云提供商,需要使用其他的置备程序。
创建StorageClass资源后,用户可以在其持久卷声明中按名称引用存储类
一个采用动态配置的PVC:mongodb-pvc-dp.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
spec:
storageClassName: fast
resources:
requests:
storage: 100Mi
accessModes:
- ReadWriteOnce
除了指定大小和访问模式,持久卷声明现在还会指定要使用的存储类别。
在创建声明时,持久卷由fast StorageClass资源中引用的provisioner创建。
即使现有手动设置的持久卷与持久卷声明匹配,也可以使用provisioner。
如果在PVC中引用一个不存在的存储类,则PV的配置将失败(在PVC上使用 kubectl describe 时,将会看到ProvisioningFailed事件)。
创建PVC,然后使用kubectl get进行查看
kubectl get pvc mongodb-pvc
VOLUME列显示了与此声明绑定的持久卷(实际名称比上面显示的长)。
现在可以尝试列出持久卷,看看是否确实自动创建了一个新的PV:
kubectl get pv
可以看到动态配置的持久卷其容量和访问模式是在PVC中所要求的。它的回收策略是Delete,这意味着当PVC被删除时,持久卷也将被删除。
除了PV,置备程序还提供了真实的存储空间,
fast StorageClass被配置为使用kubernetes.io/gce-pd从而提供了GCE持久磁盘。可以使用以下命令查看磁盘:
gcloud compute disks list
第一个持久磁盘的名称表明它是动态配置的,同时它的类型显示为一个SSD,正如在前面创建的存储类中所指定的那样。
StorageClasses的好处在于,声明是通过名称引用它们的。
因此,只要StorageClass名称在所有这些名称中相同,PVC定义便可跨不同集群移植。
要自己查看这个可移植性,可以尝试在Minikube上运行相同的示例,假设你一直在使用GKE。作为集群管理员,你必须创建一个不同的存储类(但名称相同)。
storageclass-fast-hostpath.yaml文件中定义的存储类是专用于Minikube的。
然后,一旦部署了存储类,作为集群用户,就可以像以前一样部署完全相同的PVC清单和完全相同的pod清单。这展示了pod和PVC在不同集群间的移植性。
storageclass-fast-hostpath.yaml
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast provisioner: k8s.io/minikube-hostpath parameters: type: pd-ssd
使用sc作为storageclass的简写
kubectl get sc
除了你自己创建的fast存储类,还存在standard存储类并标记为默认存储类
列举Minikube中可用的存储类,以便我们进行比较
fast存储类是由你创建的,并且此处也存在默认的standard存储类,
比较两个列表中的TYPE列,你会看到GKE正在使用kubernetes.io/gce-pd置备程序,而Minikube正在使用k8s.io/minikube-hostpath。
使用kubectl get可查看有关GKE集群中标准存储类的更多信息
GKE上的标准存储类的定义
kubectl get sc standard -o yaml
如果仔细观察清单的顶部,会看到存储类定义会包含一个注释,这会使其成为默认的存储类。如果持久卷声明没有明确指出要使用哪个存储类,则默认存储类会用于动态提供持久卷的内容。
可以在不指定storageClassName属性的情况下创建PVC,并且(在Google Kubernetes引擎上)将为你提供一个pd-standard类型的GCE持久磁盘
通过下面的代码清单中的YAML来创建一个声明。
不指定存储类别的PVC: mongodb-pvc-dp-nostorageclass.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc2
spec:
resources:
requests:
storage: 100Mi
accessModes:
- ReadWriteOnce
此PVC定义仅包含存储大小请求和所需访问模式,并不包含存储级别。在创建PVC时,将使用任何标记为默认的存储类。可以通过如下代码确认:
这最后会告诉我们为什么要在代码清单6.11中将storageClassName设置为一个空字符串(当你想让PVC绑定到你手动配置的PV时)。在这里回顾一下这个PVC定义的相关行:
如果尚未将storageClassName属性设置为空字符串,则尽管已存在适当的预配置持久卷,但动态卷置备程序仍将配置新的持久卷。此时,笔者想演示一个声明如何绑定到手动预先配置的持久卷,同时不希望置备程序干涉。
如果希望PVC使用预先配置的PV,请将storageClassName显式设置为
""
。
将持久化存储附加到一个容器的最佳方式是仅创建PVC(如果需要,可以使用明确指定的storgeClassName)和容器(其通过名称引用PVC),其他所有内容都由动态持久卷置备程序处理。
获取动态的持久卷所涉及的步骤
持久卷动态配置的完整图示
本章向你展示了如何使用卷来为pod的容器提供临时或持久存储。你已经学会了如何:
创建一个多容器pod,并通过为pod添加一个卷并将其挂载到每个容器中,来让pod中的容器操作相同的文件
使用emptyDir卷存储临时的非持久数据
使用gitRepo卷可以在pod启动时使用Git库的内容轻松填充目录
使用hostPath卷从主机节点访问文件
将外部存储装载到卷中,以便在pod重启之前保持pod数据读写
通过使用持久卷和持久卷声明解耦pod与存储基础架构
为每个持久卷声明动态设置所需(或缺省)存储类的持久卷
当需要将持久卷声明绑定到预配置的持久卷时,防止动态置备程序干扰
几乎所有的应用都需要配置信息(不同部署示例间的区分设置、访问外部系统的证书等),并且这些配置数据不应该被嵌入应用本身。
如何传递配置选项给运行在Kubernetes上的应用程序。
开发一款新应用程序的初期,除了将配置嵌入应用本身,通常会以命令行参数的形式配置应用。随着配置选项数量的逐渐增多,将配置文件化。
另一种通用的传递配置选项给容器化应用程序的方法是借助环境变量。应用程序主动查找某一特定环境变量的值,而非读取配置文件或者解析命令行参数。例如,MySQL官方镜像内部通过环境变量MYSQL_ROOT_PASSWORD设置超级用户root的密码。
为何环境变量的方案会在容器环境下如此常见?通常直接在Docker容器中采用配置文件的方式是有些许困难的,往往需要将配置文件打入容器镜像,抑或是挂载包含该文件的卷。显然,前者类似于在应用程序源代码中硬编码配置,每次修改完配置之后需要重新构建镜像。除此之外,任何拥有镜像访问权限的人可以看到配置文件中包含的敏感信息,如证书和密钥。相比之下,挂载卷的方式更好,然而在容器启动之前需确保配置文件已写入响应的卷中。
可能会想到采用gitRepo卷作为配置源。这并不是一个坏主意,通过它可以保持配置的版本化,并且能比较容易地按需回滚配置。然而有一种更加简便的方法能将配置数据置于Kubernetes的顶级资源对象中,并可与其他资源定义存入同一Git仓库或者基于文件的存储系统中。用以存储配置数据的Kubernetes资源称为ConfigMap。
无论你是否在使用ConfigMap存储配置数据,以下方法均可被用作配置你的应用程序:
向容器传递命令行参数
为每个容器设置自定义环境变量
通过特殊类型的卷将配置文件挂载到容器中
尽管绝大多数配置选项并未包含敏感信息,少量配置依旧可能含有证书、私钥,以及其他需要保持安全的相似数据。该类型数据需要被特殊对待。这也是为何Kubernetes提供另一种称作Secret的一级对象的原因。
迄今为止所有示例中容器运行的命令都是镜像中默认定义的。Kubernetes可在pod的容器中定义并覆盖命令以满足运行不同的可执行程序
容器中运行的完整指令由两部分组成:命令与参数。
ENTRYPOINT定义容器启动时被调用的可执行程序。
CMD指定传递给ENTRYPOINT的参数。
尽管可以直接使用CMD指令指定镜像运行时想要执行的命令,
正确的做法依旧是借助ENTRYPOINT指令,
仅仅用CMD指定所需的默认参数。这样,镜像可以直接运行,无须添加任何参数:
docker run <image>
或者是添加一些参数,覆盖Dockerile中任何由CMD指定的默认参数值:
dockoer run <image> <arguments>
上述两条指令均支持以下两种形式:
shell形式——如
ENTRYPOINT node app.js
。exec形式——如
ENTRYPOINT["node","app.js"]
。两者的区别在于指定的命令是否是在shell中被调用
对于第2章中创建的kubia镜像,如果使用exec形式的ENTRYPOINT指令
ENTRYPOINT["node","app.js"]
可以从容器中的运行进程列表看出:这里是直接运行node进程,而并非在shell中执行
docker exec xxx ps x
如果采用shell形式(ENTRYPOINT node app.js),容器进程如下所示
可以看出,主进程(PID 1)是shell进程而非node进程,node进程(PID 7)于shell中启动。shell进程往往是多余的,因此通常可以直接采用exec形式的ENTRYPOINT指令
通过修改fortune脚本与镜像Dockerfile使循环的延迟间隔可配置
在fortune脚本中添加VARIABLE变量并用第一个命令行参数对其初始化
通过参数可配置化fortune脚本中的循环间隔:fortune-args/fortuneloop.sh
#!/bin/bash
trap "exit" SIGINT
INTERVAL=$1
echo Configured to generate new fortune every $INTERVAL seconds
mkdir -p /var/htdocs
while :
do
echo $(date) Writing fortune to /var/htdocs/index.html
/usr/games/fortune > /var/htdocs/index.html
sleep $INTERVAL
done
修改Dockerfile,采用exec形式的ENTRYPOINT指令,以及利用CMD设置间隔的默认值为10
修改fortune镜像的Dockerfile: fortune-args/Dockerfile
FROM ubuntu:latest
RUN apt-get update ; apt-get -y install fortune
ADD fortuneloop.sh /bin/fortuneloop.sh
ENTRYPOINT ["/bin/fortuneloop.sh"]
CMD ["10"]
重新构建镜像并推送至Docker Hub。这里将镜像的tag由latest修改为args
用Docker在本地启动该镜像并进行测试:
可以通过Ctrl+C组合键来停止脚本
也可以传递一个间隔参数覆盖默认睡眠间隔值:
现在可以确保镜像能够正确应用传递给它的参数。让我们来看一下在pod中如何使用它。
在Kubernetes中定义容器时,镜像的ENTRYPOINT和CMD均可以被覆盖,
仅需在容器定义中设置属性command和args的值
指定自定义命令与参数的pod定义
只需要设置自定义参数。命令一般很少被覆盖,除非针对一些未定义ENTRYPOINT的通用镜像,例如busybox
command和args字段在pod创建后无法被修改
上述的两条Dockerfile指令与等同的pod规格字段如表7.1所示
在Docker与Kubernetes中指定可执行程序及其参数
在pod定义中传递参数值:fortune-pod-args.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune2s
spec:
containers:
- image: luksa/fortune:args
args: ["2"]
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {}
现在你已经在容器定义中添加了args数组参数,可以尝试创建该pod。数组值会在pod运行时作为命令行参数传递给容器。
少量参数值的设置可以使用上述的数组表示。多参数值情况下可以采用如下标记:
字符串值无须用引号标记,数值需要。
通过命令行参数指定参数值是给容器传递配置选项的其中一种手段
容器化应用通常会使用环境变量作为配置源。
Kubernetes允许为pod中的每一个容器都指定自定义的环境变量集合
与容器的命令和参数设置相同,环境变量列表无法在pod创建后被修改
通过环境变量使fortuneloop.sh脚本中的睡眠间隔值可配置化
每个容器都可设置环境变量
通过环境变量配置化fortune脚本中的间隔值:fortune-env/fortuneloop.sh
#!/bin/bash
trap "exit" SIGINT
echo Configured to generate new fortune every $INTERVAL seconds
mkdir -p /var/htdocs
while :
do
echo $(date) Writing fortune to /var/htdocs/index.html
/usr/games/fortune > /var/htdocs/index.html
sleep $INTERVAL
done
当前的应用仅是一个简单的bash脚本,只需要移除脚本中INTERVAL初始化所在的行即可。
如果应用由Java编写,需要使用
System.getenv(“INTERVAL”)
,对 应 到Node.JS与Python中 分 别 是
process.env.INTERVAL
与os.environ[′INTERVAL′]
。
构建完新镜像(镜像的tag变更为luksa/fortune:env)并推送至Docker Hub之后,可以通过创建一个新pod来运行它。如下面的代码清单所示,在容器定义中写入环境变量以传递给脚本。
在pod中指定环境变量:fortune-pod-env.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-env
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
value: "30"
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {}
环境变量被设置在pod的容器定义中,并非是pod级别
在每个容器中,Kubernetes会自动暴露相同命名空间下每个service对应的环境变量。这些环境变量基本上可以被看作自动注入的配置。
在前面的示例中,环境变量的值是固定的。可以采用$(VAR)语法在环境变量值中引用其他的环境变量。
假设定义了两个环境变量,第二个变量定义中可包含第一个环境变量的值
在环境变量值中引用另一个变量
SECOND_VAR的值是"foobar"。7.2节中介绍的command和args属性值同样可以像这样引用环境变量
pod定义硬编码意味着需要有效区分生产环境与开发过程中的pod定义。
为了能在多个环境下复用pod的定义,需要将配置从pod定义描述中解耦出来。
幸运的是,可以通过一种叫作ConfigMap的资源对象完成解耦,用valueFrom字段替代value字段使ConfigMap成为环境变量值的来源
应用配置的关键在于能够在多个环境中区分配置选项,将配置从应用程序源码中分离,可频繁变更配置值。如果将pod定义描述看作是应用程序源代码,显然需要将配置移出pod定义。微服务架构下正是如此,该架构定义了如何将多个个体组件组合成功能系统。
Kubernetes允许将配置选项分离到单独的资源对象ConfigMap中,本质上就是一个键/值对映射,值可以是短字面量,也可以是完整的配置文件。
应用无须直接读取ConfigMap,甚至根本不需要知道其是否存在。映射的内容通过环境变量或者卷文件(如图7.2所示)的形式传递给容器,而并非直接传递给容器。命令行参数的定义中可以通过$(ENV_VAR)语法引用环境变量,因而可以达到将ConfigMap的条目当作命令行参数传递给进程的效果。
pod通过环境变量与ConfigMap卷使用ConfigMap
应用程序同样可以通过Kubernetes Rest API按需直接读取ConfigMap的内容。不过除非是需求如此,应尽可能使你的应用保持对Kubernetes的无感知。
不管应用具体是如何使用ConfigMap的,将配置存放在独立的资源对象中有助于在不同环境(开发、测试、质量保障和生产等)下拥有多份同名配置清单。pod是通过名称引用ConfigMap的,因此可以在多环境下使用相同的pod定义描述,同时保持不同的配置值以适应不同环境
不同环境下的同名ConfigMap
从最简单的例子开始,先创建一个仅包含单一键的映射,并用它填充之前示例中的环境变量INTERVAL。
这里将使用指令
kubectl create configmap
创建ConfigMap,而非通用指令kubectl create-f
。
利用kubectl创建ConfigMap的映射条目时可以指定字面量或者存储在磁盘上的文件。
先创建一个简单的字面量条目:
kubectl create configmap fortune-config --from-literal=sleep-interval=25
ConfigMap中的键名必须是一个合法的DNS子域,仅包含数字字母、破折号、下画线以及圆点。首位的圆点符号是可选的。
通过这条命令创建了一个叫作fortune-config的ConfigMap,仅包含单映射条目sleep-interval=25
ConfigMap fortune-config包含单映射条目
ConfigMap一般包含多个映射条目。通过添加多个--from-literal参数可创建包含多条目的ConfigMap:
kubectl create configmap myconfigmap --from-literal=foo=bar --from-literal=bar=baz --from-literal=one=two
通过kubectl创建的ConfigMap的YAML格式的定义描述
kubectl get configmap fortune-config -o yaml
通过Kubernetes API创建对应的ConfigMap
kubectl create -f fortune-config.yaml
ConfigMap同样可以存储粗粒度的配置数据,比如完整的配置文件。
kubectl create configmap命令支持从磁盘上读取文件,并将文件内容单独存储为ConfigMap中的条目
kubectl create configmap my-config --from-file=config-file.conf
运行上述命令时,kubectl会在当前目录下查找config-file.conf文件,并将文件内容存储在ConfigMap中以config-file.conf为键名的条目下。
当然也可以手动指定键名:
kubectl create configmap my-config --from-file=customkey=config-file.conf
这条命令会将文件内容存在键名为customkey的条目下。
与使用字面量时相同,多次使用--from-file参数可增加多个文件条目。
除单独引入每个文件外,甚至可以引入某一文件夹中的所有文件:
kubectl create configmap my-config --from-file=/path/to/dir
这种情况下,kubectl会为文件夹中的每个文件单独创建条目,仅限于那些文件名可作为合法ConfigMap键名的文件。
创建ConfigMap时可以混合使用这里提到的所有选项
这里的ConfigMap创建自多种选项:完整文件夹、单独文件、自定义键名的条目下的文件(替代文件名作键名)以及字面量。图7.5显示了所有源选项以及最终的ConfigMap。
从文件、文件夹以及字面量创建ConfigMap
将映射中的值传递给pod的容器,有三种方法
首先尝试最为简单的一种——设置环境变量
将会使用到valueFrom字段
通过配置文件注入环境变量的pod:fortune-pod-env-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-env-from-configmap
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {}
这里定义了一个环境变量INTERVAL,并将其值设置为fortune-config ConfigMap中键名为sleep-interval对应的值。
运行在html-generator容器中的进程读取到环境变量INTERVAL的值为25
给容器的环境变量传递ConfigMap的条目
引用不存在的ConfigMap的容器会启动失败,其余容器能正常启动。
如果之后创建了这个缺失的ConfigMap,失败容器会自动启动,无须重新创建pod。
可以标记对ConfigMap的引用是可选的(设置configMapKeyRef.optional: true)。这样,即便ConfigMap不存在,容器也能正常启动。
这个例子展示了如何将配置从pod定义中分离。这样能使所有的配置项较为集中(甚至多个pod也是如此),而不是分散在各处(或者冗余复制于多个pod定义清单)。
1.6版本的Kubernetes提供了暴露ConfigMap的所有条目作为环境变量的手段
假设一个ConfigMap包含FOO、BAR和FOO-BAR三个键。可以通过envFrom属性字段将所有条目暴露作为环境变量,而非使用前面例子中的env字段。
pod包含来源于ConfigMap所有条目的环境变量
可以为所有的环境变量设置前缀,如本例中的CONFIG_,容器中两个环境变量的名称为:CONFIG_FOO与CONFIG_BAR。
前缀设置是可选的,若不设置前缀值,环境变量的名称与ConfigMap中的键名相同
CONFIG_FOO-BAR包含破折号,这并不是一个合法的环境变量名称。Kubernetes不会主动转换键名(例如不会将破折号转换为下画线)。如果ConfigMap的某键名格式不正确,创建环境变量时会忽略对应的条目(忽略时不会发出事件通知)。
如何将ConfigMap中的值作为参数值传递给运行在容器中的主进程。
在字段pod.spec.containers.args中无法直接引用ConfigMap的条目,
但是可以利用ConfigMap条目初始化某个环境变量,
然后再在参数字段中引用该环境变量
传递ConfigMap的条目作为命令行参数
在YAML文件中做到这一点
使用ConfigMap条目作为参数值:fortune-pod-args-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-args-from-configmap
spec:
containers:
- image: luksa/fortune:args
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
args: ["$(INTERVAL)"]
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {}
环境变量的定义与之前相同,需通过$(ENV_VARIABLE_NAME)将环境变量的值注入参数值。
环境变量或者命令行参数值作为配置值通常适用于变量值较短的场景。
由于ConfigMap中可以包含完整的配置文件内容,当你想要将其暴露给容器时,可以借助一种称为configMap卷的特殊卷格式。
configMap卷会将ConfigMap中的每个条目均暴露成一个文件。运行在容器中的进程可通过读取文件内容获得对应的条目值
尽管这种方法主要适用于传递较大的配置文件给容器,同样可以用于传递较短的变量值。
示例,使用配置文件配置运行在fortune pod的Web服务器容器中的Nginx web服务器。如果想要让Nginx服务器压缩传递给客户端的响应,Nginx的配置文件需开启压缩配置
开启gzip压缩的Nginx配置文件:my-nginx-config.conf
server {
listen 80;
server_name www.kubia-example.com;
gzip on;
gzip_types text/plain application/xml;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
现在首先通过kubectl delete configmap fortune-config删除现有的ConfigMap fortune-config,然后用存储在本地磁盘上的Nginx配置文件创建一个新的ConfigMap。
创建一个新文件夹confimap-files并将上面的配置文件存储于configmap-files/my-nginx-config.conf中。另外在该文件夹中添加一个名为sleep-interval的文本文件,写入值为25,使ConfigMap同样包含条目sleep-interval
configmap-files文件夹及文件的内容
从文件夹创建ConfigMap
kubectl create configmap fortune-config --from-file=configmap-files
从文件创建的ConfigMap的YAML格式定义
kubectl get configmap fortune-config -o yaml
注意 所有条目第一行最后的管道符号表示后续的条目值是多行字面量。
ConfigMap包含两个条目,条目的键名与文件名相同。接下来将在pod的容器中使用该ConfigMap。
创建包含ConfigMap条目内容的卷只需要创建一个引用ConfigMap名称的卷并挂载到容器中。已经学会了如何创建及挂载卷,接下来要学习的仅是如何用ConfigMap的条目初始化卷。
Nginx需读取配置文件/etc/nginx/nginx.conf,而Nginx镜像内的这个文件包含默认配置,并不想完全覆盖这个配置文件。幸运的是,默认配置文件会自动嵌入子文件夹/etc/nginx/conf.d/下的所有.conf文件,因此只需要将你的配置文件置于该子文件夹中即可
ConfigMap条目作为容器卷中的文件
pod挂载ConfigMap条目作为文件:fortune-pod-configmap-volume.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: config
mountPath: /tmp/whole-fortune-config-volume
readOnly: true
ports:
- containerPort: 80
name: http
protocol: TCP
volumes:
- name: html
emptyDir: {}
- name: config
configMap:
name: fortune-config
pod定义中包含了引用fortune-config ConfigMap的卷,需要被挂载到文件夹/etc/nginx/conf.d下让Nginx服务器使用它。
现在的web服务器应该已经被配置为会压缩响应,可以将localhost:8080转发到pod的80端口,利用curl检查服务器响应来验证配置是否生效
观察nginx响应是否被压缩
看一下文件夹/etc/nginx/conf.d下的内容 ConfigMap的两个条目均作为文件置于这一文件夹下。条目sleep-interval对应的文件也被包含在内,然而它只会被fortuneloop容器所使用。可以创建两个不同的ConfigMap,一个用以配置容器fortuneloop,另一个用来配置webserver,然而采用多个ConfigMap去分别配置同一pod中的不同容器的做法是不好的。毕竟同一pod中的容器是紧密联系的,需要被当作整体单元来配置。
幸运的是,可以创建仅包含ConfigMap中部分条目的configMap卷——本示例中的条目my-nginx-config.conf。这样容器fortuneloop不会受到影响,条目sleep-interval会作为环境变量传递给容器而不是以卷的方式。
通过卷的items属性能够指定哪些条目会被暴露作为configMap卷中的文件
ConfigMap的指定条目挂载至pod的文件夹:fortune-pod-configmap-volume-with-itmes.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume-with-items
spec:
containers:
- image: luksa/fortune:env
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
mountPath: /etc/nginx/conf.d/
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {}
- name: config
configMap:
name: fortune-config
items:
- key: my-nginx-config.conf
path: gzip.conf
指定单个条目时需同时设置条目的键名称以及对应的文件名。如果采用上面的配置文件创建pod,/etc/nginx/conf.d文件夹是比较干净的,仅包含所需的gzip.conf文件。
在当前与此前的示例中,将卷挂载至某个文件夹,意味着容器镜像中/etc/nginx/conf.d文件夹下原本存在的任何文件都会被隐藏。
Linux系统挂载文件系统至非空文件夹时通常表现如此。文件夹中只会包含被挂载文件系统中的文件,即便文件夹中原本的文件是不可访问的也是同样如此。
本示例中,这种现象并不会带来比较糟糕的副作用。不过假设挂载文件夹是/etc,该文件夹通常包含不少重要文件。由于/etc下的所有文件不存在,容器极大可能会损坏。如果你希望添加文件至某个文件夹如/etc,绝不能采用这种方法。
volumeMount额外的subPath字段可以被用作挂载卷中的某个独立文件或者是文件夹,无须挂载完整卷。图7.10的形象化解释可能更加容易理解。
假设拥有一个包含文件myconfig.conf的configMap卷,希望能将其添加为/etc文件夹下的文件someconfig.conf。通过属性subPath可以将该文件挂载的同时又不影响文件夹中的其他文件
挂载卷中的单独文件
pod挂载ConfigMap的指定条目至特定文件
挂载任意一种卷时均可以使用subPath属性。可以选择挂载部分卷而不是挂载完整的卷。不过这种独立文件的挂载方式会带来文件更新上的缺陷
configMap卷中所有文件的权限默认被设置为644(-rw-r-r--)。可以通过卷规格定义中的defaultMode属性改变默认权限
设置权限:fortune-pod-configmap-volume-defaultMode.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: config
mountPath: /tmp/whole-fortune-config-volume
readOnly: true
volumes:
- name: html
emptyDir: {}
- name: config
configMap:
name: fortune-config
defaultMode: 0660
ConfigMap通常被用作存储非敏感数据,不过依旧可能希望仅限于文件拥有者的用户和组可读写,正如上面的例子所示。
使用环境变量或者命令行参数作为配置源的弊端在于无法在进程运行时更新配置。
将ConfigMap暴露为卷可以达到配置热更新的效果,无须重新创建pod或者重启容器。
ConfigMap被更新之后,卷中引用它的所有文件也会相应更新,进程发现文件被改变之后进行重载。Kubernetes同样支持文件更新之后手动通知容器。
更新ConfigMap之后对应文件的更新耗时会出人意料地长(往往需要数分钟)。
修改前面示例中的Nginx配置文件,使得Nginx能够在不重启pod的前提下应用新配置。
尝试用kubectl edit命令修改ConfigMap fortune-config来关闭gzip压缩:
kubectl edit configmap fortune-config
编辑器打开,行gzip on改为gzip off,保存文件后关闭编辑器。ConfigMap被更新不久之后会自动更新卷中的对应文件。用kubectl exec命令打印出该文件内容进行确认:
kubectl exec fortune-configmap-volume -c web-server cat /etc/nginx/conf.d/my-nginx0config.conf
若尚未看到文件内容被更新,可稍等一会儿后重试。文件更新过程需要一段时间。最终你会看到配置文件的变化,然而发现这对Nginx并没有什么影响,这是因为Nginx不会去监听文件的变化并自动重载。
Nginx会持续压缩响应直到你通过以下命令主动通知它:
kubectl exec fortune-configmap-volume -c web-server -- nginx -s reload
现在再次用curl命令访问服务器后会发现响应不再被压缩(响应头中未包含Content-Encoding: gzip)。在无须重启容器或者重建pod的同时有效修改了应用配置。
你可能会疑惑在Kubernetes更新完configMap卷中的所有文件之前,应用是否会监听到文件变化并主动进行重载。幸运的是,这不会发生,所有的文件会被自动一次性更新。Kubernetes通过符号链接做到这一点。
如果尝试列出configMap卷挂载位置的所有文件,会看到如下内容。
被挂载的configMap卷中的文件
可以看到,被挂载的configMap卷中的文件是..data文件夹中文件的符号链接,而..data文件夹同样是..4984_09_04_something的符号链接。每当ConfigMap被更新后,Kubernetes会创建一个这样的文件夹,写入所有文件并重新将符号..data链接至新文件夹,通过这种方式可以一次性修改所有文件。
涉及到更新configMap卷需要提出一个警告:如果挂载的是容器中的单个文件而不是完整的卷,ConfigMap更新之后对应的文件不会被更新!至少在写本章节的时候表现如此。
如果现在你需要挂载单个文件并且在修改源ConfigMap的同时会自动修改这个文件,一种方案是挂载完整卷至不同的文件夹并创建指向所需文件的符号链接。符号链接可以原生创建在容器镜像中,也可以在容器启动时创建。
容器的一个比较重要的特性是其不变性,从同一镜像启动的多个容器之间不存在任何差异。那么通过修改被运行容器所使用的ConfigMap来打破这种不变性的行为是否是错误的?
关键点在于应用是否支持重载配置。ConfigMap更新之后创建的pod会使用新配置,而之前的pod依旧使用旧配置,这会导致运行中的不同实例的配置不同。这也不仅限于新pod,如果pod中的容器因为某种原因重启了,新进程同样会使用新配置。因此,如果应用不支持主动重载配置,那么修改某些运行pod所使用的ConfigMap并不是一个好主意。
如果应用支持主动重载配置,那么修改ConfigMap的行为就算不了什么。不过有一点仍需注意,由于configMap卷中文件的更新行为对于所有运行中示例而言不是同步的,因此不同pod中的文件可能会在长达一分钟的时间内出现不一致的情况。
配置通常会包含一些敏感数据,如证书和私钥,需要确保其安全性。
Kubernetes提供了一种称为Secret的单独资源对象。
Secret结构与ConfigMap类似,均是键/值对的映射。
Secret的使用方法也与ConfigMap相同,可以
将Secret条目作为环境变量传递给容器
将Secret条目暴露为卷中的文件
Kubernetes通过仅仅将Secret分发到需要访问Secret的pod所在的机器节点来保障其安全性。
另外,Secret只会存储在节点的内存中,永不写入物理存储,这样从节点上删除Secret时就不需要擦除磁盘了。
对于主节点本身(尤其是etcd),Secret通常以非加密形式存储,这就需要保障主节点的安全从而确保存储在Secret中的敏感数据的安全性。这种保障不仅仅是对etcd存储的安全性保障,同样包括防止未授权用户对API服务器的访问,这是因为任何人都能通过创建pod并将Secret挂载来获得此类敏感数据。
从Kubernetes 1.7开始,etcd会以加密形式存储Secret,某种程度提高了系统的安全性。正因为如此,从Secret与ConfigMap中做出正确选择是势在必行的,选择依据相对简单:
采用ConfigMap存储非敏感的文本配置数据。
采用Secret存储天生敏感的数据,通过键来引用。如果一个配置文件同时包含敏感与非敏感数据,该文件应该被存储在Secret中。
一种默认被挂载至所有容器的Secret,对任意一个pod使用命令kubectl describe pod,输出往往包含如下信息:
每个pod都会被自动挂载上一个secret卷,这个卷引用的是前面kubectl describe输出中的一个叫作default-token-cfee9的Secret。
由于Secret也是资源对象,因此可以通过kubectl get secrets命令从Secret列表中找到这个 default-token Secret:
kubectl get secrets
同样可以使用kubectl describe多了解一下这个Secret
可以看出这个Secret包含三个条目——ca.crt、namespace与token,包含了从pod内部安全访问Kubernetes API服务器所需的全部信息。尽管你希望做到应用程序对Kubernetes的完全无感知,然而在除了直连Kubernetes别无他法的情况下,你将会使用到secret卷提供的文件。
kubectl describe pod命令会显示secret卷被挂载的位置:
注意 default-token Secret默认会被挂载至每个容器。可以通过设置pod定义中的automountServiceAccountToken字段为false,或者设置pod使用的服务账户中的相同字段为false来关闭这种默认行为
Secret类似于ConfigMap,由于该Secret包含三个条目,可通过kubectl exec观察到被secret卷挂载的文件夹下包含三个文件:
default-tokenSecret 被自动创建且对应的卷被自动挂载到每个pod上
创建自己地小型Secret。改进fortune-serving的Nginx容器的配置,使其能够服务于HTTPS流量。你需要创建私钥和证书,由于需要确保私钥的安全性,可将其与证书同时存入Secret。
首先在本地机器上生成证书与私钥文件
openssl genrsa -out https.key 2048
openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj .CN=www.kubia-example.com
为了更好地理解Secret,额外创建一个内容为字符串bar的虚拟文件foo
echo bar > foo
使用kubectl create secret命令由这三个文件创建Secret
kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo
与创建ConfigMap的过程类似,这里创建了一个名为fortune-https的generic Secret,它包含有两个条目:https.key和https.cert,分别对应于两个同名文件的内容。如前所述,同样可以用--from-file=fortune-https囊括整个文件夹中的所有文件,替代单独指定每个文件的创建方式。
注意 这里创建了一个generic Secret,在此之前你可能在第5章通过kubectl create secret tls创建过一个tls Secret。两种方式创建的Secret的条目名称不同。
Secret的YAML格式定义
将其与之前创建的ConfigMap的YAML格式定义做对比:
Config的YAML格式定义
Secret条目的内容会被以Base64格式编码,而ConfigMap直接以纯文本展示。这种区别导致在处理YAML和JSON格式的Secret时会稍许有些麻烦,需要在设置和读取相关条目时对内容进行编解码。
采用Base64编码的原因很简单。Secret的条目可以涵盖二进制数据,而不仅仅是纯文本。Base64编码可以将二进制数据转换为纯文本,以YAML或JSON格式展示。
Secret甚至可以被用来存储非敏感二进制数据。不过值得注意的是,Secret的大小限于1MB。
由于并非所有的敏感数据都是二进制形式,Kubernetes允许通过Secret的stringData字段设置条目的纯文本值
通过stringData字段向Secret添加纯文本条目值
stringData字段是只写的(注意:是只写,非只读),可以被用来设置条目值。通过kubectl get-o yaml获取Secret的YAML格式定义时,不会显示stringData字段。相反,stringData字段中的所有条目(如上面示例中的foo条目)会被Base64编码之后展示在data字段下。
通过secret卷将Secret暴露给容器之后,Secret条目的值会被解码并以真实形式(纯文本或二进制)写入对应的文件。
通过环境变量暴露Secret条目亦是如此。
在这两种情况下,应用程序均无须主动解码,可直接读取文件内容或者查找环境变量。
fortune-https Secret 已经包含了证书与密钥文件,接下来需要做的是配置Nginx服务器去使用它们。
kubectl edit configmap fortune-config
文本编辑器打开后,修改条目my-nginx-config.con的内容
修改 fortune-config ConfigMap的数据
上面配置了服务器从/etc/nginx/certs中读取证书与密钥文件,因此之后需要将secret卷挂载于此。
接下来需要创建一个新的fortune-https pod,将含有证书与密钥的secret卷挂载至pod中的web-server容器
fortune-https pod的YAML格式定义:fortune-pod-https.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-https
spec:
containers:
- image: luksa/fortune:env
name: html-generator
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: certs
mountPath: /etc/nginx/certs/
readOnly: true
ports:
- containerPort: 80
- containerPort: 443
volumes:
- name: html
emptyDir: {}
- name: config
configMap:
name: fortune-config
items:
- key: my-nginx-config.conf
path: https.conf
- name: certs
secret:
secretName: fortune-https
Secret default-token以及卷、卷挂载并不包含在这一定义中,因为这些组件被自动加入pod定义,图中不予展示
与configMap卷相同,secret卷同样支持通过defaultModes属性指定卷中文件的默认权限。
pod运行之后,开启端口转发隧道将HTTPS流量转发至pod的443端口,并用curl向服务器发送请求:
组合了ConligMap录密钥运行tortune-heaps pod
若服务器配置正确,会得到一个响应,检查响应中服务器证书是否与之前生成的证书匹配。curl命令添加选项-v开启详细日志
显示Nginx发送的服务器证书
通过挂载secret卷至文件夹/etc/nginx/certs将证书与私钥成功传递给容器。secret卷采用内存文件系统列出容器的挂载点
kubectl exec fortune-https -c web-server -- mount | grep certs
由于使用的是tmpfs,存储在Secret中的数据不会写入磁盘,这样就无法被窃取。
除卷之外,Secret的独立条目可作为环境变量被暴露,就像ConfigMap中sleep-interval条目做的那样。
举个例子,若想将Secret中的键foo暴露为环境变量FOO_SECRET,需要在容器定义中添加如下片段。
Secret条目暴露为环境变量
上面片段与设置INTERVAL环境变量的基本一致,除了这里是使用secretKeyRef字段来引用Secret,而非configMapKeyRef,后者用以引用ConfigMap。
Kubernetes允许通过环境变量暴露Secret,然而此特性的使用往往不是一个好主意。应用程序通常会在错误报告时转储环境变量,或者是启动时打印在应用日志中,无意中暴露了Secret信息。另外,子进程会继承父进程的所有环境变量,如果是通过第三方二进制程序启动应用,你并不知道它使用敏感数据做了什么。
由于敏感数据可能在无意中被暴露,通过环境变量暴露Secret给容器之前请再三思考。为了确保安全性,请始终采用secret卷的方式暴露Secret。
已经学会了如何传递Secret给应用程序并使用它们包含的数据。Kubernetes自身在有些时候希望我们能够传递证书给它,比如从某个私有镜像仓库拉取镜像时。这一点同样需通过Secret来做到。
到目前为止所使用的容器镜像均存储在公共仓库,从上面拉取镜像时无须任何特殊的证书。
不过大部分组织机构不希望它们的镜像开放给所有人,因此会使用私有镜像仓库。
部署一个pod时,如果容器镜像位于私有仓库,Kubernetes需拥有拉取镜像所需的证书。
运行一个镜像来源于私有仓库的pod时,需要做以下两件事:
创建包含Docker镜像仓库证书的Secret。
pod定义中的imagePullSecrets字段引用该Secret。
创建一个包含Docker镜像仓库鉴权证书的Secret与7.5.3节中创建generic Secret并没有什么不同。同样使用kubectl create secret命令,仅仅是类型与参数选项的不同:
kubectl create secret docker-registry mydockerhubsecret --docker-username=myusername --docker-password=mypassword --docker-email=my.email@provider.com
这里创建了一个docker-registry类型的 mydockerhubsecret Secret,创建时需指定Docker Hub的用户名、密码以及邮箱。
通过kubectl describe观察新建Secret的内容时会发现仅有一个条目.dockercfg,相当于用户主目录下的.dockercfg文件。
该文件通常在运行docker login命令时由Docker自动创建。
为了Kubernetes从私有镜像仓库拉取镜像时能够使用Secret,需要在pod定义中指定docker-registry Secret的名称
指定镜像拉取Secret的pod定义:pod-with-private-image.yaml
apiVersion: v1
kind: Pod
metadata:
name: private-pod
spec:
imagePullSecrets:
- name: mydockerhubsecret
containers:
- image: username/private:tag
name: main
上述pod定义中,字段imagePullSecrets引用了 mydockerhubsecret Secret
假设某系统中通常运行大量pod,你可能会好奇是否需要为每个pod都添加相同的镜像拉取Secret。幸运的是,情况并非如此。第12章中将会学习到如何通过添加Secret至ServiceAccount使所有pod都能自动添加上镜像拉取Secret。
在pod定义中覆盖容器镜像定义的默认命令
传递命令行参数给容器主进程
为容器设置环境变量
将配置从pod定义中分离并放入ConfigMap
通过Secret存储敏感数据并安全分发至容器
创建docker-registry Secret用以从私有镜像仓库拉取镜像
应用往往需要获取所运行环境的一些信息,包括应用自身以及集群中其他组件的信息
我们已经了解到Kubernetes如何通过环境变量以及DNS进行服务发现
将了解特定的pod和容器元数据如何被传递到容器,了解在容器中运行的应用如何便捷地与Kubernetes API服务器进行交互,从而获取在集群中部署资源的信息,并且进一步了解如何创建和修改这些资源。
在之前的章节中,我们已经了解到如何通过环境变量或者configMap和secret卷向应用传递配置数据。这对于pod调度、运行前预设的数据是可行的。但是对于那些不能预先知道的数据,比如pod的IP、主机名或者是pod自身的名称(当名称被生成,比如当pod通过ReplicaSet或类似的控制器生成时)呢?此外,对于那些已经在别处定义的数据,比如pod的标签和注解呢?我们不想在多个地方重复保留同样的数据
对于此类问题,可以通过使用Kubernetes Downward API解决。Downward API允许我们通过环境变量或者文件(在downwardAPI卷中)的传递pod的元数据。不要对这个名称产生困惑,Downward API的方式并不像REST endpoint那样需要通过访问的方式获取数据。这种方式主要是将在pod的定义和状态中取得的数据作为环境变量和文件的值
Downward API通过环境变量或者文件对外暴露pod元数据
Downward API可以给在pod中运行的进程暴露pod的元数据。目前我们可以给容器传递以下数据:
pod的名称
pod的IP
pod所在的命名空间
pod运行节点的名称
pod运行所归属的服务账户的名称
每个容器请求的CPU和内存的使用量
每个容器可以使用的CPU和内存的限制
pod的标签
pod的注解
服务账户是pod访问API服务器时用来进行身份验证的账户。CPU和内存的请求和限制代表了分配给一个容器的CPU和内存的使用量,以及一个容器可以分配的上限。
列表中的大部分项目既可以通过环境变量也可以通过downwardAPI卷传递给容器,但是标签和注解只可以通过卷暴露。部分数据可以通过其他方式获取(例如,可以直接从操作系统获取),但是Downward API提供了一种更加便捷的方式。
一个向容器化的进程传递元数据的例子
如何通过环境变量的方式将pod和容器的元数据传递到容器中
根据如下列出的manifest创建一个简单的单容器
在环境变量中使用downward API: downward-api-env.yaml
apiVersion: v1
kind: Pod
metadata:
name: downward
spec:
containers:
- name: main
image: busybox
command: ["sleep", "9999999"]
resources:
requests:
cpu: 15m
memory: 100Ki
limits:
cpu: 100m
memory: 4Mi
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
- name: CONTAINER_CPU_REQUEST_MILLICORES
valueFrom:
resourceFieldRef:
resource: requests.cpu
divisor: 1m
- name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1Ki
当我们的进程在运行时,它可以获取所有我们在pod的定义文件中设定的环境变量。图8-2展示了所有的环境变量以及变量值的来源。pod的名称、IP和命名空间可以通过pod_NAME、pod_IP和pod_NAMESPACE这几个环境变量分别暴露。容器运行的节点的名称可以通过NODE_NAME变量暴露。同样,服务账户可以使用环境变量SERVICE_ACCOUNT。我们也可以创建两个环境变量来保存容器请求使用的CPU的数量,以及容器被最大允许使用的内存数量。
对于暴露资源请求和使用限制的环境变量,我们会设定一个基数单位。实际的资源请求值和限制值除以这个基数单位,所得的结果通过环境变量暴露出去。在前面的例子中,我们设定CPU请求的基数为1m(即1 millicore,也就是千分之一核CPU)。当我们设置资源请求为15m时,环境变量CONTAINER_CPU_REQUEST_MILLICORES的值就是15。同样,我们设定内存的使用限制为4Mi(4 mebibytes),设定基数为1 Ki(1 Kibibyte),则环境变量CONTAINER_MEMORY_LIMIT_KIBIBYTES的值就是4096。
pod元数据与属性通过环境变量暴露给pod
对于CPU资源请求量和使用限制可以被设定为1,也就意味着整颗CPU的计算能力,也可以设定为1m,即千分之一核的计算能力。对于内存的资源请求和使用限制可以设定为1(字节),也可以是1k(kilobute)或1Ki(kibibute),同样也可以设为1M(megavyte)或者1Mi(mebibyte),等等。
在完成创建pod后,我们可以使用kubectl exec命令来查看容器中的所有环境变量
downward pod中的环境变量
kubectl exec downward env
所有在这个容器中运行的进程都可以读取并使用它们需要的变量。
如果更倾向于使用文件的方式而不是环境变量的方式暴露元数据,可以定义一个downwardAPI卷并挂载到容器中。
由于不能通过环境变量暴露,所以必须使用downwardAPI卷来暴露pod标签或注解
与环境变量一样,需要显示地指定元器据字段来暴露份进程。下面我们将把前面的示例从使用环境变量修改为使用存储卷
一个带有dowanwardAPI卷的pod示例:dowanward-apivolume.yaml
apiVersion: v1
kind: Pod
metadata:
name: downward
labels:
foo: bar
annotations:
key1: value1
key2: |
multi
line
value
spec:
containers:
- name: main
image: busybox
command: ["sleep", "9999999"]
resources:
requests:
cpu: 15m
memory: 100Ki
limits:
cpu: 100m
memory: 4Mi
volumeMounts:
- name: downward
mountPath: /etc/downward
volumes:
- name: downward
downwardAPI:
items:
- path: "podName"
fieldRef:
fieldPath: metadata.name
- path: "podNamespace"
fieldRef:
fieldPath: metadata.namespace
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
- path: "containerCpuRequestMilliCores"
resourceFieldRef:
containerName: main
resource: requests.cpu
divisor: 1m
- path: "containerMemoryLimitBytes"
resourceFieldRef:
containerName: main
resource: limits.memory
divisor: 1
现在我们没有通过环境变量来传递元数据,而是定义了一个叫作downward的卷,并且通过/etc/downward目录挂载到我们的容器中。卷所包含的文件会通过卷定义中的downwardAPI.items属性来定义。
对于我们想要在文件中保存的每一个pod级的字段或者容器资源字段,都分别在downwardAPI.items中说明了元数据被保存和引用的path(文件名
使用dowanward API卷来传递元数据
从之前列表的manifest中删除原来的pod,并且新建一个pod。然后查看已挂载到downwardAPI卷目录的内容,存储卷被挂载在/etc/downward/目录下,列出目录中的文件
downwordAPI卷中的文件
注意 与configMAp和secret卷一样,可以通过pod定义中downwardAPI卷的defaultMode属性来改变文件的访问权限设置。
每个文件都对应了卷定义中的一项
不过由于不能通过环境变量的方式暴露label和annotation,所以看一下我们暴露的这两个文件的代码清单
展示downwardAPI卷中的标签和注解
正如我们上面看到的,每一个标签和注解都以key=value的格式保存在单独的行中,如对应多个值,则写在同一行,并且用回车符\n连接。
可以在pod运行时修改标签和注解。
当标签和注解被修改后,Kubernetes会更新存有相关信息的文件,从而使pod可以获取最新的数据。这也解释了为什么不能通过环境变量的方式暴露标签和注解,在环境变量方式下,一旦标签和注解被修改,新的值将无法暴露。
当暴露容器级的元数据时,如容器可使用的资源限制或者资源请求(使用字段resourceFieldRef),必须指定引用资源字段对应的容器名称
在downwardAPI卷中引用容器级的元数据
这样做的理由很明显,因为我们对于卷的定义是基于pod级的,而不是容器级的。当我们引用卷定义某一个容器的资源字段时,我们需要明确说明引用的容器的名称。这个规则对于只包含单容器的pod同样适用。
使用卷的方式来暴露容器的资源请求和使用限制比环境变量的方式稍显复杂,但好处是如果有必要,可以传递一个容器的资源字段到另一个容器(当然两个容器必须处于同一个pod)。使用环境变量的方式,一个容器只能传递它自身资源申请求和限制的信息。
Downward API方式并不复杂,它使得应用独立于Kubernetes。这一点在处理部分数据已在环境变量中的现有应用时特别有用。Downward API方式使得我们不必通过修改应用,或者使用shell脚本获取数据再传递给环境变量的方式来暴露数据。
不过通过Downward API的方式获取的元数据是相当有限的,如果需要获取更多的元数据,需要使用直接访问Kubernetes API服务器的方式。
Kubernetes
容器编排工具