Pod Disruption Budget in Java: Complete Guide

Introduction

Pod Disruption Budget (PDB) is a Kubernetes resource that limits the number of concurrent disruptions to your application pods during voluntary disruptions like node drains, cluster upgrades, or pod evictions. This ensures high availability and prevents simultaneous downtime of critical application components.

PDB Concepts

Key PDB Parameters:

  • minAvailable: Minimum number of pods that must be available
  • maxUnavailable: Maximum number of pods that can be unavailable
  • selector: Label selector to identify protected pods

Fabric8 Kubernetes Client Implementation

1. Dependencies

<dependencies>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>6.9.2</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-model</artifactId>
<version>6.9.2</version>
</dependency>
</dependencies>

2. Basic PDB Service

package com.example.k8s.pdb;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetBuilder;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetSpec;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetSpecBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
@Service
public class PodDisruptionBudgetService {
private static final Logger log = LoggerFactory.getLogger(PodDisruptionBudgetService.class);
private final KubernetesClient kubernetesClient;
private final String namespace;
public PodDisruptionBudgetService(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
this.namespace = kubernetesClient.getNamespace(); // Uses current context namespace
}
public PodDisruptionBudget createMinAvailablePDB(String name, 
Map<String, String> selectorLabels,
int minAvailable) {
log.info("Creating PDB '{}' with minAvailable: {}", name, minAvailable);
PodDisruptionBudget pdb = new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(name)
.withNamespace(namespace)
.addToLabels("app", name)
.addToLabels("managed-by", "java-application")
.endMetadata()
.withNewSpec()
.withMinAvailable(minAvailable)
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
return kubernetesClient.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.resource(pdb)
.create();
}
public PodDisruptionBudget createMaxUnavailablePDB(String name,
Map<String, String> selectorLabels,
int maxUnavailable) {
log.info("Creating PDB '{}' with maxUnavailable: {}", name, maxUnavailable);
PodDisruptionBudget pdb = new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(name)
.withNamespace(namespace)
.addToLabels("app", name)
.endMetadata()
.withNewSpec()
.withMaxUnavailable(maxUnavailable)
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
return kubernetesClient.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.resource(pdb)
.create();
}
public PodDisruptionBudget createPercentagePDB(String name,
Map<String, String> selectorLabels,
String maxUnavailablePercentage) {
log.info("Creating PDB '{}' with maxUnavailable: {}", name, maxUnavailablePercentage);
PodDisruptionBudget pdb = new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(name)
.withNamespace(namespace)
.endMetadata()
.withNewSpec()
.withMaxUnavailable(maxUnavailablePercentage) // e.g., "50%"
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
return kubernetesClient.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.resource(pdb)
.create();
}
}

3. Advanced PDB Manager

package com.example.k8s.pdb;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetList;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Component
public class PDBManager {
private final KubernetesClient client;
private final String namespace;
public PDBManager(KubernetesClient client) {
this.client = client;
this.namespace = client.getNamespace();
}
public Optional<PodDisruptionBudget> getPDB(String name) {
try {
PodDisruptionBudget pdb = client.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.withName(name)
.get();
return Optional.ofNullable(pdb);
} catch (KubernetesClientException e) {
log.error("Error retrieving PDB: {}", name, e);
return Optional.empty();
}
}
public List<PodDisruptionBudget> listPDBs() {
PodDisruptionBudgetList pdbList = client.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.list();
return pdbList.getItems();
}
public List<PodDisruptionBudget> listPDBsByLabels(Map<String, String> labels) {
PodDisruptionBudgetList pdbList = client.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.withLabels(labels)
.list();
return pdbList.getItems();
}
public boolean deletePDB(String name) {
try {
return client.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.withName(name)
.delete();
} catch (KubernetesClientException e) {
log.error("Error deleting PDB: {}", name, e);
return false;
}
}
public PodDisruptionBudget updatePDB(String name, PodDisruptionBudget updatedPDB) {
return client.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.withName(name)
.replace(updatedPDB);
}
public PodDisruptionBudget createOrUpdatePDB(PodDisruptionBudget pdb) {
String name = pdb.getMetadata().getName();
if (getPDB(name).isPresent()) {
return updatePDB(name, pdb);
} else {
return client.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.resource(pdb)
.create();
}
}
}

4. PDB Factory for Common Patterns

