Files
pos-system/microservices/.agent/skills/service-discovery/SKILL.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

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
author version
Velik Ho 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

# 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