Kubernetes 详解


引言

Kubernetes是Google基于Borg开源的容器编排调度引擎,作为CNCF(Cloud Native Computing Foundation)最重要的组件之一,它的目标不仅仅是一个编排系统,而是提供一个规范,可以让你来描述集群的架构,定义服务的最终状态,kubernetes可以帮你将系统自动地达到和维持在这个状态。Kubernetes作为云原生应用的基石,相当于一个云操作系统,其重要性不言而喻。

本书记录了本人从零开始学习和使用Kubernetes的心路历程,着重于经验分享和总结,同时也会有相关的概念解析,希望能够帮助大家少踩坑,少走弯路,还会指引大家关于关注kubernetes生态周边,如微服务构建、DevOps、大数据应用、Service Mesh、Cloud Native等领域。

🚗云原生

Play with Kubernetes

本书的主角是Kubernetes,在开始后面几章的长篇大论之前让大家可以零基础上手,揭开Kubernetes的神秘面纱。

本文不是讲解Kubernetes的高深原理也不是讲Kuberentes的具体用法,而是通过Play with Kubernetes来带您进入Kubernetes的世界,相当于Kubernetes世界的“Hello World”!而且除了一台可以上网的电脑和浏览器之外不需要再准备任何东西,甚至(至少目前为止)都需要注册账号,上手即玩。

当然免费使用也是有限制的,当前的限制如下:

  • 内置kubeadm来创建kubernetes集群,版本为v1.8.4
  • 每个实例配置为1 core,4G Memory,最多创建5个实例
  • 每个集群的使用时间是4个小时(当然你可以同时启动多个集群,根据浏览器的session来判断集群)
  • 在Kubernetes集群中创建的服务无法通过外网访问,只能在Play with Kubernetes的网络内访问

登陆Play with Kubernetes,点击【登陆】-【开始】即可开始你的Kubernetes之旅!

创建Kubernetes集群

启动第一个实例作为Master节点,在web终端上执行:

  1. 初始化master节点:
kubeadm init --apiserver-advertise-address $(hostname -i)
  1. 初始化集群网络:
kubectl apply -n kube-system -f  "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"
  1. 执行下列初始化命令:
mkdir -p $HOME/.kubecp -i /etc/kubernetes/admin.conf $HOME/.kube/configchown $(id -u):$(id -g) $HOME/.kube/config
  1. 启动新的实例作为node节点,根据master节点上的提示,在新的web终端上执行:
kubeadm join --token 513212.cfea0165b8988d18 192.168.0.13:6443 --discovery-token-ca-cert-hash sha256:b7b6dcc98f3ead3f9e363cb3928fbc04774ee0d63e8eb2897ae30e05aebf8070

注意:192.168.0.13是master节点的IP,请替换您的master节点的实际IP。

再添加几个实例,重复执行第四步,即可向Kubernetes集群中增加节点。

此时在master几点上执行kubectl get nodes查看节点所有节点状态,并创建nginx deployment,如下图所示:

Play with Kuberentes (PWK) is a project hacked by Marcos Lilljedahl and Jonathan Leibiusky and sponsored by Docker Inc.

Kubernetes与云原生应用概览

几个月前Mesos已经宣布支持kubernetes,而在2017年10月份的DockerCon EU上Docker公司宣布同时官方支持Swarm和Kubernetes容器编排,kubernetes已然成为容器编排调度的标准。

作为全书的开头,首先从历史、生态和应用角度介绍一下kubernetes与云原生应用,深入浅出,高屋建瓴,没有深入到具体细节,主要是为了给初次接触kubernetes的小白扫盲,具体细节请参考链接。

从云计算到微服务再到云原生计算

下面将从云计算的发展历程引入云原生计算,请先看下图:

云原生应用到2020年将比目前至少翻一番,下图是Marc Wilczek的调查报告。

云计算介绍

云计算包含的内容十分繁杂,也有很多技术和公司牵强附会说自己是云计算公司,说自己是做云的,实际上可能风马牛不相及。说白了,云计算就是一种配置资源的方式,根据资源配置方式的不同我们可以把云计算从宏观上分为以下三种类型:

  • IaaS:这是为了想要建立自己的商业模式并进行自定义的客户,例如亚马逊的EC2、S3存储、Rackspace虚拟机等都是IaaS。
  • PaaS:工具和服务的集合,对于想用它来构建自己的应用程序或者想快速得将应用程序部署到生产环境而不必关心底层硬件的用户和开发者来说是特别有用的,比如Cloud Foundry、Google App Engine、Heroku等。
  • SaaS:终端用户可以直接使用的应用程序。这个就太多,我们生活中用到的很多软件都是SaaS服务,只要基于互联网来提供的服务基本都是SaaS服务,有的服务是免费的,比如Google Docs,还有更多的是根据我们购买的Plan和使用量付费,比如GitHub、各种云存储。

微服务介绍

微服务(Microservices)这个词比较新颖,但是其实这种架构设计理念早就有了。微服务是一种分布式架构设计理念,为了推动细粒度服务的使用,这些服务要能协同工作,每个服务都有自己的生命周期。一个微服务就是一个独立的实体,可以独立的部署在PAAS平台上,也可以作为一个独立的进程在主机中运行。服务之间通过API访问,修改一个服务不会影响其它服务。

下文中会谈到kubernetes与微服务的关系,其中kubernetes的service天生就适合与微服务。

云原生概念介绍

下面是Cloud Native概念思维导图

云原生准确来说是一种文化,更是一种潮流,它是云计算的一个必然导向。它的意义在于让云成为云化战略成功的基石,而不是阻碍,如果业务应用上云之后开发和运维人员比原先还痛苦,成本还高的话,这样的云我们宁愿不不上。

自从云的概念开始普及,许多公司都部署了实施云化的策略,纷纷搭建起云平台,希望完成传统应用到云端的迁移。但是这个过程中会遇到一些技术难题,上云以后,效率并没有变得奇高,故障也没有迅速定位。

为了解决传统应用升级缓慢、架构臃肿、不能快速迭代、故障不能快速定位、问题无法快速解决等问题,云原生这一概念横空出世。云原生可以改进应用开发的效率,改变企业的组织结构,甚至会在文化层面上直接影响一个公司的决策。

另外,云原生也很好地解释了云上运行的应用应该具备什么样的架构特性——敏捷性、可扩展性、故障可恢复性。

综上所述,云原生应用应该具备以下几个关键词:

  • 敏捷
  • 可靠
  • 高弹性
  • 易扩展
  • 故障隔离保护
  • 不中断业务持续更新

以上特性也是云原生区别于传统云应用的优势特点。

从宏观概念上讲,云原生是不同思想的集合,集目前各种热门技术之大成,具体包括如下图所示的几个部分。

Kubernetes与云原生的关系

Kuberentes可以说是乘着docker和微服务的东风,一经推出便迅速蹿红,它的很多设计思想都契合了微服务和云原生应用的设计法则,这其中最著名的就是开发了Heroku PaaS平台的工程师们总结的 Twelve-factor App了。

下面我将讲解Kubernetes设计时是如何按照了十二因素应用法则,并给出kubernetes中的应用示例,并附上一句话简短的介绍。

Kubernetes介绍

Kubernetes是Google基于Borg开源的容器编排调度引擎,作为CNCF(Cloud Native Computing Foundation)最重要的组件之一,它的目标不仅仅是一个编排系统,而是提供一个规范,可以让你来描述集群的架构,定义服务的最终状态,kubernetes可以帮你将系统自动得达到和维持在这个状态。

更直白的说,Kubernetes用户可以通过编写一个yaml或者json格式的配置文件,也可以通过工具/代码生成或直接请求kubernetes API创建应用,该配置文件中包含了用户想要应用程序保持的状态,不论整个kubernetes集群中的个别主机发生什么问题,都不会影响应用程序的状态,你还可以通过改变该配置文件或请求kubernetes API来改变应用程序的状态。

12因素应用

12因素应用提出已经有几年的时间了,每个人对其可能都有自己的理解,切不可生搬硬套,也不一定所有云原生应用都必须符合这12条法则,其中有几条法则可能还有点争议,有人对其的解释和看法不同。

大家不要孤立的来看这每一个因素,将其与自己软件开发流程联系起来,这12个因素大致就是按照软件从开发到交付的流程顺序来写的。

1.基准代码

每个代码仓库(repo)都生成docker image保存到镜像仓库中,并使用唯一的ID管理,在Jenkins中使用编译时的ID。

2.依赖

显式得声明代码中的依赖,使用软件包管理工具声明,比如Go中的Glide。

3.配置

将配置与代码分离,应用部署到kubernete中可以使用容器的环境变量或ConfigMap挂载到容器中。

4.后端服务

把后端服务当作附加资源,实质上是计算存储分离和降低服务耦合,分解单体应用。

5.构建、发布、运行

严格分离构建和运行,每次修改代码生成新的镜像,重新发布,不能直接修改运行时的代码和配置。

6.进程

应用程序进程应该是无状态的,这意味着再次重启后还可以计算出原先的状态。

7.端口绑定

在kubernetes中每个Pod都有独立的IP,每个运行在Pod中的应用不必关心端口是否重复,只需在service中指定端口,集群内的service通过配置互相发现。

8.并发

每个容器都是一个进程,通过增加容器的副本数实现并发。

9.易处理

快速启动和优雅终止可最大化健壮性,kuberentes优秀的Pod生存周期控制

10.开发环境与线上环境等价

在kubernetes中可以创建多个namespace,使用相同的镜像可以很方便的复制一套环境出来,镜像的使用可以很方便的部署一个后端服务。

11.日志

把日志当作事件流,使用stdout输出并收集汇聚起来,例如到ES中统一查看。

12.管理进程

后台管理任务当作一次性进程运行,kubectl exec进入容器内部操作。

另外,Cloud Native Go 这本书的作者,CapitalOne公司的Kevin Hoffman在TalkingData T11峰会上的High Level Cloud Native的演讲中讲述了云原生应用的15个因素,在原先的12因素应用的基础上又增加了如下三个因素:

API优先

  • 服务间的合约
  • 团队协作的规约
  • 文档化、规范化
  • RESTful或RPC

监控

  • 实时监控远程应用
  • 应用性能监控(APM)
  • 应用健康监控
  • 系统日志
  • 不建议在线Debug

认证授权

  • 不要等最后才去考虑应用的安全性
  • 详细设计、明确声明、文档化
  • Bearer token、OAuth、OIDC认证
  • 操作审计

详见High Level Cloud Native From Kevin Hoffman

Kubernetes中的资源管理与容器设计模式

Kubernetes通过声明式配置,真正让开发人员能够理解应用的状态,并通过同一份配置可以立马启动一个一模一样的环境,大大提高了应用开发和部署的效率,其中kubernetes设计的多种资源类型可以帮助我们定义应用的运行状态,并使用资源配置来细粒度得明确限制应用的资源使用。

而容器生态的成熟是 Kubernetes 诞生的前提,在谈到容器的设计模式之前我们先来了解下容器生态,请看下图:

容器的设计模式

Kubernetes提供了多种资源对象,用户可以根据自己应用的特性加以选择。这些对象有:

类别 名称
资源对象 Pod、ReplicaSet、ReplicationController、Deployment、StatefulSet、DaemonSet、Job、CronJob、HorizontalPodAutoscaling
配置对象 Node、Namespace、Service、Secret、ConfigMap、Ingress、Label、ThirdPartyResource、 ServiceAccount
存储对象 Volume、Persistent Volume
策略对象 SecurityContext、ResourceQuota、LimitRange

在 Kubernetes 系统中,Kubernetes 对象 是持久化的条目。Kubernetes 使用这些条目去表示整个集群的状态。特别地,它们描述了如下信息:

  • 什么容器化应用在运行(以及在哪个 Node 上)
  • 可以被应用使用的资源
  • 关于应用如何表现的策略,比如重启策略、升级策略,以及容错策略

Kubernetes 对象是 “目标性记录” —— 一旦创建对象,Kubernetes 系统将持续工作以确保对象存在。通过创建对象,可以有效地告知 Kubernetes 系统,所需要的集群工作负载看起来是什么样子的,这就是 Kubernetes 集群的 期望状态

部署Kubernetes集群

使用二进制部署 kubernetes 集群的所有组件和插件,而不是使用 kubeadm 等自动化方式来部署集群,同时开启了集群的TLS安全认证,这样可以帮助我们解系统各组件的交互原理,进而能快速解决实际问题。

集群详情

  • Kubernetes 1.6.0
  • Docker 1.12.5(使用yum安装)
  • Etcd 3.1.5
  • Flanneld 0.7 vxlan 网络
  • TLS 认证通信 (所有组件,如 etcd、kubernetes master 和 node)
  • RBAC 授权
  • kublet TLS BootStrapping
  • kubedns、dashboard、heapster(influxdb、grafana)、EFK(elasticsearch、fluentd、kibana) 集群插件
  • 私有docker镜像仓库harbor(请自行部署,harbor提供离线安装包,直接使用docker-compose启动即可)

服务发现与负载均衡

Kubernetes在设计之初就充分考虑了针对容器的服务发现与负载均衡机制,提供了Service资源,并通过kube-proxy配合cloud provider来适应不同的应用场景。随着kubernetes用户的激增,用户场景的不断丰富,又产生了一些新的负载均衡机制。目前,kubernetes中的负载均衡大致可以分为以下几种机制,每种机制都有其特定的应用场景:

  • Service:直接用Service提供cluster内部的负载均衡,并借助cloud provider提供的LB提供外部访问
  • Ingress:还是用Service提供cluster内部的负载均衡,但是通过自定义LB提供外部访问
  • Service Load Balancer:把load balancer直接跑在容器中,实现Bare Metal的Service Load Balancer
  • Custom Load Balancer:自定义负载均衡,并替代kube-proxy,一般在物理部署Kubernetes时使用,方便接入公司已有的外部服务

持续集成与发布

应用构建和发布流程说明:

  1. 用户向Gitlab提交代码,代码中必须包含Dockerfile
  2. 将代码提交到远程仓库
  3. 用户在发布应用时需要填写git仓库地址和分支、服务类型、服务名称、资源数量、实例个数,确定后触发Jenkins自动构建
  4. Jenkins的CI流水线自动编译代码并打包成docker镜像推送到Harbor镜像仓库
  5. Jenkins的CI流水线中包括了自定义脚本,根据我们已准备好的kubernetes的YAML模板,将其中的变量替换成用户输入的选项
  6. 生成应用的kubernetes YAML配置文件
  7. 更新Ingress的配置,根据新部署的应用的名称,在ingress的配置文件中增加一条路由信息
  8. 更新PowerDNS,向其中插入一条DNS记录,IP地址是边缘节点的IP地址。关于边缘节点,请查看边缘节点配置
  9. Jenkins调用kubernetes的API,部署应用

日志收集与监控

基于现有的ELK日志收集方案,稍作改造,选用filebeat来收集日志,可以作为sidecar的形式跟应用运行在同一个Pod中,比较轻量级消耗资源比较少。

详见Kubernetes Handbook - 应用日志收集。

安全性与权限管理

Kubernetes是一个多租户的云平台,因此必须对用户的权限加以限制,对用户空间进行隔离。Kubernetes中的隔离主要包括这几种:

  • 网络隔离:需要使用网络插件,比如calico
  • 资源隔离:kubernetes原生支持资源隔离,pod就是资源就是隔离和调度的最小单位,同时使用namespace限制用户空间和资源限额。
  • 身份隔离:使用RBAC-基于角色的访问控制,多租户的身份认证和权限控制。

如何开发Kubernetes原生应用步骤介绍

当我们有了一个kubernetes集群后,如何在上面开发和部署应用,应该遵循怎样的流程?下面我将展示如何使用go语言开发和部署一个kubernetes native应用,使用wercker进行持续集成与持续发布,我将以一个很简单的前后端访问,获取伪造数据并展示的例子来说明。

云原生应用开发示例

我们将按照如下步骤来开发部署一个kubernetes原生应用并将它部署到kubernetes集群上开放给集群外访问:

  1. 服务API的定义
  2. 使用Go语言开发kubernetes原生应用
  3. 一个持续构建与发布工具与环境
  4. 使用traefik和VIP做边缘节点提供外部访问路由

我写了两个示例用于演示,开发部署一个伪造的 metric 并显示在 web 页面上,包括两个service:

定义API生成API文档

使用API blueprint格式,定义API文档,格式类似于markdown,再使用aglio生成HTML文档。

如何迁移到云原生应用架构

Pivotal 是云原生应用的提出者,并推出了 Pivotal Cloud Foundry 云原生应用平台和 Spring 开源 Java 开发框架,成为云原生应用架构中先驱者和探路者。

原书作于2015年,其中的示例主要针对 Java 应用,实际上也适用于任何应用类型,云原生应用架构适用于异构语言的程序开发,不仅仅是针对 Java 语言的程序开发。截止到本人翻译本书时,云原生应用生态系统已经初具规模,CNCF 成员不断发展壮大,基于 Cloud Native 的创业公司不断涌现,kubernetes 引领容器编排潮流,和 Service Mesh 技术(如 LinkerdIstio) 的出现,Go 语言的兴起(参考另一本书 Cloud Native Go)等为我们将应用迁移到云原生架构的提供了更多的方案选择。

迁移到云原生应用架构指南

指出了迁移到云原生应用架构需要做出的企业文化、组织架构和技术变革,并给出了迁移指南。

主要讨论的应用程序架构包括:

  • 十二因素应用程序:云原生应用程序架构模式的集合
  • 微服务:独立部署的服务,只做一件事情
  • 自助服务的敏捷基础设施:快速,可重复和一致地提供应用环境和后台服务的平台
  • 基于API的协作:发布和版本化的API,允许在云原生应用程序架构中的服务之间进行交互
  • 抗压性:根据压力变强的系统

迁移案例解析

迁移步骤示意图如下:

步骤说明:

  1. 将原有应用拆解为服务
  2. 容器化、制作镜像
  3. 准备应用配置文件
  4. 准备kubernetes YAML文件
  5. 编写bootstarp脚本
  6. 创建ConfigMaps

Service mesh基本原理和示例介绍

Service mesh现在一般被翻译作服务网格,目前主流的Service mesh有如下两款:

什么是Service mesh

如果用一句话来解释什么是 Service Mesh,可以将它比作是应用程序或者说微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控。对于编写应用程序来说一般无须关心 TCP/IP 这一层(比如通过 HTTP 协议的 RESTful 应用),同样使用 Service Mesh 也就无须关系服务之间的那些原来是通过应用程序或者其他框架实现的事情,比如 Spring Cloud、OSS,现在只要交给 Service Mesh 就可以了。

Service mesh使用指南

两款Service mesh各有千秋,我分别写了他们的使用案例指南:

  • 微服务管理框架service mesh——Linkerd安装试用笔记
  • 微服务管理框架service mesh——Istio安装试用笔记

更多关于 Service Mesh 的内容请访问 Service Mesh 中文网

DevOps

下面是社区中kubernetes开源爱好者的分享内容,我觉得是对kubernetes在DevOps中应用的很好的形式值得大家借鉴。

真正践行DevOps,让开发人员在掌握自己的开发和测试环境,让环境一致,让开发效率提升,让运维没有堆积如山的tickets,让监控更加精准,从kubernetes平台开始。

行动指南

  1. 根据环境(比如开发、测试、生产)划分namespace,也可以根据项目来划分
  2. 再为每个用户划分一个namespace、创建一个serviceaccountkubeconfig文件,不同namespace间的资源隔离,目前不隔离网络,不同namespace间的服务可以互相访问
  3. 创建yaml模板,降低编写kubernetes yaml文件编写难度
  4. kubectl命令上再封装一层,增加用户身份设置和环境初始化操作,简化kubectl命令和常用功能
  5. 管理员通过dashboard查看不同namespace的状态,也可以使用它来使操作更便捷
  6. 所有应用的日志统一收集到ElasticSearch中,统一日志访问入口
  7. 可以通过Grafana查看所有namespace中的应用的状态和kubernetes集群本身的状态
  8. 需要持久化的数据保存在分布式存储中,例如GlusterFS或Ceph中

使用Kibana查看日志

日志字段中包括了应用的标签、容器名称、主机名称、宿主机名称、IP地址、时间、

使用Grafana查看应用状态

:感谢【K8S?Cloud Native实战群】尊贵的黄金会员小刚同学提供下面的Grafana监控图?

监控分类示意图:

该监控图可以看到集群硬件使用情况。

该监控可以看到单个用户的namespace下的所有资源的使用情况。

Spark on Kubernetes

Spark原生支持standalone、mesos和YARN资源调度,现已支持Kubernetes原生调度。

使用kubernetes原生调度的spark on kubernetes是对原先的spark on yarn和yarn on docker的改变是革命性的,主要表现在以下几点:

  1. Kubernetes原生调度:不再需要二层调度,直接使用kubernetes的资源调度功能,跟其他应用共用整个kubernetes管理的资源池;
  2. 资源隔离,粒度更细:原先yarn中的queue在spark on kubernetes中已不存在,取而代之的是kubernetes中原生的namespace,可以为每个用户分别指定一个namespace,限制用户的资源quota;
  3. 细粒度的资源分配:可以给每个spark任务指定资源限制,实际指定多少资源就使用多少资源,因为没有了像yarn那样的二层调度(圈地式的),所以可以更高效和细粒度的使用资源;
  4. 监控的变革:因为做到了细粒度的资源分配,所以可以对用户提交的每一个任务做到资源使用的监控,从而判断用户的资源使用情况,所有的metric都记录在数据库中,甚至可以为每个用户的每次任务提交计量;
  5. 日志的变革:用户不再通过yarn的web页面来查看任务状态,而是通过pod的log来查看,可将所有的kuberentes中的应用的日志等同看待收集起来,然后可以根据标签查看对应应用的日志;

如何提交任务

仍然使用spark-submit提交spark任务,可以直接指定kubernetes API server地址,下面的命令提交本地jar包到kubernetes集群上运行,同时指定了运行任务的用户、提交命名的用户、运行的excutor实例数、driver和executor的资源限制、使用的spark版本等信息。

./spark-submit \
  --deploy-mode cluster \
  --class com.talkingdata.alluxio.hadooptest \
  --master k8s://https://172.20.0.113:6443 \
  --kubernetes-namespace spark-cluster \
  --conf spark.kubernetes.driverEnv.SPARK_USER=hadoop \
  --conf spark.kubernetes.driverEnv.HADOOP_USER_NAME=hadoop \
  --conf spark.executorEnv.HADOOP_USER_NAME=hadoop \
  --conf spark.executorEnv.SPARK_USER=hadoop \
  --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
  --conf spark.driver.memory=100G \
  --conf spark.executor.memory=10G \
  --conf spark.driver.cores=30 \
  --conf spark.executor.cores=2 \
  --conf spark.driver.maxResultSize=10240m \
  --conf spark.kubernetes.driver.limit.cores=32 \
  --conf spark.kubernetes.executor.limit.cores=3 \
  --conf spark.kubernetes.executor.memoryOverhead=2g \
  --conf spark.executor.instances=5 \
  --conf spark.app.name=spark-pi \
  --conf spark.kubernetes.driver.docker.image=sz-pg-oam-docker-hub-001.tendcloud.com/library/spark-driver:v2.1.0-kubernetes-0.3.1-1 \
  --conf spark.kubernetes.executor.docker.image=sz-pg-oam-docker-hub-001.tendcloud.com/library/spark-executor:v2.1.0-kubernetes-0.3.1-1 \
  --conf spark.kubernetes.initcontainer.docker.image=sz-pg-oam-docker-hub-001.tendcloud.com/library/spark-init:v2.1.0-kubernetes-0.3.1-1 \
  --conf spark.kubernetes.resourceStagingServer.uri=http://172.20.0.114:31000 \
~/Downloads/tendcloud_2.10-1.0.jar

监控

下图是从Kubernetes dashboard上看到的spark-cluster这个namespace上运行的应用情况。

下图是从Grafana监控页面上查看到的某个executor资源占用情况。

云原生应用之路——从Kubernetes到Cloud Native

从Kubernetes到Cloud Native——云原生应用之路,这是我最近在 ArchSummit2017北京站数人云&TalkingData合办的Service Mesh is comming meetup 中分享的话题。

本文简要介绍了容器技术发展的路径,为何Kubernetes的出现是容器技术发展到这一步的必然选择,而为何Kuberentes又将成为云原生应用的基石。

我的分享按照这样的主线展开:容器->Kubernetes->微服务->Cloud Native(云原生)->Service Mesh(服务网格)->使用场景->Open Source(开源)。

容器

容器——Cloud Native的基石

容器最初是通过开发者工具而流行,可以使用它来做隔离的开发测试环境和持续集成环境,这些都是因为容器轻量级,易于配置和使用带来的优势,docker和docker-compose这样的工具极大的方便的了应用开发环境的搭建,开发者就像是化学家一样在其中小心翼翼的进行各种调试和开发。

随着容器的在开发者中的普及,已经大家对CI流程的熟悉,容器周边的各种工具蓬勃发展,俨然形成了一个小生态:

该生态涵盖了容器应用中从镜像仓库、服务编排、安全管理、持续集成与发布、存储和网络管理等各个方面,随着在单主机中运行容器的成熟,集群管理和容器编排成为容器技术亟待解决的问题。譬如化学家在实验室中研究出来的新产品,如何推向市场,进行大规模生产,成了新的议题。

为什么使用Kubernetes

Kubernetes——让容器应用进入大规模工业生产。

Kubernetes是容器编排系统的事实标准

在单机上运行容器,无法发挥它的最大效能,只有形成集群,才能最大程度发挥容器的良好隔离、资源分配与编排管理的优势,而对于容器的编排管理,Swarm、Mesos和Kubernetes的大战已经基本宣告结束,kubernetes成为了无可争议的赢家。

下面这张图是Kubernetes的架构图(图片来自网络),其中显示了组件之间交互的接口CNI、CRI、OCI等,这些将Kubernetes与某款具体产品解耦,给用户最大的定制程度,使得Kubernetes有机会成为跨云的真正的云原生应用的操作系统。

随着Kubernetes的日趋成熟,“Kubernetes is becoming boring”,基于该“操作系统”之上构建的适用于不同场景的应用将成为新的发展方向,就像我们将石油开采出来后,提炼出汽油、柴油、沥青等等,所有的材料都将找到自己的用途,Kubernetes也是,毕竟我们谁也不是为了部署和管理容器而用Kubernetes,承载其上的应用才是价值之所在。

云原生的核心目标

云已经可以为我们提供稳定可以唾手可得的基础设施,但是业务上云成了一个难题,Kubernetes的出现与其说是从最初的容器编排解决方案,倒不如说是为了解决应用上云(即云原生应用)这个难题。

包括微服务和FaaS/Serverless架构,都可以作为云原生应用的架构。

但就2017年为止,kubernetes的主要使用场景也主要作为应用开发测试环境、CI/CD和运行Web应用这几个领域,如下图TheNewStack的Kubernetes生态状况调查报告所示。

另外基于Kubernetes的构建PaaS平台和Serverless也处于爆发的准备的阶段,如下图中Gartner的报告中所示:

当前各大公有云如Google GKE、微软Azure ACS、亚马逊EKS(2018年上线)、VmWare、Pivotal、腾讯云、阿里云等都提供了Kuberentes服务。

微服务

微服务——Cloud Native的应用架构。

下图是Bilgin Ibryam给出的微服务中应该关心的主题,图片来自RedHat Developers

微服务带给我们很多开发和部署上的灵活性和技术多样性,但是也增加了服务调用的开销、分布式系统管理、调试与服务治理方面的难题。

当前最成熟最完整的微服务框架可以说非Spring莫属,而Spring又仅限于Java语言开发,其架构本身又跟Kubernetes存在很多重合的部分,如何探索将Kubernetes作为微服务架构平台就成为一个热点话题。

就拿微服务中最基础的服务注册发现功能来说,其方式分为客户端服务发现服务端服务发现两种,Java应用中常用的方式是使用Eureka和Ribbon做服务注册发现和负载均衡,这属于客户端服务发现,而在Kubernetes中则可以使用DNS、Service和Ingress来实现,不需要修改应用代码,直接从网络层面来实现。

Cloud Native

DevOps——通向云原生的云梯

CNCF(云原生计算基金会)给出了云原生应用的三大特征:

  • 容器化包装:软件应用的进程应该包装在容器中独立运行。
  • 动态管理:通过集中式的编排调度系统来动态的管理和调度。
  • 微服务化:明确服务间的依赖,互相解耦。

下图是我整理的关于云原生所需要的能力和特征。

CNCF所托管的应用(目前已达12个),即朝着这个目标发展,其公布的Cloud Native Landscape,给出了云原生生态的参考体系。

使用Kubernetes构建云原生应用

我们都是知道Heroku推出了适用于PaaS的12 factor app的规范,包括如下要素:

  1. 基准代码
  2. 依赖管理
  3. 配置
  4. 后端服务
  5. 构建,发布,运行
  6. 无状态进程
  7. 端口绑定
  8. 并发
  9. 易处理
  10. 开发环境与线上环境等价
  11. 日志作为事件流
  12. 管理进程

另外还有补充的三点:

  • API声明管理
  • 认证和授权
  • 监控与告警

如果落实的具体的工具,请看下图,使用Kubernetes构建云原生架构:

结合这12因素对开发或者改造后的应用适合部署到Kubernetes之上,基本流程如下图所示:

Service Mesh

Services for show, meshes for a pro.

Kubernetes中的应用将作为微服务运行,但是Kuberentes本身并没有给出微服务治理的解决方案,比如服务的限流、熔断、良好的灰度发布支持等。

Service mesh可以用来做什么

  • Traffic Management:API网关
  • Observability:服务调用和性能分析
  • Policy Enforcment:控制服务访问策略
  • Service Identity and Security:安全保护

Service mesh的特点

  • 专用的基础设施层
  • 轻量级高性能网络代理
  • 提供安全的、快速的、可靠地服务间通讯
  • 扩展kubernetes的应用负载均衡机制,实现灰度发布
  • 完全解耦于应用,应用可以无感知,加速应用的微服务和云原生转型

使用Service Mesh将可以有效的治理Kuberentes中运行的服务,当前开源的Service Mesh有:

此外还有很多其它的Service Mesh鱼贯而出,请参考awesome-cloud-native

Istio VS Linkerd

Linkerd和Istio是最早开源的Service Mesh,它们都支持Kubernetes,下面是它们之间的一些特性对比。

Feature Istio Linkerd
部署架构 Envoy/Sidecar DaemonSets
易用性 复杂 简单
支持平台 kuberentes kubernetes/mesos/Istio/local
当前版本 0.3.0 1.3.3
是否已有生产部署

关于两者的架构可以参考各自的官方文档,我只从其在kubernetes上的部署结构来说明其区别。

Istio的组件复杂,可以分别部署的kubernetes集群中,但是作为核心路由组件Envoy是以Sidecar形式与应用运行在同一个Pod中的,所有进入该Pod中的流量都需要先经过Envoy。

Linker的部署十分简单,本身就是一个镜像,使用Kubernetes的DaemonSet方式在每个node节点上运行。

使用场景

Cloud Native的大规模工业生产

GitOps

给开发者带来最大配置和上线的灵活性,践行DevOps流程,改善研发效率,下图这样的情况将更少发生。

我们知道Kubernetes中的所有应用的部署都是基于YAML文件的,这实际上就是一种Infrastructure as code,完全可以通过Git来管控基础设施和部署环境的变更。

Big Data

Spark现在已经非官方支持了基于Kuberentes的原生调度,其具有以下特点:

  • Kubernetes原生调度:与yarn、mesos同级
  • 资源隔离,粒度更细:以namespace来划分用户
  • 监控的变革:单次任务资源计量
  • 日志的变革:pod的日志收集
Feature Yarn Kubernetes
queue queue namespace
instance ExcutorContainer Executor Pod
network host plugin
heterogeneous no yes
security RBAC ACL

下图是在Kubernetes上运行三种调度方式的spark的单个节点的应用部分对比:

从上图中可以看到在Kubernetes上使用YARN调度、standalone调度和kubernetes原生调度的方式,每个node节点上的Pod内的spark Executor分布,毫无疑问,使用kubernetes原生调度的spark任务才是最节省资源的。

提交任务的语句看起来会像是这样的:

./spark-submit \
  --deploy-mode cluster \
  --class com.talkingdata.alluxio.hadooptest \
  --master k8s://https://172.20.0.113:6443 \
  --kubernetes-namespace spark-cluster \
  --conf spark.kubernetes.driverEnv.SPARK_USER=hadoop \
  --conf spark.kubernetes.driverEnv.HADOOP_USER_NAME=hadoop \
  --conf spark.executorEnv.HADOOP_USER_NAME=hadoop \
  --conf spark.executorEnv.SPARK_USER=hadoop \
  --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
  --conf spark.driver.memory=100G \
  --conf spark.executor.memory=10G \
  --conf spark.driver.cores=30 \
  --conf spark.executor.cores=2 \
  --conf spark.driver.maxResultSize=10240m \
  --conf spark.kubernetes.driver.limit.cores=32 \
  --conf spark.kubernetes.executor.limit.cores=3 \
  --conf spark.kubernetes.executor.memoryOverhead=2g \
  --conf spark.executor.instances=5 \
  --conf spark.app.name=spark-pi \
  --conf spark.kubernetes.driver.docker.image=spark-driver:v2.1.0-kubernetes-0.3.1-1 \
  --conf spark.kubernetes.executor.docker.image=spark-executor:v2.1.0-kubernetes-0.3.1-1 \
  --conf spark.kubernetes.initcontainer.docker.image=spark-init:v2.1.0-kubernetes-0.3.1-1 \
  --conf spark.kubernetes.resourceStagingServer.uri=http://172.20.0.114:31000 \
~/Downloads/tendcloud_2.10-1.0.jar

Open Source

Contributing is Not only about code, it is about helping a community.

下图是我们刚调研准备使用Kubernetes时候的调研方案选择。

对于一个初次接触Kubernetes的人来说,看到这样一个庞大的架构选型时会望而生畏,但是Kubernetes的开源社区帮助了我们很多。

我组建了K8S&Cloud Native实战微信群,参与了k8smeetup、KEUC2017、kubernetes-docs-cn Kubernetes官方中文文档项目。

🚓概念与原理

Kubernetes架构

Kubernetes最初源于谷歌内部的Borg,提供了面向应用的容器集群部署和管理系统。Kubernetes的目标旨在消除编排物理/虚拟计算,网络和存储基础设施的负担,并使应用程序运营商和开发人员完全将重点放在以容器为中心的原语上进行自助运营。Kubernetes 也提供稳定、兼容的基础(平台),用于构建定制化的workflows 和更高级的自动化任务。
Kubernetes 具备完善的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建负载均衡器、故障发现和自我修复能力、服务滚动升级和在线扩容、可扩展的资源自动调度机制、多粒度的资源配额管理能力。
Kubernetes 还提供完善的管理工具,涵盖开发、部署测试、运维监控等各个环节。

Borg简介

Borg是谷歌内部的大规模集群管理系统,负责对谷歌内部很多核心服务的调度和管理。Borg的目的是让用户能够不必操心资源管理的问题,让他们专注于自己的核心业务,并且做到跨多个数据中心的资源利用率最大化。

Borg主要由BorgMaster、Borglet、borgcfg和Scheduler组成,如下图所示

  • BorgMaster是整个集群的大脑,负责维护整个集群的状态,并将数据持久化到Paxos存储中;
  • Scheduer负责任务的调度,根据应用的特点将其调度到具体的机器上去;
  • Borglet负责真正运行任务(在容器中);
  • borgcfg是Borg的命令行工具,用于跟Borg系统交互,一般通过一个配置文件来提交任务。

Kubernetes架构

Kubernetes借鉴了Borg的设计理念,比如Pod、Service、Labels和单Pod单IP等。Kubernetes的整体架构跟Borg非常像,如下图所示

Kubernetes主要由以下几个核心组件组成:

  • etcd保存了整个集群的状态;
  • apiserver提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;
  • controller manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
  • scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;
  • kubelet负责维护容器的生命周期,同时也负责Volume(CVI)和网络(CNI)的管理;
  • Container runtime负责镜像管理以及Pod和容器的真正运行(CRI);
  • kube-proxy负责为Service提供cluster内部的服务发现和负载均衡;

除了核心组件,还有一些推荐的Add-ons:

  • kube-dns负责为整个集群提供DNS服务
  • Ingress Controller为服务提供外网入口
  • Heapster提供资源监控
  • Dashboard提供GUI
  • Federation提供跨可用区的集群

Kubernetes架构示意图

整体架构

下图清晰表明了kubernetes的架构设计以及组件之间的通信协议。

下面是更抽象的一个视图:

Master架构

Node架构

分层架构

Kubernetes设计理念和功能其实就是一个类似Linux的分层架构,如下图所示

  • 核心层:Kubernetes最核心的功能,对外提供API构建高层的应用,对内提供插件式应用执行环境
  • 应用层:部署(无状态应用、有状态应用、批处理任务、集群应用等)和路由(服务发现、DNS解析等)
  • 管理层:系统度量(如基础设施、容器和网络的度量),自动化(如自动扩展、动态Provision等)以及策略管理(RBAC、Quota、PSP、NetworkPolicy等)
  • 接口层:kubectl命令行工具、客户端SDK以及集群联邦
  • 生态系统:在接口层之上的庞大容器集群管理调度的生态系统,可以划分为两个范畴
    • Kubernetes外部:日志、监控、配置管理、CI、CD、Workflow、FaaS、OTS应用、ChatOps等
    • Kubernetes内部:CRI、CNI、CVI、镜像仓库、Cloud Provider、集群自身的配置和管理等

关于分层架构,可以关注下Kubernetes社区正在推进的Kbernetes architectual roadmapslide

Etcd解析

Etcd是Kubernetes集群中的一个十分重要的组件,用于保存集群所有的网络配置和对象的状态信息。在后面具体的安装环境中,我们安装的etcd的版本是v3.1.5,整个kubernetes系统中一共有两个服务需要用到etcd用来协同和存储配置,分别是:

  • 网络插件flannel、对于其它网络插件也需要用到etcd存储网络的配置信息
  • kubernetes本身,包括各种对象的状态和元信息配置

注意:flannel操作etcd使用的是v2的API,而kubernetes操作kubernetes使用的v3的API,所以在下面我们执行ectdctl的时候需要设置ECTDCTL_API环境变量,该变量默认值为2。

原理

Etcd使用的是raft一致性算法来实现的,是一款分布式的一致性KV存储,只要用于共享配置和服务发现。关于raft一致性算法请参考该动画演示

关于Ectd的原理解析请参考Etcd 架构与实现解析

使用Etcd存储Flannel网络信息

我们在安装Flannel的时候配置了FLANNEL_ETCD_PREFIX="/kube-centos/network"参数,这是Flannel查询etcd的目录地址。

查看Etcd中存储的flannel网络信息:

$ etcdctl --ca-file=/etc/kubernetes/ssl/ca.pem --cert-file=/etc/kubernetes/ssl/kubernetes.pem --key-file=/etc/kubernetes/ssl/kubernetes-key.pem ls /kube-centos/network -r
2018-01-19 18:38:22.768145 I | warning: ignoring ServerName for user-provided CA for backwards compatibility is deprecated
/kube-centos/network/config
/kube-centos/network/subnets
/kube-centos/network/subnets/172.30.31.0-24
/kube-centos/network/subnets/172.30.20.0-24
/kube-centos/network/subnets/172.30.23.0-24

查看flannel的配置:

$ etcdctl --ca-file=/etc/kubernetes/ssl/ca.pem --cert-file=/etc/kubernetes/ssl/kubernetes.pem --key-file=/etc/kubernetes/ssl/kubernetes-key.pem get /kube-centos/network/config
2018-01-19 18:38:22.768145 I | warning: ignoring ServerName for user-provided CA for backwards compatibility is deprecated
{ "Network": "172.30.0.0/16", "SubnetLen": 24, "Backend": { "Type": "host-gw" } }

使用Etcd存储Kubernetes对象信息

Kubernetes使用etcd v3的API操作etcd中的数据。所有的资源对象都保存在/registry路径下,如下:

ThirdPartyResourceData
apiextensions.k8s.io
apiregistration.k8s.io
certificatesigningrequests
clusterrolebindings
clusterroles
configmaps
controllerrevisions
controllers
daemonsets
deployments
events
horizontalpodautoscalers
ingress
limitranges
minions
monitoring.coreos.com
namespaces
persistentvolumeclaims
persistentvolumes
poddisruptionbudgets
pods
ranges
replicasets
resourcequotas
rolebindings
roles
secrets
serviceaccounts
services
statefulsets
storageclasses
thirdpartyresources

如果你还创建了CRD(自定义资源定义),则在此会出现CRD的API。

Etcd V2与V3版本API的区别

Etcd V2和V3之间的数据结构完全不同,互不兼容,也就是说使用V2版本的API创建的数据只能使用V2的API访问,V3的版本的API创建的数据只能使用V3的API访问。这就造成我们访问etcd中保存的flannel的数据需要使用etcdctl的V2版本的客户端,而访问kubernetes的数据需要设置ETCDCTL_API=3环境变量来指定V3版本的API。

Etcd数据备份

我们安装的时候指定的Etcd数据的存储路径是/var/lib/etcd,一定要对该目录做好备份。

开放接口

Kubernetes作为云原生应用的的基础调度平台,相当于云原生的操作系统,为了便于系统的扩展,Kubernetes中开放的以下接口,可以分别对接不同的后端,来实现自己的业务逻辑:

  • CRI(Container Runtime Interface):容器运行时接口,提供计算资源
  • CNI(Container Network Interface):容器网络接口,提供网络资源
  • CSI(Container Storage Interface):容器存储接口,提供存储资源

以上三种资源相当于一个分布式操作系统的最基础的几种资源类型,而Kuberentes是将他们粘合在一起的纽带。

CRI - Container Runtime Interface(容器运行时接口)

CRI中定义了容器镜像的服务的接口,因为容器运行时与镜像的生命周期是彼此隔离的,因此需要定义两个服务。该接口使用Protocol Buffer,基于gRPC,在kubernetes v1.7+版本中是在pkg/kubelet/apis/cri/v1alpha1/runtimeapi.proto中定义的。

CRI架构

Container Runtime实现了CRI gRPC Server,包括RuntimeServiceImageService。该gRPC Server需要监听本地的Unix socket,而kubelet则作为gRPC Client运行。

启用CRI

除非集成了rktnetes,否则CRI都是被默认启用了,kubernetes1.7版本开始旧的预集成的docker CRI已经被移除。

要想启用CRI只需要在kubelet的启动参数重传入此参数:--container-runtime-endpoint远程运行时服务的端点。当前Linux上支持unix socket,windows上支持tcp。例如:unix:///var/run/dockershim.socktcp://localhost:373,默认是unix:///var/run/dockershim.sock,即默认使用本地的docker作为容器运行时。

关于CRI的详细进展请参考CRI: the Container Runtime Interface

CRI接口

Kubernetes1.9中的CRI接口在api.proto中的定义如下:

// Runtime service defines the public APIs for remote container runtimes
service RuntimeService {
    // Version returns the runtime name, runtime version, and runtime API version.
    rpc Version(VersionRequest) returns (VersionResponse) {}
    // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
    // the sandbox is in the ready state on success.
    rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
    // StopPodSandbox stops any running process that is part of the sandbox and
    // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
    // If there are any running containers in the sandbox, they must be forcibly
    // terminated.
    // This call is idempotent, and must not return an error if all relevant
    // resources have already been reclaimed. kubelet will call StopPodSandbox
    // at least once before calling RemovePodSandbox. It will also attempt to
    // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
    // multiple StopPodSandbox calls are expected.
    rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
    // RemovePodSandbox removes the sandbox. If there are any running containers
    // in the sandbox, they must be forcibly terminated and removed.
    // This call is idempotent, and must not return an error if the sandbox has
    // already been removed.
    rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
    // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
    // present, returns an error.
    rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
    // ListPodSandbox returns a list of PodSandboxes.
    rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
    // CreateContainer creates a new container in specified PodSandbox
    rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
    // StartContainer starts the container.
    rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
    // StopContainer stops a running container with a grace period (i.e., timeout).
    // This call is idempotent, and must not return an error if the container has
    // already been stopped.
    // TODO: what must the runtime do after the grace period is reached?
    rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
    // RemoveContainer removes the container. If the container is running, the
    // container must be forcibly removed.
    // This call is idempotent, and must not return an error if the container has
    // already been removed.
    rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
    // ListContainers lists all containers by filters.
    rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
    // ContainerStatus returns status of the container. If the container is not
    // present, returns an error.
    rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
    // UpdateContainerResources updates ContainerConfig of the container.
    rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
    // ExecSync runs a command in a container synchronously.
    rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
    // Exec prepares a streaming endpoint to execute a command in the container.
    rpc Exec(ExecRequest) returns (ExecResponse) {}
    // Attach prepares a streaming endpoint to attach to a running container.
    rpc Attach(AttachRequest) returns (AttachResponse) {}
    // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
    rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
    // ContainerStats returns stats of the container. If the container does not
    // exist, the call returns an error.
    rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}
    // ListContainerStats returns stats of all running containers.
    rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}
    // UpdateRuntimeConfig updates the runtime configuration based on the given request.
    rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}
    // Status returns the status of the runtime.
    rpc Status(StatusRequest) returns (StatusResponse) {}
}
// ImageService defines the public APIs for managing images.
service ImageService {
    // ListImages lists existing images.
    rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
    // ImageStatus returns the status of the image. If the image is not
    // present, returns a response with ImageStatusResponse.Image set to
    // nil.
    rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
    // PullImage pulls an image with authentication config.
    rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
    // RemoveImage removes the image.
    // This call is idempotent, and must not return an error if the image has
    // already been removed.
    rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
    // ImageFSInfo returns information of the filesystem that is used to store images.
    rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
}

这其中包含了两个gRPC服务:

  • RuntimeService:容器和Sandbox运行时管理
  • ImageService:提供了从镜像仓库拉取、查看、和移除镜像的RPC。

当前支持的CRI后端

我们最初在使用Kubernetes时通常会默认使用Docker作为容器运行时,其实从Kubernetes1.5开始已经开始支持CRI,目前是处于Alpha版本,通过CRI接口可以指定使用其它容器运行时作为Pod的后端,目前支持CNI的后端有:

  • cri-o:同时兼容OCI和CRI的容器运行时
  • cri-containerd:基于Containerd的Kubernetes CNI实现
  • rkt:由于CoreOS主推的用来跟docker抗衡的容器运行时
  • frakti:基于hypervisor的CRI
  • docker:kuberentes最初就开始支持的容器运行时,目前还没完全从kubelet中解耦,docker公司同时推广了OCI标准
  • clear-containers:由Intel推出的同时兼容OCI和CRI的容器运行时
  • kata-containers:符合OCI规范同时兼容CRI

CRI是由SIG-Node来维护的。

CNI - Container Network Interface(容器网络接口)

CNI(Container Network Interface)是CNCF旗下的一个项目,由一组用于配置Linux容器的网络接口的规范和库组成,同时还包含了一些插件。CNI仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。通过此链接浏览该项目:https://github.com/containernetworking/cni。

Kubernetes源码的vendor/github.com/containernetworking/cni/libcni目录中已经包含了CNI的代码,也就是说kubernetes中已经内置了CNI。

接口定义

CNI的接口中包括以下几个方法:

type CNI interface {
    AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
    DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error
    AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
    DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
}

该接口只有四个方法,添加网络、删除网络、添加网络列表、删除网络列表。

设计考量

CNI设计的时候考虑了以下问题:

  • 容器运行时必须在调用任何插件之前为容器创建一个新的网络命名空间。
  • 然后,运行时必须确定这个容器应属于哪个网络,并为每个网络确定哪些插件必须被执行。
  • 网络配置采用JSON格式,可以很容易地存储在文件中。网络配置包括必填字段,如nametype以及插件(类型)。网络配置允许字段在调用之间改变值。为此,有一个可选的字段args,必须包含不同的信息。
  • 容器运行时必须按顺序为每个网络执行相应的插件,将容器添加到每个网络中。
  • 在完成容器生命周期后,运行时必须以相反的顺序执行插件(相对于执行添加容器的顺序)以将容器与网络断开连接。
  • 容器运行时不能为同一容器调用并行操作,但可以为不同的容器调用并行操作。
  • 容器运行时必须为容器订阅ADD和DEL操作,这样ADD后面总是跟着相应的DEL。 DEL可能跟着额外的DEL,但是,插件应该允许处理多个DEL(即插件DEL应该是幂等的)。
  • 容器必须由ContainerID唯一标识。存储状态的插件应该使用(网络名称,容器ID)的主键来完成。
  • 运行时不能调用同一个网络名称或容器ID执行两次ADD(没有相应的DEL)。换句话说,给定的容器ID必须只能添加到特定的网络一次。

CNI插件

CNI插件必须实现一个可执行文件,这个文件可以被容器管理系统(例如rkt或Kubernetes)调用。

CNI插件负责将网络接口插入容器网络命名空间(例如,veth对的一端),并在主机上进行任何必要的改变(例如将veth的另一端连接到网桥)。然后将IP分配给接口,并通过调用适当的IPAM插件来设置与“IP地址管理”部分一致的路由。

参数

CNI插件必须支持以下操作:

将容器添加到网络

参数:

  • 版本。调用者正在使用的CNI规范(容器管理系统或调用插件)的版本。
  • 容器ID 。由运行时分配的容器的唯一明文标识符。一定不能是空的。
  • 网络命名空间路径。要添加的网络名称空间的路径,即/proc/[pid]/ns/net或绑定挂载/链接。
  • 网络配置。描述容器可以加入的网络的JSON文档。架构如下所述。
  • 额外的参数。这提供了一个替代机制,允许在每个容器上简单配置CNI插件。
  • 容器内接口的名称。这是应该分配给容器(网络命名空间)内创建的接口的名称;因此它必须符合Linux接口名称上的标准限制。

结果:

  • 接口列表。根据插件的不同,这可以包括沙箱(例如容器或管理程序)接口名称和/或主机接口名称,每个接口的硬件地址以及接口所在的沙箱(如果有的话)的详细信息。
  • 分配给每个接口的IP配置。分配给沙箱和/或主机接口的IPv4和/或IPv6地址,网关和路由。
  • DNS信息。包含nameserver、domain、search domain和option的DNS信息的字典。

从网络中删除容器

参数:

  • 版本。调用者正在使用的CNI规范(容器管理系统或调用插件)的版本。

  • 容器ID ,如上所述。

  • 网络命名空间路径,如上定义。

  • 网络配置,如上所述。

  • 额外的参数,如上所述。

  • 上面定义的容器内的接口的名称。

  • 所有参数应与传递给相应的添加操作的参数相同。

  • 删除操作应释放配置的网络中提供的containerid拥有的所有资源。

报告版本

  • 参数:无。
  • 结果:插件支持的CNI规范版本信息。
{
“cniVersion”:“0.3.1”,//此输出使用的CNI规范的版本
“supportedVersions”:[“0.1.0”,“0.2.0”,“0.3.0”,“0.3.1”] //此插件支持的CNI规范版本列表
}

CNI插件的详细说明请参考:CNI SPEC

IP分配

作为容器网络管理的一部分,CNI插件需要为接口分配(并维护)IP地址,并安装与该接口相关的所有必要路由。这给了CNI插件很大的灵活性,但也给它带来了很大的负担。众多的CNI插件需要编写相同的代码来支持用户需要的多种IP管理方案(例如dhcp、host-local)。

为了减轻负担,使IP管理策略与CNI插件类型解耦,我们定义了IP地址管理插件(IPAM插件)。CNI插件的职责是在执行时恰当地调用IPAM插件。 IPAM插件必须确定接口IP/subnet,网关和路由,并将此信息返回到“主”插件来应用配置。 IPAM插件可以通过协议(例如dhcp)、存储在本地文件系统上的数据、网络配置文件的“ipam”部分或上述的组合来获得信息。

IPAM插件

像CNI插件一样,调用IPAM插件的可执行文件。可执行文件位于预定义的路径列表中,通过CNI_PATH指示给CNI插件。 IPAM插件必须接收所有传入CNI插件的相同环境变量。就像CNI插件一样,IPAM插件通过stdin接收网络配置。

可用插件

Main:接口创建

  • bridge:创建网桥,并添加主机和容器到该往桥
  • ipvlan:在容器中添加一个ipvlan接口
  • loopback:创建一个回环接口
  • macvlan:创建一个新的MAC地址,将所有的流量转发到容器
  • ptp:创建veth对
  • vlan:分配一个vlan设备

IPAM:IP地址分配

  • dhcp:在主机上运行守护程序,代表容器发出DHCP请求
  • host-local:维护分配IP的本地数据库

Meta:其它插件

  • flannel:根据flannel的配置文件创建接口
  • tuning:调整现有接口的sysctl参数
  • portmap:一个基于iptables的portmapping插件。将端口从主机的地址空间映射到容器。

CSI - Container Storage Interface(容器存储接口)

CSI 代表容器存储接口,CSI 试图建立一个行业标准接口的规范,借助 CSI 容器编排系统(CO)可以将任意存储系统暴露给自己的容器工作负载。有关详细信息,请查看设计方案

csi 卷类型是一种 in-tree(即跟其它存储插件在同一个代码路径下,随 Kubernetes 的代码同时编译的) 的 CSI 卷插件,用于 Pod 与在同一节点上运行的外部 CSI 卷驱动程序交互。部署 CSI 兼容卷驱动后,用户可以使用 csi 作为卷类型来挂载驱动提供的存储。

CSI 持久化卷支持是在 Kubernetes v1.9 中引入的,作为一个 alpha 特性,必须由集群管理员明确启用。换句话说,集群管理员需要在 apiserver、controller-manager 和 kubelet 组件的 “--feature-gates =” 标志中加上 “CSIPersistentVolume = true”。

CSI 持久化卷具有以下字段可供用户指定:

  • driver:一个字符串值,指定要使用的卷驱动程序的名称。必须少于 63 个字符,并以一个字符开头。驱动程序名称可以包含 “。”、“ - ”、“_” 或数字。
  • volumeHandle:一个字符串值,唯一标识从 CSI 卷插件的 CreateVolume 调用返回的卷名。随后在卷驱动程序的所有后续调用中使用卷句柄来引用该卷。
  • readOnly:一个可选的布尔值,指示卷是否被发布为只读。默认是 false。

使用说明

下面将介绍如何使用 CSI。

动态配置

可以通过为 CSI 创建插件 StorageClass 来支持动态配置的 CSI Storage 插件启用自动创建/删除 。

例如,以下 StorageClass 允许通过名为 com.example.team/csi-driver 的 CSI Volume Plugin 动态创建 “fast-storage” Volume。

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast-storage
provisioner: com.example.team/csi-driver
parameters:
  type: pd-ssd

要触发动态配置,请创建一个 PersistentVolumeClaim 对象。例如,下面的 PersistentVolumeClaim 可以使用上面的 StorageClass 触发动态配置。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-request-for-storage
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: fast-storage

当动态创建 Volume 时,通过 CreateVolume 调用,将参数 type:pd-ssd 传递给 CSI 插件 com.example.team/csi-driver 。作为响应,外部 Volume 插件会创建一个新 Volume,然后自动创建一个 PersistentVolume 对象来对应前面的 PVC 。然后,Kubernetes 会将新的 PersistentVolume 对象绑定到 PersistentVolumeClaim,使其可以使用。

如果 fast-storage StorageClass 被标记为默认值,则不需要在 PersistentVolumeClaim 中包含 StorageClassName,它将被默认使用。

预配置 Volume

您可以通过手动创建一个 PersistentVolume 对象来展示现有 Volumes,从而在 Kubernetes 中暴露预先存在的 Volume。例如,暴露属于 com.example.team/csi-driver 这个 CSI 插件的 existingVolumeName Volume

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-manually-created-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  csi:
    driver: com.example.team/csi-driver
    volumeHandle: existingVolumeName
    readOnly: false

附着和挂载

您可以在任何的 pod 或者 pod 的 template 中引用绑定到 CSI volume 上的 PersistentVolumeClaim

kind: Pod
apiVersion: v1
metadata:
  name: my-pod
spec:
  containers:
    - name: my-frontend
      image: dockerfile/nginx
      volumeMounts:
      - mountPath: "/var/www/html"
        name: my-csi-volume
  volumes:
    - name: my-csi-volume
      persistentVolumeClaim:
        claimName: my-request-for-storage

当一个引用了 CSI Volume 的 pod 被调度时, Kubernetes 将针对外部 CSI 插件进行相应的操作,以确保特定的 Volume 被 attached、mounted, 并且能被 pod 中的容器使用。

关于 CSI 实现的详细信息请参考设计文档

创建 CSI 驱动

Kubernetes 尽可能少地指定 CSI Volume 驱动程序的打包和部署规范。这里记录了在 Kubernetes 上部署 CSI Volume 驱动程序的最低要求。

最低要求文件还包含概述部分,提供了在 Kubernetes 上部署任意容器化 CSI 驱动程序的建议机制。存储提供商可以运用这个机制来简化 Kubernetes 上容器式 CSI 兼容 Volume 驱动程序的部署。

作为推荐部署的一部分,Kubernetes 团队提供以下 sidecar(辅助)容器:

  • External-attacher

    可监听 Kubernetes VolumeAttachment 对象并触发 ControllerPublish 和 ControllerUnPublish 操作的 sidecar 容器,通过 CSI endpoint 触发 ;

  • External-provisioner

    监听 Kubernetes PersistentVolumeClaim 对象的 sidecar 容器,并触发对 CSI 端点的 CreateVolume 和DeleteVolume 操作;

  • Driver-registrar

    使用 Kubelet(将来)注册 CSI 驱动程序的 sidecar 容器,并将 NodeId (通过 GetNodeID 调用检索到 CSI endpoint)添加到 Kubernetes Node API 对象的 annotation 里面。

存储供应商完全可以使用这些组件来为其插件构建 Kubernetes Deployment,同时让它们的 CSI 驱动程序完全意识不到 Kubernetes 的存在。

另外 CSI 驱动完全是由第三方存储供应商自己维护的,在 kubernetes 1.9 版本中 CSI 还处于 alpha 版本。

Kubernetes的设计理念

Kubernetes设计理念与分布式系统

分析和理解Kubernetes的设计理念可以使我们更深入地了解Kubernetes系统,更好地利用它管理分布式部署的云原生应用,另一方面也可以让我们借鉴其在分布式系统设计方面的经验。

分层架构

Kubernetes设计理念和功能其实就是一个类似Linux的分层架构,如下图所示

  • 核心层:Kubernetes最核心的功能,对外提供API构建高层的应用,对内提供插件式应用执行环境
  • 应用层:部署(无状态应用、有状态应用、批处理任务、集群应用等)和路由(服务发现、DNS解析等)
  • 管理层:系统度量(如基础设施、容器和网络的度量),自动化(如自动扩展、动态Provision等)以及策略管理(RBAC、Quota、PSP、NetworkPolicy等)
  • 接口层:kubectl命令行工具、客户端SDK以及集群联邦
  • 生态系统:在接口层之上的庞大容器集群管理调度的生态系统,可以划分为两个范畴
    • Kubernetes外部:日志、监控、配置管理、CI、CD、Workflow、FaaS、OTS应用、ChatOps等
    • Kubernetes内部:CRI、CNI、CVI、镜像仓库、Cloud Provider、集群自身的配置和管理等

API设计原则

对于云计算系统,系统API实际上处于系统设计的统领地位,正如本文前面所说,kubernetes集群系统每支持一项新功能,引入一项新技术,一定会新引入对应的API对象,支持对该功能的管理操作,理解掌握的API,就好比抓住了kubernetes系统的牛鼻子。Kubernetes系统API的设计有以下几条原则:

  1. 所有API应该是声明式的。正如前文所说,声明式的操作,相对于命令式操作,对于重复操作的效果是稳定的,这对于容易出现数据丢失或重复的分布式环境来说是很重要的。另外,声明式操作更容易被用户使用,可以使系统向用户隐藏实现的细节,隐藏实现的细节的同时,也就保留了系统未来持续优化的可能性。此外,声明式的API,同时隐含了所有的API对象都是名词性质的,例如Service、Volume这些API都是名词,这些名词描述了用户所期望得到的一个目标分布式对象。
  2. API对象是彼此互补而且可组合的。这里面实际是鼓励API对象尽量实现面向对象设计时的要求,即“高内聚,松耦合”,对业务相关的概念有一个合适的分解,提高分解出来的对象的可重用性。事实上,Kubernetes这种分布式系统管理平台,也是一种业务系统,只不过它的业务就是调度和管理容器服务。
  3. 高层API以操作意图为基础设计。如何能够设计好API,跟如何能用面向对象的方法设计好应用系统有相通的地方,高层设计一定是从业务出发,而不是过早的从技术实现出发。因此,针对Kubernetes的高层API设计,一定是以Kubernetes的业务为基础出发,也就是以系统调度管理容器的操作意图为基础设计。
  4. 低层API根据高层API的控制需要设计。设计实现低层API的目的,是为了被高层API使用,考虑减少冗余、提高重用性的目的,低层API的设计也要以需求为基础,要尽量抵抗受技术实现影响的诱惑。
  5. 尽量避免简单封装,不要有在外部API无法显式知道的内部隐藏的机制。简单的封装,实际没有提供新的功能,反而增加了对所封装API的依赖性。内部隐藏的机制也是非常不利于系统维护的设计方式,例如PetSet和ReplicaSet,本来就是两种Pod集合,那么Kubernetes就用不同API对象来定义它们,而不会说只用同一个ReplicaSet,内部通过特殊的算法再来区分这个ReplicaSet是有状态的还是无状态。
  6. API操作复杂度与对象数量成正比。这一条主要是从系统性能角度考虑,要保证整个系统随着系统规模的扩大,性能不会迅速变慢到无法使用,那么最低的限定就是API的操作复杂度不能超过O(N),N是对象的数量,否则系统就不具备水平伸缩性了。
  7. API对象状态不能依赖于网络连接状态。由于众所周知,在分布式环境下,网络连接断开是经常发生的事情,因此要保证API对象状态能应对网络的不稳定,API对象的状态就不能依赖于网络连接状态。
  8. 尽量避免让操作机制依赖于全局状态,因为在分布式系统中要保证全局状态的同步是非常困难的

控制机制设计原则

  • 控制逻辑应该只依赖于当前状态。这是为了保证分布式系统的稳定可靠,对于经常出现局部错误的分布式系统,如果控制逻辑只依赖当前状态,那么就非常容易将一个暂时出现故障的系统恢复到正常状态,因为你只要将该系统重置到某个稳定状态,就可以自信的知道系统的所有控制逻辑会开始按照正常方式运行。
  • 假设任何错误的可能,并做容错处理。在一个分布式系统中出现局部和临时错误是大概率事件。错误可能来自于物理系统故障,外部系统故障也可能来自于系统自身的代码错误,依靠自己实现的代码不会出错来保证系统稳定其实也是难以实现的,因此要设计对任何可能错误的容错处理。
  • 尽量避免复杂状态机,控制逻辑不要依赖无法监控的内部状态。因为分布式系统各个子系统都是不能严格通过程序内部保持同步的,所以如果两个子系统的控制逻辑如果互相有影响,那么子系统就一定要能互相访问到影响控制逻辑的状态,否则,就等同于系统里存在不确定的控制逻辑。
  • 假设任何操作都可能被任何操作对象拒绝,甚至被错误解析。由于分布式系统的复杂性以及各子系统的相对独立性,不同子系统经常来自不同的开发团队,所以不能奢望任何操作被另一个子系统以正确的方式处理,要保证出现错误的时候,操作级别的错误不会影响到系统稳定性。
  • 每个模块都可以在出错后自动恢复。由于分布式系统中无法保证系统各个模块是始终连接的,因此每个模块要有自我修复的能力,保证不会因为连接不到其他模块而自我崩溃。
  • 每个模块都可以在必要时优雅地降级服务。所谓优雅地降级服务,是对系统鲁棒性的要求,即要求在设计实现模块时划分清楚基本功能和高级功能,保证基本功能不会依赖高级功能,这样同时就保证了不会因为高级功能出现故障而导致整个模块崩溃。根据这种理念实现的系统,也更容易快速地增加新的高级功能,以为不必担心引入高级功能影响原有的基本功能。

Kubernetes的核心技术概念和API对象

API对象是Kubernetes集群中的管理操作单元。Kubernetes集群系统每支持一项新功能,引入一项新技术,一定会新引入对应的API对象,支持对该功能的管理操作。例如副本集Replica Set对应的API对象是RS。

每个API对象都有3大类属性:元数据metadata、规范spec和状态status。元数据是用来标识API对象的,每个对象都至少有3个元数据:namespace,name和uid;除此以外还有各种各样的标签labels用来标识和匹配不同的对象,例如用户可以用标签env来标识区分不同的服务部署环境,分别用env=dev、env=testing、env=production来标识开发、测试、生产的不同服务。规范描述了用户期望Kubernetes集群中的分布式系统达到的理想状态(Desired State),例如用户可以通过复制控制器Replication Controller设置期望的Pod副本数为3;status描述了系统实际当前达到的状态(Status),例如系统当前实际的Pod副本数为2;那么复制控制器当前的程序逻辑就是自动启动新的Pod,争取达到副本数为3。

Kubernetes中所有的配置都是通过API对象的spec去设置的,也就是用户通过配置系统的理想状态来改变系统,这是Kubernetes重要设计理念之一,即所有的操作都是声明式(Declarative)的而不是命令式(Imperative)的。声明式操作在分布式系统中的好处是稳定,不怕丢操作或运行多次,例如设置副本数为3的操作运行多次也还是一个结果,而给副本数加1的操作就不是声明式的,运行多次结果就错了。

Pod

Kubernetes有很多技术概念,同时对应很多API对象,最重要的也是最基础的是Pod。Pod是在Kubernetes集群中运行部署应用或服务的最小单元,它是可以支持多容器的。Pod的设计理念是支持多个容器在一个Pod中共享网络地址和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。Pod对多容器的支持是K8最基础的设计理念。比如你运行一个操作系统发行版的软件仓库,一个Nginx容器用来发布软件,另一个容器专门用来从源仓库做同步,这两个容器的镜像不太可能是一个团队开发的,但是他们一块儿工作才能提供一个微服务;这种情况下,不同的团队各自开发构建自己的容器镜像,在部署的时候组合成一个微服务对外提供服务。

Pod是Kubernetes集群中所有业务类型的基础,可以看作运行在K8集群中的小机器人,不同类型的业务就需要不同类型的小机器人去执行。目前Kubernetes中的业务主要可以分为长期伺服型(long-running)、批处理型(batch)、节点后台支撑型(node-daemon)和有状态应用型(stateful application);分别对应的小机器人控制器为Deployment、Job、DaemonSet和PetSet,本文后面会一一介绍。

副本控制器(Replication Controller,RC)

RC是Kubernetes集群中最早的保证Pod高可用的API对象。通过监控运行中的Pod来保证集群中运行指定数目的Pod副本。指定的数目可以是多个也可以是1个;少于指定数目,RC就会启动运行新的Pod副本;多于指定数目,RC就会杀死多余的Pod副本。即使在指定数目为1的情况下,通过RC运行Pod也比直接运行Pod更明智,因为RC也可以发挥它高可用的能力,保证永远有1个Pod在运行。RC是Kubernetes较早期的技术概念,只适用于长期伺服型的业务类型,比如控制小机器人提供高可用的Web服务。

副本集(Replica Set,RS)

RS是新一代RC,提供同样的高可用能力,区别主要在于RS后来居上,能支持更多种类的匹配模式。副本集对象一般不单独使用,而是作为Deployment的理想状态参数使用。

部署(Deployment)

部署表示用户对Kubernetes集群的一次更新操作。部署是一个比RS应用模式更广的API对象,可以是创建一个新的服务,更新一个新的服务,也可以是滚动升级一个服务。滚动升级一个服务,实际是创建一个新的RS,然后逐渐将新RS中副本数增加到理想状态,将旧RS中的副本数减小到0的复合操作;这样一个复合操作用一个RS是不太好描述的,所以用一个更通用的Deployment来描述。以Kubernetes的发展方向,未来对所有长期伺服型的的业务的管理,都会通过Deployment来管理。

服务(Service)

RC、RS和Deployment只是保证了支撑服务的微服务Pod的数量,但是没有解决如何访问这些服务的问题。一个Pod只是一个运行服务的实例,随时可能在一个节点上停止,在另一个节点以一个新的IP启动一个新的Pod,因此不能以确定的IP和端口号提供服务。要稳定地提供服务需要服务发现和负载均衡能力。服务发现完成的工作,是针对客户端访问的服务,找到对应的的后端服务实例。在K8集群中,客户端需要访问的服务就是Service对象。每个Service会对应一个集群内部有效的虚拟IP,集群内部通过虚拟IP访问一个服务。在Kubernetes集群中微服务的负载均衡是由Kube-proxy实现的。Kube-proxy是Kubernetes集群内部的负载均衡器。它是一个分布式代理服务器,在Kubernetes的每个节点上都有一个;这一设计体现了它的伸缩性优势,需要访问服务的节点越多,提供负载均衡能力的Kube-proxy就越多,高可用节点也随之增多。与之相比,我们平时在服务器端做个反向代理做负载均衡,还要进一步解决反向代理的负载均衡和高可用问题。

任务(Job)

Job是Kubernetes用来控制批处理型任务的API对象。批处理业务与长期伺服业务的主要区别是批处理业务的运行有头有尾,而长期伺服业务在用户不停止的情况下永远运行。Job管理的Pod根据用户的设置把任务成功完成就自动退出了。成功完成的标志根据不同的spec.completions策略而不同:单Pod型任务有一个Pod成功就标志完成;定数成功型任务保证有N个任务全部成功;工作队列型任务根据应用确认的全局成功而标志成功。

后台支撑服务集(DaemonSet)

长期伺服型和批处理型服务的核心在业务应用,可能有些节点运行多个同类业务的Pod,有些节点上又没有这类Pod运行;而后台支撑型服务的核心关注点在Kubernetes集群中的节点(物理机或虚拟机),要保证每个节点上都有一个此类Pod运行。节点可能是所有集群节点也可能是通过nodeSelector选定的一些特定节点。典型的后台支撑型服务包括,存储,日志和监控等在每个节点上支持Kubernetes集群运行的服务。

有状态服务集(PetSet)

Kubernetes在1.3版本里发布了Alpha版的PetSet功能。在云原生应用的体系里,有下面两组近义词;第一组是无状态(stateless)、牲畜(cattle)、无名(nameless)、可丢弃(disposable);第二组是有状态(stateful)、宠物(pet)、有名(having name)、不可丢弃(non-disposable)。RC和RS主要是控制提供无状态服务的,其所控制的Pod的名字是随机设置的,一个Pod出故障了就被丢弃掉,在另一个地方重启一个新的Pod,名字变了、名字和启动在哪儿都不重要,重要的只是Pod总数;而PetSet是用来控制有状态服务,PetSet中的每个Pod的名字都是事先确定的,不能更改。PetSet中Pod的名字的作用,并不是《千与千寻》的人性原因,而是关联与该Pod对应的状态。

对于RC和RS中的Pod,一般不挂载存储或者挂载共享存储,保存的是所有Pod共享的状态,Pod像牲畜一样没有分别(这似乎也确实意味着失去了人性特征);对于PetSet中的Pod,每个Pod挂载自己独立的存储,如果一个Pod出现故障,从其他节点启动一个同样名字的Pod,要挂在上原来Pod的存储继续以它的状态提供服务。

适合于PetSet的业务包括数据库服务MySQL和PostgreSQL,集群化管理服务Zookeeper、etcd等有状态服务。PetSet的另一种典型应用场景是作为一种比普通容器更稳定可靠的模拟虚拟机的机制。传统的虚拟机正是一种有状态的宠物,运维人员需要不断地维护它,容器刚开始流行时,我们用容器来模拟虚拟机使用,所有状态都保存在容器里,而这已被证明是非常不安全、不可靠的。使用PetSet,Pod仍然可以通过漂移到不同节点提供高可用,而存储也可以通过外挂的存储来提供高可靠性,PetSet做的只是将确定的Pod与确定的存储关联起来保证状态的连续性。PetSet还只在Alpha阶段,后面的设计如何演变,我们还要继续观察。

集群联邦(Federation)

Kubernetes在1.3版本里发布了beta版的Federation功能。在云计算环境中,服务的作用距离范围从近到远一般可以有:同主机(Host,Node)、跨主机同可用区(Available Zone)、跨可用区同地区(Region)、跨地区同服务商(Cloud Service Provider)、跨云平台。Kubernetes的设计定位是单一集群在同一个地域内,因为同一个地区的网络性能才能满足Kubernetes的调度和计算存储连接要求。而联合集群服务就是为提供跨Region跨服务商Kubernetes集群服务而设计的。

每个Kubernetes Federation有自己的分布式存储、API Server和Controller Manager。用户可以通过Federation的API Server注册该Federation的成员Kubernetes Cluster。当用户通过Federation的API Server创建、更改API对象时,Federation API Server会在自己所有注册的子Kubernetes Cluster都创建一份对应的API对象。在提供业务请求服务时,Kubernetes Federation会先在自己的各个子Cluster之间做负载均衡,而对于发送到某个具体Kubernetes Cluster的业务请求,会依照这个Kubernetes Cluster独立提供服务时一样的调度模式去做Kubernetes Cluster内部的负载均衡。而Cluster之间的负载均衡是通过域名服务的负载均衡来实现的。

所有的设计都尽量不影响Kubernetes Cluster现有的工作机制,这样对于每个子Kubernetes集群来说,并不需要更外层的有一个Kubernetes Federation,也就是意味着所有现有的Kubernetes代码和机制不需要因为Federation功能有任何变化。

存储卷(Volume)

Kubernetes集群中的存储卷跟Docker的存储卷有些类似,只不过Docker的存储卷作用范围为一个容器,而Kubernetes的存储卷的生命周期和作用范围是一个Pod。每个Pod中声明的存储卷由Pod中的所有容器共享。Kubernetes支持非常多的存储卷类型,特别的,支持多种公有云平台的存储,包括AWS,Google和Azure云;支持多种分布式存储包括GlusterFS和Ceph;也支持较容易使用的主机本地目录hostPath和NFS。Kubernetes还支持使用Persistent Volume Claim即PVC这种逻辑存储,使用这种存储,使得存储的使用者可以忽略后台的实际存储技术(例如AWS,Google或GlusterFS和Ceph),而将有关存储实际技术的配置交给存储管理员通过Persistent Volume来配置。

持久存储卷(Persistent Volume,PV)和持久存储卷声明(Persistent Volume Claim,PVC)

PV和PVC使得Kubernetes集群具备了存储的逻辑抽象能力,使得在配置Pod的逻辑里可以忽略对实际后台存储技术的配置,而把这项配置的工作交给PV的配置者,即集群的管理者。存储的PV和PVC的这种关系,跟计算的Node和Pod的关系是非常类似的;PV和Node是资源的提供者,根据集群的基础设施变化而变化,由Kubernetes集群管理员配置;而PVC和Pod是资源的使用者,根据业务服务的需求变化而变化,有Kubernetes集群的使用者即服务的管理员来配置。

节点(Node)

Kubernetes集群中的计算能力由Node提供,最初Node称为服务节点Minion,后来改名为Node。Kubernetes集群中的Node也就等同于Mesos集群中的Slave节点,是所有Pod运行所在的工作主机,可以是物理机也可以是虚拟机。不论是物理机还是虚拟机,工作主机的统一特征是上面要运行kubelet管理节点上运行的容器。

密钥对象(Secret)

Secret是用来保存和传递密码、密钥、认证凭证这些敏感信息的对象。使用Secret的好处是可以避免把敏感信息明文写在配置文件里。在Kubernetes集群中配置和使用服务不可避免的要用到各种敏感信息实现登录、认证等功能,例如访问AWS存储的用户名密码。为了避免将类似的敏感信息明文写在所有需要使用的配置文件中,可以将这些信息存入一个Secret对象,而在配置文件中通过Secret对象引用这些敏感信息。这种方式的好处包括:意图明确,避免重复,减少暴漏机会。

用户帐户(User Account)和服务帐户(Service Account)

顾名思义,用户帐户为人提供账户标识,而服务账户为计算机进程和Kubernetes集群中运行的Pod提供账户标识。用户帐户和服务帐户的一个区别是作用范围;用户帐户对应的是人的身份,人的身份与服务的namespace无关,所以用户账户是跨namespace的;而服务帐户对应的是一个运行中程序的身份,与特定namespace是相关的。

命名空间(Namespace)

命名空间为Kubernetes集群提供虚拟的隔离作用,Kubernetes集群初始有两个命名空间,分别是默认命名空间default和系统命名空间kube-system,除此以外,管理员可以可以创建新的命名空间满足需要。

RBAC访问授权

Kubernetes在1.3版本中发布了alpha版的基于角色的访问控制(Role-based Access Control,RBAC)的授权模式。相对于基于属性的访问控制(Attribute-based Access Control,ABAC),RBAC主要是引入了角色(Role)和角色绑定(RoleBinding)的抽象概念。在ABAC中,Kubernetes集群中的访问策略只能跟用户直接关联;而在RBAC中,访问策略可以跟某个角色关联,具体的用户在跟一个或多个角色相关联。显然,RBAC像其他新功能一样,每次引入新功能,都会引入新的API对象,从而引入新的概念抽象,而这一新的概念抽象一定会使集群服务管理和使用更容易扩展和重用。

总结

从Kubernetes的系统架构、技术概念和设计理念,我们可以看到Kubernetes系统最核心的两个设计理念:一个是容错性,一个是易扩展性。容错性实际是保证Kubernetes系统稳定性和安全性的基础,易扩展性是保证Kubernetes对变更友好,可以快速迭代增加新功能的基础。

按照分布式系统一致性算法Paxos发明人计算机科学家Leslie Lamport的理念,一个分布式系统有两类特性:安全性Safety和活性Liveness。安全性保证系统的稳定,保证系统不会崩溃,不会出现业务错误,不会做坏事,是严格约束的;活性使得系统可以提供功能,提高性能,增加易用性,让系统可以在用户“看到的时间内”做些好事,是尽力而为的。Kubernetes系统的设计理念正好与Lamport安全性与活性的理念不谋而合,也正是因为Kubernetes在引入功能和技术的时候,非常好地划分了安全性和活性,才可以让Kubernetes能有这么快版本迭代,快速引入像RBAC、Federation和PetSet这种新功能。

🚕资源对象与基本概念解析

以下列举的内容都是 kubernetes 中的 Object,这些对象都可以在 yaml 文件中作为一种 API 类型来配置。

  • Pod
  • Node
  • Namespace
  • Service
  • Volume
  • PersistentVolume
  • Deployment
  • Secret
  • StatefulSet
  • DaemonSet
  • ServiceAccount
  • ReplicationController
  • ReplicaSet
  • Job
  • CronJob
  • SecurityContext
  • ResourceQuota
  • LimitRange
  • HorizontalPodAutoscaling
  • Ingress
  • ConfigMap
  • Label
  • ThirdPartyResources

我将它们简单的分类为以下几种资源对象:

类别 名称
资源对象 Pod、ReplicaSet、ReplicationController、Deployment、StatefulSet、DaemonSet、Job、CronJob、HorizontalPodAutoscaling
配置对象 Node、Namespace、Service、Secret、ConfigMap、Ingress、Label、ThirdPartyResource、 ServiceAccount
存储对象 Volume、Persistent Volume
策略对象 SecurityContext、ResourceQuota、LimitRange

理解 kubernetes 中的对象

在 Kubernetes 系统中,Kubernetes 对象 是持久化的条目。Kubernetes 使用这些条目去表示整个集群的状态。特别地,它们描述了如下信息:

  • 什么容器化应用在运行(以及在哪个 Node 上)
  • 可以被应用使用的资源
  • 关于应用如何表现的策略,比如重启策略、升级策略,以及容错策略

Kubernetes 对象是 “目标性记录” —— 一旦创建对象,Kubernetes 系统将持续工作以确保对象存在。通过创建对象,可以有效地告知 Kubernetes 系统,所需要的集群工作负载看起来是什么样子的,这就是 Kubernetes 集群的 期望状态

与 Kubernetes 对象工作 —— 是否创建、修改,或者删除 —— 需要使用 Kubernetes API。当使用 kubectl 命令行接口时,比如,CLI 会使用必要的 Kubernetes API 调用,也可以在程序中直接使用 Kubernetes API。为了实现该目标,Kubernetes 当前提供了一个 golang 客户端库 ,其它语言库(例如Python)也正在开发中。

对象 Spec 与状态

每个 Kubernetes 对象包含两个嵌套的对象字段,它们负责管理对象的配置:对象 spec 和 对象 statusspec 必须提供,它描述了对象的 期望状态—— 希望对象所具有的特征。status 描述了对象的 实际状态,它是由 Kubernetes 系统提供和更新。在任何时刻,Kubernetes 控制平面一直处于活跃状态,管理着对象的实际状态以与我们所期望的状态相匹配。

例如,Kubernetes Deployment 对象能够表示运行在集群中的应用。当创建 Deployment 时,可能需要设置 Deployment 的 spec,以指定该应用需要有 3 个副本在运行。Kubernetes 系统读取 Deployment spec,启动我们所期望的该应用的 3 个实例 —— 更新状态以与 spec 相匹配。如果那些实例中有失败的(一种状态变更),Kubernetes 系统通过修正来响应 spec 和状态之间的不一致 —— 这种情况,启动一个新的实例来替换。

关于对象 spec、status 和 metadata 更多信息,查看 Kubernetes API Conventions

描述 Kubernetes 对象

当创建 Kubernetes 对象时,必须提供对象的 spec,用来描述该对象的期望状态,以及关于对象的一些基本信息(例如,名称)。当使用 Kubernetes API 创建对象时(或者直接创建,或者基于kubectl),API 请求必须在请求体中包含 JSON 格式的信息。更常用的是,需要在 .yaml 文件中为 kubectl 提供这些信息kubectl 在执行 API 请求时,将这些信息转换成 JSON 格式。

这里有一个 .yaml 示例文件,展示了 Kubernetes Deployment 的必需字段和对象 spec:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

一种创建 Deployment 的方式,类似上面使用 .yaml 文件,是使用 kubectl 命令行接口(CLI)中的 kubectl create 命令,传递 .yaml 作为参数。下面是一个示例:

$ kubectl create -f docs/user-guide/nginx-deployment.yaml --record

输出类似如下这样:

deployment "nginx-deployment" created

必需字段

在想要创建的 Kubernetes 对象对应的 .yaml 文件中,需要配置如下的字段:

  • apiVersion - 创建该对象所使用的 Kubernetes API 的版本
  • kind - 想要创建的对象的类型
  • metadata - 帮助识别对象唯一性的数据,包括一个 name 字符串、UID 和可选的 namespace

也需要提供对象的 spec 字段。对象 spec 的精确格式对每个 Kubernetes 对象来说是不同的,包含了特定于该对象的嵌套字段。Kubernetes API 参考能够帮助我们找到任何我们想创建的对象的 spec 格式。

Pod概览

理解Pod

Pod是kubernetes中你可以创建和部署的最小也是最简单位。一个Pod代表着集群中运行的一个进程。

Pod中封装着应用的容器(有的情况下是好几个容器),存储、独立的网络IP,管理容器如何运行的策略选项。Pod代表着部署的一个单位:kubernetes中应用的一个实例,可能由一个或者多个容器组合在一起共享资源。

Docker是kubernetes中最常用的容器运行时,但是Pod也支持其他容器运行时。

Pods are employed a number of ways in a Kubernetes cluster, including:

在Kubrenetes集群中Pod有如下两种使用方式:

  • 一个Pod中运行一个容器。“每个Pod中一个容器”的模式是最常见的用法;在这种使用方式中,你可以把Pod想象成是单个容器的封装,kuberentes管理的是Pod而不是直接管理容器。
  • 在一个Pod中同时运行多个容器。一个Pod中也可以同时封装几个需要紧密耦合互相协作的容器,它们之间共享资源。这些在同一个Pod中的容器可以互相协作成为一个service单位——一个容器共享文件,另一个“sidecar”容器来更新这些文件。Pod将这些容器的存储资源作为一个实体来管理。

Kubernetes Blog 有关于Pod用例的详细信息,查看:

每个Pod都是应用的一个实例。如果你想平行扩展应用的话(运行多个实例),你应该运行多个Pod,每个Pod都是一个应用实例。在Kubernetes中,这通常被称为replication。

Pod中如何管理多个容器

Pod中可以同时运行多个进程(作为容器运行)协同工作。同一个Pod中的容器会自动的分配到同一个 node 上。同一个Pod中的容器共享资源、网络环境和依赖,它们总是被同时调度。

注意在一个Pod中同时运行多个容器是一种比较高级的用法。只有当你的容器需要紧密配合协作的时候才考虑用这种模式。例如,你有一个容器作为web服务器运行,需要用到共享的volume,有另一个“sidecar”容器来从远端获取资源更新这些文件,如下图所示:

Pod中可以共享两种资源:网络和存储。

网络

每个Pod都会被分配一个唯一的IP地址。Pod中的所有容器共享网络空间,包括IP地址和端口。Pod内部的容器可以使用localhost互相通信。Pod中的容器与外界通信时,必须分配共享网络资源(例如使用宿主机的端口映射)。

存储

可以Pod指定多个共享的Volume。Pod中的所有容器都可以访问共享的volume。Volume也可以用来持久化Pod中的存储资源,以防容器重启后文件丢失。

使用Pod

你很少会直接在kubernetes中创建单个Pod。因为Pod的生命周期是短暂的,用后即焚的实体。当Pod被创建后(不论是由你直接创建还是被其他Controller),都会被Kuberentes调度到集群的Node上。直到Pod的进程终止、被删掉、因为缺少资源而被驱逐、或者Node故障之前这个Pod都会一直保持在那个Node上。

注意:重启Pod中的容器跟重启Pod不是一回事。Pod只提供容器的运行环境并保持容器的运行状态,重启容器不会造成Pod重启。

Pod不会自愈。如果Pod运行的Node故障,或者是调度器本身故障,这个Pod就会被删除。同样的,如果Pod所在Node缺少资源或者Pod处于维护状态,Pod也会被驱逐。Kubernetes使用更高级的称为Controller的抽象层,来管理Pod实例。虽然可以直接使用Pod,但是在Kubernetes中通常是使用Controller来管理Pod的。

Pod和Controller

Controller可以创建和管理多个Pod,提供副本管理、滚动升级和集群级别的自愈能力。例如,如果一个Node故障,Controller就能自动将该节点上的Pod调度到其他健康的Node上。

包含一个或者多个Pod的Controller示例:

  • Deployment
  • StatefulSet
  • DaemonSet

通常,Controller会用你提供的Pod Template来创建相应的Pod。

Pod Templates

Pod模版是包含了其他object的Pod定义,例如Replication Controllers,Jobs和DaemonSets。Controller根据Pod模板来创建实际的Pod。

Pod解析

Pod是kubernetes中可以创建的最小部署单元。

什么是Pod?

Pod就像是豌豆荚一样,它由一个或者多个容器组成(例如Docker容器),它们共享容器存储、网络和容器运行配置项。Pod中的容器总是被同时调度,有共同的运行环境。你可以把单个Pod想象成是运行独立应用的“逻辑主机”——其中运行着一个或者多个紧密耦合的应用容器——在有容器之前,这些应用都是运行在几个相同的物理机或者虚拟机上。

尽管kubernetes支持多种容器运行时,但是Docker依然是最常用的运行时环境,我们可以使用Docker的术语和规则来定义Pod。

Pod中共享的环境包括Linux的namespace,cgroup和其他可能的隔绝环境,这一点跟Docker容器一致。在Pod的环境中,每个容器中可能还有更小的子隔离环境。

Pod中的容器共享IP地址和端口号,它们之间可以通过localhost互相发现。它们之间可以通过进程间通信,例如SystemV信号或者POSIX共享内存。不同Pod之间的容器具有不同的IP地址,不能直接通过IPC通信。

Pod中的容器也有访问共享volume的权限,这些volume会被定义成pod的一部分并挂载到应用容器的文件系统中。

根据Docker的结构,Pod中的容器共享namespace和volume,不支持共享PID的namespace。

就像每个应用容器,pod被认为是临时(非持久的)实体。在Pod的生命周期中讨论过,pod被创建后,被分配一个唯一的ID(UID),调度到节点上,并一致维持期望的状态直到被终结(根据重启策略)或者被删除。如果node死掉了,分配到了这个node上的pod,在经过一个超时时间后会被重新调度到其他node节点上。一个给定的pod(如UID定义的)不会被“重新调度”到新的节点上,而是被一个同样的pod取代,如果期望的话甚至可以是相同的名字,但是会有一个新的UID(查看replication controller获取详情)。(未来,一个更高级别的API将支持pod迁移)。

Volume跟pod有相同的生命周期(当其UID存在的时候)。当Pod因为某种原因被删除或者被新创建的相同的Pod取代,它相关的东西(例如volume)也会被销毁和再创建一个新的volume。

A multi-container pod that contains a file puller and a web server that uses a persistent volume for shared storage between the containers.

Pod的动机

管理

Pod是一个服务的多个进程的聚合单位,pod提供这种模型能够简化应用部署管理,通过提供一个更高级别的抽象的方式。Pod作为一个独立的部署单位,支持横向扩展和复制。共生(协同调度),命运共同体(例如被终结),协同复制,资源共享,依赖管理,Pod都会自动的为容器处理这些问题。

资源共享和通信

Pod中的应用可以共享网络空间(IP地址和端口),因此可以通过localhost互相发现。因此,pod中的应用必须协调端口占用。每个pod都有一个唯一的IP地址,跟物理机和其他pod都处于一个扁平的网络空间中,它们之间可以直接连通。

Pod中应用容器的hostname被设置成Pod的名字。

Pod中的应用容器可以共享volume。Volume能够保证pod重启时使用的数据不丢失。

Pod的使用

Pod也可以用于垂直应用栈(例如LAMP),这样使用的主要动机是为了支持共同调度和协调管理应用程序,例如:

  • content management systems, file and data loaders, local cache managers, etc.
  • log and checkpoint backup, compression, rotation, snapshotting, etc.
  • data change watchers, log tailers, logging and monitoring adapters, event publishers, etc.
  • proxies, bridges, and adapters
  • controllers, managers, configurators, and updaters

通常单个pod中不会同时运行一个应用的多个实例。

详细说明请看: The Distributed System ToolKit: Patterns for Composite Containers.

其他替代选择

为什么不直接在一个容器中运行多个应用程序呢?

  1. 透明。让Pod中的容器对基础设施可见,以便基础设施能够为这些容器提供服务,例如进程管理和资源监控。这可以为用户带来极大的便利。
  2. 解耦软件依赖。每个容器都可以进行版本管理,独立的编译和发布。未来kubernetes甚至可能支持单个容器的在线升级。
  3. 使用方便。用户不必运行自己的进程管理器,还要担心错误信号传播等。
  4. 效率。因为由基础架构提供更多的职责,所以容器可以变得更加轻量级。

为什么不支持容器的亲和性的协同调度?

这种方法可以提供容器的协同定位,能够根据容器的亲和性进行调度,但是无法实现使用pod带来的大部分好处,例如资源共享,IPC,保持状态一致性和简化管理等。

Pod的持久性(或者说缺乏持久性)

Pod在设计支持就不是作为持久化实体的。在调度失败、节点故障、缺少资源或者节点维护的状态下都会死掉会被驱逐。

通常,用户不需要手动直接创建Pod,而是应该使用controller(例如Deployments),即使是在创建单个Pod的情况下。Controller可以提供集群级别的自愈功能、复制和升级管理。

The use of collective APIs as the primary user-facing primitive is relatively common among cluster scheduling systems, including Borg, Marathon, Aurora, and Tupperware.

Pod is exposed as a primitive in order to facilitate:

  • scheduler and controller pluggability
  • support for pod-level operations without the need to “proxy” them via controller APIs
  • decoupling of pod lifetime from controller lifetime, such as for bootstrapping
  • decoupling of controllers and services — the endpoint controller just watches pods
  • clean composition of Kubelet-level functionality with cluster-level functionality — Kubelet is effectively the “pod controller”
  • high-availability applications, which will expect pods to be replaced in advance of their termination and certainly in advance of deletion, such as in the case of planned evictions, image prefetching, or live pod migration #3949

StatefulSet controller(目前还是beta状态)支持有状态的Pod。在1.4版本中被称为PetSet。在kubernetes之前的版本中创建有状态pod的最佳方式是创建一个replica为1的replication controller。

Pod的终止

因为Pod作为在集群的节点上运行的进程,所以在不再需要的时候能够优雅的终止掉是十分必要的(比起使用发送KILL信号这种暴力的方式)。用户需要能够放松删除请求,并且知道它们何时会被终止,是否被正确的删除。用户想终止程序时发送删除pod的请求,在pod可以被强制删除前会有一个优雅删除的时间,会发送一个TERM请求到每个容器的主进程。一旦超时,将向主进程发送KILL信号并从API server中删除。如果kubelet或者container manager在等待进程终止的过程中重启,在重启后仍然会重试完整的优雅删除阶段。

示例流程如下:

  1. 用户发送删除pod的命令,默认优雅删除时期是30秒;
  2. 在Pod超过该优雅删除期限后API server就会更新Pod的状态为“dead”;
  3. 在客户端命令行上显示的Pod状态为“terminating”;
  4. 跟第三步同时,当kubelet发现pod被标记为“terminating”状态时,开始停止pod进程:
    1. 如果在pod中定义了preStop hook,在停止pod前会被调用。如果在优雅删除期限过期后,preStop hook依然在运行,第二步会再增加2秒的优雅时间;
    2. 向Pod中的进程发送TERM信号;
  5. 跟第三步同时,该Pod将从该service的端点列表中删除,不再是replication controller的一部分。关闭的慢的pod将继续处理load balancer转发的流量;
  6. 过了优雅周期后,将向Pod中依然运行的进程发送SIGKILL信号而杀掉进程。
  7. Kublete会在API server中完成Pod的的删除,通过将优雅周期设置为0(立即删除)。Pod在API中消失,并且在客户端也不可见。

删除优雅周期默认是30秒。 kubectl delete命令支持 —grace-period=<seconds> 选项,允许用户设置自己的优雅周期时间。如果设置为0将强制删除pod。在kubectl>=1.5版本的命令中,你必须同时使用 --force--grace-period=0 来强制删除pod。

强制删除Pod

Pod的强制删除是通过在集群和etcd中将其定义为删除状态。当执行强制删除命令时,API server不会等待该pod所运行在节点上的kubelet确认,就会立即将该pod从API server中移除,这时就可以创建跟原pod同名的pod了。这时,在节点上的pod会被立即设置为terminating状态,不过在被强制删除之前依然有一小段优雅删除周期。

强制删除对于某些pod具有潜在危险性,请谨慎使用。使用StatefulSet pod的情况下,请参考删除StatefulSet中的pod文章。

Pod中容器的特权模式

从kubernetes1.1版本开始,pod中的容器就可以开启previleged模式,在容器定义文件的 SecurityContext 下使用 privileged flag。 这在使用Linux的网络操作和访问设备的能力时是很用的。同时开启的特权模式的Pod中的容器也可以访问到容器外的进程和应用。在不需要修改和重新编译kubelet的情况下就可以使用pod来开发节点的网络和存储插件。

如果master节点运行的是kuberentes1.1或更高版本,而node节点的版本低于1.1版本,则API server将也可以接受新的特权模式的pod,但是无法启动,pod将处于pending状态。

执行 kubectl describe pod FooPodName,可以看到为什么pod处于pending状态。输出的event列表中将显示:
Error validating pod "FooPodName"."FooPodNamespace" from api, ignoring: spec.containers[0].securityContext.privileged: forbidden '<*>(0xc2089d3248)true'

如果master节点的版本低于1.1,无法创建特权模式的pod。如果你仍然试图去创建的话,你得到如下错误:

The Pod "FooPodName" is invalid. spec.containers[0].securityContext.privileged: forbidden '<*>(0xc20b222db0)true'

API Object

Pod是kubernetes REST API中的顶级资源类型。

在kuberentes1.6的V1 core API版本中的Pod的数据结构如下图所示:

Init 容器

该特性在 1.6 版本已经推出 beta 版本。Init 容器可以在 PodSpec 中同应用程序的 containers 数组一起来指定。 beta 注解的值将仍需保留,并覆盖 PodSpec 字段值。

本文讲解 Init 容器的基本概念,它是一种专用的容器,在应用程序容器启动之前运行,并包括一些应用镜像中不存在的实用工具和安装脚本。

理解 Init 容器

Pod 能够具有多个容器,应用运行在容器里面,但是它也可能有一个或多个先于应用容器启动的 Init 容器。

Init 容器与普通的容器非常像,除了如下两点:

  • Init 容器总是运行到成功完成为止。
  • 每个 Init 容器都必须在下一个 Init 容器启动之前成功完成。

如果 Pod 的 Init 容器失败,Kubernetes 会不断地重启该 Pod,直到 Init 容器成功为止。然而,如果 Pod 对应的 restartPolicy 为 Never,它不会重新启动。

指定容器为 Init 容器,在 PodSpec 中添加 initContainers 字段,以 v1.Container 类型对象的 JSON 数组的形式,还有 app 的 containers 数组。 Init 容器的状态在 status.initContainerStatuses 字段中以容器状态数组的格式返回(类似 status.containerStatuses 字段)。

与普通容器的不同之处

Init 容器支持应用容器的全部字段和特性,包括资源限制、数据卷和安全设置。 然而,Init 容器对资源请求和限制的处理稍有不同,在下面 资源 处有说明。 而且 Init 容器不支持 Readiness Probe,因为它们必须在 Pod 就绪之前运行完成。

如果为一个 Pod 指定了多个 Init 容器,那些容器会按顺序一次运行一个。 每个 Init 容器必须运行成功,下一个才能够运行。 当所有的 Init 容器运行完成时,Kubernetes 初始化 Pod 并像平常一样运行应用容器。

Init 容器能做什么?

因为 Init 容器具有与应用程序容器分离的单独镜像,所以它们的启动相关代码具有如下优势:

  • 它们可以包含并运行实用工具,但是出于安全考虑,是不建议在应用程序容器镜像中包含这些实用工具的。
  • 它们可以包含使用工具和定制化代码来安装,但是不能出现在应用程序镜像中。例如,创建镜像没必要 FROM 另一个镜像,只需要在安装过程中使用类似 sedawkpythondig 这样的工具。
  • 应用程序镜像可以分离出创建和部署的角色,而没有必要联合它们构建一个单独的镜像。
  • Init 容器使用 Linux Namespace,所以相对应用程序容器来说具有不同的文件系统视图。因此,它们能够具有访问 Secret 的权限,而应用程序容器则不能。
  • 它们必须在应用程序容器启动之前运行完成,而应用程序容器是并行运行的,所以 Init 容器能够提供了一种简单的阻塞或延迟应用容器的启动的方法,直到满足了一组先决条件。

示例

下面是一些如何使用 Init 容器的想法:

  • 等待一个 Service 创建完成,通过类似如下 shell 命令:

      for i in {1..100}; do sleep 1; if dig myservice; then exit 0; fi; exit 1
  • 将 Pod 注册到远程服务器,通过在命令中调用 API,类似如下:

      curl -X POST http://$MANAGEMENT_SERVICE_HOST:$MANAGEMENT_SERVICE_PORT/register -d 'instance=$(<POD_NAME>)&ip=$(<POD_IP>)'
  • 在启动应用容器之前等一段时间,使用类似 sleep 60 的命令。

  • 克隆 Git 仓库到数据卷。

  • 将配置值放到配置文件中,运行模板工具为主应用容器动态地生成配置文件。例如,在配置文件中存放 POD_IP 值,并使用 Jinja 生成主应用配置文件。

更多详细用法示例,可以在 StatefulSet 文档生产环境 Pod 指南 中找到。

使用 Init 容器

下面是 Kubernetes 1.5 版本 yaml 文件,展示了一个具有 2 个 Init 容器的简单 Pod。 第一个等待 myservice 启动,第二个等待 mydb 启动。 一旦这两个 Service 都启动完成,Pod 将开始启动。

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
  annotations:
    pod.beta.kubernetes.io/init-containers: '[
        {
            "name": "init-myservice",
            "image": "busybox",
            "command": ["sh", "-c", "until nslookup myservice; do echo waiting for myservice; sleep 2; done;"]
        },
        {
            "name": "init-mydb",
            "image": "busybox",
            "command": ["sh", "-c", "until nslookup mydb; do echo waiting for mydb; sleep 2; done;"]
        }
    ]'
spec:
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']

这是 Kubernetes 1.6 版本的新语法,尽管老的 annotation 语法仍然可以使用。我们已经把 Init 容器的声明移到 spec 中:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox
    command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
  - name: init-mydb
    image: busybox
    command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']

1.5 版本的语法在 1.6 版本仍然可以使用,但是我们推荐使用 1.6 版本的新语法。 在 Kubernetes 1.6 版本中,Init 容器在 API 中新建了一个字段。 虽然期望使用 beta 版本的 annotation,但在未来发行版将会被废弃掉。

下面的 yaml 文件展示了 mydbmyservice 两个 Service:

kind: Service
apiVersion: v1
metadata:
  name: myservice
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
---
kind: Service
apiVersion: v1
metadata:
  name: mydb
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9377

这个 Pod 可以使用下面的命令进行启动和调试:

$ kubectl create -f myapp.yaml
pod "myapp-pod" created
$ kubectl get -f myapp.yaml
NAME        READY     STATUS     RESTARTS   AGE
myapp-pod   0/1       Init:0/2   0          6m
$ kubectl describe -f myapp.yaml 
Name:          myapp-pod
Namespace:     default
[...]
Labels:        app=myapp
Status:        Pending
[...]
Init Containers:
  init-myservice:
[...]
    State:         Running
[...]
  init-mydb:
[...]
    State:         Waiting
      Reason:      PodInitializing
    Ready:         False
[...]
Containers:
  myapp-container:
[...]
    State:         Waiting
      Reason:      PodInitializing
    Ready:         False
[...]
Events:
  FirstSeen    LastSeen    Count    From                      SubObjectPath                           Type          Reason        Message
  ---------    --------    -----    ----                      -------------                           --------      ------        -------
  16s          16s         1        {default-scheduler }                                              Normal        Scheduled     Successfully assigned myapp-pod to 172.17.4.201
  16s          16s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Pulling       pulling image "busybox"
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Pulled        Successfully pulled image "busybox"
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Created       Created container with docker id 5ced34a04634; Security:[seccomp=unconfined]
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Started       Started container with docker id 5ced34a04634
$ kubectl logs myapp-pod -c init-myservice # Inspect the first init container
$ kubectl logs myapp-pod -c init-mydb      # Inspect the second init container

一旦我们启动了 mydbmyservice 这两个 Service,我们能够看到 Init 容器完成,并且 myapp-pod 被创建:

$ kubectl create -f services.yaml
service "myservice" created
service "mydb" created
$ kubectl get -f myapp.yaml
NAME        READY     STATUS    RESTARTS   AGE
myapp-pod   1/1       Running   0          9m

这个例子非常简单,但是应该能够为我们创建自己的 Init 容器提供一些启发。

具体行为

在 Pod 启动过程中,Init 容器会按顺序在网络和数据卷初始化之后启动。 每个容器必须在下一个容器启动之前成功退出。 如果由于运行时或失败退出,导致容器启动失败,它会根据 Pod 的 restartPolicy 指定的策略进行重试。 然而,如果 Pod 的 restartPolicy 设置为 Always,Init 容器失败时会使用 RestartPolicy 策略。

在所有的 Init 容器没有成功之前,Pod 将不会变成 Ready 状态。 Init 容器的端口将不会在 Service 中进行聚集。 正在初始化中的 Pod 处于 Pending 状态,但应该会将条件 Initializing 设置为 true。

如果 Pod 重启,所有 Init 容器必须重新执行。

对 Init 容器 spec 的修改,被限制在容器 image 字段中。 更改 Init 容器的 image 字段,等价于重启该 Pod。

因为 Init 容器可能会被重启、重试或者重新执行,所以 Init 容器的代码应该是幂等的。 特别地,被写到 EmptyDirs 中文件的代码,应该对输出文件可能已经存在做好准备。

Init 容器具有应用容器的所有字段。 除了 readinessProbe,因为 Init 容器无法定义不同于完成(completion)的就绪(readiness)的之外的其他状态。 这会在验证过程中强制执行。

在 Pod 上使用 activeDeadlineSeconds,在容器上使用 livenessProbe,这样能够避免 Init 容器一直失败。 这就为 Init 容器活跃设置了一个期限。

在 Pod 中的每个 app 和 Init 容器的名称必须唯一;与任何其它容器共享同一个名称,会在验证时抛出错误。

资源

为 Init 容器指定顺序和执行逻辑,下面对资源使用的规则将被应用:

  • 在所有 Init 容器上定义的,任何特殊资源请求或限制的最大值,是 有效初始请求/限制
  • Pod 对资源的有效请求/限制要高于:
    • 所有应用容器对某个资源的请求/限制之和
    • 对某个资源的有效初始请求/限制
  • 基于有效请求/限制完成调度,这意味着 Init 容器能够为初始化预留资源,这些资源在 Pod 生命周期过程中并没有被使用。
  • Pod 的 有效 QoS 层,是 Init 容器和应用容器相同的 QoS 层。

基于有效 Pod 请求和限制来应用配额和限制。 Pod 级别的 cgroups 是基于有效 Pod 请求和限制,和调度器相同。

Pod 重启的原因

Pod 能够重启,会导致 Init 容器重新执行,主要有如下几个原因:

  • 用户更新 PodSpec 导致 Init 容器镜像发生改变。应用容器镜像的变更只会重启应用容器。
  • Pod 基础设施容器被重启。这不多见,但某些具有 root 权限可访问 Node 的人可能会这样做。
  • restartPolicy 设置为 Always,Pod 中所有容器会终止,强制重启,由于垃圾收集导致 Init 容器完整的记录丢失。

支持与兼容性

Apiserver 版本为 1.6 或更高版本的集群,通过使用 spec.initContainers 字段来支持 Init 容器。 之前的版本可以使用 alpha 和 beta 注解支持 Init 容器。 spec.initContainers 字段也被加入到 alpha 和 beta 注解中,所以 Kubernetes 1.3.0 版本或更高版本可以执行 Init 容器,并且 1.6 版本的 apiserver 能够安全地回退到 1.5.x 版本,而不会使存在的已创建 Pod 失去 Init 容器的功能。

Pod 安全策略

PodSecurityPolicy 类型的对象能够控制,是否可以向 Pod 发送请求,该 Pod 能够影响被应用到 Pod 和容器的 SecurityContext。 查看 Pod 安全策略建议 获取更多信息。

什么是 Pod 安全策略?

Pod 安全策略 是集群级别的资源,它能够控制 Pod 运行的行为,以及它具有访问什么的能力。 PodSecurityPolicy对象定义了一组条件,指示 Pod 必须按系统所能接受的顺序运行。 它们允许管理员控制如下方面:

控制面 字段名称
已授权容器的运行 privileged
为容器添加默认的一组能力 defaultAddCapabilities
为容器去掉某些能力 requiredDropCapabilities
容器能够请求添加某些能力 allowedCapabilities
控制卷类型的使用 volumes
主机网络的使用 hostNetwork
主机端口的使用 hostPorts
主机 PID namespace 的使用 hostPID
主机 IPC namespace 的使用 hostIPC
主机路径的使用 allowedHostPaths
容器的 SELinux 上下文 seLinux
用户 ID runAsUser
配置允许的补充组 supplementalGroups
分配拥有 Pod 数据卷的 FSGroup fsGroup
必须使用一个只读的 root 文件系统 readOnlyRootFilesystem

Pod 安全策略 由设置和策略组成,它们能够控制 Pod 访问的安全特征。这些设置分为如下三类:

  • 基于布尔值控制:这种类型的字段默认为最严格限制的值。
  • 基于被允许的值集合控制:这种类型的字段会与这组值进行对比,以确认值被允许。
  • 基于策略控制:设置项通过一种策略提供的机制来生成该值,这种机制能够确保指定的值落在被允许的这组值中。

RunAsUser

  • MustRunAs - 必须配置一个 range。使用该范围内的第一个值作为默认值。验证是否不在配置的该范围内。
  • MustRunAsNonRoot - 要求提交的 Pod 具有非零 runAsUser 值,或在镜像中定义了 USER 环境变量。不提供默认值。
  • RunAsAny - 没有提供默认值。允许指定任何 runAsUser

SELinux

  • MustRunAs - 如果没有使用预分配的值,必须配置 seLinuxOptions。默认使用 seLinuxOptions。验证 seLinuxOptions
  • RunAsAny - 没有提供默认值。允许任意指定的 seLinuxOptions ID。

SupplementalGroups

  • MustRunAs - 至少需要指定一个范围。默认使用第一个范围的最小值。验证所有范围的值。
  • RunAsAny - 没有提供默认值。允许任意指定的 supplementalGroups ID。

FSGroup

  • MustRunAs - 至少需要指定一个范围。默认使用第一个范围的最小值。验证在第一个范围内的第一个 ID。
  • RunAsAny - 没有提供默认值。允许任意指定的 fsGroup ID。

控制卷

通过设置 PSP 卷字段,能够控制具体卷类型的使用。当创建一个卷的时候,与该字段相关的已定义卷可以允许设置如下值:

  1. azureFile
  2. azureDisk
  3. flocker
  4. flexVolume
  5. hostPath
  6. emptyDir
  7. gcePersistentDisk
  8. awsElasticBlockStore
  9. gitRepo
  10. secret
  11. nfs
  12. iscsi
  13. glusterfs
  14. persistentVolumeClaim
  15. rbd
  16. cinder
  17. cephFS
  18. downwardAPI
  19. fc
  20. configMap
  21. vsphereVolume
  22. quobyte
  23. photonPersistentDisk
  24. projected
  25. portworxVolume
  26. scaleIO
  27. storageos
  28. * (allow all volumes)

对新的 PSP,推荐允许的卷的最小集合包括:configMap、downwardAPI、emptyDir、persistentVolumeClaim、secret 和 projected。

主机网络

  • HostPorts, 默认为 emptyHostPortRange 列表通过 min(包含) and max(包含) 来定义,指定了被允许的主机端口。

允许的主机路径

  • AllowedHostPaths 是一个被允许的主机路径前缀的白名单。空值表示所有的主机路径都可以使用。

许可

包含 PodSecurityPolicy许可控制,允许控制集群资源的创建和修改,基于这些资源在集群范围内被许可的能力。

许可使用如下的方式为 Pod 创建最终的安全上下文:

  1. 检索所有可用的 PSP。
  2. 生成在请求中没有指定的安全上下文设置的字段值。
  3. 基于可用的策略,验证最终的设置。

如果某个策略能够匹配上,该 Pod 就被接受。如果请求与 PSP 不匹配,则 Pod 被拒绝。

Pod 必须基于 PSP 验证每个字段。

创建 Pod 安全策略

下面是一个 Pod 安全策略的例子,所有字段的设置都被允许:

apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
  name: permissive
spec:
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  runAsUser:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  hostPorts:
  - min: 8000
    max: 8080
  volumes:
  - '*'

下载示例文件可以创建该策略,然后执行如下命令:

$ kubectl create -f ./psp.yaml
podsecuritypolicy "permissive" created

获取 Pod 安全策略列表

获取已存在策略列表,使用 kubectl get

$ kubectl get psp
NAME        PRIV   CAPS  SELINUX   RUNASUSER         FSGROUP   SUPGROUP  READONLYROOTFS  VOLUMES
permissive  false  []    RunAsAny  RunAsAny          RunAsAny  RunAsAny  false           [*]
privileged  true   []    RunAsAny  RunAsAny          RunAsAny  RunAsAny  false           [*]
restricted  false  []    RunAsAny  MustRunAsNonRoot  RunAsAny  RunAsAny  false           [emptyDir secret downwardAPI configMap persistentVolumeClaim projected]

修改 Pod 安全策略

通过交互方式修改策略,使用 kubectl edit

$ kubectl edit psp permissive

该命令将打开一个默认文本编辑器,在这里能够修改策略。

删除 Pod 安全策略

一旦不再需要一个策略,很容易通过 kubectl 删除它:

$ kubectl delete psp permissive
podsecuritypolicy "permissive" deleted

启用 Pod 安全策略

为了能够在集群中使用 Pod 安全策略,必须确保如下:

  1. 启用 API 类型 extensions/v1beta1/podsecuritypolicy(仅对 1.6 之前的版本)
  2. 启用许可控制器 PodSecurityPolicy
  3. 定义自己的策略

使用 RBAC

在 Kubernetes 1.5 或更新版本,可以使用 PodSecurityPolicy 来控制,对基于用户角色和组的已授权容器的访问。访问不同的 PodSecurityPolicy 对象,可以基于认证来控制。基于 Deployment、ReplicaSet 等创建的 Pod,限制访问 PodSecurityPolicy 对象,Controller Manager 必须基于安全 API 端口运行,并且不能够具有超级用户权限。

PodSecurityPolicy 认证使用所有可用的策略,包括创建 Pod 的用户,Pod 上指定的服务账户(service acount)。当 Pod 基于 Deployment、ReplicaSet 创建时,它是创建 Pod 的 Controller Manager,所以如果基于非安全 API 端口运行,允许所有的 PodSecurityPolicy 对象,并且不能够有效地实现细分权限。用户访问给定的 PSP 策略有效,仅当是直接部署 Pod 的情况。更多详情,查看 PodSecurityPolicy RBAC 示例,当直接部署 Pod 时,应用 PodSecurityPolicy 控制基于角色和组的已授权容器的访问 。

Pod 的生命周期

Pod phase

Pod 的 status 在信息保存在 PodStatus 中定义,其中有一个 phase 字段。

Pod 的相位(phase)是 Pod 在其生命周期中的简单宏观概述。该阶段并不是对容器或 Pod 的综合汇总,也不是为了做为综合状态机。

Pod 相位的数量和含义是严格指定的。除了本文档中列举的状态外,不应该再假定 Pod 有其他的 phase 值。

下面是 phase 可能的值:

  • 挂起(Pending):Pod 已被 Kubernetes 系统接受,但有一个或者多个容器镜像尚未创建。等待时间包括调度 Pod 的时间和通过网络下载镜像的时间,这可能需要花点时间。
  • 运行中(Running):该 Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态。
  • 成功(Successed):Pod 中的所有容器都被成功终止,并且不会再重启。
  • 失败(Failed):Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止。
  • 未知(Unkonwn):因为某些原因无法取得 Pod 的状态,通常是因为与 Pod 所在主机通信失败。

Pod 状态

Pod 有一个 PodStatus 对象,其中包含一个 PodCondition 数组。 PodCondition 数组的每个元素都有一个 type 字段和一个 status 字段。type 字段是字符串,可能的值有 PodScheduled、Ready、Initialized 和 Unschedulable。status 字段是一个字符串,可能的值有 True、False 和 Unknown。

容器探针

探针 是由 kubelet 对容器执行的定期诊断。要执行诊断,kubelet 调用由容器实现的 Handler。有三种类型的处理程序:

  • ExecAction:在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。
  • TCPSocketAction:对指定端口上的容器的 IP 地址进行 TCP 检查。如果端口打开,则诊断被认为是成功的。
  • HTTPGetAction:对指定的端口和路径上的容器的 IP 地址执行 HTTP Get 请求。如果响应的状态码大于等于200 且小于 400,则诊断被认为是成功的。

每次探测都将获得以下三种结果之一:

  • 成功:容器通过了诊断。
  • 失败:容器未通过诊断。
  • 未知:诊断失败,因此不会采取任何行动。

Kubelet 可以选择是否执行在容器上运行的两种探针执行和做出反应:

  • livenessProbe:指示容器是否正在运行。如果存活探测失败,则 kubelet 会杀死容器,并且容器将受到其 重启策略 的影响。如果容器不提供存活探针,则默认状态为 Success
  • readinessProbe:指示容器是否准备好服务请求。如果就绪探测失败,端点控制器将从与 Pod 匹配的所有 Service 的端点中删除该 Pod 的 IP 地址。初始延迟之前的就绪状态默认为 Failure。如果容器不提供就绪探针,则默认状态为 Success

该什么时候使用存活(liveness)和就绪(readiness)探针?

如果容器中的进程能够在遇到问题或不健康的情况下自行崩溃,则不一定需要存活探针; kubelet 将根据 Pod 的restartPolicy 自动执行正确的操作。

如果您希望容器在探测失败时被杀死并重新启动,那么请指定一个存活探针,并指定restartPolicy 为 Always 或 OnFailure。

如果要仅在探测成功时才开始向 Pod 发送流量,请指定就绪探针。在这种情况下,就绪探针可能与存活探针相同,但是 spec 中的就绪探针的存在意味着 Pod 将在没有接收到任何流量的情况下启动,并且只有在探针探测成功后才开始接收流量。

如果您希望容器能够自行维护,您可以指定一个就绪探针,该探针检查与存活探针不同的端点。

请注意,如果您只想在 Pod 被删除时能够排除请求,则不一定需要使用就绪探针;在删除 Pod 时,Pod 会自动将自身置于未完成状态,无论就绪探针是否存在。当等待 Pod 中的容器停止时,Pod 仍处于未完成状态。

Pod 和容器状态

有关 Pod 容器状态的详细信息,请参阅 PodStatusContainerStatus。请注意,报告的 Pod 状态信息取决于当前的 ContainerState

重启策略

PodSpec 中有一个 restartPolicy 字段,可能的值为 Always、OnFailure 和 Never。默认为 Always。 restartPolicy 适用于 Pod 中的所有容器。restartPolicy 仅指通过同一节点上的 kubelet 重新启动容器。失败的容器由 kubelet 以五分钟为上限的指数退避延迟(10秒,20秒,40秒…)重新启动,并在成功执行十分钟后重置。如 Pod 文档 中所述,一旦绑定到一个节点,Pod 将永远不会重新绑定到另一个节点。

Pod 的生命

一般来说,Pod 不会消失,直到人为销毁他们。这可能是一个人或控制器。这个规则的唯一例外是成功或失败的 phase 超过一段时间(由 master 确定)的Pod将过期并被自动销毁。

有三种可用的控制器:

  • 使用 Job 运行预期会终止的 Pod,例如批量计算。Job 仅适用于重启策略为 OnFailureNever 的 Pod。

  • 对预期不会终止的 Pod 使用 ReplicationControllerReplicaSetDeployment ,例如 Web 服务器。 ReplicationController 仅适用于具有 restartPolicy 为 Always 的 Pod。

  • 提供特定于机器的系统服务,使用 DaemonSet 为每台机器运行一个 Pod 。

所有这三种类型的控制器都包含一个 PodTemplate。建议创建适当的控制器,让它们来创建 Pod,而不是直接自己创建 Pod。这是因为单独的 Pod 在机器故障的情况下没有办法自动复原,而控制器却可以。

如果节点死亡或与集群的其余部分断开连接,则 Kubernetes 将应用一个策略将丢失节点上的所有 Pod 的 phase 设置为 Failed。

示例

高级 liveness 探针示例

存活探针由 kubelet 来执行,因此所有的请求都在 kubelet 的网络命名空间中进行。

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - args:
    - /server
    image: gcr.io/google_containers/liveness
    livenessProbe:
      httpGet:
        # when "host" is not defined, "PodIP" will be used
        # host: my-host
        # when "scheme" is not defined, "HTTP" scheme will be used. Only "HTTP" and "HTTPS" are allowed
        # scheme: HTTPS
        path: /healthz
        port: 8080
        httpHeaders:
          - name: X-Custom-Header
            value: Awesome
      initialDelaySeconds: 15
      timeoutSeconds: 1
    name: liveness

状态示例

  • Pod 中只有一个容器并且正在运行。容器成功退出。
    • 记录完成事件。
    • 如果restartPolicy为:
      • Always:重启容器;Pod phase 仍为 Running。
      • OnFailure:Pod phase 变成 Succeeded。
      • Never:Pod phase 变成 Succeeded。
  • Pod 中只有一个容器并且正在运行。容器退出失败。
    • 记录失败事件。
    • 如果restartPolicy为:
      • Always:重启容器;Pod phase 仍为 Running。
      • OnFailure:重启容器;Pod phase 仍为 Running。
      • Never:Pod phase 变成 Failed。
  • Pod 中有两个容器并且正在运行。有一个容器退出失败。
    • 记录失败事件。
    • 如果 restartPolicy 为:
      • Always:重启容器;Pod phase 仍为 Running。
      • OnFailure:重启容器;Pod phase 仍为 Running。
      • Never:不重启容器;Pod phase 仍为 Running。
    • 如果有一个容器没有处于运行状态,并且两个容器退出:
      • 记录失败事件。
      • 如果restartPolicy为:
        • Always:重启容器;Pod phase 仍为 Running。
        • OnFailure:重启容器;Pod phase 仍为 Running。
        • Never:Pod phase 变成 Failed。
  • Pod 中只有一个容器并处于运行状态。容器运行时内存超出限制:
    • 容器以失败状态终止。
    • 记录 OOM 事件。
    • 如果restartPolicy为:
      • Always:重启容器;Pod phase 仍为 Running。
      • OnFailure:重启容器;Pod phase 仍为 Running。
      • Never: 记录失败事件;Pod phase 仍为 Failed。
  • Pod 正在运行,磁盘故障:
    • 杀掉所有容器。
    • 记录适当事件。
    • Pod phase 变成 Failed。
    • 如果使用控制器来运行,Pod 将在别处重建。
  • Pod 正在运行,其节点被分段。
    • 节点控制器等待直到超时。
    • 节点控制器将 Pod phase 设置为 Failed。
    • 如果是用控制器来运行,Pod 将在别处重建。

Pod hook

Pod hook(钩子)是由Kubernetes管理的kubelet发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中。可以同时为Pod中的所有容器都配置hook。

Hook的类型包括两种:

  • exec:执行一段命令
  • HTTP:发送HTTP请求。

参考下面的配置:

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]

在容器创建之后,容器的Entrypoint执行之前,这时候Pod已经被调度到某台node上,被某个kubelet管理了,这时候kubelet会调用postStart操作,该操作跟容器的启动命令是在异步执行的,也就是说在postStart操作执行完成之前,kubelet会锁住容器,不让应用程序的进程启动,只有在 postStart操作完成之后容器的状态才会被设置成为RUNNING。

如果postStart或者preStop hook失败,将会终止容器。

调试hook

Hook调用的日志没有暴露个给Pod的event,所以只能通过describe命令来获取,如果有错误将可以看到FailedPostStartHookFailedPreStopHook这样的event。

Pod Preset

注意:PodPreset 资源对象只有 kubernetes 1.8 以上版本才支持。

Preset 就是预设,有时候想要让一批容器在启动的时候就注入一些信息,比如 secret、volume、volume mount 和环境变量,而又不想一个一个的改这些 Pod 的 tmeplate,这时候就可以用到 PodPreset 这个资源对象了。

本页是关于 PodPreset 的概述,该对象用来在 Pod 创建的时候向 Pod 中注入某些特定信息。该信息可以包括 secret、volume、volume mount 和环境变量。

理解 Pod Preset

Pod Preset 是用来在 Pod 被创建的时候向其中注入额外的运行时需求的 API 资源。

您可以使用 label selector 来指定为哪些 Pod 应用 Pod Preset。

使用 Pod Preset 使得 pod 模板的作者可以不必为每个 Pod 明确提供所有信息。这样一来,pod 模板的作者就不需要知道关于该服务的所有细节。

关于该背景的更多信息,请参阅 PodPreset 的设计方案

如何工作

Kubernetes 提供了一个准入控制器(PodPreset),当其启用时,Pod Preset 会将应用创建请求传入到该控制器上。当有 Pod 创建请求发生时,系统将执行以下操作:

  1. 检索所有可用的 PodPresets
  2. 检查 PodPreset 标签选择器上的标签,看看其是否能够匹配正在创建的 Pod 上的标签。
  3. 尝试将由 PodPreset 定义的各种资源合并到正在创建的 Pod 中。
  4. 出现错误时,在该 Pod 上引发记录合并错误的事件,PodPreset 不会注入任何资源到创建的 Pod 中。
  5. 注释刚生成的修改过的 Pod spec,以表明它已被 PodPreset 修改过。注释的格式为 podpreset.admission.kubernetes.io/podpreset-<pod-preset name>": "<resource version>"

每个 Pod 可以匹配零个或多个 Pod Prestet;并且每个 PodPreset 可以应用于零个或多个 Pod。 PodPreset 应用于一个或多个 Pod 时,Kubernetes 会修改 Pod Spec。对于 EnvEnvFromVolumeMounts 的更改,Kubernetes 修改 Pod 中所有容器的容器 spec;对于 Volume 的更改,Kubernetes 修改 Pod Spec。

注意:Pod Preset 可以在适当的时候修改 Pod spec 中的 spec.containers 字段。Pod Preset 中的资源定义将不会应用于 initContainers 字段。

禁用特定 Pod 的 Pod Preset

在某些情况下,您可能不希望 Pod 被任何 Pod Preset 所改变。在这些情况下,您可以在 Pod 的 Pod Spec 中添加注释:podpreset.admission.kubernetes.io/exclude:"true"

启用 Pod Preset

为了在群集中使用 Pod Preset,您必须确保以下内容:

  1. 您已启用 settings.k8s.io/v1alpha1/podpreset API 类型。例如,可以通过在 API server 的 --runtime-config 选项中包含 settings.k8s.io/v1alpha1=true 来完成此操作。
  2. 您已启用 PodPreset 准入控制器。 一种方法是将 PodPreset 包含在为 API server 指定的 --admission-control 选项值中。
  3. 您已经在要使用的命名空间中通过创建 PodPreset 对象来定义 PodPreset

更多资料

Node

Node是kubernetes集群的工作节点,可以是物理机也可以是虚拟机。

Node的状态

Node包括如下状态信息:

  • Address
    • HostName:可以被kubelet中的--hostname-override参数替代。
    • ExternalIP:可以被集群外部路由到的IP地址。
    • InternalIP:集群内部使用的IP,集群外部无法访问。
  • Condition
    • OutOfDisk:磁盘空间不足时为True
    • Ready:Node controller 40秒内没有收到node的状态报告为Unknown,健康为True,否则为False
    • MemoryPressure:当node没有内存压力时为True,否则为False
    • DiskPressure:当node没有磁盘压力时为True,否则为False
  • Capacity
    • CPU
    • 内存
    • 可运行的最大Pod个数
  • Info:节点的一些版本信息,如OS、kubernetes、docker等

Node管理

禁止pod调度到该节点上

kubectl cordon <node>

驱逐该节点上的所有pod

kubectl drain <node>

该命令会删除该节点上的所有Pod(DaemonSet除外),在其他node上重新启动它们,通常该节点需要维护时使用该命令。直接使用该命令会自动调用kubectl cordon <node>命令。当该节点维护完成,启动了kubelet后,再使用kubectl uncordon <node>即可将该节点添加到kubernetes集群中。

Namespace

在一个Kubernetes集群中可以使用namespace创建多个“虚拟集群”,这些namespace之间可以完全隔离,也可以通过某种方式,让一个namespace中的service可以访问到其他的namespace中的服务,我们在CentOS中部署kubernetes1.6集群的时候就用到了好几个跨越namespace的服务,比如Traefik ingress和kube-systemnamespace下的service就可以为整个集群提供服务,这些都需要通过RBAC定义集群级别的角色来实现。

哪些情况下适合使用多个namesapce

因为namespace可以提供独立的命名空间,因此可以实现部分的环境隔离。当你的项目和人员众多的时候可以考虑根据项目属性,例如生产、测试、开发划分不同的namespace。

Namespace使用

获取集群中有哪些namespace

kubectl get ns

集群中默认会有defaultkube-system这两个namespace。

在执行kubectl命令时可以使用-n指定操作的namespace。

用户的普通应用默认是在default下,与集群管理相关的为整个集群提供服务的应用一般部署在kube-system的namespace下,例如我们在安装kubernetes集群时部署的kubednsheapseterEFK等都是在这个namespace下面。

另外,并不是所有的资源对象都会对应namespace,nodepersistentVolume就不属于任何namespace。

Service

Kubernetes Pod 是有生命周期的,它们可以被创建,也可以被销毁,然而一旦被销毁生命就永远结束。
通过 ReplicationController 能够动态地创建和销毁 Pod(例如,需要进行扩缩容,或者执行 滚动升级)。
每个 Pod 都会获取它自己的 IP 地址,即使这些 IP 地址不总是稳定可依赖的。
这会导致一个问题:在 Kubernetes 集群中,如果一组 Pod(称为 backend)为其它 Pod (称为 frontend)提供服务,那么那些 frontend 该如何发现,并连接到这组 Pod 中的哪些 backend 呢?

关于 Service

Kubernetes Service 定义了这样一种抽象:一个 Pod 的逻辑分组,一种可以访问它们的策略 —— 通常称为微服务。
这一组 Pod 能够被 Service 访问到,通常是通过 Label Selector(查看下面了解,为什么可能需要没有 selector 的 Service)实现的。

举个例子,考虑一个图片处理 backend,它运行了3个副本。这些副本是可互换的 —— frontend 不需要关心它们调用了哪个 backend 副本。
然而组成这一组 backend 程序的 Pod 实际上可能会发生变化,frontend 客户端不应该也没必要知道,而且也不需要跟踪这一组 backend 的状态。
Service 定义的抽象能够解耦这种关联。

对 Kubernetes 集群中的应用,Kubernetes 提供了简单的 Endpoints API,只要 Service 中的一组 Pod 发生变更,应用程序就会被更新。
对非 Kubernetes 集群中的应用,Kubernetes 提供了基于 VIP 的网桥的方式访问 Service,再由 Service 重定向到 backend Pod

定义 Service

一个 Service 在 Kubernetes 中是一个 REST 对象,和 Pod 类似。
像所有的 REST 对象一样, Service 定义可以基于 POST 方式,请求 apiserver 创建新的实例。
例如,假定有一组 Pod,它们对外暴露了 9376 端口,同时还被打上 "app=MyApp" 标签。

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

上述配置将创建一个名称为 “my-service” 的 Service 对象,它会将请求代理到使用 TCP 端口 9376,并且具有标签 "app=MyApp"Pod 上。
这个 Service 将被指派一个 IP 地址(通常称为 “Cluster IP”),它会被服务的代理使用(见下面)。
Service 的 selector 将会持续评估,处理结果将被 POST 到一个名称为 “my-service” 的 Endpoints 对象上。

需要注意的是, Service 能够将一个接收端口映射到任意的 targetPort
默认情况下,targetPort 将被设置为与 port 字段相同的值。
可能更有趣的是,targetPort 可以是一个字符串,引用了 backend Pod 的一个端口的名称。
但是,实际指派给该端口名称的端口号,在每个 backend Pod 中可能并不相同。
对于部署和设计 Service ,这种方式会提供更大的灵活性。
例如,可以在 backend 软件下一个版本中,修改 Pod 暴露的端口,并不会中断客户端的调用。

Kubernetes Service 能够支持 TCPUDP 协议,默认 TCP 协议。

没有 selector 的 Service

Service 抽象了该如何访问 Kubernetes Pod,但也能够抽象其它类型的 backend,例如:

  • 希望在生产环境中使用外部的数据库集群,但测试环境使用自己的数据库。
  • 希望服务指向另一个 Namespace 中或其它集群中的服务。
  • 正在将工作负载转移到 Kubernetes 集群,和运行在 Kubernetes 集群之外的 backend。

在任何这些场景中,都能够定义没有 selector 的 Service

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

由于这个 Service 没有 selector,就不会创建相关的 Endpoints 对象。可以手动将 Service 映射到指定的 Endpoints

kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 9376

注意:Endpoint IP 地址不能是 loopback(127.0.0.0/8)、 link-local(169.254.0.0/16)、或者 link-local 多播(224.0.0.0/24)。

访问没有 selector 的 Service,与有 selector 的 Service 的原理相同。请求将被路由到用户定义的 Endpoint(该示例中为 1.2.3.4:9376)。

ExternalName ServiceService 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。
相反地,对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。

kind: Service
apiVersion: v1
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

当查询主机 my-service.prod.svc.CLUSTER时,集群的 DNS 服务将返回一个值为 my.database.example.comCNAME 记录。
访问这个服务的工作方式与其它的相同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。
如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改 Servicetype

VIP 和 Service 代理

在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式,而不是 ExternalName 的形式。
在 Kubernetes v1.0 版本,代理完全在 userspace。在 Kubernetes v1.1 版本,新增了 iptables 代理,但并不是默认的运行模式。
从 Kubernetes v1.2 起,默认就是 iptables 代理。

在 Kubernetes v1.0 版本,Service 是 “4层”(TCP/UDP over IP)概念。
在 Kubernetes v1.1 版本,新增了 Ingress API(beta 版),用来表示 “7层”(HTTP)服务。

userspace 代理模式

这种模式,kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。
对每个 Service,它会在本地 Node 上打开一个端口(随机选择)。
任何连接到“代理端口”的请求,都会被代理到 Service 的backend Pods 中的某个上面(如 Endpoints 所报告的一样)。
使用哪个 backend Pod,是基于 ServiceSessionAffinity 来确定的。
最后,它安装 iptables 规则,捕获到达该 ServiceclusterIP(是虚拟 IP)和 Port 的请求,并重定向到代理端口,代理端口再代理请求到 backend Pod

网络返回的结果是,任何到达 Service 的 IP:Port 的请求,都会被代理到一个合适的 backend,不需要客户端知道关于 Kubernetes、Service、或 Pod 的任何信息。

默认的策略是,通过 round-robin 算法来选择 backend Pod
实现基于客户端 IP 的会话亲和性,可以通过设置 service.spec.sessionAffinity 的值为 "ClientIP" (默认值为 "None")。

iptables 代理模式

这种模式,kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。
对每个 Service,它会安装 iptables 规则,从而捕获到达该 ServiceclusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend 中的某个上面。
对于每个 Endpoints 对象,它也会安装 iptables 规则,这个规则会选择一个 backend Pod

默认的策略是,随机选择一个 backend。
实现基于客户端 IP 的会话亲和性,可以将 service.spec.sessionAffinity 的值设置为 "ClientIP" (默认值为 "None")。

和 userspace 代理类似,网络返回的结果是,任何到达 Service 的 IP:Port 的请求,都会被代理到一个合适的 backend,不需要客户端知道关于 Kubernetes、Service、或 Pod 的任何信息。
这应该比 userspace 代理更快、更可靠。然而,不像 userspace 代理,如果初始选择的 Pod 没有响应,iptables 代理不能自动地重试另一个 Pod,所以它需要依赖 readiness probes

多端口 Service

很多 Service 需要暴露多个端口。对于这种情况,Kubernetes 支持在 Service 对象中定义多个端口。
当使用多个端口时,必须给出所有的端口的名称,这样 Endpoint 就不会产生歧义,例如:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
    selector:
      app: MyApp
    ports:
      - name: http
        protocol: TCP
        port: 80
        targetPort: 9376
      - name: https
        protocol: TCP
        port: 443
        targetPort: 9377

选择自己的 IP 地址

Service 创建的请求中,可以通过设置 spec.clusterIP 字段来指定自己的集群 IP 地址。
比如,希望替换一个已经已存在的 DNS 条目,或者遗留系统已经配置了一个固定的 IP 且很难重新配置。
用户选择的 IP 地址必须合法,并且这个 IP 地址在 service-cluster-ip-range CIDR 范围内,这对 API Server 来说是通过一个标识来指定的。
如果 IP 地址不合法,API Server 会返回 HTTP 状态码 422,表示值不合法。

为何不使用 round-robin DNS?

一个不时出现的问题是,为什么我们都使用 VIP 的方式,而不使用标准的 round-robin DNS,有如下几个原因:

  • 长久以来,DNS 库都没能认真对待 DNS TTL、缓存域名查询结果
  • 很多应用只查询一次 DNS 并缓存了结果
    • 就算应用和库能够正确查询解析,每个客户端反复重解析造成的负载也是非常难以管理的

我们尽力阻止用户做那些对他们没有好处的事情,如果很多人都来问这个问题,我们可能会选择实现它。

服务发现

Kubernetes 支持2种基本的服务发现模式 —— 环境变量和 DNS。

环境变量

Pod 运行在 Node 上,kubelet 会为每个活跃的 Service 添加一组环境变量。
它同时支持 Docker links 兼容 变量(查看 makeLinkVariables)、简单的 {SVCNAME}_SERVICE_HOST{SVCNAME}_SERVICE_PORT 变量,这里 Service 的名称需大写,横线被转换成下划线。

举个例子,一个名称为 "redis-master" 的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:

REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

这意味着需要有顺序的要求 —— Pod 想要访问的任何 Service 必须在 Pod 自己之前被创建,否则这些环境变量就不会被赋值。DNS 并没有这个限制。

DNS

一个可选(尽管强烈推荐)集群插件 是 DNS 服务器。
DNS 服务器监视着创建新 Service 的 Kubernetes API,从而为每一个 Service 创建一组 DNS 记录。
如果整个集群的 DNS 一直被启用,那么所有的 Pod 应该能够自动对 Service 进行名称解析。

例如,有一个名称为 "my-service"Service,它在 Kubernetes 集群中名为 "my-ns"Namespace 中,为 "my-service.my-ns" 创建了一条 DNS 记录。
在名称为 "my-ns"Namespace 中的 Pod 应该能够简单地通过名称查询找到 "my-service"
在另一个 Namespace 中的 Pod 必须限定名称为 "my-service.my-ns"
这些名称查询的结果是 Cluster IP。

Kubernetes 也支持对端口名称的 DNS SRV(Service)记录。
如果名称为 "my-service.my-ns"Service 有一个名为 "http"TCP 端口,可以对 "_http._tcp.my-service.my-ns" 执行 DNS SRV 查询,得到 "http" 的端口号。

Kubernetes DNS 服务器是唯一的一种能够访问 ExternalName 类型的 Service 的方式。
更多信息可以查看 DNS Pod 和 Service

Headless Service

有时不需要或不想要负载均衡,以及单独的 Service IP。
遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 "None" 来创建 Headless Service。

这个选项允许开发人员自由寻找他们自己的方式,从而降低与 Kubernetes 系统的耦合性。
应用仍然可以使用一种自注册的模式和适配器,对其它需要发现机制的系统能够很容易地基于这个 API 来构建。

对这类 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。
DNS 如何实现自动配置,依赖于 Service 是否定义了 selector。

配置 Selector

对定义了 selector 的 Headless Service,Endpoint 控制器在 API 中创建了 Endpoints 记录,并且修改 DNS 配置返回 A 记录(地址),通过这个地址直接到达 Service 的后端 Pod 上。

不配置 Selector

对没有定义 selector 的 Headless Service,Endpoint 控制器不会创建 Endpoints 记录。
然而 DNS 系统会查找和配置,无论是:

  • ExternalName

    类型 Service 的 CNAME 记录

    • 记录:与 Service 共享一个名称的任何 Endpoints,以及所有其它类型

发布服务 —— 服务类型

对一些应用(如 Frontend)的某些部分,可能希望通过外部(Kubernetes 集群外部)IP 地址暴露 Service。

Kubernetes ServiceTypes 允许指定一个需要的类型的 Service,默认是 ClusterIP 类型。

Type 的取值以及行为如下:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType
  • NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载均衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。
    没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

NodePort 类型

如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。该端口将通过 Servicespec.ports[*].nodePort 字段被指定。

如果需要指定的端口号,可以配置 nodePort 的值,系统将分配这个端口,否则调用 API 将会失败(比如,需要关心端口冲突的可能性)。

这可以让开发人员自由地安装他们自己的负载均衡器,并配置 Kubernetes 不能完全支持的环境参数,或者直接暴露一个或多个 Node 的 IP 地址。

需要注意的是,Service 将能够通过 <NodeIP>:spec.ports[*].nodePortspec.clusterIp:spec.ports[*].port 而对外可见。

LoadBalancer 类型

使用支持外部负载均衡器的云提供商的服务,设置 type 的值为 "LoadBalancer",将为 Service 提供负载均衡器。
负载均衡器是异步创建的,关于被提供的负载均衡器的信息将会通过 Servicestatus.loadBalancer 字段被发布出去。

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
      nodePort: 30061
  clusterIP: 10.0.171.239
  loadBalancerIP: 78.11.24.19
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
      - ip: 146.148.47.155

来自外部负载均衡器的流量将直接打到 backend Pod 上,不过实际它们是如何工作的,这要依赖于云提供商。
在这些情况下,将根据用户设置的 loadBalancerIP 来创建负载均衡器。
某些云提供商允许设置 loadBalancerIP。如果没有设置 loadBalancerIP,将会给负载均衡器指派一个临时 IP。
如果设置了 loadBalancerIP,但云提供商并不支持这种特性,那么设置的 loadBalancerIP 值将会被忽略掉。

AWS 内部负载均衡器

在混合云环境中,有时从虚拟私有云(VPC)环境中的服务路由流量是非常有必要的。
可以通过在 Service 中增加 annotation 来实现,如下所示:

[...]
metadata: 
    name: my-service
    annotations: 
        service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
[...]

在水平分割的 DNS 环境中,需要两个 Service 来将外部和内部的流量路由到 Endpoint 上。

AWS SSL 支持

对运行在 AWS 上部分支持 SSL 的集群,从 1.3 版本开始,可以为 LoadBalancer 类型的 Service 增加两个 annotation:

    metadata:
      name: my-service
      annotations:
        service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012

第一个 annotation 指定了使用的证书。它可以是第三方发行商发行的证书,这个证书或者被上传到 IAM,或者由 AWS 的证书管理器创建。

    metadata:
      name: my-service
      annotations:
         service.beta.kubernetes.io/aws-load-balancer-backend-protocol: (https|http|ssl|tcp)

第二个 annotation 指定了 Pod 使用的协议。
对于 HTTPS 和 SSL,ELB 将期望该 Pod 基于加密的连接来认证自身。

HTTP 和 HTTPS 将选择7层代理:ELB 将中断与用户的连接,当转发请求时,会解析 Header 信息并添加上用户的 IP 地址(Pod 将只能在连接的另一端看到该 IP 地址)。

TCP 和 SSL 将选择4层代理:ELB 将转发流量,并不修改 Header 信息。

外部 IP

如果外部的 IP 路由到集群中一个或多个 Node 上,Kubernetes Service 会被暴露给这些 externalIPs
通过外部 IP(作为目的 IP 地址)进入到集群,打到 Service 的端口上的流量,将会被路由到 Service 的 Endpoint 上。
externalIPs 不会被 Kubernetes 管理,它属于集群管理员的职责范畴。

根据 Service 的规定,externalIPs 可以同任意的 ServiceType 来一起指定。
在下面的例子中,my-service 可以在 80.11.12.10:80(外部 IP:端口)上被客户端访问。

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 9376
  externalIPs: 
    - 80.11.12.10

不足之处

为 VIP 使用 userspace 代理,将只适合小型到中型规模的集群,不能够扩展到上千 Service 的大型集群。
查看 最初设计方案 获取更多细节。

使用 userspace 代理,隐藏了访问 Service 的数据包的源 IP 地址。
这使得一些类型的防火墙无法起作用。
iptables 代理不会隐藏 Kubernetes 集群内部的 IP 地址,但却要求客户端请求必须通过一个负载均衡器或 Node 端口。

Type 字段支持嵌套功能 —— 每一层需要添加到上一层里面。
不会严格要求所有云提供商(例如,GCE 就没必要为了使一个 LoadBalancer 能工作而分配一个 NodePort,但是 AWS 需要 ),但当前 API 是强制要求的。

VIP 的那些骇人听闻的细节

对很多想使用 Service 的人来说,前面的信息应该足够了。
然而,有很多内部原理性的内容,还是值去理解的。

避免冲突

Kubernetes 最主要的哲学之一,是用户不应该暴露那些能够导致他们操作失败、但又不是他们的过错的场景。
这种场景下,让我们来看一下网络端口 —— 用户不应该必须选择一个端口号,而且该端口还有可能与其他用户的冲突。
这就是说,在彼此隔离状态下仍然会出现失败。

为了使用户能够为他们的 Service 选择一个端口号,我们必须确保不能有2个 Service 发生冲突。
我们可以通过为每个 Service 分配它们自己的 IP 地址来实现。

为了保证每个 Service 被分配到一个唯一的 IP,需要一个内部的分配器能够原子地更新 etcd 中的一个全局分配映射表,这个更新操作要先于创建每一个 Service
为了使 Service 能够获取到 IP,这个映射表对象必须在注册中心存在,否则创建 Service 将会失败,指示一个 IP 不能被分配。
一个后台 Controller 的职责是创建映射表(从 Kubernetes 的旧版本迁移过来,旧版本中是通过在内存中加锁的方式实现),并检查由于管理员干预和清除任意 IP 造成的不合理分配,这些 IP 被分配了但当前没有 Service 使用它们。

IP 和 VIP

不像 Pod 的 IP 地址,它实际路由到一个固定的目的地,Service 的 IP 实际上不能通过单个主机来进行应答。
相反,我们使用 iptables(Linux 中的数据包处理逻辑)来定义一个虚拟IP地址(VIP),它可以根据需要透明地进行重定向。
当客户端连接到 VIP 时,它们的流量会自动地传输到一个合适的 Endpoint。
环境变量和 DNS,实际上会根据 Service 的 VIP 和端口来进行填充。

Userspace

作为一个例子,考虑前面提到的图片处理应用程序。
当创建 backend Service 时,Kubernetes master 会给它指派一个虚拟 IP 地址,比如 10.0.0.1。
假设 Service 的端口是 1234,该 Service 会被集群中所有的 kube-proxy 实例观察到。
当代理看到一个新的 Service, 它会打开一个新的端口,建立一个从该 VIP 重定向到新端口的 iptables,并开始接收请求连接。

当一个客户端连接到一个 VIP,iptables 规则开始起作用,它会重定向该数据包到 Service代理 的端口。
Service代理 选择一个 backend,并将客户端的流量代理到 backend 上。

这意味着 Service 的所有者能够选择任何他们想使用的端口,而不存在冲突的风险。
客户端可以简单地连接到一个 IP 和端口,而不需要知道实际访问了哪些 Pod

Iptables

再次考虑前面提到的图片处理应用程序。
当创建 backend Service 时,Kubernetes master 会给它指派一个虚拟 IP 地址,比如 10.0.0.1。
假设 Service 的端口是 1234,该 Service 会被集群中所有的 kube-proxy 实例观察到。
当代理看到一个新的 Service, 它会安装一系列的 iptables 规则,从 VIP 重定向到 per-Service 规则。
该 per-Service 规则连接到 per-Endpoint 规则,该 per-Endpoint 规则会重定向(目标 NAT)到 backend。

当一个客户端连接到一个 VIP,iptables 规则开始起作用。一个 backend 会被选择(或者根据会话亲和性,或者随机),数据包被重定向到这个 backend。
不像 userspace 代理,数据包从来不拷贝到用户空间,kube-proxy 不是必须为该 VIP 工作而运行,并且客户端 IP 是不可更改的。
当流量打到 Node 的端口上,或通过负载均衡器,会执行相同的基本流程,但是在那些案例中客户端 IP 是可以更改的。

API 对象

在 Kubernetes REST API 中,Service 是 top-level 资源。关于 API 对象的更多细节可以查看:Service API 对象

Volume

容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃时,kubelet 会重启它,但是容器中的文件将丢失——容器以干净的状态(镜像最初的状态)重新启动。其次,在 Pod 中同时运行多个容器时,这些容器之间通常需要共享文件。Kubernetes 中的 Volume 抽象就很好的解决了这些问题。

建议先熟悉 pod

背景

Docker 中也有一个 volume 的概念,尽管它稍微宽松一些,管理也很少。在 Docker 中,卷就像是磁盘或是另一个容器中的一个目录。它的生命周期不受管理,直到最近才有了 local-disk-backed 卷。Docker 现在提供了卷驱动程序,但是功能还非常有限(例如Docker1.7只允许每个容器使用一个卷驱动,并且无法给卷传递参数)。

另一方面,Kubernetes 中的卷有明确的寿命——与封装它的 Pod 相同。所以,卷的生命比 Pod 中的所有容器都长,当这个容器重启时数据仍然得以保存。当然,当 Pod 不再存在时,卷也将不复存在。也许更重要的是,Kubernetes 支持多种类型的卷,Pod 可以同时使用任意数量的卷。

卷的核心是目录,可能还包含了一些数据,可以通过 pod 中的容器来访问。该目录是如何形成的、支持该目录的介质以及其内容取决于所使用的特定卷类型。

要使用卷,需要为 pod 指定为卷(spec.volumes 字段)以及将它挂载到容器的位置(spec.containers.volumeMounts 字段)。

容器中的进程看到的是由其 Docker 镜像和卷组成的文件系统视图。 Docker 镜像位于文件系统层次结构的根目录,任何卷都被挂载在镜像的指定路径中。卷无法挂载到其他卷上或与其他卷有硬连接。Pod 中的每个容器都必须独立指定每个卷的挂载位置。

卷的类型

Kubernetes 支持以下类型的卷:

  • awsElasticBlockStore
  • azureDisk
  • azureFile
  • cephfs
  • csi
  • downwardAPI
  • emptyDir
  • fc (fibre channel)
  • flocker
  • gcePersistentDisk
  • gitRepo
  • glusterfs
  • hostPath
  • iscsi
  • local
  • nfs
  • persistentVolumeClaim
  • projected
  • portworxVolume
  • quobyte
  • rbd
  • scaleIO
  • secret
  • storageos
  • vsphereVolume

我们欢迎额外贡献。

awsElasticBlockStore

awsElasticBlockStore 卷将Amazon Web Services(AWS)EBS Volume 挂载到您的容器中。与 emptyDir 类型会在删除 Pod 时被清除不同,EBS 卷的的内容会保留下来,仅仅是被卸载。这意味着 EBS 卷可以预先填充数据,并且可以在数据包之间“切换”数据。

重要提示:您必须使用 aws ec2 create-volume 或 AWS API 创建 EBS 卷,才能使用它。

使用 awsElasticBlockStore 卷时有一些限制:

  • 运行 Pod 的节点必须是 AWS EC2 实例
  • 这些实例需要与 EBS 卷位于相同的区域和可用区域
  • EBS 仅支持卷和 EC2 实例的一对一的挂载

创建 EBS 卷

在 pod 中使用的 EBS 卷之前,您需要先创建它。

aws ec2 create-volume --availability-zone=eu-west-1a --size=10 --volume-type=gp2

确保区域与您启动集群的区域相匹配(并且检查大小和 EBS 卷类型是否适合您的使用!)

AWS EBS 示例配置

apiVersion: v1
kind: Pod
metadata:
  name: test-ebs
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-ebs
      name: test-volume
  volumes:
  - name: test-volume
    # This AWS EBS volume must already exist.
    awsElasticBlockStore:
      volumeID: <volume-id>
      fsType: ext4

azureDisk

AzureDisk 用于将 Microsoft Azure Data Disk 挂载到 Pod 中。

更多细节可以在这里找到。

azureFile

azureFile 用于将 Microsoft Azure File Volume(SMB 2.1 和 3.0)挂载到 Pod 中。

更多细节可以在[这里](https://github.com/kubernetes/examples/tree/ master/staging/volumes/azure_file/README.md)找到。

cephfs

cephfs 卷允许将现有的 CephFS 卷挂载到您的容器中。不像 emptyDir,当删除 Pod 时被删除,cephfs 卷的内容将被保留,卷仅仅是被卸载。这意味着 CephFS 卷可以预先填充数据,并且可以在数据包之间“切换”数据。 CephFS 可以被多个写设备同时挂载。

重要提示:您必须先拥有自己的 Ceph 服务器,然后才能使用它。

有关更多详细信息,请参见CephFS示例

csi

CSI 代表容器存储接口,CSI 试图建立一个行业标准接口的规范,借助 CSI 容器编排系统(CO)可以将任意存储系统暴露给自己的容器工作负载。有关详细信息,请查看设计方案

csi 卷类型是一种 in-tree 的 CSI 卷插件,用于 Pod 与在同一节点上运行的外部 CSI 卷驱动程序交互。部署 CSI 兼容卷驱动后,用户可以使用 csi 作为卷类型来挂载驱动提供的存储。

CSI 持久化卷支持是在 Kubernetes v1.9 中引入的,作为一个 alpha 特性,必须由集群管理员明确启用。换句话说,集群管理员需要在 apiserver、controller-manager 和 kubelet 组件的 “--feature-gates =” 标志中加上 “CSIPersistentVolume = true”。

CSI 持久化卷具有以下字段可供用户指定:

  • driver:一个字符串值,指定要使用的卷驱动程序的名称。必须少于 63 个字符,并以一个字符开头。驱动程序名称可以包含 “.”、“- ”、“_” 或数字。
  • volumeHandle:一个字符串值,唯一标识从 CSI 卷插件的 CreateVolume 调用返回的卷名。随后在卷驱动程序的所有后续调用中使用卷句柄来引用该卷。
  • readOnly:一个可选的布尔值,指示卷是否被发布为只读。默认是 false。

downwardAPI

downwardAPI 卷用于使向下 API 数据(downward API data)对应用程序可用。它挂载一个目录,并将请求的数据写入纯文本文件。

参考 downwardAPI 卷示例查看详细信息。

emptyDir

当 Pod 被分配给节点时,首先创建 emptyDir 卷,并且只要该 Pod 在该节点上运行,该卷就会存在。正如卷的名字所述,它最初是空的。Pod 中的容器可以读取和写入 emptyDir 卷中的相同文件,尽管该卷可以挂载到每个容器中的相同或不同路径上。当出于任何原因从节点中删除 Pod 时,emptyDir 中的数据将被永久删除。

注意:容器崩溃不会从节点中移除 pod,因此 emptyDir 卷中的数据在容器崩溃时是安全的。

emptyDir 的用法有:

  • 暂存空间,例如用于基于磁盘的合并排序
  • 用作长时间计算崩溃恢复时的检查点
  • Web服务器容器提供数据时,保存内容管理器容器提取的文件

Pod 示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}

fc (fibre channel)

fc 卷允许将现有的 fc 卷挂载到 pod 中。您可以使用卷配置中的 targetWWN 参数指定单个或多个目标全球通用名称(World Wide Name)。如果指定了多个 WWN,则 targetWWN 期望这些 WWN 来自多路径连接。

重要提示:您必须配置 FC SAN 区域划分,并预先将这些 LUN(卷)分配并屏蔽到目标 WWN,以便 Kubernetes 主机可以访问它们。

参考 FC 示例获取详细信息。

flocker

Flocker 是一款开源的集群容器数据卷管理器。它提供了由各种存储后端支持的数据卷的管理和编排。

flocker 允许将 Flocker 数据集挂载到 pod 中。如果数据集在 Flocker 中不存在,则需要先使用 Flocker CLI 或使用 Flocker API 创建数据集。如果数据集已经存在,它将被 Flocker 重新连接到 pod 被调度的节点上。这意味着数据可以根据需要在数据包之间“切换”。

重要提示:您必须先运行自己的 Flocker 安装程序才能使用它。

参考 Flocker 示例获取更多详细信息。

gcePersistentDisk

gcePersistentDisk 卷将 Google Compute Engine(GCE)Persistent Disk 挂载到您的容器中。与删除 Pod 时删除的 emptyDir 不同,PD 的内容被保留,只是卸载了卷。这意味着 PD 可以预先填充数据,并且数据可以在 Pod 之间“切换”。

重要提示:您必须先使用 gcloud 或 GCE API 或 UI 创建一个 PD,然后才能使用它。

使用 gcePersistentDisk 时有一些限制:

  • 运行 Pod 的节点必须是 GCE 虚拟机
  • 那些虚拟机需要在与 PD 一样在 GCE 项目和区域中

PD 的一个特点是它们可以同时被多个用户以只读方式挂载。这意味着您可以预先使用您的数据集填充 PD,然后根据需要给多个 Pod 中并行提供。不幸的是,只能由单个消费者以读写模式挂载 PD,而不允许同时写入。
在由 ReplicationController 控制的 pod 上使用 PD 将会失败,除非 PD 是只读的或者副本数是 0 或 1。

创建 PD

在您在 pod 中使用 GCE PD 之前,需要先创建它。

gcloud compute disks create --size=500GB --zone=us-central1-a my-data-disk

Pod 示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    # This GCE PD must already exist.
    gcePersistentDisk:
      pdName: my-data-disk
      fsType: ext4

gitRepo

gitRepo 卷是一个可以演示卷插件功能的示例。它会挂载一个空目录并将 git 存储库克隆到您的容器中。将来,这样的卷可能会转移到一个更加分离的模型,而不是为每个这样的用例扩展 Kubernetes API。

下面是 gitRepo 卷示例:

apiVersion: v1
kind: Pod
metadata:
  name: server
spec:
  containers:
  - image: nginx
    name: nginx
    volumeMounts:
    - mountPath: /mypath
      name: git-volume
  volumes:
  - name: git-volume
    gitRepo:
      repository: "git@somewhere:me/my-git-repository.git"
      revision: "22f1d8406d464b0c0874075539c1f2e96c253775"

glusterfs

glusterfs 卷允许将 Glusterfs(一个开放源代码的网络文件系统)卷挂载到您的集群中。与删除 Pod 时删除的 emptyDir 不同,glusterfs 卷的内容将被保留,而卷仅仅被卸载。这意味着 glusterfs 卷可以预先填充数据,并且可以在数据包之间“切换”数据。 GlusterFS 可以同时由多个写入挂载。

重要提示:您必须先自行安装 GlusterFS,才能使用它。

有关更多详细信息,请参阅 GlusterFS 示例。

hostPath

hostPath 卷将主机节点的文件系统中的文件或目录挂载到集群中。该功能大多数 Pod 都用不到,但它为某些应用程序提供了一个强大的解决方法。

例如,hostPath 的用途如下:

  • 运行需要访问 Docker 内部的容器;使用 /var/lib/dockerhostPath
  • 在容器中运行 cAdvisor;使用 /dev/cgroupshostPath
  • 允许 pod 指定给定的 hostPath 是否应该在 pod 运行之前存在,是否应该创建,以及它应该以什么形式存在

除了所需的 path 属性之外,用户还可以为 hostPath 卷指定 type

type 字段支持以下值:

行为
空字符串(默认)用于向后兼容,这意味着在挂载 hostPath 卷之前不会执行任何检查。
DirectoryOrCreate 如果在给定的路径上没有任何东西存在,那么将根据需要在那里创建一个空目录,权限设置为 0755,与 Kubelet 具有相同的组和所有权。
Directory 给定的路径下必须存在目录
FileOrCreate 如果在给定的路径上没有任何东西存在,那么会根据需要创建一个空文件,权限设置为 0644,与 Kubelet 具有相同的组和所有权。
File 给定的路径下必须存在文件
Socket 给定的路径下必须存在 UNIX 套接字
CharDevice 给定的路径下必须存在字符设备
BlockDevice 给定的路径下必须存在块设备

使用这种卷类型是请注意,因为:

  • 由于每个节点上的文件都不同,具有相同配置(例如从 podTemplate 创建的)的 pod 在不同节点上的行为可能会有所不同
  • 当 Kubernetes 按照计划添加资源感知调度时,将无法考虑 hostPath 使用的资源
  • 在底层主机上创建的文件或目录只能由 root 写入。您需要在特权容器中以 root 身份运行进程,或修改主机上的文件权限以便写入 hostPath

Pod 示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
      # this field is optional
      type: Directory

iscsi

iscsi 卷允许将现有的 iSCSI(SCSI over IP)卷挂载到容器中。不像 emptyDir,删除 Pod 时 iscsi 卷的内容将被保留,卷仅仅是被卸载。这意味着 iscsi 卷可以预先填充数据,并且这些数据可以在 pod 之间“切换”。

重要提示:必须先创建自己的 iSCSI 服务器,然后才能使用它。

iSCSI 的一个特点是它可以同时被多个用户以只读方式安装。这意味着您可以预先使用您的数据集填充卷,然后根据需要向多个额 pod 同时提供。不幸的是,iSCSI 卷只能由单个使用者以读写模式挂载——不允许同时写入。

有关更多详细信息,请参见 [iSCSI示例](https://github.com/kubernetes/examples/tree/ master/staging/volumes/iscsi)。

local

这个 alpha 功能要求启用 PersistentLocalVolumes feature gate。

注意:从 1.9 开始,VolumeScheduling feature gate 也必须启用。

local 卷表示挂载的本地存储设备,如磁盘、分区或目录。

本地卷只能用作静态创建的 PersistentVolume。

与 HostPath 卷相比,local 卷可以以持久的方式使用,而无需手动将 pod 调度到节点上,因为系统会通过查看 PersistentVolume 上的节点关联性来了解卷的节点约束。

但是,local 卷仍然受底层节点的可用性影响,并不适用于所有应用程序。

以下是使用 local 卷的示例 PersistentVolume 规范:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
  annotations:
        "volume.alpha.kubernetes.io/node-affinity": '{
            "requiredDuringSchedulingIgnoredDuringExecution": {
                "nodeSelectorTerms": [
                    { "matchExpressions": [
                        { "key": "kubernetes.io/hostname",
                          "operator": "In",
                          "values": ["example-node"]
                        }
                    ]}
                 ]}
              }'
spec:
    capacity:
      storage: 100Gi
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Delete
    storageClassName: local-storage
    local:
      path: /mnt/disks/ssd1

注意:本地 PersistentVolume 清理和删除需要手动干预,无外部提供程序。

从 1.9 开始,本地卷绑定可以被延迟,直到通过具有 StorageClass 中的 WaitForFirstConsumer 设置为volumeBindingMode 的 pod 开始调度。延迟卷绑定可确保卷绑定决策也可以使用任何其他节点约束(例如节点资源需求,节点选择器,pod 亲和性和 pod 反亲和性)进行评估。

有关 local 卷类型的详细信息,请参见本地持久化存储用户指南

nfs

nfs 卷允许将现有的 NFS(网络文件系统)共享挂载到您的容器中。不像 emptyDir,当删除 Pod 时,nfs 卷的内容被保留,卷仅仅是被卸载。这意味着 NFS 卷可以预填充数据,并且可以在 pod 之间“切换”数据。 NFS 可以被多个写入者同时挂载。

重要提示:您必须先拥有自己的 NFS 服务器才能使用它,然后才能使用它。

有关更多详细信息,请参见[NFS示例](https://github.com/kubernetes/examples/tree/ master/staging/volumes/nfs)。

persistentVolumeClaim

persistentVolumeClaim 卷用于将 PersistentVolume 挂载到容器中。PersistentVolumes 是在用户不知道特定云环境的细节的情况下“声明”持久化存储(例如 GCE PersistentDisk 或 iSCSI 卷)的一种方式。

有关更多详细信息,请参阅 PersistentVolumes 示例

projected

projected 卷将几个现有的卷源映射到同一个目录中。

目前,可以映射以下类型的卷来源:

所有来源都必须在与 pod 相同的命名空间中。有关更多详细信息,请参阅 [all-in-one 卷设计文档](https://github.com/kubernetes/community/blob/ master/contributors/design-suggestions/node/all-in-one-volume.md)。

带有 secret、downward API 和 configmap 的 pod

apiVersion: v1
kind: Pod
metadata:
  name: volume-test
spec:
  containers:
  - name: container-test
    image: busybox
    volumeMounts:
    - name: all-in-one
      mountPath: "/projected-volume"
      readOnly: true
  volumes:
  - name: all-in-one
    projected:
      sources:
      - secret:
          name: mysecret
          items:
            - key: username
              path: my-group/my-username
      - downwardAPI:
          items:
            - path: "labels"
              fieldRef:
                fieldPath: metadata.labels
            - path: "cpu_limit"
              resourceFieldRef:
                containerName: container-test
                resource: limits.cpu
      - configMap:
          name: myconfigmap
          items:
            - key: config
              path: my-group/my-config

使用非默认权限模式设置多个 secret 的示例 pod

apiVersion: v1
kind: Pod
metadata:
  name: volume-test
spec:
  containers:
  - name: container-test
    image: busybox
    volumeMounts:
    - name: all-in-one
      mountPath: "/projected-volume"
      readOnly: true
  volumes:
  - name: all-in-one
    projected:
      sources:
      - secret:
          name: mysecret
          items:
            - key: username
              path: my-group/my-username
      - secret:
          name: mysecret2
          items:
            - key: password
              path: my-group/my-password
              mode: 511

每个映射的卷来源在 sources 下的规格中列出。除了以下两个例外,参数几乎相同:

  • 对于 secret,secretName 字段已经被更改为 name 以与 ConfigMap 命名一致。
  • defaultMode 只能在映射级别指定,而不能针对每个卷源指定。但是,如上所述,您可以明确设置每个映射的 mode

portworxVolume

portworxVolume 是一个与 Kubernetes 一起,以超融合模式运行的弹性块存储层。Portwork 指纹存储在服务器中,基于功能的分层,以及跨多个服务器聚合容量。 Portworx 在虚拟机或裸机 Linux 节点上运行。

portworxVolume 可以通过 Kubernetes 动态创建,也可以在 Kubernetes pod 中预先设置和引用。

以下是一个引用预先配置的 PortworxVolume 的示例 pod:

apiVersion: v1
kind: Pod
metadata:
  name: test-portworx-volume-pod
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /mnt
      name: pxvol
  volumes:
  - name: pxvol
    # This Portworx volume must already exist.
    portworxVolume:
      volumeID: "pxvol"
      fsType: "<fs-type>"

重要提示:在 pod 中使用之前,请确保您有一个名为 pxvol 的现有 PortworxVolume。

更多的细节和例子可以在[这里](https://github.com/kubernetes/examples/tree/ master /staging/volumes/portworx/README.md)找到。

quobyte

quobyte 卷允许将现有的 Quobyte 卷挂载到容器中。

重要提示:您必须先创建自己的 Quobyte 安装程序,然后才能使用它。

有关更多详细信息,请参见 [Quobyte示例](https://github.com/kubernetes/examples/tree/ master/staging/volumes/quobyte)。

rbd

rbd 卷允许将 Rados Block Device 卷挂载到容器中。不像 emptyDir,删除 Pod 时 rbd卷的内容被保留,卷仅仅被卸载。这意味着 RBD 卷可以预先填充数据,并且可以在 pod 之间“切换”数据。

重要提示:您必须先自行安装 Ceph,然后才能使用 RBD。

RBD 的一个特点是它可以同时为多个用户以只读方式挂载。这意味着可以预先使用您的数据集填充卷,然后根据需要同时为多个 pod 并行提供。不幸的是,RBD 卷只能由单个用户以读写模式安装——不允许同时写入。

有关更多详细信息,请参阅 [RBD示例](https://github.com/kubernetes/examples/tree/ master/staging/volumes/rbd)。

scaleIO

ScaleIO 是一个基于软件的存储平台,可以使用现有的硬件来创建可扩展的共享块网络存储集群。scaleIO 卷插件允许已部署的 pod 访问现有的 ScaleIO 卷(或者它可以为持久性卷声明动态调配新卷,请参阅 ScaleIO 持久卷)。

重要提示:您必须有一个已经配置好的 ScaleIO 集群,并和创建的卷一同运行,然后才能使用它们。

以下是使用 ScaleIO 的示例 pod 配置:

apiVersion: v1
kind: Pod
metadata:
  name: pod-0
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: pod-0
    volumeMounts:
    - mountPath: /test-pd
      name: vol-0
  volumes:
  - name: vol-0
    scaleIO:
      gateway: https://localhost:443/api
      system: scaleio
      protectionDomain: sd0
      storagePool: sp1
      volumeName: vol-0
      secretRef:
        name: sio-secret
      fsType: xfs

有关更多详细信息,请参阅 ScaleIO 示例

secret

secret 卷用于将敏感信息(如密码)传递到 pod。您可以将 secret 存储在 Kubernetes API 中,并将它们挂载为文件,以供 Pod 使用,而无需直接连接到 Kubernetes。 secret 卷由 tmpfs(一个 RAM 支持的文件系统)支持,所以它们永远不会写入非易失性存储器。

重要提示:您必须先在 Kubernetes API 中创建一个 secret,然后才能使用它。

Secret 在这里被更详细地描述。

storageOS

storageos 卷允许将现有的 StorageOS 卷挂载到容器中。

StorageOS 在 Kubernetes 环境中以容器方式运行,使本地或附加存储可以从 Kubernetes 集群中的任何节点访问。可以复制数据以防止节点故障。精简配置和压缩可以提高利用率并降低成本。

StorageOS 的核心是为容器提供块存储,可以通过文件系统访问。

StorageOS 容器需要 64 位 Linux,没有额外的依赖关系。可以使用免费的开发者许可证。

重要提示:您必须在每个要访问 StorageOS 卷的节点上运行 StorageOS 容器,或者为该池提供存储容量。相关的安装说明,请参阅 StorageOS文档

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: redis
    role: master
  name: test-storageos-redis
spec:
  containers:
    - name: master
      image: kubernetes/redis:v1
      env:
        - name: MASTER
          value: "true"
      ports:
        - containerPort: 6379
      volumeMounts:
        - mountPath: /redis-master-data
          name: redis-data
  volumes:
    - name: redis-data
      storageos:
        # The `redis-vol01` volume must already exist within StorageOS in the `default` namespace.
        volumeName: redis-vol01
        fsType: ext4

有关更多信息,包括动态配置和持久化卷声明,请参阅 StorageOS 示例

vsphereVolume

先决条件:配置了 vSphere Cloud Provider 的 Kubernetes。有关云提供商的配置,请参阅 vSphere 入门指南

vsphereVolume 用于将 vSphere VMDK 卷挂载到 Pod 中。卷的内容在卸载时会被保留。支持 VMFS 和 VSAN 数据存储。

重要提示:在 Pod 中使用它之前,您必须使用以下一种方法创建 VMDK。

创建 VMDK 卷

选择以下方法之一来创建 VMDK。

首先进入 ESX,然后使用以下命令创建一个 VMDK:

vmkfstools -c 2G /vmfs/volumes/DatastoreName/volumes/myDisk.vmdk

使用下列命令创建一个 VMDK:

vmware-vdiskmanager -c -t 0 -s 40GB -a lsilogic myDisk.vmdk

vSphere VMDK 示例配置

apiVersion: v1
kind: Pod
metadata:
  name: test-vmdk
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-vmdk
      name: test-volume
  volumes:
  - name: test-volume
    # This VMDK volume must already exist.
    vsphereVolume:
      volumePath: "[DatastoreName] volumes/myDisk"
      fsType: ext4

更多的例子可以在这里找到。

使用 subPath

有时,在单个容器中共享一个卷用于多个用途是有用的。volumeMounts.subPath 属性可用于在引用的卷内而不是其根目录中指定子路径。

下面是一个使用单个共享卷的 LAMP 堆栈(Linux Apache Mysql PHP)的示例。 HTML 内容被映射到它的 html 目录,数据库将被存储在它的 mysql 目录中:

apiVersion: v1
kind: Pod
metadata:
  name: my-lamp-site
spec:
    containers:
    - name: mysql
      image: mysql
      env:
      - name: MYSQL_ROOT_PASSWORD
        value: "rootpasswd" 
      volumeMounts:
      - mountPath: /var/lib/mysql
        name: site-data
        subPath: mysql
    - name: php
      image: php:7.0-apache
      volumeMounts:
      - mountPath: /var/www/html
        name: site-data
        subPath: html
    volumes:
    - name: site-data
      persistentVolumeClaim:
        claimName: my-lamp-site-data

资源

emptyDir 卷的存储介质(磁盘、SSD 等)由保存在 kubelet 根目录的文件系统的介质(通常是 /var/lib/kubelet)决定。 emptyDirhostPath 卷可占用多少空间并没有限制,容器之间或 Pod 之间也没有隔离。

在将来,我们预计 emptyDirhostPath 卷将能够使用 resource 规范请求一定的空间,并选择要使用的介质,适用于具有多种媒体类型的集群。

Out-of-Tree 卷插件

除了之前列出的卷类型之外,存储供应商可以创建自定义插件而不将其添加到 Kubernetes 存储库中。可以通过使用 FlexVolume 插件来实现。

FlexVolume使用户能够将供应商卷挂载到容器中。供应商插件是使用驱动程序实现的,该驱动程序支持由 FlexVolume API定义的一系列卷命令。驱动程序必须安装在每个节点的预定义卷插件路径中。

更多细节可以在这里找到。

挂载传播

注意:挂载传播是 Kubernetes 1.8 中的一个 alpha 特性,在将来的版本中可能会重新设计甚至删除。

挂载传播允许将由容器挂载的卷共享到同一个 Pod 中的其他容器上,甚至是同一节点上的其他 Pod。

如果禁用 MountPropagation 功能,则不会传播 pod 中的卷挂载。也就是说,容器按照 Linux内核文档中所述的 private 挂载传播运行。

要启用此功能,请在 --feature-gates 命令行选项中指定 MountPropagation = true。启用时,容器的 volumeMounts 字段有一个新的 mountPropagation 子字段。它的值为:

  • HostToContainer:此卷挂载将接收所有后续挂载到此卷或其任何子目录的挂载。这是 MountPropagation 功能启用时的默认模式。

    同样的,如果任何带有 Bidirectional 挂载传播的 pod 挂载到同一个卷上,带有 HostToContainer 挂载传播的容器将会看到它。

    该模式等同于Linux内核文档中描述的 rslave 挂载传播。

  • Bidirectional 卷挂载与 HostToContainer 挂载相同。另外,由容器创建的所有卷挂载将被传播回主机和所有使用相同卷的容器的所有容器。

    此模式的一个典型用例是带有 Flex 卷驱动器或需要使用 HostPath 卷在主机上挂载某些内容的 pod。

    该模式等同于 Linux内核文档中所述的 rshared 挂载传播。

小心:双向挂载传播可能是危险的。它可能会损坏主机操作系统,因此只能在特权容器中使用。强烈建议熟悉 Linux 内核行为。另外,容器在 Pod 中创建的任何卷挂载必须在容器终止时销毁(卸载)。

Persistent Volume(持久化卷)

介绍

对于管理计算资源来说,管理存储资源明显是另一个问题。PersistentVolume 子系统为用户和管理员提供了一个 API,该 API 将如何提供存储的细节抽象了出来。为此,我们引入两个新的 API 资源:PersistentVolumePersistentVolumeClaim

PersistentVolume(PV)是由管理员设置的存储,它是群集的一部分。就像节点是集群中的资源一样,PV 也是集群中的资源。 PV 是 Volume 之类的卷插件,但具有独立于使用 PV 的 Pod 的生命周期。此 API 对象包含存储实现的细节,即 NFS、iSCSI 或特定于云供应商的存储系统。

PersistentVolumeClaim(PVC)是用户存储的请求。它与 Pod 相似。Pod 消耗节点资源,PVC 消耗 PV 资源。Pod 可以请求特定级别的资源(CPU 和内存)。声明可以请求特定的大小和访问模式(例如,可以以读/写一次或 只读多次模式挂载)。

虽然 PersistentVolumeClaims 允许用户使用抽象存储资源,但用户需要具有不同性质(例如性能)的 PersistentVolume 来解决不同的问题。集群管理员需要能够提供各种各样的 PersistentVolume,这些PersistentVolume 的大小和访问模式可以各有不同,但不需要向用户公开实现这些卷的细节。对于这些需求,StorageClass 资源可以实现。

请参阅工作示例的详细过程

卷和声明的生命周期

PV 属于集群中的资源。PVC 是对这些资源的请求,也作为对资源的请求的检查。 PV 和 PVC 之间的相互作用遵循这样的生命周期:

配置(Provision)

有两种方式来配置 PV:静态或动态。

静态

集群管理员创建一些 PV。它们带有可供群集用户使用的实际存储的细节。它们存在于 Kubernetes API 中,可用于消费。

动态

当管理员创建的静态 PV 都不匹配用户的 PersistentVolumeClaim 时,集群可能会尝试动态地为 PVC 创建卷。此配置基于 StorageClasses:PVC 必须请求存储类,并且管理员必须创建并配置该类才能进行动态创建。声明该类为 "" 可以有效地禁用其动态配置。

要启用基于存储级别的动态存储配置,集群管理员需要启用 API server 上的 DefaultStorageClass 准入控制器。例如,通过确保 DefaultStorageClass 位于 API server 组件的 --admission-control 标志,使用逗号分隔的有序值列表中,可以完成此操作。有关 API server 命令行标志的更多信息,请检查 kube-apiserver 文档。

绑定

再动态配置的情况下,用户创建或已经创建了具有特定存储量的 PersistentVolumeClaim 以及某些访问模式。master 中的控制环路监视新的 PVC,寻找匹配的 PV(如果可能),并将它们绑定在一起。如果为新的 PVC 动态调配 PV,则该环路将始终将该 PV 绑定到 PVC。否则,用户总会得到他们所请求的存储,但是容量可能超出要求的数量。一旦 PV 和 PVC 绑定后,PersistentVolumeClaim 绑定是排他性的,不管它们是如何绑定的。 PVC 跟 PV 绑定是一对一的映射。

如果没有匹配的卷,声明将无限期地保持未绑定状态。随着匹配卷的可用,声明将被绑定。例如,配置了许多 50Gi PV的集群将不会匹配请求 100Gi 的PVC。将100Gi PV 添加到群集时,可以绑定 PVC。

使用

Pod 使用声明作为卷。集群检查声明以查找绑定的卷并为集群挂载该卷。对于支持多种访问模式的卷,用户指定在使用声明作为容器中的卷时所需的模式。

用户进行了声明,并且该声明是绑定的,则只要用户需要,绑定的 PV 就属于该用户。用户通过在 Pod 的 volume 配置中包含 persistentVolumeClaim 来调度 Pod 并访问用户声明的 PV。

持久化卷声明的保护

PVC 保护的目的是确保由 pod 正在使用的 PVC 不会从系统中移除,因为如果被移除的话可能会导致数据丢失。

注意:当 pod 状态为 Pending 并且 pod 已经分配给节点或 pod 为 Running 状态时,PVC 处于活动状态。

当启用PVC 保护 alpha 功能时,如果用户删除了一个 pod 正在使用的 PVC,则该 PVC 不会被立即删除。PVC 的删除将被推迟,直到 PVC 不再被任何 pod 使用。

您可以看到,当 PVC 的状态为 Teminatiing 时,PVC 受到保护,Finalizers 列表中包含 kubernetes.io/pvc-protection

kubectl described pvc hostpath
Name:          hostpath
Namespace:     default
StorageClass:  example-hostpath
Status:        Terminating
Volume:        
Labels:        <none>
Annotations:   volume.beta.kubernetes.io/storage-class=example-hostpath
               volume.beta.kubernetes.io/storage-provisioner=example.com/hostpath
Finalizers:    [kubernetes.io/pvc-protection]
...

回收

用户用完 volume 后,可以从允许回收资源的 API 中删除 PVC 对象。PersistentVolume 的回收策略告诉集群在存储卷声明释放后应如何处理该卷。目前,volume 的处理策略有保留、回收或删除。

保留

保留回收策略允许手动回收资源。当 PersistentVolumeClaim 被删除时,PersistentVolume 仍然存在,volume 被视为“已释放”。但是由于前一个声明人的数据仍然存在,所以还不能马上进行其他声明。管理员可以通过以下步骤手动回收卷。

  1. 删除 PersistentVolume。在删除 PV 后,外部基础架构中的关联存储资产(如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)仍然存在。
  2. 手动清理相关存储资产上的数据。
  3. 手动删除关联的存储资产,或者如果要重新使用相同的存储资产,请使用存储资产定义创建新的 PersistentVolume

回收

如果存储卷插件支持,回收策略会在 volume上执行基本擦除(rm -rf / thevolume / *),可被再次声明使用。

但是,管理员可以使用如此处所述的 Kubernetes controller manager 命令行参数来配置自定义回收站 pod 模板。自定义回收站 pod 模板必须包含 volumes 规范,如下面的示例所示:

apiVersion: v1
kind: Pod
metadata:
  name: pv-recycler
  namespace: default
spec:
  restartPolicy: Never
  volumes:
  - name: vol
    hostPath:
      path: /any/path/it/will/be/replaced
  containers:
  - name: pv-recycler
    image: "k8s.gcr.io/busybox"
    command: ["/bin/sh", "-c", "test -e /scrub && rm -rf /scrub/..?* /scrub/.[!.]* /scrub/*  && test -z \"$(ls -A /scrub)\" || exit 1"]
    volumeMounts:
    - name: vol
      mountPath: /scrub

但是,volumes 部分的自定义回收站模块中指定的特定路径将被替换为正在回收的卷的特定路径。

删除

对于支持删除回收策略的卷插件,删除操作将从 Kubernetes 中删除 PersistentVolume 对象,并删除外部基础架构(如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)中的关联存储资产。动态配置的卷继承其 StorageClass 的回收策略,默认为 Delete。管理员应该根据用户的期望来配置 StorageClass,否则就必须要在 PV 创建后进行编辑或修补。请参阅更改 PersistentVolume 的回收策略

扩展持久化卷声明

Kubernetes 1.8 增加了对扩展持久化存储卷的 Alpha 支持。在 v1.9 中,以下持久化卷支持扩展持久化卷声明:

  • gcePersistentDisk
  • awsElasticBlockStore
  • Cinder
  • glusterfs
  • rbd

管理员可以通过将 ExpandPersistentVolumes 特性门设置为true来允许扩展持久卷声明。管理员还应该启用PersistentVolumeClaimResize 准入控制插件来执行对可调整大小的卷的其他验证。

一旦 PersistentVolumeClaimResize 准入插件已打开,将只允许其 allowVolumeExpansion 字段设置为 true 的存储类进行大小调整。

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gluster-vol-default
provisioner: kubernetes.io/glusterfs
parameters:
  resturl: "http://192.168.10.100:8080"
  restuser: ""
  secretNamespace: ""
  secretName: ""
allowVolumeExpansion: true

一旦功能门和前述准入插件打开后,用户就可以通过简单地编辑声明以请求更大的 PersistentVolumeClaim 卷。这反过来将触发 PersistentVolume 后端的卷扩展。

在任何情况下都不会创建新的 PersistentVolume 来满足声明。 Kubernetes 将尝试调整现有 volume 来满足声明的要求。

对于扩展包含文件系统的卷,只有在 ReadWrite 模式下使用 PersistentVolumeClaim 启动新的 Pod 时,才会执行文件系统调整大小。换句话说,如果正在扩展的卷在 pod 或部署中使用,则需要删除并重新创建要进行文件系统调整大小的pod。此外,文件系统调整大小仅适用于以下文件系统类型:

  • XFS
  • Ext3、Ext4

注意:扩展 EBS 卷是一个耗时的操作。另外,每6个小时有一个修改卷的配额。

持久化卷类型

PersistentVolume 类型以插件形式实现。Kubernetes 目前支持以下插件类型:

  • GCEPersistentDisk
  • AWSElasticBlockStore
  • AzureFile
  • AzureDisk
  • FC (Fibre Channel)
  • FlexVolume
  • Flocker
  • NFS
  • iSCSI
  • RBD (Ceph Block Device)
  • CephFS
  • Cinder (OpenStack block storage)
  • Glusterfs
  • VsphereVolume
  • Quobyte Volumes
  • HostPath (仅限于但节点测试—— 不会以任何方式支持本地存储,也无法在多节点集群中工作)
  • VMware Photon
  • Portworx Volumes
  • ScaleIO Volumes
  • StorageOS

原始块支持仅适用于以上这些插件。

持久化卷

每个 PV 配置中都包含一个 sepc 规格字段和一个 status 卷状态字段。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv0003
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: slow
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    path: /tmp
    server: 172.17.0.2

容量

通常,PV 将具有特定的存储容量。这是使用 PV 的容量属性设置的。查看 Kubernetes 资源模型 以了解 capacity 预期。

目前,存储大小是可以设置或请求的唯一资源。未来的属性可能包括 IOPS、吞吐量等。

卷模式

在 v1.9 之前,所有卷插件的默认行为是在持久卷上创建一个文件系统。在 v1.9 中,用户可以指定一个 volumeMode,除了文件系统之外,它现在将支持原始块设备。 volumeMode 的有效值可以是“Filesystem”或“Block”。如果未指定,volumeMode 将默认为“Filesystem”。这是一个可选的 API 参数。

注意:该功能在 V1.9 中是 alpha的,未来可能会更改。

访问模式

PersistentVolume 可以以资源提供者支持的任何方式挂载到主机上。如下表所示,供应商具有不同的功能,每个 PV 的访问模式都将被设置为该卷支持的特定模式。例如,NFS 可以支持多个读/写客户端,但特定的 NFS PV 可能以只读方式导出到服务器上。每个 PV 都有一套自己的用来描述特定功能的访问模式。

存储模式包括:

  • ReadWriteOnce——该卷可以被单个节点以读/写模式挂载
  • ReadOnlyMany——该卷可以被多个节点以只读模式挂载
  • ReadWriteMany——该卷可以被多个节点以读/写模式挂载

在命令行中,访问模式缩写为:

  • RWO - ReadWriteOnce
  • ROX - ReadOnlyMany
  • RWX - ReadWriteMany

重要!一个卷一次只能使用一种访问模式挂载,即使它支持很多访问模式。例如,GCEPersistentDisk 可以由单个节点作为 ReadWriteOnce 模式挂载,或由多个节点以 ReadOnlyMany 模式挂载,但不能同时挂载。

Volume 插件 ReadWriteOnce ReadOnlyMany ReadWriteMany
AWSElasticBlockStore - -
AzureFile
AzureDisk - -
CephFS
Cinder - -
FC -
FlexVolume -
Flocker - -
GCEPersistentDisk -
Glusterfs
HostPath - -
iSCSI -
PhotonPersistentDisk - -
Quobyte
NFS
RBD -
VsphereVolume - - (当 pod 并列时有效)
PortworxVolume -
ScaleIO -
StorageOS - -

PV 可以具有一个类,通过将 storageClassName 属性设置为 StorageClass 的名称来指定该类。一个特定类别的 PV 只能绑定到请求该类别的 PVC。没有 storageClassName 的 PV 就没有类,它只能绑定到不需要特定类的 PVC。

过去,使用的是 volume.beta.kubernetes.io/storage-class 注解而不是 storageClassName 属性。这个注解仍然有效,但是将来的 Kubernetes 版本中将会完全弃用它。

回收策略

当前的回收策略包括:

  • Retain(保留)——手动回收
  • Recycle(回收)——基本擦除(rm -rf /thevolume/*
  • Delete(删除)——关联的存储资产(例如 AWS EBS、GCE PD、Azure Disk 和 OpenStack Cinder 卷)将被删除

当前,只有 NFS 和 HostPath 支持回收策略。AWS EBS、GCE PD、Azure Disk 和 Cinder 卷支持删除策略。

挂载选项

Kubernetes 管理员可以指定在节点上为挂载持久卷指定挂载选项。

注意:不是所有的持久化卷类型都支持挂载选项。

以下卷类型支持挂载选项:

  • GCEPersistentDisk
  • AWSElasticBlockStore
  • AzureFile
  • AzureDisk
  • NFS
  • iSCSI
  • RBD (Ceph Block Device)
  • CephFS
  • Cinder (OpenStack 卷存储)
  • Glusterfs
  • VsphereVolume
  • Quobyte Volumes
  • VMware Photon

挂载选项没有校验,如果挂载选项无效则挂载失败。

过去,使用 volume.beta.kubernetes.io/mount-options 注解而不是 mountOptions 属性。这个注解仍然有效,但在将来的 Kubernetes 版本中它将会被完全弃用。

状态

卷可以处于以下的某种状态:

  • Available(可用)——一块空闲资源还没有被任何声明绑定
  • Bound(已绑定)——卷已经被声明绑定
  • Released(已释放)——声明被删除,但是资源还未被集群重新声明
  • Failed(失败)——该卷的自动回收失败

命令行会显示绑定到 PV 的 PVC 的名称。

PersistentVolumeClaim

每个 PVC 中都包含一个 spec 规格字段和一个 status 声明状态字段。

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 8Gi
  storageClassName: slow
  selector:
    matchLabels:
      release: "stable"
    matchExpressions:
      - {key: environment, operator: In, values: [dev]}

访问模式

在请求具有特定访问模式的存储时,声明使用与卷相同的约定。

卷模式

声明使用与卷相同的约定,指示将卷作为文件系统或块设备使用。

资源

像 pod 一样,声明可以请求特定数量的资源。在这种情况下,请求是用于存储的。相同的资源模型适用于卷和声明。

选择器

声明可以指定一个标签选择器来进一步过滤该组卷。只有标签与选择器匹配的卷可以绑定到声明。选择器由两个字段组成:

  • matchLabels:volume 必须有具有该值的标签
  • matchExpressions:这是一个要求列表,通过指定关键字,值列表以及与关键字和值相关的运算符组成。有效的运算符包括 In、NotIn、Exists 和 DoesNotExist。

所有来自 matchLabelsmatchExpressions 的要求都被“与”在一起——它们必须全部满足才能匹配。

声明可以通过使用属性 storageClassName 指定 StorageClass 的名称来请求特定的类。只有所请求的类与 PVC 具有相同 storageClassName 的 PV 才能绑定到 PVC。

PVC 不一定要请求类。其 storageClassName 设置为 "" 的 PVC 始终被解释为没有请求类的 PV,因此只能绑定到没有类的 PV(没有注解或 "")。没有 storageClassName 的 PVC 根据是否打开DefaultStorageClass 准入控制插件,集群对其进行不同处理。

  • 如果打开了准入控制插件,管理员可以指定一个默认的 StorageClass。所有没有 StorageClassName 的 PVC 将被绑定到该默认的 PV。通过在 StorageClass 对象中将注解 storageclassclass.ubernetes.io/is-default-class 设置为 “true” 来指定默认的 StorageClass。如果管理员没有指定缺省值,那么集群会响应 PVC 创建,就好像关闭了准入控制插件一样。如果指定了多个默认值,则准入控制插件将禁止所有 PVC 创建。
  • 如果准入控制插件被关闭,则没有默认 StorageClass 的概念。所有没有 storageClassName 的 PVC 只能绑定到没有类的 PV。在这种情况下,没有 storageClassName 的 PVC 的处理方式与 storageClassName 设置为 "" 的 PVC 的处理方式相同。

根据安装方法的不同,默认的 StorageClass 可以在安装过程中通过插件管理器部署到 Kubernetes 集群。

当 PVC 指定了 selector,除了请求一个 StorageClass 之外,这些需求被“与”在一起:只有被请求的类的 PV 具有和被请求的标签才可以被绑定到 PVC。

注意:目前,具有非空 selector 的 PVC 不能为其动态配置 PV。

过去,使用注解 volume.beta.kubernetes.io/storage-class 而不是 storageClassName 属性。这个注解仍然有效,但是在未来的 Kubernetes 版本中不会支持。

声明作为卷

通过将声明用作卷来访问存储。声明必须与使用声明的 pod 存在于相同的命名空间中。集群在 pod 的命名空间中查找声明,并使用它来获取支持声明的 PersistentVolume。该卷然后被挂载到主机的 pod 上。

kind: Pod
apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
    - name: myfrontend
      image: dockerfile/nginx
      volumeMounts:
      - mountPath: "/var/www/html"
        name: mypd
  volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim

命名空间注意点

PersistentVolumes 绑定是唯一的,并且由于 PersistentVolumeClaims 是命名空间对象,因此只能在一个命名空间内挂载具有“多个”模式(ROXRWX)的声明。

原始块卷支持

原始块卷的静态配置在 v1.9 中作为 alpha 功能引入。由于这个改变,需要一些新的 API 字段来使用该功能。目前,Fibre Channl 是支持该功能的唯一插件。

使用原始块卷作为持久化卷

apiVersion: v1
kind: PersistentVolume
metadata:
  name: block-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Block
  persistentVolumeReclaimPolicy: Retain
  fc:
    targetWWNs: ["50060e801049cfd1"]
    lun: 0
    readOnly: false

持久化卷声明请求原始块卷

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: block-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Block
  resources:
    requests:
      storage: 10Gi

在 Pod 规格配置中为容器添加原始块设备

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-block-volume
spec:
  containers:
    - name: fc-container
      image: fedora:26
      command: ["/bin/sh", "-c"]
      args: [ "tail -f /dev/null" ]
      volumeDevices:
        - name: data
          devicePath: /dev/xvda
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: block-pvc

注意:当为 Pod 增加原始块设备时,我们在容器中指定设备路径而不是挂载路径。

绑定块卷

如果用户通过使用 PersistentVolumeClaim 规范中的 volumeMode 字段指示此请求来请求原始块卷,则绑定规则与以前不认为该模式为规范一部分的版本略有不同。

下面是用户和管理员指定请求原始块设备的可能组合的表格。该表指示卷是否将被绑定或未给定组合。静态设置的卷的卷绑定矩阵:

PV volumeMode PVC volumeMode 结果
unspecified unspecified 绑定
unspecified Block 不绑定
unspecified Filesystem 绑定
Block unspecified 不绑定
Block Block 绑定
Block Filesystem 不绑定
Filesystem Filesystem 绑定
Filesystem Block 不绑定
Filesystem unspecified 绑定

注意:alpha 版本只支持静态配置卷。使用原始块设备时,管理员应该注意考虑这些值。

编写可移植配置

如果您正在编写在多种群集上运行并需要持久存储的配置模板或示例,我们建议您使用以下模式:

  • 不要在您的在配置组合中包含 PersistentVolumeClaim 对象(与 Deployment、ConfigMap等一起)。
  • 不要在配置中包含 PersistentVolume 对象,因为用户实例化配置可能没有创建 PersistentVolume 的权限。
  • 给用户在实例化模板时提供存储类名称的选项。
    • 如果用户提供存储类名称,则将该值放入 persistentVolumeClaim.storageClassName 字段中。如果集群具有由管理员启用的 StorageClass,这将导致 PVC 匹配正确的存储类别。
    • 如果用户未提供存储类名称,则将persistentVolumeClaim.storageClassName字段保留为 nil。
      • 这将导致使用集群中默认的 StorageClass 为用户自动配置 PV。许多集群环境都有默认的 StorageClass,或者管理员可以创建自己的默认 StorageClass。
  • 在您的工具中,请注意一段时间之后仍未绑定的 PVC,并向用户展示它们,因为这表示集群可能没有动态存储支持(在这种情况下用户应创建匹配的 PV),或集群没有存储系统(在这种情况下用户不能部署需要 PVC 的配置)。

Deployment

简述

Deployment 为 Pod 和 ReplicaSet 提供了一个声明式定义(declarative)方法,用来替代以前的ReplicationController 来方便的管理应用。典型的应用场景包括:

  • 定义Deployment来创建Pod和ReplicaSet
  • 滚动升级和回滚应用
  • 扩容和缩容
  • 暂停和继续Deployment

比如一个简单的nginx应用可以定义为

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

扩容:

kubectl scale deployment nginx-deployment --replicas 10

如果集群支持 horizontal pod autoscaling 的话,还可以为Deployment设置自动扩展:

kubectl autoscale deployment nginx-deployment --min=10 --max=15 --cpu-percent=80

更新镜像也比较简单:

kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1

回滚:

kubectl rollout undo deployment/nginx-deployment

Deployment 结构示意图

参考:https://kubernetes.io/docs/api-reference/v1.6/#deploymentspec-v1beta1-apps

Deployment 概念详细解析

本文翻译自kubernetes官方文档:https://kubernetes.io/docs/concepts/workloads/controllers/deployment.md

根据2017年5月10日的Commit 8481c02 翻译。

Deployment 是什么?

Deployment为Pod和Replica Set(下一代Replication Controller)提供声明式更新。

您只需要在 Deployment 中描述您想要的目标状态是什么,Deployment controller 就会帮您将 Pod 和ReplicaSet 的实际状态改变到您的目标状态。您可以定义一个全新的 Deployment 来创建 ReplicaSet 或者删除已有的 Deployment 并创建一个新的来替换。

注意:您不该手动管理由 Deployment 创建的 ReplicaSet,否则您就篡越了 Deployment controller 的职责!下文罗列了 Deployment 对象中已经覆盖了所有的用例。如果未有覆盖您所有需要的用例,请直接在 Kubernetes 的代码库中提 issue。

典型的用例如下:

  • 使用Deployment来创建ReplicaSet。ReplicaSet在后台创建pod。检查启动状态,看它是成功还是失败。
  • 然后,通过更新Deployment的PodTemplateSpec字段来声明Pod的新状态。这会创建一个新的ReplicaSet,Deployment会按照控制的速率将pod从旧的ReplicaSet移动到新的ReplicaSet中。
  • 如果当前状态不稳定,回滚到之前的Deployment revision。每次回滚都会更新Deployment的revision。
  • 扩容Deployment以满足更高的负载。
  • 暂停Deployment来应用PodTemplateSpec的多个修复,然后恢复上线。
  • 根据Deployment 的状态判断上线是否hang住了。
  • 清除旧的不必要的 ReplicaSet。

创建 Deployment

下面是一个 Deployment 示例,它创建了一个 ReplicaSet 来启动3个 nginx pod。

下载示例文件并执行命令:

$ kubectl create -f https://kubernetes.io/docs/user-guide/nginx-deployment.yaml --record
deployment "nginx-deployment" created

将kubectl的 --record 的 flag 设置为 true可以在 annotation 中记录当前命令创建或者升级了该资源。这在未来会很有用,例如,查看在每个 Deployment revision 中执行了哪些命令。

然后立即执行 get 将获得如下结果:

$ kubectl get deployments
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         0         0            0           1s

输出结果表明我们希望的repalica数是3(根据deployment中的.spec.replicas配置)当前replica数( .status.replicas)是0, 最新的replica数(.status.updatedReplicas)是0,可用的replica数(.status.availableReplicas)是0。

过几秒后再执行get命令,将获得如下输出:

$ kubectl get deployments
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         3         3            3           18s

我们可以看到Deployment已经创建了3个 replica,所有的 replica 都已经是最新的了(包含最新的pod template),可用的(根据Deployment中的.spec.minReadySeconds声明,处于已就绪状态的pod的最少个数)。执行kubectl get rskubectl get pods会显示Replica Set(RS)和Pod已创建。

$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-2035384211   3         3         0       18s

您可能会注意到 ReplicaSet 的名字总是<Deployment的名字>-<pod template的hash值>

$ kubectl get pods --show-labels
NAME                                READY     STATUS    RESTARTS   AGE       LABELS
nginx-deployment-2035384211-7ci7o   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211
nginx-deployment-2035384211-kzszj   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211
nginx-deployment-2035384211-qqcnn   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211

刚创建的Replica Set将保证总是有3个 nginx 的 pod 存在。

注意: 您必须在 Deployment 中的 selector 指定正确的 pod template label(在该示例中是 app = nginx),不要跟其他的 controller 的 selector 中指定的 pod template label 搞混了(包括 Deployment、Replica Set、Replication Controller 等)。Kubernetes 本身并不会阻止您任意指定 pod template label ,但是如果您真的这么做了,这些 controller 之间会相互打架,并可能导致不正确的行为。

Pod-template-hash label

注意:这个 label 不是用户指定的!

注意上面示例输出中的 pod label 里的 pod-template-hash label。当 Deployment 创建或者接管 ReplicaSet 时,Deployment controller 会自动为 Pod 添加 pod-template-hash label。这样做的目的是防止 Deployment 的子ReplicaSet 的 pod 名字重复。通过将 ReplicaSet 的 PodTemplate 进行哈希散列,使用生成的哈希值作为 label 的值,并添加到 ReplicaSet selector 里、 pod template label 和 ReplicaSet 管理中的 Pod 上。

更新Deployment

注意: Deployment 的 rollout 当且仅当 Deployment 的 pod template(例如.spec.template)中的label更新或者镜像更改时被触发。其他更新,例如扩容Deployment不会触发 rollout。

假如我们现在想要让 nginx pod 使用nginx:1.9.1的镜像来代替原来的nginx:1.7.9的镜像。

$ kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
deployment "nginx-deployment" image updated

我们可以使用edit命令来编辑 Deployment,修改 .spec.template.spec.containers[0].image ,将nginx:1.7.9 改写成 nginx:1.9.1

$ kubectl edit deployment/nginx-deployment
deployment "nginx-deployment" edited

查看 rollout 的状态,只要执行:

$ kubectl rollout status deployment/nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment "nginx-deployment" successfully rolled out

Rollout 成功后,get Deployment:

$ kubectl get deployments
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         3         3            3           36s

UP-TO-DATE 的 replica 的数目已经达到了配置中要求的数目。

CURRENT 的 replica 数表示 Deployment 管理的 replica 数量,AVAILABLE 的 replica 数是当前可用的replica数量。

我们通过执行kubectl get rs可以看到 Deployment 更新了Pod,通过创建一个新的 ReplicaSet 并扩容了3个 replica,同时将原来的 ReplicaSet 缩容到了0个 replica。

$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-1564180365   3         3         0       6s
nginx-deployment-2035384211   0         0         0       36s

执行 get pods只会看到当前的新的 pod:

$ kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
nginx-deployment-1564180365-khku8   1/1       Running   0          14s
nginx-deployment-1564180365-nacti   1/1       Running   0          14s
nginx-deployment-1564180365-z9gth   1/1       Running   0          14s

下次更新这些 pod 的时候,只需要更新 Deployment 中的 pod 的 template 即可。

Deployment 可以保证在升级时只有一定数量的 Pod 是 down 的。默认的,它会确保至少有比期望的Pod数量少一个是up状态(最多一个不可用)。

Deployment 同时也可以确保只创建出超过期望数量的一定数量的 Pod。默认的,它会确保最多比期望的Pod数量多一个的 Pod 是 up 的(最多1个 surge )。

在未来的 Kuberentes 版本中,将从1-1变成25%-25%。

例如,如果您自己看下上面的 Deployment,您会发现,开始创建一个新的 Pod,然后删除一些旧的 Pod 再创建一个新的。当新的Pod创建出来之前不会杀掉旧的Pod。这样能够确保可用的 Pod 数量至少有2个,Pod的总数最多4个。

$ kubectl describe deployments
Name:           nginx-deployment
Namespace:      default
CreationTimestamp:  Tue, 15 Mar 2016 12:01:06 -0700
Labels:         app=nginx
Selector:       app=nginx
Replicas:       3 updated | 3 total | 3 available | 0 unavailable
StrategyType:       RollingUpdate
MinReadySeconds:    0
RollingUpdateStrategy:  1 max unavailable, 1 max surge
OldReplicaSets:     <none>
NewReplicaSet:      nginx-deployment-1564180365 (3/3 replicas created)
Events:
  FirstSeen LastSeen    Count   From                     SubobjectPath   Type        Reason              Message
  --------- --------    -----   ----                     -------------   --------    ------              -------
  36s       36s         1       {deployment-controller }                 Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-2035384211 to 3
  23s       23s         1       {deployment-controller }                 Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 1
  23s       23s         1       {deployment-controller }                 Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-2035384211 to 2
  23s       23s         1       {deployment-controller }                 Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 2
  21s       21s         1       {deployment-controller }                 Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-2035384211 to 0
  21s       21s         1       {deployment-controller }                 Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 3

我们可以看到当我们刚开始创建这个 Deployment 的时候,创建了一个 ReplicaSet(nginx-deployment-2035384211),并直接扩容到了3个 replica。

当我们更新这个 Deployment 的时候,它会创建一个新的 ReplicaSet(nginx-deployment-1564180365),将它扩容到1个replica,然后缩容原先的 ReplicaSet 到2个 replica,此时满足至少2个 Pod 是可用状态,同一时刻最多有4个 Pod 处于创建的状态。

接着继续使用相同的 rolling update 策略扩容新的 ReplicaSet 和缩容旧的 ReplicaSet。最终,将会在新的 ReplicaSet 中有3个可用的 replica,旧的 ReplicaSet 的 replica 数目变成0。

Rollover(多个rollout并行)

每当 Deployment controller 观测到有新的 deployment 被创建时,如果没有已存在的 ReplicaSet 来创建期望个数的 Pod 的话,就会创建出一个新的 ReplicaSet 来做这件事。已存在的 ReplicaSet 控制 label 与.spec.selector匹配但是 template 跟.spec.template不匹配的 Pod 缩容。最终,新的 ReplicaSet 将会扩容出.spec.replicas指定数目的 Pod,旧的 ReplicaSet 会缩容到0。

如果您更新了一个的已存在并正在进行中的 Deployment,每次更新 Deployment都会创建一个新的 ReplicaSet并扩容它,同时回滚之前扩容的 ReplicaSet ——将它添加到旧的 ReplicaSet 列表中,开始缩容。

例如,假如您创建了一个有5个niginx:1.7.9 replica的 Deployment,但是当还只有3个nginx:1.7.9的 replica 创建出来的时候您就开始更新含有5个nginx:1.9.1 replica 的 Deployment。在这种情况下,Deployment 会立即杀掉已创建的3个nginx:1.7.9的 Pod,并开始创建nginx:1.9.1的 Pod。它不会等到所有的5个nginx:1.7.9的 Pod 都创建完成后才开始改变航道。

Label selector 更新

我们通常不鼓励更新 label selector,我们建议事先规划好您的 selector。

任何情况下,只要您想要执行 label selector 的更新,请一定要谨慎并确认您已经预料到所有可能因此导致的后果。

  • 增添 selector 需要同时在 Deployment 的 spec 中更新新的 label,否则将返回校验错误。此更改是不可覆盖的,这意味着新的 selector 不会选择使用旧 selector 创建的 ReplicaSet 和 Pod,从而导致所有旧版本的 ReplicaSet 都被丢弃,并创建新的 ReplicaSet。
  • 更新 selector,即更改 selector key 的当前值,将导致跟增添 selector 同样的后果。
  • 删除 selector,即删除 Deployment selector 中的已有的 key,不需要对 Pod template label 做任何更改,现有的 ReplicaSet 也不会成为孤儿,但是请注意,删除的 label 仍然存在于现有的 Pod 和 ReplicaSet 中。

回退Deployment

有时候您可能想回退一个 Deployment,例如,当 Deployment 不稳定时,比如一直 crash looping。

默认情况下,kubernetes 会在系统中保存前两次的 Deployment 的 rollout 历史记录,以便您可以随时回退(您可以修改revision history limit来更改保存的revision数)。

注意: 只要 Deployment 的 rollout 被触发就会创建一个 revision。也就是说当且仅当 Deployment 的 Pod template(如.spec.template)被更改,例如更新template 中的 label 和容器镜像时,就会创建出一个新的 revision。

其他的更新,比如扩容 Deployment 不会创建 revision——因此我们可以很方便的手动或者自动扩容。这意味着当您回退到历史 revision 是,直有 Deployment 中的 Pod template 部分才会回退。

假设我们在更新 Deployment 的时候犯了一个拼写错误,将镜像的名字写成了nginx:1.91,而正确的名字应该是nginx:1.9.1

$ kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment "nginx-deployment" image updated

Rollout 将会卡住。

$ kubectl rollout status deployments nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...

按住 Ctrl-C 停止上面的 rollout 状态监控。

您会看到旧的 replica(nginx-deployment-1564180365 和 nginx-deployment-2035384211)和新的 replica (nginx-deployment-3066724191)数目都是2个。

$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-1564180365   2         2         0       25s
nginx-deployment-2035384211   0         0         0       36s
nginx-deployment-3066724191   2         2         2       6s

看下创建 Pod,您会看到有两个新的 ReplicaSet 创建的 Pod 处于 ImagePullBackOff 状态,循环拉取镜像。

$ kubectl get pods
NAME                                READY     STATUS             RESTARTS   AGE
nginx-deployment-1564180365-70iae   1/1       Running            0          25s
nginx-deployment-1564180365-jbqqo   1/1       Running            0          25s
nginx-deployment-3066724191-08mng   0/1       ImagePullBackOff   0          6s
nginx-deployment-3066724191-eocby   0/1       ImagePullBackOff   0          6s

注意,Deployment controller会自动停止坏的 rollout,并停止扩容新的 ReplicaSet。

$ kubectl describe deployment
Name:           nginx-deployment
Namespace:      default
CreationTimestamp:  Tue, 15 Mar 2016 14:48:04 -0700
Labels:         app=nginx
Selector:       app=nginx
Replicas:       2 updated | 3 total | 2 available | 2 unavailable
StrategyType:       RollingUpdate
MinReadySeconds:    0
RollingUpdateStrategy:  1 max unavailable, 1 max surge
OldReplicaSets:     nginx-deployment-1564180365 (2/2 replicas created)
NewReplicaSet:      nginx-deployment-3066724191 (2/2 replicas created)
Events:
  FirstSeen LastSeen    Count   From                    SubobjectPath   Type        Reason              Message
  --------- --------    -----   ----                    -------------   --------    ------              -------
  1m        1m          1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-2035384211 to 3
  22s       22s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 1
  22s       22s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-2035384211 to 2
  22s       22s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 2
  21s       21s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-2035384211 to 0
  21s       21s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 3
  13s       13s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-3066724191 to 1
  13s       13s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-1564180365 to 2
  13s       13s         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-3066724191 to 2

为了修复这个问题,我们需要回退到稳定的 Deployment revision。

检查 Deployment 升级的历史记录

首先,检查下 Deployment 的 revision:

$ kubectl rollout history deployment/nginx-deployment
deployments "nginx-deployment":
REVISION    CHANGE-CAUSE
1           kubectl create -f https://kubernetes.io/docs/user-guide/nginx-deployment.yaml--record
2           kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
3           kubectl set image deployment/nginx-deployment nginx=nginx:1.91

因为我们创建 Deployment 的时候使用了--record参数可以记录命令,我们可以很方便的查看每次 revision 的变化。

查看单个revision 的详细信息:

$ kubectl rollout history deployment/nginx-deployment --revision=2
deployments "nginx-deployment" revision 2
  Labels:       app=nginx
          pod-template-hash=1159050644
  Annotations:  kubernetes.io/change-cause=kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
  Containers:
   nginx:
    Image:      nginx:1.9.1
    Port:       80/TCP
     QoS Tier:
        cpu:      BestEffort
        memory:   BestEffort
    Environment Variables:      <none>
  No volumes.

回退到历史版本

现在,我们可以决定回退当前的 rollout 到之前的版本:

$ kubectl rollout undo deployment/nginx-deployment
deployment "nginx-deployment" rolled back

也可以使用 --revision参数指定某个历史版本:

$ kubectl rollout undo deployment/nginx-deployment --to-revision=2
deployment "nginx-deployment" rolled back

与 rollout 相关的命令详细文档见kubectl rollout

该 Deployment 现在已经回退到了先前的稳定版本。如您所见,Deployment controller产生了一个回退到revison 2的DeploymentRollback的 event。

$ kubectl get deployment
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         3         3            3           30m
$ kubectl describe deployment
Name:           nginx-deployment
Namespace:      default
CreationTimestamp:  Tue, 15 Mar 2016 14:48:04 -0700
Labels:         app=nginx
Selector:       app=nginx
Replicas:       3 updated | 3 total | 3 available | 0 unavailable
StrategyType:       RollingUpdate
MinReadySeconds:    0
RollingUpdateStrategy:  1 max unavailable, 1 max surge
OldReplicaSets:     <none>
NewReplicaSet:      nginx-deployment-1564180365 (3/3 replicas created)
Events:
  FirstSeen LastSeen    Count   From                    SubobjectPath   Type        Reason              Message
  --------- --------    -----   ----                    -------------   --------    ------              -------
  30m       30m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-2035384211 to 3
  29m       29m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 1
  29m       29m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-2035384211 to 2
  29m       29m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 2
  29m       29m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-2035384211 to 0
  29m       29m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-3066724191 to 2
  29m       29m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-3066724191 to 1
  29m       29m         1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-1564180365 to 2
  2m        2m          1       {deployment-controller }                Normal      ScalingReplicaSet   Scaled down replica set nginx-deployment-3066724191 to 0
  2m        2m          1       {deployment-controller }                Normal      DeploymentRollback  Rolled back deployment "nginx-deployment" to revision 2
  29m       2m          2       {deployment-controller }                Normal      ScalingReplicaSet   Scaled up replica set nginx-deployment-1564180365 to 3

清理 Policy

您可以通过设置.spec.revisonHistoryLimit项来指定 deployment 最多保留多少 revision 历史记录。默认的会保留所有的 revision;如果将该项设置为0,Deployment就不允许回退了。

Deployment 扩容

您可以使用以下命令扩容 Deployment:

$ kubectl scale deployment nginx-deployment --replicas 10
deployment "nginx-deployment" scaled

假设您的集群中启用了horizontal pod autoscaling,您可以给 Deployment 设置一个 autoscaler,基于当前 Pod的 CPU 利用率选择最少和最多的 Pod 数。

$ kubectl autoscale deployment nginx-deployment --min=10 --max=15 --cpu-percent=80
deployment "nginx-deployment" autoscaled

比例扩容

RollingUpdate Deployment 支持同时运行一个应用的多个版本。或者 autoscaler 扩 容 RollingUpdate Deployment 的时候,正在中途的 rollout(进行中或者已经暂停的),为了降低风险,Deployment controller 将会平衡已存在的活动中的 ReplicaSet(有 Pod 的 ReplicaSet)和新加入的 replica。这被称为比例扩容。

例如,您正在运行中含有10个 replica 的 Deployment。maxSurge=3,maxUnavailable=2。

$ kubectl get deploy
NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment     10        10        10           10          50s

您更新了一个镜像,而在集群内部无法解析。

$ kubectl set image deploy/nginx-deployment nginx=nginx:sometag
deployment "nginx-deployment" image updated

镜像更新启动了一个包含ReplicaSet nginx-deployment-1989198191的新的rollout,但是它被阻塞了,因为我们上面提到的maxUnavailable。

$ kubectl get rs
NAME                          DESIRED   CURRENT   READY     AGE
nginx-deployment-1989198191   5         5         0         9s
nginx-deployment-618515232    8         8         8         1m

然后发起了一个新的Deployment扩容请求。autoscaler将Deployment的repllica数目增加到了15个。Deployment controller需要判断在哪里增加这5个新的replica。如果我们没有谁用比例扩容,所有的5个replica都会加到一个新的ReplicaSet中。如果使用比例扩容,新添加的replica将传播到所有的ReplicaSet中。大的部分加入replica数最多的ReplicaSet中,小的部分加入到replica数少的ReplciaSet中。0个replica的ReplicaSet不会被扩容。

在我们上面的例子中,3个replica将添加到旧的ReplicaSet中,2个replica将添加到新的ReplicaSet中。rollout进程最终会将所有的replica移动到新的ReplicaSet中,假设新的replica成为健康状态。

$ kubectl get deploy
NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment     15        18        7            8           7m
$ kubectl get rs
NAME                          DESIRED   CURRENT   READY     AGE
nginx-deployment-1989198191   7         7         0         7m
nginx-deployment-618515232    11        11        11        7m

暂停和恢复Deployment

您可以在发出一次或多次更新前暂停一个 Deployment,然后再恢复它。这样您就能多次暂停和恢复 Deployment,在此期间进行一些修复工作,而不会发出不必要的 rollout。

例如使用刚刚创建 Deployment:

$ kubectl get deploy
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx     3         3         3            3           1m
[mkargaki@dhcp129-211 kubernetes]$ kubectl get rs
NAME               DESIRED   CURRENT   READY     AGE
nginx-2142116321   3         3         3         1m

使用以下命令暂停 Deployment:

$ kubectl rollout pause deployment/nginx-deployment
deployment "nginx-deployment" paused

然后更新 Deplyment中的镜像:

$ kubectl set image deploy/nginx nginx=nginx:1.9.1
deployment "nginx-deployment" image updated

注意新的 rollout 启动了:

$ kubectl rollout history deploy/nginx
deployments "nginx"
REVISION  CHANGE-CAUSE
1   <none>
$ kubectl get rs
NAME               DESIRED   CURRENT   READY     AGE
nginx-2142116321   3         3         3         2m

您可以进行任意多次更新,例如更新使用的资源:

$ kubectl set resources deployment nginx -c=nginx --limits=cpu=200m,memory=512Mi
deployment "nginx" resource requirements updated

Deployment 暂停前的初始状态将继续它的功能,而不会对 Deployment 的更新产生任何影响,只要 Deployment是暂停的。

最后,恢复这个 Deployment,观察完成更新的 ReplicaSet 已经创建出来了:

$ kubectl rollout resume deploy nginx
deployment "nginx" resumed
$ KUBECTL get rs -w
NAME               DESIRED   CURRENT   READY     AGE
nginx-2142116321   2         2         2         2m
nginx-3926361531   2         2         0         6s
nginx-3926361531   2         2         1         18s
nginx-2142116321   1         2         2         2m
nginx-2142116321   1         2         2         2m
nginx-3926361531   3         2         1         18s
nginx-3926361531   3         2         1         18s
nginx-2142116321   1         1         1         2m
nginx-3926361531   3         3         1         18s
nginx-3926361531   3         3         2         19s
nginx-2142116321   0         1         1         2m
nginx-2142116321   0         1         1         2m
nginx-2142116321   0         0         0         2m
nginx-3926361531   3         3         3         20s
^C
$ KUBECTL get rs
NAME               DESIRED   CURRENT   READY     AGE
nginx-2142116321   0         0         0         2m
nginx-3926361531   3         3         3         28s

注意: 在恢复 Deployment 之前您无法回退一个已经暂停的 Deployment。

Deployment 状态

Deployment 在生命周期中有多种状态。在创建一个新的 ReplicaSet 的时候它可以是 progressing 状态, complete 状态,或者 fail to progress 状态。

进行中的 Deployment

Kubernetes 将执行过下列任务之一的 Deployment 标记为 progressing 状态:

  • Deployment 正在创建新的ReplicaSet过程中。
  • Deployment 正在扩容一个已有的 ReplicaSet。
  • Deployment 正在缩容一个已有的 ReplicaSet。
  • 有新的可用的 pod 出现。

您可以使用kubectl rollout status命令监控 Deployment 的进度。

完成的 Deployment

Kubernetes 将包括以下特性的 Deployment 标记为 complete 状态:

  • Deployment 最小可用。最小可用意味着 Deployment 的可用 replica 个数等于或者超过 Deployment 策略中的期望个数。
  • 所有与该 Deployment 相关的replica都被更新到了您指定版本,也就说更新完成。
  • 该 Deployment 中没有旧的 Pod 存在。

您可以用kubectl rollout status命令查看 Deployment 是否完成。如果 rollout 成功完成,kubectl rollout status将返回一个0值的 Exit Code。

$ kubectl rollout status deploy/nginx
Waiting for rollout to finish: 2 of 3 updated replicas are available...
deployment "nginx" successfully rolled out
$ echo $?
0

失败的 Deployment

您的 Deployment 在尝试部署新的 ReplicaSet 的时候可能卡住,用于也不会完成。这可能是因为以下几个因素引起的:

  • 无效的引用
  • 不可读的 probe failure
  • 镜像拉取错误
  • 权限不够
  • 范围限制
  • 程序运行时配置错误

探测这种情况的一种方式是,在您的 Deployment spec 中指定spec.progressDeadlineSecondsspec.progressDeadlineSeconds 表示 Deployment controller 等待多少秒才能确定(通过 Deployment status)Deployment进程是卡住的。

下面的kubectl命令设置progressDeadlineSeconds 使 controller 在 Deployment 在进度卡住10分钟后报告:

$ kubectl patch deployment/nginx-deployment -p '{"spec":{"progressDeadlineSeconds":600}}'
"nginx-deployment" patched

当超过截止时间后,Deployment controller 会在 Deployment 的 status.conditions中增加一条DeploymentCondition,它包括如下属性:

  • Type=Progressing
  • Status=False
  • Reason=ProgressDeadlineExceeded

浏览 Kubernetes API conventions 查看关于status conditions的更多信息。

注意: kubernetes除了报告Reason=ProgressDeadlineExceeded状态信息外不会对卡住的 Deployment 做任何操作。更高层次的协调器可以利用它并采取相应行动,例如,回滚 Deployment 到之前的版本。

注意: 如果您暂停了一个 Deployment,在暂停的这段时间内kubernetnes不会检查您指定的 deadline。您可以在 Deployment 的 rollout 途中安全的暂停它,然后再恢复它,这不会触发超过deadline的状态。

您可能在使用 Deployment 的时候遇到一些短暂的错误,这些可能是由于您设置了太短的 timeout,也有可能是因为各种其他错误导致的短暂错误。例如,假设您使用了无效的引用。当您 Describe Deployment 的时候可能会注意到如下信息:

$ kubectl describe deployment nginx-deployment
<...>
Conditions:
  Type            Status  Reason
  ----            ------  ------
  Available       True    MinimumReplicasAvailable
  Progressing     True    ReplicaSetUpdated
  ReplicaFailure  True    FailedCreate
<...>

执行 kubectl get deployment nginx-deployment -o yaml,Deployement 的状态可能看起来像这个样子:

status:
  availableReplicas: 2
  conditions:
  - lastTransitionTime: 2016-10-04T12:25:39Z
    lastUpdateTime: 2016-10-04T12:25:39Z
    message: Replica set "nginx-deployment-4262182780" is progressing.
    reason: ReplicaSetUpdated
    status: "True"
    type: Progressing
  - lastTransitionTime: 2016-10-04T12:25:42Z
    lastUpdateTime: 2016-10-04T12:25:42Z
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  - lastTransitionTime: 2016-10-04T12:25:39Z
    lastUpdateTime: 2016-10-04T12:25:39Z
    message: 'Error creating: pods "nginx-deployment-4262182780-" is forbidden: exceeded quota:
      object-counts, requested: pods=1, used: pods=3, limited: pods=2'
    reason: FailedCreate
    status: "True"
    type: ReplicaFailure
  observedGeneration: 3
  replicas: 2
  unavailableReplicas: 2

最终,一旦超过 Deployment 进程的 deadline,kuberentes 会更新状态和导致 Progressing 状态的原因:

Conditions:
  Type            Status  Reason
  ----            ------  ------
  Available       True    MinimumReplicasAvailable
  Progressing     False   ProgressDeadlineExceeded
  ReplicaFailure  True    FailedCreate

您可以通过缩容 Deployment的方式解决配额不足的问题,或者增加您的 namespace 的配额。如果您满足了配额条件后,Deployment controller 就会完成您的 Deployment rollout,您将看到 Deployment 的状态更新为成功状态(Status=True并且Reason=NewReplicaSetAvailable)。

Conditions:
  Type          Status  Reason
  ----          ------  ------
  Available     True    MinimumReplicasAvailable
  Progressing   True    NewReplicaSetAvailable

Type=AvailableStatus=True 以为这您的Deployment有最小可用性。 最小可用性是在Deployment策略中指定的参数。Type=ProgressingStatus=True意味着您的Deployment 或者在部署过程中,或者已经成功部署,达到了期望的最少的可用replica数量(查看特定状态的Reason——在我们的例子中Reason=NewReplicaSetAvailable 意味着Deployment已经完成)。

您可以使用kubectl rollout status命令查看Deployment进程是否失败。当Deployment过程超过了deadline,kubectl rollout status将返回非0的exit code。

$ kubectl rollout status deploy/nginx
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
error: deployment "nginx" exceeded its progress deadline
$ echo $?
1

操作失败的 Deployment

所有对完成的 Deployment 的操作都适用于失败的 Deployment。您可以对它扩/缩容,回退到历史版本,您甚至可以多次暂停它来应用 Deployment pod template。

清理Policy

您可以设置 Deployment 中的 .spec.revisionHistoryLimit 项来指定保留多少旧的 ReplicaSet。 余下的将在后台被当作垃圾收集。默认的,所有的 revision 历史就都会被保留。在未来的版本中,将会更改为2。

注意: 将该值设置为0,将导致所有的 Deployment 历史记录都会被清除,该 Deployment 就无法再回退了。

用例

金丝雀 Deployment

如果您想要使用 Deployment 对部分用户或服务器发布 release,您可以创建多个 Deployment,每个 Deployment 对应一个 release,参照 managing resources 中对金丝雀模式的描述。

编写 Deployment Spec

在所有的 Kubernetes 配置中,Deployment 也需要apiVersionkindmetadata这些配置项。配置文件的通用使用说明查看 部署应用,配置容器,和 使用 kubectl 管理资源 文档。

Deployment也需要 .spec section.

Pod Template

.spec.template.spec中唯一要求的字段。

.spec.templatepod template. 它跟 Pod有一模一样的schema,除了它是嵌套的并且不需要apiVersionkind字段。

另外为了划分Pod的范围,Deployment中的pod template必须指定适当的label(不要跟其他controller重复了,参考selector)和适当的重启策略。

.spec.template.spec.restartPolicy 可以设置为 Always , 如果不指定的话这就是默认配置。

Replicas

.spec.replicas 是可以选字段,指定期望的pod数量,默认是1。

Selector

.spec.selector是可选字段,用来指定 label selector ,圈定Deployment管理的pod范围。

如果被指定, .spec.selector 必须匹配 .spec.template.metadata.labels,否则它将被API拒绝。如果 .spec.selector 没有被指定, .spec.selector.matchLabels 默认是 .spec.template.metadata.labels

在Pod的template跟.spec.template不同或者数量超过了.spec.replicas规定的数量的情况下,Deployment会杀掉label跟selector不同的Pod。

注意: 您不应该再创建其他label跟这个selector匹配的pod,或者通过其他Deployment,或者通过其他Controller,例如ReplicaSet和ReplicationController。否则该Deployment会被把它们当成都是自己创建的。Kubernetes不会阻止您这么做。

如果您有多个controller使用了重复的selector,controller们就会互相打架并导致不正确的行为。

策略

.spec.strategy 指定新的Pod替换旧的Pod的策略。 .spec.strategy.type 可以是”Recreate”或者是 “RollingUpdate”。”RollingUpdate”是默认值。

Recreate Deployment

.spec.strategy.type==Recreate时,在创建出新的Pod之前会先杀掉所有已存在的Pod。

Rolling Update Deployment

.spec.strategy.type==RollingUpdate时,Deployment使用rolling update 的方式更新Pod 。您可以指定maxUnavailablemaxSurge 来控制 rolling update 进程。

Max Unavailable

.spec.strategy.rollingUpdate.maxUnavailable 是可选配置项,用来指定在升级过程中不可用Pod的最大数量。该值可以是一个绝对值(例如5),也可以是期望Pod数量的百分比(例如10%)。通过计算百分比的绝对值向下取整。如果.spec.strategy.rollingUpdate.maxSurge 为0时,这个值不可以为0。默认值是1。

例如,该值设置成30%,启动rolling update后旧的ReplicatSet将会立即缩容到期望的Pod数量的70%。新的Pod ready后,随着新的ReplicaSet的扩容,旧的ReplicaSet会进一步缩容,确保在升级的所有时刻可以用的Pod数量至少是期望Pod数量的70%。

Max Surge

.spec.strategy.rollingUpdate.maxSurge 是可选配置项,用来指定可以超过期望的Pod数量的最大个数。该值可以是一个绝对值(例如5)或者是期望的Pod数量的百分比(例如10%)。当MaxUnavailable为0时该值不可以为0。通过百分比计算的绝对值向上取整。默认值是1。

例如,该值设置成30%,启动rolling update后新的ReplicatSet将会立即扩容,新老Pod的总数不能超过期望的Pod数量的130%。旧的Pod被杀掉后,新的ReplicaSet将继续扩容,旧的ReplicaSet会进一步缩容,确保在升级的所有时刻所有的Pod数量和不会超过期望Pod数量的130%。

Progress Deadline Seconds

.spec.progressDeadlineSeconds 是可选配置项,用来指定在系统报告Deployment的failed progressing ——表现为resource的状态中type=ProgressingStatus=FalseReason=ProgressDeadlineExceeded前可以等待的Deployment进行的秒数。Deployment controller会继续重试该Deployment。未来,在实现了自动回滚后, deployment controller在观察到这种状态时就会自动回滚。

如果设置该参数,该值必须大于 .spec.minReadySeconds

Min Ready Seconds

.spec.minReadySeconds是一个可选配置项,用来指定没有任何容器crash的Pod并被认为是可用状态的最小秒数。默认是0(Pod在ready后就会被认为是可用状态)。进一步了解什么什么后Pod会被认为是ready状态,参阅 Container Probes

Rollback To

.spec.rollbackTo 是一个可以选配置项,用来配置Deployment回退的配置。设置该参数将触发回退操作,每次回退完成后,该值就会被清除。

Revision

.spec.rollbackTo.revision是一个可选配置项,用来指定回退到的revision。默认是0,意味着回退到上一个revision。

Revision History Limit

Deployment revision history存储在它控制的ReplicaSets中。

.spec.revisionHistoryLimit 是一个可选配置项,用来指定可以保留的旧的ReplicaSet数量。该理想值取决于心Deployment的频率和稳定性。如果该值没有设置的话,默认所有旧的Replicaset或会被保留,将资源存储在etcd中,是用kubectl get rs查看输出。每个Deployment的该配置都保存在ReplicaSet中,然而,一旦您删除的旧的RepelicaSet,您的Deployment就无法再回退到那个revison了。

如果您将该值设置为0,所有具有0个replica的ReplicaSet都会被删除。在这种情况下,新的Deployment rollout无法撤销,因为revision history都被清理掉了。

Paused

.spec.paused是可以可选配置项,boolean值。用来指定暂停和恢复Deployment。Paused和没有paused的Deployment之间的唯一区别就是,所有对paused deployment中的PodTemplateSpec的修改都不会触发新的rollout。Deployment被创建之后默认是非paused。

Deployment 的替代选择

kubectl rolling update

Kubectl rolling update 虽然使用类似的方式更新Pod和ReplicationController。但是我们推荐使用Deployment,因为它是声明式的,客户端侧,具有附加特性,例如即使滚动升级结束后也可以回滚到任何历史版本。

Secret

Secret解决了密码、token、密钥等敏感数据的配置问题,而不需要把这些敏感数据暴露到镜像或者Pod Spec中。Secret可以以Volume或者环境变量的方式使用。

Secret有三种类型:

  • Service Account :用来访问Kubernetes API,由Kubernetes自动创建,并且会自动挂载到Pod的/run/secrets/kubernetes.io/serviceaccount目录中;
  • Opaque :base64编码格式的Secret,用来存储密码、密钥等;
  • kubernetes.io/dockerconfigjson :用来存储私有docker registry的认证信息。

Opaque Secret

Opaque类型的数据是一个map类型,要求value是base64编码格式:

$ echo -n "admin" | base64
YWRtaW4=
$ echo -n "1f2d1e2e67df" | base64
MWYyZDFlMmU2N2Rm

secrets.yml

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  password: MWYyZDFlMmU2N2Rm
  username: YWRtaW4=

接着,就可以创建secret了:kubectl create -f secrets.yml

创建好secret之后,有两种方式来使用它:

  • 以Volume方式
  • 以环境变量方式

将Secret挂载到Volume中

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: db
  name: db
spec:
  volumes:
  - name: secrets
    secret:
      secretName: mysecret
  containers:
  - image: gcr.io/my_project_id/pg:v1
    name: db
    volumeMounts:
    - name: secrets
      mountPath: "/etc/secrets"
      readOnly: true
    ports:
    - name: cp
      containerPort: 5432
      hostPort: 5432

将Secret导出到环境变量中

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: wordpress-deployment
spec:
  replicas: 2
  strategy:
      type: RollingUpdate
  template:
    metadata:
      labels:
        app: wordpress
        visualize: "true"
    spec:
      containers:
      - name: "wordpress"
        image: "wordpress"
        ports:
        - containerPort: 80
        env:
        - name: WORDPRESS_DB_USER
          valueFrom:
            secretKeyRef:
              name: mysecret
              key: username
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysecret
              key: password

kubernetes.io/dockerconfigjson

可以直接用kubectl命令来创建用于docker registry认证的secret:

$ kubectl create secret docker-registry myregistrykey --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
secret "myregistrykey" created.

也可以直接读取~/.docker/config.json的内容来创建:

$ cat ~/.docker/config.json | base64
$ cat > myregistrykey.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: myregistrykey
data:
  .dockerconfigjson: UmVhbGx5IHJlYWxseSByZWVlZWVlZWVlZWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx5eXl5eXl5eXl5eXl5eXl5eXl5eSBsbGxsbGxsbGxsbGxsbG9vb29vb29vb29vb29vb29vb29vb29vb29vb25ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubmdnZ2dnZ2dnZ2dnZ2dnZ2dnZ2cgYXV0aCBrZXlzCg==
type: kubernetes.io/dockerconfigjson
EOF
$ kubectl create -f myregistrykey.yaml

在创建Pod的时候,通过imagePullSecrets来引用刚创建的myregistrykey:

apiVersion: v1
kind: Pod
metadata:
  name: foo
spec:
  containers:
    - name: foo
      image: janedoe/awesomeapp:v1
  imagePullSecrets:
    - name: myregistrykey

Service Account

Service Account用来访问Kubernetes API,由Kubernetes自动创建,并且会自动挂载到Pod的/run/secrets/kubernetes.io/serviceaccount目录中。

$ kubectl run nginx --image nginx
deployment "nginx" created
$ kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
nginx-3137573019-md1u2   1/1       Running   0          13s
$ kubectl exec nginx-3137573019-md1u2 ls /run/secrets/kubernetes.io/serviceaccount
ca.crt
namespace
token

StatefulSet

StatefulSet 作为 Controller 为 Pod 提供唯一的标识。它可以保证部署和 scale 的顺序。

使用案例参考:kubernetes contrib - statefulsets,其中包含zookeeper和kakfa的statefulset设置和使用说明。

StatefulSet是为了解决有状态服务的问题(对应Deployments和ReplicaSets是为无状态服务而设计),其应用场景包括:

  • 稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现
  • 稳定的网络标志,即Pod重新调度后其PodName和HostName不变,基于Headless Service(即没有Cluster IP的Service)来实现
  • 有序部署,有序扩展,即Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次依次进行(即从0到N-1,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态),基于init containers来实现
  • 有序收缩,有序删除(即从N-1到0)

从上面的应用场景可以发现,StatefulSet由以下几个部分组成:

  • 用于定义网络标志(DNS domain)的Headless Service
  • 用于创建PersistentVolumes的volumeClaimTemplates
  • 定义具体应用的StatefulSet

StatefulSet中每个Pod的DNS格式为statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local,其中

  • serviceName为Headless Service的名字
  • 0..N-1为Pod所在的序号,从0开始到N-1
  • statefulSetName为StatefulSet的名字
  • namespace为服务所在的namespace,Headless Servic和StatefulSet必须在相同的namespace
  • .cluster.local为Cluster Domain

使用 StatefulSet

StatefulSet 适用于有以下某个或多个需求的应用:

  • 稳定,唯一的网络标志。
  • 稳定,持久化存储。
  • 有序,优雅地部署和 scale。
  • 有序,优雅地删除和终止。
  • 有序,自动的滚动升级。

在上文中,稳定是 Pod (重新)调度中持久性的代名词。 如果应用程序不需要任何稳定的标识符、有序部署、删除和 scale,则应该使用提供一组无状态副本的 controller 来部署应用程序,例如 DeploymentReplicaSet 可能更适合您的无状态需求。

限制

  • StatefulSet 是 beta 资源,Kubernetes 1.5 以前版本不支持。
  • 对于所有的 alpha/beta 的资源,您都可以通过在 apiserver 中设置 --runtime-config 选项来禁用。
  • 给定 Pod 的存储必须由 PersistentVolume Provisioner 根据请求的 storage class 进行配置,或由管理员预先配置。
  • 删除或 scale StatefulSet 将不会删除与 StatefulSet 相关联的 volume。 这样做是为了确保数据安全性,这通常比自动清除所有相关 StatefulSet 资源更有价值。
  • StatefulSets 目前要求 Headless Service 负责 Pod 的网络身份。 您有责任创建此服务。

组件

下面的示例中描述了 StatefulSet 中的组件。

  • 一个名为 nginx 的 headless service,用于控制网络域。
  • 一个名为 web 的 StatefulSet,它的 Spec 中指定在有 3 个运行 nginx 容器的 Pod。
  • volumeClaimTemplates 使用 PersistentVolume Provisioner 提供的 PersistentVolumes 作为稳定存储。
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: gcr.io/google_containers/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
      annotations:
        volume.beta.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

Pod 身份

StatefulSet Pod 具有唯一的身份,包括序数,稳定的网络身份和稳定的存储。 身份绑定到 Pod 上,不管它(重新)调度到哪个节点上。

序数

对于一个有 N 个副本的 StatefulSet,每个副本都会被指定一个整数序数,在 [0,N)之间,且唯一。

稳定的网络 ID

StatefulSet 中的每个 Pod 从 StatefulSet 的名称和 Pod 的序数派生其主机名。构造的主机名的模式是$(statefulset名称)-$(序数)。 上面的例子将创建三个名为web-0,web-1,web-2的 Pod。

StatefulSet 可以使用 Headless Service 来控制其 Pod 的域。此服务管理的域的格式为:$(服务名称).$(namespace).svc.cluster.local,其中 “cluster.local” 是 集群域

在创建每个Pod时,它将获取一个匹配的 DNS 子域,采用以下形式:$(pod 名称).$(管理服务域),其中管理服务由 StatefulSet 上的 serviceName 字段定义。

以下是 Cluster Domain,服务名称,StatefulSet 名称以及如何影响 StatefulSet 的 Pod 的 DNS 名称的一些示例。

Cluster Domain Service (ns/name) StatefulSet (ns/name) StatefulSet Domain Pod DNS Pod Hostname
cluster.local default/nginx default/web nginx.default.svc.cluster.local web-{0..N-1}.nginx.default.svc.cluster.local web-{0..N-1}
cluster.local foo/nginx foo/web nginx.foo.svc.cluster.local web-{0..N-1}.nginx.foo.svc.cluster.local web-{0..N-1}
kube.local foo/nginx foo/web nginx.foo.svc.kube.local web-{0..N-1}.nginx.foo.svc.kube.local web-{0..N-1}

注意 Cluster Domain 将被设置成 cluster.local 除非进行了 其他配置

稳定存储

Kubernetes 为每个 VolumeClaimTemplate 创建一个 PersistentVolume。上面的 nginx 的例子中,每个 Pod 将具有一个由 anything 存储类创建的 1 GB 存储的 PersistentVolume。当该 Pod (重新)调度到节点上,volumeMounts 将挂载与 PersistentVolume Claim 相关联的 PersistentVolume。请注意,与 PersistentVolume Claim 相关联的 PersistentVolume 在 产出 Pod 或 StatefulSet 的时候不会被删除。这必须手动完成。

部署和 Scale 保证

  • 对于有 N 个副本的 StatefulSet,Pod 将按照 {0..N-1} 的顺序被创建和部署。
  • 当 删除 Pod 的时候,将按照逆序来终结,从{N-1..0}
  • 对 Pod 执行 scale 操作之前,它所有的前任必须处于 Running 和 Ready 状态。
  • 在终止 Pod 前,它所有的继任者必须处于完全关闭状态。

不应该将 StatefulSet 的 pod.Spec.TerminationGracePeriodSeconds 设置为 0。这样是不安全的且强烈不建议您这样做。进一步解释,请参阅 强制删除 StatefulSet Pod

上面的 nginx 示例创建后,3 个 Pod 将按照如下顺序创建 web-0,web-1,web-2。在 web-0 处于 运行并就绪 状态之前,web-1 将不会被部署,同样当 web-1 处于运行并就绪状态之前 web-2也不会被部署。如果在 web-1 运行并就绪后,web-2 启动之前, web-0 失败了,web-2 将不会启动,直到 web-0 成果重启并处于运行并就绪状态。

如果用户通过修补 StatefulSet 来 scale 部署的示例,以使 replicas=1,则 web-2 将首先被终止。 在 web-2 完全关闭和删除之前,web-1 不会被终止。 如果 web-0 在 web-2 终止并且完全关闭之后,但是在 web-1 终止之前失败,则 web-1 将不会终止,除非 web-0 正在运行并准备就绪。

Pod 管理策略

在 Kubernetes 1.7 和之后版本,StatefulSet 允许您放开顺序保证,同时通过 .spec.podManagementPolicy 字段保证身份的唯一性。

OrderedReady Pod 管理

StatefulSet 中默认使用的是 OrderedReady pod 管理。它实现了 如上 所述的行为。

并行 Pod 管理

Parallel pod 管理告诉 StatefulSet controller 并行的启动和终止 Pod,在启动和终止其他 Pod 之前不会等待 Pod 变成 运行并就绪或完全终止状态。

更新策略

在 kubernetes 1.7 和以上版本中,StatefulSet 的 .spec.updateStrategy 字段允许您配置和禁用 StatefulSet 中的容器、label、resource request/limit、annotation 的滚动更新。

删除

OnDelete 更新策略实现了遗留(1.6和以前)的行为。 当 spec.updateStrategy 未指定时,这是默认策略。 当StatefulSet 的 .spec.updateStrategy.type 设置为 OnDelete 时,StatefulSet 控制器将不会自动更新 StatefulSet 中的 Pod。 用户必须手动删除 Pod 以使控制器创建新的 Pod,以反映对StatefulSet的 .spec.template 进行的修改。

滚动更新

RollingUpdate 更新策略在 StatefulSet 中实现 Pod 的自动滚动更新。 当StatefulSet的 .spec.updateStrategy.type 设置为 RollingUpdate 时,StatefulSet 控制器将在 StatefulSet 中删除并重新创建每个 Pod。 它将以与 Pod 终止相同的顺序进行(从最大的序数到最小的序数),每次更新一个 Pod。 在更新其前身之前,它将等待正在更新的 Pod 状态变成正在运行并就绪。

分区

可以通过指定 .spec.updateStrategy.rollingUpdate.partition 来对 RollingUpdate 更新策略进行分区。如果指定了分区,则当 StatefulSet 的 .spec.template 更新时,具有大于或等于分区序数的所有 Pod 将被更新。具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas,则其 .spec.template 的更新将不会传播到 Pod。

在大多数情况下,您不需要使用分区,但如果您想要进行分阶段更新,使用金丝雀发布或执行分阶段发布,它们将非常有用。

简单示例

以一个简单的nginx服务web.yaml为例:

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: gcr.io/google_containers/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
$ kubectl create -f web.yaml
service "nginx" created
statefulset "web" created
# 查看创建的headless service和statefulset
$ kubectl get service nginx
NAME      CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx     None         <none>        80/TCP    1m
$ kubectl get statefulset web
NAME      DESIRED   CURRENT   AGE
web       2         2         2m
# 根据volumeClaimTemplates自动创建PVC(在GCE中会自动创建kubernetes.io/gce-pd类型的volume)
$ kubectl get pvc
NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-d064a004-d8d4-11e6-b521-42010a800002   1Gi        RWO           16s
www-web-1   Bound     pvc-d06a3946-d8d4-11e6-b521-42010a800002   1Gi        RWO           16s
# 查看创建的Pod,他们都是有序的
$ kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          5m
web-1     1/1       Running   0          4m
# 使用nslookup查看这些Pod的DNS
$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
/ # nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name:      web-0.nginx
Address 1: 10.244.2.10
/ # nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name:      web-1.nginx
Address 1: 10.244.3.12
/ # nslookup web-0.nginx.default.svc.cluster.local
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name:      web-0.nginx.default.svc.cluster.local
Address 1: 10.244.2.10

还可以进行其他的操作

# 扩容
$ kubectl scale statefulset web --replicas=5
# 缩容
$ kubectl patch statefulset web -p '{"spec":{"replicas":3}}'
# 镜像更新(目前还不支持直接更新image,需要patch来间接实现)
$ kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.7"}]'
# 删除StatefulSet和Headless Service
$ kubectl delete statefulset web
$ kubectl delete service nginx
# StatefulSet删除后PVC还会保留着,数据不再使用的话也需要删除
$ kubectl delete pvc www-web-0 www-web-1

zookeeper

另外一个更能说明StatefulSet强大功能的示例为zookeeper.yaml,这个例子仅为讲解,实际可用的配置请使用 https://github.com/kubernetes/contrib/tree/master/statefulsets 中的配置。

---
apiVersion: v1
kind: Service
metadata:
  name: zk-headless
  labels:
    app: zk-headless
spec:
  ports:
  - port: 2888
    name: server
  - port: 3888
    name: leader-election
  clusterIP: None
  selector:
    app: zk
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: zk-config
data:
  ensemble: "zk-0;zk-1;zk-2"
  jvm.heap: "2G"
  tick: "2000"
  init: "10"
  sync: "5"
  client.cnxns: "60"
  snap.retain: "3"
  purge.interval: "1"
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zk-budget
spec:
  selector:
    matchLabels:
      app: zk
  minAvailable: 2
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: zk
spec:
  serviceName: zk-headless
  replicas: 3
  template:
    metadata:
      labels:
        app: zk
      annotations:
        pod.alpha.kubernetes.io/initialized: "true"
        scheduler.alpha.kubernetes.io/affinity: >
            {
              "podAntiAffinity": {
                "requiredDuringSchedulingRequiredDuringExecution": [{
                  "labelSelector": {
                    "matchExpressions": [{
                      "key": "app",
                      "operator": "In",
                      "values": ["zk-headless"]
                    }]
                  },
                  "topologyKey": "kubernetes.io/hostname"
                }]
              }
            }
    spec:
      containers:
      - name: k8szk
        imagePullPolicy: Always
        image: gcr.io/google_samples/k8szk:v1
        resources:
          requests:
            memory: "4Gi"
            cpu: "1"
        ports:
        - containerPort: 2181
          name: client
        - containerPort: 2888
          name: server
        - containerPort: 3888
          name: leader-election
        env:
        - name : ZK_ENSEMBLE
          valueFrom:
            configMapKeyRef:
              name: zk-config
              key: ensemble
        - name : ZK_HEAP_SIZE
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: jvm.heap
        - name : ZK_TICK_TIME
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: tick
        - name : ZK_INIT_LIMIT
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: init
        - name : ZK_SYNC_LIMIT
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: tick
        - name : ZK_MAX_CLIENT_CNXNS
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: client.cnxns
        - name: ZK_SNAP_RETAIN_COUNT
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: snap.retain
        - name: ZK_PURGE_INTERVAL
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: purge.interval
        - name: ZK_CLIENT_PORT
          value: "2181"
        - name: ZK_SERVER_PORT
          value: "2888"
        - name: ZK_ELECTION_PORT
          value: "3888"
        command:
        - sh
        - -c
        - zkGenConfig.sh && zkServer.sh start-foreground
        readinessProbe:
          exec:
            command:
            - "zkOk.sh"
          initialDelaySeconds: 15
          timeoutSeconds: 5
        livenessProbe:
          exec:
            command:
            - "zkOk.sh"
          initialDelaySeconds: 15
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/zookeeper
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
  - metadata:
      name: datadir
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi

详细的使用说明见zookeeper stateful application

关于StatefulSet的更多示例请参阅 github.com/kubernetes/contrib - statefulsets,其中包括了zookeeper和kafka。

集群外部访问StatefulSet的Pod

我们设想一下这样的场景:在kubernetes集群外部调试StatefulSet中有序的Pod,那么如何访问这些的pod呢?

方法是为pod设置label,然后用kubectl expose将其以NodePort的方式暴露到集群外部,以上面的zookeeper的例子来说明,下面使用命令的方式来暴露其中的两个zookeeper节点,也可以写一个serivce配置yaml文件。

kubectl label pod zk-0 zkInst=0                                                                          
kubectl label pod zk-1 zkInst=1                                                                         
kubectl expose po zk-0 --port=2181 --target-port=2181 --name=zk-0 --selector=zkInst=0 --type=NodePort
kubectl expose po zk-1 --port=2181 --target-port=2181 --name=zk-1 --selector=zkInst=1 --type=NodePort

这样在kubernetes集群外部就可以根据pod所在的主机所映射的端口来访问了。

查看zk-0这个service可以看到如下结果:

NAME      CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
zk-0      10.254.98.14   <nodes>       2181:31693/TCP   5m

集群外部就可以使用所有的node中的任何一个IP:31693来访问这个zookeeper实例。

DaemonSet

什么是 DaemonSet?

DaemonSet 确保全部(或者一些)Node 上运行一个 Pod 的副本。当有 Node 加入集群时,也会为他们新增一个 Pod 。当有 Node 从集群移除时,这些 Pod 也会被回收。删除 DaemonSet 将会删除它创建的所有 Pod。

使用 DaemonSet 的一些典型用法:

  • 运行集群存储 daemon,例如在每个 Node 上运行 glusterdceph
  • 在每个 Node 上运行日志收集 daemon,例如fluentdlogstash
  • 在每个 Node 上运行监控 daemon,例如 Prometheus Node Exportercollectd、Datadog 代理、New Relic 代理,或 Ganglia gmond

一个简单的用法是,在所有的 Node 上都存在一个 DaemonSet,将被作为每种类型的 daemon 使用。
一个稍微复杂的用法可能是,对单独的每种类型的 daemon 使用多个 DaemonSet,但具有不同的标志,和/或对不同硬件类型具有不同的内存、CPU要求。

编写 DaemonSet Spec

必需字段

和其它所有 Kubernetes 配置一样,DaemonSet 需要 apiVersionkindmetadata字段。有关配置文件的通用信息,详见文档 部署应用配置容器资源管理

DaemonSet 也需要一个 .spec 配置段。

Pod 模板

.spec 唯一必需的字段是 .spec.template

.spec.template 是一个 Pod 模板
它与 Pod 具有相同的 schema,除了它是嵌套的,而且不具有 apiVersionkind 字段。

Pod 除了必须字段外,在 DaemonSet 中的 Pod 模板必须指定合理的标签。

在 DaemonSet 中的 Pod 模板必需具有一个值为 AlwaysRestartPolicy,或者未指定它的值,默认是 Always

Pod Selector

.spec.selector 字段表示 Pod Selector,它与 Job 或其它资源的 .sper.selector 的原理是相同的。

spec.selector 表示一个对象,它由如下两个字段组成:

  • matchLabels - 与 ReplicationController.spec.selector 的原理相同。
  • matchExpressions - 允许构建更加复杂的 Selector,可以通过指定 key、value 列表,以及与 key 和 value 列表的相关的操作符。

当上述两个字段都指定时,结果表示的是 AND 关系。

如果指定了 .spec.selector,必须与 .spec.template.metadata.labels 相匹配。如果没有指定,它们默认是等价的。如果与它们配置的不匹配,则会被 API 拒绝。

如果 Pod 的 label 与 selector 匹配,或者直接基于其它的 DaemonSet、或者 Controller(例如 ReplicationController),也不可以创建任何 Pod。
否则 DaemonSet Controller 将认为那些 Pod 是它创建的。Kubernetes 不会阻止这样做。一个场景是,可能希望在一个具有不同值的、用来测试用的 Node 上手动创建 Pod。

仅在相同的 Node 上运行 Pod

如果指定了 .spec.template.spec.nodeSelector,DaemonSet Controller 将在能够匹配上 Node Selector 的 Node 上创建 Pod。
类似这种情况,可以指定 .spec.template.spec.affinity,然后 DaemonSet Controller 将在能够匹配上 Node Affinity 的 Node 上创建 Pod。
如果根本就没有指定,则 DaemonSet Controller 将在所有 Node 上创建 Pod。

如果调度 Daemon Pod

正常情况下,Pod 运行在哪个机器上是由 Kubernetes 调度器进行选择的。然而,由 Daemon Controller 创建的 Pod 已经确定了在哪个机器上(Pod 创建时指定了 .spec.nodeName),因此:

  • DaemonSet Controller 并不关心一个 Node 的 unschedulable 字段。
  • DaemonSet Controller 可以创建 Pod,即使调度器还没有被启动,这对集群启动是非常有帮助的。

Daemon Pod 关心 Taint 和 Toleration,它们会为没有指定 tolerationSecondsnode.alpha.kubernetes.io/notReadynode.alpha.kubernetes.io/unreachable 的 Taint,而创建具有 NoExecute 的 Toleration。这确保了当 alpha 特性的 TaintBasedEvictions 被启用,当 Node 出现故障,比如网络分区,这时它们将不会被清除掉(当 TaintBasedEvictions 特性没有启用,在这些场景下也不会被清除,但会因为 NodeController 的硬编码行为而被清除,Toleration 是不会的)。

与 Daemon Pod 通信

与 DaemonSet 中的 Pod 进行通信,几种可能的模式如下:

  • Push:配置 DaemonSet 中的 Pod 向其它 Service 发送更新,例如统计数据库。它们没有客户端。
  • NodeIP 和已知端口:DaemonSet 中的 Pod 可以使用 hostPort,从而可以通过 Node IP 访问到 Pod。客户端能通过某种方法知道 Node IP 列表,并且基于此也可以知道端口。
  • DNS:创建具有相同 Pod Selector 的 Headless Service,然后通过使用 endpoints 资源或从 DNS 检索到多个 A 记录来发现 DaemonSet。
  • Service:创建具有相同 Pod Selector 的 Service,并使用该 Service 访问到某个随机 Node 上的 daemon。(没有办法访问到特定 Node)

更新 DaemonSet

如果修改了 Node Label,DaemonSet 将立刻向新匹配上的 Node 添加 Pod,同时删除新近无法匹配上的 Node 上的 Pod。

可以修改 DaemonSet 创建的 Pod。然而,不允许对 Pod 的所有字段进行更新。当下次 Node(即使具有相同的名称)被创建时,DaemonSet Controller 还会使用最初的模板。

可以删除一个 DaemonSet。如果使用 kubectl 并指定 --cascade=false 选项,则 Pod 将被保留在 Node 上。然后可以创建具有不同模板的新 DaemonSet。具有不同模板的新 DaemonSet 将鞥能够通过 Label 匹配识别所有已经存在的 Pod。它不会修改或删除它们,即使是错误匹配了 Pod 模板。通过删除 Pod 或者 删除 Node,可以强制创建新的 Pod。

在 Kubernetes 1.6 或以后版本,可以在 DaemonSet 上 执行滚动升级

init 脚本

很可能通过直接在一个 Node 上启动 daemon 进程(例如,使用 initupstartd、或 systemd)。这非常好,然而基于 DaemonSet 来运行这些进程有如下一些好处:

  • 像对待应用程序一样,具备为 daemon 提供监控和管理日志的能力。
  • 为 daemon 和应用程序使用相同的配置语言和工具(如 Pod 模板、kubectl)。
  • Kubernetes 未来版本可能会支持对 DaemonSet 创建 Pod 与 Node升级工作流进行集成。
  • 在资源受限的容器中运行 daemon,能够增加 daemon 和应用容器的隔离性。然而这也实现了在容器中运行 daemon,但却不能在 Pod 中运行(例如,直接基于 Docker 启动)。

裸 Pod

可能要直接创建 Pod,同时指定其运行在特定的 Node 上。
然而,DaemonSet 替换了由于任何原因被删除或终止的 Pod,例如 Node 失败、例行节点维护,比如内核升级。由于这个原因,我们应该使用 DaemonSet 而不是单独创建 Pod。

静态 Pod

很可能,通过在一个指定目录下编写文件来创建 Pod,该目录受 Kubelet 所监视。这些 Pod 被称为 静态 Pod
不像 DaemonSet,静态 Pod 不受 kubectl 和 其它 Kubernetes API 客户端管理。静态 Pod 不依赖于 apiserver,这使得它们在集群启动的情况下非常有用。
而且,未来静态 Pod 可能会被废弃掉。

Replication Controller

DaemonSet 与 Replication Controller 非常类似,它们都能创建 Pod,这些 Pod 都具有不期望被终止的进程(例如,Web 服务器、存储服务器)。
为无状态的 Service 使用 Replication Controller,像 frontend,实现对副本的数量进行扩缩容、平滑升级,比之于精确控制 Pod 运行在某个主机上要重要得多。需要 Pod 副本总是运行在全部或特定主机上,并需要先于其他 Pod 启动,当这被认为非常重要时,应该使用 Daemon Controller。

Service Account

Service account为Pod中的进程提供身份信息。

本文是关于 Service Account 的用户指南,管理指南另见 Service Account 的集群管理指南 。

注意:本文档描述的关于 Serivce Account 的行为只有当您按照 Kubernetes 项目建议的方式搭建起集群的情况下才有效。您的集群管理员可能在您的集群中有自定义配置,这种情况下该文档可能并不适用。

当您(真人用户)访问集群(例如使用kubectl命令)时,apiserver 会将您认证为一个特定的 User Account(目前通常是admin,除非您的系统管理员自定义了集群配置)。Pod 容器中的进程也可以与 apiserver 联系。 当它们在联系 apiserver 的时候,它们会被认证为一个特定的 Service Account(例如default)。

使用默认的 Service Account 访问 API server

当您创建 pod 的时候,如果您没有指定一个 service account,系统会自动得在与该pod 相同的 namespace 下为其指派一个default service account。如果您获取刚创建的 pod 的原始 json 或 yaml 信息(例如使用kubectl get pods/podename -o yaml命令),您将看到spec.serviceAccountName字段已经被设置为 automatically set

您可以在 pod 中使用自动挂载的 service account 凭证来访问 API,如 Accessing the Cluster 中所描述。

Service account 是否能够取得访问 API 的许可取决于您使用的 授权插件和策略

在 1.6 以上版本中,您可以选择取消为 serivce account 自动挂载 API 凭证,只需在 service account 中设置 automountServiceAccountToken: false

apiVersion: v1
kind: ServiceAccount
metadata:
  name: build-robot
automountServiceAccountToken: false
...

在 1.6 以上版本中,您也可以选择只取消单个 pod 的 API 凭证自动挂载:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  serviceAccountName: build-robot
  automountServiceAccountToken: false
  ...

如果在 pod 和 service account 中同时设置了 automountServiceAccountToken , pod 设置中的优先级更高。

使用多个Service Account

每个 namespace 中都有一个默认的叫做 default 的 service account 资源。

您可以使用以下命令列出 namespace 下的所有 serviceAccount 资源。

$ kubectl get serviceAccounts
NAME      SECRETS    AGE
default   1          1d

您可以像这样创建一个 ServiceAccount 对象:

$ cat > /tmp/serviceaccount.yaml <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  name: build-robot
EOF
$ kubectl create -f /tmp/serviceaccount.yaml
serviceaccount "build-robot" created

如果您看到如下的 service account 对象的完整输出信息:

$ kubectl get serviceaccounts/build-robot -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2015-06-16T00:12:59Z
  name: build-robot
  namespace: default
  resourceVersion: "272500"
  selfLink: /api/v1/namespaces/default/serviceaccounts/build-robot
  uid: 721ab723-13bc-11e5-aec2-42010af0021e
secrets:
- name: build-robot-token-bvbk5

然后您将看到有一个 token 已经被自动创建,并被 service account 引用。

您可以使用授权插件来 设置 service account 的权限

设置非默认的 service account,只需要在 pod 的spec.serviceAccountName 字段中将name设置为您想要用的 service account 名字即可。

在 pod 创建之初 service account 就必须已经存在,否则创建将被拒绝。

您不能更新已创建的 pod 的 service account。

您可以清理 service account,如下所示:

$ kubectl delete serviceaccount/build-robot

手动创建 service account 的 API token

假设我们已经有了一个如上文提到的名为 ”build-robot“ 的 service account,我们手动创建一个新的 secret。

$ cat > /tmp/build-robot-secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: build-robot-secret
  annotations: 
    kubernetes.io/service-account.name: build-robot
type: kubernetes.io/service-account-token
EOF
$ kubectl create -f /tmp/build-robot-secret.yaml
secret "build-robot-secret" created

现在您可以确认下新创建的 secret 取代了 “build-robot” 这个 service account 原来的 API token。

所有已不存在的 service account 的 token 将被 token controller 清理掉。

$ kubectl describe secrets/build-robot-secret 
Name:   build-robot-secret
Namespace:  default
Labels:   <none>
Annotations:  kubernetes.io/service-account.name=build-robot,kubernetes.io/service-account.uid=870ef2a5-35cf-11e5-8d06-005056b45392
Type: kubernetes.io/service-account-token
Data
====
ca.crt: 1220 bytes
token: ...
namespace: 7 bytes

注意该内容中的token被省略了。

为 service account 添加 ImagePullSecret

首先,创建一个 imagePullSecret,详见这里

然后,确认已创建。如:

$ kubectl get secrets myregistrykey
NAME             TYPE                              DATA    AGE
myregistrykey    kubernetes.io/.dockerconfigjson   1       1d

然后,修改 namespace 中的默认 service account 使用该 secret 作为 imagePullSecret。

kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "myregistrykey"}]}'

Vi 交互过程中需要手动编辑:

$ kubectl get serviceaccounts default -o yaml > ./sa.yaml
$ cat sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2015-08-07T22:02:39Z
  name: default
  namespace: default
  resourceVersion: "243024"
  selfLink: /api/v1/namespaces/default/serviceaccounts/default
  uid: 052fb0f4-3d50-11e5-b066-42010af0d7b6
secrets:
- name: default-token-uudge
$ vi sa.yaml
[editor session not shown]
[delete line with key "resourceVersion"]
[add lines with "imagePullSecret:"]
$ cat sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2015-08-07T22:02:39Z
  name: default
  namespace: default
  selfLink: /api/v1/namespaces/default/serviceaccounts/default
  uid: 052fb0f4-3d50-11e5-b066-42010af0d7b6
secrets:
- name: default-token-uudge
imagePullSecrets:
- name: myregistrykey
$ kubectl replace serviceaccount default -f ./sa.yaml
serviceaccounts/default

现在,所有当前 namespace 中新创建的 pod 的 spec 中都会增加如下内容:

spec:
  imagePullSecrets:
  - name: myregistrykey

ReplicationController和ReplicaSet

ReplicationCtronller用来确保容器应用的副本数始终保持在用户定义的副本数,即如果有容器异常退出,会自动创建新的Pod来替代;而如果异常多出来的容器也会自动回收。

在新版本的Kubernetes中建议使用ReplicaSet来取代ReplicationCtronller。ReplicaSet跟ReplicationCtronller没有本质的不同,只是名字不一样,并且ReplicaSet支持集合式的selector。

虽然ReplicaSet可以独立使用,但一般还是建议使用 Deployment 来自动管理ReplicaSet,这样就无需担心跟其他机制的不兼容问题(比如ReplicaSet不支持rolling-update但Deployment支持)。

ReplicaSet示例:

apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
  name: frontend
  # these labels can be applied automatically
  # from the labels in the pod template if not set
  # labels:
    # app: guestbook
    # tier: frontend
spec:
  # this replicas value is default
  # modify it according to your case
  replicas: 3
  # selector can be applied automatically
  # from the labels in the pod template if not set,
  # but we are specifying the selector here to
  # demonstrate its usage.
  selector:
    matchLabels:
      tier: frontend
    matchExpressions:
      - {key: tier, operator: In, values: [frontend]}
  template:
    metadata:
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: gcr.io/google_samples/gb-frontend:v3
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        - name: GET_HOSTS_FROM
          value: dns
          # If your cluster config does not include a dns service, then to
          # instead access environment variables to find service host
          # info, comment out the 'value: dns' line above, and uncomment the
          # line below.
          # value: env
        ports:
        - containerPort: 80

Job

Job负责批处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个Pod成功结束。

Job Spec格式

  • spec.template格式同Pod
  • RestartPolicy仅支持Never或OnFailure
  • 单个Pod时,默认Pod成功运行后Job即结束
  • .spec.completions标志Job结束需要成功运行的Pod个数,默认为1
  • .spec.parallelism标志并行运行的Pod的个数,默认为1
  • spec.activeDeadlineSeconds标志失败Pod的重试最大时间,超过这个时间不会继续重试

一个简单的例子:

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    metadata:
      name: pi
    spec:
      containers:
      - name: pi
        image: perl
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never
$ kubectl create -f ./job.yaml
job "pi" created
$ pods=$(kubectl get pods --selector=job-name=pi --output=jsonpath={.items..metadata.name})
$ kubectl logs $pods
3.141592653589793238462643383279502...

Bare Pods

所谓Bare Pods是指直接用PodSpec来创建的Pod(即不在ReplicaSets或者ReplicationController的管理之下的Pods)。这些Pod在Node重启后不会自动重启,但Job则会创建新的Pod继续任务。所以,推荐使用Job来替代Bare Pods,即便是应用只需要一个Pod。

CronJob

Cron Job 管理基于时间的 Job,即:

  • 在给定时间点只运行一次
  • 周期性地在给定时间点运行

一个 CronJob 对象类似于 crontab (cron table)文件中的一行。它根据指定的预定计划周期性地运行一个 Job,格式可以参考 Cron

前提条件

当使用的 Kubernetes 集群,版本 >= 1.4(对 ScheduledJob),>= 1.5(对 CronJob),当启动 API Server(参考 为集群开启或关闭 API 版本 获取更多信息)时,通过传递选项 --runtime-config=batch/v2alpha1=true 可以开启 batch/v2alpha1 API。

典型的用法如下所示:

  • 在给定的时间点调度 Job 运行
  • 创建周期性运行的 Job,例如:数据库备份、发送邮件。

CronJob Spec

  • .spec.schedule调度,必需字段,指定任务运行周期,格式同 Cron

  • .spec.jobTemplateJob 模板,必需字段,指定需要运行的任务,格式同 Job

  • .spec.startingDeadlineSeconds启动 Job 的期限(秒级别),该字段是可选的。如果因为任何原因而错过了被调度的时间,那么错过执行时间的 Job 将被认为是失败的。如果没有指定,则没有期限

  • .spec.concurrencyPolicy并发策略,该字段也是可选的。它指定了如何处理被 Cron Job 创建的 Job 的并发执行。只允许指定下面策略中的一种:

    • Allow(默认):允许并发运行 Job
    • Forbid:禁止并发运行,如果前一个还没有完成,则直接跳过下一个
    • Replace:取消当前正在运行的 Job,用一个新的来替换

    注意,当前策略只能应用于同一个 Cron Job 创建的 Job。如果存在多个 Cron Job,它们创建的 Job 之间总是允许并发运行。

  • .spec.suspend挂起,该字段也是可选的。如果设置为 true,后续所有执行都会被挂起。它对已经开始执行的 Job 不起作用。默认值为 false

  • .spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit历史限制,是可选的字段。它们指定了可以保留多少完成和失败的 Job。

    默认没有限制,所有成功和失败的 Job 都会被保留。然而,当运行一个 Cron Job 时,Job 可以很快就堆积很多,推荐设置这两个字段的值。设置限制的值为 0,相关类型的 Job 完成后将不会被保留。

apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

当然,也可以用kubectl run来创建一个CronJob:

$ kubectl create -f cronjob.yaml
cronjob "hello" created
$ kubectl get cronjob
NAME      SCHEDULE      SUSPEND   ACTIVE    LAST-SCHEDULE
hello     */1 * * * *   False     0         <none>
$ kubectl get jobs
NAME               DESIRED   SUCCESSFUL   AGE
hello-1202039034   1         1            49s
$ pods=$(kubectl get pods --selector=job-name=hello-1202039034 --output=jsonpath={.items..metadata.name} -a)
$ kubectl logs $pods
Mon Aug 29 21:34:09 UTC 2016
Hello from the Kubernetes cluster
# 注意,删除cronjob的时候不会自动删除job,这些job可以用kubectl delete job来删除
$ kubectl delete cronjob hello
cronjob "hello" deleted

Cron Job 限制

Cron Job 在每次调度运行时间内 大概 会创建一个 Job 对象。我们之所以说 大概 ,是因为在特定的环境下可能会创建两个 Job,或者一个 Job 都没创建。我们尝试少发生这种情况,但却不能完全避免。因此,创建 Job 操作应该是 幂等的

Job 根据它所创建的 Pod 的并行度,负责重试创建 Pod,并就决定这一组 Pod 的成功或失败。Cron Job 根本就不会去检查 Pod。

删除 Cron Job

一旦不再需要 Cron Job,简单地可以使用 kubectl 命令删除它:

$ kubectl delete cronjob hello
cronjob "hello" deleted

这将会终止正在创建的 Job。然而,运行中的 Job 将不会被终止,不会删除 Job 或 它们的 Pod。为了清理那些 Job 和 Pod,需要列出该 Cron Job 创建的全部 Job,然后删除它们:

$ kubectl get jobs
NAME               DESIRED   SUCCESSFUL   AGE
hello-1201907962   1         1            11m
hello-1202039034   1         1            8m
...
$ kubectl delete jobs hello-1201907962 hello-1202039034 ...
job "hello-1201907962" deleted
job "hello-1202039034" deleted
...

一旦 Job 被删除,由 Job 创建的 Pod 也会被删除。注意,所有由名称为 “hello” 的 Cron Job 创建的 Job 会以前缀字符串 “hello-” 进行命名。如果想要删除当前 Namespace 中的所有 Job,可以通过命令 kubectl delete jobs --all 立刻删除它们。

Ingress解析

前言

这是kubernete官方文档中Ingress Resource的翻译,后面的章节会讲到使用Traefik来做Ingress controller,文章末尾给出了几个相关链接。

术语

在本篇文章中你将会看到一些在其他地方被交叉使用的术语,为了防止产生歧义,我们首先来澄清下。

  • 节点:Kubernetes集群中的一台物理机或者虚拟机。
  • 集群:位于Internet防火墙后的节点,这是kubernetes管理的主要计算资源。
  • 边界路由器:为集群强制执行防火墙策略的路由器。 这可能是由云提供商或物理硬件管理的网关。
  • 集群网络:一组逻辑或物理链接,可根据Kubernetes网络模型实现群集内的通信。 集群网络的实现包括Overlay模型的 flannel 和基于SDN的OVS
  • 服务:使用标签选择器标识一组pod成为的Kubernetes服务。 除非另有说明,否则服务假定在集群网络内仅可通过虚拟IP访问。

什么是Ingress?

通常情况下,service和pod仅可在集群内部网络中通过IP地址访问。所有到达边界路由器的流量或被丢弃或被转发到其他地方。从概念上讲,可能像下面这样:

    internet
        |
  ------------
  [ Services ]

Ingress是授权入站连接到达集群服务的规则集合。

    internet
        |
   [ Ingress ]
   --|-----|--
   [ Services ]

你可以给Ingress配置提供外部可访问的URL、负载均衡、SSL、基于名称的虚拟主机等。用户通过POST Ingress资源到API server的方式来请求ingress。 Ingress controller负责实现Ingress,通常使用负载平衡器,它还可以配置边界路由和其他前端,这有助于以HA方式处理流量。

先决条件

在使用Ingress resource之前,有必要先了解下面几件事情。Ingress是beta版本的resource,在kubernetes1.1之前还没有。你需要一个Ingress Controller来实现Ingress,单纯的创建一个Ingress没有任何意义。

GCE/GKE会在master节点上部署一个ingress controller。你可以在一个pod中部署任意个自定义的ingress controller。你必须正确地annotate每个ingress,比如 运行多个ingress controller关闭glbc.

确定你已经阅读了Ingress controller的beta版本限制。在非GCE/GKE的环境中,你需要在pod中部署一个controller

Ingress Resource

最简化的Ingress配置:

1: apiVersion: extensions/v1beta1
2: kind: Ingress
3: metadata:
4:   name: test-ingress
5: spec:
6:   rules:
7:   - http:
8:       paths:
9:       - path: /testpath
10:        backend:
11:           serviceName: test
12:           servicePort: 80

如果你没有配置Ingress controller就将其POST到API server不会有任何用处

配置说明

1-4行:跟Kubernetes的其他配置一样,ingress的配置也需要apiVersionkindmetadata字段。配置文件的详细说明请查看部署应用, 配置容器使用resources.

5-7行: Ingress spec 中包含配置一个loadbalancer或proxy server的所有信息。最重要的是,它包含了一个匹配所有入站请求的规则列表。目前ingress只支持http规则。

8-9行:每条http规则包含以下信息:一个host配置项(比如for.bar.com,在这个例子中默认是*),path列表(比如:/testpath),每个path都关联一个backend(比如test:80)。在loadbalancer将流量转发到backend之前,所有的入站请求都要先匹配host和path。

10-12行:正如 services doc中描述的那样,backend是一个service:port的组合。Ingress的流量被转发到它所匹配的backend。

全局参数:为了简单起见,Ingress示例中没有全局参数,请参阅资源完整定义的api参考。 在所有请求都不能跟spec中的path匹配的情况下,请求被发送到Ingress controller的默认后端,可以指定全局缺省backend。

Ingress controllers

为了使Ingress正常工作,集群中必须运行Ingress controller。 这与其他类型的控制器不同,其他类型的控制器通常作为kube-controller-manager二进制文件的一部分运行,在集群启动时自动启动。 你需要选择最适合自己集群的Ingress controller或者自己实现一个。 示例和说明可以在这里找到。

在你开始前

以下文档描述了Ingress资源中公开的一组跨平台功能。 理想情况下,所有的Ingress controller都应该符合这个规范,但是我们还没有实现。 GCE和nginx控制器的文档分别在这里这里确保您查看控制器特定的文档,以便您了解每个文档的注意事项。

Ingress类型

单Service Ingress

Kubernetes中已经存在一些概念可以暴露单个service(查看替代方案),但是你仍然可以通过Ingress来实现,通过指定一个没有rule的默认backend的方式。

ingress.yaml定义文件:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
spec:
  backend:
    serviceName: testsvc
    servicePort: 80

使用kubectl create -f命令创建,然后查看ingress:

$ kubectl get ing
NAME                RULE          BACKEND        ADDRESS
test-ingress        -             testsvc:80     107.178.254.228

107.178.254.228就是Ingress controller为了实现Ingress而分配的IP地址。RULE列表示所有发送给该IP的流量都被转发到了BACKEND所列的Kubernetes service上。

简单展开

如前面描述的那样,kubernete pod中的IP只在集群网络内部可见,我们需要在边界设置一个东西,让它能够接收ingress的流量并将它们转发到正确的端点上。这个东西一般是高可用的loadbalancer。使用Ingress能够允许你将loadbalancer的个数降低到最少,例如,假如你想要创建这样的一个设置:

foo.bar.com -> 178.91.123.132 -> / foo    s1:80
                                 / bar    s2:80

你需要一个这样的ingress:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /foo
        backend:
          serviceName: s1
          servicePort: 80
      - path: /bar
        backend:
          serviceName: s2
          servicePort: 80

使用kubectl create -f创建完ingress后:

$ kubectl get ing
NAME      RULE          BACKEND   ADDRESS
test      -
          foo.bar.com
          /foo          s1:80
          /bar          s2:80

只要服务(s1,s2)存在,Ingress controller就会将提供一个满足该Ingress的特定loadbalancer实现。 这一步完成后,您将在Ingress的最后一列看到loadbalancer的地址。

基于名称的虚拟主机

Name-based的虚拟主机在同一个IP地址下拥有多个主机名。

foo.bar.com --|                 |-> foo.bar.com s1:80
              | 178.91.123.132  |
bar.foo.com --|                 |-> bar.foo.com s2:80

下面这个ingress说明基于Host header的后端loadbalancer的路由请求:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - backend:
          serviceName: s1
          servicePort: 80
  - host: bar.foo.com
    http:
      paths:
      - backend:
          serviceName: s2
          servicePort: 80

默认backend:一个没有rule的ingress,如前面章节中所示,所有流量都将发送到一个默认backend。你可以用该技巧通知loadbalancer如何找到你网站的404页面,通过制定一些列rule和一个默认backend的方式。如果请求header中的host不能跟ingress中的host匹配,并且/或请求的URL不能与任何一个path匹配,则流量将路由到你的默认backend。

TLS

你可以通过指定包含TLS私钥和证书的secret来加密Ingress。 目前,Ingress仅支持单个TLS端口443,并假定TLS termination。 如果Ingress中的TLS配置部分指定了不同的主机,则它们将根据通过SNI TLS扩展指定的主机名(假如Ingress controller支持SNI)在多个相同端口上进行复用。 TLS secret中必须包含名为tls.crttls.key的密钥,这里面包含了用于TLS的证书和私钥,例如:

apiVersion: v1
data:
  tls.crt: base64 encoded cert
  tls.key: base64 encoded key
kind: Secret
metadata:
  name: testsecret
  namespace: default
type: Opaque

在Ingress中引用这个secret将通知Ingress controller使用TLS加密从将客户端到loadbalancer的channel:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: no-rules-map
spec:
  tls:
    - secretName: testsecret
  backend:
    serviceName: s1
    servicePort: 80

请注意,各种Ingress controller支持的TLS功能之间存在差距。 请参阅有关nginxGCE或任何其他平台特定Ingress controller的文档,以了解TLS在你的环境中的工作原理。

Ingress controller启动时附带一些适用于所有Ingress的负载平衡策略设置,例如负载均衡算法,后端权重方案等。更高级的负载平衡概念(例如持久会话,动态权重)尚未在Ingress中公开。 你仍然可以通过service loadbalancer获取这些功能。 随着时间的推移,我们计划将适用于跨平台的负载平衡模式加入到Ingress资源中。

还值得注意的是,尽管健康检查不直接通过Ingress公开,但Kubernetes中存在并行概念,例如准备探查,可以使你达成相同的最终结果。 请查看特定控制器的文档,以了解他们如何处理健康检查(nginxGCE)。

更新Ingress

假如你想要向已有的ingress中增加一个新的Host,你可以编辑和更新该ingress:

$ kubectl get ing
NAME      RULE          BACKEND   ADDRESS
test      -                       178.91.123.132
          foo.bar.com
          /foo          s1:80
$ kubectl edit ing test

这会弹出一个包含已有的yaml文件的编辑器,修改它,增加新的Host配置。

spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - backend:
          serviceName: s1
          servicePort: 80
        path: /foo
  - host: bar.baz.com
    http:
      paths:
      - backend:
          serviceName: s2
          servicePort: 80
        path: /foo
..

保存它会更新API server中的资源,这会触发ingress controller重新配置loadbalancer。

$ kubectl get ing
NAME      RULE          BACKEND   ADDRESS
test      -                       178.91.123.132
          foo.bar.com
          /foo          s1:80
          bar.baz.com
          /foo          s2:80

在一个修改过的ingress yaml文件上调用kubectl replace -f命令一样可以达到同样的效果。

跨可用域故障

在不通云供应商之间,跨故障域的流量传播技术有所不同。 有关详细信息,请查看相关Ingress controller的文档。 有关在federation集群中部署Ingress的详细信息。

未来计划

  • 多样化的HTTPS/TLS模型支持(如SNI,re-encryption)
  • 通过声明来请求IP或者主机名
  • 结合L4和L7 Ingress
  • 更多的Ingress controller

请跟踪L7和Ingress的proposal,了解有关资源演进的更多细节,以及Ingress repository,了解有关各种Ingress controller演进的更多详细信息。

替代方案

你可以通过很多种方式暴露service而不必直接使用ingress:

Traefik Ingress Controller

我们在前面部署了 Traefik 作为Ingress Controller,如果集群外部直接访问Kubenretes内部服务的话,可以直接创建Ingress如下所示:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: traefik-ingress
  namespace: default
spec:
  rules:
  - host: traefik.nginx.io
    http:
      paths:
      - path: /
        backend:
          serviceName: my-nginx
          servicePort: 80

Traefik Ingress Controller

当我们处于迁移应用到kuberentes上的阶段时,可能有部分服务实例不在kubernetes上,服务的路由使用nginx配置,这时处于nginx和ingress共存的状态。参考下面的配置:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: td-ingress
  namespace: default
  annotations:
    traefik.frontend.rule.type: PathPrefixStrip
    kubernetes.io/ingress.class: traefik
spec:
  rules:
  - host: "*.tendcloud.com"
    http:
      paths:
      - path: /docGenerate
        backend:
          serviceName: td-sdmk-docgenerate
          servicePort: 80

注意annotation的配置:

  • traefik.frontend.rule.type: PathPrefixStrip:表示将截掉URL中的path
  • kubernetes.io/ingress.class:表示使用的ingress类型

关于Ingress annotation的更多信息请参考:Ingress Annotations - kubernetes.io

在nginx中增加配置:

upstream docGenerate {
       server 172.20.0.119:80;
       keepalive 200;
    }

172.20.0.119是我们的边缘节点的VIP.

ConfigMap

其实ConfigMap功能在Kubernetes1.2版本的时候就有了,许多应用程序会从配置文件、命令行参数或环境变量中读取配置信息。这些配置信息需要与docker image解耦,你总不能每修改一个配置就重做一个image吧?ConfigMap API给我们提供了向容器中注入配置信息的机制,ConfigMap可以被用来保存单个属性,也可以用来保存整个配置文件或者JSON二进制大对象。

ConfigMap概览

ConfigMap API资源用来保存key-value pair配置数据,这个数据可以在pods里使用,或者被用来为像controller一样的系统组件存储配置数据。虽然ConfigMap跟Secrets类似,但是ConfigMap更方便的处理不含敏感信息的字符串。 注意:ConfigMaps不是属性配置文件的替代品。ConfigMaps只是作为多个properties文件的引用。你可以把它理解为Linux系统中的/etc目录,专门用来存储配置文件的目录。下面举个例子,使用ConfigMap配置来创建Kuberntes Volumes,ConfigMap中的每个data项都会成为一个新文件。

kind: ConfigMap
apiVersion: v1
metadata:
  creationTimestamp: 2016-02-18T19:14:38Z
  name: example-config
  namespace: default
data:
  example.property.1: hello
  example.property.2: world
  example.property.file: |-
    property.1=value-1
    property.2=value-2
    property.3=value-3

data一栏包括了配置数据,ConfigMap可以被用来保存单个属性,也可以用来保存一个配置文件。 配置数据可以通过很多种方式在Pods里被使用。ConfigMaps可以被用来:

  1. 设置环境变量的值
  2. 在容器里设置命令行参数
  3. 在数据卷里面创建config文件

用户和系统组件两者都可以在ConfigMap里面存储配置数据。

其实不用看下面的文章,直接从kubectl create configmap -h的帮助信息中就可以对ConfigMap究竟如何创建略知一二了。

Examples:
  # Create a new configmap named my-config based on folder bar
  kubectl create configmap my-config --from-file=path/to/bar
  # Create a new configmap named my-config with specified keys instead of file basenames on disk
  kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt
  # Create a new configmap named my-config with key1=config1 and key2=config2
  kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2

创建ConfigMaps

可以使用该命令,用给定值、文件或目录来创建ConfigMap。

kubectl create configmap

使用目录创建

比如我们已经有个了包含一些配置文件,其中包含了我们想要设置的ConfigMap的值:

$ ls docs/user-guide/configmap/kubectl/
game.properties
ui.properties
$ cat docs/user-guide/configmap/kubectl/game.properties
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
$ cat docs/user-guide/configmap/kubectl/ui.properties
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice

使用下面的命令可以创建一个包含目录中所有文件的ConfigMap。

$ kubectl create configmap game-config --from-file=docs/user-guide/configmap/kubectl

—from-file指定在目录下的所有文件都会被用在ConfigMap里面创建一个键值对,键的名字就是文件名,值就是文件的内容。

让我们来看一下这个命令创建的ConfigMap:

$ kubectl describe configmaps game-config
Name:           game-config
Namespace:      default
Labels:         <none>
Annotations:    <none>
Data
====
game.properties:        158 bytes
ui.properties:          83 bytes

我们可以看到那两个key是从kubectl指定的目录中的文件名。这些key的内容可能会很大,所以在kubectl describe的输出中,只能够看到键的名字和他们的大小。 如果想要看到键的值的话,可以使用kubectl get

$ kubectl get configmaps game-config -o yaml

我们以yaml格式输出配置。

apiVersion: v1
data:
  game.properties: |
    enemies=aliens
    lives=3
    enemies.cheat=true
    enemies.cheat.level=noGoodRotten
    secret.code.passphrase=UUDDLRLRBABAS
    secret.code.allowed=true
    secret.code.lives=30
  ui.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true
    how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T18:34:05Z
  name: game-config
  namespace: default
  resourceVersion: "407"
  selfLink: /api/v1/namespaces/default/configmaps/game-config
  uid: 30944725-d66e-11e5-8cd0-68f728db1985

使用文件创建

刚才使用目录创建的时候我们—from-file指定的是一个目录,只要指定为一个文件就可以从单个文件中创建ConfigMap。

$ kubectl create configmap game-config-2 --from-file=docs/user-guide/configmap/kubectl/game.properties 
$ kubectl get configmaps game-config-2 -o yaml
apiVersion: v1
data:
  game-special-key: |
    enemies=aliens
    lives=3
    enemies.cheat=true
    enemies.cheat.level=noGoodRotten
    secret.code.passphrase=UUDDLRLRBABAS
    secret.code.allowed=true
    secret.code.lives=30
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T18:54:22Z
  name: game-config-3
  namespace: default
  resourceVersion: "530"
  selfLink: /api/v1/namespaces/default/configmaps/game-config-3
  uid: 05f8da22-d671-11e5-8cd0-68f728db1985

—from-file这个参数可以使用多次,你可以使用两次分别指定上个实例中的那两个配置文件,效果就跟指定整个目录是一样的。

使用字面值创建

使用文字值创建,利用—from-literal参数传递配置信息,该参数可以使用多次,格式如下;

$ kubectl create configmap special-config --from-literal=special.how=very --from-literal=special.type=charm
$ kubectl get configmaps special-config -o yaml
apiVersion: v1
data:
  special.how: very
  special.type: charm
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T19:14:38Z
  name: special-config
  namespace: default
  resourceVersion: "651"
  selfLink: /api/v1/namespaces/default/configmaps/special-config
  uid: dadce046-d673-11e5-8cd0-68f728db1985

Pod中使用ConfigMap

使用ConfigMap来替代环境变量

ConfigMap可以被用来填入环境变量。看下下面的ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm

apiVersion: v1
kind: ConfigMap
metadata:
  name: env-config
  namespace: default
data:
  log_level: INFO

我们可以在Pod中这样使用ConfigMap:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh", "-c", "env" ]
      env:
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.how
        - name: SPECIAL_TYPE_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.type
      envFrom:
        - configMapRef:
            name: env-config
  restartPolicy: Never

这个Pod运行后会输出如下几行:

SPECIAL_LEVEL_KEY=very
SPECIAL_TYPE_KEY=charm
log_level=INFO

用ConfigMap设置命令行参数

ConfigMap也可以被使用来设置容器中的命令或者参数值。它使用的是Kubernetes的$(VAR_NAME)替换语法。我们看下下面这个ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm

为了将ConfigMap中的值注入到命令行的参数里面,我们还要像前面那个例子一样使用环境变量替换语法${VAR_NAME)。(其实这个东西就是给Docker容器设置环境变量,以前我创建镜像的时候经常这么玩,通过docker run的时候指定-e参数修改镜像里的环境变量,然后docker的CMD命令再利用该$(VAR_NAME)通过sed来修改配置文件或者作为命令行启动参数。)

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ]
      env:
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.how
        - name: SPECIAL_TYPE_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.type
  restartPolicy: Never

运行这个Pod后会输出:

very charm

通过数据卷插件使用ConfigMap

ConfigMap也可以在数据卷里面被使用。还是这个ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm

在数据卷里面使用这个ConfigMap,有不同的选项。最基本的就是将文件填入数据卷,在这个文件中,键就是文件名,键值就是文件内容:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh", "-c", "cat /etc/config/special.how" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config
  restartPolicy: Never

运行这个Pod的输出是very

我们也可以在ConfigMap值被映射的数据卷里控制路径。

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh","-c","cat /etc/config/path/to/special-key" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config
        items:
        - key: special.how
          path: path/to/special-key
  restartPolicy: Never

运行这个Pod后的结果是very

ConfigMap的热更新

ConfigMap是用来存储配置文件的kubernetes资源对象,所有的配置内容都存储在etcd中,下文主要是探究 ConfigMap 的创建和更新流程,以及对 ConfigMap 更新后容器内挂载的内容是否同步更新的测试。

测试示例

假设我们在 default namespace 下有一个名为 nginx-config 的 ConfigMap,可以使用 kubectl命令来获取:

$ kubectl get configmap nginx-config
NAME           DATA      AGE
nginx-config   1         99d

获取该ConfigMap的内容。

kubectl get configmap nginx-config -o yaml

apiVersion: v1
data:
  nginx.conf: |-
    worker_processes 1;
    events { worker_connections 1024; }
    http {
        sendfile on;
        server {
            listen 80;
            # a test endpoint that returns http 200s
            location / {
                proxy_pass http://httpstat.us/200;
                proxy_set_header  X-Real-IP  $remote_addr;
            }
        }
        server {
            listen 80;
            server_name api.hello.world;
            location / {
                proxy_pass http://l5d.default.svc.cluster.local;
                proxy_set_header Host $host;
                proxy_set_header Connection "";
                proxy_http_version 1.1;
                more_clear_input_headers 'l5d-ctx-*' 'l5d-dtab' 'l5d-sample';
            }
        }
        server {
            listen 80;
            server_name www.hello.world;
            location / {
                # allow 'employees' to perform dtab overrides
                if ($cookie_special_employee_cookie != "letmein") {
                  more_clear_input_headers 'l5d-ctx-*' 'l5d-dtab' 'l5d-sample';
                }
                # add a dtab override to get people to our beta, world-v2
                set $xheader "";
                if ($cookie_special_employee_cookie ~* "dogfood") {
                  set $xheader "/host/world => /srv/world-v2;";
                }
                proxy_set_header 'l5d-dtab' $xheader;
                proxy_pass http://l5d.default.svc.cluster.local;
                proxy_set_header Host $host;
                proxy_set_header Connection "";
                proxy_http_version 1.1;
            }
        }
    }
kind: ConfigMap
metadata:
  creationTimestamp: 2017-08-01T06:53:17Z
  name: nginx-config
  namespace: default
  resourceVersion: "14925806"
  selfLink: /api/v1/namespaces/default/configmaps/nginx-config
  uid: 18d70527-7686-11e7-bfbd-8af1e3a7c5bd

ConfigMap中的内容是存储到etcd中的,然后查询etcd:

ETCDCTL_API=3 etcdctl get /registry/configmaps/default/nginx-config -w json|python -m json.tool

注意使用 v3 版本的 etcdctl API,下面是输出结果:

{
    "count": 1,
    "header": {
        "cluster_id": 12091028579527406772,
        "member_id": 16557816780141026208,
        "raft_term": 36,
        "revision": 29258723
    },
    "kvs": [
        {
            "create_revision": 14925806,
            "key": "L3JlZ2lzdHJ5L2NvbmZpZ21hcHMvZGVmYXVsdC9uZ2lueC1jb25maWc=",
            "mod_revision": 14925806,
            "value": "azhzAAoPCgJ2MRIJQ29uZmlnTWFwEqQMClQKDG5naW54LWNvbmZpZxIAGgdkZWZhdWx0IgAqJDE4ZDcwNTI3LTc2ODYtMTFlNy1iZmJkLThhZjFlM2E3YzViZDIAOABCCwjdyoDMBRC5ss54egASywsKCm5naW54LmNvbmYSvAt3b3JrZXJfcHJvY2Vzc2VzIDE7CgpldmVudHMgeyB3b3JrZXJfY29ubmVjdGlvbnMgMTAyNDsgfQoKaHR0cCB7CiAgICBzZW5kZmlsZSBvbjsKCiAgICBzZXJ2ZXIgewogICAgICAgIGxpc3RlbiA4MDsKCiAgICAgICAgIyBhIHRlc3QgZW5kcG9pbnQgdGhhdCByZXR1cm5zIGh0dHAgMjAwcwogICAgICAgIGxvY2F0aW9uIC8gewogICAgICAgICAgICBwcm94eV9wYXNzIGh0dHA6Ly9odHRwc3RhdC51cy8yMDA7CiAgICAgICAgICAgIHByb3h5X3NldF9oZWFkZXIgIFgtUmVhbC1JUCAgJHJlbW90ZV9hZGRyOwogICAgICAgIH0KICAgIH0KCiAgICBzZXJ2ZXIgewoKICAgICAgICBsaXN0ZW4gODA7CiAgICAgICAgc2VydmVyX25hbWUgYXBpLmhlbGxvLndvcmxkOwoKICAgICAgICBsb2NhdGlvbiAvIHsKICAgICAgICAgICAgcHJveHlfcGFzcyBodHRwOi8vbDVkLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWw7CiAgICAgICAgICAgIHByb3h5X3NldF9oZWFkZXIgSG9zdCAkaG9zdDsKICAgICAgICAgICAgcHJveHlfc2V0X2hlYWRlciBDb25uZWN0aW9uICIiOwogICAgICAgICAgICBwcm94eV9odHRwX3ZlcnNpb24gMS4xOwoKICAgICAgICAgICAgbW9yZV9jbGVhcl9pbnB1dF9oZWFkZXJzICdsNWQtY3R4LSonICdsNWQtZHRhYicgJ2w1ZC1zYW1wbGUnOwogICAgICAgIH0KICAgIH0KCiAgICBzZXJ2ZXIgewoKICAgICAgICBsaXN0ZW4gODA7CiAgICAgICAgc2VydmVyX25hbWUgd3d3LmhlbGxvLndvcmxkOwoKICAgICAgICBsb2NhdGlvbiAvIHsKCgogICAgICAgICAgICAjIGFsbG93ICdlbXBsb3llZXMnIHRvIHBlcmZvcm0gZHRhYiBvdmVycmlkZXMKICAgICAgICAgICAgaWYgKCRjb29raWVfc3BlY2lhbF9lbXBsb3llZV9jb29raWUgIT0gImxldG1laW4iKSB7CiAgICAgICAgICAgICAgbW9yZV9jbGVhcl9pbnB1dF9oZWFkZXJzICdsNWQtY3R4LSonICdsNWQtZHRhYicgJ2w1ZC1zYW1wbGUnOwogICAgICAgICAgICB9CgogICAgICAgICAgICAjIGFkZCBhIGR0YWIgb3ZlcnJpZGUgdG8gZ2V0IHBlb3BsZSB0byBvdXIgYmV0YSwgd29ybGQtdjIKICAgICAgICAgICAgc2V0ICR4aGVhZGVyICIiOwoKICAgICAgICAgICAgaWYgKCRjb29raWVfc3BlY2lhbF9lbXBsb3llZV9jb29raWUgfiogImRvZ2Zvb2QiKSB7CiAgICAgICAgICAgICAgc2V0ICR4aGVhZGVyICIvaG9zdC93b3JsZCA9PiAvc3J2L3dvcmxkLXYyOyI7CiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIHByb3h5X3NldF9oZWFkZXIgJ2w1ZC1kdGFiJyAkeGhlYWRlcjsKCgogICAgICAgICAgICBwcm94eV9wYXNzIGh0dHA6Ly9sNWQuZGVmYXVsdC5zdmMuY2x1c3Rlci5sb2NhbDsKICAgICAgICAgICAgcHJveHlfc2V0X2hlYWRlciBIb3N0ICRob3N0OwogICAgICAgICAgICBwcm94eV9zZXRfaGVhZGVyIENvbm5lY3Rpb24gIiI7CiAgICAgICAgICAgIHByb3h5X2h0dHBfdmVyc2lvbiAxLjE7CiAgICAgICAgfQogICAgfQp9GgAiAA==",
            "version": 1
        }
    ]
}

其中的value就是 nginx.conf 配置文件的内容。

代码

ConfigMap 结构体的定义:

// ConfigMap holds configuration data for pods to consume.
type ConfigMapstruct{
    metav1.TypeMeta`json:",inline"`
// Standard object's metadata.
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata
// +optional
    metav1.ObjectMeta`json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Data contains the configuration data.
// Each key must be a valid DNS_SUBDOMAIN with an optional leading dot.
// +optional
Data map[string]string`json:"data,omitempty" protobuf:"bytes,2,rep,name=data"`
}

staging/src/k8s.io/client-go/kubernetes/typed/core/v1/configmap.go 中ConfigMap 的接口定义:

// ConfigMapInterface has methods to work with ConfigMap resources.
type ConfigMapInterfaceinterface{
Create(*v1.ConfigMap)(*v1.ConfigMap, error)
Update(*v1.ConfigMap)(*v1.ConfigMap, error)
Delete(name string, options *meta_v1.DeleteOptions) error
DeleteCollection(options *meta_v1.DeleteOptions, listOptions meta_v1.ListOptions) error
Get(name string, options meta_v1.GetOptions)(*v1.ConfigMap, error)
List(opts meta_v1.ListOptions)(*v1.ConfigMapList, error)
Watch(opts meta_v1.ListOptions)(watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string)(result *v1.ConfigMap, err error)
ConfigMapExpansion
}

staging/src/k8s.io/client-go/kubernetes/typed/core/v1/configmap.go 中创建 ConfigMap 的方法如下:

// Create takes the representation of a configMap and creates it.  Returns the server's representation of the configMap, and an error, if there is any.
func (c *configMaps)Create(configMap *v1.ConfigMap)(result *v1.ConfigMap, err error){
    result =&v1.ConfigMap{}
    err = c.client.Post().
Namespace(c.ns).
Resource("configmaps").
Body(configMap).
Do().
Into(result)
return
}

通过 RESTful 请求在 etcd 中存储 ConfigMap 的配置,该方法中设置了资源对象的 namespace 和 HTTP 请求中的 body,执行后将请求结果保存到 result 中返回给调用者。

注意 Body 的结构

// Body makes the request use obj as the body. Optional.
// If obj is a string, try to read a file of that name.
// If obj is a []byte, send it directly.
// If obj is an io.Reader, use it directly.
// If obj is a runtime.Object, marshal it correctly, and set Content-Type header.
// If obj is a runtime.Object and nil, do nothing.
// Otherwise, set an error.

创建 ConfigMap RESTful 请求中的的 Body 中包含 ObjectMetanamespace

HTTP 请求中的结构体:

// Request allows for building up a request to a server in a chained fashion.
// Any errors are stored until the end of your call, so you only have to
// check once.
type Requeststruct{
// required
    client HTTPClient
    verb   string
    baseURL     *url.URL
    content     ContentConfig
    serializers Serializers
// generic components accessible via method setters
    pathPrefix string
    subpath    string
params     url.Values
    headers    http.Header
// structural elements of the request that are part of the Kubernetes API conventions
namespacestring
    namespaceSet bool
    resource     string
    resourceName string
    subresource  string
    timeout      time.Duration
// output
    err  error
    body io.Reader
// This is only used for per-request timeouts, deadlines, and cancellations.
    ctx context.Context
    backoffMgr BackoffManager
    throttle   flowcontrol.RateLimiter
}

测试

分别测试使用 ConfigMap 挂载 Env 和 Volume 的情况。

更新使用ConfigMap挂载的Env

使用下面的配置创建 nginx 容器测试更新 ConfigMap 后容器内的环境变量是否也跟着更新。

apiVersion: extensions/v1beta1
kind:Deployment
metadata:
  name:my-nginx
spec:
  replicas:1
template:
    metadata:
      labels:
        run:my-nginx
    spec:
      containers:
- name:my-nginx
        image: sz-pg-oam-docker-hub-001.tendcloud.com/library/nginx:1.9
        ports:
- containerPort:80
        envFrom:
- configMapRef:
            name: env-config
---
apiVersion: v1
kind:ConfigMap
metadata:
  name: env-config
namespace:default
data:
  log_level: INFO

获取环境变量的值

$ kubectl exec `kubectl get pods -l run=my-nginx  -o=name|cut -d "/" -f2` env|grep log_level
log_level=INFO

修改 ConfigMap

$ kubectl edit configmap env-config

修改 log_level 的值为 DEBUG

再次查看环境变量的值。

$ kubectl exec `kubectl get pods -l run=my-nginx  -o=name|cut -d "/" -f2` env|grep log_level
log_level=INFO

实践证明修改 ConfigMap 无法更新容器中已注入的环境变量信息。

更新使用ConfigMap挂载的Volume

使用下面的配置创建 nginx 容器测试更新 ConfigMap 后容器内挂载的文件是否也跟着更新。

apiVersion: extensions/v1beta1
kind:Deployment
metadata:
  name:my-nginx
spec:
  replicas:1
template:
    metadata:
      labels:
        run:my-nginx
    spec:
      containers:
- name:my-nginx
        image: sz-pg-oam-docker-hub-001.tendcloud.com/library/nginx:1.9
        ports:
- containerPort:80
      volumeMounts:
- name: config-volume
        mountPath:/etc/config
      volumes:
- name: config-volume
          configMap:
            name: special-config
---
apiVersion: v1
kind:ConfigMap
metadata:
  name: special-config
namespace:default
data:
  log_level: INFO

$ kubectl exec `kubectl get pods -l run=my-nginx  -o=name|cut -d "/" -f2` cat /tmp/log_level
INFO

修改 ConfigMap

$ kubectl edit configmap special-config

修改 log_level 的值为 DEBUG

等待大概10秒钟时间,再次查看环境变量的值。

$ kubectl exec `kubectl get pods -l run=my-nginx  -o=name|cut -d "/" -f2` cat /tmp/log_level
DEBUG

我们可以看到使用 ConfigMap 方式挂载的 Volume 的文件中的内容已经变成了 DEBUG

总结

更新 ConfigMap 后:

  • 使用该 ConfigMap 挂载的 Env 不会同步更新
  • 使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间(实测大概10秒)才能同步更新

ENV 是在容器启动的时候注入的,启动之后 kubernetes 就不会再改变环境变量的值,且同一个 namespace 中的 pod 的环境变量是不断累加的,参考 Kubernetes中的服务发现与docker容器间的环境变量传递源码探究。为了更新容器中使用 ConfigMap 挂载的配置,可以通过滚动更新 pod 的方式来强制重新挂载 ConfigMap,也可以在更新了 ConfigMap 后,先将副本数设置为 0,然后再扩容。

Horizontal Pod Autoscaling

应用的资源使用率通常都有高峰和低谷的时候,如何削峰填谷,提高集群的整体资源利用率,让service中的Pod个数自动调整呢?这就有赖于Horizontal Pod Autoscaling了,顾名思义,使Pod水平自动缩放。这个Object(跟Pod、Deployment一样都是API resource)也是最能体现kubernetes之于传统运维价值的地方,不再需要手动扩容了,终于实现自动化了,还可以自定义指标,没准未来还可以通过人工智能自动进化呢!

HPA属于Kubernetes中的autoscaling SIG(Special Interest Group),其下有两个feature:

Kubernetes自1.2版本引入HPA机制,到1.6版本之前一直是通过kubelet来获取监控指标来判断是否需要扩缩容,1.6版本之后必须通过API server、Heapseter或者kube-aggregator来获取监控指标。

对于1.6以前版本中开启自定义HPA请参考[@marko](https://medium.com/.luksa/kubernetes-autoscaling-based-on-custom-metrics-without-using-a-host-port-b783ed6241ac">Kubernetes autoscaling based on custom metrics without using a host port,对于1.7及以上版本请参考Configure Kubernetes Autoscaling With Custom Metrics in Kubernetes 1.7 - Bitnami

HPA解析

Horizontal Pod Autoscaling仅适用于Deployment和ReplicationController,在V1版本中仅支持根据Pod的CPU利用率扩所容,在v1alpha版本中,支持根据内存和用户自定义的metric扩缩容。

如果你不想看下面的文章可以直接看下面的示例图,组件交互、组件的配置、命令示例,都画在图上了。

Horizontal Pod Autoscaling由API server和controller共同实现。

Metrics支持

在不同版本的API中,HPA autoscale时可以根据以下指标来判断:

  • autoscaling/v1
    • CPU
  • autoscaling/v2alpha1
    • 内存
    • 自定义metrics
      • kubernetes1.6起支持自定义metrics,但是必须在kube-controller-manager中配置如下两项:
        • --horizontal-pod-autoscaler-use-rest-clients=true
        • --api-server指向kube-aggregator,也可以使用heapster来实现,通过在启动heapster的时候指定--api-server=true。查看kubernetes metrics
    • 多种metrics组合
      • HPA会根据每个metric的值计算出scale的值,并将最大的那个指作为扩容的最终结果。

使用kubectl管理

Horizontal Pod Autoscaling作为API resource也可以像Pod、Deployment一样使用kubeclt命令管理,使用方法跟它们一样,资源名称为hpa

kubectl create hpa
kubectl get hpa
kubectl describe hpa
kubectl delete hpa

有一点不同的是,可以直接使用kubectl autoscale直接通过命令行的方式创建Horizontal Pod Autoscaler。

用法如下:

kubectl autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS
[--cpu-percent=CPU] [flags] [options]

举个例子:

kubectl autoscale deployment foo --min=2 --max=5 --cpu-percent=80

为Deployment foo创建 一个autoscaler,当Pod的CPU利用率达到80%的时候,RC的replica数在2到5之间。该命令的详细使用文档见https://kubernetes.io/docs/user-guide/kubectl/v1.6/#autoscale

注意 :如果为ReplicationController创建HPA的话,无法使用rolling update,但是对于Deployment来说是可以的,因为Deployment在执行rolling update的时候会自动创建新的ReplicationController。

什么是 Horizontal Pod Autoscaling?

利用 Horizontal Pod Autoscaling,kubernetes 能够根据监测到的 CPU 利用率(或者在 alpha 版本中支持的应用提供的 metric)自动的扩容 replication controller,deployment 和 replica set。

Horizontal Pod Autoscaler 作为 kubernetes API resource 和 controller 的实现。Resource 确定 controller 的行为。Controller 会根据监测到用户指定的目标的 CPU 利用率周期性得调整 replication controller 或 deployment 的 replica 数量。

Horizontal Pod Autoscaler 如何工作?

Horizontal Pod Autoscaler 由一个控制循环实现,循环周期由 controller manager 中的 --horizontal-pod-autoscaler-sync-period 标志指定(默认是 30 秒)。

在每个周期内,controller manager 会查询 HorizontalPodAutoscaler 中定义的 metric 的资源利用率。Controller manager 从 resource metric API(每个 pod 的 resource metric)或者自定义 metric API(所有的metric)中获取 metric。

  • 每个 Pod 的 resource metric(例如 CPU),controller 通过 resource metric API 获取 HorizontalPodAutoscaler 中定义的每个 Pod 中的 metric。然后,如果设置了目标利用率,controller 计算利用的值与每个 Pod 的容器里的 resource request 值的百分比。如果设置了目标原始值,将直接使用该原始 metric 值。然后 controller 计算所有目标 Pod 的利用率或原始值(取决于所指定的目标类型)的平均值,产生一个用于缩放所需 replica 数量的比率。 请注意,如果某些 Pod 的容器没有设置相关的 resource request ,则不会定义 Pod 的 CPU 利用率,并且 Aucoscaler 也不会对该 metric 采取任何操作。 有关自动缩放算法如何工作的更多细节,请参阅 自动缩放算法设计文档
  • 对于每个 Pod 自定义的 metric,controller 功能类似于每个 Pod 的 resource metric,只是它使用原始值而不是利用率值。
  • 对于 object metric,获取单个度量(描述有问题的对象),并与目标值进行比较,以产生如上所述的比率。

HorizontalPodAutoscaler 控制器可以以两种不同的方式获取 metric :直接的 Heapster 访问和 REST 客户端访问。

当使用直接的 Heapster 访问时,HorizontalPodAutoscaler 直接通过 API 服务器的服务代理子资源查询 Heapster。需要在集群上部署 Heapster 并在 kube-system namespace 中运行。

有关 REST 客户端访问的详细信息,请参阅 支持自定义度量

Autoscaler 访问相应的 replication controller,deployment 或 replica set 来缩放子资源。

Scale 是一个允许您动态设置副本数并检查其当前状态的接口。

有关缩放子资源的更多细节可以在 这里 找到。

API Object

Horizontal Pod Autoscaler 是 kubernetes 的 autoscaling API 组中的 API 资源。当前的稳定版本中,只支持 CPU 自动扩缩容,可以在autoscaling/v1 API 版本中找到。

在 alpha 版本中支持根据内存和自定义 metric 扩缩容,可以在autoscaling/v2alpha1 中找到。autoscaling/v2alpha1 中引入的新字段在autoscaling/v1 中是做为 annotation 而保存的。

关于该 API 对象的更多信息,请参阅 HorizontalPodAutoscaler Object

在 kubectl 中支持 Horizontal Pod Autoscaling

Horizontal Pod Autoscaler 和其他的所有 API 资源一样,通过 kubectl 以标准的方式支持。

我们可以使用kubectl create命令创建一个新的 autoscaler。

我们可以使用kubectl get hpa列出所有的 autoscaler,使用kubectl describe hpa获取其详细信息。

最后我们可以使用kubectl delete hpa删除 autoscaler。

另外,可以使用kubectl autoscale命令,很轻易的就可以创建一个 Horizontal Pod Autoscaler。

例如,执行kubectl autoscale rc foo —min=2 —max=5 —cpu-percent=80命令将为 replication controller foo 创建一个 autoscaler,目标的 CPU 利用率是80%,replica 的数量介于 2 和 5 之间。

关于kubectl autoscale的更多信息请参阅 这里

滚动更新期间的自动扩缩容

目前在Kubernetes中,可以通过直接管理 replication controller 或使用 deployment 对象来执行 滚动更新,该 deployment 对象为您管理基础 replication controller。

Horizontal Pod Autoscaler 仅支持后一种方法:Horizontal Pod Autoscaler 被绑定到 deployment 对象,它设置 deployment 对象的大小,deployment 负责设置底层 replication controller 的大小。

Horizontal Pod Autoscaler 不能使用直接操作 replication controller 进行滚动更新,即不能将 Horizontal Pod Autoscaler 绑定到 replication controller,并进行滚动更新(例如使用kubectl rolling-update)。

这不行的原因是,当滚动更新创建一个新的 replication controller 时,Horizontal Pod Autoscaler 将不会绑定到新的 replication controller 上。

支持多个 metric

Kubernetes 1.6 中增加了支持基于多个 metric 的扩缩容。您可以使用autoscaling/v2alpha1 API 版本来为 Horizontal Pod Autoscaler 指定多个 metric。然后 Horizontal Pod Autoscaler controller 将权衡每一个 metric,并根据该 metric 提议一个新的 scale。在所有提议里最大的那个 scale 将作为最终的 scale。

支持自定义 metric

注意: Kubernetes 1.2 根据特定于应用程序的 metric ,通过使用特殊注释的方式,增加了对缩放的 alpha 支持。

在 Kubernetes 1.6中删除了对这些注释的支持,有利于autoscaling/v2alpha1 API。 虽然旧的收集自定义 metric 的旧方法仍然可用,但是这些 metric 将不可供 Horizontal Pod Autoscaler 使用,并且用于指定要缩放的自定义 metric 的以前的注释也不在受 Horizontal Pod Autoscaler 认可。

Kubernetes 1.6增加了在 Horizontal Pod Autoscale r中使用自定义 metric 的支持。

您可以为autoscaling/v2alpha1 API 中使用的 Horizontal Pod Autoscaler 添加自定义 metric 。

Kubernetes 然后查询新的自定义 metric API 来获取相应自定义 metric 的值。

自定义指标HPA

Kubernetes中不仅支持CPU、内存为指标的HPA,还支持自定义指标的HPA,例如QPS。

本文中使用的yaml文件见manifests/HPA

设置自定义指标

kubernetes1.6

在kubernetes1.6集群中配置自定义指标的HPA的说明已废弃。

在设置定义指标HPA之前需要先进行如下配置:

  • 将heapster的启动参数 --api-server 设置为 true

  • 启用custom metric API

  • 将kube-controller-manager的启动参数中--horizontal-pod-autoscaler-use-rest-clients设置为true,并指定--master为API server地址,如
    ```

  • -master=http://172.20.0.113:8080

在kubernetes1.5以前很容易设置,参考[@marko](https://medium.com/.luksa/kubernetes-autoscaling-based-on-custom-metrics-without-using-a-host-port-b783ed6241ac">1.6以前版本的kubernetes中开启自定义HPA,而在1.6中因为取消了原来的annotation方式设置custom metric,只能通过API server和kube-aggregator来获取custom metric,因为只有两种方式来设置了,一是直接通过API server获取heapster的metrics,二是部署kube-aggragator来实现。

我们将在kubernetes1.8版本的kubernetes中,使用聚合的API server来实现自定义指标的HPA。

kuberentes1.7+

确认您的kubernetes版本在1.7或以上,修改以下配置:

  • 将kube-controller-manager的启动参数中--horizontal-pod-autoscaler-use-rest-clients设置为true,并指定--master为API server地址,如`—master=http://172.20.0.113:8080
  • 修改kube-apiserver的配置文件apiserver,增加一条配置--requestheader-client-ca-file=/etc/kubernetes/ssl/ca.pem --requestheader-allowed-names=aggregator --requestheader-extra-headers-prefix=X-Remote-Extra- --requestheader-group-headers=X-Remote-Group --requestheader-username-headers=X-Remote-User --proxy-client-cert-file=/etc/kubernetes/ssl/kubernetes.pem --proxy-client-key-file=/etc/kubernetes/ssl/kubernetes-key.pem,用来配置aggregator的CA证书。

已经内置了apiregistration.k8s.io/v1beta1 API,可以直接定义APIService,如:

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1.custom-metrics.metrics.k8s.io
spec:
  insecureSkipTLSVerify: true
  group: custom-metrics.metrics.k8s.io
  groupPriorityMinimum: 1000
  versionPriority: 5
  service:
    name: api
    namespace: custom-metrics
  version: v1alpha1

部署Prometheus

使用prometheus-operator.yaml文件部署Prometheus operator。

注意:将镜像修改为你自己的镜像仓库地址。

这产生一个自定义的API:http://172.20.0.113:8080/apis/custom-metrics.metrics.k8s.io/v1alpha1,可以通过浏览器访问,还可以使用下面的命令可以检查该API:

$ kubectl get --raw=apis/custom-metrics.metrics.k8s.io/v1alpha1
{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"custom-metrics.metrics.k8s.io/v1alpha1","resources":[{"name":"jobs.batch/http_requests","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"namespaces/http_requests","singularName":"","namespaced":false,"kind":"MetricValueList","verbs":["get"]},{"name":"jobs.batch/up","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"pods/up","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"services/scrape_samples_scraped","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"namespaces/scrape_samples_scraped","singularName":"","namespaced":false,"kind":"MetricValueList","verbs":["get"]},{"name":"pods/scrape_duration_seconds","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"services/scrape_duration_seconds","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"pods/http_requests","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"pods/scrape_samples_post_metric_relabeling","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"jobs.batch/scrape_samples_scraped","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"jobs.batch/scrape_duration_seconds","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"namespaces/scrape_duration_seconds","singularName":"","namespaced":false,"kind":"MetricValueList","verbs":["get"]},{"name":"namespaces/scrape_samples_post_metric_relabeling","singularName":"","namespaced":false,"kind":"MetricValueList","verbs":["get"]},{"name":"services/scrape_samples_post_metric_relabeling","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"services/up","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"pods/scrape_samples_scraped","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"services/http_requests","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"jobs.batch/scrape_samples_post_metric_relabeling","singularName":"","namespaced":true,"kind":"MetricValueList","verbs":["get"]},{"name":"namespaces/up","singularName":"","namespaced":false,"kind":"MetricValueList","verbs":["get"]}]}

Label

Label是附着到object上(例如Pod)的键值对。可以在创建object的时候指定,也可以在object创建后随时指定。Labels的值对系统本身并没有什么含义,只是对用户才有意义。

"labels": {
  "key1" : "value1",
  "key2" : "value2"
}

Kubernetes最终将对labels最终索引和反向索引用来优化查询和watch,在UI和命令行中会对它们排序。不要在label中使用大型、非标识的结构化数据,记录这样的数据应该用annotation。

动机

Label能够将组织架构映射到系统架构上(就像是康威定律),这样能够更便于微服务的管理,你可以给object打上如下类型的label:

  • "release" : "stable", "release" : "canary"
  • "environment" : "dev", "environment" : "qa", "environment" : "production"
  • "tier" : "frontend", "tier" : "backend", "tier" : "cache"
  • "partition" : "customerA", "partition" : "customerB"
  • "track" : "daily", "track" : "weekly"
  • "team" : "teamA","team:" : "teamB"

语法和字符集

Label key的组成:

  • 不得超过63个字符
  • 可以使用前缀,使用/分隔,前缀必须是DNS子域,不得超过253个字符,系统中的自动化组件创建的label必须指定前缀,kubernetes.io/由kubernetes保留
  • 起始必须是字母(大小写都可以)或数字,中间可以有连字符、下划线和点

Label value的组成:

  • 不得超过63个字符
  • 起始必须是字母(大小写都可以)或数字,中间可以有连字符、下划线和点

Label selector

Label不是唯一的,很多object可能有相同的label。

通过label selector,客户端/用户可以指定一个object集合,通过label selector对object的集合进行操作。

Label selector有两种类型:

  • equality-based :可以使用===!=操作符,可以使用逗号分隔多个表达式
  • set-based :可以使用innotin!操作符,另外还可以没有操作符,直接写出某个label的key,表示过滤有某个key的object而不管该key的value是何值,!表示没有该label的object

示例

$ kubectl get pods -l environment=production,tier=frontend
$ kubectl get pods -l 'environment in (production),tier in (frontend)'
$ kubectl get pods -l 'environment in (production, qa)'
$ kubectl get pods -l 'environment,environment notin (frontend)'

在API object中设置label selector

servicereplicationcontroller等object中有对pod的label selector,使用方法只能使用等于操作,例如:

selector:
    component: redis

JobDeploymentReplicaSetDaemonSet这些object中,支持set-based的过滤,例如:

selector:
  matchLabels:
    component: redis
  matchExpressions:
    - {key: tier, operator: In, values: [cache]}
    - {key: environment, operator: NotIn, values: [dev]}

如Service通过label selector将同一类型的pod作为一个服务expose出来。

另外在node affinity和pod affinity中的label selector的语法又有些许不同,示例如下:

  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value

垃圾收集

Kubernetes 垃圾收集器的角色是删除指定的对象,这些对象曾经有但以后不再拥有 Owner 了。

注意:垃圾收集是 beta 特性,在 Kubernetes 1.4 及以上版本默认启用。

Owner 和 Dependent

一些 Kubernetes 对象是其它一些的 Owner。例如,一个 ReplicaSet 是一组 Pod 的 Owner。具有 Owner 的对象被称为是 Owner 的 Dependent。每个 Dependent 对象具有一个指向其所属对象的 metadata.ownerReferences 字段。

有时,Kubernetes 会自动设置 ownerReference 的值。例如,当创建一个 ReplicaSet 时,Kubernetes 自动设置 ReplicaSet 中每个 Pod 的 ownerReference 字段值。在 1.6 版本,Kubernetes 会自动为一些对象设置 ownerReference 的值,这些对象是由 ReplicationController、ReplicaSet、StatefulSet、DaemonSet 和 Deployment 所创建或管理。

也可以通过手动设置 ownerReference 的值,来指定 Owner 和 Dependent 之间的关系。

这有一个配置文件,表示一个具有 3 个 Pod 的 ReplicaSet:

apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
  name: my-repset
spec:
  replicas: 3
  selector:
    matchLabels:
      pod-is-for: garbage-collection-example
  template:
    metadata:
      labels:
        pod-is-for: garbage-collection-example
    spec:
      containers:
      - name: nginx
        image: nginx

如果创建该 ReplicaSet,然后查看 Pod 的 metadata 字段,能够看到 OwnerReferences 字段:

kubectl create -f https://k8s.io/docs/concepts/abstractions/controllers/my-repset.yaml
kubectl get pods --output=yaml

输出显示了 Pod 的 Owner 是名为 my-repset 的 ReplicaSet:

apiVersion: v1
kind: Pod
metadata:
  ...
  ownerReferences:
  - apiVersion: extensions/v1beta1
    controller: true
    blockOwnerDeletion: true
    kind: ReplicaSet
    name: my-repset
    uid: d9607e19-f88f-11e6-a518-42010a800195
  ...

控制垃圾收集器删除 Dependent

当删除对象时,可以指定是否该对象的 Dependent 也自动删除掉。自动删除 Dependent 也称为 级联删除*。Kubernetes 中有两种 *级联删除 的模式:background 模式和 foreground 模式。

如果删除对象时,不自动删除它的 Dependent,这些 Dependent 被称作是原对象的 孤儿

Background 级联删除

background 级联删除 模式下,Kubernetes 会立即删除 Owner 对象,然后垃圾收集器会在后台删除这些 Dependent。

Foreground 级联删除

foreground 级联删除 模式下,根对象首先进入 “删除中” 状态。在 “删除中” 状态会有如下的情况:

  • 对象仍然可以通过 REST API 可见
  • 会设置对象的 deletionTimestamp 字段
  • 对象的 metadata.finalizers 字段包含了值 “foregroundDeletion”

一旦被设置为 “删除中” 状态,垃圾收集器会删除对象的所有 Dependent。垃圾收集器删除了所有 “Blocking” 的 Dependent(对象的 ownerReference.blockOwnerDeletion=true)之后,它会删除 Owner 对象。

注意,在 “foreground 删除” 模式下,Dependent 只有通过 ownerReference.blockOwnerDeletion 才能阻止删除 Owner 对象。在 Kubernetes 1.7 版本中将增加 admission controller,基于 Owner 对象上的删除权限来控制用户去设置 blockOwnerDeletion 的值为 true,所以未授权的 Dependent 不能够延迟 Owner 对象的删除。

如果一个对象的ownerReferences 字段被一个 Controller(例如 Deployment 或 ReplicaSet)设置,blockOwnerDeletion 会被自动设置,没必要手动修改这个字段。

设置级联删除策略

通过为 Owner 对象设置 deleteOptions.propagationPolicy 字段,可以控制级联删除策略。可能的取值包括:“orphan”、“Foreground” 或 “Background”。

对很多 Controller 资源,包括 ReplicationController、ReplicaSet、StatefulSet、DaemonSet 和 Deployment,默认的垃圾收集策略是 orphan。因此,除非指定其它的垃圾收集策略,否则所有 Dependent 对象使用的都是 orphan 策略。

下面是一个在后台删除 Dependent 对象的例子:

kubectl proxy --port=8080
curl -X DELETE localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/my-repset \
-d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Background"}' \
-H "Content-Type: application/json"

下面是一个在前台删除 Dependent 对象的例子:

kubectl proxy --port=8080
curl -X DELETE localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/my-repset \
-d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Foreground"}' \
-H "Content-Type: application/json"

下面是一个孤儿 Dependent 的例子:

kubectl proxy --port=8080
curl -X DELETE localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/my-repset \
-d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Orphan"}' \
-H "Content-Type: application/json"

kubectl 也支持级联删除。 通过设置 --cascade 为 true,可以使用 kubectl 自动删除 Dependent 对象。设置 --cascade 为 false,会使 Dependent 对象成为孤儿 Dependent 对象。--cascade 的默认值是 true。

下面是一个例子,使一个 ReplicaSet 的 Dependent 对象成为孤儿 Dependent:

kubectl delete replicaset my-repset --cascade=false

Network Policy

网络策略说明一组 Pod 之间是如何被允许互相通信,以及如何与其它网络 Endpoint 进行通信。 NetworkPolicy 资源使用标签来选择 Pod,并定义了一些规则,这些规则指明允许什么流量进入到选中的 Pod 上。

前提条件

网络策略通过网络插件来实现,所以必须使用一种支持 NetworkPolicy 的网络方案 —— 非 Controller 创建的资源,是不起作用的。

隔离的与未隔离的 Pod

默认 Pod 是未隔离的,它们可以从任何的源接收请求。 具有一个可以选择 Pod 的网络策略后,Pod 就会变成隔离的。 一旦 Namespace 中配置的网络策略能够选择一个特定的 Pod,这个 Pod 将拒绝任何该网络策略不允许的连接。(Namespace 中其它未被网络策略选中的 Pod 将继续接收所有流量)

NetworkPolicy 资源

查看 API参考 可以获取该资源的完整定义。

下面是一个 NetworkPolicy 的例子:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379

将上面配置 POST 到 API Server 将不起任何作用,除非选择的网络方案支持网络策略。

必选字段:像所有其它 Kubernetes 配置一样, NetworkPolicy 需要 apiVersionkindmetadata 这三个字段,关于如何使用配置文件的基本信息,可以查看 这里这里这里

specNetworkPolicy spec 具有在给定 Namespace 中定义特定网络的全部信息。

podSelector:每个 NetworkPolicy 包含一个 podSelector,它可以选择一组应用了网络策略的 Pod。由于 NetworkPolicy 当前只支持定义 ingress 规则,这个 podSelector 实际上为该策略定义了一组 “目标Pod”。示例中的策略选择了标签为 “role=db” 的 Pod。一个空的 podSelector 选择了该 Namespace 中的所有 Pod。

ingress:每个NetworkPolicy 包含了一个白名单 ingress 规则列表。每个规则只允许能够匹配上 fromports配置段的流量。示例策略包含了单个规则,它从这两个源中匹配在单个端口上的流量,第一个是通过namespaceSelector 指定的,第二个是通过 podSelector 指定的。

因此,上面示例的 NetworkPolicy:

  1. 在 “default” Namespace中 隔离了标签 “role=db” 的 Pod(如果他们还没有被隔离)
  2. 在 “default” Namespace中,允许任何具有 “role=frontend” 的 Pod,连接到标签为 “role=db” 的 Pod 的 TCP 端口 6379
  3. 允许在 Namespace 中任何具有标签 “project=myproject” 的 Pod,连接到 “default” Namespace 中标签为 “role=db” 的 Pod 的 TCP 端口 6379

查看 NetworkPolicy 入门指南 给出的更进一步的例子。

默认策略

通过创建一个可以选择所有 Pod 但不允许任何流量的 NetworkPolicy,你可以为一个 Namespace 创建一个 “默认的” 隔离策略,如下所示:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector:

这确保了即使是没有被任何 NetworkPolicy 选中的 Pod,将仍然是被隔离的。

可选地,在 Namespace 中,如果你想允许所有的流量进入到所有的 Pod(即使已经添加了某些策略,使一些 Pod 被处理为 “隔离的”),你可以通过创建一个策略来显式地指定允许所有流量:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
spec:
  podSelector:
  ingress:
  - {}

Annotation

Annotation,顾名思义,就是注解。Annotation可以将Kubernetes资源对象关联到任意的非标识性元数据。使用客户端(如工具和库)可以检索到这些元数据。

关联元数据到对象

Label和Annotation都可以将元数据关联到Kubernetes资源对象。Label主要用于选择对象,可以挑选出满足特定条件的对象。相比之下,annotation 不能用于标识及选择对象。annotation中的元数据可多可少,可以是结构化的或非结构化的,也可以包含label中不允许出现的字符。

annotation和label一样都是key/value键值对映射结构:

"annotations": {
  "key1" : "value1",
  "key2" : "value2"
}

以下列出了一些可以记录在 annotation 中的对象信息:

  • 声明配置层管理的字段。使用annotation关联这类字段可以用于区分以下几种配置来源:客户端或服务器设置的默认值,自动生成的字段或自动生成的 auto-scaling 和 auto-sizing 系统配置的字段。

  • 创建信息、版本信息或镜像信息。例如时间戳、版本号、git分支、PR序号、镜像哈希值以及仓库地址。

  • 记录日志、监控、分析或审计存储仓库的指针

  • 可以用于debug的客户端(库或工具)信息,例如名称、版本和创建信息。

  • 用户信息,以及工具或系统来源信息、例如来自非Kubernetes生态的相关对象的URL信息。

  • 轻量级部署工具元数据,例如配置或检查点。

  • 负责人的电话或联系方式,或能找到相关信息的目录条目信息,例如团队网站。

如果不使用annotation,您也可以将以上类型的信息存放在外部数据库或目录中,但这样做不利于创建用于部署、管理、内部检查的共享工具和客户端库。

示例

如 Istio 的 Deployment 配置中就使用到了 annotation:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: istio-manager
spec:
  replicas: 1
  template:
    metadata:
      annotations:
        alpha.istio.io/sidecar: ignore
      labels:
        istio: manager
    spec:
      serviceAccountName: istio-manager-service-account
      containers:
      - name: discovery
        image: sz-pg-oam-docker-hub-001.tendcloud.com/library/manager:0.1.5
        imagePullPolicy: Always
        args: ["discovery", "-v", "2"]
        ports:
        - containerPort: 8080
        env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
      - name: apiserver
        image: sz-pg-oam-docker-hub-001.tendcloud.com/library/manager:0.1.5
        imagePullPolicy: Always
        args: ["apiserver", "-v", "2"]
        ports:
        - containerPort: 8081
        env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace

alpha.istio.io/sidecar 注解就是用来控制是否自动向 pod 中注入 sidecar 的。参考:安装 Istio sidecar - istio.doczh.cn

Aggregated API Server

Aggregated(聚合的)API server是为了将原来的API server这个巨石(monolithic)应用给拆分成,为了方便用户开发自己的API server集成进来,而不用直接修改kubernetes官方仓库的代码,这样一来也能将API server解耦,方便用户使用实验特性。这些API server可以跟core API server无缝衔接,使用kubectl也可以管理它们。

架构

我们需要创建一个新的组件,名为kube-aggregator,它需要负责以下几件事:

  • 提供用于注册API server的API
  • 汇总所有的API server信息
  • 代理所有的客户端到API server的请求

注意:这里说的API server是一组“API Server”,而不是说我们安装集群时候的那个API server,而且这组API server是可以横向扩展的。

关于聚合的API server的更多信息请参考:Aggregated API Server

安装配置聚合的API server

有两种方式来启用kube-aggregator

  • 使用test mode/single-user mode,作为一个独立的进程来运行
  • 使用gateway modekube-apiserver将嵌入到kbe-aggregator组件中,它将作为一个集群的gateway,用来聚合所有apiserver。

kube-aggregator二进制文件已经包含在kubernetes release里面了。

使用自定义资源扩展API

注意:TPR已经停止维护,kubernetes 1.7及以上版本请使用CRD。

自定义资源是对Kubernetes API的扩展,kubernetes中的每个资源都是一个API对象的集合,例如我们在YAML文件里定义的那些spec都是对kubernetes中的资源对象的定义,所有的自定义资源可以跟kubernetes中内建的资源一样使用kubectl操作。

自定义资源

Kubernetes1.6版本中包含一个内建的资源叫做TPR(ThirdPartyResource),可以用它来创建自定义资源,但该资源在kubernetes1.7中版本已被CRD(CustomResourceDefinition)取代。

扩展API

自定义资源实际上是为了扩展kubernetes的API,向kubenetes API中增加新类型,可以使用以下三种方式:

  • 修改kubenetes的源码,显然难度比较高,也不太合适
  • 创建自定义API server并聚合到API中
  • 1.7以下版本编写TPR,kubernetes1.7及以上版本用CRD

编写自定义资源是扩展kubernetes API的最简单的方式,是否编写自定义资源来扩展API请参考Should I add a custom resource to my Kubernetes Cluster?,行动前请先考虑以下几点:

  • 你的API是否属于声明式的
  • 是否想使用kubectl命令来管理
  • 是否要作为kubenretes中的对象类型来管理,同时显示在kuberetes dashboard上
  • 是否可以遵守kubernetes的API规则限制,例如URL和API group、namespace限制
  • 是否可以接受该API只能作用于集群或者namespace范围
  • 想要复用kubernetes API的公共功能,比如CRUD、watch、内置的认证和授权等

如果这些都不是你想要的,那么你可以开发一个独立的API。

TPR

注意:TPR已经停止维护,kubernetes 1.7及以上版本请使用CRD。

假如我们要创建一个名为cron-tab.stable.example.com的TPR,yaml文件定义如下:

apiVersion: extensions/v1beta1
kind: ThirdPartyResource
metadata:
  name: cron-tab.stable.example.com
description: "A specification of a Pod to run on a cron style schedule"
versions:
- name: v1

然后使用kubectl create命令创建该资源,这样就可以创建出一个API端点/apis/stable.example.com/v1/namespaces/<namespace>/crontabs/...

下面是在Linkerd中的一个实际应用,Linkerd中的一个名为namerd的组件使用了TPR,定义如下:

---
kind: ThirdPartyResource
apiVersion: extensions/v1beta1
metadata:
  name: d-tab.l5d.io
description: stores dtabs used by namerd
versions:
- name: v1alpha1

CRD

参考下面的CRD,resourcedefinition.yaml:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  # 名称必须符合下面的格式:<plural>.<group>
  name: crontabs.stable.example.com
spec:
  # REST API使用的组名称:/apis/<group>/<version>
  group: stable.example.com
  # REST API使用的版本号:/apis/<group>/<version>
  version: v1
  # Namespaced或Cluster
  scope: Namespaced
  names:
    # URL中使用的复数名称: /apis/<group>/<version>/<plural>
    plural: crontabs
    # CLI中使用的单数名称
    singular: crontab
    # CamelCased格式的单数类型。在清单文件中使用
    kind: CronTab
    # CLI中使用的资源简称
    shortNames:
    - ct

创建该CRD:

kubectl create -f resourcedefinition.yaml

访问RESTful API端点如http://172.20.0.113:8080将看到如下API端点已创建:

/apis/stable.example.com/v1/namespaces/*/crontabs/...

创建自定义对象

如下所示:

apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
  name: my-new-cron-object
spec:
  cronSpec: "* * * * /5"
  image: my-awesome-cron-image

引用该自定义资源的API创建对象。

终止器

可以为自定义对象添加一个终止器,如下所示:

apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
  finalizers:
  - finalizer.stable.example.com

删除自定义对象前,异步执行的钩子。对于具有终止器的一个对象,删除请求仅仅是为metadata.deletionTimestamp字段设置一个值,而不是删除它,这将触发监控该对象的控制器执行他们所能处理的任意终止器。

详情参考:Extend the Kubernetes API with CustomResourceDefinitions

使用kubernetes1.7及以上版本请参考Migrate a ThirdPartyResource to CustomResourceDefinition

自定义控制器

单纯设置了自定义资源,并没有什么用,只有跟自定义控制器结合起来,才能讲资源对象中的声明式API翻译成用户所期望的状态。自定义控制器可以用来管理任何资源类型,但是一般是跟自定义资源结合使用。

请参考使用Operator模式,该模式可以让开发者将自己的领域知识转换成特定的kubenretes API扩展。

API server聚合

Aggregated(聚合的)API server是为了将原来的API server这个巨石(monolithic)应用给拆分成,为了方便用户开发自己的API server集成进来,而不用直接修改kubernetes官方仓库的代码,这样一来也能将API server解耦,方便用户使用实验特性。这些API server可以跟core API server无缝衔接,试用kubectl也可以管理它们。

APIService

APIService是用来表示一个特定的GroupVersion的中的server,它的结构定义位于代码staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/types.go中。

下面是一个APIService的示例配置:

apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
  name: v1alpha1.custom-metrics.metrics.k8s.io
spec:
  insecureSkipTLSVerify: true
  group: custom-metrics.metrics.k8s.io
  groupPriorityMinimum: 1000
  versionPriority: 5
  service:
    name: api
    namespace: custom-metrics
  version: v1alpha1

APIService详解

使用apiregistration.k8s.io/v1beta1 版本的APIService,在metadata.name中定义该API的名字。

使用上面的yaml的创建v1alpha1.custom-metrics.metrics.k8s.io APIService。

  • insecureSkipTLSVerify:当与该服务通信时,禁用TLS证书认证。强加建议不要设置这个参数,默认为 false。应该使用CABundle代替。
  • service:与该APIService通信时引用的service,其中要注明service的名字和所属的namespace,如果为空的话,则所有的服务都会该API groupversion将在本地443端口处理所有通信。
  • groupPriorityMinimum:该组API的处理优先级,主要排序是基于groupPriorityMinimum,该数字越大表明优先级越高,客户端就会与其通信处理请求。次要排序是基于字母表顺序,例如v1.bar比v1.foo的优先级更高。
  • versionPriority:VersionPriority控制其组内的API版本的顺序。必须大于零。主要排序基于VersionPriority,从最高到最低(20大于10)排序。次要排序是基于对象名称的字母比较。 (v1.foo在v1.bar之前)由于它们都是在一个组内,因此数字可能很小,一般都小于10。

查看我们使用上面的yaml文件创建的APIService。

kubectl get apiservice v1alpha1.custom-metrics.metrics.k8s.io -o yaml

apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
  creationTimestamp: 2017-12-14T08:27:35Z
  name: v1alpha1.custom-metrics.metrics.k8s.io
  resourceVersion: "35194598"
  selfLink: /apis/apiregistration.k8s.io/v1beta1/apiservices/v1alpha1.custom-metrics.metrics.k8s.io
  uid: a31a3412-e0a8-11e7-9fa4-f4e9d49f8ed0
spec:
  caBundle: null
  group: custom-metrics.metrics.k8s.io
  groupPriorityMinimum: 1000
  insecureSkipTLSVerify: true
  service:
    name: api
    namespace: custom-metrics
  version: v1alpha1
  versionPriority: 5
status:
  conditions:
  - lastTransitionTime: 2017-12-14T08:27:38Z
    message: all checks passed
    reason: Passed
    status: "True"
    type: Available

查看集群支持的APISerivce

作为Kubernetes中的一种资源对象,可以使用kubectl get apiservice来查看。

例如查看集群中所有的APIService:

$ kubectl get apiservice
NAME                                     AGE
v1.                                      2d
v1.authentication.k8s.io                 2d
v1.authorization.k8s.io                  2d
v1.autoscaling                           2d
v1.batch                                 2d
v1.monitoring.coreos.com                 1d
v1.networking.k8s.io                     2d
v1.rbac.authorization.k8s.io             2d
v1.storage.k8s.io                        2d
v1alpha1.custom-metrics.metrics.k8s.io   2h
v1beta1.apiextensions.k8s.io             2d
v1beta1.apps                             2d
v1beta1.authentication.k8s.io            2d
v1beta1.authorization.k8s.io             2d
v1beta1.batch                            2d
v1beta1.certificates.k8s.io              2d
v1beta1.extensions                       2d
v1beta1.policy                           2d
v1beta1.rbac.authorization.k8s.io        2d
v1beta1.storage.k8s.io                   2d
v1beta2.apps                             2d
v2beta1.autoscaling                      2d

另外查看当前kubernetes集群支持的API版本还可以使用kubectl api-version

$ kubectl api-versions
apiextensions.k8s.io/v1beta1
apiregistration.k8s.io/v1beta1
apps/v1beta1
apps/v1beta2
authentication.k8s.io/v1
authentication.k8s.io/v1beta1
authorization.k8s.io/v1
authorization.k8s.io/v1beta1
autoscaling/v1
autoscaling/v2beta1
batch/v1
batch/v1beta1
certificates.k8s.io/v1beta1
custom-metrics.metrics.k8s.io/v1alpha1
extensions/v1beta1
monitoring.coreos.com/v1
networking.k8s.io/v1
policy/v1beta1
rbac.authorization.k8s.io/v1
rbac.authorization.k8s.io/v1beta1
storage.k8s.io/v1
storage.k8s.io/v1beta1
v1

Taint和Toleration(污点和容忍)

Taint(污点)和 Toleration(容忍)可以作用于 node 和 pod 上,其目的是优化 pod 在集群间的调度,这跟节点亲和性类似,只不过它们作用的方式相反,具有 taint 的 node 和 pod 是互斥关系,而具有节点亲和性关系的 node 和 pod 是相吸的。另外还有可以给 node 节点设置 label,通过给 pod 设置 nodeSelector 将 pod 调度到具有匹配标签的节点上。

Taint 和 toleration 相互配合,可以用来避免 pod 被分配到不合适的节点上。每个节点上都可以应用一个或多个 taint ,这表示对于那些不能容忍这些 taint 的 pod,是不会被该节点接受的。如果将 toleration 应用于 pod 上,则表示这些 pod 可以(但不要求)被调度到具有相应 taint 的节点上。

示例

以下分别以为 node 设置 taint 和为 pod 设置 toleration 为例。

为 node 设置 taint

为 node1 设置 taint:

kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule

删除上面的 taint:

kubectl taint nodes node1 key1:NoSchedule-
kubectl taint nodes node1 key1:NoExecute-
kubectl taint nodes node1 key2:NoSchedule-

查看 node1 上的 taint:

kubectl describe nodes node1

为 pod 设置 toleration

只要在 pod 的 spec 中设置 tolerations 字段即可,可以有多个 key,如下所示:

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
- key: "node.alpha.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000
  • value 的值可以为 NoSchedulePreferNoScheduleNoExecute
  • tolerationSeconds 是当 pod 需要被驱逐时,可以继续在 node 上运行的时间。

Pod中断与PDB(Pod中断预算)

这篇文档适用于要构建高可用应用程序的所有者,因此他们需要了解 Pod 可能发生什么类型的中断。也适用于要执行自动集群操作的集群管理员,如升级和集群自动扩容。

自愿中断和非自愿中断

Pod 不会消失,直到有人(人类或控制器)将其销毁,或者当出现不可避免的硬件或系统软件错误。

我们把这些不可避免的情况称为应用的非自愿性中断。例如:

  • 后端节点物理机的硬件故障
  • 集群管理员错误地删除虚拟机(实例)
  • 云提供商或管理程序故障使虚拟机消失
  • 内核恐慌(kernel panic)
  • 节点由于集群网络分区而从集群中消失
  • 由于节点资源不足而将容器逐出

除资源不足的情况外,大多数用户应该都熟悉以下这些情况;它们不是特定于 Kubernetes 的。

我们称这些情况为”自愿中断“。包括由应用程序所有者发起的操作和由集群管理员发起的操作。典型的应用程序所有者操作包括:

  • 删除管理该 pod 的 Deployment 或其他控制器
  • 更新了 Deployment 的 pod 模板导致 pod 重启
  • 直接删除 pod(意外删除)

集群管理员操作包括:

这些操作可能由集群管理员直接执行,也可能由集群管理员或集群托管提供商自动执行。

询问您的集群管理员或咨询您的云提供商或发行文档,以确定是否为您的集群启用了任何自动中断源。如果没有启用,您可以跳过创建 Pod Disruption Budget(Pod 中断预算)。

处理中断

以下是一些减轻非自愿性中断的方法:

  • 确保您的 pod 请求所需的资源
  • 如果您需要更高的可用性,请复制您的应用程序。 (了解有关运行复制的无状态有状态应用程序的信息。)
  • 为了在运行复制应用程序时获得更高的可用性,请跨机架(使用反亲和性)或跨区域(如果使用多区域集群)分布应用程序。

自愿中断的频率各不相同。在 Kubernetes 集群上,根本没有自愿的中断。但是,您的集群管理员或托管提供商可能会运行一些导致自愿中断的附加服务。例如,节点软件更新可能导致自愿更新。另外,集群(节点)自动缩放的某些实现可能会导致碎片整理和紧缩节点的自愿中断。您的集群管理员或主机提供商应该已经记录了期望的自愿中断级别(如果有的话)。

Kubernetes 提供的功能可以满足在频繁地自动中断的同时运行高可用的应用程序。我们称之为“中断预算”。

中断预算的工作原理

应用程序所有者可以为每个应用程序创建一个 PodDisruptionBudget 对象(PDB)。 PDB 将限制在同一时间自愿中断的复制应用程序中宕机的 Pod 的数量。例如,基于定额的应用程序希望确保运行的副本数量永远不会低于仲裁所需的数量。Web 前端可能希望确保提供负载的副本的数量永远不会低于总数的某个百分比。

集群管理器和托管提供商应使用遵循 Pod Disruption Budgets 的工具,方法是调用Eviction API而不是直接删除 Pod。例如 kubectl drain 命令和 Kubernetes-on-GCE 集群升级脚本(cluster/gce/upgrade.sh)。

当集群管理员想要排空节点时,可以使用 kubectl drain 命令。该命令会试图驱逐机器上的所有 pod。驱逐请求可能会暂时被拒绝,并且该工具会定期重试所有失败的请求,直到所有的 pod 都被终止,或者直到达到配置的超时时间。

PDB 指定应用程序可以容忍的副本的数量,相对于应该有多少副本。例如,具有 spec.replicas:5 的 Deployment 在任何给定的时间都应该有 5 个 Pod。如果其 PDB 允许在某一时刻有 4 个副本,那么驱逐 API 将只允许仅有一个而不是两个 Pod 自愿中断。

使用标签选择器来指定应用程序的一组 pod,这与应用程序的控制器(Deployment、StatefulSet 等)使用的相同。

Pod 控制器的 .spec.replicas 计算“预期的” pod 数量。使用对象的 .metadata.ownerReferences 值从控制器获取。

PDB 不能阻止非自愿中断的发生,但是它们确实会影响预算。

由于应用程序的滚动升级而被删除或不可用的 Pod 确实会计入中断预算,但控制器(如 Deployment 和 StatefulSet)在进行滚动升级时不受 PDB 的限制——在应用程序更新期间的故障处理是在控制器的规格(spec)中配置(了解更新 Deployment)。

使用驱逐 API 驱逐 pod 时,pod 会被优雅地终止(请参阅 PodSpec 中的 terminationGracePeriodSeconds)。

PDB 示例

假设集群有3个节点,node-1node-3。集群中运行了一些应用,其中一个应用有3个副本,分别是 pod-apod-bpod-c。另外,还有一个与它相关的不具有 PDB 的 pod,我们称为之为 pod-x。最初,所有 Pod 的分布如下:

node-1 node-2 node-3
pod-a available pod-b available pod-c available
pod-x available

所有的3个 pod 都是 Deployment 中的一部分,并且它们共同拥有一个 PDB,要求至少有3个 pod 中的2个始终处于可用状态。

例如,假设集群管理员想要重启系统,升级内核版本来修复内核中的错误。集群管理员首先使用 kubectl drain 命令尝试排除 node-1。该工具试图驱逐 pod-apod-x。这立即成功。两个 Pod 同时进入终止状态。这时的集群处于这种状态:

node-1 draining node-2 node-3
pod-a terminating pod-b available pod-c available
pod-x terminating

Deployment 注意到其中有一个 pod 处于正在终止,因此会创建了一个 pod-d 来替换。由于 node-1 被封锁(cordon),它落在另一个节点上。同时其它控制器也创建了 pod-y 作为 pod-x 的替代品。

(注意:对于 StatefulSetpod-a 将被称为 pod-1,需要在替换之前完全终止,替代它的也称为 pod-1,但是具有不同的 UID,可以创建。否则,示例也适用于 StatefulSet。)

当前集群的状态如下:

node-1 draining node-2 node-3
pod-a terminating pod-b available pod-c available
pod-x terminating pod-d starting pod-y

在某一时刻,pod 被终止,集群看起来像下面这样子:

node-1 drained node-2 node-3
pod-b available pod-c available
pod-d starting pod-y

此时,如果一个急躁的集群管理员试图排空(drain)node-2node-3,drain 命令将被阻塞,因为对于 Deployment 只有2个可用的 pod,并且其 PDB 至少需要2个。经过一段时间,pod-d 变得可用。

node-1 drained node-2 node-3
pod-b available pod-c available
pod-d available pod-y

现在,集群管理员尝试排空 node-2。drain 命令将尝试按照某种顺序驱逐两个 pod,假设先是 pod-b,然后再 pod-d。它将成功驱逐 pod-b。但是,当它试图驱逐 pod-d 时,将被拒绝,因为这样对 Deployment 来说将只剩下一个可用的 pod。

Deployment 将创建一个名为 pod-epod-b 的替代品。但是,集群中没有足够的资源来安排 pod-e。那么,drain 命令就会被阻塞。集群最终可能是这种状态:

node-1 drained node-2 node-3 no node
pod-b available pod-c available pod-e pending
pod-d available pod-y

此时,集群管理员需要向集群中添加回一个节点以继续升级操作。

您可以看到 Kubernetes 如何改变中断发生的速率,根据:

  • 应用程序需要多少副本
  • 正常关闭实例需要多长时间
  • 启动新实例需要多长时间
  • 控制器的类型
  • 集群的资源能力

分离集群所有者和应用程序所有者角色

将集群管理者和应用程序所有者视为彼此知识有限的独立角色通常是很有用的。这种责任分离在这些情况下可能是有意义的:

  • 当有许多应用程序团队共享一个 Kubernetes 集群,并且有自然的专业角色
  • 使用第三方工具或服务来自动化群集管理

Pod Disruption Budget(Pod 中断预算) 通过在角色之间提供接口来支持这种角色分离。

如果您的组织中没有这样的职责分离,则可能不需要使用 Pod 中断预算。

如何在集群上执行中断操作

如果您是集群管理员,要对集群的所有节点执行中断操作,例如节点或系统软件升级,则可以使用以下选择:

  • 在升级期间接受停机时间。
  • 故障转移到另一个完整的副本集群。
    • 没有停机时间,但是对于重复的节点和人工协调成本可能是昂贵的。
  • 编写可容忍中断的应用程序和使用 PDB。
    • 没有停机时间。
    • 最小的资源重复。
    • 允许更多的集群管理自动化。
    • 编写可容忍中断的应用程序是很棘手的,但对于可容忍自愿中断,和支持自动调整以容忍非自愿中断,两者在工作上有大量的重叠。

文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录