package com.example.k8s.pdb;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudgetBuilder;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class PDBFactory {
public PodDisruptionBudget createHighAvailabilityPDB(String appName, 
Map<String, String> selectorLabels,
int totalReplicas) {
// For high availability, allow only 1 pod to be disrupted at a time
int maxUnavailable = Math.max(1, totalReplicas / 4); // 25% or 1, whichever is larger
return new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(appName + "-pdb")
.addToLabels("app", appName)
.addToLabels("pdb-type", "high-availability")
.endMetadata()
.withNewSpec()
.withMaxUnavailable(maxUnavailable)
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
}
public PodDisruptionBudget createCriticalServicePDB(String appName,
Map<String, String> selectorLabels,
int totalReplicas) {
// For critical services, require at least 50% availability
int minAvailable = (int) Math.ceil(totalReplicas * 0.5);
return new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(appName + "-critical-pdb")
.addToLabels("app", appName)
.addToLabels("pdb-type", "critical")
.endMetadata()
.withNewSpec()
.withMinAvailable(minAvailable)
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
}
public PodDisruptionBudget createSingleInstancePDB(String appName,
Map<String, String> selectorLabels) {
// For single-instance applications, prevent any disruption
return new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(appName + "-pdb")
.addToLabels("app", appName)
.addToLabels("pdb-type", "single-instance")
.endMetadata()
.withNewSpec()
.withMinAvailable(1) // Must always be available
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
}
public PodDisruptionBudget createStatefulPDB(String appName,
Map<String, String> selectorLabels,
int replicas) {
// For stateful applications, allow disruption of only one pod at a time
return new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(appName + "-pdb")
.addToLabels("app", appName)
.addToLabels("pdb-type", "stateful")
.endMetadata()
.withNewSpec()
.withMaxUnavailable(1)
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
}
public PodDisruptionBudget createPercentageBasedPDB(String appName,
Map<String, String> selectorLabels,
String maxUnavailablePercentage) {
return new PodDisruptionBudgetBuilder()
.withNewMetadata()
.withName(appName + "-pdb")
.addToLabels("app", appName)
.endMetadata()
.withNewSpec()
.withMaxUnavailable(maxUnavailablePercentage)
.withNewSelector()
.withMatchLabels(selectorLabels)
.endSelector()
.endSpec()
.build();
}
}

5. PDB Validation Service

package com.example.k8s.pdb;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class PDBValidationService {
private final KubernetesClient client;
private final String namespace;
public PDBValidationService(KubernetesClient client) {
this.client = client;
this.namespace = client.getNamespace();
}
public ValidationResult validatePDB(PodDisruptionBudget pdb) {
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
String name = pdb.getMetadata().getName();
var spec = pdb.getSpec();
// Check if both minAvailable and maxUnavailable are set
if (spec.getMinAvailable() != null && spec.getMaxUnavailable() != null) {
errors.add("Both minAvailable and maxUnavailable cannot be set simultaneously");
}
// Check if neither is set
if (spec.getMinAvailable() == null && spec.getMaxUnavailable() == null) {
errors.add("Either minAvailable or maxUnavailable must be set");
}
// Check selector
if (spec.getSelector() == null) {
errors.add("Selector must be specified");
} else if (spec.getSelector().getMatchLabels() == null || 
spec.getSelector().getMatchLabels().isEmpty()) {
warnings.add("PDB selector has no matchLabels, ensure this is intentional");
}
// Validate numeric values
if (spec.getMinAvailable() != null) {
try {
int minAvailable = Integer.parseInt(spec.getMinAvailable().toString());
if (minAvailable < 0) {
errors.add("minAvailable cannot be negative");
}
} catch (NumberFormatException e) {
// It might be a percentage, validate percentage format
if (!spec.getMinAvailable().toString().endsWith("%")) {
errors.add("minAvailable must be an integer or percentage");
}
}
}
if (spec.getMaxUnavailable() != null) {
try {
int maxUnavailable = Integer.parseInt(spec.getMaxUnavailable().toString());
if (maxUnavailable < 0) {
errors.add("maxUnavailable cannot be negative");
}
} catch (NumberFormatException e) {
// It might be a percentage, validate percentage format
if (!spec.getMaxUnavailable().toString().endsWith("%")) {
errors.add("maxUnavailable must be an integer or percentage");
}
}
}
return new ValidationResult(name, errors, warnings);
}
public boolean isPDBCompliant(String pdbName) {
Optional<PodDisruptionBudget> pdbOpt = getPDB(pdbName);
if (pdbOpt.isEmpty()) {
return false;
}
PodDisruptionBudget pdb = pdbOpt.get();
var status = pdb.getStatus();
if (status == null) {
return false;
}
// Check if current healthy pods meet PDB requirements
Integer desiredHealthy = status.getDesiredHealthy();
Integer currentHealthy = status.getCurrentHealthy();
return desiredHealthy != null && currentHealthy != null && 
currentHealthy >= desiredHealthy;
}
public static class ValidationResult {
private final String pdbName;
private final List<String> errors;
private final List<String> warnings;
private final boolean isValid;
public ValidationResult(String pdbName, List<String> errors, List<String> warnings) {
this.pdbName = pdbName;
this.errors = errors;
this.warnings = warnings;
this.isValid = errors.isEmpty();
}
// Getters
public String getPdbName() { return pdbName; }
public List<String> getErrors() { return errors; }
public List<String> getWarnings() { return warnings; }
public boolean isValid() { return isValid; }
}
private Optional<PodDisruptionBudget> getPDB(String name) {
try {
PodDisruptionBudget pdb = client.policy().v1().podDisruptionBudget()
.inNamespace(namespace)
.withName(name)
.get();
return Optional.ofNullable(pdb);
} catch (Exception e) {
return Optional.empty();
}
}
}

