08 DaemonSet(每个node上只调度一个pod)
DaemonSet是 Kubernetes1.2 版本新增的一种资源对象,用于管理在集群中的每个Node上仅运行一份Pod的副本实例,如下图所示:
8.1 DaemonSet
8.1.1 应用场景
下面举例DaemonSet的一些使用场景:
- 在每个
Node上都运行一个GlusterFS存储或者Ceph存储的Daemon进程; - 在每个
Node上都运行一个日志采集程序,例如Fluentd或者Logstach; - 在每个
Node上都运行一个性能监控程序,采集该Node的运行性能数据, 例如Prometheus Node Exporter、collectd、New Relic agent或者Ganglia gmond等。
DaemonSet的Pod调度策略与RC类似,除了使用系统内置的算法在每个Node上进行调度,也可以在Pod的定义中使用NodeSelector或NodeAffinity来指定满足条件的Node范围进行调度。
8.1.2 举例
下面的例子定义了为在每个Node上都启动一个fluentd容器,配置文件 fluentd-ds.yaml的内容如下 (其中挂载了物理机的两个目录"/var/log"和 “/var/lib/docker/containers”):
apiVersion: apps/v1 kind: DaemonSet metadata: name: fluentd-cloud-logging namespace: kube-system labels: k8s-app: fluentd-cloud-logging spec: template: metadata: namespace: kube-system labels: k8s-app: fluentd-cloud-logging spec: containers: - name: fluentd-cloud-logging image: gcr.io/google containers/fluentd-elasticsearch:1.17 resources: limits: cpu: 100m memory: 200Mi env: - name: FLUENTD ARGS value: -q volumeMounts - name: varlog mountPath: /var/log readOnly: false - name: containers mountPath: /var/lib/docker/containers readonly: false volumes: - name: containers hostPath: path: /var/lib/docker/containers - name: varlog hostPath: path: /var/log
使用kubectl create命令创建该DaemonSet:
kubectl create -f fluentd-ds.yaml daemonset "fluentd-cloud-logging"created
查看创建好的DaemonSet和Pod,可以看到在每个Node上都创建了一个
Pod:
8.1.3 注意事项
DaemonSet调度不同于普通的Pod调度,所以没有用默认的Kubernetes Scheduler进行调度,而是通过专有的DaemonSet Controller进行调度。但是随着Kubernetes版本的改进和调度特性不断丰富,产生了一些难以解决的矛盾,最主要的两个矛盾如下:
- 普通的Pod是在Pending状态触发调度并被实例化的,DaemonSet Controller并不是在这个状态调度Pod的,这种不一致容易误导和迷惑用户;
- Pod优先级调度是被Kubernetes Scheduler执行的,而DaemonSet
Controller并没有考虑到Pod优先级调度的问题,也产生了不一致的结果。
从Kubernetes 1.8开始,DaemonSet的调度默认切换到Kubernetes Scheduler进行,从而一劳永逸地解决了以上问题及未来可能的新问题,因为默认切换到了Kubernetes Scheduler统一调度Pod,因此DaemonSet也能正确处理Taints和Tolerations的问题。
09 批处理调度
Kubernetes从1.2版本开始支持批处理类型的应用,我们可以通过Kubernetes Job资源对象来定义并启动一个批处理任务。
9.1 批处理调度
批处理任务通常并行(或者串行)启动多个计算进程去处理一批工作项(Work item),处理完成后,整个批处理任务结束。
9.1.1 任务模式分类
9.1.1.1 按实现方式分类
按照批处理任务实现方式的不同,批处理任务可以分为如图所示的几种模式:
模式分类:
| 模式名称 | 描述 |
| Job Template Expansion | 一个Job对象对应一个待处理的Work item,有几个Work item就产生几个独立的Job,通常适合Work item数量少、每 个Work item要处理的数据量比较大的场景,比如有一个100GB的文件作为一个 Work item,总共有10个文件需要处理。 |
| Queue with Pod Per Work Item | 采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,在这种模式下,Job会启动N个Pod,每个Pod都对应一个Work item |
| Queue with Variable Pod Count | 也是采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,但与上面的模式不同,Job启动的Pod数量是可变的 |
| Single Job with Static Work Assignment | 也是一个Job产生多个Pod,但它采用程序静态方式分配任务项,而不是采用队列模式进行动态分配 |
模式对比:
| 模式名称 | 是否是一个job | pod的数量少于work item | 用户程序是否要做相应的修改 | kubernetes是否支持 |
| Job Template Expansion | / | / | 是 | 是 |
| Queue with Pod Per Work Item | 是 | / | 有时候需要 | 是 |
| Queue with Variable Pod Count | 是 | / | / | 是 |
| Single Job with Static Work Assignment | 是 | / | 是 | / |
9.1.1.2 按批处理并行分类
考虑到批处理的并行问题,Kubernetes将Job分以下三种类型:
| 类型 | 描述 |
| Non-parallel Jobs | 通常一个Job只启动一个Pod,除非Pod异常,才会重启该Pod,一旦此Pod正常结束,Job将结束 |
| Parallel Jobs with a fixed completion count | 并行Job会启动多个Pod,此时需要设定Job的.spec.completions参数为一个正数,当正常结束的Pod 数量达至此参数设定的值后,Job结束。此外,Job的.spec.parallelism参数用来控制并行度,即同时启动几个Job来处理Work item |
| Parallel Jobs with a work queue | 任务队列方式的并行Job需要一个独 立的Queue,Work item都在一个Queue中存放,不能设置Job 的.spec.completions参数,此时Job有以下特性:① 每个Pod都能独立判断和决定是否还有任务项需要处理,如果某个Pod正常结束,则Job不会再启动新的Pod 。 ②如果一个Pod成功结束,则此时应该不存在其他Pod还在工作的情况,它们应该都处于即将结束、退出的状态。③如果所有Pod都结束了,且至少有一个Pod成功结束,则整个Job成功结束。 |
9.1.2 案例
9.1.2.1 Job Template Expansion案例
首先是Job Template Expansion模式,由于在这种模式下每个Work item都对应一个Job实例,所以这种模式首先定义一个Job模板,模板里的主要参数是Work item的标识,因为每个Job都处理不同的Work item。
如下所示的Job模板(文件名为job.yaml.txt)中的 $ITEM 可以作为任务项的标识:
apiVersion: batch/v1 kind: Job metadata: name: process-item-$ITEM labels: jobgroup: jobexample spec: template: metadata: name: jobexample labels: jobgroup: jobexample spec: containers: - name: c image: busybox command: ["sh","-c","echo Processing item $ITEM &sleep 5"] restartPolicy: Never
通过下面的操作,生成了3个对应的Job定义文件并创建Job:
> for i in apple banana cherry > do > cat job.yaml.txt | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml > done # ls jobs job-apple.yaml job-banana.yaml job-cherry.yaml # kubectl create -f jobs job "process-item-apple"created job "process-item-banana"created job "process-item-cherry"created
观察Job的运行情况:
$ kubect1 get jobs -l jobgroup=jobexample NAME DESIRED SUCCESSFUL AGE process-item-apple 1 1 4m process-item-banana 1 1 4m process-item-cherry 1 1 4m
9.1.2.2 Queue with Pod Per Work Item案例
在这种模式下需要一个任务队列存放Work item,比如RabbitMQ客户端程序先把要处理的任务变成Work item放入任务队列,然后编写Worker程序、打包镜像并定义成为Job中的Work Pod。
Worker程序的实现逻辑是从任务队列中拉取一个Work item并处理, 在处理完成后结束进程。并行度为2的Demo如下图所示:
9.1.2.3 Queue with Variable Pod Count案例
由于这种模式下,Worker程序需要知道队列中是否还有等待处理的Work item,如果有就取出来处理,否则就认为所有工作完成并结束进程,所以任务队列通常要采用Redis或者数据库来实现:
10 定时任务
Kubernetes从1.5版本开始增加了一种新类型的Job,即类似Linux Cron的定时任务Cron Job,下面看看如何定义和使用这种类型的Job。
10.1 基本语法
首先,确保
Kubernetes的版本为1.8及以上。
Cron Job的定时表达式基本上照搬了Linux Cron的表达式,格式如下:
Minutes Hours DayofMonth Month DayofWeek • 1
其中每个域都可出现的字符如下。
| 域 | 描述 |
| Minutes | 可出现“,”“-”“*”“/” 这4个字符,有效范围为0~59的整数 |
| Hours | 可出现“,”“-” “%”“/” 这4个字符,有效范围为0~23的整数 |
| DayofMonth | 可出现“,”“-“*”“/““?”“L”“W““C”这8个字符,有效范围 为1~31的整数 |
| Month | 可出现“,”“-”“*”“/”这4个字符,有效范围为1~12的整数或JAN~DEC |
| DayofWeek | 可出现“,”“*”“/”“?”“L”“C”“#” 这8个字符,有效范围为1~7的整数或SUN~SAT。1表示星期天,2表示星期一,以此类推 |
表达式中的特殊字符“*”与“/”的含义如下:
*:表示匹配该域的任意值,假如在Minutes域使用“*”,则表示每分钟都会触发事件;/:表示从起始时间开始触发,然后每隔固定时间触发一次,例如在
Minutes域设置为5/20,则意味着第1次触发在第5min时,接下来每20min触发一 次,将在第25min、第45min等时刻分别触发。
10.2 案例
比如,我们要每隔1min执行一次任务,则Cron表达式如下:
*/1 * * * *
编写一个Cron Job的配置文件(cron.yaml):
apiversion: batch/vl beta kind: CronJob metadata: name: hello spec: schedule: "*/1 * * * *" jobTemplate: spec: template: spec: containers: - name:hello image:busybox args: - /bin/sh - -C - date;echo Hello from the Kubernetes cluster restartPolicy:OnFailure
该例子定义了一个名为hello的Cron Job,任务每隔1min执行一次,运行的镜像是busybox,运行的命令是Shell脚本,脚本运行时会在控制台输出当前时间和字符串"Hello from the Kubernetes cluster".
接下来运行kubectl create命令完成创建:
$ kubectl create -f cron.yaml cronjob "hello"created
然后每隔1min运行kubectl get cronjob hello查看任务状态,发现的确每分钟调度了一次:
还可以通过查找Cron Job对应的容器,验证每隔1min产生一个容器的事实:
查看任意一个容器的日志,结果如下:
运行下面的命令,可以更直观地了解Cron Job定期触发任务执行的历史和现状:
在Kubernetes1.9版本后,kubectl命令增加了别名cj来表示cronjob,同时 kubectl set image/env命令也可以作用在CronJob对象上。
11 容灾调度
我们可以将Pod的各种常规调度策略认为是将整个集群视为一个整体,然后进行 “打散或聚合” 的调度。
当我们的集群是为了容灾而建设的跨区域的多中心(多个Zone)集群,即集群中的节点位于不同区域的机房时,比如:
北京、上海、广 州、武汉,要求每个中心的应用相互容灾备份,又能同时提供服务,此时最好的调度策略就是将需要容灾的应用均匀调度到各个中心,当某个中心出现问题时, 又自动调度到其他中心均匀分布,
Pod的多中心均匀分布调度效果图如下所示(不管每个中心的Nod节点数量如何):
11.1 如何实现?
用普通的基于Node标签选择的调度方式也可以实现上述效果,比如为每个
Zone都建立一个Deployment,Pod的副本总数除以Zone的数量就是每个分区的
Pod副本数量。但这样做有个问题:如果某个Zone失效,那么这个Zone的Pod就无法迁移到其他Zone。
另外,topology.kubernetes.io/zone就是Kubernetes默认支持的重要拓扑域之
一,那是否可以用Pod的亲和性调度来解决这个问题呢?不能,因为Pod的亲和性 调度用于解决相关联的Pod的调度问题,不能保证被依赖的Pod被均匀调度到多个Zone。
为了满足这种容灾场景下的特殊调度需求,在Kubernetes1.16版本中首次引入Even Pod Spreading特性,用于通过topologyKey属性识别Zone,并通过设置新 的参数topologySpreadConstraints来将Pod均匀调度到不同的Zone。
11.2 举例
举个例子, 假如我们的集群被划分为多个Zone,我们有一个应用(对应的Pod标签为 app=foo)需要在每个Zone均匀调度以实现容灾,则可以定义YAML文件如下:
spec: topologySpreadConstraints: - maxSkew: 1 whenUnsatisfiable: DoNotSchedule topologyKey: topology.kubernetes.io/zone selector: matchLabels: app: foo
在以上YAML定义中,关键的参数是maxSkew,用于指定Pod在各个Zone上调度时能容忍的最大不均衡数:
- 值越大,表示能接受的不均衡调度越大;
- 值越小,表示各个Zone的Pod数量分布越均匀。
为了理解maxSkew,我们需要先理解skew参数的计算公式:
skew[topo]=count[topo]-min(count[topo]) • 1
即每个拓扑区域的skew值都为该区域包括的目标Pod数量与整个拓扑区域最少Pod数量的差,而naxSkew就是最大的skew值。
假如在上面的例子中有3个拓扑区域,分别为Zone A、Zone B及Zone C,有3个目标Pod需要调度到这些拓扑区域,那么前两个毫无疑问会被调度到Zone A和Zone B,Even Pod Spreading调度效果如图所示:
那么,第3个Pod会被调度到哪里呢?我们可以手动计算每个Zone的skew:
- 首先计算出
min(count[topo])是0,对应Zone C; - 于是
Zone A的skew=1-0=1,Zone B的skew=1-0=0,Zone C的skew=0-0=0,于是第3个Pod应该被放在Zone C,此时min(count[topo])的值就变成了1,而实际的maxSkew的值为0,符合预期设置; - 如果我们把maxSkew设置为2,则在这种情况下,第3个Pod被放在
Zone A或Zone B都是符合要求的。
有了新的Even Pod Spreading调度特性的加持,再加上之前就已成熟的Pod亲和性调度,Kubernetes就可以完美实现特定应用的容灾部署目标了。
具体做法也很简单:将一个应用中需要部署在一起的几个Pod用亲和性调度声明捆绑,然后选 择其中一个Pod,加持Even Pod Spreading调度规则即可。最终的部署效果图如下:











