18 KiB
18 KiB
name, description, compatibility, metadata
| name | description | compatibility | metadata | ||||
|---|---|---|---|---|---|---|---|
| service-discovery | Service Discovery và Service Registry patterns. Use for dynamic service location, health checking, và load balancing trong microservices. | .NET 10+, Consul, Kubernetes, DNS |
|
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
# 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
/// <summary>
/// EN: HTTP client using Kubernetes DNS.
/// VI: HTTP client sử dụng Kubernetes DNS.
/// </summary>
// Program.cs
builder.Services.AddHttpClient<IUserServiceClient, UserServiceClient>(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
/// <summary>
/// EN: Register service with Consul.
/// VI: Đăng ký service với Consul.
/// </summary>
public static class ConsulServiceExtensions
{
public static IServiceCollection AddConsulServiceDiscovery(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddSingleton<IConsulClient, ConsulClient>(sp =>
{
var consulAddress = configuration["Consul:Address"];
return new ConsulClient(cfg =>
{
cfg.Address = new Uri(consulAddress!);
});
});
services.AddHostedService<ConsulRegistrationService>();
return services;
}
}
/// <summary>
/// EN: Background service for Consul registration.
/// VI: Background service cho Consul registration.
/// </summary>
public class ConsulRegistrationService : IHostedService
{
private readonly IConsulClient _consulClient;
private readonly IConfiguration _configuration;
private readonly ILogger<ConsulRegistrationService> _logger;
private string? _registrationId;
public ConsulRegistrationService(
IConsulClient consulClient,
IConfiguration configuration,
ILogger<ConsulRegistrationService> 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
/// <summary>
/// EN: HTTP client with Consul-based service discovery.
/// VI: HTTP client với service discovery dựa trên Consul.
/// </summary>
public class ConsulServiceDiscoveryClient
{
private readonly IConsulClient _consulClient;
private readonly ILogger<ConsulServiceDiscoveryClient> _logger;
public ConsulServiceDiscoveryClient(
IConsulClient consulClient,
ILogger<ConsulServiceDiscoveryClient> logger)
{
_consulClient = consulClient;
_logger = logger;
}
public async Task<Uri?> 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;
}
}
/// <summary>
/// EN: HTTP message handler with service discovery.
/// VI: HTTP message handler với service discovery.
/// </summary>
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<HttpResponseMessage> 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
/// <summary>
/// EN: Health check endpoints for service discovery.
/// VI: Health check endpoints cho service discovery.
/// </summary>
// Program.cs
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("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
// ❌ 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
# ❌ 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
// ❌ 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 | <service-name> |
user-service |
| Different namespace | <service>.<namespace> |
user-service.goodgo |
| Full FQDN | <service>.<namespace>.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 - Full code examples
- Deployment Kubernetes - K8s patterns
- Docker Traefik - Container networking
- Inter-service Communication - HTTP clients
- Error Handling - Resilience patterns