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.