13 Jobs and Cron Jobs Why Not Use Pods Directly for Business Processes

13 Jobs and CronJobs Why Not Use Pods Directly for Business Processes #

Hello, I am Chrono.

In the last lesson, we learned about the core Kubernetes object Pod, which is used to orchestrate one or more containers, allowing them to share network and storage resources, and always schedule together to closely collaborate.

Because Pods are more capable of representing actual applications than containers, Kubernetes does not orchestrate business at the container level, but rather uses Pods as the smallest unit for scheduling and operation in the cluster.

Earlier, we also saw a diagram of Kubernetes resource objects, with Pods at the center, extending to many other resource objects representing various businesses. So you might wonder: since the functionality of Pods is already sufficient, why define these additional objects? Why not directly add functionality to Pods to handle business requirements?

This question reflects Google’s deep thinking on managing large-scale computing clusters. Today, I will talk about the design philosophy of Kubernetes based on Pods, starting with the simplest two objects: Job and CronJob.

Why not use Pod directly #

By now, you should know that Kubernetes uses RESTful API to abstract various business aspects in the cluster as HTTP resource objects. At this level, we can consider the problem in an object-oriented way.

If you have some programming experience, you would know about object-oriented programming (OOP), which treats everything as cohesive objects and emphasizes communication between objects to accomplish tasks.

Although object-oriented design thinking is mostly used in software development, it surprisingly fits well in Kubernetes. This is because Kubernetes uses YAML to describe resources, simplifying business into individual objects with internal attributes, external connections, and the need for collaboration. However, we don’t need to code everything as Kubernetes handles it automatically (in fact, Kubernetes heavily utilizes object-oriented concepts in its Go language implementation).

There are several fundamental principles in object-oriented design, two of which I believe appropriately describe Kubernetes’ object design approach: “Single Responsibility” and “Composition over Inheritance”.

“Single Responsibility” means that an object should only focus on doing one thing well, avoiding being overly ambitious, and keeping a small enough granularity for better reusability and management.

“Composition over Inheritance” means that objects should ideally establish connections during runtime, remain loosely coupled, and avoid fixing object relationships through hardcoded means.

Applying these two principles, we can better understand Kubernetes’ resource objects. Since the Pod is already a relatively complete object responsible for managing containers, we should not blindly complicate it by adding unnecessary functionality. Instead, we should maintain its independence, and define other objects for functionalities outside of containers, “composing” the Pod as a member of those objects.

By doing so, each Kubernetes object can focus solely on its own business domain, doing only what it excels at, while leaving other tasks to other objects. This approach neither results in a “missing position” nor an “overstepped position”, as it allows for division of labor and collaboration, thus achieving maximum benefits at minimal costs.

Why do we need Jobs/CronJobs? #

Now let’s take a look at two new objects in Kubernetes: Job and CronJob. They combine Pods to handle offline tasks.

In the previous class, we ran two Pods: Nginx and busybox, which represent the two major types of workloads in Kubernetes. One type is “online workloads” like Nginx, Node.js, MySQL, Redis, etc., which run continuously and are always online. The other type is “offline workloads” like busybox, which run for a short period of time and don’t directly serve external users. These offline workloads are mainly for internal purposes such as log analysis, data modeling, video transcoding, etc. Although they require a large amount of computation, they only run for a certain period of time. The characteristic of offline workloads is that they will eventually exit and have different scheduling strategies compared to online workloads. Offline workloads require considerations such as runtime timeouts, status checks, failure retries, and obtaining computation results.

However, these workload characteristics are not necessarily related to container management. If we implement them using Pods, it would burden the Pods with unnecessary responsibilities, violating the principle of “single responsibility”. Therefore, we should separate these functionalities into another object to control the execution of Pods and complete the additional work.

Offline workloads can also be divided into two types: “temporary tasks” that are executed once and then completed, and “scheduled tasks” that can be executed periodically without much intervention.

In Kubernetes, “temporary tasks” are represented by the API object Job, and “scheduled tasks” are represented by the API object CronJob. With these two objects, you can schedule and manage any type of offline workload in Kubernetes.

Since both Job and CronJob belong to offline workloads, they are quite similar. Let’s first learn about Jobs, which usually run only once, and how to operate them.

How to Use YAML to Describe a Job #

