.NET Core uses Consul service registration discovery

ConsulIt is an open source tool used to realize distributed system service discovery and configuration. It has built-in service registration and discovery framework, implementation of distributed consistency protocol, health check, Key/Value storage, and multiple data center solutions. It no longer needs to rely on other tools and is relatively simple to use.

  • ConsulOfficial website: https://www.consul.io

  • Open source address: https://github.com/hashicorp/consul, https://github.com/G-Research/consuldotnet

installation

ConsulSupport the installation of various platforms, installation documents: https://www.consul.io/downloads, in order to quickly use, I choose to use docker installation.

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

Provide one docker-compose.yaml, use the docker-compose uporchestration script to start Consul, if you are not familiar, you can choose other ways to run it Consul.

Here, Docker is used to build 3 server nodes + 1 client node, and API services are registered and discovered through the client node.

After the installation is complete and start Consul, open the default address http://localhost:8500 to see the Consului interface.

Quick to use

Add two webapi services, ServiceA and ServiceB, and a webapi client Client to call 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.csproj

Add Consulcomponent packages to the project

Install-Package Consul

Service registration

Next, add the necessary code to the two services to register the service Consul.

First add the Consulconfiguration information toappsettings.json

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

Because we want to run all projects in docker, the address here needs to be replaced by host.docker.internal, and localhost cannot be started normally. If it is not running in docker, configure the layer localhost here.

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

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客户端地址
            });

            var registration = new AgentServiceRegistration
            {
                ID = Guid.NewGuid().ToString(), // 唯一Id
                Name = configuration["Consul:Name"], // 服务名
                Address = configuration["Consul:Ip"], // 服务绑定IP
                Port = Convert.ToInt32(configuration["Consul:Port"]), // 服务绑定端口
                Check = new AgentServiceCheck
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5), // 服务启动多久后注册
                    Interval = TimeSpan.FromSeconds(10), // 健康检查时间间隔
                    HTTP = $"http://{configuration["Consul:Ip"]}:{configuration["Consul:Port"]}{configuration["Consul:HealthCheck"]}", // 健康检查地址
                    Timeout = TimeSpan.FromSeconds(5) // 超时时间
                }
            };

            // 注册服务
            client.Agent.ServiceRegister(registration).Wait();

            // 应用程序终止时,取消服务注册
            lifetime.ApplicationStopping.Register(() =>
            {
                client.Agent.ServiceDeregister(registration.ID).Wait();
            });

            return app;
        }
    }
}

Then Startup.csuse the extension method in.

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

Note that the IConfigurationsum IHostApplicationLifetimeis passed in as a parameter here, and you can modify it according to actual development.

Complete the above operations in ServiceA and ServiceB respectively. Because it is not an actual project, many duplicate codes generated here can be considered in a separate project in the real project development process. ServiceA and ServiceB are quoted and called separately.

Then go to implement the health check interface.

// ServiceA
using Microsoft.AspNetCore.Mvc;

namespace ServiceA.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class HealthCheckController : ControllerBase
    {
        /// <summary>
        /// 健康检查
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult api()
        {
            return Ok();
        }
    }
}
// ServiceB
using Microsoft.AspNetCore.Mvc;

namespace ServiceB.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class HealthCheckController : ControllerBase
    {
        /// <summary>
        /// 健康检查
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Get()
        {
            return Ok();
        }
    }
}

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 = $"我是{nameof(ServiceA)},当前时间:{DateTime.Now:G}",
                ip = Request.HttpContext.Connection.LocalIpAddress.ToString(),
                port = configuration["Consul:Port"]
            };

            return Ok(result);
        }
    }
}
// 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 = $"我是{nameof(ServiceB)},当前时间:{DateTime.Now:G}",
                ip = Request.HttpContext.Connection.LocalIpAddress.ToString(),
                port = configuration["Consul:Port"]
            };

            return Ok(result);
        }
    }
}

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

Let's run it now to see the effect. Any method can be used as long as it can be started. Here I choose to run in docker, right-click the two solutions in Visual Studio to add, select Docker support, and it will automatically help us by default. It is very convenient to create a Dockfile.

The contents of the generated Dockfile file are as follows:

# 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"]
# 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"]

Then locate the root directory of the project and use the command to compile 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 .

After seeing Successfully, we docker image lscan see the two images we packaged.

By the way, we can already see the mirrors we compiled, service_a and service_b, but there are still many <none>mirrors named as , and these mirrors can be ignored. This kind of mirror is called a dangling mirror, which has neither a warehouse name nor a label. It is because docker buildof this phenomenon. As the old and the new image of the same name, the name of the old mirror was canceled, which appeared warehouse name tags are <none>mirrored.

Generally speaking, dangling mirrors have lost their value of existence, and can be deleted at will. You can docker image pruneorder to delete them, so that the mirror list is much cleaner.

Finally, two mirrors, 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"

After successful operation, the next moment is to witness the miracle. Go and Consulsee.

Successfully registered two services Consul, and each service has multiple instances.

Try to access the interface and see if the result can be successfully displayed.

Because of terminal encoding problems, garbled characters are displayed. This does not affect, ok, and the service registration is complete.

Service discovery

After finishing the service registration, let's demonstrate how to service discovery. First, Consulconfigure the address in the Client project appsettings.json.

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

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

using System.Threading.Tasks;

namespace Client
{
    public interface IService
    {
        /// <summary>
        /// 获取 ServiceA 返回数据
        /// </summary>
        /// <returns></returns>
        Task<string> GetServiceA();

        /// <summary>
        /// 获取 ServiceB 返回数据
        /// </summary>
        /// <returns></returns>
        Task<string> GetServiceB();

        /// <summary>
        /// 初始化服务
        /// </summary>
        void InitServices();
    }
}

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正在初始化...");

            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正在初始化...");

            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)
                    {
                        await InitServicesAsync(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);
                    }
                }
            }
        }
    }
}

The code is not explained, I believe you can understand it, using the Randomclass to get a service randomly, you can choose a more appropriate load balancing method for this.

In Startup.csAdding interface dependency injection, using an initialization service code.

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();
        }
    }
}

Everything is ready, add 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 = await service.GetServiceB()
            });
        }
    }
}

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

You're done, service registration and discovery, now even if one of the nodes is down, the service can still run as usual.

Guess you like

Origin blog.csdn.net/sD7O95O/article/details/108839264