.NET Core 使用 Consul 服务注册发现

Consul是一个用来实现分布式系统服务发现与配置的开源工具。它内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具,使用起来也较为简单。

  • Consul官网:https://www.consul.io

  • 开源地址:https://github.com/hashicorp/consul、https://github.com/G-Research/consuldotnet

安装

Consul支持各种平台的安装,安装文档:https://www.consul.io/downloads,为了快速使用,我这里选择用docker方式安装。

version: "3"services:service_1:image: consulcommand: agent -server -client=0.0.0.0 -bootstrap-expect=3 -node=service_1volumes:- /usr/local/docker/consul/data/service_1:/dataservice_2:image: consulcommand: agent -server -client=0.0.0.0 -retry-join=service_1 -node=service_2volumes:- /usr/local/docker/consul/data/service_2:/datadepends_on:- service_1service_3:image: consulcommand: agent -server -client=0.0.0.0 -retry-join=service_1 -node=service_3volumes:- /usr/local/docker/consul/data/service_3:/datadepends_on:- service_1client_1:image: consulcommand: agent -client=0.0.0.0 -retry-join=service_1 -ui -node=client_1ports:- 8500:8500volumes:- /usr/local/docker/consul/data/client_1:/datadepends_on:- service_2- service_3

提供一个docker-compose.yaml,使用docker-compose up编排脚本启动Consul,如果你不熟悉,可以选择其它方式能运行Consul即可。

这里使用 Docker 搭建 3个 server 节点 + 1 个 client 节点,API 服务通过 client 节点进行服务注册和发现。

安装完成启动Consul,打开默认地址 http://localhost:8500 可以看到Consului界面。

快速使用

添加两个webapi服务,ServiceA和ServiceB,一个webapi客户端Client来调用服务。

dotnet new sln -n consul_demodotnet new webapi -n ServiceA
dotnet sln add ServiceA/ServiceA.csprojdotnet new webapi -n ServiceB
dotnet sln add ServiceB/ServiceB.csprojdotnet new webapi -n Client
dotnet sln add Client/Client.csproj

在项目中添加Consul组件包

Install-Package Consul

服务注册

接下来在两个服务中添加必要的代码来实现将服务注册到Consul中。

首先将Consul配置信息添加到appsettings.json

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

因为我们要将项目都运行在docker中,所以这里的地址要用 host.docker.internal 代替,使用 localhost 无法正常启动,如果不在 docker 中运行,这里就配置层 localhost。

添加一个扩展方法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(), // 唯一IdName = configuration["Consul:Name"], // 服务名Address = configuration["Consul:Ip"], // 服务绑定IPPort = 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;}}
}

然后在Startup.cs中使用扩展方法即可。

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

注意,这里将IConfigurationIHostApplicationLifetime作为参数传进来的,根据实际开发做对应的修改就可以了。

分别在ServiceA和ServiceB都完成一遍上述操作,因为不是实际项目,这里就产生的许多重复代码,在真正的项目开发过程中可以考虑放在一个单独的项目中,ServiceA和ServiceB分别引用,调用。

接着去实现健康检查接口。

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

最后在ServiceA和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);}}
}

这样我们写了两个服务,ServiceA和ServiceB。都添加了健康检查接口和一个自己的服务接口,返回一段json。

我们现在来运行看看效果,可以使用任何方式,只要能启动即可,我这里选择在docker中运行,直接在 Visual Studio中对着两个解决方案右键添加,选择Docker支持,默认会帮我们自动创建好Dockfile,非常方便。

生成的Dockfile文件内容如下:

# ServiceA
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443FROM 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/buildFROM build AS publish
RUN dotnet publish "ServiceA.csproj" -c Release -o /app/publishFROM 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 443FROM 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/buildFROM build AS publish
RUN dotnet publish "ServiceB.csproj" -c Release -o /app/publishFROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ServiceB.dll"]

然后定位到项目根目录,使用命令去编译两个镜像,service_a和service_b

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

看到 Successfully 就成功了,通过docker image ls可以看到我们打包的两个镜像。

这里顺便提一句,已经可以看到我们编译的镜像,service_a和service_b了,但是还有许多名称为<none>的镜像,这些镜像可以不用管它,这种叫做虚悬镜像,既没有仓库名,也没有标签。是因为docker build导致的这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以docker image prune命令删除,这样镜像列表就干净多了。

最后将两个镜像service_a和service_b,分别运行三个实例。

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"

运行成功,接下来就是见证奇迹的时刻,去到Consul看看。

