Consul is an open source tool for discovering and configuring distributed system services. It has built-in service registration and discovery framework, distributed consistency protocol implementation, health check, Key/Value storage, multi-data center solution, no need to rely on other tools, and is relatively simple to use.

  • ConsulWebsite:www.consul.io
  • Open source: github.com/hashicorp/c… , github.com/G-Research/…

The installation

Consul supports the installation of Consul on a variety of platforms. The installation documentation is www.consul.io/downloads. For quick use, I have chosen to install Consul using Docker.

version: "3"

services:
  service_1:
    image: consul
    command: agent -server - client = 0.0.0.0 -bootstrap-expect=3 -node=service_1
    volumes:
      - /usr/local/docker/consul/data/service_1:/data
  service_2:
    image: consul
    command: agent -server - client = 0.0.0.0 -retry-join=service_1 -node=service_2
    volumes:
      - /usr/local/docker/consul/data/service_2:/data
    depends_on:
      - service_1
  service_3:
    image: consul
    command: agent -server - client = 0.0.0.0 -retry-join=service_1 -node=service_3
    volumes:
      - /usr/local/docker/consul/data/service_3:/data
    depends_on:
      - service_1
  client_1:
    image: consul
    command: agent - client = 0.0.0.0 -retry-join=service_1 -ui -node=client_1
    ports:
      - 8500: 8500
    volumes:
      - /usr/local/docker/consul/data/client_1:/data
    depends_on:
      - service_2
      - service_3
Copy the code

Yaml provides a docker-compose up script to start Consul. If you are not familiar with Consul, you can choose another way to run Consul.

Docker is used to build three server nodes and one client node. API services are registered and discovered through the client node.

Consul after installing Consul, open the default address http://localhost:8500 to view the Consului interface.

Quick to use

Add two WebAPI services, ServiceA and ServiceB, and a WebAPI Client to invoke the service.

dotnet new sln -n consul_demo dotnet new webapi -n ServiceA dotnet sln add ServiceA/ServiceA.csproj dotnet new webapi -n  ServiceB dotnet sln add ServiceB/ServiceB.csproj dotnet new webapi -n Client dotnet sln add Client/Client.csprojCopy the code

Add Consul component package to the project

Install-Package Consul
Copy the code

The service registry

Next add the necessary code to both services to register the service into Consul.

First add Consul configuration information to appSettings. json

{
    "Consul": {
        "Address": "http://host.docker.internal:8500"."HealthCheck": "/healthcheck"."Name": "ServiceA"."Ip": "host.docker.internal"}}Copy the code

Since we are going to run the project in docker, we need to replace the address with host.docker.internal. Using localhost cannot start normally, if it is not running in Docker, we need to configure the layer localhost.

Add an extension method UseConul(this IApplicationBuilder app, IConfiguration Configuration, IHostApplicationLifetime).

using System;
using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace ServiceA
{
    public static class Extensions
    {
        public static IApplicationBuilder UseConul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime)
        {
            var client = new ConsulClient(options =>
            {
                options.Address = new Uri(configuration["Consul:Address"]); // Consul client address
            });

            var registration = new AgentServiceRegistration
            {
                ID = Guid.NewGuid().ToString(), / / the only Id
                Name = configuration["Consul:Name"]./ / service name
                Address = configuration["Consul:Ip"].// Service binding IP address
                Port = Convert.ToInt32(configuration["Consul:Port"]), // Service binding port
                Check = new AgentServiceCheck
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5), // How long after the service starts to register
                    Interval = TimeSpan.FromSeconds(10), // Health check interval
                    HTTP = $"http://{configuration["Consul:Ip"]}:{configuration["Consul:Port"]}{configuration["Consul:HealthCheck"]}".// Health check address
                    Timeout = TimeSpan.FromSeconds(5) // The timeout period}};// Register service
            client.Agent.ServiceRegister(registration).Wait();

            // Cancel service registration when the application terminates
            lifetime.ApplicationStopping.Register(() =>
            {
                client.Agent.ServiceDeregister(registration.ID).Wait();
            });

            returnapp; }}}Copy the code

Then use the extension method in startup. cs.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime){... app.UseConul(Configuration, lifetime); }Copy the code

Note that the IConfiguration and IHostApplicationLifetime are passed in as parameters and can be modified accordingly.

