Service Discovery is a key pattern in microservice architectures, where services need to find each other dynamically. Unlike monolithic applications, where all components are contained within a single executable, microservices are distributed across multiple instances, often across different servers, virtual machines, or containers. Service discovery simplifies the way these services locate one another, allowing for load balancing, failover, and flexibility.
What is Service Discovery?
Service Discovery refers to the process by which services in a distributed system dynamically find each other. Instead of hard-coding the addresses of service instances (which can change in a dynamic environment like Kubernetes), services register themselves with a centralized service registry. Other services query this registry to find the addresses of the services they need to communicate with.
Benefits:
- Scalability: New instances of services can be added dynamically.
- Load Balancing: Requests can be routed to different instances of the same service.
- Fault Tolerance: Dead services can be automatically removed from the registry.
Types of Service Discovery:
Client-Side Discovery
In client-side service discovery, the client directly queries the service registry to discover service instances and choose one to send a request to. This method requires the client to implement the logic for discovery and load balancing.
Server-Side Discovery
In server-side discovery, the client makes a request to a load balancer, and the load balancer queries the service registry to find available instances. The load balancer then forwards the request to an appropriate service instance.
Service Discovery in .NET/C#
In the .NET ecosystem, there are several tools and libraries that help with service discovery. These include:
- Consul
- etcd
- ZooKeeper
- Eureka (Spring Cloud for .NET)
We'll focus on Consul, which is one of the most popular choices for service discovery and distributed configuration.
Example Using Consul
Step 1: Install ConsulBefore diving into C#, we need to install and run a Consul agent. You can download Consul from Consul Download and follow the installation instructions for your operating system.
Once Consul is installed, you can start the agent by running:
consul agent -dev
This command starts a local Consul agent in development mode.Open url http://localhost:8500/ in browser , you will see below ui-
Now, let’s create a .NET Core API project Service.Discovery.Demo.Api.This API will act as a service that needs to be discovered by other applications.
Next, configure the API to register with the Consul upon startup. Add the Consul client package to the project
dotnet add package Consul
In the Program.cs, we'll register a service with Consul. When a service starts, it registers itself with Consul using a unique service ID and some metadata.
builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Consul Configuration builder.Services.AddSingleton<IConsulClient>(p => new ConsulClient(consulConfig => { var consulHost = builder.Configuration["Consul:Host"]; var consulPort = Convert.ToInt32(builder.Configuration["Consul:Port"]); consulConfig.Address = new Uri($"http://{consulHost}:{consulPort}"); })); builder.Services.AddSingleton<IServiceDiscovery, ServiceDiscovery>();
Create a class named ServiceDiscovery that implements the IServiceDiscovery interface to handle service registration:
//IServiceDiscovery interface public interface IServiceDiscovery { Task RegisterServiceAsync(string serviceName, string serviceId, string serviceAddress, int servicePort); Task RegisterServiceAsync(AgentServiceRegistration serviceRegistration); Task DeRegisterServiceAsync(string serviceId); } //IServiceDiscovery Implementation public class ServiceDiscovery : IServiceDiscovery { private readonly IConsulClient _consulClient; public ServiceDiscovery(IConsulClient consulClient) { _consulClient = consulClient; } public async Task RegisterServiceAsync(string serviceName, string serviceId, string serviceAddress, int servicePort) { var registration = new AgentServiceRegistration { ID = serviceId, Name = serviceName, Address = serviceAddress, Port = servicePort }; await _consulClient.Agent.ServiceDeregister(serviceId); await _consulClient.Agent.ServiceRegister(registration); } public async Task RegisterServiceAsync(AgentServiceRegistration serviceRegistration) { await _consulClient.Agent.ServiceDeregister(serviceRegistration.ID); await _consulClient.Agent.ServiceRegister(serviceRegistration); } public async Task DeRegisterServiceAsync(string serviceId) { await _consulClient.Agent.ServiceDeregister(serviceId); } }
Now add the service registration logic in Program.cs file
app.MapControllers(); #region Service Registration var discovery = app.Services.GetRequiredService<IServiceDiscovery>(); var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>(); var serviceName = builder.Configuration["ServiceRegistration:serviceName"]; var serviceId = builder.Configuration["ServiceRegistration:serviceId"]; var serviceAddress = builder.Configuration["ServiceRegistration:servcieHost"]; var servicePort = Convert.ToInt32(builder.Configuration["ServiceRegistration:servciePort"]); lifetime.ApplicationStarted.Register(async () => { var registration = new AgentServiceRegistration { ID = serviceId, Name = serviceName, Address = serviceAddress, Port = servicePort }; await discovery.RegisterServiceAsync(registration); }); lifetime.ApplicationStopping.Register(async () => { await discovery.DeRegisterServiceAsync(serviceId); }); #endregion
In the above code we are getting host, post and other configuration values from appsettings.json file. Add the below configuration in appsettings.json file.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", /* Consul Config*/ "Consul": { "Host": "localhost", "Port": 8500 }, /* Service Registration Config*/ "ServiceRegistration": { "serviceName": "DemoApi", "serviceId": "6a5f4761-ac01-4407-a81b-831a28894f0f", "servcieHost": "locahost", "servciePort": "7087" } }
With these configurations, the API will register itself with the Consul upon startup and deregister upon shutdown.
Next, create a console application named Service.Discovery.Demo.Client and add the Consul client package to the project.
dotnet add package Consul
In the Program.cs file, configure the Consul client to discover services:
using Consul; using (var client = new ConsulClient(consulConfig => { consulConfig.Address = new Uri("http://localhost:8500"); })) { var services = await client.Catalog.Service("DemoAPi"); foreach (var service in services.Response) { Console.WriteLine($"Service ID: {service.ServiceID}, Address: {service.ServiceAddress}, Port: {service.ServicePort}"); } } Console.ReadLine();
This code snippet retrieves all instances of the Api service registered with the Consul.
Step 5: Testing the API and Client Application:Below is the project structure
Now run both applications using the command dotnet run. When this application starts, the Consul portal will display the registered service.
Below is the output of the console application
Conclusion
Service discovery is a crucial component in building microservices at scale. In this post, we covered the basics of service discovery, types of service discovery (client-side and server-side), and demonstrated how to use Consul in a .NET application for registering and discovering services.
Happy coding!! 😊
No comments:
Post a Comment