在这篇文章中,我们将快速了解什么是服务发现,使用consul实现一个基本的服务基础设施;使用asp.net核心mvc框架,并使用dns client.net实现基于dns的客户端服务发现。
Service Discovery
在现代微服务体系结构中,服务可以在容器中运行,并且可以动态地启动、停止和扩展。这将导致一个非常动态的托管环境,其中可能有数百个实际的端点,无法手动配置或找到正确的端点。
尽管如此,我相信服务发现不仅仅是针对生活在容器中的细粒度微服务。它可以被任何需要访问其他资源的应用程序使用。资源可以是数据库、其他web服务,也可以只是托管在其他地方的网站的一部分。服务发现有助于删除特定于环境的配置文件!
服务发现可以用来解决这个问题,但是和往常一样,有很多不同的方法来实现它
客户端服务发现
一种解决方案是有一个中心服务注册中心,所有服务实例都在这里注册。客户机必须实现逻辑来查询他们需要的服务,最终验证端点是否仍然存在,并可能将请求分发到多个端点。
服务器端/负载平衡
所有流量都通过一个负载平衡器,它知道所有实际的、动态变化的端点,并相应地重定向所有请求
Consul Setup
consul是一个服务注册中心,可用于实现客户端服务发现。
除了使用这种方法的许多优点和特性外,它的缺点是每个客户机应用程序都需要实现一些逻辑来使用这个中央注册表。这种逻辑可能会变得非常具体,因为consun和任何其他技术都有自定义的api和工作原理。
负载平衡也可能不会自动完成。客户机可以查询服务的所有可用/注册的端点,然后决定选择哪个端点。
好消息是consul不仅提供了一个rest api来查询服务注册中心。它还提供返回标准srv和txt记录的dns端点。
DNS终结点确实关心服务运行状况,因为它不会返回不正常的服务实例。它还通过以交替顺序返回记录来实现负载平衡!此外,它可能会赋予服务更高的优先级,使其更接近客户端。
consul是hashicorp开发的一个软件,它不仅进行服务发现(如上所述),还进行“健康检查”,并提供一个分布式的“密钥值存储”。
consul应该在一个集群中运行,其中至少有三个实例处理集群和宿主环境中每个节点上的“代理”的协调。应用程序总是只与本地代理通信,这使得通信速度非常快,并将网络延迟降至最低。
不过,对于本地开发,您可以在--dev模式下运行consul,而不是设置完整的集群。但请记住,为了生产使用,需要做一些工作来正确设置consul。
Download and Run Consul
下载地址:https://www.consul.io/downloads.html
用代理——dev参数运行consul。这将在本地服务模式下引导consul,无需配置文件,并且只能在本地主机上访问。
http://localhost:8500,打开consul ui。
下面开始
目标
通过appsettings.json配置服务名
主机和端口不应硬编码
使用microsoft.extensions.configuration和options正确配置所有需要的内容
将注册设置为启动管道的一部分
集成Identity Server4到Identity api
添加Ocelot网关并集成identity server4认证
.Ocelot集成Consul服务发现
新建项目User.Api
添加UserController
[Route("api/[controller]")]
[ApiController]
public class UserController : BaseController
{
private readonly UserDbContext _userContext;
private readonly ILogger<UserController> _logger;
public UserController(UserDbContext userContext, ILogger<UserController> logger)
{
_userContext = userContext;
_logger = logger;
}
/// <summary>
/// 检查或者创建用户 但其那手机号码不存在的时候创建
/// </summary>
/// <returns></returns>
[HttpPost("check-or-create")]
public async Task<ActionResult> CheckOrCreate([FromForm]string phone)
{
var user = await _userContext.Users.SingleOrDefaultAsync(s => s.Phone == phone);
if (user == null)
{
user = new Users() { Phone = phone };
await _userContext.Users.AddAsync(user);
await _userContext.SaveChangesAsync();
}
return Ok(user.Id);
}
[HttpGet]
public async Task<IActionResult> Get()
{
var user = await _userContext.Users.AsNoTracking().
Include(u => u.Properties).
SingleOrDefaultAsync(s => s.Id == UserIdentity.UserId);
if (user == null)
throw new UserOperationException($"错误的用户上下文id:{UserIdentity.UserId}");
return Json(user);
}
[HttpPatch]
public async Task<IActionResult> Patch(JsonPatchDocument<Users> patch)
{
var user = await _userContext.Users.SingleOrDefaultAsync(u => u.Id == UserIdentity.UserId);
if (user == null)
throw new UserOperationException($"错误的用户上下文id:{UserIdentity.UserId}");
patch.ApplyTo(user);
var originProperties = await _userContext.UserProperty.AsNoTracking().Where(s => s.UserId == user.Id).ToListAsync();
var allProperties = originProperties.Distinct();
if (user.Properties != null)
{
allProperties = originProperties.Union(user.Properties).Distinct();
}
var removeProperties = allProperties;
var newProperties = allProperties.Except(originProperties);
if (removeProperties != null)
{
_userContext.UserProperty.RemoveRange(removeProperties);
}
if (newProperties != null)
{
foreach (var item in newProperties)
{
item.UserId = user.Id;
}
await _userContext.AddRangeAsync(newProperties);
}
_userContext.Users.Update(user);
await _userContext.SaveChangesAsync();
return Json(user);
}
}
appsettings.json 添加下面配置
"ServiceDiscovery": {
"ServiceName": "userapi",
"Consul": {
"HttpEndpoint": "http://127.0.0.1:8500",
"DnsEndpoint": {
"Address": "127.0.0.1",
"Port": 8600
}
}
添加poco,映射类
public class ServiceDisvoveryOptions
{
public string ServiceName { get; set; }
public ConsulOptions Consul { get; set; }
}
public class ConsulOptions
{
public string HttpEndpoint { get; set; }
public DnsEndpoint DnsEndpoint { get; set; }
}
public class DnsEndpoint
{
public string Address { get; set; }
public int Port { get; set; }
public IPEndPoint ToIPEndPoint()
{
return new IPEndPoint(IPAddress.Parse(Address), Port);
}
}
然后在Startup对其进行配置。ConfigureServices
services.Configure<ServiceDisvoveryOptions>(Configuration.GetSection("ServiceDiscovery"));
//使用此配置设置consul客户端:
services.AddSingleton<IConsulClient>(p => new ConsulClient(cfg =>
{
var serviceConfiguration = p.GetRequiredService<IOptions<ServiceDisvoveryOptions>>().Value;
if (!string.IsNullOrEmpty(serviceConfiguration.Consul.HttpEndpoint))
{
// 如果未配置,客户端将使用默认值“127.0.0.1:8500”
cfg.Address = new Uri(serviceConfiguration.Consul.HttpEndpoint);
}
}));
//consulclient不一定需要配置,如果没有指定任何内容,它将返回到默认值(localhost:8500)。
动态服务注册
只要kestrel用于在某个端口上托管服务,app.properties[“server.features”]就可以用来确定服务的托管位置。如上所述,如果使用了iis集成或任何其他反向代理,则此解决方案将不再工作,并且必须使用服务可访问的实际端点在consul中注册服务。但在启动过程中无法获取这些信息。
如果要将IIS集成与服务发现结合使用,请不要使用以下代码。相反,可以通过配置配置端点,或者手动注册服务。
无论如何,对于红隼,我们可以执行以下操作:获取承载服务的uri红隼(这不适用于useurls(“*:5000”)之类的通配符,然后遍历地址以在consul中注册所有地址:这里默认使用 UseUrls("http://localhost:92")
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime applicationLifetime,
IOptions<ServiceDisvoveryOptions> serviceOptions, IConsulClient consul)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseSwagger();
//启用中间件服务对swagger-ui,指定Swagger JSON终结点
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
//app.UseHttpsRedirection();
//启动的时候注册服务
applicationLifetime.ApplicationStarted.Register(() =>
{
RegisterService(app, serviceOptions, consul);
});
//停止的时候移除服务
applicationLifetime.ApplicationStopped.Register(() =>
{
RegisterService(app, serviceOptions, consul);
});
app.UseMvc();
UserContextSeed.SeedAsync(app, loggerFactory).Wait();
// InitDataBase(app);
}
private void RegisterService(IApplicationBuilder app, IOptions<ServiceDisvoveryOptions> serviceOptions, IConsulClient consul)
{
var features = app.Properties["server.Features"] as FeatureCollection;
var addresses = features.Get<IServerAddressesFeature>()
.Addresses
.Select(p => new Uri(p));
foreach (var address in addresses)
{
var serviceId = $"{serviceOptions.Value.ServiceName}_{address.Host}:{address.Port}";
//serviceid必须是唯一的,以便以后再次找到服务的特定实例,以便取消注册。这里使用主机和端口以及实际的服务名
var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
Interval = TimeSpan.FromSeconds(30),
HTTP = new Uri(address, "HealthCheck").OriginalString
};
var registration = new AgentServiceRegistration()
{
Checks = new[] { httpCheck },
Address =address.Host,
ID = serviceId,
Name = serviceOptions.Value.ServiceName,
Port = address.Port
};
consul.Agent.ServiceRegister(registration).GetAwaiter().GetResult();
}
}
private void DeRegisterSWervice(IApplicationBuilder app, IOptions<ServiceDisvoveryOptions> serviceOptions, IConsulClient consul)
{
var features = app.Properties["server.Features"] as FeatureCollection;
var addresses = features.Get<IServerAddressesFeature>().
Addresses.Select(p => new Uri(p));
foreach (var address in addresses)
{
var serviceId = $"{serviceOptions.Value.ServiceName}_{address.Host}:{address.Port}";
consul.Agent.ServiceDeregister(serviceId).GetAwaiter().GetResult();
}
}
添加健康检查接口
[Route("HealthCheck")]
[ApiController]
public class HealthCheckController : ControllerBase
{
[HttpGet]
[HttpHead]
public IActionResult Get()
{
return Ok();
}
}
添加项目Cateway.Api:添加端口为91
添加Ocelot.json
{
"ReRoutes": [
{
//暴露出去的地址
"UpstreamPathTemplate": "/{controller}",
"UpstreamHttpMethod": [ "Get" ],
//转发到下面这个地址
"DownstreamPathTemplate": "/api/{controller}",
"DownstreamScheme": "http",
//资源服务器列表
"DownstreamHostAndPorts": [
{
"host": "localhost",
"port": 92
}
],
"AuthenticationOptions": {
"AuthenticationProviderKey": "finbook",
"AllowedScopes": []
}
},
{
//暴露出去的地址
"UpstreamPathTemplate": "/connect/token",
"UpstreamHttpMethod": [ "Post" ],
//转发到下面这个地址
"DownstreamPathTemplate": "/connect/token",
"DownstreamScheme": "http",
//资源服务器列表
"DownstreamHostAndPorts": [
{
"host": "localhost",
"port": 93
}
]
}
],
//对外暴露的访问地址 也就是Ocelot所在的服务器地址
"GlobalConfiguration": {
"BaseUrl": "http://localhost:91"
}
}
program 修改
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args).
ConfigureAppConfiguration((hostingContext, builder) =>
{
builder
.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
.AddJsonFile("Ocelot.json");
})
.UseUrls("http://+:91")
.UseStartup<Startup>();
}
Startup
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
var authenticationProviderKey = "finbook";
services.AddAuthentication(). AddIdentityServerAuthentication(authenticationProviderKey, options =>
{
options.Authority = "http://localhost:93";//.Identity服务 配置
options.ApiName = "gateway_api";
options.SupportedTokens = SupportedTokens.Both;
options.ApiSecret = "secret";
options.RequireHttpsMetadata = false;
});
services.AddOcelot();
}
// 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().Wait();
app.UseMvc();
}
}
添加项目User.Identity idneity代码省略
使用dnsclient
appsettings.json 添加配置
"ServiceDiscovery": {
"UserServiceName": "userapi",
"Consul": {
"HttpEndpoint": "http://127.0.0.1:8500",
"DnsEndpoint": {
"Address": "127.0.0.1",
"Port": 8600
}
}
注册dnslookup客户端:
services.Configure<ServiceDisvoveryOptions>(Configuration.GetSection("ServiceDiscovery"));
//di中注册dns lookup客户端
services.AddSingleton<IDnsQuery>(p =>
{
var serviceConfiguration = p.GetRequiredService<IOptions<ServiceDisvoveryOptions>>().Value;
return new LookupClient(serviceConfiguration.Consul.DnsEndpoint.ToIPEndPoint());
});
private readonly IDnsQuery _dns;
private readonly IOptions<ServiceDisvoveryOptions> _options;
public SomeController(IDnsQuery dns, IOptions<ServiceDisvoveryOptions> options)
{
_dns = dns ?? throw new ArgumentNullException(nameof(dns));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
[HttpGet("")]
[HttpHead("")]
public async Task<IActionResult> DoSomething()
{
var result = await _dns.ResolveServiceAsync("service.consul", _options.Value.ServiceName);
...
}
dnsclient.net的resolveserviceasync执行dns srv查找,匹配cname记录,并为每个包含主机名和端口(以及地址(如果使用)的条目返回一个对象。
现在,我们可以用一个简单的httpclient调用(或生成的客户端)来调用服务:
var address = result.First().AddressList.FirstOrDefault();
var port = result.First().Port;
using (var client = new HttpClient())
{
var serviceResult = await client.GetStringAsync($"http://{address}:{port}/Values");
}
启动项目
打开http://localhost:8500,查看服务运行
测试identity 服务
请求userapi -api/user
git:https://gitee.com/LIAOKUI/user.api
参考:http://michaco.net/blog/ServiceDiscoveryAndHealthChecksInAspNetCoreWithConsul?tag=ASP.NET%20Core#service-discovery