服务发现

小技巧

对于全面容器化后的独立产品,我们推荐直接使用 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 的方式直接访问服务。在代码中直接使用即可。

小技巧

服务本身的健康检查参见 应用健康检查 中的说明。