519 lines
18 KiB
Markdown
519 lines
18 KiB
Markdown
---
|
|
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
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```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 | `<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](./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
|