成功将两个服务注册到Consul,并且每个服务都有多个实例。

访问一下接口试试吧,看看能不能成功出现结果。

因为终端编码问题,导致显示乱码,这个不影响,ok,至此服务注册大功告成。

服务发现

搞定了服务注册,接下来演示一下如何服务发现,在Client项目中先将Consul地址配置到appsettings.json中。

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

然后添加一个接口,IService.cs,添加三个方法,分别获取两个服务的返回结果以及初始化服务的方法。

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

实现类: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);}}}}}
}

代码就不解释了,相信都可以看懂,使用了Random类随机获取一个服务,关于这点可以选择更合适的负载均衡方式。

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

一切就绪,添加api访问我们的两个服务。

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

直接在Visual Studio中运行Client项目,在浏览器访问api。

大功告成,服务注册与发现,现在就算之中的某个节点挂掉,服务也可以照常运行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307565.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯-成绩统计

一:题目 题目描述 小蓝给学生们组织了一场考试&#xff0c;卷面总分为 100 分&#xff0c;每个学生的得分都是一个 0 到 100 的整数。 如果得分至少是 60 分&#xff0c;则称为及格。如果得分至少为 85 分&#xff0c;则称为优秀。 请计算及格率和优秀率&#xff0c;用百分数…

Mosh 的 MySQL 课程编程练习题目与答案

这篇文章是我观看 Mosh 的 MySQL 完整版课程进行题目练习的记录&#xff0c;视频的话去 B 站搜索就能找到&#xff0c;数据库文件的话可以从这里下载。 目录第二章2- SELECT 子句3- WHERE 子句4- 逻辑运算符5- IN 运算符6- BETWEEN 运算符7- LIKE 运算符8- REGEXP 运算符&#…

《ASP.NET Core 真机拆解》 送书活动结果公布

截至2020.09.26 本次送书活动 送福利 | 送书5本 ASP.NET Core 真机拆解 。下面把Top 5的留言截图给大家回顾一下。特别鸣谢作者罗志超提供的图书。以下5位同学将获赠书籍一本&#xff1a;傲慢的上校静阿花阿定傲慢与偏见以上同学请在2020年9月30日24&#xff1a;00之前加小二微…

跟我一起学.NetCore之静态文件处理的那些事

前言如今前后端分离开发模式如火如荼&#xff0c;开发职责更加分明&#xff08;当然前后端一起搞的模式也没有完全褪去&#xff09;&#xff1b;而对于每个公司产品实施来说&#xff0c;部署模式会稍有差别&#xff0c;有的会单独将前端文件部署为一个站点&#xff0c;有的会将…

深度学习入门笔记(3)——用梯度下降进行参数更新

首先是对感知器的简单回顾&#xff0c;假设现有的训练集为 D&#xff0c;共有 n 个训练数据&#xff0c;每个数据都有 m 个输入特征和一个输出标签。一个 epoch 就是遍历一次整个训练集&#xff0c;对于每一个训练数据&#xff0c;都计算其预测、计算误差、更新参数。 在一个 e…

ASP.NET Core Blazor Webassembly 之 路由

web最精妙的设计就是通过url把多个页面串联起来&#xff0c;并且可以互相跳转。我们开发系统的时候总是需要使用路由来实现页面间的跳转。传统的web开发主要是使用a标签或者是服务端redirect来跳转。那今天来看看Blazor是如何进行路由的。使用page指定组件的路由path我们可以在…

手撕单例模式(详解)

一:设计模式概述 1:设计模式的概念 软件设计模式&#xff08;Software Design Pattern&#xff09;&#xff0c;又称设计模式&#xff0c;是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题&#xff0…

微软发布.NET 5.0 RC1,未来将只有一个.NET

本周早些时候&#xff0c;微软发布了.NET 5.0 RC1&#xff0c;这是.NET 5 在11 月正式发布之前的第一个“go-live”版本。.NET 5 是.NET 生态系统的统一平台&#xff0c;将所有组件打包成一个跨平台包。新版本包含了.NET Core 3 的很多改进&#xff0c;包括新的语言版本(C# 9 和…

深度学习入门笔记(4)—— Pytorch 和计算图的简单介绍

Pytorch 顾名思义&#xff0c;就是 Torch 7 移植到 Python 的版本&#xff0c;其最大亮点就是自动微分、动态计算图和 Numpy 集成。 Pytorch 相比于 Numpy&#xff0c;优点在于支持 GPU 计算甚至是多设备计算&#xff0c;以及动态计算图。 在 import 的时候&#xff0c;得记住 …

