本文将为您提供关于.NETCore使用Consul服务注册发现的详细介绍,我们还将为您解释.net服务注册与发现的相关知识,同时,我们还将为您提供关于.NETCore+Consul服务注册与发现、.N
本文将为您提供关于.NET Core 使用 Consul 服务注册发现的详细介绍,我们还将为您解释.net 服务注册与发现的相关知识,同时,我们还将为您提供关于.NET Core + Consul 服务注册与发现、.Net Core Grpc Consul 实现服务注册 服务发现 负载均衡、.Net Core Ocelot Consul 实现API网关 服务注册 服务发现 负载均衡、.Net Core 商城微服务项目系列(二):使用Ocelot + Consul构建具备服务注册和发现功能的网关的实用信息。
本文目录一览:- .NET Core 使用 Consul 服务注册发现(.net 服务注册与发现)
- .NET Core + Consul 服务注册与发现
- .Net Core Grpc Consul 实现服务注册 服务发现 负载均衡
- .Net Core Ocelot Consul 实现API网关 服务注册 服务发现 负载均衡
- .Net Core 商城微服务项目系列(二):使用Ocelot + Consul构建具备服务注册和发现功能的网关
.NET Core 使用 Consul 服务注册发现(.net 服务注册与发现)
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: 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
提供一个 docker-compose.yaml
,使用 docker-compose up
编排脚本启动 Consul
,如果你不熟悉,可以选择其它方式能运行 Consul
即可。
这里使用 Docker 搭建 3 个 server 节点 + 1 个 client 节点,API 服务通过 client 节点进行服务注册和发现。
安装完成启动 Consul
,打开默认地址 可以看到 Consul
ui 界面。
快速使用
添加两个 webapi 服务,ServiceA 和 ServiceB,一个 webapi 客户端 Client 来调用服务。
dotnet new sln -n consul_demodotnet new webapi -n ServiceAdotnet sln add ServiceA/ServiceA.csprojdotnet new webapi -n ServiceBdotnet sln add ServiceB/ServiceB.csprojdotnet new webapi -n Clientdotnet 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(), // 唯一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; } }}
然后在 Startup.cs
中使用扩展方法即可。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime){ ... app.UseConul(Configuration, lifetime);}
注意,这里将 IConfiguration
和 IHostApplicationLifetime
作为参数传进来的,根据实际开发做对应的修改就可以了。
分别在 ServiceA 和 ServiceB 都完成一遍上述操作,因为不是实际项目,这里就产生的许多重复代码,在真正的项目开发过程中可以考虑放在一个单独的项目中,ServiceA 和 ServiceB 分别引用,调用。
接着去实现健康检查接口。
// ServiceAusing Microsoft.AspNetCore.Mvc;namespace ServiceA.Controllers{ [Route("[controller]")] [ApiController] public class HealthCheckController : ControllerBase { /// <summary> /// 健康检查 /// </summary> /// <returns></returns> [HttpGet] public IActionResult api() { return Ok(); } }}
// ServiceBusing 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 中都添加一个接口。
// ServiceAusing 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); } }}
// ServiceBusing 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 文件内容如下:
# ServiceAFROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS baseWORKDIR /appEXPOSE 80EXPOSE 443FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS buildWORKDIR /srcCOPY ["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 publishRUN dotnet publish "ServiceA.csproj" -c Release -o /app/publishFROM base AS finalWORKDIR /appCOPY --from=publish /app/publish .ENTRYPOINT ["dotnet", "ServiceA.dll"]
# ServiceBFROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS baseWORKDIR /appEXPOSE 80EXPOSE 443FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS buildWORKDIR /srcCOPY ["ServiceB/ServiceB.csproj", "ServiceB/"]RUN dotnet restore "ServiceB/ServiceB.cspro.........
.NET Core + Consul 服务注册与发现
在分布式架构中,服务治理是必须面对的问题,如果缺乏简单有效治理方案,各服务之间只能通过人肉配置的方式进行服务关系管理,当遇到服务关系变化时,就会变得极其麻烦且容易出错。
Consul[1] 是一个用来实现分布式系统服务发现与配置的开源工具。它内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如 ZooKeeper 等),使用起来也较为简单。
Consul 架构
Consul 集群支持多数据中心,在上图中有两个 DataCenter,他们通过 Internet 互联,为了提高通信效率,只有 Server 节点才加入跨数据中心的通信。在单个数据中心中,Consul 分为 Client 和 Server 两种节点(所有的节点也被称为 Agent),Server 节点保存数据,Client 负责健康检查及转发数据请求到 Server,本身不保存注册信息;Server 节点有一个 Leader 和多个 Follower,Leader 节点会将数据同步到 Follower,Server 节点的数量推荐是3个或者5个,在 Leader 挂掉的时候会启动选举机制产生一个新 Leader。
Consul 集群搭建
这里使用 Docker 搭建 3个 Server 节点 + 1 个 Client 节点,API 服务通过 Client 节点进行服务注册和发现。
从 Docker Hub 拉取 Consul 镜像
docker pull consul
启动 3个 Server 节点 + 1 个 Client 节点
docker-compose.yaml
如下:
version: ''3''
services:
cs1:
image: consul
command: agent -server -client=0.0.0.0 -bootstrap-expect=3 -node=cs1 -data-dir=/data
volumes:
- /usr/local/docker/consul/data/cs1:/data
cs2:
image: consul
command: agent -server -client=0.0.0.0 -retry-join=cs1 -node=cs2 -data-dir=/data
volumes:
- /usr/local/docker/consul/data/cs2:/data
depends_on:
- cs1
cs3:
image: consul
command: agent -server -client=0.0.0.0 -retry-join=cs1 -node=cs3 -data-dir=/data
volumes:
- /usr/local/docker/consul/data/cs3:/data
depends_on:
- cs1
cc1:
image: consul
command: agent -client=0.0.0.0 -retry-join=cs1 -ui -node=cc1 -data-dir=/data
ports:
- 8500:8500
volumes:
- /usr/local/docker/consul/data/cc1:/data
depends_on:
- cs2
- cs3
主要参数说明:
参数名 | 解释 |
---|---|
-server | 设置为 Server 类型节点,不加则为 Client 类型节点 |
-client | 注册或者查询等一系列客户端对它操作的IP,默认是127.0.0.1 |
-bootstrap-expect | 集群期望的 Server 节点数,只有达到这个值才会选举 Leader |
-node | 指定节点名称 |
-data-dir | 数据存放位置 |
-retry-join | 指定要加入的节点地址(组建集群) |
-ui | 启用 UI 界面 |
集群状态
(e002ca62ac24 为容器名称,可通过 docker ps | grep consul 查看,选择任意一个即可)
查看节点状态和类型
docker exec -t e002ca62ac24 consul members
当前为3 个 Server 类型节点 ,1 个 Client 类型节点。
查看 Server 节点类型
docker exec -t e002ca62ac24 consul operator raft list-peers
当前为 cs1 为 leader,可以测试将 cs1 stop 观察 leader 的重新选举。
通过 http://192.168.124.9:8500
UI 界面查看 Consul 节点状态如下:(192.168.124.9 是 consul 容器外部访问 IP)
.NET Core 接入 Consul
创建 .NET Core WebAPI(3.1) 服务 ServiceA(2个实例) 和 ServiceB
NuGet 安装 Consul
注册到 Consul 的核心代码如下(源码下载[2]):
public static class ConsulBuilderExtensions
{
public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IHostApplicationLifetime lifetime, ConsulOption consulOption)
{
var consulClient = new ConsulClient(x =>
{
x.Address = new Uri(consulOption.Address);
});
var registration = new AgentServiceRegistration()
{
ID = Guid.NewGuid().ToString(),
Name = consulOption.ServiceName,// 服务名
Address = consulOption.ServiceIP, // 服务绑定IP
Port = consulOption.ServicePort, // 服务绑定端口
Check = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册
Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔
HTTP = consulOption.ServiceHealthCheck,//健康检查地址
Timeout = TimeSpan.FromSeconds(5)
}
};
// 服务注册
consulClient.Agent.ServiceRegister(registration).Wait();
// 应用程序终止时,服务取消注册
lifetime.ApplicationStopping.Register(() =>
{
consulClient.Agent.ServiceDeregister(registration.ID).Wait();
});
return app;
}
}添加配置如下:
"Consul": {
"ServiceName": "service-a",
"ServiceIP": "192.168.124.11", // 当前服务访问 IP
"ServicePort": 8000,
"ServiceHealthCheck": "http://192.168.124.11:8000/healthCheck",
"Address": "http://192.168.124.9:8500"
}注册成功结果如下:
ServiceB 调用 ServiceA 接口
ServiceB 通过 ConsulClient 进行服务发现,获取到 ServiceA 的地址,然后随机请求请求任意一个实例,关键代码如下:
using (var consulClient = new ConsulClient(a => a.Address = new Uri(_consulOption.Address)))
{
var services = consulClient.Catalog.Service("service-a").Result.Response;
if (services != null && services.Any())
{
// 模拟随机一台进行请求,这里只是测试,可以选择合适的负载均衡框架
var service = services.ElementAt(new Random().Next(services.Count()));
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync($"http://{service.ServiceAddress}:{service.ServicePort}/test/get");
var result = await response.Content.ReadAsStringAsync();
return result;
}
}多次调用结果如下:
参考资料
Consul: https://www.consul.io/
[2]源码下载: https://github.com/beckjin/ConsulDotnetSamples
本文分享自微信公众号 - dotNET跨平台(opendotnet)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
.Net Core Grpc Consul 实现服务注册 服务发现 负载均衡
本文是基于..net core grpc consul 实现服务注册 服务发现 负载均衡 (二) 的,很多内容是直接复制过来的,..net core grpc consul 实现服务注册 服务发现 负载均衡 (二) 的版权属于原作者,此文的版权归属我及 @蜗牛丨大神,因此,转载前请必要声明 @蜗牛丨大神及本人。谢谢。
文章内容如下:
在上一篇 .net core grpc 实现通信 (一) 中,我们实现的 grpc 通信在.net core 中的可行性,但要在微服务中真正使用,还缺少 服务注册,服务发现及负载均衡等,本篇我们将在 .net core grpc 通信 的基础上加上 服务注册,服务发现,负载均衡。
如对.net core grpc 通信不太熟悉的,可以看上一篇 .net core grpc 实现通信 (一) ,然后再看本篇。
grpc (https://grpc.io/) 是 google 发布的一个开源、高性能、通用 RPC(Remote Procedure Call)框架,使用 HTTP/2 协议,支持多路复用,并用 ProtoBuf 作为序列化工具,提供跨语言、跨平台支持。
Consul (https://www.consul.io) 是一个分布式,高可用、支持多数据中心的服务注册、发现、健康检查和配置共享的服务软件,由 HashiCorp 公司用 Go 语言开发。
本次服务注册、发现 通过 Consul Api 来实现,开发过程中结合.net core 依赖注入,切面管道思想等。
软件版本
.net core:3.0
grpc:1.20.1
Consul:1.5.0
Consul Nuget 注册组件:0.7.2.6
项目结构
.net core 代码部分:
Snai.GrpcClient 客户端 .net core 3.0 控制台程序
Snai.GrpcService.Hosting 服务端宿主,Api 服务注册,asp.net core 3.0 网站程序
Snai.GrpcService.Impl 协议方法实现 .net standard 2.0 类库
Snai.GrpcService.Protocol 生成协议方法 .net standard 2.0 类库
Consul:(我的 Windows x64 版本下载下来压缩包里面只有一个 consul.exe 文件,可能原作者使用的是 Linux 版本或当时环境下的版本导致)
conf 配置目录,本次用 api 注册服务,可以删除
data 缓存数据目录,可清空里面内容
dist Consul UI 目录,本次用默认的 UI,可以删除
consul.exe 注册软件
startup.bat 执行脚本
项目实现
一、服务端
服务端主要包括 Grpc 服务端,Consul Api 服务注册、健康检查等。
新建 Snai.GrpcService 解决方案,由于这次加入了 Consul Api 服务注册,所以我们先从 Api 服务注册开始。
1、实现 Consul Api 服务注册
新建 Snai.GrpcService.Hosting 基于 Asp.net Core 3.0 空网站,在 依赖项 右击 管理 NuGet 程序包 浏览 找到 Consul 版本 0.7.2.6 安装,用于 Api 服务注册使用
编辑 appsettings.json 配置文件,配置 GrpcService Grpc 服务端 IP 和端口,HealthService 健康检测名称、IP 和地址,ConsulService Consul 的 IP 和端口,代码如下
1 {
2 "GrpcService": {
3 "IP": "localhost",
4 "Port": "5031"
5 },
6 "HealthService": {
7 "Name": "GrpcService",
8 "IP": "localhost",
9 "Port": "5021"
10 },
11 "ConsulService": {
12 "IP": "localhost",
13 "Port": "8500"
14 },
15 "Logging": {
16 "LogLevel": {
17 "Default": "Information",
18 "Microsoft": "Warning",
19 "Microsoft.Hosting.Lifetime": "Information"
20 }
21 },
22 "AllowedHosts": "*"
23 }
新建 Consul 目录,用于放 Api 注册相关代码
在 Consul 目录下新建 Entity 目录,在 Entity 目录下新建 HealthService.cs,ConsulService.cs 类,分别对应 HealthService,ConsulService 两个配置项,代码如下
HealthService.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5
6 namespace Snai.GrpcService.Hosting.Consul.Entity
7 {
8 public class HealthService
9 {
10 public string Name { get; set; }
11 public string IP { get; set; }
12 public int Port { get; set; }
13
14 }
15 }
ConsulService.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5
6 namespace Snai.GrpcService.Hosting.Consul.Entity
7 {
8 public class ConsulService
9 {
10 public string IP { get; set; }
11 public int Port { get; set; }
12 }
13 }
在 Consul 目录下新建 AppRregister.cs 类,添加 IApplicationBuilder 扩展方法 RegisterConsul,来调用 Consul Api 实现服务注册,代码如下
1 using Consul;
2 using Microsoft.AspNetCore.Builder;
3 using Microsoft.Extensions.Hosting;
4 using Microsoft.Extensions.Options;
5 using Snai.GrpcService.Hosting.Consul.Entity;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Threading.Tasks;
10
11 namespace Snai.GrpcService.Hosting.Consul
12 {
13 public static class AppRregister
14 {
15 // 服务注册
16 public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IHostApplicationLifetime lifetime, IOptions<HealthService> healthService, IOptions<ConsulService> consulService)
17 {
18 var consulClient = new ConsulClient(cfg => cfg.Address = new Uri($"http://{consulService.Value.IP}:{consulService.Value.Port}"));//请求注册的 Consul 地址
19 var httpCheck = new AgentServiceCheck()
20 {
21 DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册
22 Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔
23 HTTP = $"http://{healthService.Value.IP}:{healthService.Value.Port}/health",//健康检查地址
24 Timeout = TimeSpan.FromSeconds(5)
25 };
26
27 // Register service with consul
28 var registration = new AgentServiceRegistration()
29 {
30 Checks = new[] { httpCheck },
31 ID = healthService.Value.Name + "_" + healthService.Value.Port,
32 Name = healthService.Value.Name,
33 Address = healthService.Value.IP,
34 Port = healthService.Value.Port,
35 Tags = new[] { $"urlprefix-/{healthService.Value.Name}" }//添加 urlprefix-/servicename 格式的 tag 标签,以便 Fabio 识别
36 };
37
38 consulClient.Agent.ServiceRegister(registration).Wait();//服务启动时注册,内部实现其实就是使用 Consul API 进行注册(HttpClient发起)
39 lifetime.ApplicationStopping.Register(() =>
40 {
41 consulClient.Agent.ServiceDeregister(registration.ID).Wait();//服务停止时取消注册
42 });
43
44 return app;
45 }
46 }
47 }
修改 Startup.cs 代码
加入 Startup (IConfiguration configuration) 构造函数,实现配置注入,如果建的是 Web Api 或 MVC 网站,默认是有的
修改 ConfigureServices (IServiceCollection services) 方法,注册全局配置
修改 Configure () 方法,添加健康检查路由地址 app.Map ("/health", HealthMap),调用 RegisterConsul 扩展方法实现服务注册
添加 HealthMap (IApplicationBuilder app) 实现 health 路由。由于只有一个健康检查地址,所以没有建 Web Api 网站,只建了个空网站
代码如下,注册配置 GrpcService 、 注册 Rpc 服务、启动 Rpc 服务 后面用到等下讲
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Builder;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.Http;
8 using Microsoft.AspNetCore.HttpsPolicy;
9 using Microsoft.Extensions.Configuration;
10 using Microsoft.Extensions.DependencyInjection;
11 using Microsoft.Extensions.Hosting;
12 using Microsoft.Extensions.Options;
13 using Snai.GrpcService.Hosting.Consul;
14 using Snai.GrpcService.Hosting.Consul.Entity;
15
16 namespace Snai.GrpcService.Hosting
17 {
18 public class Startup
19 {
20 public Startup(IConfiguration configuration)
21 {
22 Configuration = configuration;
23 }
24
25 public IConfiguration Configuration { get; }
26
27 // This method gets called by the runtime. Use this method to add services to the container.
28 public void ConfigureServices(IServiceCollection services)
29 {
30 //注册全局配置
31 services.AddOptions();
32 services.Configure<Impl.Entity.GrpcService>(Configuration.GetSection(nameof(Impl.Entity.GrpcService)));
33 services.Configure<HealthService>(Configuration.GetSection(nameof(HealthService)));
34 services.Configure<ConsulService>(Configuration.GetSection(nameof(ConsulService)));
35
36 //注册Rpc服务
37 services.AddSingleton<IRpcConfig, RpcConfig>();
38 }
39
40 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
41 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime, IOptions<HealthService> healthService, IOptions<ConsulService> consulService, IRpcConfig rpc)
42 {
43 if (env.IsDevelopment())
44 {
45 app.UseDeveloperExceptionPage();
46 }
47
48 // 添加健康检查路由地址
49 app.Map("/health", HealthMap);
50
51 // 服务注册
52 app.RegisterConsul(lifetime, healthService, consulService);
53
54 // 启动Rpc服务
55 rpc.Start();
56 }
57
58 private static void HealthMap(IApplicationBuilder app)
59 {
60 app.Run(async context =>
61 {
62 await context.Response.WriteAsync("OK");
63 });
64 }
65 }
66 }
修改 Program.cs 代码,调置网站地址为 .UseUrls ("http://localhost:5021"),代码如下
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Hosting;
6 using Microsoft.Extensions.Configuration;
7 using Microsoft.Extensions.Hosting;
8 using Microsoft.Extensions.Logging;
9
10 namespace Snai.GrpcService.Hosting
11 {
12 public class Program
13 {
14 public static void Main(string[] args)
15 {
16 CreateHostBuilder(args).Build().Run();
17 }
18
19 public static IHostBuilder CreateHostBuilder(string[] args) =>
20 Host.CreateDefaultBuilder(args)
21 .ConfigureWebHostDefaults(webBuilder =>
22 {
23 webBuilder.UseUrls("http://localhost:5021");
24 webBuilder.UseStartup<Startup>();
25 });
26 }
27 }
到此 Consul Api 服务注册 已完成,最终项目结构如下:
2、协议编写,将协议生成 C# 代码
新建 Snai.GrpcService.Protocol 协议类库项目,在 依赖项 右击 管理 NuGet 程序包 浏览 找到 Grpc.Core 版本 1.20.1,Google.Protobuf 版本 3.7.0,Grpc.Tools 版本 1.20.1 包下载安装
在项目根目录下新建一个 Protos 文件夹并新建 msg.proto 文件,编写基于 proto3 语言的协议代码,用于生成各语言协议,msg.proto 代码如下
syntax = "proto3";
package Snai.GrpcService.Protocol;
service MsgService{
rpc GetSum(GetMsgNumRequest) returns (GetMsgSumReply){}
}
message GetMsgNumRequest {
int32 Num1 = 1;
int32 Num2 = 2;
}
message GetMsgSumReply {
int32 Sum = 1;
}
编写 csproj 文件使项目自动生成 C# 代码,添加如下节点。
1 <ItemGroup>
2 <Protobuf Include="Protos/*.proto" OutputDir="%(RelativeDir)" CompileOutputs="false" />
3 </ItemGroup>
点击重新生成后,项目生成了 C# 代码,代码结构如下:
3、编写协议实现代码
新建 Snai.GrpcService.Impl 实现类库项目,在 依赖项 下载安装 Grpc.Core 包,项目引用 Snai.GrpcService.Protocol
新建 Entity 目录,在 Entity 目录下新建 GrpcService.cs 类,对应 Snai.GrpcService.Hosting 项目下 appsettings.json 配置文件的 GrpcService 配置项,代码如下
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace Snai.GrpcService.Impl.Entity
6 {
7 public class GrpcService
8 {
9 public string IP { get; set; }
10 public int Port { get; set; }
11 }
12 }
在根目录下新建 RpcService 目录,在 RpcService 目录下新建 MsgServiceImpl.cs 类,继承 MsgService.MsgServiceBase 协议类,实现服务方法,代码如下
1 using Grpc.Core;
2 using Snai.GrpcService.Protocol;
3 using System;
4 using System.Collections.Generic;
5 using System.Text;
6 using System.Threading.Tasks;
7
8 namespace Snai.GrpcService.Impl.RpcService
9 {
10 public class MsgServiceImpl:MsgService.MsgServiceBase
11 {
12 public override Task<GetMsgSumReply> GetSum(GetMsgNumRequest request, ServerCallContext context)
13 {
14 var result = new GetMsgSumReply();
15
16 result.Sum = request.Num1 + request.Num2;
17
18 Console.WriteLine(request.Num1 + "+" + request.Num2 + "=" + result.Sum);
19
20 return Task.FromResult(result);
21 }
22 }
23 }
在根目录下新建 IRpcConfig.cs 接口,定义 Start () 用于 Rpc 启动基方法,代码如下
1 using System;
2
3 namespace Snai.GrpcService.Impl
4 {
5 public interface IRpcConfig
6 {
7 void Start();
8 }
9 }
在根目录下新建 RpcConfig.cs 类,用于实现 IRpcConfig.cs 接口,启动 Rpc 服务,代码如下
1 using Grpc.Core;
2 using Microsoft.Extensions.Options;
3 using Snai.GrpcService.Impl.RpcService;
4 using Snai.GrpcService.Protocol;
5 using System;
6 using System.Collections.Generic;
7 using System.Text;
8
9 namespace Snai.GrpcService.Impl
10 {
11 public class RpcConfig : IRpcConfig
12 {
13 private static Server _server;
14 private static IOptions<Entity.GrpcService> _grpcSettings;
15
16 public RpcConfig(IOptions<Entity.GrpcService> grpcSettings)
17 {
18 _grpcSettings = grpcSettings;
19 }
20 public void Start()
21 {
22 _server = new Server
23 {
24 Services = { MsgService.BindService(new MsgServiceImpl()) },
25 Ports = { new ServerPort(_grpcSettings.Value.IP, _grpcSettings.Value.Port, ServerCredentials.Insecure) }
26 };
27 _server.Start();
28
29 Console.WriteLine($"Grpc ServerListening On Port {_grpcSettings.Value.Port}");
30 }
31 }
32 }
在回到 Snai.GrpcService.Hosting 项目中,添加对 Snai.GrpcService.Impl 的引用。
在 Startup.cs 中 ConfigureServices 中注册 GrpcService 配置、注册 Rpc 服务,在 Configure 中 启动 Rpc 服务 就是上面说到的蓝色字体标识的,如图
最终项目结构如下:
到此服务端的代码实现已完成,下面我们启动 Consul 和服务端,验证 Api 注册和 Grpc 启动。
二、Consul 和服务端启动
启动 Consul,启动 Grpc 服务、注册服务到 Consul
1、启动 Consul
首先下载 Consul:https://www.consul.io/downloads.html,本项目是 Windows 下进行测试,下载相应压缩包得到 consul.exe
由于本次用 Api 注册,用 Consul 默认自带 UI,所以 conf 和 dist 可删除(我的压缩包里面只有 consul.exe)
清除 Consul/data 内容,新建 startup.bat 文件,输入下面代码,双击启动 Consul,本项目测试时一台机器,所以把 本机 IP 改成 127.0.0.1
consul agent -server -datacenter=grpc-consul -bootstrap -data-dir ./data -ui -node=grpc-consul1 -bind 本机IP -client=0.0.0.0
再在 Consul 目录下启动另一个 cmd 命令行窗口,输入命令:consul operator raft list-peers 查看状态查看状态,结果如下
输入 http://localhost:8500 打开 Consul UI 查看情况
Consul 启动成功。
在 .net core Ocelot Consul 实现 API 网关 服务注册 服务发现 负载均衡 中后面 Consul 部分,有 Consul 集群搭建等其他介绍,可以去参考看下。
2、启动服务端,启动 Grpc 服务、注册服务到 Consul
由于客户端要实现负载,所以把 Snai.GrpcService.Hosting 项目生成两次,启动两个一样的服务端,只是端口不同
服务 5021 地址为 5021: .UseUrls ("http://localhost:5021"),GrpcService:5031,如下图
启动 服务 5021 和服务 5022 两个服务端,如下面
看到 Grpc ServerListening On Port 5031,Grpc ServerListening On Port 5032 说明 Grpc 服务端启动成功
打开 Consul 服务查看地址 http://localhost:8500/ui/grpc-consul/services 查看,两个 GrpcService 注册成功,健康检查状态正常
到此,Grpc 启动正常,Consul Api 服务注册、健康检查都正常,下面开始实现 Grpc 客户端
三、客户端
客户端主要包括 Grpc 客户端,Consul Api 服务发现、负载均衡等。
新建 Snai.GrpcClient 控制台程序,在 依赖项 下载安装 Grpc.Core 包,项目引用 Snai.GrpcService.Protocol,在依赖项下载安装下面工具组件包
用于读取 json 配置:Microsoft.Extensions.Configuration,Microsoft.Extensions.Configuration.Json
用于依赖注入:Microsoft.Extensions.DependencyInjection
用于注入全局配置:Microsoft.Extensions.Options,Microsoft.Extensions.Options.ConfigurationExtensions
在项目根目录下新建 Utils 目录,在 Utils 目录下新建 HttpHelper.cs 类,用于程序内发送 http 请求,代码如下
1 using System;
2 using System.Collections.Generic;
3 using System.Net.Http;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 /// <summary>
8 /// 参考 pudefu https://www.cnblogs.com/pudefu/p/7581956.html ,在此表示感谢
9 /// </summary>
10 namespace Snai.GrpcClient.Utils
11 {
12 public class HttpHelper
13 {
14 /// <summary>
15 /// 同步GET请求
16 /// </summary>
17 /// <param name="url"></param>
18 /// <param name="headers"></param>
19 /// <param name="timeout">请求响应超时时间,单位/s(默认100秒)</param>
20 /// <returns></returns>
21 public static string HttpGet(string url, Dictionary<string, string> headers = null, int timeout = 0)
22 {
23 using (HttpClient client = new HttpClient())
24 {
25 if (headers != null)
26 {
27 foreach (KeyValuePair<string, string> header in headers)
28 {
29 client.DefaultRequestHeaders.Add(header.Key, header.Value);
30 }
31 }
32 if (timeout > 0)
33 {
34 client.Timeout = new TimeSpan(0, 0, timeout);
35 }
36 Byte[] resultBytes = client.GetByteArrayAsync(url).Result;
37 return Encoding.UTF8.GetString(resultBytes);
38 }
39 }
40
41 /// <summary>
42 /// 异步GET请求
43 /// </summary>
44 /// <param name="url"></param>
45 /// <param name="headers"></param>
46 /// <param name="timeout">请求响应超时时间,单位/s(默认100秒)</param>
47 /// <returns></returns>
48 public static async Task<string> HttpGetAsync(string url, Dictionary<string, string> headers = null, int timeout = 0)
49 {
50 using (HttpClient client = new HttpClient())
51 {
52 if (headers != null)
53 {
54 foreach (KeyValuePair<string, string> header in headers)
55 {
56 client.DefaultRequestHeaders.Add(header.Key, header.Value);
57 }
58 }
59 if (timeout > 0)
60 {
61 client.Timeout = new TimeSpan(0, 0, timeout);
62 }
63 Byte[] resultBytes = await client.GetByteArrayAsync(url);
64 return Encoding.Default.GetString(resultBytes);
65 }
66 }
67
68
69 /// <summary>
70 /// 同步POST请求
71 /// </summary>
72 /// <param name="url"></param>
73 /// <param name="postData"></param>
74 /// <param name="headers"></param>
75 /// <param name="contentType"></param>
76 /// <param name="timeout">请求响应超时时间,单位/s(默认100秒)</param>
77 /// <param name="encoding">默认UTF8</param>
78 /// <returns></returns>
79 public static string HttpPost(string url, string postData, Dictionary<string, string> headers = null, string contentType = null, int timeout = 0, Encoding encoding = null)
80 {
81 using (HttpClient client = new HttpClient())
82 {
83 if (headers != null)
84 {
85 foreach (KeyValuePair<string, string> header in headers)
86 {
87 client.DefaultRequestHeaders.Add(header.Key, header.Value);
88 }
89 }
90 if (timeout > 0)
91 {
92 client.Timeout = new TimeSpan(0, 0, timeout);
93 }
94 using (HttpContent content = new StringContent(postData ?? "", encoding ?? Encoding.UTF8))
95 {
96 if (contentType != null)
97 {
98 content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
99 }
100 using (HttpResponseMessage responseMessage = client.PostAsync(url, content).Result)
101 {
102 Byte[] resultBytes = responseMessage.Content.ReadAsByteArrayAsync().Result;
103 return Encoding.UTF8.GetString(resultBytes);
104 }
105 }
106 }
107 }
108
109 /// <summary>
110 /// 异步POST请求
111 /// </summary>
112 /// <param name="url"></param>
113 /// <param name="postData"></param>
114 /// <param name="headers"></param>
115 /// <param name="contentType"></param>
116 /// <param name="timeout">请求响应超时时间,单位/s(默认100秒)</param>
117 /// <param name="encoding">默认UTF8</param>
118 /// <returns></returns>
119 public static async Task<string> HttpPostAsync(string url, string postData, Dictionary<string, string> headers = null, string contentType = null, int timeout = 0, Encoding encoding = null)
120 {
121 using (HttpClient client = new HttpClient())
122 {
123 if (headers != null)
124 {
125 foreach (KeyValuePair<string, string> header in headers)
126 {
127 client.DefaultRequestHeaders.Add(header.Key, header.Value);
128 }
129 }
130 if (timeout > 0)
131 {
132 client.Timeout = new TimeSpan(0, 0, timeout);
133 }
134 using (HttpContent content = new StringContent(postData ?? "", encoding ?? Encoding.UTF8))
135 {
136 if (contentType != null)
137 {
138 content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
139 }
140 using (HttpResponseMessage responseMessage = await client.PostAsync(url, content))
141 {
142 Byte[] resultBytes = await responseMessage.Content.ReadAsByteArrayAsync();
143 return Encoding.UTF8.GetString(resultBytes);
144 }
145 }
146 }
147 }
148 }
149 }
在项目根目录下新建 Consul 目录,在 Consul 目录下新建 Entity 目录,在 Entity 目录下新建 HealthCheck.cs 类,用于接收 Consul Api 发现的信息实体,代码如下
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace Snai.GrpcClient.Consul.Entity
6 {
7 public class HealthCheck
8 {
9 public string Node { get; set; }
10 public string CheckID { get; set; }
11 public string Name { get; set; }
12 public string Status { get; set; }
13 public string Notes { get; set; }
14 public string Output { get; set; }
15 public string ServiceID { get; set; }
16 public string ServiceName { get; set; }
17 public string[] ServiceTags { get; set; }
18 public dynamic Definition { get; set; }
19 public int CreateIndex { get; set; }
20 public int ModifyIndex { get; set; }
21 }
22 }
在项目根目录下新建 Framework 目录,在 Framework 目录下新建 Entity 目录,在 Entity 目录下新建 ConsulService.cs 和 GrpcServiceSettings.cs 类,分别对应配置 appsettings.json 的 ConsulService,GrpcServiceSettings 两个配置项,代码如下
ConsulService.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace Snai.GrpcClient.Framework.Entity
6 {
7 public class ConsulService
8 {
9 public string IP { get; set; }
10 public int Port { get; set; }
11 }
12 }
GrpcServiceSettings.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace Snai.GrpcClient.Framework.Entity
6 {
7 public class GrpcServiceSettings
8 {
9 public List<GrpcService> GrpcServices { get; set; }
10 }
11 public class GrpcService
12 {
13 public string ServiceName { get; set; }
14 public string ServiceID { get; set; }
15 public string IP { get; set; }
16 public int Port { get; set; }
17 public int Weight { get; set; }
18 }
19 }
在 Consul 目录下新建 IAppFind.cs 接口,定义 FindConsul () 用于 Consul 服务发现基方法,代码如下
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace Snai.GrpcClient.Consul
6 {
7 public interface IAppFind
8 {
9 IEnumerable<string> FindConsul(string ServiceName);
10 }
11 }
在 Consul 目录下新建 AppFind.cs 类,用于实现 IAppFind.cs 接口,实现 Consul 服务发现方法,代码如下
1 using Microsoft.Extensions.Options;
2 using Newtonsoft.Json;
3 using Snai.GrpcClient.Consul.Entity;
4 using Snai.GrpcClient.Framework.Entity;
5 using Snai.GrpcClient.Utils;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Text;
10
11 namespace Snai.GrpcClient.Consul
12 {
13 /// <summary>
14 /// 服务发现
15 /// (服务和健康信息)http://localhost:8500/v1/health/service/GrpcService
16 /// (健康信息)http://localhost:8500/v1/health/checks/GrpcService
17 /// </summary>
18 public class AppFind : IAppFind
19 {
20 private static IOptions<GrpcServiceSettings> _grpcSettings;
21 private static IOptions<ConsulService> _consulSettings;
22 public AppFind(IOptions<GrpcServiceSettings> grpcSettings, IOptions<ConsulService> consulSettings)
23 {
24 _grpcSettings = grpcSettings;
25 _consulSettings = consulSettings;
26 }
27 public IEnumerable<string> FindConsul(string ServiceName)
28 {
29 Dictionary<string, string> headers = new Dictionary<string, string>();
30
31 var consul = _consulSettings.Value;
32 string findUrl = $"http://{consul.IP}:{consul.Port}/v1/health/checks/{ServiceName}";
33
34 string findResult = HttpHelper.HttpGet(findUrl, headers, 5);
35 if (findResult.Equals(""))
36 {
37 var grpcServices = _grpcSettings.Value.GrpcServices;
38 return grpcServices.Where(g => g.ServiceName.Equals(ServiceName, StringComparison.CurrentCultureIgnoreCase)).Select(s => s.ServiceID);
39 }
40
41 var findCheck = JsonConvert.DeserializeObject<List<HealthCheck>>(findResult);
42
43 return findCheck.Where(g => g.Status.Equals("passing", StringComparison.CurrentCultureIgnoreCase)).Select(g => g.ServiceID);
44 }
45 }
46 }
在项目根目录下新建 LoadBalance 目录,在 LoadBalance 目录下新建 ILoadBalance.cs 接口,定义 GetGrpcService () 用于负载均衡基方法,代码如下
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace Snai.GrpcClient.LoadBalance
6 {
7 public interface ILoadBalance
8 {
9 string GetGrpcService(string ServiceName);
10 }
11 }
在 LoadBalance 目录下新建 WeightRoundBalance.cs 类,用于实现 ILoadBalance.cs 接口,实现 GetGrpcService () 负载均衡方法,本次负载均衡实现权重轮询算法,代码如下
1 using Microsoft.Extensions.Options;
2 using Snai.GrpcClient.Consul;
3 using Snai.GrpcClient.Framework.Entity;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Text;
8
9 namespace Snai.GrpcClient.LoadBalance
10 {
11 /// <summary>
12 /// 权重轮询
13 /// </summary>
14 public class WeightRoundBalance: ILoadBalance
15 {
16 int Balance;
17 IOptions<GrpcServiceSettings> GrpcSettings;
18 IAppFind AppFind;
19
20 public WeightRoundBalance(IOptions<GrpcServiceSettings> grpcSettings, IAppFind appFind)
21 {
22 Balance = 0;
23 GrpcSettings = grpcSettings;
24 AppFind = appFind;
25 }
26
27 public string GetGrpcService(string ServiceName)
28 {
29 var grpcServices = GrpcSettings.Value.GrpcServices;
30
31 var healthServiceID = AppFind.FindConsul(ServiceName);
32
33 if (grpcServices == null || grpcServices.Count() == 0 || healthServiceID == null || healthServiceID.Count() == 0)
34 {
35 return "";
36 }
37
38 //健康的服务
39 var healthServices = new List<Framework.Entity.GrpcService>();
40
41 foreach (var service in grpcServices)
42 {
43 foreach (var health in healthServiceID)
44 {
45 if (service.ServiceID.Equals(health, StringComparison.CurrentCultureIgnoreCase))
46 {
47 healthServices.Add(service);
48 break;
49 }
50 }
51 }
52
53 if (healthServices == null || healthServices.Count() == 0)
54 {
55 return "";
56 }
57
58 //加权轮询
59 var services = new List<string>();
60
61 foreach (var service in healthServices)
62 {
63 services.AddRange(Enumerable.Repeat(service.IP + ":" + service.Port, service.Weight));
64 }
65
66 var servicesArray = services.ToArray();
67
68 Balance = Balance % servicesArray.Length;
69 var grpcUrl = servicesArray[Balance];
70 Balance = Balance + 1;
71
72 return grpcUrl;
73 }
74 }
75 }
在项目根目录下新建 RpcClient 目录,在 RpcClient 目录下新建 IMsgClient.cs 接口,定义 GetSum () 用于 Grpc 客户端调用基方法,代码如下
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace Snai.GrpcClient.RpcClient
6 {
7 public interface IMsgClient
8 {
9 void GetSum(int num1, int num2);
10 }
11 }
在 RpcClient 目录下新建 MsgClient.cs 类,用于实现 IMsgClient.cs 接口,实现 GetSum () 方法用于 Grpc 客户端调用,代码如下
1 using Grpc.Core;
2 using Snai.GrpcClient.LoadBalance;
3 using Snai.GrpcService.Protocol;
4 using System;
5 using System.Collections.Generic;
6 using System.Text;
7
8 namespace Snai.GrpcClient.RpcClient
9 {
10 public class MsgClient:IMsgClient
11 {
12 ILoadBalance LoadBalance;
13 Channel GrpcChannel;
14 MsgService.MsgServiceClient GrpcClient;
15
16 public MsgClient(ILoadBalance loadBalance)
17 {
18 LoadBalance = loadBalance;
19
20 var grpcUrl = LoadBalance.GetGrpcService("GrpcService");
21
22 if (!grpcUrl.Equals(""))
23 {
24 Console.WriteLine($"Grpc Service:{grpcUrl}");
25
26 GrpcChannel = new Channel(grpcUrl, ChannelCredentials.Insecure);
27 GrpcClient = new MsgService.MsgServiceClient(GrpcChannel);
28 }
29 }
30
31 public void GetSum(int num1, int num2)
32 {
33 if (GrpcClient != null)
34 {
35 GetMsgSumReply msgSum = GrpcClient.GetSum(new GetMsgNumRequest
36 {
37 Num1 = num1,
38 Num2 = num2
39 });
40
41 Console.WriteLine("Grpc Client Call GetSum():" + msgSum.Sum);
42 }
43 else
44 {
45 Console.WriteLine("所有负载都挂掉了!");
46 }
47 }
48 }
49 }
在 Framework 目录下新建 DependencyInitialize.cs 类,定义 AddImplement () 方法用于注册全局配置和类到容器,实现依赖注入,代码如下
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.DependencyInjection;
3 using Snai.GrpcClient.Consul;
4 using Snai.GrpcClient.Framework.Entity;
5 using Snai.GrpcClient.LoadBalance;
6 using Snai.GrpcClient.RpcClient;
7 using System;
8 using System.Collections.Generic;
9 using System.IO;
10 using System.Text;
11
12 namespace Snai.GrpcClient.Framework
13 {
14 /// <summary>
15 /// IServiceCollection 依赖注入生命周期
16 /// AddTransient 每次都是全新的
17 /// AddScoped 在一个范围之内只有同一个实例(同一个线程,同一个浏览器请求只有一个实例)
18 /// AddSingleton 单例
19 /// </summary>
20 public static class DependencyInitialize
21 {
22 /// <summary>
23 /// 注册对象
24 /// </summary>
25 /// <param name="services">The services.</param>
26 /*
27 * IAppFind AppFind;
28 * 构造函数注入使用 IAppFind appFind
29 * AppFind = appFind;
30 */
31 public static void AddImplement(this IServiceCollection services)
32 {
33 //添加 json 文件路径
34 var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json");
35 //创建配置根对象
36 var configurationRoot = builder.Build();
37
38 //注册全局配置
39 services.AddConfigImplement(configurationRoot);
40
41 //注册服务发现
42 services.AddScoped<IAppFind, AppFind>();
43
44 //注册负载均衡
45 if (configurationRoot["LoadBalancer"].Equals("WeightRound", StringComparison.CurrentCultureIgnoreCase))
46 {
47 services.AddSingleton<ILoadBalance, WeightRoundBalance>();
48 }
49
50 //注册Rpc客户端
51 services.AddTransient<IMsgClient, MsgClient>();
52 }
53
54 /// <summary>
55 /// 注册全局配置
56 /// </summary>
57 /// <param name="services">The services.</param>
58 /// <param name="configurationRoot">The configurationRoot.</param>
59 /*
60 * IOptions<GrpcServiceSettings> GrpcSettings;
61 * 构造函数注入使用 IOptions<GrpcServiceSettings> grpcSettings
62 * GrpcSettings = grpcSettings;
63 */
64 public static void AddConfigImplement(this IServiceCollection services, IConfigurationRoot configurationRoot)
65 {
66 //注册配置对象
67 services.AddOptions();
68 services.Configure<GrpcServiceSettings>(configurationRoot.GetSection(nameof(GrpcServiceSettings)));
69 services.Configure<ConsulService>(configurationRoot.GetSection(nameof(ConsulService)));
70 }
71 }
72 }
在根目录下新建 appsettings.json 配置文件,配置 GrpcServiceSettings 的 GrpcServices 为服务端发布的两个服务 5021 和 5022,LoadBalancer 负载均衡为 WeightRound 权重轮询(如实现其他负载方法可做相应配置,注册负载均衡时也做相应修改),ConsulService Consul 的 IP 和端口,代码如下
1 {
2 "GrpcServiceSettings": {
3 "GrpcServices": [
4 {
5 "ServiceName": "GrpcService",
6 "ServiceID": "GrpcService_5021",
7 "IP": "localhost",
8 "Port": "5031",
9 "Weight": "2"
10 },
11 {
12 "ServiceName": "GrpcService",
13 "ServiceID": "GrpcService_5022",
14 "IP": "localhost",
15 "Port": "5032",
16 "Weight": "1"
17 }
18 ]
19 },
20 "LoadBalancer": "WeightRound",
21 "ConsulService": {
22 "IP": "localhost",
23 "Port": "8500"
24 }
25 }
GrpcServices Grpc 服务列表
ServiceName:服务名称,负载同一服务名称相同
ServiceID:服务 ID,保持唯一
IP:服务 IP
Port:端口
Weight:服务权重
修改 Program.cs 的 Main () 方法,调用 AddImplement (),注册全局配置和类到容器,注入使用 MsgClient 类的 GetSum () 方法,实现 Grpc 调用,代码如下
1 using Microsoft.Extensions.DependencyInjection;
2 using Snai.GrpcClient.Framework;
3 using Snai.GrpcClient.RpcClient;
4 using System;
5
6 namespace Snai.GrpcClient
7 {
8 class Program
9 {
10 static void Main(string[] args)
11 {
12 IServiceCollection service = new ServiceCollection();
13
14 //注册对象
15 service.AddImplement();
16
17 //注入使用对象
18 var provider = service.BuildServiceProvider();
19
20 string exeArg = string.Empty;
21 Console.WriteLine("Grpc调用!");
22 Console.WriteLine("-c\t调用Grpc服务;");
23 Console.WriteLine("-q\t退出服务;");
24
25 while (true)
26 {
27 exeArg = Console.ReadKey().KeyChar.ToString();
28 Console.WriteLine();
29
30 if (exeArg.ToLower().Equals("c", StringComparison.CurrentCultureIgnoreCase))
31 {
32 //调用服务
33 var rpcClient = provider.GetService<IMsgClient>();
34 rpcClient.GetSum(10, 2);
35 }
36 else if (exeArg.ToLower().Equals("q", StringComparison.CurrentCultureIgnoreCase))
37 {
38 break;
39 }
40 else
41 {
42 Console.WriteLine("参数异常!");
43 }
44 }
45 }
46 }
47 }
右击项目生成,最终项目结构如下:
到此客户端的代码实现已完成,下面运行测试 Grpc+Consul 服务注册、服务发现和负载均衡。
四、运行测试 Grpc+Consul 服务注册、服务发现和负载均衡
双击 startup.bat 启动 Consul,再启动服务 5021 和 5022,启动成功打开 http://localhost:8500/ui/#/grpc-consul/services/GrpcService 查看服务情况
启动 Snai.GrpcClient 客户端
输入 c 调用 Grpc 服务,调用 3 次,5031 调用 2 次,5032 调用 1 次,成功实现负载均衡
关掉服务 5022,等 10 秒左右 (因为设置健康检查时间间隔 10 秒),再输入 c 调用 Grpc 服务,只调用 5031
打开 http://localhost:8500/ui/#/grpc-consul/services/GrpcService 查看,5022 状态失败,或消失
Grpc+Consul 实现服务注册、服务发现、健康检查和负载均衡已完成
源码访问地址:https://github.com/Liu-Alan/Grpc-Consul
.Net Core Ocelot Consul 实现API网关 服务注册 服务发现 负载均衡
转载至@蜗牛丨大神的.net core Ocelot Consul 实现API网关 服务注册 服务发现 负载均衡一文,仅对文中所做部分内容进行更新及修改,版权归属原作者。谢谢
文章内容:
大神张善友 分享过一篇 《.NET Core 在腾讯财付通的企业级应用开发实践》里面就是用.net core 和 Ocelot搭建的可扩展的高性能Api网关。
Ocelot(http://ocelot.readthedocs.io)是一个用.NET Core实现并且开源的API网关,它功能强大,包括了:路由、负载均衡、请求聚合、认证、鉴权、限流熔断等,这些功能只都只需要简单的配置即可完成。
Consul(https://www.consul.io)是一个分布式,高可用、支持多数据中心的服务注册、发现、健康检查和配置共享的服务软件,由 HashiCorp 公司用 Go 语言开发。
Ocelot天生集成对Consul支持,在OcelotGateway项目中Ocelot.json配置就可以开启ocelot+consul的组合使用,实现服务注册、服务发现、健康检查、负载均衡。
软件版本
Asp.net Core:3.0预览5
Ocelot:13.5.0(开发时最新)
Consul:1.5.0(开发时最新)
本文分开两部分:1、基于Ocelot搭建Api网关;2、Ocelot+Consul 实现下游服务的服务注册、服务发现、健康检查、负载均衡。
项目结构
Snai.Ocelot 网关:
Snai.ApiGateway Asp.net Core 2.0 Api网关(由于在3.0预览5版上面发生了依赖问题,没有找到解决方案,所以我退回到了Core2.2稳定版本)
Snai.ApiServiceA Asp.net Core 3.0 Api下游服务A
Snai.ApiServiceB Asp.net Core 3.0 Api下游服务B
ApiServiceA和ApiServiceB其实是一样的,用于负载,为了测试方便,我建了两个项目
Consul:(我从官网下载的1.5.0中只有一个可执行文件,并没有其他的目录及文件,可能是实现的工具比较新的缘故)
conf 配置目录
data 缓存数据目录,可清空里面内容
dist Consul UI目录
consul.exe 注册软件
startup.bat 执行脚本
项目实现
一、基于Ocelot搭建Api网关
新建Snai.Ocelot解决方案
1、搭建Api网关
新建 Snai.ApiGateway 基于Asp.net Core 2.2空网站,在 依赖项 右击 管理NuGet程序包 浏览 找到 Ocelot 版本13.5.0安装
1.1、在项目根目录下新建一个 Ocelot.json 文件,打开 Ocelot.json 文件,配置Ocelot参数,Ocelot.json 代码如下
1 {
2 "ReRoutes": [
3 {
4 "UpstreamPathTemplate": "/apiservice/{controller}",
5 "UpstreamHttpMethod": [ "Get" ],
6 "DownstreamPathTemplate": "/apiservice/{controller}",
7 "DownstreamScheme": "http",
8 "DownstreamHostAndPorts": [
9 {
10 "host": "localhost",
11 "port": 5011
12 },
13 {
14 "host": "localhost",
15 "port": 5012
16 }
17 ],
18 "LoadBalancerOptions": {
19 "Type": "LeastConnection"
20 }
21 }
22 ],
23
24 "GlobalConfiguration": {
25 "BaseUrl": "http://localhost:5000"
26 }
27 }
如果有多个下游服务,把ReRoutes下 {...} 复制多份,最终如: "ReRoutes":[{...},{...}]
Ocelot参数说明如下,详情查看官网(http://ocelot.readthedocs.io)
ReRoutes 路由配置
UpstreamPathTemplate 请求路径模板
UpstreamHttpMethod 请求方法数组
DownstreamPathTemplate 下游请求地址模板
DownstreamScheme 请求协议,目前应该是支持http和https
DownstreamHostAndPorts 下游地址和端口
LoadBalancerOptions 负载均衡 RoundRobin(轮询)/LeastConnection(最少连接数)/CookieStickySessions(相同的Sessions或Cookie发往同一个地址)/NoLoadBalancer(不使用负载)
DownstreamHostAndPorts配了两个localhost 5011和localhost 5012用于负载均衡,负载均衡已经可以了,但没有健康检查,当其中一个挂了,负载可能还是会访问这样就会报错,所以我们要加入Consul,我们稍后再讲。
请求聚合,认证,限流,熔错告警等查看官方配置说明
GlobalConfiguration 全局配置
BaseUrl 告诉别人网关对外暴露的域名
1.2、修改 Program.cs 代码,读取Ocelot.json配置,修改网关地址为 http://localhost:5000
代码如下:
1 using System;
2 using System.Collections.Generic;
3 using System.IO;
4 using System.Linq;
5 using System.Threading.Tasks;
6 using Microsoft.AspNetCore;
7 using Microsoft.AspNetCore.Hosting;
8 using Microsoft.Extensions.Configuration;
9 using Microsoft.Extensions.Logging;
10
11 namespace Snai.ApiGateway
12 {
13 public class Program
14 {
15 public static void Main(string[] args)
16 {
17 CreateWebHostBuilder(args).Build().Run();
18 }
19
20 public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
21 WebHost.CreateDefaultBuilder(args)
22 .ConfigureAppConfiguration((context, builder) =>
23 {
24 builder.SetBasePath(context.HostingEnvironment.ContentRootPath);
25 builder.AddJsonFile("Ocelot.json");
26 })
27 .UseUrls("http://localhost:5000")
28 .UseStartup<Startup>();
29 }
30 }
1.3、修改Startup.cs代码,注入Ocelot到容器,并使用Ocelot
代码如下:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Builder;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.HttpsPolicy;
8 using Microsoft.AspNetCore.Mvc;
9 using Microsoft.Extensions.Configuration;
10 using Microsoft.Extensions.DependencyInjection;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Ocelot.DependencyInjection;
14 using Ocelot.Middleware;
15
16 namespace Snai.ApiGateway
17 {
18 public class Startup
19 {
20 public Startup(IConfiguration configuration)
21 {
22 Configuration = configuration;
23 }
24
25 public IConfiguration Configuration { get; }
26
27 // This method gets called by the runtime. Use this method to add services to the container.
28 public void ConfigureServices(IServiceCollection services)
29 {
30 services.AddOcelot();
31 }
32
33 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
34 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
35 {
36 if (env.IsDevelopment())
37 {
38 app.UseDeveloperExceptionPage();
39 }
40 app.UseOcelot().Wait();
41 }
42 }
43 }
最终项目结构如下:
2、搭建服务Snai.ApiServiceA,Snai.ApiServiceB
新建 Snai.ApiServiceA 基于Asp.net Core 3.0 Api网站
2.1、修改Controllers/ValuesController.cs代码
修改路由为Ocelot 配置的下游地址 apiservice/[controller],注入appsettings.json配置实体,修改Get方法为返回读取配置内容,其他方法可以删除
代码如下:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Mvc;
6 using Microsoft.Extensions.Configuration;
7
8 namespace Snai.ApiServiceA.Controllers
9 {
10 [Route("apiservice/[controller]")]
11 [ApiController]
12 public class ValuesController : ControllerBase
13 {
14 public IConfiguration _configuration { get; }
15 public ValuesController(IConfiguration configuration)
16 {
17 this._configuration = configuration;
18 }
19
20 [HttpGet]
21 public string Get()
22 {
23 return HttpContext.Request.Host.Port + " " + _configuration["AppName"] + " " + DateTime.Now.ToString();
24 }
25 }
26 }
2.2、修改appsettings.json配置,加入 "AppName": "ServiceA"
1 {
2 "Logging": {
3 "LogLevel": {
4 "Default": "Information",
5 "Microsoft": "Warning",
6 "Microsoft.Hosting.Lifetime": "Information"
7 }
8 },
9 "AllowedHosts": "*",
10 "AppName": "ServiceA"
11 }
2.3、修改Program.cs代码,修改该服务地址为 http://localhost:5011
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Hosting;
6 using Microsoft.Extensions.Configuration;
7 using Microsoft.Extensions.Hosting;
8 using Microsoft.Extensions.Logging;
9
10 namespace Snai.ApiServiceA
11 {
12 public class Program
13 {
14 public static void Main(string[] args)
15 {
16 CreateHostBuilder(args).Build().Run();
17 }
18
19 public static IHostBuilder CreateHostBuilder(string[] args) =>
20 Host.CreateDefaultBuilder(args)
21 .ConfigureWebHostDefaults(webBuilder =>
22 {
23 webBuilder.UseUrls("http://localhost:5011");
24 webBuilder.UseStartup<Startup>();
25 });
26 }
27 }
2.4、新建 Snai.ApiServiceB 基于Asp.net Core 2.0 Api网站,几乎与Snai.ApiServiceA一样,除了 "AppName": "ServiceB",.UseUrls("http://localhost:5012")
到此 基于Ocelot Api网关 搭建完成
3、启动 运行 Snai.ApiServiceA,Snai.ApiServiceB,Snai.ApiGateway项目,在浏览器打开 http://localhost:5000/apiservice/values 地址
刷新页面负载得到ServiceA,ServiceB返回内容。
5012没有返回任何数据,Ocelot已内置负载均衡,但没有健康检查,不能踢除坏掉的服务,所以加入Consul,Consul提供服务注册发现、健康检查,配合Ocelot负载就能发现坏掉的服务,只负载到正常的服务上,下面介绍加入Consul。
***作者的5012是返回数据的,而我的没有,这里说一下原因,我在Program.cs设置了启动端口,在调试的时候并没有生效,于是我在launchSettings.json中配置了applicationUrl属性并将sslPort属性设置为0关闭SSL,5012的数据就能正常返回了。
二、在Ocelot网关加入Consul,实现服务注册发现、健康检查
1、启动Consul,开启服务注册、服务发现
首先下载Consul:https://www.consul.io/downloads.html,本项目是Windows下进行测试,得到consul.exe(我下载的压缩包里面只有一个可执行文件。)
再下载Consul配置文件和Consul UI(配置文件适合本例Demo的,可根据具体项目修改调整):https://github.com/Liu-Alan/Ocelot-Consul/tree/master/Consul
conf:配置文件目录
data:缓存数据目录,可清空里面内容
dist:Consul UI,用于浏览器查看注册的服务情况;如果用Consul默认自带UI,该目录可以删除,Consul 启动脚本 -ui-dir ./dist 改为 -ui
Consul支持配置文件和Api两种方式服务注册、服务发现,下面主要讲解配置文件方式
在consul.exe同级目录下新建conf,并创建以下json文件放入其中。
Consul 配置文件service.json配置如下:
1 {
2 "encrypt": "7TnJPB4lKtjEcCWWjN6jSA==",
3 "services": [
4 {
5 "id": "ApiServiceA",
6 "name": "ApiService",
7 "tags": [ "ApiServiceA" ],
8 "address": "localhost",
9 "port": 5011,
10 "checks": [
11 {
12 "id": "ApiServiceA_Check",
13 "name": "ApiServiceA_Check",
14 "http": "http://localhost:5011/health",
15 "interval": "10s",
16 "tls_skip_verify": false,
17 "method": "GET",
18 "timeout": "1s"
19 }
20 ]
21 },
22 {
23 "id": "ApiServiceB",
24 "name": "ApiService",
25 "tags": [ "ApiServiceB" ],
26 "address": "localhost",
27 "port": 5012,
28 "checks": [
29 {
30 "id": "ApiServiceB_Check",
31 "name": "ApiServiceB_Check",
32 "http": "http://localhost:5012/health",
33 "interval": "10s",
34 "tls_skip_verify": false,
35 "method": "GET",
36 "timeout": "1s"
37 }
38 ]
39 }
40 ]
41 }
两个服务ApiServiceA和ApiServiceB,跟着两个健康检查ApiServiceA_Check和ApiServiceB_Check
由于ApiServiceA和ApiServiceB做负载均衡,现在 "name": "ApiService" 配置一样
修改Snai.ApiServiceA、Snai.ApiServiceB项目 加入health 健康检查地址
打开ValuesController.cs 加入 health
代码如下:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Mvc;
6 using Microsoft.Extensions.Configuration;
7
8 namespace Snai.ApiServiceB.Controllers
9 {
10 [Route("apiservice/[controller]")]
11 [ApiController]
12 public class ValuesController : ControllerBase
13 {
14 public IConfiguration _configuration { get; }
15 public ValuesController(IConfiguration configuration)
16 {
17 this._configuration = configuration;
18 }
19 [HttpGet]
20 public string Get()
21 {
22 return HttpContext.Request.Host.Port + " " + _configuration["AppName"] + " " + DateTime.Now.ToString();
23 }
24
25 [HttpGet("health")]
26 public async Task<IActionResult> Heathle()
27 {
28 return await Task.FromResult(Ok());
29 }
30 }
31 }
重新生成运行项目Snai.ApiServiceA、Snai.ApiServiceB
清除Consul/data 内容,新建startup.bat文件,输入下面代码,双击启动Consul,本项目测试时一台机器,所以把 本机IP 改成 127.0.0.1
consul agent -server -datacenter=dc1 -bootstrap -data-dir ./data -config-file ./conf -ui-dir ./dist -node=n1 -bind 本机IP -client=0.0.0.0
consul agent -server -datacenter=dc1 -bootstrap -data-dir ./data -config-file ./conf -ui -node=n1 -bind 本机IP -client=0.0.0.0
再在Consul目录下启动另一个cmd命令行窗口,输入命令:consul operator raft list-peers 查看状态查看状态,结果如下
打开Consul UI:http://localhost:8500 查看服务情况,可以看到ApiServiceA、ApiServiceB 服务,且健康检查都是正常的。
由于ApiServiceA、ApiServiceB是在一台机器上两个服务做负载 所以在一个Consul里配置了两个name一样的服务。
如果用两个机器做ApiServiceA负载,本机IP是192.168.0.5,另一台IP是192.168.0.6上,以本机上主Consul
本机【192.168.0.5】 Consul配置如下
1 {
2 "encrypt": "7TnJPB4lKtjEcCWWjN6jSA==",
3 "services": [
4 {
5 "id": "ApiServiceA",
6 "name": "ApiService",
7 "tags": [ "ApiServiceA" ],
8 "address": "192.168.0.5",
9 "port": 5011,
10 "checks": [
11 {
12 "id": "ApiServiceA_Check",
13 "name": "ApiServiceA_Check",
14 "http": "http://192.168.0.5:5011/health",
15 "interval": "10s",
16 "tls_skip_verify": false,
17 "method": "GET",
18 "timeout": "1s"
19 }
20 ]
21 }
22 ]
23 }
把ApiServiceA和Consul拷到另一个【192.168.0.6】机器,修改Consul配置文件
1 {
2 "encrypt": "7TnJPB4lKtjEcCWWjN6jSA==",
3 "services": [
4 {
5 "id": "ApiServiceA",
6 "name": "ApiService",
7 "tags": [ "ApiServiceA" ],
8 "address": "192.168.0.6",
9 "port": 5011,
10 "checks": [
11 {
12 "id": "ApiServiceA_Check",
13 "name": "ApiServiceA_Check",
14 "http": "http://192.168.0.6:5011/health",
15 "interval": "10s",
16 "tls_skip_verify": false,
17 "method": "GET",
18 "timeout": "1s"
19 }
20 ]
21 }
22 ]
23 }
修改启动Consul脚本的IP为192.168.0.6,-node=n2,去掉 -bootstrap,启动Consul,在Consul UI下查看服务是否正常
在192.168.0.5下,把192.168.0.6加到集群中,命令如下
consul join 192.168.0.6
注意,consul集群中,consul配置文件中的encrypt,一定要相同,否则无法放加入同一个集群
用consul operator raft list-peers查看状态,会发现n1,n2在一个集群中了
Node ID Address State Voter RaftProtocol
n1 d02c3cd0-d9c8-705b-283e-121a9105cf52 192.168.0.5:8300 leader true 3
n2 efe954ce-9840-5c66-fa80-b9022167d782 192.168.0.6:8300 follower true 3
2、配置Ocelot,加入Consul,启用服务健康检查,负载均衡
2.1 打开 Snai.ApiGateway 网关下的Ocelot.json文件,加入下面配置
完整配置信息如下:
1 {
2 "ReRoutes": [
3 {
4 "UpstreamPathTemplate": "/apiservice/{controller}",
5 "UpstreamHttpMethod": [ "Get" ],
6 "DownstreamPathTemplate": "/apiservice/{controller}",
7 "DownstreamScheme": "http",
8 "DownstreamHostAndPorts": [
9 {
10 "host": "localhost",
11 "port": 5011
12 },
13 {
14 "host": "localhost",
15 "port": 5012
16 }
17 ],
18 "LoadBalancerOptions": {
19 "Type": "LeastConnection"
20 },
21 "ServiceName": "ApiService",
22 "UseServiceDiscovery": true
23 }
24 ],
25
26 "GlobalConfiguration": {
27 "BaseUrl": "http://localhost:5000",
28 "ServiceDiscoveryProvider": {
29 "Host": "localhost",
30 "Port": 8500,
31 "Type": "Consul"
32 }
33 }
34 }
ServiceName 是Cousul配置中服务的name名字
UseServiceDiscovery 是否启用Consul服务发现
ServiceDiscoveryProvider 是Consul服务发现的地址和端口
2.2修改Startup.cs添加Consul
完整代码如下:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Builder;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.HttpsPolicy;
8 using Microsoft.AspNetCore.Mvc;
9 using Microsoft.Extensions.Configuration;
10 using Microsoft.Extensions.DependencyInjection;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Ocelot.DependencyInjection;
14 using Ocelot.Middleware;
15 using Ocelot.Provider.Consul;
16
17 namespace Snai.ApiGateway
18 {
19 public class Startup
20 {
21 public Startup(IConfiguration configuration)
22 {
23 Configuration = configuration;
24 }
25
26 public IConfiguration Configuration { get; }
27
28 // This method gets called by the runtime. Use this method to add services to the container.
29 public void ConfigureServices(IServiceCollection services)
30 {
31 services.AddOcelot().AddConsul();
32 }
33
34 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
35 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
36 {
37 if (env.IsDevelopment())
38 {
39 app.UseDeveloperExceptionPage();
40 }
41 app.UseOcelot().Wait();
42 }
43 }
44 }
重新生成启动Ocelot网关,到此Ocelot+Consul配置完成
三、运行测试Ocelot+Consul服务发现、负载均衡
打开 http://localhost:5000/apiservice/values 地址,刷新页面负载得到ServiceA,ServiceB返回内容
当把ApiServiceB服务关掉,再多次刷新页面,只能得到ServiceA的内容
打开Consul UI去看,ServiceB健康检查失败
Ocolot+Consul实现API网关 服务注册、服务发现、健康检查和负载均衡已完成
原作者源码访问地址:https://github.com/Liu-Alan/Ocelot-Consul
.Net Core 商城微服务项目系列(二):使用Ocelot + Consul构建具备服务注册和发现功能的网关
1.服务注册
在上一篇的鉴权和登录服务中分别通过NuGet引用Consul这个包,同时新增AppBuilderExtensions类:
public static class AppBuilderExtensions
{
public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app,IApplicationLifetime lifetime,ServiceEntity serviceEntity)
{
var consulClient = new ConsulClient(x => x.Address = new Uri($"http://{serviceEntity.ConsulIP}:{serviceEntity.ConsulPort}"));//请求注册的Consul地址
var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter=TimeSpan.FromSeconds(5),//服务启动多久后注册
Interval=TimeSpan.FromSeconds(10),//健康检查时间间隔,或者成为心跳间隔
HTTP=$"http://{serviceEntity.IP}:{serviceEntity.Port}/api/health",//健康检查地址
Timeout=TimeSpan.FromSeconds(5)
};
//Register service with consul
var registration = new AgentServiceRegistration()
{
Checks = new[] {httpCheck},
ID=Guid.NewGuid().ToString(),
Name=serviceEntity.ServiceName,
Address=serviceEntity.IP,
Port=serviceEntity.Port,
Tags = new[] { $"urlprefix-/{serviceEntity.ServiceName}"} //添加urlprefix-/servicename格式的tag标签,以便Fabio识别
};
consulClient.Agent.ServiceRegister(registration).Wait();//服务启动时注册,内部实现其实就是使用Consul API进行注册(HttpClient发起)
lifetime.ApplicationStopping.Register(() =>
{
consulClient.Agent.ServiceDeregister(registration.ID).Wait();//服务停止时取消注册
});
return app;
}
}
public class ServiceEntity
{
public string IP { get; set; }
public int Port { get; set; }
public string ServiceName { get; set; }
public string ConsulIP { get; set; }
public int ConsulPort { get; set; }
}
通过这个类可以提供服务注册的基本参数。
修改Startup启动项中的Configure方法:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
#region Consul 服务注册
ServiceEntity serviceEntity = new ServiceEntity
{
IP = "127.0.0.1", //服务运行地址
Port = Convert.ToInt32(Configuration["Consul:ServicePort"]), //服务运行端口
ServiceName = Configuration["Consul:Name"], //服务标识,Ocelot中会用到
ConsulIP = Configuration["Consul:IP"], //Consul运行地址
ConsulPort = Convert.ToInt32(Configuration["Consul:Port"]) //Consul运行端口(默认8500)
};
app.RegisterConsul(lifetime, serviceEntity);
#endregion
app.UseIdentityServer();
//app.UseAuthentication();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
看下配置文件需要新增的东西:
{
"Service": {
"Name": "MI.Service",
"Port": "7001",
"DocName": "Account Service",
"Version": "v1",
"Title": "Account Service API"
},
"Identity": {
"IP": "localhost",
"Port": "7000",
"Scheme": "Bearer"
},
"ConnectionStrings": {
"SqlConnection": "server=.;uid=sa;pwd=sa;database=MI"
},
"Consul": {
"Name": "MI.Service.Account",
"ServiceProt": "7001",
"IP": "localhost",
"Port": "8500"
}
}
蓝色标识的Consul部分是我们这里需要用到的,这里我把项目名称当作服务注册标识。
然后还需要为两个服务添加两个方法,一个是用来做健康检查的,一个是用来测试的:
[Route("api/Health")]
public class HealthController : Controller
{
[HttpGet]
public IActionResult Get() => Ok("ok");
}
public class MiUserController : Controller
{
public MIContext _context;
public MiUserController(MIContext _context)
{
this._context = _context;
}
public string Index()
{
return "Successful";
}
。。。。。。
}
通过“consul agent -dev”命令运行Consul,访问127.0.0.1:8500我们可以看到Consul的UI界面:
这里可以看到我们已经注册的两个服务。
2.服务发现
新建API项目MI.Ocelot,通过NuGet引用Ocelot和Ocelot.Provider.Consul两个包,并修改启动项注册Ocelot和Consul:
public void ConfigureServices(IServiceCollection services)
{
//services.AddMvc();
services.AddOcelot(Configuration)
.AddConsul();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseOcelot();
//app.UseMvc();
}
然后添加配置文件consul.json:
{
"ReRoutes": [
{
"UseServiceDiscovery": true, //启用服务发现
"DownstreamPathTemplate": "/Account/{url}", //下游转发路由
"DownstreamScheme": "http", //标识头
"ServiceName": "MI.Service.Account", //服务注册标识
"LoadBalancer": "RoundRobin", //服务均衡:轮询
"UpstreamPathTemplate": "/Account/{url}", //上游请求路由
"UpstreamHttpMethod": [ "Get", "Post" ], //请求的方法类型
"ReRoutesCaseSensitive": false //不区分大小写
},
{
"UseServiceDiscovery": true,
"DownstreamPathTemplate": "/Identity/{url}",
"DownstreamScheme": "http",
"ServiceName": "MI.Service.IdentityServer",
"LoadBalancer": "RoundRobin",
"UpstreamPathTemplate": "/Identity/{url}",
"UpstreamHttpMethod": [ "Get", "Post" ],
"ReRoutesCaseSensitive": false
}
],
"GlobalConfiguration": {
//"BaseUrl": "http://localhost:7003",
"ServiceDiscoveryProvider": {
"Host": "127.0.0.1", // Consul Service IP
"Port": 8500, // Consul Service Port
"Type": "PollConsul",
"PollingInterval": 100 //健康检查时间端
}
}
}
在Program中启用这个配置文件:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureAppConfiguration((hostingContext,builder)=> {
builder.AddJsonFile("consul.json");
})
.Build();
到此,网关配置完毕。现在我将网关项目MI.Gateway部署在7003端口,登录服务MI.Service.Account部署在7001端口,鉴权服务部署在7000端口,我会通过访问网关服务来请求登录服务:
这里的流程是这样的,Ocelot通过“/Account/MiUser/Index”匹配到了“/Account/{url}”这个路由,进而拿到了“MI.Service.Account”这个服务注册标识,然后通过Consul拿到了对应的地址,并转发了请求,同时返回结果。
到此,具备服务注册和发现的简单网关服务就搭建完毕了,后面有时间会继续优化,添加限流、熔断,同时身份验证会在Ocelot中进行,而不是再去访问单独的鉴权服务。
关于.NET Core 使用 Consul 服务注册发现和.net 服务注册与发现的介绍已经告一段落,感谢您的耐心阅读,如果想了解更多关于.NET Core + Consul 服务注册与发现、.Net Core Grpc Consul 实现服务注册 服务发现 负载均衡、.Net Core Ocelot Consul 实现API网关 服务注册 服务发现 负载均衡、.Net Core 商城微服务项目系列(二):使用Ocelot + Consul构建具备服务注册和发现功能的网关的相关信息,请在本站寻找。
本文标签: