Container Memory Configuration
Managing Java memory in containers requires a shift from traditional fixed settings. Kubernetes allocates resources dynamically, so hardcoded heap sizes risk out-of-memory terminations. We configure the JVM to adapt to container limits using percentage-based flags. In your Dockerfile, replace -Xmx
arguments with -XX:MaxRAMPercentage
. This instructs the JVM to use a portion of the container’s memory rather than the host’s resources. For example:
FROM eclipse-temurin:17-jdk
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseContainerSupport"
COPY target/app.jar /app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
Here, MaxRAMPercentage=75.0
caps heap usage at 75% of the container’s memory limit, leaving room for non-heap overhead. The UseContainerSupport
flag ensures compatibility with container runtimes. I once debugged a production outage caused by a fixed -Xmx2g
setting—when Kubernetes reduced pod memory during autoscaling, the JVM exceeded limits and crashed. This approach prevents such incidents.
Liveness and Readiness Probes
Kubernetes relies on health probes to manage pod lifecycles. Liveness probes detect frozen applications, while readiness probes signal when traffic should start. In Spring Boot, we enable these endpoints via Actuator:
# application.properties
management.endpoint.health.probes.enabled=true
management.endpoint.health.group.readiness.include=db,diskSpace
management.health.livenessstate.enabled=true
The /actuator/health/liveness
endpoint returns UP
after startup, while /actuator/health/readiness
checks dependencies like databases. Define custom checks for critical subsystems:
@Component
public class PaymentServiceHealth implements HealthIndicator {
@Override
public Health health() {
return paymentService.isConnected() ? Health.up().build() : Health.down().build();
}
}
During a major database migration, I used readiness probes to temporarily route traffic away from pods until schema upgrades completed. This prevented cascading failures.
ConfigMap Integration
Externalizing configuration keeps applications portable across environments. Kubernetes ConfigMaps inject settings via environment variables or mounted files. For Spring applications, bind ConfigMap data to @Value
fields:
@RestController
public class ConfigController {
@Value("${feature.enabled}")
private boolean featureEnabled;
}
Deploy the ConfigMap:
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
feature.enabled: "true"
Mount it in your deployment:
# deployment.yaml
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: app-config
For file-based configurations (like application.yml
), mount ConfigMaps as volumes. I prefer this for multi-environment deployments—switching between staging and production requires zero code changes.
Graceful Shutdown Handling
Abrupt shutdowns truncate transactions and corrupt data. Kubernetes sends a SIGTERM
signal before terminating pods, giving applications time to clean up. Configure embedded servers to respect this:
@Configuration
public class GracefulShutdownConfig {
@Bean
public GracefulShutdown gracefulShutdown() {
return new GracefulShutdown(Duration.ofSeconds(25));
}
@Bean
public ServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addConnectorCustomizers(gracefulShutdown());
return factory;
}
}
The GracefulShutdown
class stops accepting new requests while allowing existing ones to complete within 25 seconds—under Kubernetes’ default 30-second grace period. In high-traffic systems, I’ve seen this prevent order duplication during deployments.
Jib Containerization
Building Docker images without Dockerfiles accelerates CI/CD pipelines. Jib creates optimized container layers directly from Maven or Gradle. Add this to pom.xml
:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<to>
<image>my-registry/app:${project.version}</image>
</to>
<container>
<jvmFlags>
<jvmFlag>-XX:MaxRAMPercentage=75.0</jvmFlag>
</jvmFlags>
</container>
</configuration>
</plugin>
Run mvn compile jib:build
to push the image. Jib separates dependencies, resources, and classes into distinct layers. If only your code changes, Kubernetes pulls just the updated layer. My team reduced deployment times by 70% after switching from Docker to Jib.
Distributed Tracing
Diagnosing failures in microservices demands end-to-end visibility. Distributed tracing links requests across services via unique IDs. Integrate Micrometer with Jaeger:
// pom.xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
Configure tracing in application.yml
:
management:
tracing:
sampling:
probability: 1.0 # Sample all requests
Annotate methods to create spans:
@NewSpan("process_order")
public void processOrder(Order order) {
// Business logic
}
In a retail system, tracing revealed a 10-second delay in payment processing—a bottleneck we fixed by adding caching.
Circuit Breaker Implementation
Protect applications from downstream failures using circuit breakers. Resilience4j halts calls to failing services, reducing cascading errors. Define a circuit breaker and fallback:
@Slf4j
@Service
public class PaymentService {
@CircuitBreaker(name = "payments", fallbackMethod = "fallback")
public PaymentResponse charge(Order order) {
return paymentGateway.charge(order);
}
private PaymentResponse fallback(Order order, Exception ex) {
log.error("Payment failed. Using fallback", ex);
return PaymentResponse.PENDING; // Queue for retry
}
}
Configure thresholds in application.yml
:
resilience4j.circuitbreaker:
instances:
payments:
failureRateThreshold: 50
waitDurationInOpenState: 10s
During a third-party API outage, this pattern kept our checkout service partially operational.
Horizontal Pod Autoscaling
Scale pods based on JVM metrics to handle traffic spikes. First, expose CPU usage via Micrometer:
@Bean
MeterRegistryCustomizer<PrometheusMeterRegistry> metricsConfig() {
return registry -> registry.config().commonTags("app", "order-service");
}
Then define a HorizontalPodAutoscaler:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
For custom scaling (e.g., based on queue depth), use Prometheus metrics.
Service Discovery
Locating services in Kubernetes is simplified through DNS. Spring Cloud Kubernetes auto-discovers endpoints:
@FeignClient(name = "inventory-service")
public interface InventoryClient {
@GetMapping("/stock/{itemId}")
StockInfo getStock(@PathVariable String itemId);
}
@SpringBootApplication
@EnableFeignClients
public class App { }
Kubernetes services resolve to DNS names like inventory-service.default.svc.cluster.local
. No hardcoded URLs.
Security Context Configuration
Harden containers by restricting privileges:
# deployment.yaml
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: secrets
mountPath: "/etc/secrets"
readOnly: true
This configuration runs the JVM as a non-root user, mounts secrets read-only, and disables Linux capabilities. I enforce this in all deployments—compromised pods become significantly less destructive.
These techniques ensure Java applications thrive in Kubernetes environments. By prioritizing adaptability, observability, and resilience, we build systems that withstand cloud complexities. Start small—implement graceful shutdowns and memory configuration first, then progressively adopt tracing and autoscaling.