The “header” of a Job’s YAML file still consists of several required fields, and I won’t repeat the explanations. Instead, I’ll briefly mention them:

  • apiVersion is not v1, but batch/v1.
  • kind is Job, which is consistent with the object name.
  • metadata still needs to include name for specifying the name, and you can also add arbitrary labels using labels.

If you can’t remember these, it’s alright. You can use the command kubectl explain job to view the field descriptions. However, if you want to generate a YAML template file, you can’t use kubectl run because it can only create Pods. To create API objects other than Pods, you need to use the kubectl create command together with the object type.

For example, to create a “echo-job” using busybox, the command is as follows:

export out="--dry-run=client -o yaml"              # Define a shell variable
kubectl create job echo-job --image=busybox $out

This will generate a basic YAML file. After saving it and making some modifications, you’ll have a Job object:

apiVersion: batch/v1
kind: Job
metadata:
  name: echo-job

spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
      - image: busybox
        name: echo-job
        imagePullPolicy: IfNotPresent
        command: ["/bin/echo"]
        args: ["hello", "world"]

You will notice that the description of a Job is similar to that of a Pod, but with some differences. The main difference lies in the “spec” field, which includes an additional template field that contains another “spec”. This might appear a bit odd.

If you understand the principles of object-oriented design that I mentioned earlier, you will understand the logic behind this approach. It actually applies the composite pattern in the Job object. The template field defines an “application template”, which contains a Pod. This way, a Job can create Pods based on this template.

Since this Pod is managed by the Job and doesn’t directly interact with the API server, it doesn’t need to repeat the “header fields” such as apiVersion. It only needs to define the key spec to describe the container-related information. It can be considered as a “headless” Pod object.

To help you understand this, I reorganized the Job object and used different colors to distinguish the fields. This way, you can easily see that this “echo-job” doesn’t have too many additional functionalities. It simply wraps a Pod:

Image

In summary, the workload of this Pod is very simple. In the containers section, we specify the name and image of the container, use the command to execute /bin/echo, and output “hello world”.

However, due to the specific nature of Jobs, we need to add another field restartPolicy in the spec to determine the behavior when the Pod fails. OnFailure means the container will be restarted in place, while Never means the container won’t be restarted and the Job will be rescheduled to create a new Pod.

How to Operate Jobs in Kubernetes #

Now let’s create a Job object and run this simple offline task using the kubectl apply command:

```sh
kubectl apply -f job.yml

Once created, Kubernetes will extract the Pod from the YAML template definition and run the Pod under the control of the Job. You can use kubectl get job and kubectl get pod to check the status of the Job and Pod respectively:

kubectl get job
kubectl get pod

Image

As you can see, because the Pod is managed by the Job, it will not restart repeatedly and show errors. Instead, it will display Completed to indicate that the task is finished. The Job will also list the number of successfully run jobs, in this case, there is only one job, so it is 1/1.

You can also see that the Pod is automatically assigned a name, which is the name of the Job (echo-job) plus a random string (pb5gh). This is the “credit” of Job management, saving us from manual definition. Now we can use the command kubectl logs to obtain the running result of the Pod:

Image

At this point, you may think that after two layers of encapsulation with Job and Pod, although the concept is clear, it does not seem to bring any practical benefits and is not much different from running containers directly.

In fact, Kubernetes’ YAML description object framework provides a lot of flexibility, allowing you to add arbitrary fields at the Job level or Pod level to customize the business process. This advantage is unparalleled compared to simple container technology.

Here are some important fields for controlling offline jobs. For more detailed information, you can refer to the Job documentation:

  • activeDeadlineSeconds: Set the timeout for Pod execution.
  • backoffLimit: Set the number of retries in case of Pod failure.
  • completions: Specify how many Pods need to be run to complete the Job. The default value is 1.
  • parallelism: It is related to completions and represents the number of Pods that can run concurrently to avoid excessive resource occupation.

Note that these four fields are not under the template field, but under the spec field. Therefore, they belong to the Job level and are used to control the Pod objects in the template.

Next, I will create another Job object named “sleep-job”, which randomly sleeps for a period of time and then exits, simulating a long-running job like MapReduce. The Job parameters are set to a 15-second timeout, a maximum of 2 retries, a total of 4 Pods to be run, and a maximum of 2 Pods running concurrently at the same time:

apiVersion: batch/v1
kind: Job
metadata:
  name: sleep-job

spec:
  activeDeadlineSeconds: 15
  backoffLimit: 2
  completions: 4
  parallelism: 2

  template:
    spec:
      restartPolicy: OnFailure
      containers:
      - image: busybox
        name: echo-job
        imagePullPolicy: IfNotPresent
        command:
          - sh
          - -c
          - sleep $(($RANDOM % 10 + 1)) && echo done

After creating the Job using kubectl apply, we can use kubectl get pod -w to observe the status of Pods in real-time, seeing the process of Pods queuing, creating, and running:

kubectl apply -f sleep-job.yml
kubectl get pod -w

Image

When all four Pods have completed running, we can use kubectl get to see the status of the Job and the Pods:

Image

You will see that the completion quantity of the Job is as expected, 4, and all four Pods are in the completed state.

Clearly, the “declarative” Job object makes the description of offline jobs very intuitive. With just a few simple fields, you can control the parallelism and completion quantity of the job without the need for manual monitoring and intervention. Kubernetes automates all of this.

How to use YAML to describe CronJob #

After learning about the “one-off tasks” with Job objects, it is relatively easy to learn about the “scheduled tasks” with CronJob objects. I will directly use the kubectl create command to create a template for CronJob.

Two things to note: first, because the name of CronJob is a bit long, Kubernetes provides the abbreviation “cj”, which can also be seen using the kubectl api-resources command; second, CronJob needs to run on a schedule, so we also need to specify the --schedule parameter in the command line.

export out="--dry-run=client -o yaml"              # Define shell variable
kubectl create cj echo-cj --image=busybox --schedule="" $out

Then we edit this YAML template to generate the CronJob object:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: echo-cj

spec:
  schedule: '*/1 * * * *'
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - image: busybox
            name: echo-cj
            imagePullPolicy: IfNotPresent
            command: ["/bin/echo"]
            args: ["hello", "world"]

We still focus on its spec field. You will find that it has three nested spec levels:

  • The first spec is the specification declaration of CronJob itself.
  • The second spec is a subordinate of “jobTemplate”, which defines a Job object.
  • The third spec is a subordinate of “template”, which defines the Pod running in the Job.

Therefore, CronJob is actually a new object generated by combining Job. I have also drawn a diagram to help you understand its nesting structure:

Image

In addition to the “jobTemplate” field that defines the Job object, CronJob also has a new field called “schedule”, which is used to define the rules for the task to run periodically. It uses standard Cron syntax to specify minutes, hours, days, months, and weeks, just like crontab on Linux. Here, I have specified to run every minute. You can refer to the Kubernetes official documentation for the specific meaning of the format.

Apart from the different names, the usage of CronJob is almost the same as that of Job. Use kubectl apply to create CronJob and use kubectl get cj and kubectl get pod to check the status:

kubectl apply -f cronjob.yml
kubectl get cj
kubectl get pod

Image

Summary #

Alright, today we analyzed the design of resource objects in Kubernetes from an object-oriented perspective, emphasizing “single responsibility” and “object composition”, in other words, “objects nesting objects”.

Through this nesting approach, these API objects in Kubernetes form a “chain of control”:

CronJobs control Jobs using scheduling rules, Jobs control Pods using concurrency control, Pods define parameters to control containers, containers isolate and control processes, and processes ultimately implement business functionalities. This progressive structure is somewhat reminiscent of the Decorator design pattern in software engineering, with each link in the chain fulfilling its own role and completing tasks under the unified command of Kubernetes.

To summarize today’s content:

  1. Pods are the smallest scheduling unit in Kubernetes, but to maintain their independence, unnecessary functionalities should not be added to them.
  2. Kubernetes provides two API objects, Job and CronJob, for offline tasks and scheduled tasks respectively.
  3. The key field of a Job is spec.template, which defines the Pod template used to run the tasks. Other important fields include completions, parallelism, etc.
  4. The key fields of a CronJob are spec.jobTemplate and spec.schedule, which respectively define the Job template and the scheduling rules for periodic execution.

Homework #

Finally, it’s homework time. I have two questions for you to ponder:

  1. How do you understand the way Kubernetes combines objects? What advantages does it bring?
  2. What are the specific application scenarios for Job and CronJob? What kind of problems can they solve?

Feel free to share your questions and learning experiences in the comments section. If you find it valuable, you are also welcome to share it with your friends for learning together.

See you in the next class.

Image