6. PDB Controller for Automated Management

package com.example.k8s.pdb;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.WatcherException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class PDBAutoManager {
private static final Logger log = LoggerFactory.getLogger(PDBAutoManager.class);
private final KubernetesClient client;
private final PDBFactory pdbFactory;
private final PDBManager pdbManager;
private final String namespace;
private final Map<String, String> managedDeployments = new ConcurrentHashMap<>();
private Watcher<Deployment> deploymentWatcher;
public PDBAutoManager(KubernetesClient client, PDBFactory pdbFactory, PDBManager pdbManager) {
this.client = client;
this.pdbFactory = pdbFactory;
this.pdbManager = pdbManager;
this.namespace = client.getNamespace();
}
@PostConstruct
public void init() {
startDeploymentWatcher();
reconcileExistingDeployments();
}
@PreDestroy
public void cleanup() {
if (deploymentWatcher != null) {
// Watcher is automatically closed by the client
}
}
private void startDeploymentWatcher() {
deploymentWatcher = client.apps().deployments()
.inNamespace(namespace)
.watch(new Watcher<Deployment>() {
@Override
public void eventReceived(Action action, Deployment deployment) {
handleDeploymentEvent(action, deployment);
}
@Override
public void onClose(WatcherException cause) {
if (cause != null) {
log.error("Deployment watcher closed with error", cause);
// Implement reconnection logic
}
}
});
}
private void handleDeploymentEvent(Watcher.Action action, Deployment deployment) {
String deploymentName = deployment.getMetadata().getName();
Map<String, String> labels = deployment.getMetadata().getLabels();
if (labels != null && "true".equals(labels.get("auto-pdb"))) {
switch (action) {
case ADDED:
case MODIFIED:
manageDeploymentPDB(deployment);
break;
case DELETED:
removeDeploymentPDB(deploymentName);
break;
}
}
}
private void manageDeploymentPDB(Deployment deployment) {
String deploymentName = deployment.getMetadata().getName();
Map<String, String> selectorLabels = deployment.getSpec().getSelector().getMatchLabels();
Integer replicas = deployment.getSpec().getReplicas();
if (replicas == null || replicas < 1) {
log.warn("Deployment {} has invalid replica count: {}", deploymentName, replicas);
return;
}
Map<String, String> labels = deployment.getMetadata().getLabels();
String pdbStrategy = labels != null ? labels.get("pdb-strategy") : "default";
PodDisruptionBudget pdb;
switch (pdbStrategy) {
case "high-availability":
pdb = pdbFactory.createHighAvailabilityPDB(deploymentName, selectorLabels, replicas);
break;
case "critical":
pdb = pdbFactory.createCriticalServicePDB(deploymentName, selectorLabels, replicas);
break;
case "single-instance":
if (replicas == 1) {
pdb = pdbFactory.createSingleInstancePDB(deploymentName, selectorLabels);
} else {
log.warn("Single-instance PDB strategy used for multi-replica deployment: {}", deploymentName);
pdb = pdbFactory.createHighAvailabilityPDB(deploymentName, selectorLabels, replicas);
}
break;
default:
pdb = pdbFactory.createHighAvailabilityPDB(deploymentName, selectorLabels, replicas);
}
pdbManager.createOrUpdatePDB(pdb);
managedDeployments.put(deploymentName, pdb.getMetadata().getName());
log.info("Managed PDB for deployment: {}", deploymentName);
}
private void removeDeploymentPDB(String deploymentName) {
String pdbName = managedDeployments.remove(deploymentName);
if (pdbName != null) {
pdbManager.deletePDB(pdbName);
log.info("Removed PDB for deleted deployment: {}", deploymentName);
}
}
@Scheduled(fixedDelay = 300000) // Run every 5 minutes
public void reconcileExistingDeployments() {
log.info("Starting PDB reconciliation");
client.apps().deployments()
.inNamespace(namespace)
.list()
.getItems()
.stream()
.filter(deployment -> {
Map<String, String> labels = deployment.getMetadata().getLabels();
return labels != null && "true".equals(labels.get("auto-pdb"));
})
.forEach(this::manageDeploymentPDB);
log.info("Completed PDB reconciliation");
}
}