leetcode26. 删除有序数组中的重复项

一:题目 二:上码 // class Solution { // public: // int removeDuplicates(vector<int>& nums) { // //排序 // sort(nums.begin(),nums.end());// //调用unique&#xff08;&#xff09;将vector当中的相同元素 放到 容器的最后面 //…

楼继伟:现有5G技术很不成熟

日前&#xff0c;财政部原部长楼继伟表示&#xff0c;基础设施适度超前是必要的&#xff0c;但有些方面过度超前&#xff0c;抬高了用户成本或不可持续的公共部门债务。现有5G技术很不成熟&#xff0c;数千亿级的投资已经布下&#xff0c;而且运营成本极高&#xff0c;找不到应…

深度学习入门笔记(5)—— Pytorch API 的基本用法框架

第一步&#xff0c;通常是定义我们的神经网络模型。类名后的括号表示我们定义的类会继承 torch.nn.Module&#xff0c;而 super 函数就是调用父类的方法&#xff0c;后面跟 __init__() 就是调用 torch.nn.Module 的构造函数&#xff0c;然后就是我们自定义的模型及其参数。在 f…

跟我一起学.NetCore之路由的最佳实现

前言路由&#xff0c;这词绝对不陌生&#xff0c;不管在前端还是后端都经常提到&#xff0c;而这节不说其他&#xff0c;就聊.NetCore的路由&#xff1b;在之前的Asp.Net MVC 中&#xff0c;路由算是面试时必问的考点&#xff0c;可见其重要性&#xff0c;它的主要作用是映射UR…

leetcode844. 比较含退格的字符串

一:题目 二:上码 class Solution {public:/**思路:1.利用栈 我们将字符串中的单个元素都入栈 当遇到#的时候将将栈顶元素弹出*/bool backspaceCompare(string s, string t) {stack<int> s1;stack<int> s2;for (int i 0; i < s.size(); i) {if(s[i] ! #) s1.pu…

BeetleX之HTTP网关部署

BeetleX组件提供了HTTP网关组件&#xff0c;但使用过程中需要自己写宿主程序来启动它。为了更好的直接使用&#xff0c;组件发布了一个基于.net core 3.1的可运行版本程序并集成了管理插件&#xff1b;只需下载程序即可以在linux或windows下部署一个HTTP网关部署。部署安装运行…

深度学习入门笔记(6)—— Logistic Regression

对比第三节中的 Adaline 和 Logistic Regression&#xff0c;可以发现它们只有两点不同&#xff1a;1、激活函数&#xff0c;Adaline 中的激活函数是恒等函数&#xff08;线性&#xff09;&#xff0c;而 Logistic Regression 中的激活函数是 Sigmoid 函数&#xff08;非线性&a…

微服务模式下,实现前后端多资源服务调用

MicroServices首先&#xff0c;我先解释下&#xff0c;文章标题的意思&#xff1a;咋看起来特别像是一个标题党????&#xff0c;可能是我没想好怎么表达&#xff0c;其实白话文就是&#xff1a;在微服务场景下&#xff0c;肯定会有很多子服务API&#xff0c;那多个前端项目…

leetcode54. 螺旋矩阵(详解)

一:题目 二:上码 class Solution { public:/**思路:1.1 2 3 45 6 7 89 10 11 1213 14 15 162.我们模拟顺时针的顺序,那么得分为4步 上面得一行 (1,2,3)左面得一行 (4,8,12)下面得一行 (16,15,14)右面得一行 (13,9,5)可以看到我们在取数字得时候,我们是每次只取…

深度学习入门笔记(7)—— Multinomial Logistic Regression / Softmax Regression

首先介绍一个非常著名的多分类数据集 MNIST&#xff0c;也就是 0 到 9 的手写数字数据集。每个图像都是 28 * 28&#xff0c;用于Pytorch 数据读取的格式是 NCHW&#xff0c;即 Number、Channel、Height、Weight。 读取图像之后&#xff0c;就能看到一个只有单通道的&#xff0…

[C#.NET 拾遗补漏]09:数据标注与数据校验

数据标注&#xff08;Data Annotation&#xff09;是类或类成员添加上下文信息的一种方式&#xff0c;在 C# 通常用特性&#xff08;Attribute&#xff09;类来描述。它的用途主要可以分为下面这三类&#xff1a;验证 Validation&#xff1a;向数据添加验证规则展现 Display&am…