How do you implement service discovery?
4 minadvancedmicroservicesservice-discoveryinfrastructure
Quick Answer
Service discovery lets services find each other's network locations dynamically instead of hard-coding addresses. In client-side discovery, services query a registry (e.g., Consul, Eureka) and pick an instance; in server-side discovery, a load balancer or platform (e.g., Kubernetes Services/DNS) resolves and routes for them. It enables elastic scaling and resilience as instances come and go.
Detailed Answer
Service Discovery allows services to find and communicate with each other without hard-coding network locations. Services register themselves and discover other services dynamically.
1. Client-Side Discovery with Consul:
// Install: Install-Package Consul
// Service Registration
public class ConsulServiceRegistration : IHostedService
{
private readonly IConsulClient _consulClient;
private readonly IConfiguration _configuration;
private string _registrationId;
public async Task StartAsync(CancellationToken cancellationToken)
{
var serviceName = _configuration["ServiceName"];
var serviceId = $"{serviceName}-{Guid.NewGuid()}";
var registration = new AgentServiceRegistration
{
ID = serviceId,
Name = serviceName,
Address = _configuration["ServiceAddress"],
Port = int.Parse(_configuration["ServicePort"]),
Check = new AgentServiceCheck
{
HTTP = $"http://{_configuration["ServiceAddress"]}:{_configuration["ServicePort"]}/health",
Interval = TimeSpan.FromSeconds(10),
Timeout = TimeSpan.FromSeconds(5)
}
};
await _consulClient.Agent.ServiceDeregister(serviceId, cancellationToken);
await _consulClient.Agent.ServiceRegister(registration, cancellationToken);
_registrationId = serviceId;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _consulClient.Agent.ServiceDeregister(_registrationId, cancellationToken);
}
}
// Service Discovery
public class ConsulServiceDiscovery
{
private readonly IConsulClient _consulClient;
public async Task GetServiceUriAsync(string serviceName)
{
var services = await _consulClient.Health.Service(serviceName, tag: null, passingOnly: true);
if (!services.Response.Any())
{
throw new Exception($"Service {serviceName} not found");
}
// Simple round-robin
var service = services.Response[Random.Shared.Next(services.Response.Length)];
return new Uri($"http://{service.Service.Address}:{service.Service.Port}");
}
}
// Usage in HttpClient
public class OrderServiceClient
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ConsulServiceDiscovery _serviceDiscovery;
public async Task GetOrderAsync(int orderId)
{
var serviceUri = await _serviceDiscovery.GetServiceUriAsync("order-service");
var client = _httpClientFactory.CreateClient();
client.BaseAddress = serviceUri;
return await client.GetFromJsonAsync($"/api/orders/{orderId}");
}
}
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(p => new ConsulClient(config =>
{
config.Address = new Uri("http://consul:8500");
}));
services.AddSingleton();
services.AddHostedService();
}
2. Server-Side Discovery with Kubernetes:
# Kubernetes Service Definition
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- protocol: TCP
port: 80
targetPort: 5000
type: ClusterIP
// In .NET Core, services discover each other via Kubernetes DNS
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("OrderService", c =>
{
// Kubernetes DNS: ..svc.cluster.local
c.BaseAddress = new Uri("http://order-service.default.svc.cluster.local");
});
}
3. Using Eureka (Netflix OSS):
// Install: Install-Package Steeltoe.Discovery.Eureka
// appsettings.json
{
"spring": {
"application": {
"name": "product-service"
}
},
"eureka": {
"client": {
"serviceUrl": "http://eureka-server:8761/eureka/",
"shouldRegisterWithEureka": true,
"shouldFetchRegistry": true
},
"instance": {
"port": 5000,
"preferIpAddress": true,
"healthCheckUrlPath": "/health"
}
}
}
// Program.cs
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDiscoveryClient(builder.Configuration);
builder.Services.AddHttpClient();
var app = builder.Build();
app.Run();
}
}
// Service Client with Discovery
public class ProductServiceClient
{
private readonly IDiscoveryClient _discoveryClient;
private readonly IHttpClientFactory _httpClientFactory;
public ProductServiceClient(IDiscoveryClient discoveryClient, IHttpClientFactory httpClientFactory)
{
_discoveryClient = discoveryClient;
_httpClientFactory = httpClientFactory;
}
public async Task GetProductAsync(int productId)
{
var instances = await _discoveryClient.GetInstancesAsync("product-service");
var instance = instances.FirstOrDefault();
if (instance == null)
throw new Exception("No instances of product-service available");
var client = _httpClientFactory.CreateClient();
var uri = new Uri($"{instance.Uri}/api/products/{productId}");
return await client.GetFromJsonAsync(uri);
}
}
4. Custom Service Registry:
// Simple in-memory service registry
public interface IServiceRegistry
{
Task RegisterServiceAsync(ServiceRegistration registration);
Task DeregisterServiceAsync(string serviceId);
Task<List> DiscoverServiceAsync(string serviceName);
}
public class InMemoryServiceRegistry : IServiceRegistry
{
private readonly ConcurrentDictionary _services = new();
public Task RegisterServiceAsync(ServiceRegistration registration)
{
_services[registration.ServiceId] = registration;
return Task.CompletedTask;
}
public Task DeregisterServiceAsync(string serviceId)
{
_services.TryRemove(serviceId, out _);
return Task.CompletedTask;
}
public Task<List> DiscoverServiceAsync(string serviceName)
{
var instances = _services.Values
.Where(s => s.ServiceName == serviceName && s.IsHealthy)
.Select(s => new ServiceInstance
{
ServiceId = s.ServiceId,
Host = s.Host,
Port = s.Port
})
.ToList();
return Task.FromResult(instances);
}
}
public class ServiceRegistration
{
public string ServiceId { get; set; }
public string ServiceName { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public bool IsHealthy { get; set; }
public DateTime LastHeartbeat { get; set; }
}