在這篇文章中,我們將快速了解什么是服務發現,使用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
