Building Microservices On .NET Core – Part 3 Service Discovery with Eureka

Building Microservices On .NET Core – Part 3 Service Discovery with Eureka

This is the third article in our series about building microservices on .NET Core. In the first article we introduced the series and prepared the plan: business case and solution architecture. In the second article we described how you can structure internal architecture of one microservice using CQRS pattern and MediatR library.

In this article we are going to focus on service discovery, which is one of the fundamental concepts of microservice based architecture.

Source code for complete solution can be found on our GitHub.

 

What is service discovery and do I need one?

If you are building microservice based solution sooner or later (rather sooner) you will encounter a situation when one service need to talk to another. In order to do this caller must know the exact location of a target microservice in a network they operate in.
You must somehow provide IP address and port where target microservice listens for requests. You can do it using configuration file or environment variables, but this approach has some drawbacks and limitations.

First is that you have to maintain and properly deploy configuration files for all your environments: local development, test, pre-production and production. Forgetting to update any of these configurations when adding a new service or moving existing one to a different node will result in errors discovered at runtime.
Second, more important issue, is that it works only in static environment, meaning you cannot dynamically add/remove nodes, therefore you won’t be able to dynamically scale your system. Ability to scale and deploy given microservice autonomously is one of key advantages of microservice based architecture, and we do not want to lose this ability.

Therefore we need to introduce service discovery. Service discovery is a mechanism that allows services to find each others network location. There are many possible implementations of this pattern, but in this article we will focus on implementation that consist of Service Registry component and Service Registry Clients.
Service Registry is a central component that maintains list of microservices instances currently running with their corresponding network locations. Service Registry Clients are used by your microservices to: register itself in registry, to query registry for address of a given microservices they need to communicate with.

There are many existing implementations of service registry available. Unfortunately I do not know of any native .NET solution. In Java world there are two solutions we use at Altkom Software & Consulting in many projects: Netflix Eureka and HashiCorp Consul.

For the purpose of this article we will use Eureka as it has a very good client library for .NET Core developed and maintained by Pivotal (creators of Spring Framework) –  Steeltoe.

 

Setting up a Service Registry with Eureka

The simplest way to start Eureka Server is clone a GitHub repository and run with Maven wrapper which is attached to this project. This allows you to run the Maven project without having Maven installed and present on the path.

We cloned the repository to the eureka folder, so in our case, it is enough:

cd eureka
mvnw spring-boot:run

Eureka Server configuration is stored in a file application.yml.

server:
  port: 8761

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
  server:
    waitTimeInMsWhenSyncEmpty: 0

If you want to check that Eureka started correctly, go to localhost:8761. You should show something like this:

spring Eureka

 

Registering microservice in Eureka / Register Eureka Clients

We used Steeltoe’s Eureka client implementation to register and fetch services from Eureka Server. In this section we focus on registration in service registry. We do it all on the example of one of our microservices – PricingService.

First step is adding required NuGet packages.

dotnet add package Steeltoe.Discovery.ClientCore --version 2.1.1

Second step is adding discovery client in the Startup.cs class. Here we must add two lines, one to ConfigureServices method and second to Configure method.

using Steeltoe.Discovery.Client;

namespace PricingService
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDiscoveryClient(Configuration);
            [...]
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
           [...]
            app.UseDiscoveryClient();
        }
    }
}

Last step and the most important one is setting up configuration in appsettings.json

"spring" : {
    "application" : {
      "name" : "PricingService"
    }
  },
  "eureka" : {
    "client" : {
      "shouldRegisterWithEureka" : true,
      "serviceUrl" : "http://localhost:8761/eureka",
      "ValidateCertificates":  false
    },
    "instance" : {
      "appName" : "PricingService",
      "hostName" : "localhost",
      "port" : "5040”
    }
  }

Configuration consist of the following elements:

  • spring.application.name: contains name of our service
  • eureka.client contains
    • shouldRegisterWithEureka: tells if our service should register itself in Eureka, if we only want to call other services we can set it to false, if we want other services to be able to call our service then we must set it to true,
    • serviceUrl: address of Eureka service
    • instance: tells how our service should be registered in Eureka, we specify:
      • appName, with this name other services will be able to query for address of our service
      • hostName, name of host our service is running
      • port, port which our service is using

