Introduction
Linkerd is a lightweight, ultralight service mesh that provides critical features like observability, reliability, and security for microservices without requiring code changes. It works by deploying sidecar proxies alongside each service instance.
Architecture Overview
Linkerd Service Mesh Architecture
┌─────────────────────────────────────────────────────────────┐ │ Java Microservice │ ├─────────────────────────────────────────────────────────────┤ │ Linkerd Proxy (Sidecar) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ HTTP/2 │ │ mTLS │ │ Load Balancing │ │ │ │ │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ Kubernetes Network │ └─────────────────────────────────────────────────────────────┘
Installation and Setup
1. Linkerd Installation
Prerequisites
# Install Linkerd CLI curl -sL https://run.linkerd.io/install | sh export PATH=$PATH:$HOME/.linkerd2/bin # Verify installation linkerd version # Check Kubernetes cluster linkerd check --pre
Install Linkerd Control Plane
# Install Linkerd linkerd install | kubectl apply -f - # Wait for installation to complete linkerd check # Install Viz extension for metrics linkerd viz install | kubectl apply -f - linkerd check # Install Jaeger for distributed tracing (optional) linkerd jaeger install | kubectl apply -f -
2. Java Application Preparation
Sample Microservice Application
// UserService - Spring Boot Microservice
@SpringBootApplication
@RestController
@RequestMapping("/api/users")
public class UserServiceApplication {
private final UserRepository userRepository;
private final RestTemplate restTemplate;
public UserServiceApplication(UserRepository userRepository) {
this.userRepository = userRepository;
this.restTemplate = new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
return ResponseEntity.ok(user);
}
@GetMapping("/{id}/profile")
public ResponseEntity<UserProfile> getUserWithProfile(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// Call Profile Service
ResponseEntity<Profile> profileResponse = restTemplate.getForEntity(
"http://profile-service:8080/api/profiles/" + user.getProfileId(),
Profile.class);
UserProfile userProfile = new UserProfile(user, profileResponse.getBody());
return ResponseEntity.ok(userProfile);
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User savedUser = userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
@GetMapping("/health")
public ResponseEntity<Map<String, String>> health() {
Map<String, String> status = new HashMap<>();
status.put("status", "UP");
status.put("timestamp", Instant.now().toString());
return ResponseEntity.ok(status);
}
}
// Custom exception
class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found: " + id);
}
}
Kubernetes Deployment with Linkerd
3. Annotated Kubernetes Manifests
User Service Deployment
# k8s/user-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: user-service labels: app: user-service version: v1 spec: replicas: 3 selector: matchLabels: app: user-service template: metadata: labels: app: user-service version: v1 annotations: # Linkerd injection annotation linkerd.io/inject: enabled # Custom metrics annotations config.linkerd.io/proxy-await: enabled config.alpha.linkerd.io/skip-outbound-ports: "5432" # Database port spec: containers: - name: user-service image: company/user-service:1.0.0 ports: - containerPort: 8080 env: - name: JAVA_OPTS value: "-Xmx512m -Xms256m -Dspring.profiles.active=production" - name: DATABASE_URL valueFrom: secretKeyRef: name: db-secret key: url livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" --- apiVersion: v1 kind: Service metadata: name: user-service labels: app: user-service spec: selector: app: user-service ports: - port: 80 targetPort: 8080 name: http type: ClusterIP
Profile Service Deployment
# k8s/profile-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: profile-service labels: app: profile-service version: v1 spec: replicas: 2 selector: matchLabels: app: profile-service template: metadata: labels: app: profile-service version: v1 annotations: linkerd.io/inject: enabled config.alpha.linkerd.io/skip-outbound-ports: "5432" spec: containers: - name: profile-service image: company/profile-service:1.0.0 ports: - containerPort: 8080 env: - name: JAVA_OPTS value: "-Xmx512m -Xms256m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 --- apiVersion: v1 kind: Service metadata: name: profile-service spec: selector: app: profile-service ports: - port: 80 targetPort: 8080 name: http
4. Deploy with Linkerd Injection
# Manual injection linkerd inject k8s/user-service.yaml | kubectl apply -f - linkerd inject k8s/profile-service.yaml | kubectl apply -f - # Verify injection kubectl get pods -l app=user-service kubectl get pods -l app=profile-service # Check Linkerd status linkerd check --proxy
Advanced Linkerd Configuration
5. Traffic Splitting for Canary Deployments
Canary Deployment Configuration
# k8s/user-service-canary.yaml apiVersion: apps/v1 kind: Deployment metadata: name: user-service-v2 labels: app: user-service version: v2 spec: replicas: 1 selector: matchLabels: app: user-service version: v2 template: metadata: labels: app: user-service version: v2 annotations: linkerd.io/inject: enabled spec: containers: - name: user-service image: company/user-service:2.0.0 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: user-service-v2 spec: selector: app: user-service version: v2 ports: - port: 80 targetPort: 8080
TrafficSplit Resource
# k8s/traffic-split.yaml apiVersion: split.smi-spec.io/v1alpha1 kind: TrafficSplit metadata: name: user-service-split spec: service: user-service backends: - service: user-service weight: 900 # 90% traffic to v1 - service: user-service-v2 weight: 100 # 10% traffic to v2
6. Retry and Timeout Configuration
ServiceProfile for Resilience
# k8s/service-profile.yaml
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: user-service.default.svc.cluster.local
namespace: default
spec:
routes:
- name: "GET /api/users/{id}"
condition:
method: GET
pathRegex: /api/users/\d+
isRetryable: true
timeout: 500ms
- name: "POST /api/users"
condition:
method: POST
pathRegex: /api/users
isRetryable: false
timeout: 1s
retryBudget:
retryRatio: 0.2
minRetriesPerSecond: 10
ttl: 10s
---
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: profile-service.default.svc.cluster.local
namespace: default
spec:
routes:
- name: "GET /api/profiles/{id}"
condition:
method: GET
pathRegex: /api/profiles/\d+
isRetryable: true
timeout: 300ms
Java Application Integration
7. Enhanced Java Microservice with Linkerd Awareness
Linkerd-Aware RestTemplate Configuration
@Configuration
public class LinkerdAwareConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(2))
.setReadTimeout(Duration.ofSeconds(5))
.additionalInterceptors(new LinkerdAwareInterceptor())
.build();
}
@Bean
public HttpClient httpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}
}
@Component
public class LinkerdAwareInterceptor implements ClientHttpRequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LinkerdAwareInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// Add Linkerd context headers
request.getHeaders().add("l5d-dst-override",
getServiceName(request.getURI()) + ".default.svc.cluster.local:80");
// Add tracing headers
String traceId = MDC.get("traceId");
if (traceId != null) {
request.getHeaders().add("x-b3-traceid", traceId);
request.getHeaders().add("x-b3-spanid", traceId);
}
long startTime = System.currentTimeMillis();
try {
ClientHttpResponse response = execution.execute(request, body);
long duration = System.currentTimeMillis() - startTime;
logger.info("HTTP {} to {} completed in {}ms with status {}",
request.getMethod(), request.getURI(), duration,
response.getStatusCode().value());
return response;
} catch (IOException e) {
long duration = System.currentTimeMillis() - startTime;
logger.error("HTTP {} to {} failed after {}ms: {}",
request.getMethod(), request.getURI(), duration,
e.getMessage());
throw e;
}
}
private String getServiceName(URI uri) {
String host = uri.getHost();
if (host.contains(".")) {
return host.substring(0, host.indexOf('.'));
}
return host;
}
}
8. Circuit Breaker with Resilience4j
@Service
public class ProfileServiceClient {
private final RestTemplate restTemplate;
private final CircuitBreaker circuitBreaker;
private final MeterRegistry meterRegistry;
public ProfileServiceClient(RestTemplate restTemplate,
CircuitBreakerRegistry circuitBreakerRegistry,
MeterRegistry meterRegistry) {
this.restTemplate = restTemplate;
this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("profileService");
this.meterRegistry = meterRegistry;
}
@Retry(name = "profileService", fallbackMethod = "getProfileFallback")
@TimeLimiter(name = "profileService")
@CircuitBreaker(name = "profileService", fallbackMethod = "getProfileFallback")
public CompletableFuture<Profile> getProfileAsync(Long profileId) {
return CompletableFuture.supplyAsync(() -> getProfile(profileId));
}
public Profile getProfile(Long profileId) {
String url = "http://profile-service/api/profiles/" + profileId;
return circuitBreaker.executeSupplier(() -> {
try {
ResponseEntity<Profile> response = restTemplate.getForEntity(url, Profile.class);
meterRegistry.counter("profile_service_calls", "status", "success").increment();
return response.getBody();
} catch (HttpClientErrorException.NotFound e) {
meterRegistry.counter("profile_service_calls", "status", "not_found").increment();
throw new ProfileNotFoundException(profileId);
} catch (ResourceAccessException e) {
meterRegistry.counter("profile_service_calls", "status", "timeout").increment();
throw new ServiceUnavailableException("Profile service timeout");
} catch (Exception e) {
meterRegistry.counter("profile_service_calls", "status", "error").increment();
throw new ServiceUnavailableException("Profile service error: " + e.getMessage());
}
});
}
// Fallback method
public Profile getProfileFallback(Long profileId, Exception e) {
meterRegistry.counter("profile_service_calls", "status", "fallback").increment();
logger.warn("Using fallback for profile {}, error: {}", profileId, e.getMessage());
return new Profile(profileId, "Default", "User");
}
}
9. Distributed Tracing Integration
@Component
public class TracingConfiguration {
@Bean
public Tracing tracing() {
return Tracing.newBuilder()
.localServiceName("user-service")
.sampler(Sampler.create(0.1f)) // Sample 10% of requests
.build();
}
@Bean
public Brave brave() {
return new Brave.Builder("user-service")
.traceSampler(Sampler.create(0.1f))
.build();
}
@Bean
public SpringCloudSleuthSpanInjector spanInjector(Brave brave) {
return new SpringCloudSleuthSpanInjector(brave);
}
}
@Aspect
@Component
public class TracingAspect {
private final Tracer tracer;
public TracingAspect(Tracer tracer) {
this.tracer = tracer;
}
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public Object traceRestController(ProceedingJoinPoint joinPoint) throws Throwable {
Span span = tracer.nextSpan().name(joinPoint.getSignature().getName()).start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
span.tag("component", "rest-controller");
span.tag("class", joinPoint.getTarget().getClass().getSimpleName());
span.tag("method", joinPoint.getSignature().getName());
return joinPoint.proceed();
} catch (Exception e) {
span.tag("error", "true");
span.tag("error.message", e.getMessage());
throw e;
} finally {
span.finish();
}
}
}
Monitoring and Observability
10. Custom Metrics with Micrometer
@Service
public class UserServiceMetrics {
private final MeterRegistry meterRegistry;
private final Counter userRequestsCounter;
private final Timer userRequestTimer;
private final DistributionSummary userResponseSize;
public UserServiceMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.userRequestsCounter = Counter.builder("user_service_requests")
.description("Total requests to user service")
.tag("service", "user-service")
.register(meterRegistry);
this.userRequestTimer = Timer.builder("user_service_request_duration")
.description("Request duration for user service")
.tag("service", "user-service")
.register(meterRegistry);
this.userResponseSize = DistributionSummary.builder("user_service_response_size")
.description("Response size from user service")
.baseUnit("bytes")
.register(meterRegistry);
}
public void recordUserRequest(String method, String path, int status) {
userRequestsCounter.increment();
Tags tags = Tags.of(
Tag.of("method", method),
Tag.of("path", path),
Tag.of("status", String.valueOf(status))
);
meterRegistry.counter("user_service_requests_detailed", tags).increment();
}
public Timer.Sample startRequestTimer() {
return Timer.start(meterRegistry);
}
public void stopRequestTimer(Timer.Sample sample, String method, String path) {
sample.stop(Timer.builder("user_service_request_duration_detailed")
.tag("method", method)
.tag("path", path)
.register(meterRegistry));
}
public void recordResponseSize(int size) {
userResponseSize.record(size);
}
}
@RestControllerAdvice
public class MetricsControllerAdvice {
private final UserServiceMetrics metrics;
public MetricsControllerAdvice(UserServiceMetrics metrics) {
this.metrics = metrics;
}
@ModelAttribute
public void addMetrics(HttpServletRequest request) {
String method = request.getMethod();
String path = request.getRequestURI();
Timer.Sample sample = metrics.startRequestTimer();
request.setAttribute("requestTimer", sample);
request.setAttribute("requestMethod", method);
request.setAttribute("requestPath", path);
}
@AfterReturning(pointcut = "within(@org.springframework.web.bind.annotation.RestController *)",
returning = "response")
public void afterSuccessfulRequest(JoinPoint joinPoint, Object response,
HttpServletRequest request) {
Timer.Sample sample = (Timer.Sample) request.getAttribute("requestTimer");
String method = (String) request.getAttribute("requestMethod");
String path = (String) request.getAttribute("requestPath");
if (sample != null) {
metrics.stopRequestTimer(sample, method, path);
}
metrics.recordUserRequest(method, path, 200);
if (response instanceof ResponseEntity) {
ResponseEntity<?> responseEntity = (ResponseEntity<?>) response;
Object body = responseEntity.getBody();
if (body != null) {
try {
int size = ObjectMapperFactory.getObjectMapper()
.writeValueAsBytes(body).length;
metrics.recordResponseSize(size);
} catch (JsonProcessingException e) {
// Ignore size recording error
}
}
}
}
@AfterThrowing(pointcut = "within(@org.springframework.web.bind.annotation.RestController *)",
throwing = "ex")
public void afterFailedRequest(JoinPoint joinPoint, Exception ex,
HttpServletRequest request) {
String method = (String) request.getAttribute("requestMethod");
String path = (String) request.getAttribute("requestPath");
int status = 500;
if (ex instanceof UserNotFoundException) {
status = 404;
}
metrics.recordUserRequest(method, path, status);
}
}
Security Configuration
11. mTLS and Security Policies
# k8s/network-policies.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: user-service-policy
namespace: default
spec:
podSelector:
matchLabels:
app: user-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: profile-service
ports:
- protocol: TCP
port: 8080
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
---
apiVersion: policy.linkerd.io/v1alpha1
kind: Server
metadata:
name: user-service-server
namespace: default
spec:
podSelector:
matchLabels:
app: user-service
port: 8080
proxyProtocol: HTTP/1
---
apiVersion: policy.linkerd.io/v1alpha1
kind: ServerAuthorization
metadata:
name: user-service-auth
namespace: default
spec:
server:
name: user-service-server
client:
meshTLS:
identities:
- "*.default.serviceaccount.identity.linkerd.cluster.local"
Advanced Features
12. Retry and Load Balancing Configuration
# k8s/service-profiles-advanced.yaml apiVersion: linkerd.io/v1alpha2 kind: ServiceProfile metadata: name: user-service.default.svc.cluster.local spec: routes: - name: "GetUser" condition: method: GET pathRegex: /api/users/\d+ responseClasses: - condition: status: min: 500 max: 599 isFailure: true timeout: 1s - name: "CreateUser" condition: method: POST pathRegex: /api/users timeout: 2s retryBudget: retryRatio: 0.2 minRetriesPerSecond: 10 ttl: 10s dstOverrides: - authority: user-service.default.svc.cluster.local:8080 weight: 10000
13. Java Application with Linkerd Health Checks
@Component
public class LinkerdHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
private final String[] dependencies;
public LinkerdHealthIndicator(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
this.dependencies = new String[]{
"http://profile-service/actuator/health",
"http://database-service/actuator/health"
};
}
@Override
public Health health() {
Map<String, Object> details = new HashMap<>();
boolean allHealthy = true;
for (String dependency : dependencies) {
try {
ResponseEntity<Map> response = restTemplate.getForEntity(
dependency, Map.class);
if (response.getStatusCode().is2xxSuccessful()) {
details.put(dependency, "UP");
} else {
details.put(dependency, "DOWN");
allHealthy = false;
}
} catch (Exception e) {
details.put(dependency, "DOWN - " + e.getMessage());
allHealthy = false;
}
}
if (allHealthy) {
return Health.up().withDetails(details).build();
} else {
return Health.down().withDetails(details).build();
}
}
}
@Configuration
public class HealthConfig {
@Bean
public HealthEndpointGroups healthEndpointGroups() {
return HealthEndpointGroups.of(
"linkerd",
Set.of("readiness", "liveness", "linkerd")
);
}
}
Deployment and Management
14. Automated Deployment Scripts
#!/bin/bash
# deploy-with-linkerd.sh
set -euo pipefail
APP_NAME=$1
VERSION=$2
ENVIRONMENT=${3:-staging}
echo "Deploying $APP_NAME version $VERSION to $ENVIRONMENT"
# Build and push image
docker build -t company/$APP_NAME:$VERSION .
docker push company/$APP_NAME:$VERSION
# Apply Kubernetes manifests with Linkerd injection
kubectl apply -f k8s/$APP_NAME/namespace-$ENVIRONMENT.yaml
# Inject Linkerd and deploy
linkerd inject k8s/$APP_NAME/deployment.yaml | \
kubectl apply -f -
# Apply service profiles
kubectl apply -f k8s/$APP_NAME/service-profile.yaml
# Wait for deployment to complete
kubectl rollout status deployment/$APP_NAME -n $ENVIRONMENT
# Run Linkerd checks
linkerd check --namespace $ENVIRONMENT
echo "Deployment completed successfully"
# Canary deployment (if applicable)
if [[ "$ENVIRONMENT" == "production" ]]; then
echo "Starting canary deployment..."
kubectl apply -f k8s/$APP_NAME/canary.yaml
sleep 300 # Wait 5 minutes
linkerd viz stat deployment/$APP_NAME -n production
fi
15. Monitoring and Dashboard
# Access Linkerd Dashboard linkerd viz dashboard & # Check service metrics linkerd viz stat deployment -n default linkerd viz top deployment/user-service -n default linkerd viz edges deployment -n default # Check service profiles linkerd viz routes deployment/user-service -n default # Generate traffic report linkerd viz tap deployment/user-service -n default --to deployment/profile-service
Best Practices
16. Production-Ready Configuration
Resource Limits for Linkerd Proxy
# k8s/proxy-resources.yaml apiVersion: v1 kind: ConfigMap metadata: name: linkerd-proxy-config namespace: linkerd data: proxy-cpu-request: "100m" proxy-cpu-limit: "200m" proxy-memory-request: "64Mi" proxy-memory-limit: "128Mi"
Comprehensive Health Checks
@Component
public class ComprehensiveHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
private final RestTemplate restTemplate;
private final MeterRegistry meterRegistry;
public ComprehensiveHealthIndicator(DataSource dataSource,
RestTemplate restTemplate,
MeterRegistry meterRegistry) {
this.dataSource = dataSource;
this.restTemplate = restTemplate;
this.meterRegistry = meterRegistry;
}
@Override
public Health health() {
Map<String, Object> details = new HashMap<>();
// Database health check
try (Connection conn = dataSource.getConnection()) {
boolean dbValid = conn.isValid(5);
details.put("database", dbValid ? "UP" : "DOWN");
} catch (Exception e) {
details.put("database", "DOWN - " + e.getMessage());
}
// External service health check
try {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://profile-service/actuator/health", String.class);
details.put("profileService",
response.getStatusCode().is2xxSuccessful() ? "UP" : "DOWN");
} catch (Exception e) {
details.put("profileService", "DOWN - " + e.getMessage());
}
// Memory health check
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
double memoryUsage = (double) usedMemory / maxMemory * 100;
details.put("memoryUsage", String.format("%.2f%%", memoryUsage));
details.put("maxMemory", humanReadableByteCount(maxMemory));
details.put("usedMemory", humanReadableByteCount(usedMemory));
if (memoryUsage > 90) {
return Health.down().withDetails(details).build();
} else if (memoryUsage > 80) {
return Health.status("WARNING").withDetails(details).build();
} else {
return Health.up().withDetails(details).build();
}
}
private String humanReadableByteCount(long bytes) {
int unit = 1024;
if (bytes < unit) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(unit));
String pre = "KMGTPE".charAt(exp-1) + "i";
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}
}
Conclusion
Linkerd provides powerful service mesh capabilities for Java microservices with minimal code changes:
- Automatic mTLS: Secure service-to-service communication
- Traffic Management: Fine-grained control over routing and splitting
- Observability: Detailed metrics, tracing, and monitoring
- Reliability: Automatic retries, timeouts, and circuit breaking
- Security: Network policies and service authentication
Key benefits for Java applications:
- Zero code changes required for basic functionality
- Enhanced resilience without complex client-side logic
- Comprehensive observability out of the box
- Automatic load balancing and connection pooling
- Simplified canary deployments and traffic management
By integrating Linkerd with Java microservices, organizations can achieve production-grade reliability, security, and observability while maintaining developer productivity and operational simplicity.