7. Spring Boot Configuration

package com.example.k8s.config;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KubernetesConfig {
@Value("${kubernetes.namespace:default}")
private String namespace;
@Value("${kubernetes.master.url:}")
private String masterUrl;
@Value("${kubernetes.oauth.token:}")
private String oauthToken;
@Bean
public KubernetesClient kubernetesClient() {
Config config = new ConfigBuilder()
.withNamespace(namespace)
.withMasterUrl(masterUrl.isEmpty() ? null : masterUrl)
.withOauthToken(oauthToken.isEmpty() ? null : oauthToken)
.withTrustCerts(true)
.build();
return new DefaultKubernetesClient(config);
}
}

8. Usage Examples

package com.example.k8s.controller;
import com.example.k8s.pdb.*;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/pdb")
public class PDBController {
private final PodDisruptionBudgetService pdbService;
private final PDBManager pdbManager;
private final PDBValidationService validationService;
public PDBController(PodDisruptionBudgetService pdbService, 
PDBManager pdbManager,
PDBValidationService validationService) {
this.pdbService = pdbService;
this.pdbManager = pdbManager;
this.validationService = validationService;
}
@PostMapping("/min-available")
public PodDisruptionBudget createMinAvailablePDB(
@RequestParam String name,
@RequestParam int minAvailable,
@RequestBody Map<String, String> selectorLabels) {
return pdbService.createMinAvailablePDB(name, selectorLabels, minAvailable);
}
@PostMapping("/max-unavailable")
public PodDisruptionBudget createMaxUnavailablePDB(
@RequestParam String name,
@RequestParam int maxUnavailable,
@RequestBody Map<String, String> selectorLabels) {
return pdbService.createMaxUnavailablePDB(name, selectorLabels, maxUnavailable);
}
@GetMapping
public List<PodDisruptionBudget> listPDBs() {
return pdbManager.listPDBs();
}
@GetMapping("/{name}")
public PodDisruptionBudget getPDB(@PathVariable String name) {
return pdbManager.getPDB(name)
.orElseThrow(() -> new RuntimeException("PDB not found: " + name));
}
@DeleteMapping("/{name}")
public void deletePDB(@PathVariable String name) {
pdbManager.deletePDB(name);
}
@PostMapping("/{name}/validate")
public PDBValidationService.ValidationResult validatePDB(@PathVariable String name) {
PodDisruptionBudget pdb = pdbManager.getPDB(name)
.orElseThrow(() -> new RuntimeException("PDB not found: " + name));
return validationService.validatePDB(pdb);
}
@GetMapping("/{name}/compliant")
public boolean isPDBCompliant(@PathVariable String name) {
return validationService.isPDBCompliant(name);
}
}

9. Application Properties

# application.yml
kubernetes:
namespace: my-app-namespace
master:
url: https://kubernetes.default.svc
oauth:
token: ${KUBERNETES_TOKEN}
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
logging:
level:
com.example.k8s: DEBUG

Best Practices

  1. Use PDBs for Stateful Applications: Protect databases, message queues, and other stateful services
  2. Balance Availability and Upgradability: Don't set minAvailable too high, as it can prevent cluster maintenance
  3. Monitor PDB Compliance: Regularly check if your PDBs are being respected
  4. Use Labels Strategically: Ensure PDB selectors match your pod labels accurately
  5. Test Disruption Scenarios: Verify PDB behavior during controlled node drains

Conclusion

Pod Disruption Budgets are essential for maintaining application availability during Kubernetes cluster operations. The Java implementation using Fabric8 Kubernetes Client provides a robust way to programmatically manage PDBs, ensuring your applications remain available during voluntary disruptions while allowing necessary cluster maintenance operations.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper