← Kỹ sư dữ liệu← Data Engineer
Kỹ sư dữ liệuData Engineer19 Th7, 2026Jul 19, 202620 phút đọc16 min read

Container & OrchestrationContainers & Orchestration

Thuộc bộ kiến thức Data Engineer Roadmap.

Tổng quan

Một data pipeline chạy hoàn hảo trên laptop rồi âm thầm fail khi lên production là một trong những sự cố phổ biến — và cũng dễ tránh nhất — trong nghề này. Thủ phạm gần như luôn là environment drift: một phiên bản Python khác, thiếu một system library mà pandas hay JDBC driver cần, một locale khiến việc parse ngày tháng ra kết quả khác, một version Spark không khớp hoàn toàn với version trên cluster. Container ra đời để loại bỏ hẳn nhóm lỗi này bằng cách đóng gói code cùng với đúng runtime nó cần, để cùng một artifact chạy không đổi từ máy developer, qua CI, đến production. Ghi chú này giả định bạn đã hiểu container là gì — namespace, cgroup, image, layer, cú pháp Dockerfile — những nội dung đó được trình bày đầy đủ trong DevOps — Containers. Nội dung ở đây bổ sung góc nhìn của data engineer về cùng công nghệ đó: phần nào trong công việc của bạn thực sự cần containerize, vì sao Kubernetes nói riêng trở thành nơi mặc định để chạy các data workload (chứ không chỉ để đóng gói chúng), và một số cách mà data workload gây áp lực lên nền tảng container khác hẳn so với một web service stateless thông thường.

Kubernetes xuất hiện trong thế giới của data engineer ở ba vai trò khác nhau, và nên tách bạch chúng ngay từ đầu. Thứ nhất, đó là nơi để chạy chính code pipeline của bạn — một script Python được containerize, một dbt project, một custom transformation job — dưới dạng một Job theo lịch hoặc một task bên trong orchestrator. Thứ hai, nó ngày càng trở thành execution engine cho chính các framework tính toán phân tán: Spark, Flink, Ray giờ đều chạy native dưới dạng Pod do Kubernetes scheduler quản lý, thay vì trên cluster manager riêng của chúng. Thứ ba, nó là nền tảng mà chính orchestrator chạy trên đó — Airflow, Dagster và các công cụ tương tự thường được deploy bên trong Kubernetes, và thậm chí có thể dùng Kubernetes làm backend thực thi task. Hiểu vai trò nào áp dụng cho vấn đề cụ thể nào là phần lớn những gì bạn cần để suy luận tốt về chủ đề này; cơ chế của bản thân Kubernetes được trình bày đầy đủ trong bộ kiến thức Kubernetes và sẽ không lặp lại ở đây.

Kiến thức nền tảng

Containerize code pipeline

Kỹ năng nền tảng khá đơn giản: lấy một transformation script, một dbt project, hoặc một ETL job nhỏ, và đóng gói nó vào một Docker image để nó mang theo đúng phiên bản interpreter, các system library (libpq, ODBC driver, libgomp cho các thư viện tính toán số), và các phiên bản package Python/R đã được pin. Ví dụ tối giản cho một Python transformation job:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["python", "run_pipeline.py"]

Toàn bộ kỹ thuật viết Dockerfile tốt — multi-stage build, layer caching, non-root user, image scanning — không đặc thù cho data engineering và đã được trình bày chi tiết trong DevOps — Containers; áp dụng ở đây y hệt như với bất kỳ service nào khác. Điều đặc thù cho data pipeline là:

Spark trên Kubernetes so với standalone và YARN

Trong nhiều năm, đích deploy mặc định của Apache Spark là một cluster YARN riêng (kế thừa từ hệ sinh thái Hadoop) hoặc cluster manager standalone nhẹ của chính Spark. Cả hai đều hoạt động tốt, nhưng cả hai đều nghĩa là data engineering phải sở hữu và capacity-plan một fleet máy riêng, tĩnh, chỉ để chạy Spark — được size theo tải đỉnh, phần lớn thời gian idle, và tách biệt về mặt vận hành với bất kỳ nền tảng nào đang chạy phần còn lại của các service trong tổ chức.

