<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Kevin Yang on Medium]]></title>
        <description><![CDATA[Stories by Kevin Yang on Medium]]></description>
        <link>https://medium.com/@yangke?source=rss-9fdf8d77249c------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*kDiPrNJY2B0PKBWL97zX-Q.jpeg</url>
            <title>Stories by Kevin Yang on Medium</title>
            <link>https://medium.com/@yangke?source=rss-9fdf8d77249c------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 23 May 2026 08:59:37 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@yangke/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How we learned to improve Kubernetes CronJobs at Scale (Part 2 of 2)]]></title>
            <link>https://eng.lyft.com/how-we-learned-to-improve-kubernetes-cronjobs-at-scale-part-2-of-2-dad0c973ffca?source=rss-9fdf8d77249c------2</link>
            <guid isPermaLink="false">https://medium.com/p/dad0c973ffca</guid>
            <category><![CDATA[k8s]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[cronjob]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Kevin Yang]]></dc:creator>
            <pubDate>Wed, 05 Aug 2020 19:06:22 GMT</pubDate>
            <atom:updated>2020-11-12T00:41:33.270Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9PdliSohxjdWr7A82-g15w.jpeg" /><figcaption>Fr. Dougal McGuire / CC BY-SA (<a href="https://creativecommons.org/licenses/by-sa/2.0">https://creativecommons.org/licenses/by-sa/2.0</a>) <a href="https://commons.wikimedia.org/wiki/File:Bultaco_engine_exploded_view.jpg">https://commons.wikimedia.org/wiki/File:Bultaco_engine_exploded_view.jpg</a></figcaption></figure><p><em>This is Part 2 of a two-part blog series on Improving Kubernetes CronJobs at Lyft. If you haven’t already, checkout </em><a href="https://medium.com/@yangke/improving-kubernetes-cronjobs-at-scale-part-1-cf1479df98d4"><em>Part 1</em></a><em>.</em></p><p>It became clear that Kubernetes CronJobs out-of-the-box were not going to be an easy to use, drop-in replacement for running repeated, scheduled tasks. If we wanted to move all of Lyft’s crons onto Kubernetes confidently, <strong><em>we needed to not only address the technical shortcomings of CronJobs, but the human experience of using them as well</em></strong>. Namely, we needed to:</p><p><strong>Listen to our developers</strong> to understand what questions they wanted answered about their crons:</p><ul><li><em>“Did my cron run?”</em> (<em>“Did the application code execute?”</em>)</li><li><em>“Did it run successfully?”</em></li><li><em>“How long did the cron take to execute?”</em> (<em>“How long did it take the application code to execute?”</em>)</li></ul><p><strong>Scale platform support</strong> by making Kubernetes CronJobs easier to reason about, their life cycles well-understood, and the platform / application boundary clear.</p><p><strong>Instrument our platform</strong> with built-in metrics and alerts to reduce the amount of bespoke alarm configurations and duplicated cron wrapper scripts that developers need to write and maintain.</p><p><strong>Build tooling</strong> to make it easy to not only recover from failures but test new CronJob configuration as well.</p><p><strong>Fix long-standing, technical issues in Kubernetes </strong>like the<a href="https://github.com/kubernetes/kubernetes/issues/42649"> TooManyMissedStarts bug</a> that require manual intervention to remedy and cause an important failure scenario (<a href="https://github.com/kubernetes/kubernetes/issues/73169">when startingDeadlineSeconds is missed</a>) to silently fail.</p><h3>Solution</h3><p>We solved these problems by:</p><ol><li>Exposing observability that not only enables developers to debug their CronJobs, but allows platform engineers to define and monitor Service Level Objectives (SLOs) as well.</li><li>Adding a tool to make ad hoc invocations of a CronJob easy in our Kubernetes stack.</li><li>Fixing those long-standing issues inside Kubernetes itself.</li></ol><h3>CronJob Metrics and Alerts</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ql36G1xqoLvtkFAK" /><figcaption><em>An example of a dashboard generated by the platform to monitor a particular CronJob</em></figcaption></figure><p>We instrumented our Kubernetes CronJob stack with the following metrics that are emitted for all CronJobs at Lyft:</p><p>started.count — This is a <strong>counter</strong> that is incremented specifically when the application container of a CronJob invocation starts <em>for the first time</em>. This answers the question: <em>“Did the application code execute?”</em></p><p>{success, failure}.count— These are <strong>counters</strong> that are incremented when a given CronJob invocation reaches a <em>terminal state</em> (when a Job has finished running and the jobcontroller no longer tries to execute it). These answer the question: <em>“Did it run successfully?”</em></p><p>scheduling-decision.{invoke, skip}.count — These are <strong>counters</strong> that expose the decisions the cronjobcontroller makes when invoking a CronJob. In particular, skip.count helps answer: <em>“Why is my cron not running?”</em> and is parametrized by the following reason labels:</p><ul><li>reason = concurrencyPolicy — The cronjobcontroller skipped invoking a CronJob because doing so would be a violation of its ConcurrencyPolicy .</li><li>reason = missedDeadline — The cronjobcontroller skipped invoking a CronJob because it has missed the invocation window defined by .spec.startingDeadlineSeconds .</li><li>reason = error — This is a catch-all for other errors encountered when trying to invoke a CronJob.</li></ul><p>app-container-duration.seconds — This is a <strong>timer</strong> that measures the wall-time of the application container. It answers the question: <em>“How long did it take the application code to execute?”</em> This timer deliberately does not include time taken to schedule a Pod, startup sidecars, etc., which are part of what the platform team owns and is encompassed by start delay.</p><p>start-delay.seconds — This is a <strong>timer</strong> that measures start delay. This metric, when aggregated across the platform, enables platform engineers to not only quantify, monitor, and tune the performance of the platform, but also begin to define SLOs for things like start delay and maximum cron schedule frequency.</p><p>With these metrics, we were then able to create default alerts that notified developers when:</p><ul><li>Their CronJob did not run when it was supposed to (rate(scheduling-decision.skip.count) &gt; 0)</li><li>Their CronJob failed (rate(failure.count) &gt; 0)</li></ul><p>Developers no longer need to maintain their own alerts and metrics for crons on Kubernetes as the platform provides them built-in.</p><h3>Ad-hoc Cron Run</h3><p>We adapted <a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#-em-job-em-">kubectl create job test-job — from=cronjob/&lt;your-cronjob&gt;</a> to our internal CLI tool that every engineer at Lyft uses to interact with their services on Kubernetes to make it simple to invoke their CronJob ad hoc in order to:</p><ul><li>Recover from intermittent CronJob failures.</li><li>Reproduce and debug run-time failures at a time that is not 3:00 AM (a more convenient time when you can inspect CronJob, Job, and Pod events in real-time) instead of trying to <em>catch it in the act.</em></li><li>Test run-time configuration when developing a new CronJob or migrating an existing Unix cron without waiting for the cron schedule to pass by.</li></ul><h3>Fixing TooManyMissed Starts</h3><p>We <a href="https://github.com/lyft/kubernetes/pull/13">fixed</a> the <em>TooManyMissedStarts</em> bug so that CronJobs would no longer get “stuck” after 100 missed starts in a row. In addition to removing the need for manual intervention, this patch also allowed us to <a href="https://github.com/lyft/kubernetes/pull/18"><em>actually </em>monitor</a> when startingDeadlineSeconds <a href="https://github.com/kubernetes/kubernetes/issues/73169">is exceeded</a>. Hats off to <a href="https://twitter.com/vllry">Vallery Lancey</a> for designing and writing this patch as well as <a href="https://tomwanielista.com/">Tom Wanielista</a> for help come up with the algorithm. There is currently an <a href="https://github.com/kubernetes/kubernetes/pull/89397">open PR in Kubernetes</a> to upstream this patch.</p><h3>Implementation Deep Dive: Cron Monitoring</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QWvLUV7rbhoVTu40" /><figcaption><em>Where in the life cycle of Kubernetes CronJobs we have added instrumentation to emit metrics</em></figcaption></figure><h4>Alerts that don’t depend on cron schedules</h4><p>One of the tricky parts about implementing an alert on missed cron invocations is dealing with cron schedules (<a href="https://crontab.guru/">crontab.guru</a> is tremendously helpful for deciphering these!). For example, consider a cron schedule like:</p><pre># At every 5th minute</pre><pre>*/5 * * * *</pre><p>To instrument this cron, you might increment a counter metric every time the cron finishes (or use a <em>cron wrapper</em>). In your alerting system, you would then write a conditional query that says, “Look back through a 60 minute window, and alert me if the counter increased by less than 12”. Problem solved, right?</p><p>What if instead you had a cron schedule like:</p><pre># At minute 0 past every hour from 9 through 17 on every day-of-week from Monday through Friday.</pre><pre># in other words, “business hours” (9–5, Mon-Fri)</pre><pre>0 9–17 * * 1–5</pre><p>Now you need to get fancy with your query, or maybe your alerting system has some features that allow you to only be alerted during “business hours”. Regardless, these examples illustrate that coupling the cron schedule to the alert definition has several downsides:</p><ol><li>Changing the cron schedule means changing the alert.</li><li>Some cron schedules require complex time series queries to replicate.</li><li>Crons that don’t start exactly on time will require some amount of “grace period” to be built in to the query to minimize false positives.</li></ol><p>#2 alone makes generating default alerts for all crons on a platform a very difficult task, and #3 is especially pertinent to distributed platforms like Kubernetes CronJob where start delay is non-negligible. Alternatively, there are solutions that use <a href="https://en.wikipedia.org/wiki/Dead_man%27s_switch">dead man switches</a>, which still requires coupling the cron schedule to the alert, and/or anomaly detection algorithms, which require learning expectations over time and thus don’t work immediately for new CronJobs nor changes to a cron schedule.</p><p><strong>Another way of looking at the problem is to ask: what does it mean when a cron is supposed to run but hasn’t?</strong></p><p>In Kubernetes, barring bugs in the cronjobcontroller or your control-plane being down (the latter of which should be very obvious if you are monitoring your cluster correctly), this means that the cronjobcontroller evaluated the CronJob, determined (according to the cron schedule) that it needed to be invoked, <strong>yet still deliberately chose not to</strong>.</p><p>Sound familiar? This is exactly what our scheduling-decision.skip.count metric captures! Now, we only need to check for changes in rate(scheduling-decision.skip.count) in order to alert someone that a CronJob was supposed to run but hasn’t.</p><p>This solution decouples the cron schedule from the alert itself, which yields several advantages:</p><ul><li>No need to re-configure alerts when cron schedules change.</li><li>No complex time series queries and conditionals.</li><li>Easy to generate default alerts for all CronJobs on the platform.</li></ul><p>This, combined with the other time series and alerts mentioned previously, helps paint a more complete and easier to understand picture of a CronJob’s state.</p><h3>Implementing the start delay timer</h3><p>Due to the complex nature of CronJob life cycles, we needed to be precise about where in our stack we added instrumentation in order to accurately measure this metric. This boiled down to capturing 2 timestamps:</p><ul><li>T1: When the cron is expected to run (as dictated by the cron schedule).</li><li>T2: When the application code actually begins executing.</li></ul><p>Then, start delay = T2 — T1. For T1, we <a href="https://github.com/lyft/kubernetes/pull/24">added some code to the cron invocation logic</a> in the cronjobcontroller itself to write the expected start time as a .metadata.Annotation on Job objects that the cronjobcontroller creates when invoking a CronJob. Then we can consume it directly from any API client by issuing a basic GET Job request.</p><p>T2 is trickier to get right. Because we are interested in capturing the <em>tightest bound</em> of start delay, we want T2 to be the <strong>first time</strong> the application container starts running. If instead we recorded T2 at <em>any</em> application container start (including restarts), then our start delay would include application code execution time as well. To accomplish this, when we detected that the application container for a given Job transitioned to Running for the first time, we wrote another .metadata.Annotation to the Job object, essentially creating a distributed lock so that future application container starts for a given Job would be ignored, and only the timestamp of the <em>first</em> start would be recorded.</p><h3>Impact</h3><p>Since rolling out these features and bug fixes, we’ve received a lot of positive feedback from our developers. To summarize, developers using our Kubernetes CronJob platform:</p><ul><li>No longer need to maintain their own bespoke monitoring and alerts.</li><li>Can have high confidence that their CronJobs are working, and will be alerted when they are not.</li><li>Can easily recover from failures and test new CronJobs in this environment using our ad-hoc CronJob invoking tool.</li><li>Can understand the performance of their application code (using the app-container-duration.seconds timer metric).</li></ul><p>Additionally, platform engineers now have another dimension (<em>start delay</em>) for measuring the user experience and performance of the platform.</p><p>Finally (and perhaps the biggest win), by building richer observability to make CronJob state easier to reason about, developers and platform engineers can now debug issues together while looking at the same data, and more often than not, developers can diagnose and solve issues themselves all-together using the tools the platform provides.</p><h3>Conclusion</h3><p>Orchestrating distributed, scheduled tasks is hard. Kubernetes CronJobs are just one of many ways of doing so. While they are far from perfect, CronJobs can work at scale if you are willing to invest the time and effort into adding observability, understanding how they can fail, and building what your users need.</p><p><em>Lyft is hiring! If you’re passionate about Kubernetes and building infrastructure platforms, read more about them on our blog and </em><a href="https://grnh.se/99e060d22us"><em>join our team</em></a><em>!</em></p><p><em>Note: There is an</em><a href="https://github.com/kubernetes/enhancements/pull/978"><em> open Kubernetes Enhancement Proposal (KEP)</em></a><em> to address the shortcomings of CronJobs and graduate them to GA.</em></p><p><em>Big thanks to </em><a href="https://twitter.com/rithu_john"><em>Rithu John</em></a><em>, Scott Lau, </em><a href="https://twitter.com/scarlettp3rry"><em>Scarlett Perry</em></a><em>, Julien Silland, and </em><a href="https://tomwanielista.com"><em>Tom Wanielista</em></a><em> for their help in reviewing this blog series.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dad0c973ffca" width="1" height="1" alt=""><hr><p><a href="https://eng.lyft.com/how-we-learned-to-improve-kubernetes-cronjobs-at-scale-part-2-of-2-dad0c973ffca">How we learned to improve Kubernetes CronJobs at Scale (Part 2 of 2)</a> was originally published in <a href="https://eng.lyft.com">Lyft Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How we learned to improve Kubernetes CronJobs at Scale (Part 1 of 2)]]></title>
            <link>https://eng.lyft.com/improving-kubernetes-cronjobs-at-scale-part-1-cf1479df98d4?source=rss-9fdf8d77249c------2</link>
            <guid isPermaLink="false">https://medium.com/p/cf1479df98d4</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[cronjob]]></category>
            <category><![CDATA[k8s]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[lyft]]></category>
            <dc:creator><![CDATA[Kevin Yang]]></dc:creator>
            <pubDate>Mon, 03 Aug 2020 18:51:24 GMT</pubDate>
            <atom:updated>2020-08-05T19:07:22.119Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ilC1-HqK70BiS2lAO3CdkQ.jpeg" /></figure><p>At Lyft, we chose to move our server infrastructure onto Kubernetes, a distributed container orchestration system in order to take advantage of automation, have a solid platform we can build upon, and lower overall cost with efficiency gains.</p><p>Distributed systems can be difficult to reason about and understand, and Kubernetes is no exception. Despite the many benefits of Kubernetes, we discovered several pain points while adopting Kubernetes’ built-in <a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/">CronJob</a> as a platform for running repeated, scheduled tasks. In this two-part blog series, we will dive deep into the technical and operational shortcomings of Kubernetes CronJob at scale and share what we did to overcome them.</p><p>Part 1 (this article) of this series discusses in detail the shortcomings we’ve encountered using Kubernetes CronJob at Lyft. In part 2, we share what we did to address these issues in our Kubernetes stack to improve usability and reliability.</p><h4>Who is this for?</h4><ul><li>Users of Kubernetes CronJob</li><li>Anyone building a platform on top of Kubernetes</li><li>Anyone interested in running distributed, scheduled tasks on Kubernetes</li><li>Anyone interested in learning about Kubernetes usage at scale in the real-world</li><li>Kubernetes contributors</li></ul><h4>What will you gain from reading this?</h4><ul><li>Insight into how parts of Kubernetes (in particular, CronJob) behave at scale in the real-world.</li><li>Lessons learned from using Kubernetes as a platform at a company like Lyft, and how we addressed the shortcomings.</li></ul><h4>Prerequisites</h4><ul><li>Basic familiarity with the <a href="https://en.wikipedia.org/wiki/Cron">cron concept</a></li><li>Basic understanding of how <a href="https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/">CronJob works</a>, specifically the relationship between the CronJob controller, the Jobs it creates, and their underlying Pods, in order to better understand the CronJob deep-dives and comparisons with Unix cron later in this article.</li><li>Familiarity with the <a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/#understanding-pods">sidecar container pattern</a> and what it is used for. At Lyft, we make use of <a href="https://github.com/lyft/kubernetes/commit/ba9e7975957d61a7b68adb75f007c410fc9c80cc">sidecar container ordering</a> to make sure that runtime dependencies like Envoy, statsd, etc., packaged as sidecar containers, are up and running <em>prior to</em> the application container itself.</li></ul><h4>Background &amp; Terminology</h4><ul><li>The <a href="https://github.com/kubernetes/kubernetes/tree/master/pkg/controller/cronjob">cronjobcontroller</a> is the piece of code in the Kubernetes control-plane that reconciles CronJobs</li><li>A cron is said to be <em>invoked</em> when it is executed by some machinery (usually in accordance to its schedule)</li><li>Lyft Engineering operates on a platform infrastructure model where there is an infrastructure team (henceforth referred to as <strong>platform team</strong>, <strong>platform engineers</strong>, or <strong>platform infrastructure</strong>) and the customers of the platform are other engineers at Lyft (henceforth referred to as <strong>developers</strong>, <strong>service developers</strong>, <strong>users</strong>, or <strong>customers</strong>). Engineers at Lyft own, operate, and maintain what they build, hence “<strong>operat-</strong>” is used throughout this article.</li></ul><h3>CronJobs at Lyft</h3><p>Today at Lyft, we run nearly 500 cron tasks with more than 1500 invocations per-hour in our multi-tenant production Kubernetes environment.</p><p>Repeated, scheduled tasks are widely used at Lyft for a variety of use cases. Prior to adopting Kubernetes, these were executed using Unix cron directly on Linux boxes. Developer teams were responsible for writing their crontab definitions and provisioning the instances that run them using the <a href="https://eng.lyft.com/saltstack-as-an-alternative-to-terraform-for-aws-orchestration-cd2ceb06bf8c">Infrastructure As Code</a> (IaC) pipelines that the platform infrastructure team maintained.</p><p>As part of a larger effort to containerize and migrate workloads to our internal Kubernetes platform, we chose to adopt Kubernetes CronJob* to replace Unix cron as a cron executor in this new, containerized environment. Like many, we chose Kubernetes for many of its theoretical benefits, one of which is efficient resource usage.</p><p>Consider a cron that runs once a week for 15 minutes. In our old environment, the machine running that cron is sitting idle 99.85% of the time. With Kubernetes CronJob, compute resources (CPU, memory) are only used during the lifetime of a cron invocation. The rest of the time, Kubernetes can efficiently use those resources to run other CronJobs or scale down the cluster all together. Given the previous method for executing cron tasks, there was much to gain by transitioning to a model where jobs are made ephemeral.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*eIMxW7UBHj3BR30v" /><figcaption><em>The platform and developer ownership boundary in Lyft’s K8s stack</em></figcaption></figure><p>Since adopting Kubernetes as a platform, developer teams no longer provision and operate their own compute instances. Instead, the platform engineering team is responsible for maintaining and operating the compute resources and runtime dependencies used in our Kubernetes stack, as well as generating the Kubernetes CronJob objects themselves. Developers need only configure their cron schedule and application code.</p><p>This all sounds good on paper, but in practice, we discovered several pain points in moving crons away from the well-understood environment of traditional Unix cron to the distributed, ephemeral environment of Kubernetes using CronJob.</p><p>* <em>while CronJob was, and still is (as of Kubernetes v1.18), a beta API, we found that it fit the bill for the requirements we had at the time, and further, it fit in nicely with the rest of the Kubernetes infrastructure tooling we had already built.</em></p><h3>What’s so different about Kubernetes CronJob (versus Unix cron)?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/711/0*EoBumCpoCiWur8tU" /><figcaption><em>A simplified sequence of events and K8s software components involved in executing a Kubernetes CronJob</em></figcaption></figure><p>To better understand why Kubernetes CronJobs can be difficult to work with in a production environment, we must first discuss what makes CronJob different. Kubernetes CronJobs promise to <a href="https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/">run like cron tasks on a Linux or Unix system</a>; however, there are a few key differences in their behavior compared to a Unix cron: <strong>Startup Performance</strong> and <strong>Failure handling</strong>.</p><h4>Startup Performance</h4><p>We begin by defining <em>start delay </em>to be the wall time from expected cron start to application code actually executing. That is, if a cron is expected to run at 00:00:00, and the application code actually begins execution at 00:00:22, then the particular cron invocation has a start delay of 22 seconds.</p><p>Traditional Unix crons experience very minimal start delay. When it is time for a Unix cron to be invoked, the specified command <em>just runs</em>. To illustrate this, consider the following cron definition:</p><pre># run the date command at midnight every night</pre><pre>0 0 * * * date &gt;&gt; date-cron.log</pre><p>With this cron definition, one can expect the following output:</p><pre># date-cron.log</pre><pre>Mon Jun 22 00:00:00 PDT 2020</pre><pre>Tue Jun 23 00:00:00 PDT 2020</pre><p>On the other hand, Kubernetes CronJobs can <strong>experience significant start delays </strong>because they require several events to happen prior to any application code beginning to run. Just to name a few:</p><ol><li>cronjobcontroller processes and decides to invoke the CronJob</li><li>cronjobcontroller creates a Job out of the CronJob’s Job spec</li><li>jobcontroller notices the newly created Job and creates a Pod</li><li>Kubernetes admission controllers inject sidecar Container specs into the Pod spec*</li><li>kube-scheduler schedules the Pod onto a kubelet</li><li>kubelet runs the Pod (pulling all container images)</li><li>kubelet starts all sidecar containers*</li><li>kubelet starts the application container*</li></ol><p><em>* unique to Lyft’s Kubernetes stack</em></p><p>At Lyft, we found that start delay was especially compounded by #1, #5, and #7 once we reached a certain scale of CronJobs in our Kubernetes environment.</p><h4>Cronjobcontroller Processing Latency</h4><p>To better understand where this latency comes from, let’s dive into the source-code of the built-in cronjobcontroller. Through Kubernetes 1.18, the cronjobcontroller simply lists all CronJobs <a href="https://github.com/kubernetes/kubernetes/blob/3b1b2d469eacb2d45b112dbe1f4ed0a0e399d96a/pkg/controller/cronjob/cronjob_controller.go#L98-L99">every 10 seconds</a> and does some <a href="https://github.com/kubernetes/kubernetes/blob/3b1b2d469eacb2d45b112dbe1f4ed0a0e399d96a/pkg/controller/cronjob/cronjob_controller.go#L212-L216">controller logic</a> over each. The cronjobcontroller implementation does so synchronously, issuing at least 1 <a href="https://github.com/kubernetes/kubernetes/blob/3b1b2d469eacb2d45b112dbe1f4ed0a0e399d96a/pkg/controller/cronjob/cronjob_controller.go#L252">additional API call</a> for every CronJob. When the number of CronJobs exceeds a certain amount, these API calls begin to be<a href="https://github.com/kubernetes/kubernetes/blob/3b1b2d469eacb2d45b112dbe1f4ed0a0e399d96a/cmd/controller-manager/app/options/generic.go#L57-L58"> rate-limited client-side</a>. The latencies from the 10 second polling cycle and API client rate-limiting add up and contribute to a noticeable start-delay for CronJobs.</p><h4>Scheduling Cron Pods</h4><p>Due to the nature of cron schedules, most crons are expected to run at the top of the minute (XX:YY:00). For example, an @hourly<em> </em>cron is expected to execute at 01:00:00, 02:00:00, and so on. In a multi-tenant cron platform with lots of crons scheduled to run every hour, every 15 minutes, every 5 minutes, etc., this produces <strong>hot-spots</strong> where lots of crons need to be invoked simultaneously. At Lyft, we noticed that one such hot spot is the top of the hour (XX:00:00). These hot-spots can put strain on and expose additional client-side rate-limiting in control-plane components involved in the happy-path of CronJob execution like the kube-scheduler and kube-apiserver causing start delay to increase noticeably.</p><p>Additionally, if you do not provision compute for peak demand (and/or use a cloud-provider for compute instances) and instead use something like<a href="https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler"> cluster autoscaler</a> to dynamically scale nodes, then node launch times can contribute additional delays to launching CronJob Pods.</p><h4>Pod Execution: Non-application Containers</h4><p>Once a CronJob Pod has successfully scheduled onto a kubelet, the kubelet needs to pull and execute the container images of all sidecars and the application itself. Due to the way Lyft uses sidecar ordering to gate application containers, if any of these sidecar containers are slow to start, or need to be restarted, they will propagate additional start delay.</p><p>To summarize, each of these events that happen prior to application code actually executing combined with the scale of CronJobs in a multi-tenant environment can introduce noticeable and unpredictable start delay. As we will see later on, this start delay can negatively affect the behavior of a CronJob in the real-world by causing CronJobs to miss runs.</p><h3>Container Failure handling</h3><p>It is good practice to monitor the execution of crons. With Unix cron, doing so is fairly straight-forward. Unix crons interpret the given command with the specified $SHELL, and, when the command exits (whether successful or not), that particular invocation is done. One rudimentary way of monitoring a Unix cron then is to introduce a command-wrapper script like so:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e83d72d76221ce17bd65c607298f1bf0/href">https://medium.com/media/e83d72d76221ce17bd65c607298f1bf0/href</a></iframe><p>With Unix cron, stat-and-log will be executed exactly once per complete cron invocation, regardless of the $exitcode. One can then use these metrics for simple alerts on failed executions.</p><p>With Kubernetes CronJob, where there are retries on failures by default and an execution can have multiple failure states (Job failure and container failure), monitoring is not as straightforward.</p><p>Using a similar script in an application container and with Jobs configured to restart on failure, a CronJob will instead repeatedly execute and spew metrics and logs up to a <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#jobspec-v1-batch">BackoffLimit</a> number of times on failure, introducing lots of noise to a developer trying to debug it. Additionally, a naive alert using the first failure from the wrapper script can be un-actionable noise as the application container may recover and complete successfully on its own.</p><p>Alternatively, you could alert at the Job level instead of the application container level using an API-layer metric for Job failures like<a href="https://github.com/kubernetes/kube-state-metrics/blob/master/docs/job-metrics.md"> kube_job_status_failed</a> from<a href="https://github.com/kubernetes/kube-state-metrics"> kube-state-metrics</a>. The drawback of this approach is that an on-call won’t be alerted until the Job has reached the terminal failure state once BackoffLimit has been reached, which can be much later than the first application container failure.</p><h3>What causes CronJobs to fail intermittently?</h3><p>Non-negligible start delay and retry-on-failure loops contribute additional delay that can interfere with the repeated execution of Kubernetes CronJobs. For frequent CronJobs, or those with long application execution times relative to idling time, this additional delay can carry over into the next scheduled invocation. If the CronJob has ConcurrencyPolicy: Forbid set to <a href="https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/#concurrency-policy">disallow concurrent runs</a>, then this carry-over causes future invocations to not execute on-time and get backed up.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Dx9KHcKNb_zT63i2" /><figcaption><em>Example timeline (from the perspective of the </em><em>cronjobcontroller) where </em><em>startingDeadlineSeconds is exceeded for a particular hourly CronJob — the CronJob misses its run and won’t be invoked until the next scheduled time</em></figcaption></figure><p>A more sinister scenario that we observed at Lyft where CronJobs can miss invocations entirely is when a CronJob has <a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations">startingDeadlineSeconds</a> set. In that scenario, when start delay exceeds the startingDeadlineSeconds, the CronJob will miss the run entirely. Additionally, if the CronJob also has ConcurrencyPolicy set to Forbid, a previous invocation’s retry-on-failure loop can also delay the next invocation, causing the CronJob to miss as well.</p><h3>The Real-world operational burden of Kubernetes CronJobs</h3><p>Since beginning to move these repeated, scheduled tasks onto Kubernetes, we found that using CronJob out-of-the-box introduced several pain-points from both the developers’ and the platform team’s points of view that began to negate the benefits and cost-savings we initially chose Kubernetes CronJob for. We soon realized that neither our developers nor the platform team were equipped with the necessary tools for operating and understanding the complex life cycles of CronJobs.</p><p><strong>Developers at Lyft came to us with lots of questions and complaints</strong> when trying to operate and debug their Kubernetes CronJobs like:</p><ul><li><em>“Why isn’t my cron running?”</em></li><li><em>“I think my cron stopped running. How can I tell if my cron is actually running?”</em></li><li><em>“I didn’t know the cron wasn’t running, I just assumed it was!”</em></li><li><em>“How do I remedy X failed cron? I can’t just ssh in and run the command myself.”</em></li><li><em>“Can you explain why this cron seemed to miss a few schedules between X and Y [time periods]?”</em></li><li><em>“We have X (large number) of crons, each with their own alarms, and it’s becoming tedious/painful to maintain them all.”</em></li><li><em>“What is all this Job, Pod, and sidecar nonsense?”</em></li></ul><p><strong>As a platform team</strong>, we were not equipped to answer questions like:</p><ul><li><em>How do we quantify the performance characteristics of our Kubernetes Cron platform?</em></li><li><em>What is the impact of on-boarding more CronJobs onto our Kubernetes environment?</em></li><li><em>How does running multi-tenant Kubernetes CronJobs perform compared to single-tenant Unix cron?</em></li><li><em>How do we begin to define Service-Level-Objectives (SLOs) to communicate with our customers?</em></li><li><em>What do we monitor and alarm on as platform operators to make sure platform-wide issues are tended to quickly with minimal impact on our customers?</em></li></ul><p>Debugging CronJob failures is no easy task, and often requires an intuition for where failures happen and where to look to find proof. Sometimes this evidence can be difficult to dig up, such as logs in the cronjobcontroller which are only logged at a high verbosity log-level. Or, the traces simply disappear after a certain time period and make debugging a game of “whack-a-mole”, such as Kubernetes Events on the CronJob, Job, and Pod objects themselves, which are only retained for one hour by default. None of these methods are easy to use, and do not scale well from a support point-of-view with more and more CronJobs on the platform.</p><p>In addition, sometimes Kubernetes would just <strong>quit</strong> when a CronJob had missed too many runs, requiring someone to manually “un-stick” the CronJob. This happens <a href="https://github.com/kubernetes/kubernetes/issues/42649">in real-world usage</a> more often than you would think, and became painful to remedy manually each time.</p><p>This concludes<em> </em>the dive into the technical and operational issues we’ve encountered using Kubernetes CronJob at scale. In <a href="https://eng.lyft.com/how-we-learned-to-improve-kubernetes-cronjobs-at-scale-part-2-of-2-dad0c973ffca">Part 2</a> we share what we did to address these issues in our Kubernetes stack to improve the usability and reliability of CronJobs.</p><p><em>As always, Lyft is hiring! If you’re passionate about Kubernetes and building infrastructure platforms, read more about them on our blog and </em><a href="https://www.lyft.com/careers"><em>join our team</em></a><em>!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cf1479df98d4" width="1" height="1" alt=""><hr><p><a href="https://eng.lyft.com/improving-kubernetes-cronjobs-at-scale-part-1-cf1479df98d4">How we learned to improve Kubernetes CronJobs at Scale (Part 1 of 2)</a> was originally published in <a href="https://eng.lyft.com">Lyft Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>