At first I was a bit disappointed that I have to specify host name and port my service is using instead of Eureka client being able to dynamically discover this information at runtime, but it turn out to be a very useful feature.
While running your services in a docker container we do not want them to register ports and addresses local to container instance but addresses and ports as visible on a docker network.
More information concerning configuration options can be found in Steeltoe docs.

Now we can run our microservice using, for example, command line. From root repository folder:

dotnet run --project ./PricingService

and open Eureka to see if your service is visible. Open your browser and go to localhost:8761

eureca

If everything worked as expected, an instance of your service should be listed in the “Instances currently registered with Eureka” section.

You can create a health check endpoint so that Eureka could check if you service is up and running. This would enable almost real-time monitoring of available services and their status via Eureka dashboard.
If you implement such endpoint you can specify it using healthCheckUrl configuration property.

 

Call another microservice using service discovery

Our first step is adding Eureka client configuration in the same way as we did for PricingService.

After previous steps we are ready to call our service from another one. We want to call PricingService from PolicyService, because on one of policy creation step we need some data about price that are in PricingService.
We are going to combine forces Steeltoe discovery client, RestEase and Polly to create a nice, declarative and resilient client for our PricingService.

using Microsoft.Extensions.Configuration;
using Polly;
using PricingService.Api.Commands;
using RestEase;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Steeltoe.Common.Discovery;

namespace PolicyService.RestClients
{
    public interface IPricingClient
    {
        [Post]
        Task CalculatePrice([Body] CalculatePriceCommand cmd);
    }

    public class PricingClient : IPricingClient
    {
        private readonly IPricingClient client;

        private static Policy retryPolicy = Policy
            .Handle()
            .WaitAndRetryAsync(retryCount: 3, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(3));

        public PricingClient(IConfiguration configuration, IDiscoveryClient discoveryClient)
        {
            var handler = new DiscoveryHttpClientHandler(discoveryClient);
            var httpClient = new HttpClient(handler, false)
            {
                BaseAddress = new Uri(configuration.GetValue("PricingServiceUri"))
            };
            client = RestClient.For(httpClient);
        }

        public Task CalculatePrice([Body] CalculatePriceCommand cmd)
        {
            return retryPolicy.ExecuteAsync(async () => await client.CalculatePrice(cmd));
        }
    }
}

We declared an interface that represents operations exposed by PricingService.

We created implementation of this interface that internally uses service discovery client that will fetch address of PricingService from service registry in Eureka, combined with Polly and RestEase.

The most important lines from this example is handler and HTTP client creation:

var handler = new DiscoveryHttpClientHandler(discoveryClient);
var httpClient = new HttpClient(handler, false)
{
    BaseAddress = new Uri(configuration.GetValue("PricingServiceUri"))
};

We use the URL to PricingService configured in appsettings.json and looks like this:

"PricingServiceUri" : "http://PricingService/api/pricing"

As you can see address in configuration points to service name not to actual network address.

Last step is registration of our client in IoC container. For this purpose we create a class called RestClientsInstaller.

    public static class RestClientsInstaller
    {
        public static IServiceCollection AddPricingRestClient(this IServiceCollection services)
        {
            services.AddSingleton(typeof(IPricingClient), typeof(PricingClient));
...
            return services;
        }
    }

and we use this class in our Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddDiscoveryClient(Configuration);
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            services.AddMediatR();
            services.AddPricingRestClient();
            services.AddNHibernate(Configuration.GetConnectionString("DefaultConnection"));
            services.AddRabbit();
        }

Now you can inject IPricingClient and use it to call PricingService.

 

Summary

Service discovery is one of the fundamentals of microservices based architecture. Without it there is a lot of manual and error prone work required to properly configure and deploy microservices.

Setting up service discovery with Eureka and Steeltoe is pretty easy and gives you ability to dynamically add and remove service instances. Also it removes the need to hard code addresses of services that need to communicate with.
Check out complete solution source code at:  https://github.com/asc-lab/dotnetcore-microservices-poc.

Of course, this solution is just one of the possible ways of implementation service discovery mechanism. You can also try alternative approaches with Kubernetes, nginx or Consul.

Authors:

Wojciech Suwała, Head Architect, ASC LAB

Robert Witkowski, Senior Software Engineer, ASC LAB

1 Star2 Stars3 Stars4 Stars5 Stars (2 votes, average: 5.00 out of 5)
Loading...

Have you enjoyed the article? If yes, please share it with your network!