--- name: service-discovery description: Service Discovery và Service Registry patterns. Use for dynamic service location, health checking, và load balancing trong microservices. compatibility: ".NET 10+, Consul, Kubernetes, DNS" metadata: author: Velik Ho version: "1.0" --- # Service Discovery / Service Discovery Pattern Patterns cho service discovery và dynamic service location trong microservices. ## When to Use This Skill / Khi Nào Sử Dụng Use this skill when: - Services need to find each other dynamically / Services cần tìm nhau động - Deploying to Kubernetes / Triển khai trên Kubernetes - Implementing client-side load balancing / Triển khai load balancing phía client - Health checking service instances / Kiểm tra health của service instances ## Core Concepts / Khái Niệm Cốt Lõi ### Service Discovery Patterns ``` ┌─────────────────────────────────────────────────────────────┐ │ SERVICE DISCOVERY PATTERNS │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ CLIENT-SIDE DISCOVERY │ │ │ │ │ │ │ │ Client ──Query──▶ Registry ──▶ Service List │ │ │ │ │ │ │ │ │ └──Direct Call──▶ Service Instance │ │ │ │ │ │ │ │ Pros: Simpler, fewer hops │ │ │ │ Cons: Client needs discovery logic │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ SERVER-SIDE DISCOVERY │ │ │ │ │ │ │ │ Client ──▶ Load Balancer ──Query──▶ Registry │ │ │ │ │ │ │ │ │ └──▶ Service Instance │ │ │ │ │ │ │ │ Pros: Client stays simple │ │ │ │ Cons: Extra network hop │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### Service Registry / Registry dịch vụ ``` ┌─────────────────────────────────────────────────────────────┐ │ SERVICE REGISTRY │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Service: user-service │ │ │ │ Instances: │ │ │ │ - host: 10.0.1.10, port: 5001, health: ✅ │ │ │ │ - host: 10.0.1.11, port: 5001, health: ✅ │ │ │ │ - host: 10.0.1.12, port: 5001, health: ❌ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Service: order-service │ │ │ │ Instances: │ │ │ │ - host: 10.0.2.10, port: 5002, health: ✅ │ │ │ │ - host: 10.0.2.11, port: 5002, health: ✅ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### Discovery Options Comparison | Option | Type | Complexity | Best For | |--------|------|------------|----------| | **Kubernetes DNS** | Server-side | Low | K8s deployments | | **Consul** | Client-side | Medium | Multi-platform | | **Eureka** | Client-side | Medium | Spring ecosystem | | **DNS + Load Balancer** | Server-side | Low | Simple setups | ## Key Patterns / Mẫu Chính ### Kubernetes Service Discovery ```yaml # EN: Kubernetes Service for internal discovery # VI: Kubernetes Service cho discovery nội bộ apiVersion: v1 kind: Service metadata: name: user-service namespace: goodgo spec: selector: app: user-service ports: - port: 80 targetPort: 5001 type: ClusterIP --- # EN: Deployment with health probes apiVersion: apps/v1 kind: Deployment metadata: name: user-service namespace: goodgo spec: replicas: 3 selector: matchLabels: app: user-service template: metadata: labels: app: user-service spec: containers: - name: user-service image: goodgo/user-service:latest ports: - containerPort: 5001 livenessProbe: httpGet: path: /health/live port: 5001 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /health/ready port: 5001 initialDelaySeconds: 5 periodSeconds: 5 ``` ### Using Kubernetes DNS in .NET ```csharp /// /// EN: HTTP client using Kubernetes DNS. /// VI: HTTP client sử dụng Kubernetes DNS. /// // Program.cs builder.Services.AddHttpClient(client => { // EN: Use Kubernetes service DNS name // VI: Sử dụng Kubernetes service DNS name client.BaseAddress = new Uri("http://user-service.goodgo.svc.cluster.local"); }) .AddStandardResilienceHandler(); // appsettings.json for different environments { "Services": { "UserService": { "BaseUrl": "http://user-service.goodgo.svc.cluster.local" // K8s // "BaseUrl": "http://localhost:5001" // Local dev } } } ``` ### Consul Service Registration ```csharp /// /// EN: Register service with Consul. /// VI: Đăng ký service với Consul. /// public static class ConsulServiceExtensions { public static IServiceCollection AddConsulServiceDiscovery( this IServiceCollection services, IConfiguration configuration) { services.AddSingleton(sp => { var consulAddress = configuration["Consul:Address"]; return new ConsulClient(cfg => { cfg.Address = new Uri(consulAddress!); }); }); services.AddHostedService(); return services; } } /// /// EN: Background service for Consul registration. /// VI: Background service cho Consul registration. /// public class ConsulRegistrationService : IHostedService { private readonly IConsulClient _consulClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; private string? _registrationId; public ConsulRegistrationService( IConsulClient consulClient, IConfiguration configuration, ILogger logger) { _consulClient = consulClient; _configuration = configuration; _logger = logger; } public async Task StartAsync(CancellationToken ct) { var serviceName = _configuration["Service:Name"]!; var serviceHost = _configuration["Service:Host"]!; var servicePort = int.Parse(_configuration["Service:Port"]!); _registrationId = $"{serviceName}-{Guid.NewGuid():N}"; var registration = new AgentServiceRegistration { ID = _registrationId, Name = serviceName, Address = serviceHost, Port = servicePort, Tags = new[] { "api", "v1" }, Check = new AgentServiceCheck { HTTP = $"http://{serviceHost}:{servicePort}/health", Interval = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromSeconds(5), DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1) } }; await _consulClient.Agent.ServiceRegister(registration, ct); _logger.LogInformation( "Registered service {ServiceName} with Consul as {RegistrationId}", serviceName, _registrationId); } public async Task StopAsync(CancellationToken ct) { if (_registrationId != null) { await _consulClient.Agent.ServiceDeregister(_registrationId, ct); _logger.LogInformation("Deregistered service {RegistrationId}", _registrationId); } } } ``` ### Client-Side Load Balancing with Consul ```csharp /// /// EN: HTTP client with Consul-based service discovery. /// VI: HTTP client với service discovery dựa trên Consul. /// public class ConsulServiceDiscoveryClient { private readonly IConsulClient _consulClient; private readonly ILogger _logger; public ConsulServiceDiscoveryClient( IConsulClient consulClient, ILogger logger) { _consulClient = consulClient; _logger = logger; } public async Task GetServiceUriAsync( string serviceName, CancellationToken ct = default) { var services = await _consulClient.Health.Service( serviceName, tag: null, passingOnly: true, ct); if (services.Response.Length == 0) { _logger.LogWarning("No healthy instances found for {ServiceName}", serviceName); return null; } // EN: Simple round-robin load balancing // VI: Load balancing round-robin đơn giản var service = services.Response[Random.Shared.Next(services.Response.Length)]; var uri = new Uri($"http://{service.Service.Address}:{service.Service.Port}"); _logger.LogDebug( "Resolved {ServiceName} to {Uri}", serviceName, uri); return uri; } } /// /// EN: HTTP message handler with service discovery. /// VI: HTTP message handler với service discovery. /// public class ServiceDiscoveryHandler : DelegatingHandler { private readonly ConsulServiceDiscoveryClient _discovery; private readonly string _serviceName; public ServiceDiscoveryHandler( ConsulServiceDiscoveryClient discovery, string serviceName) { _discovery = discovery; _serviceName = serviceName; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken ct) { var serviceUri = await _discovery.GetServiceUriAsync(_serviceName, ct); if (serviceUri == null) throw new ServiceNotFoundException(_serviceName); // EN: Replace the request URI with discovered service // VI: Thay thế URI request với service được discover var builder = new UriBuilder(request.RequestUri!) { Scheme = serviceUri.Scheme, Host = serviceUri.Host, Port = serviceUri.Port }; request.RequestUri = builder.Uri; return await base.SendAsync(request, ct); } } ``` ### Health Check Endpoints ```csharp /// /// EN: Health check endpoints for service discovery. /// VI: Health check endpoints cho service discovery. /// // Program.cs builder.Services.AddHealthChecks() .AddDbContextCheck("database") .AddRedis(builder.Configuration.GetConnectionString("Redis")!, "redis") .AddRabbitMQ(builder.Configuration.GetConnectionString("RabbitMQ")!, "rabbitmq"); var app = builder.Build(); // EN: Liveness probe - is the service running? app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false, // No checks, just confirm the process is running ResponseWriter = WriteMinimalResponse }); // EN: Readiness probe - can the service handle requests? app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready"), ResponseWriter = WriteDetailedResponse }); // EN: Full health check for monitoring app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = WriteDetailedResponse }); static Task WriteMinimalResponse(HttpContext context, HealthReport report) { context.Response.ContentType = "text/plain"; return context.Response.WriteAsync(report.Status.ToString()); } static Task WriteDetailedResponse(HttpContext context, HealthReport report) { context.Response.ContentType = "application/json"; var response = new { status = report.Status.ToString(), checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.TotalMilliseconds }), totalDuration = report.TotalDuration.TotalMilliseconds }; return context.Response.WriteAsJsonAsync(response); } ``` ## Common Mistakes / Lỗi Thường Gặp ### 1. Hardcoded Service URLs ```csharp // ❌ BAD: Hardcoded URLs var client = new HttpClient { BaseAddress = new Uri("http://10.0.1.50:5001") // What if IP changes? }; // ✅ GOOD: Use service discovery var client = new HttpClient { BaseAddress = new Uri("http://user-service.goodgo.svc.cluster.local") }; // Or with Consul var serviceUri = await _discovery.GetServiceUriAsync("user-service"); ``` ### 2. No Health Checks ```yaml # ❌ BAD: No health probes spec: containers: - name: api image: my-api:latest # ✅ GOOD: With health probes spec: containers: - name: api image: my-api:latest livenessProbe: httpGet: path: /health/live port: 80 readinessProbe: httpGet: path: /health/ready port: 80 ``` ### 3. Missing Retry on Discovery Failure ```csharp // ❌ BAD: Single attempt var service = await _discovery.GetServiceAsync("order-service"); await _client.GetAsync(service.Uri + "/api/orders"); // ✅ GOOD: With retry and fallback var services = await _discovery.GetHealthyServicesAsync("order-service"); foreach (var service in services) { try { return await _client.GetAsync(service.Uri + "/api/orders"); } catch (HttpRequestException) { continue; // Try next instance } } throw new ServiceUnavailableException("order-service"); ``` ## Quick Reference / Tham Chiếu Nhanh ### Kubernetes Service DNS Format | Type | Format | Example | |------|--------|---------| | Same namespace | `` | `user-service` | | Different namespace | `.` | `user-service.goodgo` | | Full FQDN | `..svc.cluster.local` | `user-service.goodgo.svc.cluster.local` | ### Consul Health Check Types | Type | Use For | |------|---------| | HTTP | REST APIs | | TCP | Databases, caches | | gRPC | gRPC services | | Script | Custom checks | ### Health Probe Configuration | Probe | Purpose | Failure Action | |-------|---------|----------------| | Liveness | Is process alive? | Restart container | | Readiness | Can handle traffic? | Remove from LB | | Startup | Is starting up? | Wait before checking | ## Resources / Tài Nguyên - [Detailed Examples](./references/REFERENCE.md) - Full code examples - [Deployment Kubernetes](../deployment-kubernetes/SKILL.md) - K8s patterns - [Docker Traefik](../docker-traefik/SKILL.md) - Container networking - [Inter-service Communication](../inter-service-communication/SKILL.md) - HTTP clients - [Error Handling](../error-handling-patterns/SKILL.md) - Resilience patterns