首发于kubernetes
我们如何学会大规模改善Kubernetes CronJobs(第2部分,共2部分)

我们如何学会大规模改善Kubernetes CronJobs(第2部分,共2部分)

我们如何学会大规模改善Kubernetes CronJobs(第2部分,共2部分)

这是有关在Lyft上改善Kubernetes CronJobs的两部分博客系列的第2部分。如果你还未读第1部分,可以通过下面链接转到第1

[mp.weixin.qq.com/s/rMY7]

很明显,开箱即用的Kubernetes CronJobs并不是简单易用的,可替换的,可重复运行的计划任务。如果我们想将Lyft的所有分支都放心地移到Kubernetes上,我们不仅需要解决CronJobs的技术缺陷,还需要解决我们去使用它的经验。即,我们需要:

听一下我们的开发人员了解他们想要回答的有关问题

  • “我的cron跑了吗?” (“应用程序代码是否执行?”)
  • “它运行成功了吗?”
  • “执行任务需要多长时间?” (“应用程序代码执行需要多长时间?”)

通过使Kubernetes CronJobs易于推理,易于理解的生命周期以及清晰的平台/应用程序边界来扩展平台支持。

通过内置指标和警报来检测我们的平台,以减少开发人员需要编写和维护的定制警报配置和重复的cron包装程序脚本的数量。

通过构建工具,不仅可以轻松地从故障中恢复,还可以轻松测试新的CronJob配置。

修复了Kubernetes中长期存在的技术问题,例如TooManyMissedStarts错误,该错误需要手动干预以进行补救,并导致重要的故障场景(当缺少startDeadlineSeconds时)导致静默失败。

我们通过以下方式解决了这些问题:

  • 公开的可观察性不仅使开发人员能够调试其CronJob,而且使平台工程师还可以定义和监视服务级别目标(SLO)。
  • 添加一个工具,使我们的Kubernetes堆栈中的CronJob临时调用变得容易。
  • 解决Kubernetes本身内部的那些长期存在的问题。

CronJob指标和警报

平台生成的用于监控特定CronJob的仪表板的示例

我们使用针对Lyft的所有CronJob发出的以下指标来检测Kubernetes CronJob堆栈:

「started.count」—这是一个计数器,专门在CronJob调用的应用程序容器首次启动时增加。答案是:“应用程序代码是否执行?”

「{success, failure}.count」—这些计数器在给定的CronJob调用达到终端状态时(作业完成运行且jobcontroller不再尝试执行时)增加。这些回答了问题:“它运行成功了吗?”

「scheduling-decision.{invoke, skip}.count」—这些计数器可显示cronjobcontroller调用CronJob时做出的决策。特别是,skip.count有助于回答:“为什么我的cron无法运行?” 并由以下原因标签参数化:

  • 「reason = concurrencyPolicy-cronjobcontroller」跳过的调用CronJob的原因是,这样做会违反CronJob的规范ConcurrencyPolicy。
  • 「reason = missedDeadline—cronjobcontroller」跳过的调用CronJob,因为它错过了由定义的调用窗口.spec.startingDeadlineSeconds。
  • 「reason = error」 —这是尝试调用CronJob时遇到的其他错误的全部内容。

「app-container-duration.seconds」—这是一个计时器,用于测量应用程序容器的挂壁时间。它回答了一个问题:“应用程序代码执行需要多长时间?” 该计时器故意不包括安排Pod,启动附带工具等所需的时间,这些时间是平台团队拥有的一部分,并且由启动延迟所涵盖。

「start-delay.seconds」—这是一个计时器,用于测量启动延迟。跨平台汇总时,此指标使平台工程师不仅可以量化,监视和调整平台的性能,而且可以开始定义SLO(例如启动延迟和最大cron计划频率)。

有了这些指标,我们便能够创建默认警报,这些警报在以下情况下通知开发人员:

  • 他们的CronJob应该在(**rate(scheduling-decision.skip.count) > 0)**时没有运行
  • 他们的CronJob失败**(rate(failure.count) > 0)**

由于平台内置了这些功能,因此开发人员不再需要维护自己的Kubernetes警报和指标。

临时Cron Run

我们将**kubectl create job test-job —从= cronjob / **调整为我们的内部CLI工具,Lyft的每个工程师都使用该工具与Kubernetes上的服务进行交互,从而简化了调用CronJob ad hoc的工作,以便:

  • 从间歇性的CronJob故障中恢复。
  • 在非凌晨3:00(这是更方便的时间,您可以实时检查CronJob,Job和Pod事件)时重现和调试运行时故障,而不是试图抓住问题。
  • 在开发新的CronJob或迁移现有的Unix cron时,无需等待cron时间表 通过就测试运行时配置。

修复太多遗漏的问题

我们修复了「TooManyMissedStarts」错误,以使CronJobs在连续100次错过启动之后不再“卡住”。除了消除手动干预的需要之外,此补丁还使我们能够实际监视何时**startingDeadlineSeconds **超出。由Vallery Lancey设计和编写此补丁,并向Tom Wanielista寻求帮助,以提出算法。Kubernetes中目前有一个开放的PR可以向该补丁提供上游资源。

深入研究实施:Cron监控

在Kubernetes CronJobs的生命周期中的何处,我们添加了工具来发出指标

不依赖于cron时间表的警报

