Ray on Kubernetes: Distributed Python and AI Workloads Without the YAML Pain
Running distributed Python at scale usually involves a choice between "too much manual effort" (hand-rolling SSH scripts and static IP lists) and "too much abstraction" (managed platforms that hide the cost and performance knobs). Ray on Kubernetes, managed via the KubeRay operator, is the professional middle ground.
But deploying a `RayCluster` CR and calling it "production-ready" is a recipe for an OOM-kill loop and network saturation. To run Ray at the 100+ node scale, you have to solve for three things: the shared memory trap, the "One Pod Per Node" rule, and GCS state persistence.
The Architectural Blueprint
Ray transforms a cluster of machines into a single shared-memory pool. Unlike Spark, which is primarily a data-flow engine, Ray is a distributed execution engine for actors and tasks.
The Control Plane Flow
KubeRay implements the operator pattern to bridge the gap between Ray's cluster-centric view and Kubernetes' pod-centric view.
User/CI -> Create RayCluster CR -> KubeRay Operator -> [Provision Head Pod + Head Service] -> [Provision Worker Groups (Pods)] -> Ray Head (GCS) registers Worker Nodes
The Global Control Store (GCS) is the brain of the cluster. It lives on the head pod and maintains the cluster's metadata: which actors are on which nodes, the status of pending tasks, and the resource map. If the head pod dies, the GCS dies, and the cluster typically loses its state—unless you have configured external state stores.
The Data Plane: Plasma and Zero-Copy
The secret to Ray's performance is Plasma, a shared-memory object store. Instead of serializing and deserializing data between processes on the same node, Ray uses shared memory (`/dev/shm`) to allow multiple workers to read the same object without copying it.
[ Ray Head Pod ] <---------------------> [ Ray Worker Pod A ]
| |
| (GCS / Scheduler) | (Local Plasma Store)
| |
+-----------------------------------------+
|
[ Kubernetes Network / CNI ]
|
[ Ray Worker Pod B ] <-----+------> [ Ray Worker Pod C ]
| |
| (Local Plasma Store) | (Local Plasma Store)
Production Implementation Guide
Step 1: Deploy the KubeRay Operator
The operator is the only way to manage the lifecycle of Ray clusters without manually tracking pod IPs.
# Add the KubeRay Helm repository
helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm repo update
Install the operator and CRDs
helm install kuberay-operator kuberay/kuberay-operator --version 1.2.2
Step 2: The Hardened RayCluster Manifest
A basic `RayCluster` manifest will fail in production because Kubernetes defaults `/dev/shm` to 64MiB, while Ray's object store defaults to 30% of node memory. This mismatch causes immediate `ObjectStoreFullError` crashes.
Below is a production-grade manifest for a GPU-accelerated cluster.
apiVersion: ray.io/v1alpha1
kind: RayCluster
metadata:
name: ray-prod-cluster
spec:
rayVersion: "2.30.0"
enableInTreeAutoscaling: true
headGroupSpec:
rayStartParams:
dashboard-host: "0.0.0.0"
template:
spec:
containers:
- name: ray-head
image: rayproject/ray-ml:2.30.0
resources:
limits:
cpu: "4"
memory: "16Gi"
requests:
cpu: "4"
memory: "16Gi"
volumeMounts:
- mountPath: /dev/shm
name: dshm
volumes:
- name: dshm
emptyDir:
medium: Memory
sizeLimit: "4Gi" # ~25% of head memory
workerGroupSpecs:
- groupName: gpu-workers
replicas: 2
minReplicas: 1
maxReplicas: 10
rayStartParams: {}
template:
spec:
containers:
- name: ray-worker
image: rayproject/ray-ml:2.30.0
resources:
limits:
cpu: "16"
memory: "64Gi"
nvidia.com/gpu: "1"
requests:
cpu: "16"
memory: "64Gi"
nvidia.com/gpu: "1"
volumeMounts:
- mountPath: /dev/shm
name: dshm
volumes:
- name: dshm
emptyDir:
medium: Memory
sizeLimit: "20Gi" # Match object-store-memory setting
Step 3: Verifying the Object Store
Once the pods are running, verify that `/dev/shm` is actually backed by memory and not the default 64MiB limit.
# Identify the worker pod
WORKER_POD=$(kubectl get pods -l ray.io/node-type=worker -o jsonpath='{.items[0].metadata.name}')
Check the mount size
kubectl exec -it $WORKER_POD -- df -h /dev/shm
Production Realities: Where it Breaks
The /dev/shm Memory Paradox
The most common failure in Ray-on-K8s is the "Invisible OOM". Because `/dev/shm` is a separate mount, the memory it consumes is often not tracked by the standard Kubernetes pod memory limit. You can have a pod that appears to be using 10GiB of its 64GiB limit, but the kernel OOM-kills it because the `emptyDir` memory limit was hit.
Fix: Always explicitly set `sizeLimit` on the `emptyDir` volume and ensure it matches the `object-store-memory` parameter in `ray start`.The "One Pod Per Node" Rule
Ray is designed for high-throughput, low-latency communication. Running multiple small Ray pods on a single large node introduces massive overhead:
1. Redundant Raylets: Every pod runs its own Raylet (node manager), wasting CPU.
2. Fragmented Object Store: Instead of one large 200GB Plasma store, you have ten 20GB stores, forcing Ray to move data across the network (TCP) instead of using local shared memory.
Fix: Size your Pod requests to match the node's capacity. Use `Taints` and `Tolerations` to ensure Ray pods have exclusive access to their nodes.Network Saturation during "Shuffle"
In large-scale training or data processing, Ray performs "all-to-all" communication. If you are using a standard 10Gbps CNI, a burst of object transfers between 50 nodes can saturate the node's network interface, causing the GCS heartbeats to time out and triggering a "node failure" cascade.
Fix: Use high-bandwidth instances (e.g., AWS `p4d` or `g5` with EFA) and monitor `node_network_transmit_bytes_total`.Who Should Skip This
Do NOT use Ray on Kubernetes if:
- Your workload is "Embarrassingly Parallel": If you are just running 1,000 independent Python scripts, use a Kubernetes `Job` or `Argo Workflows`. Ray's complexity is only worth it for stateful distributed actors.
- You have < 32GB of RAM per node: Ray's overhead (GCS + Raylet + Object Store) makes it inefficient on small instances.
- You cannot manage a K8s Operator: If your team struggles with basic Deployments, the `RayCluster` reconciliation loop will be a nightmare to debug.
References & Further Reading
- [KubeRay Official Documentation](https://docs.ray.io/en/latest/cluster/kubernetes/index.html)
- [Ray Object Store Mechanics](https://docs.ray.io/en/latest/ray-core/objects.html)
- [Kubernetes emptyDir Memory Medium](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir)
- [Anyscale: Running Ray at Scale](https://anyscale.com/blog/kuberay)