Kubernetes学习笔记
Kubernates(发音 / kubə'neitis /,简称K8S)是一个容器编排工具,使用它可以自动化容器的部署、扩容、管理。使用K8S可以将应用程序封装为容易管理、发现的逻辑单元。使用K8S你可以打造完全以容器为中心的开发环境。
K8S的特性包括:
- 自动装箱:根据容器的资源需求和其他约束条件,自动部署容器到合适的位置,与此同时,不影响可用性。K8S可以混合管理关键负载、非关键负载并尽可能的有效利用资源
- 自我修复:当容器宕掉后自动重启它,当节点宕掉后重新调度容器。关闭不能正确响应自定义健康检查的容器,在容器准备好提供服务之前,不将他们暴露给客户端
- 水平扩容:手工(UI、命令行)或自动(根据CPU负载)进行自动的扩容/缩容
- 服务发现/负载均衡:不需要修改应用程序来使用第三方的服务发现机制,K8S为容器提供专有IP,同时为一组容器(类似Swarm中的Service)提供单一的DNS名称,应用程序可以基于DNS名称发现服务。K8S还内置的负载均衡支持
- 无缝滚动更新/回滚:支持逐步的将更新应用到程序或配置,与此同时监控程序的健康状况,避免同时杀死程序的所有实例。如果出现问题,K8S能够自动回滚更新
- 密码和配置管理:部署、更新应用程序的密码、配置信息时,不需要重新构建镜像,不需要在配置信息中暴露密码信息
- 存储编排:自动从本地磁盘、公有云、网络存储系统挂载存储系统
- 资源监控、访问并处理日志、调试应用、提供认证和授权功能……
K8S提供了PaaS的简单性、IaaS的灵活性,支持在各基础设施提供商之间迁移。
尽管K8S提供了部署、扩容、负载均衡、日志、监控等服务,但是它并不是传统的PaaS平台:
- 它不限制能支持的应用程序类型,不限制编程语言、SDK。只要应用能在容器中运行,就可以在K8S下运行
- 不内置中间件(例如消息总线)、数据处理框架(例如Spark)、数据库、或者存储系统
- 不提供服务市场来下载服务
- 允许用户选择日志、监控、报警系统
同时,很多PaaS平台可以运行在K8S之上,例如OpenShift、Deis,你可以在K8S上部署自己的PaaS平台,集成自己喜欢的CI环境,或者仅仅是部署容器镜像。
以Kubernetes为核心的技术生态圈,已经成为构建云原生架构的基石。
云原生架构没有权威的定义,但是基于这种架构的应用具有一系列的模式:
- 代码库:每个可部署的应用程序,都有独立的代码库,可以在不同环境部署多个实例
- 明确的依赖:应用程序的依赖应该基于适当的工具(例如Maven、Bazel)明确的声明,不对部署环境有任何依赖
- 配置注入:和发布环境(dev/stage/pdt)变化的配置信息,应该以操作系统级的环境变量注入
- 后端服务:数据库、消息代理应视为附加资源,并在所有环境中同等看待
- 编译、发布、运行:构建一个可部署的应用程序并将它与配置绑定,根据这个组件/配置的组合来启动一个或者多个进程,这两个阶段是严格分离的
- 进程:应用程序运行一个或多个无状态进程,不共享任何东西 —— 任何状态存储于后端服务
- 端口绑定:应用程序是独立的,并通过端口绑定(包括HTTP)导出任何服务
- 并发:并发通常通过水平扩展应用程序进程来实现
- 可任意处置:通过迅速启动和优雅的终止进程,可以最大程度上的实现鲁棒性。这些方面允许快速弹性缩放、部署更改和从崩溃中恢复
- 开发/生产平等:通过保持开发、预发布和生产环境尽可能的一致来实现持续交付和部署
- 日志:不管理日志文件,将日志视为事件流,允许执行环境通过集中式服务收集、聚合、索引和分析事件
- 管理任务:管理性的任务,例如数据库迁移,应该在应用程序运行的那个环境下,一次性的执行
关键字:环境解耦、无状态、微服务。
两者之间存在高度的功能重叠,例如服务发现、负载均衡。
Swarm的功能比K8S要弱的多,可以实现简单的负载均衡、HA等特性,小规模部署可以使用。
K8S网络包括CNI、Service网络、Ingress、DNS这几个方面的内容:
- CNI负责Pod到Pod的网络连接
- Service网络负责Pod到Service的连接
- Ingress负责外部到集群的访问
- DNS负责解析集群内部域名
取决于具体实现,节点可能可以直接访问CNI、Service的IP地址。
Pod到集群外部的访问,基于SNAT实现。
Pod在节点内部的连接,经典方案是veth pair + bridge,也就是说多个Pod会连接到同一个网桥上,实现互联。
Pod在节点之间的连接,经典方案是bridge、overlay,Calico等插件则基于虚拟路由。
Kubernetes容器网络由Kubenet或CNI插件负责,前者未来会被废弃。
Kubenet是K8S早期的原生网络驱动,提供简单的单机容器网络。需要用Kubelet参数 --network-plugin=kubenet启用。
Kubenet本身不实现跨节点网络连接、网络策略,它通常和云提供商一起使用来实现路由规则配置、跨主机通信。Kubenet也用到一些CNI的功能,例如它基于CNI bridge插件创建名为cbr0的Linux Bridge,为每个容器创建一对veth pair并连接到cbr0网桥。
Kubenet在CNI插件的基础上拓展了很多功能:
- 基于host-local IP地址管理插件,为Pod分配IP地址并定期释放分配而未使用的IP
- 设置sysctl的net.bridge.bridge-nf-call-iptables=1
- 为Pod的IP设置SNAT(MASQUERADE),以允许Pod 访问外部网络
- 开启Linux Bridge的hairpin、混杂模式,允许Pod访问自己所在的Service IP
- HostPort管理、设置端口映射
- 带宽控制。可以通过kubernetes.io/ingress-bandwith、kubernetes.io/egress-bandwith来配置Pod网络带宽限制
Kubenet会使用bridge、lo、host-local等几个CNI插件,默认在/opt/cni/bin中搜索这些插件的二进制文件。你可以通过--network-plugin-dir参数定制搜索位置。此外Kubenet来回去/etc/cni/net.d中搜索CNI配置文件。
支持通过Kubelet参数--network-plugin-mtu定制MTU,支持限制带宽。这两个特性是Kubenet不能被CNI完全代替的原因。
CNI是容器网络的标准,试图通过一段JSON来描述容器网络配置。CNI是K8S和底层网络插件之间的抽象层。
CNI包含以下接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
type CNI interface { AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error) AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error) CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error) } type RuntimeConf struct { ContainerID string NetNS string IfName string Args [][2]string // A dictionary of capability-specific data passed by the runtime // to plugins as top-level keys in the 'runtimeConfig' dictionary // of the plugin's stdin data. libcni will ensure that only keys // in this map which match the capabilities of the plugin are passed // to the plugin CapabilityArgs map[string]interface{} // DEPRECATED. Will be removed in a future release. CacheDir string } type NetworkConfig struct { Network *types.NetConf Bytes []byte } |
AddNetwork负责在创建容器时,进行网络接口的配置;DelNetwork则在删除容器时,清理掉网络接口。参数NetworkConfig是网络配置信息,RuntimeConf则是容器运行时传入的网络命名空间信息。
CNI配置编写在如下形式的JSON文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ name: "mynet", // 使用bridge CNI插件 type: "bridge", bridge: "cni0", ipam: { // 基于host-local插件进行IP地址管理 type: "host-local", subnet: "10.0.0.0/16", routes: [ { "dst": "0.0.0.0/0" } ] } } |
上述配置文件,默认需要存在/etc/cni/net.d目录下,并且命名为*.conf,对应的插件二进制文件默认需要存放在/opt/cni/bin下。
在K8S中启用基于CNI的网络插件,需要配置Kubelet参数 --network-plugin=cni。CNI配置文件位置通过参数 --cni-conf-dir配置,如果目录中存在多个配置文件则仅仅取第一个。CNI插件二进制文件位置通过 --cni-bin-dir配置。
K8S对象是指运行在K8S系统中的持久化实体,K8S使用这些实体来表示你的应用集群的状态。例如:
- 哪些容器化应用程序在执行,在何处(Node)执行
- 上述应用程序可用的资源情况
- 和应用程序行为有关的策略,例如重启策略、升级策略、容错策略
通过创建一系列对象,你可以告诉K8S,你的集群的负载是什么样的,所谓集群的期望状态(desired state)。
要创建/修改/删除K8S对象,可以调用Kubernetes API。提供命令行kubectl可以间接的调用此API,你也可以在程序中调用这些API,目前支持Go语言。
K8S对象包括:
- Pod、Service、Volume、Namespace等基本对象
- ReplicaSet、Deployment、StatefulSet、DaemonSet、Job等高级对象,这些高级对象在上述基本对象之上构建,并且提供了额外功能、便利化功能
本章不会一一解释这些不同类型的对象。
任何K8S对象都有两个内嵌的字段:规格、状态。前者由你提供,声明对象期望状态(所谓对象的期望状态即集群的期望状态)。后者则表示某一时刻对象的实际状态,此状态由K8S更新,K8S控制平面会积极的管理对象的状态,使其尽可能满足规格。
例如,Kubernetes Deployment是描述运行在集群中的一个应用程序的K8S对象。你创建一个Deployment时,可能在规格中声明你需要3个应用程序的Replica。K8S会读取规格并启动3个应用实例,如果一段时间后宕掉1个实例,则K8S会检测到Spec和Status之间的不同,进而启动一个新的实例代替宕掉的那个。
调用K8S API创建对象时,你需要提供一个JSON格式的规格说明,其中包含期望状态和一些基本信息(例如应用的名称)。
通过kubectl创建对象时,你需要提供一个YML文件,kubectl会自动将其转换为JSON格式。
下面是对象规格(YML)的一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# 如果K8S版本小于1.8则使用 apps/v1beta1 apiVersion: apps/v1beta2 # 对象类型 kind: Deployment # 对象元数据,唯一的识别对象,字段包括name、UID、namespace metadata: # 名字通常由客户端提供,每一个对象类型内部,名字不得重复。在资源URL中名字用于引用对象,例如/api/v1/pods/some-name # 名字应该仅仅包含小写字母、数字、-、.这几类字符 name: nginx-deployment # UID由K8S自动生成,全局唯一 # namespace 指定对象所属的名字空间 # 对象规格 spec: replicas: 3 # 标签选择器 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80 # 端口名称,可以被Service引用 name: web protocol: TCP # 标签 labels: ... |
基于上述规格创建对象的命令示例:
1 |
kubectl create -f https://blog.gmem.cc/nginx-deployment.yaml --record |
K8S支持在一个物理集群上,创建多个虚拟集群,这些虚拟集群称为名字空间。名字空间为资源名称限定了作用域,同一名字空间内部名字不能重复,但是跨越名字空间则不受限制。
可以考虑使用名字空间的场景:
- 当跨越多个团队/项目的人员共享一套集群时,可以考虑使用名字空间机制。如果使用集群的人员仅仅在数十个级别,不需要使用名字空间
- 希望利用K8S的资源配额机制,在名字空间之间划分集群资源
- 在未来版本的K8S中,同一名字空间中的对象将具有一致的默认范围控制策略
如果两个资源仅仅有些许的不同,例如应用的两个版本,则不需要利用名字空间隔离。考虑使用标签(Label)来在名字空间内部区分这些资源。
大部分K8S资源(Pod、Service、Replication Controller...)都位于名字空间中。但是名字空间本身(也是资源)则不位于任何名字空间中。事件(Event)则可能位于、也可能不位于名字空间中,取决于事件的源对象。
执行下面的命令可以查看集群中现有的名字空间:
1 2 3 4 5 |
kubectl get namespaces # NAME STATUS AGE # default Active 1d # kube-system Active 1d # kube-public Active 1d |
上述命令输出了3个名字空间:
名字空间 | 说明 |
default | 默认名字空间,没有显式指定名字空间的对象位于此空间 |
kube-system | 由K8S系统创建的对象 |
kube-public | 自动创建、可以被任何用户(包括未进行身份验证的)访问的名字空间。此名字空间基本上是保留供集群使用的,因为某些资源需要公开的、跨越整个集群的访问 |
要在单个命令请求中,指定名字空间,可以:
1 2 |
kubectl --namespace=ns-dev run nginx --image=nginx kubectl --namespace=ns-dev get pods |
要永久的为某个上下文切换默认名字空间,可以:
1 2 |
kubectl config set-context $(kubectl config current-context) --namespace=ns-pdt kubectl config view | grep namespace # 验证设置 |
当你创建一个服务时,相应的DNS条目自动创建。此条目的默认格式是: ..svc.cluster.local。如果容器仅仅使用来引用服务,则解析到当前名字空间。这种特性可以方便的在多个名字空间(开发、测试、生产)中使用完全相同的配置信息。如果需要跨名字空间引用服务,则必须使用FQDN。
所谓标签(Label)就是用户自定义的、关联到对象的键值对,这些标签和K8S的核心系统没有意义。你可以使用标签来管理、选择对象的子集。
你可以在创建对象的时候设置标签,也可以后续添加、修改标签。对于单个对象来说,标签的键必须是唯一的。
K8S会对标签进行索引/反向索引,以便对标签进行高效的查询、监控,在UI、CLI中对输出进行分组、排序。
标签提供一种松散耦合的风格,让用户自由的映射他们的组织结构到K8S对象,而不需要客户端记住这些映射关系。
服务部署、批处理流水线,常常都是多维的实体(多分区/部署、多个发行条线、多层、每层多个微服务)。对这些实体进行管理,常需要横切性的操作——打破严格的层次性封装,这些层次可能由基础设施死板的规定。
样例标签集:
- 区分发行条线:{ "release" : "stable", "release" : "canary" }
- 区分运行环境:{ "environment" : "dev", "environment" : "qa", "environment" : "production" }
- 区分架构层次:{ "tier" : "frontend", "tier" : "backend", "tier" : "cache" }
- 区分分区: { "partition" : "customerA", "partition" : "customerB" }
- 区分Track: { "track" : "daily", "track" : "weekly" }
标签的键,可以由两段组成:可选的前缀和名称,它们用 / 分隔:
- 名称部分必须63-字符,支持大小写字母、数字、下划线、短横线、点号
- 前缀如果存在,则必须是DNS子域名 —— 一系列以点号分隔的DNS标签,最后加上一个 /
- 如果前缀被省略,则暗示标签是用户私有的
- 前缀 kubernetes.io/ 为K8S核心组件保留
一系列对象常常具有相同的标签。利用标签选择器,客户端可以识别一组对象。标签选择器是K8S提供的核心分组原语。
目前K8S API提供两种标签选择器:
- equality-based。操作符=、==(和=同义)、!=。示例:environment = production,tier != frontend
- set-based。操作符in、notin、exists。示例:environment in (production, qa),tier notin (frontend, backend)
如果有多个选择器需要匹配,则使用逗号(作用类似于&&)分隔。
NULL选择器匹配空集,空白选择器则匹配集合中所有对象。
LIST、WATCH之类的操作可以指定标签选择器,以过滤对象。
基于CLI的例子:
1 2 3 |
kubectl get pods -l environment=production,tier=frontend kubectl get pods -l 'environment in (production),tier in (frontend)' kubectl get pods -l 'environment,environment notin (frontend)' |
某些K8S对象(Service、复制控制器)使用标签选择器指定关联的对象(例如Pod)。例如,作为服务目标的Pods是通过标签选择器指定的。replicationcontroller需要管理的pod生产,也是通过标签指定的。对象配置片断示例:
1 2 |
selector: component: redis |
ReplicaSet、Deployment、DaemonSet、Job等对象,支持set-based风格的标签选择器,来指定其需求:
1 2 3 4 5 6 |
selector: matchLabels: component: redis matchExpressions: - {key: tier, operator: In, values: [cache]} - {key: environment, operator: NotIn, values: [dev]} |
除了标签以外,你还可以使用注解(Annotations)来为K8S对象附加非识别性(non-identifying)元数据。客户端可以通过API获得这些元数据。
注意注解和标签不一样,后者可以用来识别、选择对象,前者则不能。此外注解的值大小没有限制。
注解也是键值对形式:
1 2 3 4 |
"annotations": { "key1" : "value1", "key2" : "value2" } |
节点是K8S中的Worker机器,之前被叫做minion。节点可以是物理机器,也可以是VM。节点上运行着一些服务,运行Pod需要这些服务,这些服务被Master组件管理。运行在节点上的服务包括Docker、kubelet、kube-proxy等。
节点应该是x86_64(amd64)架构的Linux系统。其它架构或系统有可能支持K8S。
以下几类信息用于描述节点的状态:
字段类别 | 说明 | ||
Addresses |
这些字段的用法取决于你使用的云服务,或者裸金属的配置: HostName:由节点的内核报告,可以由kubelet的--hostname-override参数覆盖 |
||
Condition |
这些字段描述运行中节点的状态信息: OutOfDisk:如果为True则意味着节点上没有足够空间供新Pod使用 节点的状态以JSON形式表示,例如:
如果节点的Ready状态为False/Unknown,并且持续时间大于pod-eviction-timeout(默认5分钟),则kube-controller-manager会接收到一个argument,该节点上所有Pods将被节点控制器调度以删除 某些情况下节点不可达,APIServer无法与其上运行的kubelet通信。这样Pods删除信息无法传递到被分区(partitioned,网络被隔离的)节点,因而知道网络通信恢复前,其上的Pods会继续运行 在1.5-版本中,节点控制器会强制的从APIServer中删除不可达Pods,1.5+版本后只有在确认这些Pods已经从集群中停止运行后才进行删除。这些运行在不可达节点上的Pod的状态可能为Terminating或者Unknown 某些情况下,K8S无法在基础设施层推断节点是否永久的离开了集群,管理员可能需要手工进行节点移除 移除节点将导致其上的所有Pod对象从APIServer上删除
从1.8开始K8S引入了一个Alpha特性,可以自动创建代表Condition的taints。要启用此特性,为APIServer、控制器管理器、调度器提供参数: --feature-gates=...,TaintNodesByCondition=true 当TaintNodesByCondition启用后,调度器会忽略节点的Conditions,代之以查看节点的taints、Pod的toleration。你可以选择旧有的调度模型,或者使用新的更加灵活的调度模型:
|
||
Capacity | 描述节点上可用的资源,包括CPU、内存、最多可以在其上调度的Pod数量 | ||
Info | 一般性信息,例如内核版本、K8S版本(kubelet/kube-proxy)、Docker版本 |
与Node/Service不同,节点不是K8S创建的,它常常由外部云服务(例如Google Compute Engine)创建,或者存在于你的物理机/虚拟机池子中。K8S中创建一个节点,仅仅是创建代表此节点的对象。在创建节点对象后,K8S会验证其有效性,例如下面的节点配置信息:
1 2 3 4 5 6 7 8 9 10 |
{ "kind": "Node", "apiVersion": "v1", "metadata": { "name": "10.240.79.157", "labels": { "name": "my-first-k8s-node" } } } |
执行创建后,K8S会基于metadata.name字段来检查节点的健康状态。如果节点是有效的(所有必须的服务在其上运行)则它有资格运行Pod,否则它会被任何集群活动排除在外,知道它变为有效状态。
除非显式删除,K8S会一致维护你创建的节点对象,并且周期性的检查其健康状态。
目前和节点接口交互的组件有三个:节点控制器、Kubelet、Kubectl。
节点控制器属于K8S管理组件,可以管理节点的方方面面:
- 当节点注册时,给节点分配一个CIDR块(如果CIDR分配打开)
- 在内部维护一个最新的节点列表。在云环境下,节点控制器会调用云提供商的接口,判断不健康节点的VM是否仍然可用,如果答案是否则则节点控制器从子集的节点列表中删除不健康节点
- 监控节点的健康状态。当节点不可达(节点由于某种原因不再响应心跳,例如宕机,默认40s)时,更新NodeStatus的NodeReady状态为ConditionUnknown。如果节点持续处于不可达状态(默认5m),则优雅的清除(Terminate)节点上所有Pods。控制器每隔--node-monitor-period秒检查节点状态
- 从1.6开始,节点控制器还负责清除运行在具有NoExecute taints节点上的Pods(如果Pod不容忍NoExecutes)
从1.4开始,K8S更新了节点控制器的逻辑,当Master节点本身网络出现问题导致大量节点不可达的场景被优化处理。在决定清除一个Pods时,节点控制器会查看集群中所有节点的状态。
大部分情况下,节点控制器限制了节点清除的速度。参数--node-eviction-rate默认为0.1,也就是每10秒最多从单个节点清除一个Pod。
当给定可用性区域(availability zone,集群可能跨越云服务的多个可用性区域,例如北美、亚太)中节点变得不健康时,节点清除行为会有改变。 节点控制器会计算可用性区域中不健康(NodeReady=ConditionUnknown or ConditionFalse)节点的百分比:
- 如果此比值不小于--unhealthy-zone-threshold(默认0.55)则清除速率降低。降低到--secondary-node-eviction-rate(默认0.01)
- 如果集群规模较小(小于--large-cluster-size-threshold,默认50)则清除行为停止
之所以按可用性区域来决定上述行为,是因为某些区域可能和Master之间形成网络分区,另外一些这和Master之间保持连通。
跨越可用性区域分布节点的关键原因是,当整个区域不可用时,工作负载可以迁移到健康的区域中。
当为Kubelet提供参数--register-node=true(默认)时,Kubelet会尝试自动的到API服务器上注册自己。自动注册相关的参数:
参数 | 说明 |
--kubeconfig | 用于包含自己的身份验证信息的文件路径 |
--cloud-provider | 指定云服务提供商信息,用于读取关于节点本身的元数据 |
--register-node | 自动注册自己 |
--register-with-taints | 使用指定的taints注册自己,多个taints用逗号分隔:=:,=: |
--node-ip | 节点的IP地址 |
--node-labels | 注册时附加到节点对象的标签集 |
--node-status-update-frequency | 节点向Master提交自身状态的间隔 |
当前,Kubelet有权创建/修改任何节点资源,但是通常它仅应该修改自己的。
作为集群管理员,你可以随时创建、修改节点对象。你可以设置kubelet标记--register-node=false,仅仅允许手工的管理。
你可以管理节点的资源,例如设置标签、标记其为unschedulable:
1 2 |
# 设置节点为unschedulable kubectl cordon $NODENAME |
标记节点为unschedulable可以禁止新的Pods被分配到节点上,但是对节点上现有的Pod则没有影响。
注意DaemonSet控制器创建的Pod跳过了K8S的调度器,因而unschedulable标记为其无意义。
节点的容量(Capacity,CPU数量、内存量)属于节点对象属性的一部分。通常节点自我注册时会提供容量信息。如果你手工的注册节点,则必须提供容量信息。
K8S调度器会确保节点拥有足够的资源来运行分配给它的所有Pods,它会检查请求在节点上启动的容器(仅限kubelet启动的)所需总资源不大于节点的容量。
你可以显式的为非Pod进程保留节点资源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 作为占位符的Pod apiVersion: v1 kind: Pod metadata: name: resource-reserver spec: containers: - name: sleep-forever image: gcr.io/google_containers/pause:0.8.0 resources: # 请求保留的资源数量 requests: cpu: 100m memory: 100Mi |
本节讨论Master(准确的说是APIServer)和K8S集群之间的通信。
集群到Master的通信路径,总是在APIServer处终结,因为其它的Master组件均不暴露远程接口。
在典型配置上,API服务器基于HTTPS协议监听443端口,并且启用1-N种客户端验证、授权机制。
节点应该被预先分配集群的根证书,以便能够使用有效的客户端证书链接到APIServer。
利用服务账户(Service Account),Pod可以和APIServer安全的通信,根据服务账户的配置,K8S会自动把根证书、不记名令牌(bearer token )注入到Pod中。所有名字空间中的K8S服务都配备了一个虚拟IP,通过kube-proxy重定向到APIServer上的HTTPS端点。
Master组件和APIServer基于非安全端口通信,此端口通常仅仅暴露在Master机器的localhost接口上。
从Master(APIServer)到集群的通信路径主要包括两条:
- 从APIServer到运行在各节点上的kubelet进程。这些通信路径用于:
- 抓取Pod的日志
- Attach到(利用kubectl)到运行中的Pod
- 提供Kubelet的端口转发功能
- 从APIServer到任何节点/Pod/Service,基于APIServer的代理功能
对于第1类通信路径,APIServer默认不会校验kubelet的服务器端证书,因而存在中间人攻击的可能性。使用--kubelet-certificate-authority标记可以为APIServer提供一个根证书,用于验证kubelet的服务器证书。
对于第2类通信路径,默认使用HTTP连接,不验证身份或加密。
这类组件构成了K8S集群的控制平面(Plane),负责集群的全局性管理,例如调度、检测/响应集群事件。
Master组件可以运行在集群的任何节点上,你可以创建多Master的集群以获得高可用性。
Master组件主要包括kube-apiserver、kube-controller-manager、kube-scheduler
暴露K8S的API,作为控制平面的前端。此服务器本身支持水平扩容。
一个分布式的键值存储,K8S集群的信息全部存放在其中。要注意做好etcd的备份。
运行控制器。控制器是一系列执行集群常规任务的后台线程,控制器包括:
- 节点控制器:检测节点宕机并予以响应
- 复制控制器:为系统中每个复制控制器对象维护正确数量的Pod
- 端点控制器:产生端点对象
- 服务账号/令牌控制器:为新的名字空间创建默认账户、API访问令牌
运行和云服务提供商交互的控制器,1.6引入的试验特性。CCM和其它Master组件在一起运行,它也可以作为Addon启动(运行在K8S的上层)。
CCM的设计初衷是,让云服务商相关的代码和K8S核心解耦,相互独立演进。不使用CCM时的K8S集群架构如下:
上述架构中,云服务和Kubelet、APIServer、KCM进行交互,实现集成,交互复杂,不符合最少知识原则。
使用CCM后,K8S集群架构变为:
新的架构中,CCM作为一个Facade,统一负责和云服务的交互。CCM分离了部分KCM的功能,在独立进程中运行它们,分离的原因是这些功能是依赖于具体云服务的:节点控制器、 卷控制器、路由控制器、服务控制器。在1.8版本中CCM运行前述控制器中的:
- 节点控制器:通过从云服务获取运行在集群中的节点信息,来初始化节点对象。添加云服务特定的Zone/Regin标签、云服务特定的节点实例细节,获取节点网络地址和主机名,在节点不可用时调用云服务确认节点是否被删除(如果是则级联删除节点对象)
- 路由控制器:在云服务中配置路由,确保不同节点中运行的容器能够相互通信,仅用于GCE集群
- 服务控制器:监听服务的创建/更新/删除事件。它会根据K8S中当前服务的状态来配置云服务的负载均衡器
CCM还运行一个PersistentVolumeLabels控制器,用于在PersistentVolumes(GCP/AWS提供)上设置Zone/Regin标签。卷控制器没有作为CCM的一部分是刻意的设计,主要原因是K8S已经在剥离云服务相关的卷逻辑上做了很多工作。
CCM还包含了云服务特定的Kubelet功能。在引入CCM之前,Kubelet负责利用云服务特定的信息(例如IP、Regin/Zone标签、节点实例类型)初始化节点,引入CCM之后这些职责被转移。在新架构中,Kubelet初始化节点时不知晓云服务特定的信息,但是它会给节点添加一个taint,从而将节点标记为不可调度的。直到CCM初始化了云服务特定的信息,taint才被移除,节点可以被调度。
CCM基于Go开发,暴露了一系列的接口(CloudProvider),允许任何云服务实现这些接口
此接口定义在pkg/cloudprovider/cloud.go,包含的功能有:
- 管理第三层(TCP)负载均衡器
- 管理节点实例(云服务提供的)
- 管理网络路由
不是所有功能都需要实现,取决于K8S组件的标记如何设置。运行K8S也不一定需要此接口的实现,比如在裸金属上运行时。
监控新创建的、没有分配到Node的Pod,然后选择适当的Node供其运行。
加载项是实现了集群特性的Pod和服务,这些Pod可以被Deployments、ReplicationControllers等管理。限定了名字空间的加载项在名字空间kube-system中创建。加载项管理器创建、维护加载项资源。常见加载项如下:
加载项 | 说明 |
DNS |
所有集群都需要此加载项,以提供集群DNS服务。此加载项为K8S服务提供DNS记录 K8S启动的容器自动使用此DNS服务 |
Dashboard | 此加载项是一个一般用途的Web客户端,用户可以基于此加载项来管理集群、管理或者针对K8S集群中运行的应用程序 |
CRM | 容器资源监控(Container Resource Monitoring)在中心数据库中记录容器的度量信息,并提供查看这些信息的UI |
CIL | 集群级别日志(Cluster-level Logging)收集容器日志,集中保存,并提供搜索/查询接口 |
这些组件可以运行在集群中的任何节点上,它们维护运行中的Pods、提供K8S运行时环境。主要包括kubelet、kube-proxy。
主要的节点代理程序,监控分配到(通过APIServer或本地配置文件)当前节点的Pod,并且:
- 挂载Pod所需的卷
- 下载Pod的Secrets
- 通过Docker或rkt运行Pod的容器
- 周期性执行任何请求的容器存活探针
- 将Pod状态反馈到系统的其他部分
- 将节点状态反馈到系统的其它部分
在每个节点上映射K8S的网络服务的代理。提供在宿主机上维护网络规则、执行连接转发,来实现K8S服务抽象。
最早的版本完全在用户空间实现,性能较差。从1.1开始,K8S实现了基于Iptable代理模式的kube-proxy,从1.8开始,添加基于IPVS实现的kube-proxy。
用于运行容器。
轻量级的监控系统,用于确保kubelet、docker持续运行(宕机重启之)。
用于配合实现集群级别日志(CIL)。
在Pod中引用Docker镜像之前,你必须将其Push到Registry中。
container对象的image属性可以包含Registry前缀和Tag,这个命名规则和Docker是一致的。
默认的镜像拉取策略是IfNotPresent,如果镜像已经存在于本地,则Kubelet不会重复抓取。如果希望总是取抓取镜像,可以使用以下三种方法之一:
- 设置imagePullPolicy=Always
- 使用镜像的:latest标签
- 启用AlwaysPullImages这一admission controller
当不指定镜像tag时,默认即使用:latest,因而会导致每次都抓取最新镜像。:latest是应该尽可能避免使用的。
Docker将访问私服所需要的密钥信息存放在$HOME/.dockercfg或者$HOME/.docker/config.json文件中。如果在Kubelet的root的$home目录下存在这两个文件K8S会使用之。
可以使用如下Pod能验证私服能否正常访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
apiVersion: v1 kind: Pod metadata: name: private-image-test-1 spec: containers: - name: uses-private-image image: $PRIVATE_IMAGE_NAME imagePullPolicy: Always command: [ "echo", "SUCCESS" ] # 创建Pod对象 kubectl create -f /tmp/private-image-test-1.yaml # 查看运行结果, 期望输出SUCCESS kubectl logs private-image-test-1 |
注意:
- 所有节点必须具有相同的.docker/config.json,否则可能出现Pod仅能在部分节点上运行的情况。例如,如果你使用节点自动扩容,则每个实例模板需要包含.docker/config.json文件或者挂载包含此文件的卷
- 只要私服的访问密钥被添加到config.json中,则任何Pod都有权访问这些私服中的镜像
K8S的容器环境,为容器提供了很多重要的资源:
- 一个文件系统,由镜像 + 1-N个卷构成
- 关于容器本身的信息:
- 容器的hostname就是容器所在的Pod的name,此名称可以通过hostname命令/gethostname函数获得
- 容器所属的Pod name和namespace还可以通过downward API获得
- 用户在Pod定义中声明的环境变量,对于容器都是可用的
- 关于集群中其它对象的信息:
- 容器创建时所有服务的列表,可以通过环境变量得到。例如名为foo的服务对应环境变量如下:
123# 在容器中可以访问环境变量:FOO_SERVICE_HOST=FOO_SERVICE_PORT= - 服务拥有专享的IP地址,容器可以基于DNS名称访问它(如果DNS Addon启用的话)
- 容器创建时所有服务的列表,可以通过环境变量得到。例如名为foo的服务对应环境变量如下:
利用容器生命周期钩子框架,你可以在Kubelet管理的容器启动、关闭时执行特定的代码。可用的钩子(事件)包括:PostStart、PreStop。
如果:
- 既不提供command和args,则使用Docker镜像中定义的默认值
- 如果仅提供了command,则Docker镜像中的CMD/ENTRYPOINT被忽略
- 如果仅提供了args,则使用Docker镜像中的ENTRYPOINT + args
- 如果同时提供了command和args,则Docker镜像中的CMD/ENTRYPOINT被忽略
Pod(本义:豆荚,箱子)是K8S对象模型中,最小的部署、复制、扩容单元 —— K8S中单个应用程序实例。
Pod封装了以下内容:
- 应用程序容器
- 存储资源
- 唯一的网络IP
- 管理容器运行方式的选项
Pod最常用的容器运行时是Docker,尽管其它容器运行时也被支持。
Pod的两种使用方式:
- 运行单个容器:容器和Pod呈现一对一关系,这是最常见的用法。你可以将Pod看作是容器的简单包装器,K8S管理Pod而不直接管理容器
- 运行多个容器:Pod封装了由多个较小的、紧耦合的、共享资源的容器相互协作而组成的应用,以及和这些容器相关的存储资源
Pod本身只是一套环境,因此容器可以重启,Pod则没有这一概念。
每个Pod对应一个应用程序实例,如果你需要水平扩容,则需要使用多个Pod。这种水平扩容在K8S中一般叫做复制(Replication)。复制的Pod由控制器创建、管理。
如果Pod包含多个容器,则这些容器一般被调度、运行在单个节点上。这些容器可以共享资源、依赖,相互通信,并且协调如何关闭(例如谁先关闭)。容器之间共享的资源主要有:
- 网络:每个Pod具有唯一性的IP地址,Pod中的每个容器共享网络名字空间,包括IP地址和端口。Pod内的容器之间可以通过localhost + 端口相互通信。当与外部实体通信时,这些容器必须协调如何共享网络资源(例如端口)
- 存储:Pod可以指定一系列的共享存储卷,所有容器可以访问这些卷。如果Pod中部分容器重启,这些卷仍然保持可用
- 容器之间也可以基于IPC机制进行通信,例如SystemV信号量、POSIX共享内存
在基于Docker时,Pod中的容器是共享名字空间、共享卷的Docker容器。
Pod可以和其它物理机器、其它Pod进行网络通信。
- 用户向API Server发送创建Pod的请求
- K8S Scheduler选取一个节点,将Pod分配到节点上
- 节点上的Kubelet负责Pod的创建:
- 调用CNI实现(dockershim、containerd等)创建Pod内的容器
- 第一个创建的容器是pause,它会允许一个简单的程序,进行永久的阻塞。该容器的作用是维持命名空间,因为Linux的命名空间需要其中至少包含一个进程才能存活
- 其他容器会加入到pause容器所在的命名空间
- 初始化容器网络接口,由kubenet(即将废弃)获CNI插件负责。CNI会在pause容器内创建eth0接口并分配IP地址
- 容器和主机的网络协议栈,通过veth pair连接
在Pod中,pause容器是所有其他容器的“父”容器,它:
- 是各容器共享的命名空间的基础
- 可以启用PID命名空间共享,为每个Pod提供init进程
pause容器的逻辑非常简单,它启动后就通过pause()系统调用暂停自己。当子进程变为孤儿进程(父进程提前出错退出)时,它会调用wait()防止僵尸进程的出现。
在1.8版本之前,默认启用PID命名空间共享,也就是说Pod的所有容器共享一个PID命名空间,之后的版本则默认禁用共享,可以配置 spec.shareProcessNamespace强制启用。不共享PID命名空间时,每个容器都有PID 1进程。
你很少需要直接创建单个Pod,甚至是单例(不复制)的Pod,这是因为Pod被设计为是短命的、可丢弃的实体。当Pod被(你直接、或控制器)创建时,它被调度到集群中的某个节点。直到进程退出,Pod会一直存在于节点上。如果缺少资源、节点失败,则Pod会被清除、Pod对象也被从APIServer中删除。
Pod本身没有自愈能力,如果Pod被调度到一个失败的节点,或者调度操作本身失败,则Pod会被删除。类似的,当资源不足或者节点维护时,Pod也会被删除。K8S使用控制器这一高层抽象来管理Pod实例。通常你都是通过控制器来间接使用Pod。
控制器能够创建、管理多个Pod,处理复制/回滚,并提供自愈(在集群级别)功能 —— 例如当节点失败时控制器会在其它节点上创建等价的Pod。一般来说,控制器使用你提供的Pod模板来创建Pod。
控制器主要有三类:
- Job:用于控制那些期望会终结的Pod,在批处理计算场景下用到。Job必须和restartPolicy为OnFailure/Never的Pod联用
- ReplicationController, ReplicaSet,Deployment:用于控制不期望终结的Pod,例如Web服务。这些控制器必须和restartPolicy=Always联用
- DaemonSet:用于某个机器上仅运行一个实例的Pod,用于提供机器特定的系统服务
这三类控制器都包含了对应的Pod模板。
Pod模板是包含在其它对象(复制控制器、Jobs、DaemonSets...)中的Pod的规格说明。控制器使用Pod模板来创建实际的Pod,模板示例如下:
1 2 3 4 5 6 7 8 9 10 11 |
apiVersion: v1 kind: Pod metadata: name: myapp-pod labels: app: myapp spec: containers: - name: myapp-container image: busybox command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600'] |
Pod模板发生改变不会影响已经创建的Pod。
当用户请求删除Pod时,K8S首先发送TERM信号给每个容器的主进程,当超过时限(完整宽限期,Full Grace Period)后,则发送KILL信号,之后删除Pod对象。
如果在等待容器进程退出期间Kubelet、Docker守护进程重启,则重启后重新进行完整宽限期内的TERM,以及必要的KILL。
强制删除:这种删除操作会导致Pod对象立即从APIServer中移除,这样它的名字可以立即被重用。实际的Pod会在较短的宽限期内被清除。
在容器规格的SecurityContext字段中指定privileged标记,则容器进入特权模式。如果容器需要操作网络栈、访问设备,则需要特权模式。
Pod的status字段是一个PodStatus对象,后者具有一个phase字段。该字段是Pod所处生命周期阶段的概要说明:
- Pending:Pod已经被K8S系统接受,单是一或多个容器镜像尚未创建。此时Pod尚未调度到节点上,或者镜像尚未下载完毕
- Running:Pod已经被调度到某个节点,且所有容器已被创建,至少有一个容器处于运行中/重启中/启动中
- Succeeded:Pod的所有容器被正常终结,且不会再重启
- Failed:Pod的所有容器被终结,且至少一个被异常终结——要么退出码非0要么被强杀
- Unknown:Pod状态未知,通常由于和目标节点的通信中断导致
PodStatus具有一个 PodCondition数组,数组的每个元素具有type、status字段。type可选值是PodScheduled、Ready、Initialized、Unschedulable,status字段的可选值是True、False、Unknown。
通常情况下,重复你或者控制器销毁Pod,它不会小时。唯一的例外是,phase值为Succeeded/Failed,并持续一定时间(具体由Master确定),则Pod会因为过期而自动销毁。
如果节点和集群断开,则K8S会把该节点上所有的Pod的phase更新为Failed
Probe由Kubelet周期性的针对容器调用,以执行健康检查。Kubelet会调用容器实现的Handler,Handler包括三类:
- ExecAction:在容器内部执行特定的命令,如果命令为0则意味着诊断成功
- TCPSocketAction:针对容器的IP/端口进行TCP检查,如果端口打开则诊断成功
- HTTPGetAction:针对容器的IP/端口进行HTTP检查,如果响应码为[200,400)之间则诊断成功
每个探针的诊断结果可以是Success、Failure、Unknown
Kubelet可以在运行中的容器上执行两个可选探针,并作出反应:
- livenessProbe:探测容器是否还在运行。如果探测失败则杀死容器,并根据其重启策略决定如何反应。如果容器没有提供此探针,则总是返回Success
- readinessProbe:探测容器是否能提供服务。如果探测失败,端点控制器会从所有匹配Pod的服务的端点中移除Pod的IP地址。在Initial delay之前默认值Failure,如果容器没有提供此探针,则Initial delay之后默认Success。仅仅用于在Pod启动初期、延迟的把它加入到服务集群中,不能用于将Pod从服务集群中移除
注意,这些探针在Kubelet的网络名字空间中运行。
如果你的容器在出现问题时会自己崩溃,则不需要使用探针,只需要设置好restartPolicy即可。
基于HTTP的探针示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
apiVersion: v1 kind: Pod metadata: labels: test: liveness name: liveness-http spec: containers: - args: - /server image: gcr.io/google_containers/liveness # 指定探针,在容器级别上指定 livenessProbe: httpGet: path: /healthz port: 8080 # 也可以使用命名端口(ContainerPort) httpHeaders: - name: X-Custom-Header value: Awesome # 第一次探针延迟多久执行 initialDelaySeconds: 15 # 每隔多久执行一次探针 periodSeconds: 5 # 探针执行超时,默认1秒 timeoutSeconds: 1 # 被判定为失败后,连续多少次探测没问题,才被重新认为是成功 successThreshold: 1 # 连续多少次探测失败,则认为无法恢复,自动重启 failureThreshold: 3 name: liveness |
基于命令的探针示例:
1 2 3 4 5 6 7 8 9 10 |
spec: containers: - name: runtime livenessProbe: exec: command: - cat - /app/liveness initialDelaySeconds: 5 periodSeconds: 5 |
要注意:命令的标准输出被收集,并在kubectl describe pod命令中显示为Unhealthy事件的原因,因此探针的标准输出应该简洁明了。
某些情况下,容器临时的不适合处理请求,例如其正在启动、正在加载大量数据。此时可以使用readiness探针。readiness探测失败的Pod不会接收到K8S Service转发来的请求。
readinessProbe的配置和livenessProbe没有区别。两个探针可以被同时执行。
PodSpec包含一个restartPolicy字段,可选值为:
- Always,默认值,应用到Pod的所有容器
- OnFailure
- Never
重启时如果再次失败,重启延迟呈指数增长(10s,20s,40s)但是最大5分钟。启动成功后,10m后清除重启延迟。
在PodSpec的containers字段之后,你可以声明一个初始化容器。这类容器在应用容器之前运行,可能包含应用镜像中没有的实用工具、安装脚本。
初始化容器可以有多个,K8S会按照声明顺序逐个执行它们,只有前一个初始化容器成功完成,后面的初始化容器才会被调用。如果某个初始化容器运行失败,K8S会反复重启它,直到成功,除非你设置其restartPolicy=Never。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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;'] |
PodPreset用于在创建Pod时,向Pod注入额外的运行时需求信息。你可以使用标签选择器来指定预设要应用到的Pod。当Pod创建请求出现时,系统中发生以下事件:
- 取得所有可用的PodPreset
- 检查是否存在某个PodPreset,其标签选择器匹配准备创建的Pod
- 尝试合并PodPreset中的各种资源到准备创建的Pod中
- 如果出错,触发一个事件说明合并出错。在不注入任何资源的情况下创建Pod
- 如果成功,标注被修改的PodSpec为已被PodPreset修改
要在集群中使用Pod预设功能,你需要:
- 确保API类型settings.k8s.io/v1alpha1/podpreset启用。可以设置APIServer的--runtime-config选项,包含settings.k8s.io/v1alpha1=true
- 确保PodPreset这一Admission controller已经启用。可以设置APIServer的--admission-control选项,包含PodPreset
- 在你需要使用的名字空间中,定义一个PodPreset对象
如果你在使用Kubeadm,则修改配置文件/etc/kubernetes/manifests/kube-apiserver.yaml即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
apiVersion: settings.k8s.io/v1alpha1 kind: PodPreset metadata: name: allow-database spec: selector: matchLabels: role: frontend # 支持的Pod配置项是有限的,仅仅支持: # 预设容器的环境变量 env: - name: DB_PORT value: "6379" - name: expansion value: $(REPLACE_ME) # 预设容器的环境变量,从ConfigMap读取变量 envFrom: - configMapRef: name: etcd-env-config # 预设容器的卷挂载 volumeMounts: - mountPath: /cache name: cache-volume - mountPath: /etc/app/config.json readOnly: true name: secret-volume # 预设Pod的卷定义 volumes: - name: cache-volume emptyDir: {} - name: secret-volume secret: secretName: config-details |
要构建基于K8S的高可用应用,就要明白Pod什么时候会被中断(Disrpution)。
Pod不会凭空消失,除非你或控制器销毁了它们,或者出现不可避免的软硬件错误。这些不可避免的错误被称为非自愿中断(Involuntary Disruptions),例如:
- 作为节点支撑的物理机器,出现硬件错误
- 集群管理员错误的删除了VM
- 云服务或者Hypervisor的错误导致VM消失
- 内核错误
- 网络分区导致节点从集群分离
- 节点资源不足导致Pod被清除
其它情况则称为自愿中断,中断操作可能由应用程序所有者、集群管理员触发。应用程序所有者的操作包括:
- 删除部署或者其它管理Pod的控制器
- 更新部署的Pod模板导致重启
- 直接意外的删除了Pod
集群管理员的操作包括:
- Drain了某个节点,进行硬件维护或软件升级
- Drain了某个节点,进行缩容
- 从节点移除Pod,流下资源供其他人使用
缓和非自愿中断造成的影响,手段包括:
- 确保Pod请求了其需要的资源
- 如果需要HA,启用(无状态/有状态)应用程序复制
- 如果需要进一步HA,启用跨机柜复制(anti-affinity)甚至跨区域复制(multi-zone cluster)
自愿中断发生的频率根据集群用法的不同,有很大差异。对于某些基本的集群,根本不会出现自愿中断。
集群管理员/云服务提供商可能运行额外的服务,进而导致自愿中断。例如,滚动进行节点的软件更新就可能导致这种中断。某些节点自动扩容的实现,可能进行节点的取碎片化,从而导致自愿中断。
K8S提供了中断预算(Disruption Budgets)机制,帮助在频繁的自愿中断的情况下,实现HA。
这是从1.8引入的特性,目前处于Alpha状态。该特性允许Pod具有优先级,并且能够在无法调度Pod时,从节点驱除低优先级的Pod。
从1.9开始,优先级影响Pod的调度顺序、资源不足时Pod的驱逐顺序。高优先级的Pod更早被调度,低优先级的Pod更早被驱除。
你需要为APIServer、scheduler、kubelet启用下面的特性开关:
1 |
--feature-gates=PodPriority=true,... |
同时在APIServer中启用scheduling.k8s.io/v1alpha1这个API以及Priority这个准许控制器:
1 |
--runtime-config=scheduling.k8s.io/v1alpha1=true --admission-control=Controller-Foo,Controller-Bar,...,Priority |
这是一类非名字空间化的对象,定义了一个优先级类的名称,以及一个整数的优先级值。最大取值为10亿,更大的值保留。规格示例:
1 2 3 4 5 6 7 8 9 10 11 |
apiVersion: scheduling.k8s.io/v1alpha1 kind: PriorityClass metadata: # 名 name: high-priority # 值 value: 1000000 # 是否全局默认值,如果是,则没有定义PriorityClassName属性的Pod,其优先级均为此PriorityClass.value globalDefault: false # 供用户阅读的描述 description: "..." |
当Pod被创建后,它会列队等待调度。调度器会选取队列中的一个Pod并尝试调度到某个节点上,如果没有节点能满足Pod的需求,则抢占逻辑被激活:
- 尝试寻找这样的节点:其上具有更低优先级的Pod,并且将这些Pod驱除后,能满足正被调度的Pod的需求
- 如果找到匹配的节点,则对其上的低优先级Pod执行驱除,并调度当前Pod到节点
同一个Pod内的多个容器,可以通过共享卷(Shared Volume)进行交互。
卷天然是Pod内共享的,因为卷只能在Pod级别定义,而后任何一个容器都可以挂载它到自己的某个路径下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
kind: Pod spec: volumes: - name: shared-data # Pod被调度到某个节点上后,创建一个空目录 emptyDir: {} containers: - name: nginx-container image: nginx volumeMounts: - name: shared-data mountPath: /usr/share/nginx/html - name: debian-container image: debian volumeMounts: - name: shared-data mountPath: /pod-data command: ["/bin/sh"] # 修改共享卷的内容 args: ["-c", "echo Hello from the debian container > /pod-data/index.html"] |
注意,K8S的共享卷和Docker的--volumes-from不同。
跨斗,本指三轮摩托旁边的那个座位。
Sidecar用于辅助主要容器,让其更好的工作。例如Pod中的主容器是Nginx,它提供HTTP服务,Sidecar中运行一个Git,周期性的将最新的代码拉取过来,通过共享文件系统推送给Nginx。
使者模式,辅助容器作为一个代理服务器,主容器直接通过localhost(因为Pod内所有容器共享网络名字空间)访问外部的服务。
好处是,主容器可以和开发环境完全一致,因为在开发时常常所有东西都在localhost上。
不同容器输出的监控指标信息格式不一致,可以由一个配套的辅助容器对这些格式进行适配。
建议统一规划、配置标签:
- tier,标识应用所属的层次,取值:
- control-plane:K8S控制平面
- infrastructure:提供网络、存储等基础设施
- devops:开发、运维相关的工具
- middleware:中间件、数据库等
- application:业务域应用程序
- app,标识应用程序的类别
- comp,标识应用程序组件
- version,标识应用程序的版本
- release,标识Helm Release的名称,或者手工部署的应用程序的实例名
将此配置项设置为true,则Pod直接使用宿主机的网络。Pod直接在宿主机的网络接口上监听。
将Pod的Spec/Status字段注入为环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: POD_SERVICE_ACCOUNT valueFrom: fieldRef: fieldPath: spec.serviceAccountName |
你可以指定Pod和Node的对应关系,让Pod只能(或优选)在某些节点上运行。具体实现方式有几种,都是基于标签选择器的。
大部分情况下你不需要强制指定对应关系,因为K8S会进行合理的调度。你需要这种细致的控制机制的场景包括:
- 确保Pod在具有SSD的机器上运行
- 让两个位于不同Service中的、频繁协作的Pod,能在同一个Zone内部运行
这是Pod规格的一个字段,规定有资格运行Pod的节点,所具有的标签集。
你需要首先为节点添加标签:
1 2 3 4 5 6 7 |
# 获取集群的节点列表 kubectl get nodes # 为某个节点添加标签 kubectl label nodes disktype=ssd # 显示节点标签 kubectl get nodes --show-labels |
然后,需要为Pod规格添加一个nodeSelector字段:
1 2 3 |
spec: nodeSelector: disktype: ssd |
执行命令: kubectl get pods -o wide可以看到Pod被分配到的节点。
截止1.4,K8S内置的节点标签有:
节点标签 |
kubernetes.io/hostname |
failure-domain.beta.kubernetes.io/zone |
failure-domain.beta.kubernetes.io/region |
beta.kubernetes.io/instance-type |
beta.kubernetes.io/os |
beta.kubernetes.io/arch |
Beta特性affinity,也可以用于将Pod分配到节点。但是这一特性更加强大:
- 表达能力更强,不是简单的限制于nodeSelector的那种 k == v && k == v
- 可以标注为“软规则”而非强制要求,如果调度器无法满足Pod的需求,Pod仍然会被调度
- 可以基于其它Pod上的标签进行约束,也就是说,可以让一系列的Pod同地协作( co-located)
Affinity分为两种类型:
- Node affinity:类似于nodeSelector,但是表达能力更强、可以启用软规则(对应上面的第1、2条)
- Inter-pod affinity:基于Pod标签而非Node标签进行约束(对应上面第3条)
包含两种子类型:requiredDuringSchedulingIgnoredDuringExecution(硬限制)、preferredDuringSchedulingIgnoredDuringExecution(软限制)。
“IgnoredDuringExecution“的含义是,如果在Pod调度到Node并运行后,Node的标签发生改变,则Pod会继续运行,nodeSelector的行为也是这样的。未来可能支持后缀“RequiredDuringExecution“,也就说当运行是Node的标签发生改变,导致不满足规则后,Pod会被驱除。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
spec: affinity: nodeAffinity: # 硬限制 requiredDuringSchedulingIgnoredDuringExecution: # 加强版的节点选择器,值为数组。如果数组元素有多个,目标节点只需要匹配其中一个即可 nodeSelectorTerms: # 每个节点选择器可以包含多个表达式,如果表达式有多个,目标节点必须匹配全部表达式 - matchExpressions: # 必须在AZ(Availability Zone)e2e-az1或者e2e-az2中运行 - key: kubernetes.io/e2e-az-name # 支持的操作符包括 In, NotIn, Exists, DoesNotExist, Gt, Lt # 其中 NotIn,DoesNotExist 用于实现anti-affinity operator: In values: - e2e-az1 - e2e-az2 # 软限制 preferredDuringSchedulingIgnoredDuringExecution: # 权重,值越大越需要优先满足 - weight: 1 preference: # 节点选择器 matchExpressions: - key: another-node-label-key operator: In values: - another-node-label-value |
基于已经运行在节点上的Pod,而不是节点本身的标签进行匹配 —— 当前Pod应该/不应该运行在,已经运行了匹配规则R的Pod(s)的X上。其中:
- R表现为关联了一组名字空间的标签选择器
- X是一个拓扑域(Topology Domain),可以是Node、Rack、Zone、Region等等
和节点不同,Pod是限定名字空间的,因此它的标签也是有名字空间的。针对Pod的标签选择器必须声明其针对的名字空间。
Inter-pod affinity要求较大的计算量,因此可能拖累调度性能,不建议在大型集群(K+节点)上使用。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
spec: affinity: # Pod亲和性 podAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 如果Zone(由topologyKey指定)上已经运行了具有标签security=S1的Pod,则当前Pod也必须调度到该Zone - labelSelector: matchExpressions: - key: security # 支持的操作符In, NotIn, Exists, DoesNotExist operator: In values: - S1 topologyKey: failure-domain.beta.kubernetes.io/zone # Pod反亲和性 podAntiAffinity: # 强制 requiredDuringSchedulingIgnoredDuringExecution: # 尽可能 preferredDuringSchedulingIgnoredDuringExecution: # 如果Host(节点)上已经运行了具有标签security=S2的Pod,则当前Pod不得调度到该Host - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: security operator: In values: - S2 topologyKey: kubernetes.io/hostname |
当需要保证高级对象(ReplicaSets, Statefulsets, Deployments)在同一拓扑域内运行时, podAffinity非常有用。
下面是一个示例,3实例的Redis集群 + 3实例的Nginx前端,每个Redis不在同一节点运行,每个Nginx也不在同一节点运行,每个Nginx必须在本地有个Redis:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
kind: Deployment metadata: name: redis-cache spec: replicas: 3 template: metadata: labels: app: store # Pod模板 spec: affinity: # 当前节点上不得存在Pod的标签是app=store,也就是说,复制集的每个实例都占据单独的节点 podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - store topologyKey: "kubernetes.io/hostname" containers: - name: redis-server image: redis:3.2-alpine |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
kind: Deployment metadata: name: web-server spec: replicas: 3 template: metadata: labels: app: web-store spec: affinity: podAntiAffinity: # 当前节点上不得存在Pod具有标签app=web-store # 也就是说当前复制集的实例,都不会在同一节点上运行 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - web-store topologyKey: "kubernetes.io/hostname" podAffinity: # 当前节点上的某个Pod必须具有app=store标签 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - store topologyKey: "kubernetes.io/hostname" containers: - name: web-app image: nginx:1.12-alpine |
前文讨论的Node affinity,可以把Node和Pod吸引到一起。Taints则做相反的事情,让Pod拒绝某些Node。
使用kubectl可以为节点添加一个Taint,例如:
1 2 3 4 5 6 |
# taint的键为key,值为value # taint effect为 NoSchedule,意味着Pod不能被调度到该节点,除非它具有匹配的toleration kubectl taint nodes node1 key=value:NoSchedule # 恢复Master节点的禁止调度 kubectl taint nodes master node-role.kubernetes.io/master=:NoSchedule |
1 2 |
# 删除Taint kubectl taint nodes --all node-role.kubernetes.io/master- |
下面的Pod都能容忍上述Taint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
tolerations: # 容忍 键、值、效果组合 - key: "node-role.kubernetes.io/master" # 操作符,不指定默认为Equal operator: "Equal" value: "value" effect: "NoSchedule" # 容忍 键、效果组合 tolerations: - key: "node-role.kubernetes.io/master" operator: "Exists" effect: "NoSchedule" # 容忍一切 tolerations: - operator: "Exists" # 容忍键和一切效果 tolerations: - key: "node-role.kubernetes.io/master" operator: "Exists" # 容忍某种效果 tolerations: - effect: NoSchedule operator: Exists |
取值 | 说明 | ||
NoSchedule | 不能容忍Taint的Pod绝不会调度到节点 | ||
PreferNoSchedule | 不能容忍Taint的Pod尽量不被调度到节点 | ||
NoExecute |
如果Pod不能容忍此Effect且正在节点上运行,它会被从节点上被驱除:
|
场景 | 说明 |
专用节点 |
如果希望某些节点被特定Pod集专用,你可以为节点添加Taint、并为目标Pod添加匹配的Toleration。这样,目标Pod可以在专用节点上运行 如果要让目标Pod仅能在专用节点,可以配合nodeSelector |
特殊硬件节点 | 集群中可能存在一小部分具有特殊硬件(例如GPU)的节点,Taint可以让不需要特殊硬件的Pod和这些节点隔离 |
定制化Pod驱除 | 可以在节点出现问题时,为每个Pod定制驱除策略,这是一个Alpha特性 |
使用NoExecute可以导致已经运行在节点上的Pod被驱除,Pod配置tolerationSeconds可以指定驱除的Buffer时间。
从1.6开始,节点控制器会在节点状态变化时,自动添加Taint,包括:
Taint | 说明 |
node.kubernetes.io/not-ready | 节点没有准备好,对应NodeCondition.Ready=False |
node.alpha.kubernetes.io/unreachable | 节点控制器无法连接到节点,对应NodeCondition.Ready=Unknown |
node.kubernetes.io/out-of-disk | 节点磁盘空间不足 |
node.kubernetes.io/memory-pressure | 节点面临内存压力 |
node.kubernetes.io/disk-pressure | 节点面临磁盘压力 |
node.kubernetes.io/network-unavailable | 节点的网络不可用 |
node.cloudprovider.kubernetes.io/uninitialized | 当Kubelet通过“外部”云服务启动时,它会设置此Taint,当CCM初始化了节点后,移除此Taint |
PDB(PodDisruptionBudget)可以用来构建高可用的应用程序。
除非被人工或控制器删除,或者出现不可避免的软硬件错误,Pod不会消失。那些不可避免软硬件错误导致的Pod删除,称为非自愿中断(involuntary disruptions ),具体包括:
- 硬件故障
- 虚拟机故障,例如被误删除
- 内核崩溃
- 集群网络分区导致节点丢失
- 由于资源不足,Pod被kubelet驱除
要避免非自愿中断影响应用程序的可用性,可以考虑:
- 合理的配置资源请求
- 使用Deployment/StatefulSet
要避免资源中断影响应用程序的可用性,可以使用PDB。
PDB可以限制复制集中,同时由于自愿中断而宕掉的Pod的最大数量。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: policy/v1beta1 kind: PodDisruptionBudget metadata: name: zookeeper spec: # 最小保证2实例处于运行中,可以指定百分比,例如 25% minAvailable: 2 # 也可以指定最大宕机数量,可以指定百分比,例如 25% maxUnavailable:1 # 控制的目标应用程序 selector: matchLabels: app: zookeeper |
ConfigMap用于把配置信息和容器的镜像解耦,保证容器化应用程序的可移植性。
ConfigMap中文叫配置字典,下一章的Secrets中文叫保密字典,两者很类似,只是后者的内容是编码存储的。
命令格式:
1 2 3 4 5 6 |
# map-name是ConfigMap的名称 # data-source 目录、文件,或者硬编码的字面值 kubectl create configmap # 在ConfigMap对象中,data-source表现为键值对: # key:文件名,或者通过命令行提供的key # value:文件内容,或者通过命令行提供的字面值 |
下面是把目录作为数据源的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
kubectl create configmap game-config --from-file=path-to-dir # ls path-to-dir # game.properties # ui.properties kubectl describe configmaps game-config # Name: game-config # Namespace: default # Labels: # Annotations: # Data # ==== # game.properties: 158 bytes # ui.properties: 83 bytes |
目录中文件的内容,会读取到ConfigMap的data段中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
apiVersion: v1 data: # 文件名为键,文件内容为值(字符串) # 如果要自定义键,可以 --from-file== 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:52:05Z name: game-config namespace: default resourceVersion: "516" selfLink: /api/v1/namespaces/default/configmaps/game-config uid: b4952dc3-d670-11e5-8cd0-68f728db1985 |
如果将文件作为数据源,可以指定--from-file多次,引用多个文件。
使用该选项可以从Env-File创建ConfigMap,例如文件:
1 2 3 |
enemies=aliens lives=3 allowed="true" |
可以创建ConfigMap:
1 |
kubectl create configmap game-config-env-file --from-env-file=game-env-file.properties |
结果ConfigMap为:
1 2 3 4 5 6 7 |
apiVersion: v1 data: # 注意是一个个键值对,而不是字符串 allowed: '"true"' enemies: aliens lives: "3" kind: ConfigMap |
使用该选项,直接通过命令行给出ConfigMap的数据:
1 2 |
kubectl create configmap special-config --from-literal=special.how=very --from-literal=special.type=charm |
定义单个环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
apiVersion: v1 kind: Pod metadata: name: test spec: containers: - name: test env: # 定义一个环境变量 - name: SPECIAL_LEVEL_KEY valueFrom: configMapKeyRef: # 环境变量的值,从名为special-config的ConfigMap中获取,使用其中的special.how的值 name: special-config key: special.how |
你可以把ConfigMap中的所有键值对导出为Pod的环境变量:
1 2 3 4 5 6 |
spec: containers: - name: test-container envFrom: - configMapRef: name: special-config |
使用特殊语法 $(VAR_NAME),例如:?
1 |
command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ] |
1 2 3 4 5 6 7 8 9 10 |
spec: containers: - name: test-container volumeMounts: - name: config-volume mountPath: /etc/config volumes: - name: config-volume configMap: name: special-config |
这样,Pod启动后,/etc/config目录下会出现若干文件。这些文件的名字是configMap的键,内容是键对应的值。
你可以使用path属性指定某个ConfigMap键映射到的文件路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
spec: containers: - volumeMounts: - name: config-volume mountPath: /etc/config volumes: - name: config-volume configMap: name: special-config items: # ConfigMap中条目的key - key: special.level # 映射到mountPath下的什么文件,默认为key path: special.lv |
special.level的内容会映射到容器路径:/etc/config/special.lv
加载为卷的ConfigMap被修改后,K8S会自动的监测到,并更新Pod的卷的内容。延迟取决于Kubelet的同步周期。
加载为环境变量的ConfigMap修改后不会刷新到已经运行的Pod。
挂载ConfigMap时,如果ConfigMap中包含多个配置文件,可以指定每个文件映射到容器的什么路径:
1 2 3 4 5 6 7 |
volumeMounts: # 卷名,必须是ConfigMap或者Secret卷 - name: dangconf # 挂载到容器路径 mountPath: /etc/dangdangconfig/digital/api.config # ConfigMap或Secret中的文件名 subPath: api-config.properties |
SubPath挂载的内容不会随ConfigMap变更,这可能是Kubernetes的Bug。
Secret是一种K8S对象,用于保存敏感信息,例如密码、OAuth令牌、SSH私钥。将这些信息存放在Secret(而不是Pod规格、Docker镜像)中更加安全、灵活。
你可以在Pod的规格配置中引用Secret。
K8S会自动生成一些Secret,其中包含访问API所需的凭证,并自动修改Pod以使用这些Secret。
你可以通过kubectl来创建Secret:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# 创建一个名为db-user-pass的一般性Secret kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt # 创建用作Ingress TLS证书的Secret kubectl create secret tls tls-secret --cert=/home/alex/Documents/puTTY/k8s.gmem.cc/tls.crt --key=/home/alex/Documents/puTTY/k8s.gmem.cc/tls.key # 直接提供字面值 kubectl -n kube-system create secret generic chartmuseum --from-literal=BASIC_AUTH_USER=alex --from-literal=BASIC_AUTH_PASS=alex kubectl get secrets # NAME TYPE DATA AGE # db-user-pass Opaque 2 51s kubectl describe secrets/db-user-pass # ... # Type: Opaque # Data # ==== # password.txt: 12 bytes # username.txt: 5 bytes |
你也可以手工的指定Secret规格:
1 2 3 4 5 6 7 8 9 10 |
apiVersion: v1 kind: Secret metadata: name: mysecret type: Opaque data: # 每个数据必须Base64处理 # echo -n "admin" | base64 username: YWRtaW4= password: MWYyZDFlMmU2N2Rm |
使用如下命令可以获得Secret的所有属性,并输出为YML格式:
1 |
kubectl get secret mysecret -o yaml |
然后你可以复制Base格式的数据并解码:
1 |
echo "MWYyZDFlMmU2N2Rm" | base64 --decode |
Secret可以作为数据卷挂载,示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
spec: containers: - name: mypod image: redis volumeMounts: # 把foo卷只读挂载到文件系统路径下 - name: foo mountPath: "/etc/foo" readOnly: true volumes: # 定义一个名为foo的卷,其内容为mysecret这个Secret - name: foo secret: secretName: mysecret |
默认的,Secret中的每个Data的key,都作为mountPath下的一个文件名。你可以定制Data到容器文件系统的映射关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
spec: containers: - name: mypod image: redis volumeMounts: - name: foo mountPath: "/etc/foo" readOnly: true volumes: - name: foo secret: secretName: mysecret items: - key: username # 在容器中,访问路径为/etc/foo/my-group/my-username path: my-group/my-username # 文件模式为0400,这里必须转为十进制 defaultMode: 256 |
在容器中读取Data映射文件时,获得的是明文。
Secret还可以映射为容器的环境变量:
1 2 3 4 5 6 7 8 9 10 |
spec: containers: - name: mycontainer image: redis env: - name: SECRET_USERNAME valueFrom: secretKeyRef: name: mysecret key: username |
当你通过kubectl访问集群时,APIServer基于特定的用户账号(默认admin)对你进行身份验证。
服务账号(ServiceAccount)为运行在Pod中的进程提供身份(Identity)信息,这样当这些进程联系APIServer时,也能够通过身份验证。对服务账号的授权由授权插件和策略负责。
三个组件进行协作,完成和SA相关的自动化:
- 服务账户准入控制器(Service account admission controller),是API Server的一部分,当Pod创建或更新时:
- 如果该 pod 没有 ServiceAccount 设置,将其 ServiceAccount 设为 default
- 如果设置的ServiceAccount不存在则拒绝Pod
- 如果Pod不包含ImagePullSecrets设置,则将SA中的ImagePullSecrets拷贝进来
- 将一个用于访问API Server的Token作为卷挂载到Pod
- 将/var/run/secrets/kubernetes.io/serviceaccount下的VolumeSource添加到每个容器
- Token 控制器(Token controller),是controller-manager的一部分,异步方式来:
- 当SA创建后,创建对应的Secret,用于支持API 访问
- 当SA删除后,删除对应的Token Secret
- 当Token Secret删除后,在SA中去除对应的数组元素
- 当通过annotation引用了SA的ServiceAccountToken类型的Secret创建后,自动生成Token并更新Secret字段:
12345678apiVersion: v1kind: Secretmetadata:name: build-robot-secretannotations:# 提示此Secret为了哪个SAkubernetes.io/service-account.name: build-robottype: kubernetes.io/service-account-token
- 服务账户控制器(Service account controller)
- 确保每个命名空间具有default帐户
你需要通过 --service-account-private-key-file传递一个私钥给controller-manager,用于对SA的Token进行签名。
你需要通过 --service-account-key-file传递一个公钥给kube-api-server,以便API Server对Token进行校验。
v1.12引入的Beta特性。要启用此特性,需要给API Server传递:
- --service-account-issuer:SA Token颁发者的标识符,颁发者会断言颁发的Token的iss claim中具有此参数的值,字符串或URI,例如kubernetes.default.svc
- --service-account-signing-key-file:包含SA Token颁发者私钥的路径,需要打开特性TokenRequest
- --service-account-api-audiences:API的标识符
Kubelet能够把SA Token影射到Pod中,你需要提供必须的属性,包括audience、有效期:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - image: nginx name: nginx volumeMounts: - mountPath: /var/run/secrets/tokens name: vault-token serviceAccountName: build-robot volumes: - name: vault-token projected: sources: - serviceAccountToken: path: vault-token expirationSeconds: 7200 audience: vault |
对于上述配置,Kubelet会代表Pod来请求Token,并存储到对应位置,并且在80%有效期过去后,自动刷新Token。应用程序需要自己检测Token已经刷新。
当你创建Pod时,如果显式提供服务账号,则K8S在相同名字空间内为Pod分配一个默认账号(名为default),并自动设置到spec.serviceAccountName字段。
你可以在Pod内部调用K8S API,基于自动挂载的服务账号凭证(令牌)。从1.6开始,令牌的自动挂载可以禁用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
apiVersion: v1 kind: ServiceAccount metadata: name: default # 在服务账号上禁用 automountServiceAccountToken: false --- apiVersion: v1 kind: Pod metadata: name: my-pod spec: serviceAccountName: default # 针对单个Pod禁用 automountServiceAccountToken: false |
每个名字空间都有一个名为default的默认服务账号,你可以自己创建新的账号。
执行下面的命令列出所有账号:
1 2 3 |
kubectl get serviceAccounts # NAME SECRETS AGE # default 1 1d |
ServiceAccount的规格很简单,上一节有示例。创建账号后,你可以利用授权插件来设置账号的权限。
要让Pod使用非默认账号,配置spec.serviceAccountName字段即可。
如果禁用了服务账号的自动凭证生成功能,你需要手工的创建令牌。令牌是一个Secret对象:
1 2 3 4 5 6 7 |
apiVersion: v1 kind: Secret metadata: name: build-robot-secret annotations: kubernetes.io/service-account.name: build-robot type: kubernetes.io/service-account-token |
ImagePullSecrets是用于访问镜像私服的Secret,假设你已经创建好一个名为gmemregsecret的Secret, 则可以使用下面的命令,为服务账号添加ImagePullSecrets:
1 |
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}' |
创建docker.gmem.cc的Secret:
1 2 3 4 |
# 创建名为gmemregsecret类型为docker-registry的secret kubectl create secret docker-registry gmemregsecret \ --docker-server=docker.gmem.cc --docker-username=alex \ --docker-password=pswd --docker-email=k8s@gmem.cc |
查看刚刚创建的Secret:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
kubectl get secret gmemregsecret --output=yaml # apiVersion: v1 # data: # .dockerconfigjson: eyJhdXRocyI6eyJkb2NrZXIuZ21lbS5jYzo1MDAwIjp7InVzZXJuYW1lIjoiYWxleCIsInBhc3N3b3JkIjoibGF2ZW5kZXIiLCJlbWFpbCI6Ims4c0BnbWVtLmNjIiwiYXV0aCI6IllXeGxlRHBzWVhabGJtUmxjZz09In19fQ== # kind: Secret # metadata: # creationTimestamp: 2018-02-11T14:04:52Z # name: gmemregsecret # namespace: default # resourceVersion: "1023033" # selfLink: /api/v1/namespaces/default/secrets/gmemregsecret # uid: 87db6e2e-0f34-11e8-92db-deadbeef00a0 # type: kubernetes.io/dockerconfigjson |
其中data字段为BASE64编码的密码数据,可以查看一下它的明文:
1 2 |
echo eyJhdXRocyI6eyJkb2NrZXIuZ21lbS5jYzo1MDAwIjp7InVzZXJuYW1lIjoiYWxleCIsInBhc3N3b3JkIjoibGF2ZW5kZXIiLCJlbWFpbCI6Ims4c0BnbWVtLmNjIiwiYXV0aCI6IllXeGxlRHBzWVhabGJtUmxjZz09In19fQ== | base64 -d # {"auths":{"docker.gmem.cc":{"username":"alex","password":"lavender","email":"k8s@gmem.cc","auth":"YWxleDpsYXZlbmRlcg=="}}}alex@Zircon:~$ |
现在你可以创建使用此Secret来拉取镜像的Pod了:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: v1 kind: Pod metadata: name: digitalsrv-1 labels: tier: rpc app: digitalsrv spec: containers: - name: digitalsrv-1 image: docker.gmem.cc/digitalsrv:1.0 imagePullSecrets: - name: gmemregsecret |
执行 kubectl edit serviceaccounts default,修改为如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: v1 kind: ServiceAccount metadata: creationTimestamp: 2018-02-12T07:53:30Z name: default namespace: default resourceVersion: "1843" selfLink: /api/v1/namespaces/default/serviceaccounts/default uid: d0e062a3-0fc9-11e8-b942-deadbeef00a0 secrets: - name: default-token-j9zdx imagePullSecrets: - name: gmemregsecret |
或者,执行命令:
1 |
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}' |
下一代复制控制器,与Replication Controller仅仅的不同是对选择器的支持, ReplicaSet支持指定set-based的选择器(来匹配Pod),后者仅支持equality-based的选择器。
尽管ReplicaSet可以被独立使用,但是实际上它主要通过Deployment间接使用,作为编排Pod创建、更新、删除的工具。使用Deployment时你无需关心其自动创建的ReplicaSet对象。
ReplicaSet用于确保,在任何时刻,指定数量的Pod实例同时在集群中运行。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
apiVersion: apps/v1 kind: ReplicaSet metadata: name: frontend labels: app: guestbook tier: frontend spec: # 复制实例的数量,默认1 replicas: 3 # 选择器,用于匹配被控制的Pod # 只要匹配,即使Pod不是ReplicaSet自己创建的,也被管理 —— 这允许替换ReplicaSet而不影响已存在的Pod的控制 selector: matchLabels: tier: frontend matchExpressions: - {key: tier, operator: In, values: [frontend]} # template是spec段唯一强制要求的元素,提供一个Pod模板 # 此Pod模板的结构和Pod对象完全一样,只是没有apiVersion/kind字段 template: metadata: # ReplicaSet中的Pod模板必须指定标签、以及适当的重启策略 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 # 如果集群没有配备DNS Addon,则可以从环境变量获取service的主机名 # value: env ports: - containerPort: 80 |
要删除ReplicaSet及其Pod,可以使用 kubectl delete命令。Kubectl会将ReplicaSet缩容为0,并在删除ReplicaSet之前等待其所有Pod的删除,使用REST API或者Go客户端库时,你需要手工缩容、等待Pod删除、并删除ReplicaSet。
要仅删除ReplicaSet,可以使用 kubectl delete --cascade=false命令。 使用REST API/Go客户端库时,简单删除ReplicaSet对象即可。
在删除ReplicaSet之后,你可以创建.spec.selector字段与之一样的新的ReplicaSet,这个新的RS会管理原先的Pod。但是现有Pod不会匹配新RS的Pod 模板,你可以通过滚动更新(Rolling Update)实现现有Pod的更新。
要从RS中隔离一个Pod,可以修改Pod的标签。注意,此Pod会很快被代替,以满足ReplicaSet的复制数量要求。
要对RS进行扩容/缩容,你仅仅需要更新.spec.replicas字段。
RS可以作为Pod水平自动扩容器(Horizontal Pod Autoscaler)的目标。HPA的示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata: name: frontend-scaler spec: # 扩容控制的目标 scaleTargetRef: kind: ReplicaSet name: frontend minReplicas: 3 maxReplicas: 10 targetCPUUtilizationPercentage: 50 |
v2版本的HPA示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: podinfo namespace: default spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: podinfo minReplicas: 1 maxReplicas: 5 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 50 |
针对上述配置文件调用kubectl create -f即可创建HPA。你也可以直接调用
1 |
kubectl autoscale rs frontend |
更加简便。
类似于ReplicaSet。ReplicaSet是下一代的复制控制器。
大部分支持ReplicationController的kubectl命令,同时也支持ReplicaSet,一个例外是rolling-update命令。此命令专门用于RC的滚动更新 —— 每次更新一个Pod。
此控制器为Pod或ReplicaSet提供声明式的更新支持。在Deployment中,你可以指定一个期望状态,Deployment控制器会以一定的控制速率来修改实际状态,让它和期望状态匹配。通过定义Deployment你可以创建新的ReplicaSet,或者移除现有的Deployment并接收其全部资源。
Deployment的典型应用场景:
- 创建一个Deployment,来rollout一个ReplicaSet。ReplicaSet会在后台创建Pod。检查rollout的状态来确认是否成功
- 更新Deployment的PodTemplateSpec部分,来声明Pods的新状态。一个新的复制集会被创建,Pod会已移动的速率,从老的复制集中移动到新的复制集
- 回滚到旧的部署版本,如果当前版本的部署不稳定,则可以回滚到旧的Deployment版本
- 扩容以满足负载需要
- 暂停部署,对PodTemplateSpec进行更新,然后恢复部署,进行rollout
- 将Deployment的状态作为rollout卡死的提示器
- 清除你不再需要的复制集
下面的例子创建了三个运行Nginx的Pod构成的复制集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: # 复制集容量 replicas: 3 # 可选此控制器如何找到需要管理的Pod,必须匹配.spec.template.metadata.labels selector: matchLabels: app: nginx # 如何替换旧Pod的策略,Recreate或RollingUpdate,默认RollingUpdate # Recreate 当新的Pod创建之前,所有旧Pod被删除 # RollingUpdate 滚动更新(逐步替换) strategy.type: RollingUpdate # 更新期间,处于不可用状态的Pod的数量,可以指定绝对值或者百分比。默认25 strategy.rollingUpdate.maxUnavailable: 0 # 更新期间,同时存在的Pod可以超过期望数量的多少,可以指定绝对值或者百分比。默认25 strategy.rollingUpdate.maxSurge: 1 # 在报告failed progressing之前等待更新完成的最长时间 progressDeadlineSeconds: 60 # 新创建的Pod,最少在启动多久后,才被认为是Ready。默认0 minReadySeconds: 0 # 保留rollout历史的数量,默认无限 revisionHistoryLimit: # Pod的模板 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: # 打开端口80,供Pod使用 - containerPort: 80 |
根据上述规格创建Deployment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
kubectl create -f nginx-deployment.yaml # 获取对象信息 kubectl get deployments # 部署名称 期望实例数 当前运行实体数 到达期望状态实例数 对用户可用实例数 应用运行时长 # UP-TO-DATE 意味着使用最新的Pod模板 # AVAILABLE的Pod至少进入Ready状态.spec.minReadySeconds秒 # NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE # nginx-deployment 3 0 0 0 1s # 要查看部署的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 # 一段时间后,再次获取对象信息 NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE nginx-deployment 3 3 3 3 18s # 要查看Deployment自动创建的复制集,执行 kubectl get rs # NAME DESIRED CURRENT READY AGE # nginx-deployment-2035384211 3 3 3 18s # 复制集名称的格式为 [DEPLOYMENT-NAME]-[POD-TEMPLATE-HASH-VALUE] # 要查看为Pod自动创建的标签,执行 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 |
部署的rollout仅仅在其Pod模板发生变化时才会触发。其它情况,例如进行扩容,不会触发rollout。
修改Pod使用的镜像,示例命令:
1 2 3 4 |
# 方式一 kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1 # 方式二 kubectl edit deployment/nginx-deployment |
Deployment能够控制在更新时:
- 仅一定数量的Pod可以处于宕机状态。默认值是,最多运行期望Pod数量 - 1个处于宕机状态
- 存在超过期望数量的Pod存在。默认值是,最多同时存在 期望Pod数量 + 1 个Pod。K8S会先创建新Pod,然后删除旧Pod,这会导致同时存在的Pod数量超过期望Pod数
Deployment允许多重同时进行中的更新(multiple updates in-flight),所谓Rollover。每当部署控制器监控到新的Deployment对象时,旧有的控制标签匹配.spec.selector而模板不匹配.spec.template的Pod复制集会缩容为0,新的复制集则会扩容到期望Pod数量。如果你更新Deployment时,它正在进行rollout,则新的复制集被创建(每次更新对应一个)并扩容,并且roll over 前一次更新创建的复制集 —— 将其加入到旧复制集列表,并进行缩容。举例来说:
- 某个部署创建了5实例的nginx:1.7.9
- 随后更新部署为5实例的nginx:1.9.1,此时nginx:1.7.9的3个实例已经被创建
- 这时,部署控制器会立刻杀死nginx:1.7.9的3个实例,随后开始创建nginx:1.9.1实例。而不是等待nginx:1.7.9的5个实例都创建完毕,再对其缩容
你也可以进行标签选择器的更新,但是这并不推荐,你应该预先规划好标签。
某些情况下你需要回滚一次部署,这通常是因为新版本存在问题,例如无限循环崩溃。
默认情况下,Deployment所有的rollout历史(revision)都会保存在系统中,方便你随时进行回滚。注意revision仅仅在Deployment的rollout被触发时才生成,也就是仅仅在Deployment的Pod模板变更时才生成。执行回滚后,仅仅Pod模板部分被回滚,扩容、标签部分不受影响。
相关命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 列出rollout历史 kubectl rollout history deployment/nginx-deployment # deployments "nginx-deployment" # REVISION CHANGE-CAUSE # 1 kubectl create -f 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 # 查看某次rollout的详细信息 kubectl rollout history deployment/nginx-deployment --revision=2 # 回滚到上一个版本 kubectl rollout undo deployment/nginx-deployment # 回滚到指定的版本 kubectl rollout undo deployment/nginx-deployment --to-revision=2 |
设置Pod实例数量:
1 |
kubectl scale deployment nginx-deployment --replicas=10 |
如果集群启用了Pod自动水平扩容(horizontal pod autoscaling),你可以为Deployment设置autoscaler:
1 2 |
# 实例数量在10到15之间,尽可能让这些实例的CPU占用靠近80% kubectl autoscale deployment nginx-deployment --min=10 --max=15 --cpu-percent=80 |
在触发一个或多个更新之前,你可以暂停Deployment,避免不必要的rollout:
1 2 3 4 5 6 7 8 9 |
# 暂停rollout kubectl rollout pause deployment/nginx-deployment # 更新Deployment kubectl set image deploy/nginx-deployment nginx=nginx:1.9.1 kubectl set resources deployment nginx-deployment -c=nginx --limits=cpu=200m,memory=512Mi # 恢复rollout kubectl rollout resume deploy/nginx-deployment |
Deployment在其生命周期中,会进入若干不同的状态:
状态 | 说明 |
progressing |
执行以下任务之一时,K8S将Deployment标记为此状态:
|
complete |
如果具有以下特征,K8S将Deployment标记为此状态:
|
fail to progress |
无法部署Deployment的最新复制集,可能原因是:
具体多久没有完成部署,会让Deployment变为此状态,受spec.progressDeadlineSeconds控制 一旦超过上述Deadline,则部署控制器为Deployment.status.conditions添加一个元素,其属性为 Type=Progressing,Status=False,Reason=ProgressDeadlineExceeded 注意,暂停中的Deployment不会超过Deadline |
执行下面的命令可以查看Deployment的状态:
1 |
kubectl rollout status |
设置Deployment的.spec.revisionHistoryLimit字段,可以控制更新历史(也就是多少旧的ReplicaSet)被保留,默认所有历史都保留。
原先叫做PetSet。该控制器用于管理部署、扩容Pod集。并提供一个保证:确保Pod的有序性、唯一性。
和Deployment类似,SS也能管理(基于相同的容器规格的一组)Pods,但是SS还能够维护每个Pod的粘性身份(Sticky Identity )。尽管这些Pod的规格完全一样,但是不能相互替换(有状态),每个Pod都有自己的持久化的唯一标识,即使发生重新调度,也不会改变。
SS的行为模式和其它控制器类似,你需要定义一个StatefulSet对象,说明期望的状态。StatefulSet控制器会执行必要的更新以达到此状态。
对于有以下需求的应用程序,考虑使用SS:
- 稳定(Pod重调度后不变)的、唯一的网络标识符(network identity)
- 稳定的、持久的存储
- 有序的、优雅的部署和扩容
- 有序的、优雅的删除和终结
- 有序的、自动化的滚动更新
如果不满足上述需求之一,你应该考虑提供无状态复制集的控制器,例如Deployment、ReplicaSet。
使用SS时,要注意:
- 在1.9之前处于Beta状态,1.5-版本完全不可用
- 和其它所有Alpha/Beta资源一样,SS可以通过APIServer参数--runtime-config禁用
- Pod所需的存储资源,要么由 PersistentVolume Provisioner 提供,要么由管理员预先提供
- 删除/缩容SS时,和SS关联的卷不会自动删除
- SS目前依赖Headless Service,后者负责维护Pod的网络标识符,此Service需要你来创建
Headless Service的规格示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 用于控制网络域(Network Domain) apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx |
StatefulSet的规格示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: # 1.8-,如果spec.selector不设置,K8S使用默认值 # 1.8+,不设置和.spec.template.metadata.labels匹配的值会在创建SS时出现验证错误 selector: matchLabels: app: nginx # has to match .spec.template.metadata.labels # 管理此SS的Headless Service的名称 serviceName: "nginx" # 包含三个有标识的Pod中启动的Nginx容器 replicas: 3 template: metadata: labels: app: nginx # has to match .spec.selector.matchLabels spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: gcr.io/google_containers/nginx-slim:0.8 ports: - containerPort: 80 name: web # 将卷www挂载到目录树 volumeMounts: - name: www mountPath: /usr/share/nginx/html # 使用PersistentVolumeProvisioner提供的PersistentVolume,作为持久化的存储 volumeClaimTemplates: - metadata: name: www spec: accessModes: [ "ReadWriteOnce" ] storageClassName: my-storage-class resources: requests: storage: 1Gi |
SS创建的Pod具有唯一性的身份,此身份对应了稳定的网络标识符、稳定的存储。 此身份总是关联到Pod,不管Pod被重新调度到哪个节点上。
序号索引:对于副本份数为N的SS,每个Pod被分配一个整数序号,值范围 [ 0, N)
稳定网络标识符:每个Pod的hostname的形式为 ${SS名称}-${序号索引}。例如上面的例子中,会创建web-0、web-1、web-2三个Pod。
SS可以利用Headless Service来控制其Pod的域名,HS的域名格式为 $(service name).$(namespace).svc.cluster.local,其中cluster.local是集群的域名,上例中的HS域名为nginx.default.svc.cluster.local。
在HS的管理下,Pod的域名格式为 $(podname).$(governing service domain),因此web-0的域名为web-0.nginx.default.svc.cluster.local
对于每个VolumeClaimTemplate,K8S会创建对应的PersistentVolume。在上面的例子中,每个Pod会被赋予StorageClass为my-storage-class的单个PersistentVolume,以及1GB的存储空间。如果不指定StorageClass使用默认值。
当Pod被重新调度时,volumeMounts指定的挂载规则会重新挂载PersistentVolume。此外,即使SS或Pod被删除,PersistentVolume也不会被自动删除,你必须手工的删除它。
SS创建一个新Pod时,会为其添加一个标签:statefulset.kubernetes.io/pod-name。通过此标签,你可以为某个Pod实例Attach一个服务。
- 当SS部署Pod时,会顺序的依次创建,从序号0开始逐个的
- 当SS删除Pod时,会逆序的依次删除,从序号N-1开始逐个的
- 水平扩容时,新序号之前的所有Pod必须已经Running & Ready
- 在Pod被终结时,其后面的所有Pod必须已经被完全关闭
SS不应该指定pod.Spec.TerminationGracePeriodSeconds=0。
该控制器能确保所有(或部分)节点运行Pod的单个副本。每当节点加入到集群时,Pod就被添加到其上;每当节点离开集群时,其上的Pod就被回收。删除DS会导致所有Pod被删除。
DS的典型应用场景包括:
- 运行集群级别的存储守护程序,例如glusterd、ceph这些程序每个节点仅需要一个
- 在每个节点运行日志收集程序,例如fluentd、logstash
- 在每个节点上运行监控程序,例如collectd
DS的规格示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
apiVersion: apps/v1 kind: DaemonSet metadata: name: fluentd-elasticsearch namespace: kube-system labels: k8s-app: fluentd-logging spec: selector: matchLabels: name: fluentd-elasticsearch template: metadata: labels: name: fluentd-elasticsearch spec: # 用于仅仅在部分节点上运行Pod nodeSelector: tolerations: - key: node-role.kubernetes.io/master effect: NoSchedule containers: - name: fluentd-elasticsearch image: gcr.io/google-containers/fluentd-elasticsearch:1.20 resources: limits: memory: 200Mi requests: cpu: 100m memory: 200Mi volumeMounts: - name: varlog mountPath: /var/log - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true terminationGracePeriodSeconds: 30 volumes: - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers |
要和DaemonSet中的Pod通信,有以下几种手段:
- Push:DS中的Pod可能配置为,向其它服务发送更新,例如向统计数据库
- 节点IP + 已知端口:Pod可能使用hostPort来暴露端口
- DNS:使用和DS相同的Pod选择器,创建一个Headless Service。然后你可以通过endpoints资源来发现DS
可以为DaemonSet的Pod声明hostPort,这样,宿主机的端口会通过iptables的NAT转发给Pod:
1 2 3 4 5 |
ports: - name: http containerPort: 80 protocol: TCP hostPort: 80 |
Job可以创建一或多个Pod,并且确保一定数量的Pod成功的完成。Job会跟踪Pod们的执行状态,并判断它们是否成功执行,当指定数量的Pod成功了,则Job本身的状态变为成功。删除Job会清理掉它创建的Pod。
Job的一个简单用例是,确保Pod成功执行完成。如果第一个Pod失败/被删除,则Job会启动第二个实例。
Job也支持并行的运行多个Pod。
Job的规格示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 backoffLimit: 4 |
定期调度执行的Job,规格示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
apiVersion: batch/v1beta1 kind: CronJob metadata: name: hello spec: # Cron表达式,每分钟执行 schedule: "*/1 * * * *" # 如果由于某些原因,错过了调度时间,那么在什么时间差异之内,允许启动Job。默认没有deadline,总是允许启动 startingDeadlineSeconds: 5 # 并发控制策略,此CronJob创建的多个Job如何并发运行 # Allow,默认,允许并发运行多个Job # Forbid,禁止并发运行,如果尝试调度时发现先前的Job仍然在运行,跳过本次调度 # Replace,禁止并发运行,如果尝试调度时发现先前的Job仍然在运行,替换掉先前的Job concurrencyPolicy: Allow # 如果设置为true,则暂停后续调度,已经存在的Job不受影响 suspend: false # 保留的成功、失败的Job的数量.超过限制则删除对应的K8S资源 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 0 jobTemplate: # 下面的配置同Job spec: template: spec: containers: - name: hello image: busybox args: - /bin/sh - -c - date restartPolicy: OnFailure |
Kubernetes garbage collector是控制器的一种,它的职责是删除哪些曾经拥有,但是现在已经没有Owner的对象。
某些对象是其它对象的所有者(Owner),例如ReplicaSet是一系列Pod的所有者。被所有者管理的对象称为依赖者(Dependent )。任何依赖者都具有字段 metadata.ownerReferences,指向其所有者。
某些情况下,K8S会自动设置ownerReference。例如创建ReplicaSet时,对应Pod的ownerReference即自动设置。常作为所有者的内置资源类型包括ReplicationController, ReplicaSet, StatefulSet, DaemonSet, Deployment, Job和CronJob。
你也可以手工配置ownerReference字段,以建立所有者-依赖者关系。
在删除对象时,可以指定是否级联删除(Cascading deletion)其依赖者(依赖被删除对象的哪些对象)。级联删除有两种执行模式:前台删除、后台删除。
在这种模式下,根对象(被显式删除的顶级Owner)首先进入删除中“(deletion in progress)”状态。此时:
- 对象仍然可以通过REST API看到
- 对象的deletionTimestamp字段不再为空
- 对象的 metadata.finalizers数组包含元素“foregroundDeletion“
一旦进入“删除中”状态,垃圾回收器就开始删除对象的依赖者。一旦所有阻塞依赖者(ownerReference.blockOwnerDeletion=true)全被删除,根对象就被删除。
当某种控制器设置了ownerReferences,它也会自动设置blockOwnerDeletion,不需要人工干预。
根对象立即被删除。垃圾回收器异步的在后台删除依赖者。
通过设置删除请求的DeleteOptions.propagationPolicy,可以修改级联删除模式:
- Orphan,孤儿化,解除ownerReferences而不删除
- Foreground,前台级联删除
- Background,后台级联删除
准许控制器(Admission Controller)是一小片的代码,被编译到kube-apiserver的二进制文件中。它能够拦截针对APIServer的请求,具体拦截时机是:操控对象被持久化之前、请求通过身份验证之后。很多K8S的高级特性需要准许控制器的介入。
准许控制器可以具有两个行为:
- validating:不能修改其admit的对象
- mutating:能够修改其admit的对象
准许控制的流程分为两个阶段,首先运行mutating类控制器,然后运行validating类控制器,任何一个控制器在任何阶段拒绝请求,则客户端收到一个错误。需要注意某些控制器同时有mutating、validating行为。
控制器的执行顺序,等同于它们在--admission-control参数中声明的顺序。
对于1.9版本,建议按序开启以下准许控制器:
1 2 3 |
--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel, DefaultStorageClass,ValidatingAdmissionWebhook,ResourceQuota, DefaultTolerationSeconds,MutatingAdmissionWebhook |
(静态)准许控制器本身不够灵活:
- 需要被编译到kube-apiserver镜像中
- 仅仅当API Server启动后才可以被配置
Admission Webhooks可以解决此问题,用它可以来开发代码独立(Out-of-tree)、支持运行时配置的准许控制器。
和静态的准许控制器一样,Webhook也分为validating、mutating两类。
Webhook实际上是由ValidatingAdmissionWebhook、MutatingAdmissionWebhook这两个静态准许控制器适配(到API Server)的,因此这两个遵顼控制器必须启用。
这种服务需要处理admissionReview请求,并将其准许决定封装在admissionResponse中。
admissionReview可以是版本化的,Webhook可以使用admissionReviewVersions字段声明它能处理的Review的版本列表。调用Webhook时API Server会选择此列表中、它支持的第一个版本,如果找不到匹配版本,则失败。
下面是Kubernetes官方提供的样例Webhook服务器代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
package main import ( "encoding/json" "flag" "fmt" "io/ioutil" "net/http" "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" ) // 一个助手函数,创建内嵌error的AdmissionResponse func toAdmissionResponse(err error) *v1beta1.AdmissionResponse { return &v1beta1.AdmissionResponse{ Result: &metav1.Status{ Message: err.Error(), }, } } // 所有validators和mutators都由admitFunc类型的函数负责实现 type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse // 核心逻辑,API Server使用HTTP协议调用Webhook Server func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { var body []byte if r.Body != nil { if data, err := ioutil.ReadAll(r.Body); err == nil { body = data } } // 请求体校验 contentType := r.Header.Get("Content-Type") if contentType != "application/json" { klog.Errorf("contentType=%s, expect application/json", contentType) return } klog.V(2).Info(fmt.Sprintf("handling request: %s", body)) // 请求Review requestedAdmissionReview := v1beta1.AdmissionReview{} // 响应Review responseAdmissionReview := v1beta1.AdmissionReview{} deserializer := codecs.UniversalDeserializer() // 反串行化响应体为requestedAdmissionReview if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil { klog.Error(err) responseAdmissionReview.Response = toAdmissionResponse(err) } else { // 如果没有错误,则执行准许逻辑 responseAdmissionReview.Response = admit(requestedAdmissionReview) } // 返回相同的UID responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response)) respBytes, err := json.Marshal(responseAdmissionReview) if err != nil { klog.Error(err) } if _, err := w.Write(respBytes); err != nil { klog.Error(err) } } /* 各种validators和mutators */ func serveAlwaysDeny(w http.ResponseWriter, r *http.Request) { serve(w, r, alwaysDeny) } func serveAddLabel(w http.ResponseWriter, r *http.Request) { serve(w, r, addLabel) } func servePods(w http.ResponseWriter, r *http.Request) { serve(w, r, admitPods) } func serveAttachingPods(w http.ResponseWriter, r *http.Request) { serve(w, r, denySpecificAttachment) } func serveMutatePods(w http.ResponseWriter, r *http.Request) { serve(w, r, mutatePods) } func serveConfigmaps(w http.ResponseWriter, r *http.Request) { serve(w, r, admitConfigMaps) } func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) { serve(w, r, mutateConfigmaps) } func serveCustomResource(w http.ResponseWriter, r *http.Request) { serve(w, r, admitCustomResource) } func serveMutateCustomResource(w http.ResponseWriter, r *http.Request) { serve(w, r, mutateCustomResource) } func serveCRD(w http.ResponseWriter, r *http.Request) { serve(w, r, admitCRD) } type Config struct { CertFile string KeyFile string } func (c *Config) addFlags() { // 包含用于HTTPS的x509服务器证书,如果包含CA证书,则连接在服务器证书后面 flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, "...") // 匹配上述服务器证书的私钥 flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, "...") } func configTLS(config Config) *tls.Config { sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) if err != nil { klog.Fatal(err) } return &tls.Config{ Certificates: []tls.Certificate{sCert}, // 下面一行用于启用mTLS,也就是对客户端(API Server)进行身份验证 // ClientAuth: tls.RequireAndVerifyClientCert, } } func main() { var config Config config.addFlags() flag.Parse() // 不同的URL路径,对应不同的Webhook,处理函数也不同 http.HandleFunc("/always-deny", serveAlwaysDeny) http.HandleFunc("/add-label", serveAddLabel) http.HandleFunc("/pods", servePods) http.HandleFunc("/pods/attach", serveAttachingPods) http.HandleFunc("/mutating-pods", serveMutatePods) http.HandleFunc("/configmaps", serveConfigmaps) http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps) http.HandleFunc("/custom-resource", serveCustomResource) http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource) http.HandleFunc("/crd", serveCRD) server := &http.Server{ Addr: ":443", TLSConfig: configTLS(config), } server.ListenAndServeTLS("", "") } |
此样例中包含很多Webhook的样板代码。下面选取几个分析。
addlabel.go是一个mutator,它为对象添加{"added-label": "yes"}标签:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package main import ( "encoding/json" "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" ) const ( addFirstLabelPatch string = `[ { "op": "add", "path": "/metadata/labels", "value": {"added-label": "yes"}} ]` addAdditionalLabelPatch string = `[ { "op": "add", "path": "/metadata/labels/added-label", "value": "yes" } ]` ) func addLabel(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { klog.V(2).Info("calling add-label") obj := struct { metav1.ObjectMeta Data map[string]string }{} // 从Review中提取原始对象(被拦截的API Server请求中的对象) raw := ar.Request.Object.Raw err := json.Unmarshal(raw, &obj) if err != nil { klog.Error(err) return toAdmissionResponse(err) } // 响应 reviewResponse := v1beta1.AdmissionResponse{} // 准许通过 reviewResponse.Allowed = true if len(obj.ObjectMeta.Labels) == 0 { // Patch原始对象 reviewResponse.Patch = []byte(addFirstLabelPatch) } else { reviewResponse.Patch = []byte(addAdditionalLabelPatch) } pt := v1beta1.PatchTypeJSONPatch reviewResponse.PatchType = &pt return &reviewResponse } |
pods.go提供了三个Webhook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
package main import ( "fmt" "strings" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/api/admission/v1beta1" "k8s.io/klog" ) const ( podsInitContainerPatch string = `[ {"op":"add","path":"/spec/initContainers","value":[{"image":"webhook-added-image","name":"webhook-added-init-container","resources":{}}]} ]` ) // 校验Pod的规格 func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { klog.V(2).Info("admitting pods") // 校验Review中的GVK podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} if ar.Request.Resource != podResource { err := fmt.Errorf("expect resource to be %s", podResource) klog.Error(err) return toAdmissionResponse(err) } // 将原始对象转换为Pod raw := ar.Request.Object.Raw pod := corev1.Pod{} deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil { klog.Error(err) return toAdmissionResponse(err) } reviewResponse := v1beta1.AdmissionResponse{} // 默认准许通过 reviewResponse.Allowed = true var msg string // 演示各种不准许的情形 if v, ok := pod.Labels["webhook-e2e-test"]; ok { if v == "webhook-disallow" { reviewResponse.Allowed = false msg = msg + "the pod contains unwanted label; " } } for _, container := range pod.Spec.Containers { if strings.Contains(container.Name, "webhook-disallow") { reviewResponse.Allowed = false msg = msg + "the pod contains unwanted container name; " } } if !reviewResponse.Allowed { reviewResponse.Result = &metav1.Status{Message: strings.TrimSpace(msg)} } return &reviewResponse } // 修改Pod,Patch一个Init容器进去 func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { klog.V(2).Info("mutating pods") podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} if ar.Request.Resource != podResource { klog.Errorf("expect resource to be %s", podResource) return nil } raw := ar.Request.Object.Raw pod := corev1.Pod{} deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil { klog.Error(err) return toAdmissionResponse(err) } reviewResponse := v1beta1.AdmissionResponse{} reviewResponse.Allowed = true if pod.Name == "webhook-to-be-mutated" { reviewResponse.Patch = []byte(podsInitContainerPatch) pt := v1beta1.PatchTypeJSONPatch reviewResponse.PatchType = &pt } return &reviewResponse } // 禁止kubectl attach to-be-attached-pod -i -c=container1请求 func denySpecificAttachment(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { klog.V(2).Info("handling attaching pods") if ar.Request.Name != "to-be-attached-pod" { return &v1beta1.AdmissionResponse{Allowed: true} } // 期望接收到的是Pod资源,attach子资源 podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} if e, a := podResource, ar.Request.Resource; e != a { err := fmt.Errorf("expect resource to be %s, got %s", e, a) klog.Error(err) return toAdmissionResponse(err) } if e, a := "attach", ar.Request.SubResource; e != a { err := fmt.Errorf("expect subresource to be %s, got %s", e, a) klog.Error(err) return toAdmissionResponse(err) } raw := ar.Request.Object.Raw podAttachOptions := corev1.PodAttachOptions{} deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &podAttachOptions); err != nil { klog.Error(err) return toAdmissionResponse(err) } klog.V(2).Info(fmt.Sprintf("podAttachOptions=%#v\n", podAttachOptions)) // 如果不使用Stdin,或者容器不是container1则允许,否则不允许 if !podAttachOptions.Stdin || podAttachOptions.Container != "container1" { return &v1beta1.AdmissionResponse{Allowed: true} } return &v1beta1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Message: "attaching to pod 'to-be-attached-pod' is not allowed", }, } } |
可以直接作为Deployment部署在K8S集群内部,也可以部署在外面,需要对应的配置来配合。
Webhook的配置通过ValidatingWebhookConfiguration、MutatingWebhookConfiguration这两类K8S资源进行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: sidecar-injectors webhooks: # Webhoooks列表 - name: init-inject # 匹配任意一个规则,则API Server会发送AdmissionReview # 给clientConfig所指定的Webhook服务器 rules: - apiGroups: - "" apiVersions: - v1 operations: - CREATE resources: - pods scope: "Namespaced" clientConfig: # Webhook服务 service: namespace: kube-system name: sidecar-injector # 签名了服务器端证书的CA,PEM格式 # 有时候会设置为 caBundle: Cg== (\n),这是一个占位符,防止该CR被API Server拒绝。证书后续由程序自动更新 caBundle: pem encoded ca cert that signs the server cert used by the webhook admissionReviewVersions: - v1beta1 timeoutSeconds: 1 |
如果Webhook需要对API Server进行身份验证,则需要进行以下步骤:
- 为API Server提供 --admission-control-config-file参数,指定准许控制配置文件
- 在准许控制配置文件中,声明MutatingAdmissionWebhook、ValidatingAdmissionWebhook从何处读取凭证信息:
12345678910111213apiVersion: apiserver.k8s.io/v1alpha1kind: AdmissionConfigurationplugins:- name: ValidatingAdmissionWebhookconfiguration:apiVersion: apiserver.config.k8s.io/v1alpha1kind: WebhookAdmissionkubeConfigFile: /path/to/kubeconfig- name: MutatingAdmissionWebhookconfiguration:apiVersion: apiserver.config.k8s.io/v1alpha1kind: WebhookAdmissionkubeConfigFile: /path/to/kubeconfig - 凭证信息必须存放在KubeConfig文件中,示例:
123456789101112131415161718192021apiVersion: v1kind: Configusers:# 基于数字证书的身份验证# Webhook服务器的DNS名称- name: 'webhook1.ns1.svc'user:client-certificate-data: pem encoded certificateclient-key-data: pem encoded key# HTTP基本验证# 可以使用通配符- name: '*.webhook-company.org'user:password: passwordusername: name# 基于令牌的身份验证# 默认凭证- name: '*'user:token: <token>
总是允许所有请求通过。
总是拒绝所有请求通过,仅测试用。
修改所有新的Pod,将它们的镜像拉取策略改为Always。在多团队共享集群上有用,可以确保仅具有合法凭证的用户才能拉取镜像。
如果没有该AC,则一旦镜像被拉取到Node上,任何用户、Pod都可以不提供凭证的使用之。
监控PersistentVolumeClaim对象的创建过程,如果PVC没有要求任何存储类,则为其自动添加。
设置当Taint和Toleration不匹配时,默认能够容忍的最长时间。
禁止对以特权级别(privileged Pod、能访问宿主机IPC名字空间的Pod、能访问宿主机PID名字空间的Pod)运行的Pod执行exec/attach命令。
Alpha/1.9,可以缓和APIServer被事件请求泛洪的问题。
1.9引入的插件,辅助带有扩展资源(GPU/FPGA...)的节点的创建,这类节点应该以扩展资源名为键配置Taints。
该控制器自动为请求扩展资源的Pod添加Tolerations,用户不需要手工添加。
允许后端的webhook给出准许决策。
Alpha/1.7,根据现有的InitializerConfiguration来确定资源的初始化器。
监控Pod创建请求,如果容器没有提供资源用量请求/限制,该AC会根据基于相同镜像的容器的历史运行状况,自动为容器添加资源用量请求/限制。
拒绝任何这样的Pod,它的AntiAffinity(requiredDuringSchedulingRequiredDuringExecution)拓扑键是kubernetes.io/hostname之外的值。
监控入站请求,确保Namespace中LimitRange对象列出的任何约束条件不被违反。如果在部署中使用LimitRange,你必须启用该AC。
检查针对所有名字空间化的资源的请求,如果所引用的名字空间不存在,则自动创建名字空间。
检查针对所有名字空间化的资源的请求,如果所引用的名字空间不存在,则返回错误。
具有以下功能:
- 确保正在删除的名字空间中,不会由新的对象被创建
- 确保针对不存在名字空间的资源创建请求被拒绝
- 禁止删除三个系统保留名字空间:default, kube-system, kube-public
Namespace删除操作会级联删除其内部的Pod、Service等对象,为了确保删除过程的完整性,应当使用此AC。
限制Kubelet能够修改的Pod、Node对象。
自动将云服务商定义的Region/Zone标签附加到PV。可以用于保证PV和Pod位于相同的Region/Zone。
读取Namespace上的注解和全局配置,限制某个命名空间中的Pod能够运行在什么节点(通过nodeSelector)。
要启用此控制器,需要修改APIServer的配置/etc/kubernetes/manifests/kube-apiserver.yaml,在--enable-admission-plugins=后添加PodNodeSelector。
使用下面的注解,可以为命名空间中任何Pod添加默认nodeSelector:
1 2 3 4 5 6 |
apiVersion: v1 kind: Namespace metadata name: devops annotations: scheduler.alpha.kubernetes.io/node-selector: k8s.gmem.cc/dedicated-to=devops |
这样,此命名空间中创建的新Pod,都会具有nodeSelector:
1 2 |
nodeSelector k8s.gmem.cc/dedicated-to:devops |
此控制器首先校验Pod的容忍和命名空间的容忍,如果存在冲突则拒绝Pod创建。然后,它将命名空间的容忍合并到Pod的容忍中,合并后的结果进行命名空间容忍白名单检查,如果检查不通过则Pod被拒绝创建。
命名空间的容忍,使用注解scheduler.alpha.kubernetes.io/defaultTolerations配置,示例:
1 2 3 4 5 |
apiVersion: v1 kind: Namespace metadata: annotations: scheduler.alpha.kubernetes.io/defaultTolerations: '[{"key":"k8s.gmem.cc/dedicated-to","operator":"Equal","value":"devops","effect":"NoSchedule"}]' |
命名空间的容忍白名单,使用注解scheduler.alpha.kubernetes.io/tolerationsWhitelist配置。 此注解的值类似上面的注解。
打开特性开关ExpandPersistentVolumes后,你应该启用该AC。
此AC默认即用所有PVC的Resizing请求,除非PVC的存储类明确的启用(allowVolumeExpansion=true)
注入匹配的PodPreset中的规格字段。
使用priorityClassName字段并为Pod产生一个整数类型的优先级。如果目标priorityClass不存在则Pod被拒绝。
监控入站请求,确保没有违反Namespace中ResourceQuota对象所列出的资源配额限制。
如果你使用ResourceQuota对象,则必须启用此AC。
实现ServiceAccount的自动化。当使用ServiceAccount对象是应当启用此AC。
禁止那些尝试通过SecurityContext中某些字段实现权限提升的Pod。如果没有启用PodSecurityPolicy,应当考虑启用该控制器。
K8S中的Pod是有生命周期的,当Pod死掉后它无法复活。某些控制器会动态的创建、删除Pod。尽管Pod会获得一个IP地址,但是随着时间的推进,IP也不能作为联系它的可靠手段,因为IP会改变。
那么,如果某个Pod(后端)集向其它Pod(前端)集提供功能,那么前端如何找到、跟踪后端集中的Pod呢?Service能帮助你实现这一需求。
所谓Service,是一个抽象,它定义了:
- 一个逻辑的Pod集合。常常通过标签选择器来锚定
- 访问这些Pod的策略
Service有时候也被称为微服务(micro-service)。
举个例子,假设有一个图像处理后端,包括三个实例。这些实例是无状态的,因而前端不关心它调用的是哪一个。组成后端的Pod实例可能发生变化(宕机、扩容),如果前端跟踪其调用的Pod显然会造成耦合。Service这个抽象层可以实现解耦。
对于K8S-Native应用程序,K8S提供了Endpoints API。每当Service包含的Pod集发生变化后,即更新端点。对于非K8S-Native的应用程序,K8S为Service提供了一个基于虚拟IP的桥,用于将请求重定向到后端的Pod。
假设你有一组Pod,其标签为app=MyApp,并且都暴露了9376端口。你可以为它们定义如下服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
kind: Service apiVersion: v1 metadata: name: my-service spec: # 被服务管理的Pod selector: app: MyApp ports: - protocol: TCP # 服务的集群IP上的端口 port: 80 # Pod的端口,可以指定为Pod中定义的端口名 targetPort: 9376 |
Service本身也被分配一个IP(称为集群IP),此IP会被服务代理使用。
Service的selector会被不断的估算,其匹配结果会被发送给与Service同名的Endpoints对象。
Service能够进行端口映射。针对服务的请求可以被映射到Pod的端口。支持的网络协议包括TCP、UDP两种。
尽管Service通常用来抽象对Pod的访问,它也可以用来抽象其它类型的后端,例如:
- 在生产环境下你希望使用外部数据库集群,但是在开发/测试时你希望使用自己的数据库
- 你希望把服务指向其它名字空间中的服务,甚至其它集群中的服务
- 你在把一部分工作负载迁移到K8S,而让另一部分在外部运行
以上情况下,你都可以定义一个没有选择器的Service。由于没有选择器,Endpoints对象不会被创建。你可能需要手工映射服务到端点:
1 2 3 4 5 6 7 8 9 10 |
kind: Endpoints apiVersion: v1 metadata: name: my-service # 端点列表,这个例子中流量被路由给1.2.3.4:9376 subsets: - addresses: - ip: 1.2.3.4 ports: - port: 9376 |
这是一种特殊的Service,它即不选择端点,也不定义端口。它只用于提供集群外部的服务的别名,集群内的组件可以基于别名访问外部服务:
1 2 3 4 5 6 7 8 9 |
kind: Service apiVersion: v1 metadata: name: database namespace: prod spec: type: ExternalName # IP地址也支持 externalName: db.gmem.cc |
当集群内组件查找database.prod.svc.${CLUSTER_DOMAIN}时,集群的DNS服务会返回一个CNAME记录,其值为db.gmem.cc。
注意:经过试验,externalName填写IP地址也是可以的,这种情况下,集群DNS服务应该是做了一个A记录。
K8S集群中的每个节点都运行着kube-proxy组件。该组件负责为服务(除了ExternalName)提供虚拟IP。
在1.0版本,Service属于第四层(TCP/UDP over IP)构造,kube-proxy完全工作在用户空间。从1.1开始引入了Ingress API,用于实现第七层(HTTP)Service。基于iptables的代理也被添加,并从1.2开始成为默认操作模式。从1.9开始,ipvs代理被添加。
此模式下,kube-proxy监控Master,关注Service、Endpoints对象的添加和删除。
对于每个Service,KP会在本地节点随机打开一个代理端口,任何发向此端口的的请求会被转发给服务的某个后端Pod(从Endpoints对象中获取)。到底使用哪个Pod,取决于Service的SessionAffinity设置,默认选取后端的算法是round robin。KP会把转发规则写入到iptables中,捕获Service的集群IP(虚拟的)的流量。
这种模式下,需要在内核空间和用户空间传递流量。
此模式下,KP会直接安装iptables规则,捕获Service的集群IP + Service 端口的流量,转发给某个Service后端Pod,选择Pod的算法是随机。
这种模式下,不需要在内核空间和用户空间之间切换,通常比userspace模式更快、更可靠。但是,该模式不支持当第一次选择的Pod不响应后重新选择另外一个Pod,因此需要配合Readness探针使用。
关于IPVS的知识参考:IPVS和Keepalived
在1.9中处于Beta状态。
此模式下,KP会调用netlink网络接口,创建和K8S的Service/Endpoint对应的ipvs规则,并且周期性的将K/E和ipvs规则同步。当访问Service时,流量被转发给某个Pod。
类似于iptables,ipvs基于netfilter钩子函数,但是它使用哈希表作为底层数据结构,且运行在内核态。这意味着ipvs的流量转发速度会快的多,且同步代理规则时性能更好。
在未来的版本中,ipvs会提供更多的负载均衡算法,包括:
- rr:循环选择
- lc:最少连接(least connection)
- dh:目的地哈希(destination hashing)
- sh:源哈希(destination hashing)
- sed:最短预期延迟(shortest expected delay)
- nq:绝不排队(never queue)
注意,该模式要求节点上安装了IPVS内核模块。如果该模块没有安装,会自动fallback为iptables模式。
基于IPVS的K8S Service,都是使用NAT模式,原因是,只有NAT模式才能进行端口映射,让Sevice端口和Pod端口不一致。
IPVS模式下,客户端角色是当前节点(中的Pod,或者节点本身),Direct角色是当前节点,Real Server是当前或其它节点上的Pod。不同客户端,对应的网络路径不一样。IPVS模式仍然需要iptables规则进行一些配合。
考虑Service 10.96.54.11:8080,Pod 10.244.1.2:8080,基于Flannel构建容器网络,当前flannel.1的IP地址为10.244.0.0:
- 当前节点出现一个虚拟网络接口 kube-ipvs0,所有Service IP都绑定在上面,这是IPVS所要求的,Direct必须具有VIP。这也导致了IPVS模式下,VIP可Ping(iptables模式下不可以)
- OUTPUT链被添加如下规则:
123456789-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES# 如果源IP不是Pod IP,且目的IP是K8S Service(所有Service IP放在IPSet中,大大减少iptables规则条目数量)-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment--comment "Kubernetes service cluster ip + port for masquerade purpose"# 则打上标记-m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000不是Pod发出的对Service IP的请求,打标记0x4000/0x4000的目的是,后续进行SNAT。因为节点自身(而非其中的Pod)对VIP进行访问时,因为VIP就在本机网卡,所以它自动设置IP封包的源地址、目的地址均为10.96.54.11,这样的封包不做SNAT,仅仅DNAT到Pod上,回程报文是发不回来的(因为每个节点都具有所有VIP)
- POSTROUTING链被添加如下规则:
1234-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT"-m mark --mark 0x4000/0x4000 -j MASQUERADE如第2条所属,需要作SNAT, MASQUERADE意味着源地址取决于封包出口网卡
为了更深入的理解,可以看看地址映射的过程,在节点上访问curl http://10.96.54.11:8080时:
- 产生如下封包: 10.96.54.11:xxxxx -> 10.96.54.11:8080。原因是目标地址在kube-ipvs0这个本机网卡上
- 经过IPVS模块,选择一个RIP,进行DNAT:10.96.54.11:xxxxx -> 10.244.2.2:8080
- 经过OUTPUT、POSTROUTING链,进行SNAT:10.244.0.0:xxxxx -> 10.244.2.2:8080 。10.244.0.0是出口网卡flannel.1的地址,封包发给此网卡后,经过内核包装为vxlan UDP包,再由物理网卡发出去
Pod具有普通的IP地址,因为针对此IP的请求被路由到固定的位置。
Service的IP则是虚拟的 —— 它不会由某台固定的主机来应答。K8S使用Linux的iptables来定义透明、按需进行重定向的VIP。当客户端连接到VIP时,流量会透明的传输给适当的端点。Service相关的环境变量、DNS条目使用的都是Service的VIP和端口。
不管使用哪种代理模式,均保证:
- 任何从Service的集群IP:Port进入的流量,都被转发给适当的后端Pod。客户端不会感知到K8S、Service、Pod这些内部组件的存在
- 要实现基于客户端IP的会话绑定(session affinity ),可以设置service.spec.sessionAffinity = ClientIP (默认None)。你可以进一步设置service.spec.sessionAffinityConfig.clientIP.timeoutSeconds来指定会话绑定的最长时间(默认10800)
由于Iptables底层数据结构是链表,而IPVS则是哈希,IPVS比起iptables模式显著提高的性能,几乎不受服务规模增大的影响。现在的集群都使用IPVS模式的服务代理。
IPVS支持三种工作模式:DR(直接路由)、Tunneling(ipip)、NAT(Masq)。其中只有NAT模式支持端口映射(ClusterIP端口和Pod端口不一样),因此K8S使用NAT模式。此外,K8S还在NAT的DNAT(将对服务的请求的目的地址改为Pod IP)的基础上,进行SNAT(将对服务的请求的源地址改为入站网络接口的地址,否则Pod直接发送源地址为Pod IP的回程报文,客户端不接受),尽管fullNAT这种IPVS扩展可以支持DNAT+SNAT。
IPVS是在INPUT链上挂了钩子,运行复杂的负载均衡算法,然后执行DNAT后从FORWARD链离开本机网络栈。
让网络包进入INPUT链有两种方式:
- 将虚IP写到内核路由表中
- 创建一个Dummy网卡,将续IP绑定到此网卡
Kube Proxy使用的是第二种方式。一旦Service对象创建,Kube proxy就会:
- 确保Dummy网卡kube-ipvs0存在
- 将服务的虚IP绑定给kube-ipvs0
- 通过socket创建IPVS的virtual server。virtual server和service是N:1关系,原因是Service可能有多个虚拟IP,例如LoadBalancer IP + Cluster IP
当Endpoint对象创建后,Kube Proxy会:
- 创建IPVS的real server。real server和endpoint是1:1关系
即使使用IPVS模式,只能解决流量转发的问题。Kube Proxy的其它问题仍然依靠 Iptables 解决,它在以下情况下依赖Iptables:
- 如果配置启动参数 --masquerade--all=true,也就是所有经过kube-proxy的包都进行一次SNAT
- 启动参数指定了集群IP地址范围
- 对于LoadBalancer类型的服务,需要Iptables配置白名单
- 对于NodePort类型的服务,用于在封包从入站节点发往其它节点的Pod时进行MASQUERADE
Kube Proxy使用ipset来减少需要创建的iptables规则,IPVS模式下iptables规则的数量不超过5个
很多服务需要暴露多个端口,K8S支持在Service的规格中指定多个端口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
kind: Service apiVersion: v1 metadata: name: my-service spec: selector: app: MyApp ports: # 端口1 - name: http protocol: TCP port: 80 targetPort: 9376 # 端口2 - name: https protocol: TCP port: 443 targetPort: 9377 |
在创建Service时,你可以指定你想要的集群IP,设置spec.clusterIP字段即可。
你选择的IP地址必须在service-cluster-ip-range(APIServer参数)范围内,如果IP地址不合法,APIServer会返回响应码422。
K8S提供两种途径,来发现一个服务:环境变量、DNS。
当Pod运行时,Kubelet会以环境变量的形式注入每个活动的服务的信息。环境变量名称格式为{SVCNAME}_SERVICE_HOST、{SVCNAME}_SERVICE_PORT。其中SVCNAME是转换为大写、-转换为_的服务名。
例如服务redis-master,暴露TCP端口6379,获得集群IP 10.0.0.11。它会产生以下环境变量:
1 2 3 4 5 6 7 |
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之前就创建好,否则Pod的环境变量无法更新。
K8S提供了DNS Addon。此DNS服务器会监控新创建的Service,并为其提供DNS条目。集群中所有Pod都可以使用该DNS服务。
位于名字空间my-ns中的服务my-svc,创建的DNS条目是my-svc.my-ns。位于名字空间my-ns中的Pod可以直接通过my-svc访问服务,其它名字空间中的Pod则需要使用my-svc.my-ns访问服务。DNS名称解析的结果是服务的Cluster IP。
要访问ExternalName类型的Service,DNS是唯一的途径。
某些情况下,你不需要Service提供的负载均衡功能,也不需要单个Service IP。这种情况下你可以创建Headless Service,其实就是设置spec.clusterIP=None。
使用HS,开发者可以减少和K8S的耦合,自行实现服务发现机制。
HS没有Cluster IP,kube-proxy也不会处理这类服务,K8S不会为这类服务提供负载均衡机制。
DNS记录如何创建,取决于HS有没有配置选择器。
如果定义了选择器,端点控制器为HS创建Endpoints对象。并且修改DNS配置,返回A记录(地址),直接指向HS选择的Pod。
端点控制器不会为这类Service创建Endpoints对象,但是DNS系统会配置:
- 为ExternalName类型的Service配置CNAME记录
- 对于其它类型的Service,为和Service共享名称的任何Endpoints创建一条DNS记录
对于无选择器的HS,不会自动创建关联的Endpoints,因此我们有机会手工的关联Endpoints,从而实现:
- 指向一个集群外部的数据库
- 指向其它namespace或集群的服务
示例:
1 2 3 4 5 6 7 8 9 |
kind: Endpoints apiVersion: v1 metadata: name: my-service subsets: - addresses: - ip: 1.2.3.4 ports: - port: 1234 |
Endpoint的地址不能是Loopback、link-local、link-local多播地址。
对于应用程序中的某些部分(前端),你通常需要在外部(集群外部)IP上暴露一个服务。
K8S允许你指定为服务指定类型(ServiceTypes):
类型 | 说明 | ||
ClusterIP |
在集群内部的IP地址上暴露服务,如果你仅希望在集群内部能访问到服务,则使用该类型 这是默认的类型 |
||
NodePort |
在所有节点的IP上的一个静态端口上暴露服务。K8S会自动创建一个ClusterIP类型的服务,用于把NodePort类型的服务路由到适当的Pod 外部可以将任何Node作为入口,访问此类型的服务 K8S会自动分配一个范围内的端口(默认30000-32767),每个节点都会转发到此端口的请求。此端口表现在spec.ports[*].nodePort字段中 如果你需要手工指定端口,设置nodePort的值即可。手工指定的值必须在允许的端口范围之内,你还需要注意可能存在的端口冲突。建议自动分配端口 服务可以通过以下两种方式访问到:
NodePort是丐版的LoadBalancer,可供外部访问且成本低廉。但是在大规模集群上 |
||
LoadBalancer |
向外部暴露服务,并使用云服务提供的负载均衡器。外部负载均衡器所需要的NodePort,以及ClusterIP会自动创建 服务的EXTERNAL-IP字段为云提供商的负载均衡器的IP 某些云服务(GCE、AWS等)提供负载均衡器,K8S支持和这种外部LB集成:
|
||
ExternalName | 将外部服务映射为别名 |
注:修改服务的类型,不会导致ClusterIP重新分配。
如果你拥有一个或多个外部(例如公网)IP路由到集群的某些节点,则Service可以在这些外部IP上暴露。基于这些IP流入的流量,如果其端口就是Service的端口,会被K8S路由到服务的某个端点(Pod ...)上。
例如下面这个服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 |
外部客户端可以通过80.11.12.10:80访问服务,流量会被转发给app=MyApp的Pod的9376端口。
当访问NodePort、LoadBalancer类型的Service时,流量到达节点后,可能再被转发到其它节点,从而增加了一跳。
要避免这种情况,可以设置:
1 2 3 4 |
kind: service spec: # 默认 Cluster externalTrafficPolicy: Local |
这样,K8S会强制将流量发往具有Service后端Pod的端点。注意ClusterIP类型的服务不支持这种局部性访问配置。
服务的端点,通常情况下对应到Pod的一个端口。带有选择器的Service通常自动管理Endpoint对象,你也可以定义不使用选择器的服务,这种情况下需要手工管理端点,Endpoints对象的名字和Service的名字要一致,位于相同命名空间。
所有网络端点都保存在同一个 Endpoints 资源中,这类资源可能变得非常巨大,进而影响控制平面组件性能,EndpointSlice可以缓解这一(大量端点)问题。此外涉及如何路由内部流量时,EndpointSlice 可以充当 kube-proxy 的决策依据。
默认情况下,单个端点切片最多包含100个端点,可以通过控制器管理器的标记 --max-endpoints-per-slice来定制。
下面是端点切片对象的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
apiVersion: discovery.k8s.io/v1beta1 kind: EndpointSlice metadata: name: example-abc labels: kubernetes.io/service-name: example # 端点的地址类型:支持IPv4 IPv6 FQDN addressType: IPv4 # 端口列表,适用于切片中所有端点 ports: - name: http protocol: TCP port: 80 endpoints: - addresses: - "10.1.2.3" # EndpointSlice存储了可能对使用者有用的、有关端点的状况。 这三个状况分别是 # ready 对应Pod的Ready状态,运行中的Pod的Ready为True # serving 如果Pod处于终止过程中则Ready不为True,此时应该查看serving获知Pod是否就绪 # 例外是spec.publishNotReadyAddresses的服务,这种服务的端点的Ready永远为True # terminating conditions: ready: true hostname: pod-1 # 拓扑感知,1.20废弃 topology: kubernetes.io/hostname: node-1 # 未来使用nodeName字段代替 topology.kubernetes.io/zone: us-west2-a |
通常情况下,由端点切片控制器自动创建、管理 EndpointSlice 对象。EndpointSlice 对象还有一些其他使用场景, 例如作为服务网格的实现。这些场景都会导致有其他实体 或者控制器负责管理额外的 EndpointSlice 集合。
为了确保多个实体可以管理 EndpointSlice 而且不会相互产生干扰,Kubernetes 定义了 标签 endpointslice.kubernetes.io/managed-by,用来标明哪个实体在管理某个 EndpointSlice。端点切片控制器会在自己所管理的所有 EndpointSlice 上将该标签值设置 为 endpointslice-controller.k8s.io。 管理 EndpointSlice 的其他实体也应该为此标签设置一个唯一值。
在大多数场合下,EndpointSlice 都由某个 Service 所有,这一属主关系是通过:
- 为每个 EndpointSlice 设置一个 owner引用
- 设置 kubernetes.io/service-name 标签来标明的,目的是方便查找隶属于某服务的所有 EndpointSlice
每个 EndpointSlice 都有一组端口值,适用于资源内的所有端点。 当为服务使用命名端口时,Pod 可能会就同一命名端口获得不同的端口号,因而需要 不同的 EndpointSlice。
控制面尝试尽量将 EndpointSlice 填满,不过不会主动地在若干 EndpointSlice 之间 执行再平衡操作。这里的逻辑也是相对直接的:
- 列举所有现有的 EndpointSlices,移除那些不再需要的端点并更新那些已经变化的端点
- 列举所有在第一步中被更改过的 EndpointSlices,用新增加的端点将其填满
- 如果还有新的端点未被添加进去,尝试将这些端点添加到之前未更改的切片中, 或者创建新切片
这里比较重要的是,与在 EndpointSlice 之间完成最佳的分布相比,第3步中更看重限制 EndpointSlice 更新的操作次数。例如,如果有 10 个端点待添加,有两个 EndpointSlice 中各有 5 个空位,上述方法会创建一个新的 EndpointSlice 而不是 将现有的两个 EndpointSlice 都填满。换言之,与执行多个 EndpointSlice 更新操作 相比较,方法会优先考虑执行一个 EndpointSlice 创建操作。
由于 kube-proxy 在每个节点上运行并监视 EndpointSlice 状态,EndpointSlice 的 每次变更都变得相对代价较高,因为这些状态变化要传递到集群中每个节点上。 这一方法尝试限制要发送到所有节点上的变更消息个数,即使这样做可能会导致有 多个 EndpointSlice 没有被填满。
在实践中,上面这种并非最理想的分布是很少出现的。大多数被 EndpointSlice 控制器 处理的变更都是足够小的,可以添加到某已有 EndpointSlice 中去的。并且,假使无法添加到已有的切片中,不管怎样都会快就会需要一个新的 EndpointSlice 对象。 Deployment 的滚动更新为 EndpointSlice重新打包提供了一个自然的机会,所有 Pod 及其对应的端点在这一期间都会被替换掉。
由于 EndpointSlice 变化的自身特点,端点可能会同时出现在不止一个 EndpointSlice 中。鉴于不同的 EndpointSlice 对象在不同时刻到达 Kubernetes 的监视/缓存中, 这种情况的出现是很自然的。 使用 EndpointSlice 的实现必须能够处理端点出现在多个切片中的状况。 关于如何执行端点去重(deduplication)的参考实现,你可以在 kube-proxy 的 EndpointSlice 实现中找到。
K8S支持在集群中调度DNS Service/Pod,并且让Kubelet告知每个容器,DNS服务器的IP地址。
集群中的每个服务,包括DNS服务本身,都被分配一个DNS名称。
默认的,Pod的DNS搜索列表会包含Pod自己的名字空间,以及集群的默认Domain。如果一个服务foo运行在名字空间bar中,则位于bar中的Pod可以通过foo引用服务,位于其它名字空间中的Pod则需要通过foo.bar引用服务。
普通服务被分配以A记录,格式:my-svc.my-namespace.svc.cluster.local,此记录解析到服务的Cluster IP。
Headless服务(无Cluster IP)也被分配如上的A记录,但是记录解析到服务选择的Pod的IP集(如果没有选择器则解析到什么IP取决于你给出的Endpoints配置),客户端应当使用此IP集或者使用Round-Robin方式从IP集中选取IP。
这类记录为服务的知名端口分配,SRV记录格式:_my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local。
对于普通服务,SRV记录解析为端口号 + CNAME(my-svc.my-namespace.svc.cluster.local)。
对于Headless服务,SVR记录解析到多个答复,每个对应服务选择的Pod:端口号 + Pod的CNAME(auto-generated-name.my-svc.my-namespace.svc.cluster.local)。
当启用后,Pod被分配一个DNS A记录:pod-ip-address.my-namespace.pod.cluster.local。例如:
1 2 |
# 一个名为influx的Pod,它位于dev名字空间中,IP地址为172.27.0.20 172-27-0-20.dev.pod.k8s.gmem.cc |
Pod被创建后,其hostname为metadata.name字段的值。
在Pod的Spec中你可以:
- 指定可选的hostname字段,可以手工指定hostname,比metadata.name的优先级高
- 指定可选可选的subdomain字段,说明指定Pod的子域。例如一个Pod的hostname为influxdb,subdomain为pods,名字空间为default,则Pod的全限定名为influxdb.default.dev.svc.k8s.gmem.cc
如果在Pod所在名字空间中,存在一个Headless服务,其名称与Pod的subdomain字段一致。则KubeDNS服务器会为Pod的全限定名返回一个A记录,指向Pod的IP地址。
你可以为每个Pod定义DNS策略(在规格中使用dnsPolicy字段)。目前K8S支持以下策略:
策略 | 说明 |
Default | 从Pod所在节点继承DNS解析设置 |
ClusterFirst | 任何不匹配集群DNS后缀(可以设置)的查询,转发给上游(从节点继承)DNS处理 |
ClusterFirstWithHostNet | 运行在hostNetwork中的Pod应当明确的设置为该值 |
None | 1.9 Alpha,忽略来自K8S环境的DNS设置。所有DNS设置从Pod规格的dnsConfig字段获取 |
1.9 Alpha。用户可以对Pod的DNS设置进行细粒度控制。要启用该特性,集群管理员需要为APIServer、kubelet启用特性开关(Feature Gate)CustomPodDNS,示例:--feature-gates=CustomPodDNS=true,... 从1.14开始默认开启。
当启用上述特性后,用户可以设置Pod规格的dnsPolicy=None,并添加dnsConfig字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
apiVersion: v1 kind: Pod metadata: namespace: ns1 name: dns-example spec: containers: - name: test image: nginx dnsPolicy: "None" dnsConfig: # 此Pod使用的DNS服务器列表 nameservers: - 1.2.3.4 # DNS搜索后缀列表 searches: - ns1.svc.cluster.local - my.dns.search.suffix options: # 最大dot数量。最大值15 - name: ndots value: "1" # 重试另外一个DNS服务器之前,等待应答的最大时间,单位秒。最大值30 - name: timeout value: "5" # 放弃并返回错误给调用者之前,最大尝进行DNS查询的次数。最大值5 - name: attempts value: "2" # 使用round-robin风格选择不同的DNS服务器,向其发送查询 - name: rotate # 支持RFC 2671描述的DNS扩展 - name: edns0 # 串行发送A/AAAA请求 - name: single-request # 发送第二个请求时重新打开套接字 - name: single-request-reopen # 使用TCP协议发送DNS请求 - name: use-vc # 禁止自动重新载入修改后的配置文件,需要glibc 2.26+ - name: no-reload |
本节以Nginx服务为例,说明如何在K8S集群中连接应用程序和服务。
在讨论K8S的通信机制之前,我们先看一下普通的Docker网络如何运作。
默认情况下,Docker使用Host-private的网络,也就是说Docker容器仅能和同一台宿主机上的其它容器通信。为了让容器跨节点通信,它们必须在宿主机上暴露端口。这意味着,开发者需要仔细规划,让多个容器协调暴露宿主机的不同端口。
跨越多名开发者进行端口协调很麻烦,特别是进行扩容的时候,这种协调工作应该由集群负责,而不应该暴露给用户。
K8S假设所有Pod都是需要相互通信的,不管它们是不是位于同一台宿主机上。K8S为每个Pod分配集群私有IP地址,你不需要向Docker那样,为容器之间创建连接,或者暴露端口到宿主机的网络接口。
Pod内部的多个容器,可以利用localhost进行相互通信。
集群内所有Pod中的容器,都可以相互看到,不需要NAT的介入。
考虑下面的Pod规格:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
apiVersion: apps/v1beta1 kind: Deployment metadata: name: my-nginx spec: replicas: 2 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx ports: - containerPort: 80 |
你可以从集群的任何节点访问此Pod,执行下面的命令获得Pod的IP地址:
1 2 3 4 |
kubectl get pods -l run=my-nginx -o wide # NAME READY STATUS RESTARTS AGE IP NODE # my-nginx-3800858182-jr4a2 1/1 Running 0 13s 10.244.3.4 kubernetes-minion-905m # my-nginx-3800858182-kna2y 1/1 Running 0 13s 10.244.2.5 kubernetes-minion-ljyd |
登陆到任何集群节点后,你都可以访问上面的两个IP地址。需要注意,Pod不会使用节点的80端口,也不会使用任何NAT机制来把流量路由到Pod。这意味着你可以在单个节点上运行containerPort相同的多个Pod。
你可以像Docker那样,把容器端口映射到节点端口,单由于K8S网络模型的存在,这种映射往往没有必要。
现在,我们有了两个Pod,它们运行着Nginx,IP位于一个扁平的、集群范围的地址空间中。 理论上说,应用程序可以和Pod直接通信,但是如何Pod所在节点宕机了会怎么样?Deployment会自动在另外一个节点上调度一个Pod,这个新的Pod的IP地址会改变。如果直接和Pod 通信,你就要处理这种IP地址可能改变的情形。
服务可以避免上述处理逻辑,它是Pod之上的抽象层,它定义了一个逻辑的Pod集,这些Pod提供一模一样的功能。当创建服务时,它被分配以唯一性的IP地址(Cluster IP),在服务的整个生命周期中,该IP都不会变化。
应用程序可以配置和服务(而非Pod)通信,服务会进行负载均衡处理,把请求转发给某个Pod处理。
针对上述Deployment的Service规格:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: ports: # 服务暴露的端口 - port: 80 # 对接到Pod的端口 targetPort: 80 protocol: TCP selector: run: my-nginx |
创建服务后,可以查询其状态:
1 2 3 |
kubectl get svc my-nginx # NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE # my-nginx 10.0.162.149 80/TCP 21s |
服务的背后是一个Pod集,服务的选择器会不断的估算,来选择匹配的Pods。匹配的Pod会被发送到一个Endpoints对象中,该对象的名字也叫my-nginx(和服务同名)。当Pod死去时它会自动从Endpoints中移除,类似的当新Pod出生后也被加入到Endpoints中。
下面的命令可以获得端点的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# 获得服务关联的端点 kubectl describe svc my-nginx # Name: my-nginx # Namespace: default # Labels: run=my-nginx # Annotations: # Selector: run=my-nginx # Type: ClusterIP # IP: 10.0.162.149 # Port: 80/TCP # Endpoints: 10.244.2.5:80,10.244.3.4:80 # Session Affinity: None # Events: # 获得端点信息 kubectl get ep my-nginx # NAME ENDPOINTS AGE # my-nginx 10.244.2.5:80,10.244.3.4:80 1m |
注意:端点的IP/Port就是Pod的IP/Port。
现在,你可以在集群中任何节点上 wget :,再次强调,集群IP是纯粹的虚拟IP,它不会对应到哪一根网线。
K8S提供了两种服务访问方式:环境变量、DNS。
变量变量方式是开箱即用的,当Pod启动时,Kubelet会为其注入一系列的环境变量。这意味着在Pod创建之后才创建的服务,无法注入为环境变量。
K8S有一个DNS Addon,负责提供集群内的域名服务,执行下面的命令了解此服务是否启用:
1 2 3 4 |
# 服务名为kube-dns,运行在kube-system名字空间 kubectl get services kube-dns --namespace=kube-system # NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE # kube-dns 10.0.0.10 53/UDP,53/TCP 8m |
如果没有运行DNS服务,你需要启用它。
DNS服务运行后,会为每个服务都分配一个域名,映射到它的集群IP。
现在,在集群内部的任何节点,都可以正常访问服务,而且可以享受K8S提供的负载均衡、HA了。
在把服务暴露到因特网之前,你需要确保信道的安全性。例如:
- 提供数字证书,要么购买要么自签名
- 配置Nginx的HTTPS
- 配置一个Secret,让Pod都能访问证书
创建密钥和自签名证书,可以参考:
1 |
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=nginxsvc/O=nginxsvc" |
K8S提供了一些examples,可以简化第三步。执行下面的命令下载样例:
1 |
git clone https://github.com/kubernetes/examples.git |
下面使用examples中的工具,根据上面生成的密钥,产生一个Secret的规格文件:
1 2 3 |
# make和go必须已经安装 # 实际通过调用 make_secret.go 产生Secret,后者则是调用K8S的API make keys secret KEY=/tmp/nginx.key CERT=/tmp/nginx.crt SECRET=/tmp/secret.json |
使用规格文件,在K8S上创建一个Secret对象:
1 2 |
kubectl create -f /tmp/secret.json # secret "nginxsecret" created |
查看Secret对象列表:
1 2 3 4 |
kubectl get secrets # NAME TYPE DATA AGE # default-token-il9rc kubernetes.io/service-account-token 1 1d # nginxsecret Opaque 2 1m |
下一步,修改Deployment、Service的规格:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# Deployment规格 apiVersion: apps/v1beta1 kind: Deployment metadata: name: my-nginx spec: replicas: 1 template: metadata: labels: run: my-nginx spec: # 声明一个卷,引用Secret volumes: - name: secret-volume secret: secretName: nginxsecret containers: - name: nginxhttps image: bprashanth/nginxhttps:1.0 ports: - containerPort: 443 - containerPort: 80 # 挂载Secret卷到对应位置 volumeMounts: - mountPath: /etc/nginx/ssl name: secret-volume # Service规格 apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: type: NodePort ports: - port: 8080 targetPort: 80 protocol: TCP name: http - port: 443 protocol: TCP name: https selector: run: my-nginx |
要把服务暴露到集群外部的IP地址,可以使用NodePorts或者LoadBalancers。
这是一个API对象,可以管理到集群内部服务(通常是HTTP)的访问。Ingress能够提供:负载均衡、SSL Termination、基于名称的虚拟主机服务。
典型情况下,Service/Pod的IP地址仅仅能在集群内部路由到,所有流量到达边缘路由器(Edge Router,为集群强制应用了防火墙策略的路由器,可能是云服务管理的网关,也可能是物理硬件)时要么被丢弃,要么被转发到别的地方。
Ingress(入口)是一个规则集,允许进入集群的连接到达对应的集群服务。
要定义Ingress对象,同样需要经过APIServer。一个Ingress控制器负责处理Ingress。
Ingress从1.1+开始可用,目前处于Beta版本。
要使用Ingress,你需要配备Ingress控制器。你可以在Pod中部署任意数量的自定义Ingress控制器,你需要为每个控制器标注适当的class。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: test-ingress annotations: ingress.kubernetes.io/rewrite-target: / # 规格中包含了创建LB或代理服务器所需的全部信息 spec: # 匹配入站请求的规则列表 rules: # 目前仅支持HTTP规则 - http: # 将来自任何域名的/testpat这个URL下的请求,都转发给test服务处理 host: * paths: - path: /testpath backend: serviceName: test servicePort: 80 |
注意:Ingress控制器可以响应不同名字空间中的多个Ingress。
为了让Ingress对象能生效,集群中必须存在Ingress控制器。Ingress实际上仅仅声明了转发规则,没有能执行转发的程序。
大部分控制器都是kube-controller-manager这个可执行程序的一部分,并且随着集群的创建而自动启动。但是Ingress控制器不一样,它作为Pod运行,你需要根据集群的需要来选择一个Ingress控制器实现,或者自己实现一个。
Ingress Controller的实现非常多,大多以Nginx或Envoy为基础。可以参考文章:Comparing Ingress controllers for Kubernetes。
K8S目前实现并维护GCE和nginx两个Ingress控制器。下面是基于Nginx的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# 默认后端,如果Nginx无处理请求,此后端负责兜底 apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: nginx-ingress chart: nginx-ingress-0.14.1 component: default-backend name: nginx-ingress-default-backend spec: replicas: 1 selector: matchLabels: app: nginx-ingress component: default-backend strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 type: RollingUpdate template: metadata: creationTimestamp: null labels: app: nginx-ingress component: default-backend spec: containers: - image: k8s.gcr.io/defaultbackend:1.3 imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 httpGet: path: /healthz port: 8080 scheme: HTTP initialDelaySeconds: 30 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 name: nginx-ingress-default-backend ports: - containerPort: 8080 protocol: TCP resources: |