有关对错过的cron调用实施警报的棘手部分之一是处理cron计划(crontab.guru对于解密这些事件非常有帮助!)。例如,考虑一个cron时间表,例如:

#每5分钟
*/5 * * * *

要检测此cron,您可以在每次cron完成时增加一个计数器(或使用cron包装器)。然后,在警报系统中,您将写一个条件查询,说:“回顾60分钟的窗口,如果计数器增加的次数少于12,则提醒我”。问题解决了吧? 相反,如果您有一个cron时间表,例如:

#从星期一到星期五的每周的9点到17点,每小时每小时0分钟。
#换句话说,就是“营业时间”(周一至周五9-5)
0 9–17 * * 1–5

现在,你需要看中查询,或者你的警报系统具有一些功能,使你仅在“工作时间”内收到警报。无论如何,这些示例说明了将cron计划与警报定义耦合会带来一些不利影响:

  • 更改计划时间表意味着更改警报。
  • 某些cron计划需要复杂的时间序列查询才能复制。
  • 不完全按时启动的Cron将需要在查询中内置一定量的“宽限期”,以最大程度地减少误报。

仅#2使得在平台上为所有cron生成默认警报是一项非常艰巨的任务,并且#3特别与诸如Kubernetes CronJob之类的分布式平台相关,在这些平台上启动延迟不可忽略。或者,有些解决方案使用固定开关,这仍然需要将cron计划与警报和/或异常检测算法耦合,这需要随着时间的流逝学习期望,因此不能立即用于新的CronJobs或更改cron时间表。

解决问题的另一种方法是问:应该运行cron但不能运行cron是什么意思?

在Kubernetes中,除非「cronjobcontroller」控制面板中的bug处于关闭状态(如果你正确监控集群,后者应该非常明显),这意味着「cronjobcontroller」对CronJob进行评估后(根据cron计划)确定需要被调用,但仍故意选择不调用。

听起来有点熟?这正是我们的「scheduling-decision.skip.count」指标所捕获的!现在,我们只需要检查是否有更改,**rate(scheduling-decision.skip.count)**就可以提醒某人CronJob应该运行但尚未运行。

此解决方案使cron计划与警报本身脱钩,从而产生了多个优点:

  • cron计划更改时,无需重新配置警报。
  • 没有复杂的时间序列查询和条件。
  • 易于为平台上的所有CronJob生成默认警报。

这与前面提到的其他时间序列和警报相结合,有助于绘制更完整,更容易理解的CronJob状态图。

实现启动延迟计时器

由于CronJob生命周期的复杂性,我们需要精确地确定要在堆栈中添加的位置,以便准确地测量该指标。归结为捕获2个时间戳:

  • T1:预计cron何时运行(由cron时间表规定)。
  • T2:应用程序代码实际开始执行的时间。 然后,「start delay = T2 — T1」。对于T1,我们在本身的cron调用逻辑中添加了一些代码,「cronjobcontroller」以将预期的开始时间写为调用CronJob时创建的**.metadata.Annotationon** Job对象cronjobcontroller。然后,我们可以通过发出基本GET Job请求直接从任何API客户端使用它。

T2正确一点比较棘手。因为我们有兴趣捕获启动延迟的最严格边界,所以我们希望T2成为应用程序容器第一次开始运行。相反,如果我们T2在任何应用程序容器启动(包括重新启动)时进行记录,那么我们的启动延迟将包括应用程序代码执行时间。为此,当我们第一次检测到给定Job的应用程序容器过渡到该容器Running时,我们**.metadata.Annotation**向Job对象写入了另一个,本质上创建了一个分布式锁,以便将来将忽略给定Job的应用程序容器启动,并且只有第一次开始的时间戳会被记录。

影响力

自推出这些功能和错误修复以来,我们已经收到来自开发人员的很多积极反馈。总而言之,使用我们的Kubernetes CronJob平台的开发人员:

  • 不再需要维护自己的定制监视和警报。
  • 可以高度肯定自己的CronJobs在工作,并在不工作时收到警报。
  • 可以使用我们的临时CronJob调用工具轻松地从故障中恢复并测试新的CronJob。
  • 可以了解其应用程序代码的性能(使用app-container-duration.seconds计时器指标)。

此外,平台工程师现在具有另一个维度(启动延迟)来衡量用户体验和平台性能。

最后(也许是最大的胜利),通过建立更丰富的可观察性以使CronJob状态更容易推理,开发人员和平台工程师现在可以在查看相同数据时一起调试问题,而且开发人员通常可以诊断和解决问题自己一起使用平台提供的工具。

结论

编排分布式计划任务很困难。Kubernetes CronJobs只是这样做的许多方式之一。尽管CronJobs远非完美,但如果您愿意花费时间和精力来增加可观察性,了解它们如何失败以及建立用户需求,CronJobs可以大规模工作。

注意:有一个开放的Kubernetes增强建议(KEP),以解决CronJobs的缺点并将其毕业于GA。 github.com/kubernetes/e

个人座右铭留言区 「——欢迎投稿」



如果喜欢文章的话,点点关注,就差你的关注了,更多好玩有趣的云原生前沿技术尽在云原生CTO,如果对你有帮助,欢迎分享给更多人

专注云原生技术分享 #公众号:云原生CTO

发布于 2020-11-21 22:16