Do the same for both ServiceA and ServiceB. Since this is not a real project, a lot of the duplicate code generated here can be considered in a separate project during real project development. ServiceA and ServiceB reference and call each other.

Then implement the health check interface.

// ServiceA
using Microsoft.AspNetCore.Mvc;

namespace ServiceA.Controllers{[Route("[controller]")]
    [ApiController]
    public class HealthCheckController : ControllerBase
    {
        /// <summary>
        ///Health check
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult api()
        {
            returnOk(); }}}Copy the code
// ServiceB
using Microsoft.AspNetCore.Mvc;

namespace ServiceB.Controllers{[Route("[controller]")]
    [ApiController]
    public class HealthCheckController : ControllerBase
    {
        /// <summary>
        ///Health check
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Get()
        {
            returnOk(); }}}Copy the code

Finally, add an interface to both ServiceA and ServiceB.

// ServiceA
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

namespace ServiceA.Controllers{[Route("api/[controller]")]
    [ApiController]
    public class ServiceAController : ControllerBase{[HttpGet]
        public IActionResult Get([FromServices] IConfiguration configuration)
        {
            var result = new
            {
                msg = I was $"{nameof(ServiceA)}, current time:{DateTime.Now:G}",
                ip = Request.HttpContext.Connection.LocalIpAddress.ToString(),
                port = configuration["Consul:Port"]};returnOk(result); }}}Copy the code
// ServiceB
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

namespace ServiceB.Controllers{[Route("api/[controller]")]
    [ApiController]
    public class ServiceBController : ControllerBase{[HttpGet]
        public IActionResult Get([FromServices] IConfiguration configuration)
        {
            var result = new
            {
                msg = I was $"{nameof(ServiceB)}, current time:{DateTime.Now:G}",
                ip = Request.HttpContext.Connection.LocalIpAddress.ToString(),
                port = configuration["Consul:Port"]};returnOk(result); }}}Copy the code

So we wrote two services, ServiceA and ServiceB. Both add a health check interface and a service interface of their own, returning a piece of JSON.

We now run to see the effect, can use any way, as long as it can be started, I choose to run in Docker, directly in Visual Studio on the two solutions to add right click, select Docker support, the default will help us automatically create a good Dockfile, very convenient.

The generated Dockfile contains the following contents:

# ServiceA
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["ServiceA/ServiceA.csproj"."ServiceA/"]
RUN dotnet restore "ServiceA/ServiceA.csproj"
COPY.
WORKDIR "/src/ServiceA"
RUN dotnet build "ServiceA.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "ServiceA.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet"."ServiceA.dll"]
Copy the code
# ServiceB
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["ServiceB/ServiceB.csproj"."ServiceB/"]
RUN dotnet restore "ServiceB/ServiceB.csproj"
COPY.
WORKDIR "/src/ServiceB"
RUN dotnet build "ServiceB.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "ServiceB.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet"."ServiceB.dll"]
Copy the code

Then navigate to the project root directory and use the command to compile the two images, service_a and service_b

docker build -t service_a:dev -f ./ServiceA/Dockerfile .

docker build -t service_b:dev -f ./ServiceB/Dockerfile .
Copy the code

You can see the two images we packed through docker image LS.

By the way, we can already see our compiled images, service_a and service_b, but there are many images named < None > that can be left alone, called dangling images, with no repository name or label. This phenomenon is caused by the Docker build. Because the old and new images have the same name, the old image name is cancelled, resulting in an image with a warehouse name and a < None > tag.

In general, the dangling image has lost its value and can be removed at will by using the Docker image prune command, which makes the image list much cleaner.

Finally, two images, service_a and service_b, run three instances respectively.

docker run -d -p 5050:80 --name service_a1 service_a:dev --Consul:Port="5050"
docker run -d -p 5051:80 --name service_a2 service_a:dev --Consul:Port="5051"
docker run -d -p 5052:80 --name service_a3 service_a:dev --Consul:Port="5052"

docker run -d -p 5060:80 --name service_b1 service_b:dev --Consul:Port="5060"
docker run -d -p 5061:80 --name service_b2 service_b:dev --Consul:Port="5061"
docker run -d -p 5062:80 --name service_b3 service_b:dev --Consul:Port="5062"
Copy the code

It worked, and now it’s time for miracles. Check out Consul.

Two services were successfully registered with Consul with multiple instances of each service.

Try accessing the interface and see if it works.

Because the terminal coding problem, resulting in the display of garbled code, this does not affect, OK, so far the service registration is complete.

