作业帮上万个 CronJob 和在线业务混部,如何解决弱隔离问题并进一步提升资源利用率?

768 阅读8分钟

作者

吕亚霖,作业帮基础架构 - 架构研发团队负责人。负责技术中台和基础架构工作。在作业帮期间主导了云原生架构演进、推动实施容器化改造、服务治理、GO 微服务框架、DevOps 的落地实践。

别路,作业帮基础架构-高级研发工程师,在作业帮期间,负责多云 K8s 集群建设、K8s 组件研发、Linux 内核优化调优相关工作。

背景

作业帮在云原生容器化改造的过程中,随着集群规模越来越大、业务混合部署的场景越来越复杂,面临的集群问题也越来越多,走到了 Kubernetes 及容器化的深水区, 尤其是在上万个 CronJob 容器化,和在线业务混合部署在同一个生产集群后,问题就更加明显。

作业帮在线的生产业务使用 TKE 部署在黑石2.0 物理机上,单个机器规格比较大,部署的pod 也就比较多,而 cronjob 的特性是频繁、定时启动和销毁,同时也需要给这部分业务预留一定的固定资源,所以这块主要有 2 个问题;一是在大规模pod 频繁创建销毁场景下,cgroup 弱隔离性导致的节点稳定性问题,从而影响同一节点其他业务,二是资源预留导致的资源利用率低的问题。这两个问题其实已经超出了 原生 Kubernetes 的能力覆盖范围,我们需要新的思路来解决。

下面将详细介绍这两个问题产生的原因及解决办法。

问题一:集群内节点稳定性

由于业务上存在很多分钟级执行的定时任务,导致 pod 的创建和销毁非常频繁,单个节点平均每分钟有上百个容器创建和销毁,机器的稳定性问题频繁出现。

一个典型的问题是频繁创建 pod 导致节点上 cgroup 过多,特别是 memory cgroup 不能及时回收,读取/sys/fs/cgroup/memory/memory.stat 变慢,由于 kubelet 会定期读取该文件来统计各个 cgroup namespace 的内存消耗,CPU 内核态逐渐上升,上升到一定程度时,部分 CPU 核心会长时间陷入内核态,导致明显的网络收发包延迟。

在节点 perf record cat /sys/fs/cgroup/memory/memory.stat 和 perf report 会发现,CPU 主要消耗在 memcg_stat_show 上:

img

而 cgroup-v1 的 memcg_stat_show 函数会对每个 CPU 核心遍历多次 memcg tree,而在一个 memcg tress 的节点数量达到几十万级别时,其带来的耗时是灾难性的。

为什么 memory cgroup 没有随着容器的销毁而立即释放呢?主要是因为 memory cgroup 释放时会遍历所有缓存页,这可能很慢,内核会在这些内存需要用到时才回收,当所有内存页被清理后,相应的 memory cgroup 才会释放。整体来看,这个策略是通过延迟回收来分摊直接整体回收的耗时,一般情况下,一台机器上创建容器不会太多,通常几百到几千基本都没什么问题,但是在大规模定时任务场景下,一台机器每分钟都有上百个容器被创建和销毁,而节点并不存在内存压力,memory cgroup 没有被回收,一段时间后机器上的 memory cgroup 数量达到了几十万,读取一次 memory.stat 耗时达到了十几秒,CPU 内核态大幅上升,导致了明显的网络延迟。

img

除此之外,dockerd 负载过高、响应变慢、kubelet PLEG 超时导致节点 unready 等问题。

问题二:集群的节点资源利用率

由于我们使用的是 TKE vpc-cni 的网络模式,这种网络模式依赖节点绑定的弹性网卡数量,所以单个节点上的 pod ip 数量存在上限,节点有几乎一半的 podip 是为定时任务的 pod 保留的,造成ip 浪费,另外定时任务的 pod 运行时间普遍很短,这就导致了集群为定时任务预留的资源产生了较多闲置,不利于整体的机器资源使用率提升。

其他问题:调度速度、服务间隔离性

在某些时段,比如每天 0 点,会同时产生几千个 Job 需要运行。而原生调度器是 K8s 调度 pod 本身对集群资源分配,反应在调度流程上则是预选和打分阶段是顺序进行的,也就是串行。几千个 Job 调度完成需要几分钟,而大部分业务是要求 00:00:00 准时运行或者业务接受误差在 3s 内。