Chạy Spark trên Kubernetes (qua hỗ trợ có sẵn spark-submit --master k8s://..., hoặc Spark Operator nhiều tính năng hơn) thay đổi điều này theo vài cách cụ thể:

Đây là một trường hợp cụ thể của một mẫu hình rộng hơn nhiều được trình bày trong Big Data & Distributed Computing: các engine tính toán phân tán ngày càng ủy quyền việc lập lịch và quản lý tài nguyên cho Kubernetes thay vì tự tái triển khai nó, giống như trước đây chúng từng ủy quyền cho YARN.

Managed Kubernetes cho nền tảng dữ liệu

Vận hành Kubernetes tốt — vá control plane, quản lý etcd, xử lý nâng cấp version — là một gánh nặng vận hành chuyên biệt, liên tục, và không liên quan gì đến bản thân data engineering. Với một team nền tảng dữ liệu, lựa chọn thực dụng mặc định là một control plane managed: Amazon EKS hoặc Google Cloud GKE (Azure AKS là tương đương trên cloud đó). Nhà cung cấp vận hành và vá control plane cùng etcd; team của bạn sở hữu node pool, workload, và các data service bạn deploy lên trên. Chế độ Autopilot của GKE đi xa hơn một bước và quản lý cả việc cấp phát node, tính phí theo Pod thay vì theo node — hấp dẫn với các team muốn có API và mô hình lập lịch của Kubernetes mà không phải sở hữu vòng đời node chút nào.

Với một team dữ liệu cụ thể, điều này quan trọng hơn so với một team backend thông thường, bởi vì mục đích của việc áp dụng Kubernetes cho data workload thường là để đưa Spark, Airflow, và các custom service lên một hạ tầng dùng chung, co giãn được — chứ không phải để trở thành một platform team Kubernetes tay ngang. Tự host Kubernetes chỉ để chạy data job nghĩa là gánh thêm cả một chuyên môn thứ hai (vận hành cluster) chồng lên chuyên môn bạn đã phải chịu trách nhiệm (dữ liệu). Cơ chế chi tiết của các dịch vụ managed, node pool, tích hợp IAM, và kiến trúc cluster nằm trong bộ kiến thức KubernetesCloud Data Platform; bài học cho data engineering đơn giản là: ưu tiên control plane managed trừ khi có lý do cụ thể, được hiểu rõ để không làm vậy.

GitOps cho hạ tầng dữ liệu

Khi footprint Kubernetes của một nền tảng dữ liệu lớn lên vượt quá vài lệnh kubectl apply thủ công — bản deploy của chính Airflow, một loạt template job Spark, các custom microservice nuôi pipeline — nó cần cùng kỷ luật mà bất kỳ hạ tầng production nào khác cần: một nguồn chân lý duy nhất cho những gì nên đang chạy, và một cách tự động để đưa cluster hội tụ về đúng như vậy. Đây chính xác là những gì GitOps (Argo CD, Flux) cung cấp, và nó áp dụng cho hạ tầng dữ liệu không có ngoại lệ đặc biệt nào: Helm values của Airflow, manifest job Spark, và Deployment của các custom service đều nằm trong Git, và một agent trong cluster reconcile cluster đang chạy để khớp với đó. Cơ chế đầy đủ — Argo CD so với Flux, app-of-apps, xử lý secret, promote giữa các môi trường — được trình bày trong DevOps — GitOps và áp dụng ở đây không đổi. Điểm đặc thù cho dữ liệu đáng lưu ý: các job chạy một lần và migration (một lượt Spark backfill, một schema migration, một lượt reprocessing một lần) không khớp gọn với mô hình “reconcile mãi mãi” thuần túy, vì chúng nên chạy một lần rồi ngừng là trạng thái mong muốn — các công cụ GitOps xử lý việc này bằng sync hook hoặc bằng cách coi chúng là tài nguyên Job với điều kiện hoàn thành rõ ràng, chứ không phải như các Deployment chạy dài hạn.

Khái niệm chính

Containerize chính orchestrator: KubernetesExecutor của Airflow

Rất dễ nhầm lẫn giữa “Airflow chạy pipeline” với “Airflow chạy trên Kubernetes” — hai quyết định liên quan nhưng khác nhau, và quyết định thứ hai có những đánh đổi riêng đáng hiểu rõ (nội dung orchestrator đầy đủ nằm trong Workflow Orchestration). Bản thân Airflow có thể được deploy trên Kubernetes (qua Helm chart chính thức của cộng đồng), và độc lập với đó, nó có thể được cấu hình để thực thi task bằng một trong nhiều executor:

ExecutorTask chạy như thế nàoCách ly
SequentialExecutorTừng task một, trong cùng processKhông có — chỉ dùng cho dev/test
LocalExecutorCác subprocess song song trên chính máy schedulerTài nguyên máy dùng chung; không có giới hạn theo từng task
CeleryExecutorTask được phân phối tới một pool cố định các Celery worker process chạy dài hạnWorker dùng chung giữa các task; được size theo trường hợp xấu nhất
KubernetesExecutorMỗi task instance khởi chạy thành một Pod Kubernetes riêng, rồi Pod đó kết thúcCách ly hoàn toàn theo từng task: image riêng, resource requests/limits CPU/memory riêng

KubernetesExecutor phổ biến trong các nền tảng dữ liệu chính bởi vì pipeline workload không đồng nhất theo cách mà hầu hết web service không gặp phải: một task trong DAG có thể là một cuộc gọi poll API nhẹ chỉ cần 100Mi bộ nhớ, và task tiếp theo trong cùng DAG đó có thể là một job pandas load một dataframe nhiều gigabyte cần đến 16Gi. Pool worker cố định của CeleryExecutor phải được size theo task lớn nhất mà bất kỳ DAG nào có thể từng chạy, lãng phí capacity ở mọi task nhỏ hơn; KubernetesExecutor thay vào đó yêu cầu đúng lượng tài nguyên mỗi task khai báo, được lập lịch độc lập, và scale node pool bên dưới (qua cluster autoscaler hoặc Karpenter) theo đúng nhu cầu thực tế của từng task thay vì một pool cố định theo trường hợp xấu nhất. Cái giá phải trả là độ trễ khởi động Pod cho từng task (thường vài giây) và việc pull image cho mỗi task trừ khi node đã được làm ấm sẵn — một đánh đổi thực sự so với việc Celery nhận task gần như tức thì trên một worker đã chạy sẵn, đó là lý do một số team chạy hybrid: KubernetesExecutor cho các job nặng, bất thường và một pool Celery nhỏ cho các task nhẹ, tần suất cao.

Resource requests và limits cho data workload

Việc tinh chỉnh resource Kubernetes cho một web API stateless và cho một job Spark hay một tác vụ batch dựa trên pandas trông chẳng giống nhau chút nào, và đối xử với chúng như nhau là một nguồn sự cố production phổ biến:

Khía cạnhWeb service điển hìnhData workload (Spark executor / job pandas)
Hồ sơ bộ nhớBằng phẳng, dễ đoán, nhỏ (vài trăm MB)Nhảy vọt, lớn, khó đoán — có thể tăng vài GB giữa chừng khi một shuffle hoặc một groupby materialize
Hồ sơ CPUỔn định, theo requestBùng nổ từng đợt — gần như bằng 0 khi chờ I/O, rồi kịch trần khi shuffle hoặc ở giai đoạn tính toán nặng
Hệ quả khi cấp thiếuPhản hồi chậm, có thể bị throttleOOMKilled — container bị kill thẳng, không phải throttle, và cả task (có thể hàng giờ công việc) bị mất
Cách right-sizeMetric ở trạng thái ổn định qua nhiều ngày/tuầnProfile chính job cụ thể (metric executor từ Spark UI, memory profiling của pandas) — nhu cầu bộ nhớ của job này hiếm khi áp dụng chung cho job khác
Chi phí restartRẻ — vài giây, không mất stateĐắt — một Spark executor bị kill kích hoạt tính toán lại task; một batch job bị kill có thể phải chạy lại từ checkpoint gần nhất hoặc từ đầu

Hệ quả thực tế: đặt memory limit hào phóng và dựa trên profiling thực tế, không phải đoán mò, bởi vì một data workload chạm memory limit không suy giảm nhẹ nhàng — OOM killer của kernel kill thẳng container (hiển thị dưới dạng OOMKilled trong kubectl describe pod), và tùy framework, điều đó có thể âm thầm retry, âm thầm drop kết quả một phần, hoặc fail cả job. Riêng với Spark, nhớ rằng memory limit của một Pod phải tính đến nhiều hơn heap của JVM: bộ nhớ off-heap, overhead của chính JVM, và (quan trọng) bất kỳ UDF Python hay workload pandas-on-Spark nào chạy trong một process Python riêng biệt song song với JVM đều tính vào tổng memory limit của container — size container chỉ theo spark.executor.memory và quên mất spark.executor.memoryOverhead là một trong những nguyên nhân phổ biến nhất khiến Spark executor bị OOMKilled trên Kubernetes.

Ephemeral storage cho các tập dữ liệu trung gian lớn

Web service hiếm khi ghi một lượng dữ liệu đáng kể xuống đĩa cục bộ; data workload thì làm việc đó liên tục. Một shuffle của Spark spill dữ liệu trung gian xuống đĩa cục bộ khi không vừa bộ nhớ; một job pandas hay Polars đọc một file lớn có thể materialize các file Parquet hoặc CSV tạm thời; một lượt chạy dbt có thể stage các bảng trung gian. Trên Kubernetes, không gian scratch cục bộ này là ephemeral storage — hệ thống file ghi được của container cộng với bất kỳ volume emptyDir nào — và đó là một tài nguyên phải được request và limit y hệt như CPU và memory:

resources:
  requests:
    memory: "8Gi"
    cpu: "2"
    ephemeral-storage: "20Gi"
  limits:
    memory: "10Gi"
    cpu: "4"
    ephemeral-storage: "40Gi"
volumes:
  - name: scratch
    emptyDir:
      sizeLimit: 40Gi

Nếu không có request và limit tường minh, một Spark job nặng shuffle hoặc một lượt ghi trung gian lớn có thể âm thầm lấp đầy đĩa của một node, lúc đó kubelet bắt đầu evict Pod — kể cả các Pod không liên quan gì đến Pod gây ra vấn đề — để giải phóng không gian, đây là một kiểu lỗi ồn ào và khó chẩn đoán hơn nhiều so với một OOMKill thông thường. Hãy mount volume scratch emptyDir với sizeLimit tường minh, ưu tiên các node có NVMe cục bộ nhanh cho các job Spark nặng shuffle khi nhà cung cấp cloud có hỗ trợ, và giám sát mức dùng ephemeral storage nghiêm túc như với memory — đây là chiều tài nguyên mà data workload dễ dùng cạn nhất trong khi web service hầu như không bao giờ nghĩ tới.

Tóm tắt: điều gì khác biệt khi data workload chạy trên container/Kubernetes

Mối quan tâmCân nhắc đặc thù cho data engineeringĐi sâu hơn
Đóng gói code pipelinePin phiên bản data library chính xác; bake các native dependency nặng (JVM Spark, GDAL, CUDA) vào base image dùng chungDevOps — Containers
Lập lịch tính toánSpark/Flink/Ray chạy như Pod native của Kubernetes thay vì trên YARN/standalone — executor co giãn, một hạ tầng dùng chungBig Data & Distributed Computing
Vận hành control planeƯu tiên EKS/GKE managed thay vì tự host Kubernetes — đừng gánh vận hành cluster chỉ để chạy data jobBộ kiến thức Kubernetes, Cloud Data Platform
Deploy khai báoAirflow, template job Spark, và custom service được quản lý qua Git + Argo CD/Flux, với sync hook cho job chạy một lầnDevOps — GitOps
Cách ly thực thi taskKubernetesExecutor của Airflow: một Pod cho mỗi task để đáp ứng nhu cầu memory/CPU không đồng nhất, so với pool worker Celery cố địnhWorkflow Orchestration
Size bộ nhớRequests/limits dựa trên profiling thực tế, không phải mặc định kiểu web service; tính cả overhead memory của Spark, không chỉ heapDevOps — Container Orchestration
Dùng đĩa cục bộRequest/limit ephemeral-storage; giới hạn emptyDir bằng sizeLimit để tránh eviction Pod toàn node do shuffle spillDevOps — Container Orchestration

Best Practices

  1. Coi image container là đơn vị promote. Build một lần ở CI, gắn tag bất biến, và chạy đúng image đó ở staging lẫn production — không bao giờ build lại theo từng môi trường, và không bao giờ vá tay một container đang chạy.
  2. Pin phiên bản data library và native dependency trong image, không phải lúc runtime. Một sự trôi phiên bản âm thầm của pandas/pyarrow/JDBC driver là một kiểu lỗi tinh vi và nguy hiểm hơn việc thiếu hẳn một package.
  3. Chọn Spark-on-Kubernetes khi bạn đã native Kubernetes ở những nơi khác, và giữ đánh đổi trung thực: nó mang lại scaling co giãn và một nền tảng dùng chung, đổi lại một câu chuyện dynamic allocation kém trưởng thành hơn YARN đôi chút.
  4. Mặc định dùng control plane managed (EKS/GKE) cho hạ tầng dữ liệu — lợi ích vận hành của việc tự host Kubernetes gần như không bao giờ xứng đáng với chuyên môn nó đòi hỏi từ một team dữ liệu.
  5. Đặt bản deploy của chính Airflow, manifest job Spark, và custom service dưới GitOps — một nguồn chân lý duy nhất được reconcile trong Git có giá trị với hạ tầng dữ liệu không kém gì với bất kỳ hệ thống production nào khác.
  6. Chọn KubernetesExecutor của Airflow khi nhu cầu tài nguyên của các task không đồng nhất — task Spark/pandas nặng nằm cạnh các task nhẹ — và chấp nhận chi phí khởi động Pod theo từng task như cái giá của sự cách ly thật sự.
  7. Size memory limit dựa trên profiling thực tế, không phải mặc định copy từ web service — workload Spark và pandas nhảy vọt khó đoán, và chạm memory limit nghĩa là OOMKill thẳng, không phải throttle nhẹ nhàng.
  8. Luôn tính đến bộ nhớ off-heap và process Python của Spark, không chỉ executor.memory, khi đặt memory limit của Pod — sơ suất duy nhất này là nguyên nhân phổ biến nhất khiến Spark executor bị OOMKilled trên Kubernetes.
  9. Request và limit ephemeral-storage tường minh, và giới hạn volume scratch emptyDir bằng sizeLimit — dùng đĩa cục bộ không giới hạn từ shuffle hoặc file trung gian dẫn đến eviction Pod toàn node, chứ không chỉ làm fail một job gây ra nó.
  10. Tách các job chạy một lần (backfill, migration) khỏi các Deployment ổn định trong mô hình GitOps của bạn — dùng tài nguyên Job với sync hook thay vì ép chúng vào tư duy “luôn luôn reconcile” vốn chỉ phù hợp với các service chạy dài hạn.

Tài liệu tham khảo

Part of the Data Engineer Roadmap knowledge base.

Overview

A data pipeline that runs perfectly on your laptop and then fails silently in production is one of the most common — and most avoidable — incidents in this field. The culprit is almost always environment drift: a different Python version, a missing system library that pandas or a JDBC driver needed, a locale setting that changes how a date parses, a version of Spark that isn’t quite the one the cluster has. Containers exist to eliminate that class of bug entirely by packaging your code together with the exact runtime it needs, so the same artifact runs unchanged from a developer’s machine through CI into production. This note assumes you already understand what a container is — namespaces, cgroups, images, layers, the Dockerfile format — which is covered in full in DevOps — Containers. What it adds is the data engineer’s lens on the same technology: which parts of your work actually need containerizing, why Kubernetes in particular became the default place to run data workloads (not just to package them), and the handful of ways data workloads stress a container platform differently than a typical stateless web service.

Kubernetes shows up in a data engineer’s world in three distinct roles, and it’s worth keeping them separate from the start. First, it is a place to run your own pipeline code — a containerized Python script, a dbt project, a custom transformation job — as a scheduled Job or a task inside an orchestrator. Second, it is increasingly the execution engine for distributed compute frameworks themselves: Spark, Flink, and Ray all now run natively as Kubernetes-scheduled Pods rather than on their own bespoke cluster managers. Third, it is the platform the orchestrator itself runs on — Airflow, Dagster, and similar tools are commonly deployed inside Kubernetes, and can even use Kubernetes as their task-execution backend. Understanding which role applies to a given problem is most of what you need to reason well about this topic; the mechanics of Kubernetes itself are covered thoroughly in the Kubernetes knowledge base and are not repeated here.

Fundamentals

Containerizing pipeline code

The baseline skill is straightforward: take a transformation script, a dbt project, or a small ETL job, and wrap it in a Docker image so it carries its own interpreter version, system libraries (libpq, ODBC drivers, libgomp for numerical libraries), and pinned Python/R package versions. A minimal example for a Python transformation job:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["python", "run_pipeline.py"]

The full craft of writing good Dockerfiles — multi-stage builds, layer caching, non-root users, image scanning — is not specific to data engineering and is covered in depth in DevOps — Containers; apply it here exactly as you would to any other service. What is specific to data pipelines:

Spark on Kubernetes vs. standalone and YARN

For years, Apache Spark’s default deployment target was a dedicated YARN cluster (inherited from the Hadoop ecosystem) or Spark’s own lightweight standalone cluster manager. Both work, but both mean data engineering owns and capacity-plans a separate, static fleet of machines just for Spark — sized for peak load, mostly idle the rest of the time, and operationally distinct from whatever platform runs the rest of the organization’s services.

Running Spark on Kubernetes (via the built-in spark-submit --master k8s://... support, or the more feature-rich Spark Operator) changes this in a few concrete ways:

This is one specific instance of a much broader pattern covered in Big Data & Distributed Computing: distributed compute engines increasingly delegate scheduling and resource management to Kubernetes rather than reimplementing it, the same way they once delegated it to YARN.

Managed Kubernetes for data platforms

Running Kubernetes well — patching the control plane, managing etcd, handling version upgrades — is a specialized, ongoing operational burden that has nothing to do with data engineering itself. For a data platform team, the pragmatic default is a managed control plane: Amazon EKS or Google Cloud GKE (Azure AKS is the equivalent on that cloud). The provider runs and patches the control plane and etcd; your team owns node pools, workloads, and the data services you deploy onto them. GKE’s Autopilot mode goes a step further and manages node provisioning too, billing per Pod rather than per node — attractive for teams that want Kubernetes’ API and scheduling model without owning node lifecycle at all.

For a data team specifically, this matters more than it might for a typical backend team, because the point of adopting Kubernetes for data workloads is usually to get Spark, Airflow, and custom services onto shared, elastic infrastructure — not to become a Kubernetes platform team on the side. Self-hosting Kubernetes purely to run data jobs adds an entire second specialization (cluster operations) on top of the one you’re already responsible for (data). The deep mechanics of managed offerings, node pools, IAM integration, and cluster architecture live in the Kubernetes knowledge base and Cloud Data Platforms; the data-engineering takeaway is simply: prefer a managed control plane unless you have a specific, well-understood reason not to.

GitOps for data infrastructure

Once a data platform’s Kubernetes footprint grows beyond a handful of ad-hoc kubectl apply commands — Airflow’s own deployment, a fleet of Spark job templates, custom microservices feeding the pipeline — it needs the same discipline any other production infrastructure needs: a single source of truth for what should be running, and an automated way to converge the cluster to match it. This is exactly what GitOps (Argo CD, Flux) provides, and it applies to data infrastructure with no special exceptions: Airflow’s Helm values, Spark job manifests, and custom service Deployments all live in Git, and an in-cluster agent reconciles the live cluster to match. The full mechanics — Argo CD vs. Flux, app-of-apps, secrets handling, promotion between environments — are covered in DevOps — GitOps and apply here unchanged. The one data-specific wrinkle worth flagging: one-shot jobs and migrations (a Spark backfill, a schema migration, a one-time reprocessing run) don’t fit the pure “reconcile forever” model cleanly, since they should run once and then stop being desired state — GitOps tools handle this with sync hooks or by treating them as Job resources with an explicit completion condition, not as long-running Deployments.

Key Concepts

Containerizing the orchestrator itself: Airflow’s KubernetesExecutor

It’s easy to conflate “Airflow runs pipelines” with “Airflow runs on Kubernetes” — they’re related but separate decisions, and the second one has its own trade-offs worth understanding on its own terms (full orchestrator coverage lives in Workflow Orchestration). Airflow itself can be deployed on Kubernetes (via the community Helm chart), and independently, it can be configured to execute tasks using one of several executors:

ExecutorHow a task runsIsolation
SequentialExecutorOne task at a time, in-processNone — dev/testing only
LocalExecutorParallel subprocesses on the scheduler’s own machineShared machine resources; no per-task limits
CeleryExecutorTasks distributed to a fixed pool of long-running Celery worker processesWorkers are shared across tasks; sized for the worst case
KubernetesExecutorEach task instance launches as its own Kubernetes Pod, then the Pod terminatesFull per-task isolation: its own image, its own CPU/memory requests and limits

The KubernetesExecutor is popular in data platforms specifically because pipeline workloads are heterogeneous in a way most web services are not: one task in a DAG might be a lightweight API poll needing 100Mi of memory, and the next task in the same DAG might be a pandas job loading a multi-gigabyte dataframe that needs 16Gi. A CeleryExecutor’s fixed worker pool has to be sized for the largest task any DAG might ever run, wasting capacity on every smaller task; the KubernetesExecutor instead requests exactly the resources each task declares, scheduled independently, and scales the underlying node pool (via the cluster autoscaler or Karpenter) to match actual, per-task demand rather than a fixed worst-case pool. The cost is per-task Pod startup latency (usually a handful of seconds) and image pulls per task unless nodes are warmed — a real trade-off against Celery’s near-instant task pickup on an already-running worker, which is why some teams run a hybrid: KubernetesExecutor for heavy, irregular jobs and a small Celery pool for high-frequency, lightweight tasks.

Resource requests and limits for data workloads

Kubernetes resource tuning for a stateless web API and for a Spark job or a pandas-based batch task look nothing alike, and treating them the same way is a common source of production incidents:

AspectTypical web serviceData workload (Spark executor / pandas job)
Memory profileFlat, predictable, small (100s of MB)Spiky, large, hard to predict — can jump by GBs mid-task as a shuffle or a groupby materializes
CPU profileSteady, request-drivenBursty — near-zero while waiting on I/O, then pegged during a shuffle or a compute-heavy stage
Consequence of under-provisioningSlow responses, maybe throttlingOOMKilled — the container is killed outright, not throttled, and the whole task (possibly hours of work) is lost
Right-sizing approachSteady-state metrics over days/weeksProfile the specific job (Spark UI executor metrics, pandas memory profiling) — one job’s memory needs rarely generalize to another’s
Restart costCheap — a few seconds, no state lostExpensive — a killed Spark executor triggers task re-computation; a killed batch job may need to rerun from the last checkpoint or from scratch

The practical consequence: set memory limits generously and based on real profiling, not guesswork, because a data workload hitting its memory limit doesn’t degrade gracefully — the kernel’s OOM killer terminates the container outright (visible as OOMKilled in kubectl describe pod), and depending on the framework, that can silently retry, silently drop partial results, or fail the whole job. For Spark specifically, remember that a Pod’s memory limit must account for more than the JVM heap: off-heap memory, the JVM’s own overhead, and (critically) any Python UDF or pandas-on-Spark workload running in a separate Python process alongside the JVM all count against the container’s total memory limit — sizing the container to just spark.executor.memory and forgetting spark.executor.memoryOverhead is one of the single most common causes of OOMKilled Spark executors on Kubernetes.

Ephemeral storage for large intermediate datasets

Web services rarely write meaningful amounts of data to local disk; data workloads do it constantly. A Spark shuffle spills intermediate data to local disk when it doesn’t fit in memory; a pandas or Polars job reading a large file may materialize temporary Parquet or CSV files; a dbt run might stage intermediate tables. On Kubernetes, this local scratch space is ephemeral storage — the writable container filesystem plus any emptyDir volumes — and it is a resource that must be requested and limited exactly like CPU and memory:

resources:
  requests:
    memory: "8Gi"
    cpu: "2"
    ephemeral-storage: "20Gi"
  limits:
    memory: "10Gi"
    cpu: "4"
    ephemeral-storage: "40Gi"
volumes:
  - name: scratch
    emptyDir:
      sizeLimit: 40Gi

Without an explicit request and limit, a shuffle-heavy Spark job or a large intermediate write can quietly fill up a node’s disk, at which point the kubelet starts evicting Pods — including Pods unrelated to the one that caused the problem — to reclaim space, which is a noisier and harder-to-diagnose failure than a straightforward OOMKill. Mount emptyDir scratch volumes with an explicit sizeLimit, prefer nodes with fast local NVMe for shuffle-heavy Spark work when the cloud provider offers it, and monitor ephemeral storage usage with the same seriousness as memory — it is the resource dimension data workloads are most likely to exhaust and web services almost never think about.

Summary: what’s different about data workloads on containers/Kubernetes

ConcernData-engineering-specific considerationGo deeper
Packaging pipeline codePin data-library versions precisely; bake heavy native deps (Spark JVM, GDAL, CUDA) into a shared base imageDevOps — Containers
Compute schedulingSpark/Flink/Ray run as Kubernetes-native Pods instead of on YARN/standalone — elastic executors, one shared infraBig Data & Distributed Computing
Control plane operationsPrefer managed EKS/GKE over self-hosted Kubernetes — don’t take on cluster ops just to run data jobsKubernetes knowledge base, Cloud Data Platforms
Declarative deploymentAirflow, Spark job templates, and custom services managed via Git + Argo CD/Flux, with sync hooks for one-shot jobsDevOps — GitOps
Task execution isolationAirflow’s KubernetesExecutor: one Pod per task for heterogeneous memory/CPU needs, vs. a fixed Celery worker poolWorkflow Orchestration
Memory sizingRequests/limits based on real profiling, not web-service defaults; account for Spark overhead memory, not just heapDevOps — Container Orchestration
Local disk usageRequest/limit ephemeral-storage; cap emptyDir with sizeLimit to avoid node-wide Pod eviction from shuffle spillDevOps — Container Orchestration

Best Practices

  1. Treat the container image as the unit of promotion. Build once in CI, tag immutably, and run the exact same image in staging and production — never rebuild per environment, and never patch a running container by hand.
  2. Pin data-library and native-dependency versions in the image, not at runtime. A silent pandas/pyarrow/JDBC-driver version drift is a subtler and more dangerous failure mode than a missing package.
  3. Choose Spark-on-Kubernetes when you’re already Kubernetes-native elsewhere, and keep the trade-off honest: it buys elastic scaling and one shared platform, at the cost of a somewhat less mature dynamic-allocation story than YARN.
  4. Default to a managed control plane (EKS/GKE) for data infrastructure — the operational payoff of self-hosting Kubernetes almost never justifies the specialization it demands from a data team.
  5. Put Airflow’s own deployment, Spark job manifests, and custom services under GitOps — a single reconciled source of truth in Git is as valuable for data infrastructure as for any other production system.
  6. Choose Airflow’s KubernetesExecutor when task resource needs are heterogeneous — heavy Spark/pandas tasks alongside lightweight ones — and accept the per-task Pod startup cost as the price of true isolation.
  7. Size memory limits from real profiling, not defaults copied from a web service — Spark and pandas workloads spike unpredictably, and hitting a memory limit means an outright OOMKill, not graceful throttling.
  8. Always account for Spark’s off-heap and Python-process memory, not just executor.memory, when setting a Pod’s memory limit — this single oversight is the most common cause of OOMKilled Spark executors on Kubernetes.
  9. Explicitly request and limit ephemeral-storage, and cap emptyDir scratch volumes with sizeLimit — unbounded local disk use from shuffles or intermediate files leads to node-wide Pod eviction, not just a failure of the one job responsible.
  10. Separate one-shot jobs (backfills, migrations) from steady-state Deployments in your GitOps model — use Job resources with sync hooks rather than forcing them into the “always reconcile” mental model that fits long-running services.

References