服务发现
小技巧
对于全面容器化后的独立产品,我们推荐直接使用 K8S 本身的服务发现机制。服务之间调用推荐直接使用集群内的服务名称.名称空间方式。服务的健康检查全部托管给 K8S。
但如果产品还有部分服务没有容器化,或者还有部分服务部署在其他环境中,我们推荐使用 Consul 作为服务发现。服务发现及健康监测都由 Consul 完成。
基于 Consul 的服务发现
# 添加包依赖
dotnet add package Consul
/// <summary>
/// ServiceDiscoveryExtensions
/// </summary>
public static class ServiceDiscoveryExtensions
{
/// <summary>
/// Consul 服务发现注册服务
/// </summary>
public static IApplicationBuilder UseServiceDiscovery(this IApplicationBuilder app, IHostApplicationLifetime lifetime)
{
var consulClient = app.ApplicationServices.GetService<IConsulClient>();
var registration = new AgentServiceRegistration()
{
ID = Runtime.ServerId, // 服务实例唯一标识
Name = Runtime.ServiceName, // 服务名
Address = Runtime.PodIP, // 服务IP
Port = Runtime.PodPort, // 服务端口
Tags = new string[] { "swagger", "订单服务" }, // 填写自己服务的简单描述
Check = new AgentServiceCheck()
{
Interval = TimeSpan.FromSeconds(10), // 健康检查时间间隔
HTTP = $"http://{Runtime.PodIP}:{Runtime.PodPort}/api/healthz", // 健康检查地址
Timeout = TimeSpan.FromSeconds(5) // 超时时间
}
};
// 程序启动后注册 Consul
lifetime.ApplicationStarted.Register(async () =>
{
// 检测注册ip是否冲突,主要防止在 k8s 环境下,pod ip 复用问题。
// 出现问题的原因主要是部分程序意外的下线,导致 consul 服务注册信息未注销。
var serviceQueryResult = await consulClient?.Catalog.Services();
if (serviceQueryResult?.StatusCode != HttpStatusCode.OK)
{
throw new Exception("consul register error. query consul server fail");
}
var services = serviceQueryResult.Response;
foreach (var service in services.Keys)
{
var serviceInstancesQueryResult = consulClient?.Catalog.Service(service).Result;
var serviceInstances = serviceInstancesQueryResult?.Response ?? [];
// 其他服务已经占用,失败,抛出异常
if (serviceInstances.Any(
s => s.ServiceAddress == Runtime.PodIP
&& s.ServiceName != Runtime.ServiceName))
{
throw new Exception($"consul register error. ip conflict with service {service}, ip {Runtime.PodIP}");
}
}
// 服务注册,先执行注销,兼容自身重启的情况。
await consulClient.Agent.ServiceDeregister(registration.ID);
await consulClient.Agent.ServiceRegister(registration);
});
// 应用程序终止时,手动取消注册
lifetime.ApplicationStopping.Register(async () =>
{
await consulClient.Agent.ServiceDeregister(registration.ID);
// 等待5s,处理注销时可能获取的请求
await Task.Delay(5000);
});
return app;
}
}
// 使用服务发现调用其他服务。
services.AddSingleton<IConsulClient, ConsulClient>(p => new ConsulClient(consulConfig =>
{
consulConfig.Address = new Uri("http://localhost:8500");
}));
public class ConsumeService
{
private readonly IConsulClient _consulClient;
public ConsumeService(IConsulClient consulClient)
{
_consulClient = consulClient;
}
public async Task<string> CallServiceAsync()
{
var services = await _consulClient.Catalog.Service("my-service");
var service = services.Response.FirstOrDefault();
if (service == null)
{
throw new Exception("Service not found");
}
using (var httpClient = new HttpClient())
{
var response = await httpClient.GetStringAsync($"http://{service.ServiceAddress}:{service.ServicePort}/api/values");
return response;
}
}
}
基于 K8S 的服务发现
基于 K8S 的服务发现则比较简单,因集群本身提供了 DNS 服务,支持通过 service.namespace 的方式直接访问服务。在代码中直接使用即可。
小技巧
服务本身的健康检查参见 应用健康检查 中的说明。