有些服务 pod 是计算或者 IO 密集型,这种服务会大量抢占节点 CPU 或者 IO,而 cgroup 的隔离并不彻底,所以会干扰其他正常在线服务运行。

解决思路及方案

所以,对 CronJob 型任务我们需要一个更彻底的隔离方式,更细粒度的节点,更快的调度模式。

为解决上诉问题,我们考虑将定时任务 pod 和普通在线服务的 pod 隔离开,但是由于很多定时任务需要和集群内服务互通,还不能通过分集群的方式隔离。

腾讯云弹性容器服务 EKS 提供的虚拟节点,给我们解决上诉问题提供了一个新的思路,

EKS 的虚拟节点是 serverless 形态的kubernetes 服务,可以加入到现有的TKE 集群中,部署在虚拟节点上的 pod 具备与部署在正常 TKE 节点上的 pod 具备一致的网络连通性,但虚拟节点上的pod 是在vm 层面做了隔离,又具有无需预留资源,按量计费的特性,可以很好的满足我们这个场景的需求,所以我们将CronJob 这种类型的业务都调度到了虚拟节点,如图所示:

img

任务调度器

为解决 K8s 默认串行调度慢的问题,我们针对 job 类任务,开发了任务调度器,所有 CronJob 型 workload 都使用任务调度器,任务调度器批量并行调度任务 pod 到 虚拟 节点,实现大规模pod 任务 ms 级调度,也支持 虚拟节点故障时或者资源不足时调度回标准 TKE 节点。

解决 TKE 节点和虚拟节点在运维方式上的差异

在使用 虚拟节点前,首先要解决 虚拟节点 pod 和运行在标准节点上的 pod 差异,做到对业务研发无感。

日志采集统一

在日志采集方面,由于 EKS 这种 nodeless 的形态,无法运行 DaemonSet,而我们的日志采集组件是以 DaemonSet 形式运行的,这就需要对虚拟节点上的日志做单独的采集方案。EKS 虚拟节点 本身提供日志采集agent, 可以将容器的标准输采集并吐到一个 Kafka topic,然后我们统一在这个 topic 里消费。

监控报警统一

在监控方面,我们对 虚拟节点 上的 pod 做了实时 CPU/内存/磁盘/网络流量等监控,做到了和普通节点上的 pod 一致,暴露 pod sanbox 的 export 接口,promethus 负责统一采集,迁移到 虚拟节点 时做到了业务完全无感。

提升启动性能

虚拟节点上的 Job 需要具备秒级的启动速度才能满足定时任务对启动速度的要求,比如业务要求 00:00:00 准时运行或者业务接受误差在 3s 内。

主要耗时在以下两个步骤:

  1. 业务镜像拉取加速

  2. 虚拟节点 pod 创建和初始化加速

针对第一个问题:EKS 提供镜像缓存的功能,第一次拉取的时候稍微慢一些,拉下来后默认会缓存一段时间,同一个业务第二次启动就不需要再拉取镜像,所有镜像下载慢的问题基本就没有了。

针对第二个问题:业务要求的启动时间误差在 3s 内,所以我们和 腾讯云 EKS 团队沟通后,为这种大规模、高频、短时的计算作业场景进行了针对性优化,提升了频繁启动的效率并降低了运行环境初始化的时间。

最终实现了虚拟节点上的 Pod 秒级启动。

总结

通过 TKE + EKS 虚拟节点的方式,我们将正常在线任务和定时任务隔离开,有效保障了在线业务的稳定性,结合 自研 Job 任务调度器、EKS 镜像缓存、pod 启动加速等能力,实现 任务pod 秒级调度并启动,同时 TKE + 虚拟节点 都是标准的 K8s API ,做到了业务平滑迁移。最终要的是,我们固定的集群不需要再为 CronJob 类任务预留资源,释放了集群里 10% 的资源,结合 EKS 随用随取、按量计费的特性,定时任务的资源成本降低了 70%左右。

关于我们

更多关于云原生的案例和知识,可关注同名【腾讯云原生】公众号~

福利:

①公众号后台回复【手册】,可获得《腾讯云原生路线图手册》&《腾讯云原生最佳实践》~

②公众号后台回复【系列】,可获得《15个系列100+篇超实用云原生原创干货合集》,包含Kubernetes 降本增效、K8s 性能优化实践、最佳实践等系列。

③公众号后台回复【白皮书】,可获得《腾讯云容器安全白皮书》&《降本之源-云原生成本管理白皮书v1.0》