Service discovery

With service registration in place, we will demonstrate how to discover the service by configuring Consul’s address to appSettings. json in the Client project.

{
    "Consul": {
        "Address": "http://host.docker.internal:8500"}}Copy the code

Then add an interface, IService. Cs, and add three methods to get the return results of the two services and the methods to initialize the services.

using System.Threading.Tasks;

namespace Client
{
    public interface IService
    {
        /// <summary>
        ///Get ServiceA return data
        /// </summary>
        /// <returns></returns>
        Task<string> GetServiceA();

        /// <summary>
        ///Get the data returned by ServiceB
        /// </summary>
        /// <returns></returns>
        Task<string> GetServiceB();

        /// <summary>
        ///Initializing the service
        /// </summary>
        void InitServices(); }}Copy the code

Implementation class: service.cs

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Consul;
using Microsoft.Extensions.Configuration;

namespace Client
{
    public class Service : IService
    {
        private readonly IConfiguration _configuration;
        private readonly ConsulClient _consulClient;

        private ConcurrentBag<string> _serviceAUrls;
        private ConcurrentBag<string> _serviceBUrls;

        private IHttpClientFactory _httpClient;

        public Service(IConfiguration configuration, IHttpClientFactory httpClient)
        {
            _configuration = configuration;

            _consulClient = new ConsulClient(options =>
            {
                options.Address = new Uri(_configuration["Consul:Address"]);
            });

            _httpClient = httpClient;
        }

        public async Task<string> GetServiceA()
        {
            if (_serviceAUrls == null)
                return await Task.FromResult("ServiceA initializing...");

            using var httpClient = _httpClient.CreateClient();

            var serviceUrl = _serviceAUrls.ElementAt(new Random().Next(_serviceAUrls.Count()));

            Console.WriteLine("ServiceA:" + serviceUrl);

            var result = await httpClient.GetStringAsync($"{serviceUrl}/api/servicea");

            return result;
        }

        public async Task<string> GetServiceB()
        {
            if (_serviceBUrls == null)
                return await Task.FromResult("ServiceB initializing...");

            using var httpClient = _httpClient.CreateClient();

            var serviceUrl = _serviceBUrls.ElementAt(new Random().Next(_serviceBUrls.Count()));

            Console.WriteLine("ServiceB:" + serviceUrl);

            var result = await httpClient.GetStringAsync($"{serviceUrl}/api/serviceb");

            return result;
        }

        public void InitServices()
        {
            var serviceNames = new string[] { "ServiceA"."ServiceB" };

            foreach (var item in serviceNames)
            {
                Task.Run(async() = > {var queryOptions = new QueryOptions
                    {
                        WaitTime = TimeSpan.FromMinutes(5)};while (true)
                    {
                        awaitInitServicesAsync(queryOptions, item); }}); }async Task InitServicesAsync(QueryOptions queryOptions, string serviceName)
            {
                var result = await _consulClient.Health.Service(serviceName, null.true, queryOptions);

                if(queryOptions.WaitIndex ! = result.LastIndex) { queryOptions.WaitIndex = result.LastIndex;var services = result.Response.Select(x => $"http://{x.Service.Address}:{x.Service.Port}");

                    if (serviceName == "ServiceA")
                    {
                        _serviceAUrls = new ConcurrentBag<string>(services);
                    }
                    else if (serviceName == "ServiceB")
                    {
                        _serviceBUrls = new ConcurrentBag<string>(services);
                    }
                }
            }
        }
    }
}
Copy the code

The Random class is used to obtain a service randomly, which can be used to choose a more appropriate load balancing method.

Add interface dependency injection, use initialization service, and so on to startup. cs.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

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

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();

            services.AddHttpClient();

            services.AddSingleton<IService, Service>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IService service)
        {
            if(env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); service.InitServices(); }}}Copy the code

With everything in place, add the API to access our two services.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace Client.Controllers{[Route("api")]
    [ApiController]
    public class HomeController : ControllerBase{[HttpGet]
        [Route("service_result")]
        public async Task<IActionResult> GetService([FromServices] IService service)
        {
            return Ok(new
            {
                serviceA = await service.GetServiceA(),
                serviceB = awaitservice.GetServiceB() }); }}}Copy the code

Run the Client project directly in Visual Studio and access the API in a browser.

Once the service is registered and discovered, it can now run even if one of the nodes fails.