VSCode Icon

File

Edit

View

Go

Run

Terminal

Help

Sejoon Kim - Visual Studio Code

Kubernetes StatefulSet: A Deep Dive

Kubernetes StatefulSet: A Deep Dive

Kubernetes StatefulSet: A Deep Dive

Still find that taking notes by hand helps things stick better.

When I first started working with Kubernetes, I thought everything could be a Deployment. Then I tried to run a database cluster. That's when I learned about StatefulSets the hard way.

What Makes StatefulSet Different?

In Kubernetes, most applications can run as Deployments. You scale them up and down, pods get random names, and if one dies, another takes its place. But what about databases? Message queues? Distributed systems that need stable network identities?

That's where StatefulSet comes in.

The Core Differences

FeatureDeploymentStatefulSet
Pod NamesRandom (web-7d8f9c-xk2m)Ordered (web-0, web-1, web-2)
Creation OrderParallelSequential (0→1→2)
Deletion OrderRandomReverse (2→1→0)
StorageShared (optional)Dedicated per pod
Network IdentityUnstableStable DNS names
ScalingFast, parallelSlow, sequential

How StatefulSet Works

Pod Naming and Ordering

StatefulSet creates pods with predictable names following the pattern: <statefulset-name>-<ordinal-index>

cassandra-0  ← Created first
cassandra-1  ← Created after cassandra-0 is Ready
cassandra-2  ← Created after cassandra-1 is Ready

This ordering matters for applications like:

  • Databases: Leader election needs the first pod ready before followers
  • Distributed systems: Node discovery depends on predictable hostnames
  • Message queues: Cluster formation requires ordered startup

Sequential Creation Process

When you create a StatefulSet with 3 replicas:

Time 0s:  cassandra-0 → Pending
Time 5s:  cassandra-0 → Running (waiting for Ready)
Time 30s: cassandra-0 → Ready ✓
Time 31s: cassandra-1 → Pending
Time 36s: cassandra-1 → Running (waiting for Ready)
Time 50s: cassandra-1 → Ready ✓
Time 51s: cassandra-2 → Pending
...

Why sequential? Many stateful applications need:

  1. Leader/master node established first
  2. Configuration copied from previous replicas
  3. Cluster membership registered before next node joins

Stable Network Identity

Each StatefulSet pod gets a stable DNS name that survives restarts:

<pod-name>.<service-name>.<namespace>.svc.cluster.local

Example for a Cassandra cluster:

cassandra-0.cassandra.default.svc.cluster.local
cassandra-1.cassandra.default.svc.cluster.local
cassandra-2.cassandra.default.svc.cluster.local

Even if cassandra-1 crashes and restarts, it keeps the same DNS name. Your application code can hardcode these addresses without worry.

Persistent Storage with StatefulSet

VolumeClaimTemplates

The real power of StatefulSet is dedicated persistent storage per pod:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:14
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: postgres-data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: postgres-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

This creates:

postgres-0 → PVC: postgres-data-postgres-0 (10Gi)
postgres-1 → PVC: postgres-data-postgres-1 (10Gi)
postgres-2 → PVC: postgres-data-postgres-2 (10Gi)

What Happens During Scaling?

Scaling Up (3→4 replicas):

1. Create new PVC: postgres-data-postgres-3
2. Wait for PVC to bind to a PersistentVolume
3. Create pod: postgres-3
4. Mount postgres-data-postgres-3 to postgres-3

Scaling Down (4→3 replicas):

1. Delete pod: postgres-3
2. PVC postgres-data-postgres-3 remains (not deleted!)
3. Data preserved for future scale-up

Key insight: StatefulSet NEVER deletes PVCs automatically. This prevents accidental data loss.

Real-World Example: Cassandra Cluster

Let me show you a real Cassandra StatefulSet I deployed:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  namespace: databases
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      containers:
      - name: cassandra
        image: cassandra:4.0
        ports:
        - containerPort: 9042
          name: cql
        - containerPort: 7000
          name: intra-node
        env:
        - name: CASSANDRA_SEEDS
          value: "cassandra-0.cassandra.databases.svc.cluster.local"
        - name: CASSANDRA_CLUSTER_NAME
          value: "ProductionCluster"
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        volumeMounts:
        - name: cassandra-data
          mountPath: /var/lib/cassandra
        resources:
          requests:
            memory: 4Gi
            cpu: 2
          limits:
            memory: 8Gi
            cpu: 4
      terminationGracePeriodSeconds: 300
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 100Gi

Why This Configuration?

CASSANDRA_SEEDS: Points to cassandra-0 as the seed node. Since it's created first, other nodes can discover the cluster through it.

terminationGracePeriodSeconds: 300: Cassandra needs time to flush data to disk and notify the cluster it's leaving. 5 minutes ensures clean shutdown.

Resources: Cassandra is memory-hungry. 4Gi request, 8Gi limit gives it room to handle spikes.

storageClassName: fast-ssd: Databases need fast storage. SSDs dramatically improve query performance.

Headless Service for StatefulSet

StatefulSets need a "headless service" (ClusterIP: None) for pod DNS resolution:

apiVersion: v1
kind: Service
metadata:
  name: cassandra
  namespace: databases
spec:
  clusterIP: None  # Headless service
  selector:
    app: cassandra
  ports:
  - port: 9042
    name: cql
  - port: 7000
    name: intra-node

Why headless?

  • Normal services distribute traffic across all pods (load balancing)
  • Headless services create DNS records for EACH pod individually
  • Allows direct pod-to-pod communication: cassandra-0.cassandra.databases.svc.cluster.local

Update Strategies

StatefulSet supports two update strategies:

1. RollingUpdate (Default)

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 0

Updates pods in reverse order (2→1→0):

1. Delete cassandra-2
2. Wait for new cassandra-2 to be Ready
3. Delete cassandra-1
4. Wait for new cassandra-1 to be Ready
5. Delete cassandra-0
6. Wait for new cassandra-0 to be Ready

Partition parameter: If you set partition: 2, only pods with ordinal ≥2 will update. Useful for canary deployments.

2. OnDelete

spec:
  updateStrategy:
    type: OnDelete

Pods only update when you manually delete them. Gives you complete control over rollout timing.

Common Pitfalls

1. Forgetting PVC Cleanup

After deleting a StatefulSet, PVCs remain. You must manually delete them:

# Delete StatefulSet
kubectl delete statefulset postgres

# PVCs still exist!
kubectl get pvc
# postgres-data-postgres-0   Bound    ...
# postgres-data-postgres-1   Bound    ...
# postgres-data-postgres-2   Bound    ...

# Manually clean up
kubectl delete pvc postgres-data-postgres-{0..2}

Why? Kubernetes protects your data. PVCs are never automatically deleted.

2. Slow Scaling Operations

Scaling StatefulSets is SLOW because it's sequential:

# This might take 5-10 minutes!
kubectl scale statefulset cassandra --replicas=10

Each pod must:

  1. Get PVC created and bound
  2. Pod scheduled and image pulled
  3. Pod starts and passes readiness probe
  4. THEN next pod starts

Solution: Pre-create PVCs if you know you'll scale up, or increase replica count during off-peak hours.

3. Readiness Probe Configuration

StatefulSet heavily relies on readiness probes. If misconfigured, your StatefulSet gets stuck:

readinessProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - cqlsh -e "describe cluster"
  initialDelaySeconds: 60
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

Too aggressive (short delays, low failure threshold) → Pods never become Ready → Next pod never starts

Too lenient (long delays, high threshold) → Slow scaling, sick pods stay in rotation too long

When to Use StatefulSet

Good Use Cases ✅

Databases

  • PostgreSQL, MySQL, MongoDB
  • Need stable hostnames for replication
  • Need persistent storage per instance

Distributed Coordination

  • Apache Zookeeper, etcd, Consul
  • Leader election requires stable identities
  • Cluster formation needs ordered startup

Message Queues

  • RabbitMQ, Kafka
  • Persistent message storage per broker
  • Cluster discovery via stable DNS

Caching Layers

  • Redis Cluster, Memcached
  • Consistent hashing requires stable node IDs
  • Persistent cache per pod

Bad Use Cases ❌

Stateless Web Apps

  • No need for stable identities
  • Deployment is simpler and faster

Workers Processing Queue Jobs

  • No persistent state needed
  • Parallel scaling is better with Deployment

API Gateways

  • Don't need ordered startup
  • Use Deployment with HPA instead

Debugging StatefulSet Issues

Check Pod Status

kubectl get pods -l app=cassandra -o wide

Check PVC Binding

kubectl get pvc
kubectl describe pvc cassandra-data-cassandra-0

Check Events

kubectl describe statefulset cassandra
kubectl get events --sort-by='.lastTimestamp'

Check Logs

kubectl logs cassandra-0
kubectl logs cassandra-0 --previous  # If pod crashed

Manual Pod Deletion (Force Recreation)

# StatefulSet will automatically recreate it
kubectl delete pod cassandra-1

Performance Considerations

Storage Class Selection

volumeClaimTemplates:
- metadata:
    name: data
  spec:
    storageClassName: fast-ssd  # Critical!
    accessModes: [ "ReadWriteOnce" ]
    resources:
      requests:
        storage: 100Gi

SSD vs HDD: For databases, SSD can be 10-100x faster. Worth the cost.

Local vs Network Storage: Local SSDs have lowest latency but no replication. Network storage (EBS, Persistent Disk) has built-in redundancy.

Resource Limits

resources:
  requests:
    memory: 4Gi
    cpu: 2
  limits:
    memory: 8Gi
    cpu: 4

Don't set limits too tight: Databases have bursty workloads. Leave headroom.

Monitor actual usage: Use Prometheus to track real resource consumption and adjust.

Advanced Patterns

Blue-Green Deployments

Use partition parameter for staged rollouts:

# Update only cassandra-2
kubectl patch statefulset cassandra -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl set image statefulset cassandra cassandra=cassandra:4.1

# Test cassandra-2 in production
# If good, update cassandra-1
kubectl patch statefulset cassandra -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":1}}}}'

# Finally update cassandra-0
kubectl patch statefulset cassandra -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'

Conclusion

StatefulSet is Kubernetes' solution for running stateful applications. Key takeaways:

  1. Sequential ordering ensures safe cluster formation
  2. Stable network identities enable peer discovery
  3. Persistent storage per pod prevents data loss
  4. Manual PVC cleanup required after deletion
  5. Slow scaling is the price of safety

After running StatefulSets in production for databases and message queues, I've learned: they're slower and more complex than Deployments, but for stateful applications, they're essential.

Choose StatefulSet when your application needs:

  • Stable, unique network identifiers
  • Ordered deployment and scaling
  • Persistent storage per pod

Otherwise, stick with Deployments. They're simpler, faster, and good enough for most workloads.


This article is based on hands-on experience running StatefulSets in production Kubernetes clusters, including Cassandra, PostgreSQL, and Redis deployments.