zoukankan      html  css  js  c++  java
  • Asp.net core 向Consul 注册服务

    Consul服务发现的使用方法:
    1. 在每台电脑上都以Client Mode的方式运行一个Consul代理, 这个代理只负责与Consul Cluster高效地交换最新注册信息(不参与Leader的选举)
    2. 每台电脑上的服务Service都向本机的consul代理注册 服务名称和提供服务的url
    3. 当Computer1上部署的程序ServiceA需要调用服务ServiceB时, 程序ServiceA直接从本机的Consul Agent通过服务名称获取ServiceB的访问地址, 然后直接向ServiceB的url发出请求

    本文的重点不是描述上述过程, 只是准备分享一下自己编写的服务注册代码, 虽然网上已经有不少类似的文章, 个人觉得自己的代码要智能一点
    其他文章一般要求在参数中提供 服务访问的外部地址, 但我想可以直接从 IWebHostBuilder.UseUrls(params string[] urls)获取, 这样就可以少输入一个参数.
    但是在实现的时候才发现, 问题很多
    1. urls可以输入多个, 要有一个种最佳匹配的方式, 从里面选取一个注册到consul里
        这里假设该服务会被不同服务器的程序调用, 那么localhost(127.0.0.1)首先要排除掉, 剩下urls随便选取一个供其他程序调用, 当然用户也可以指定一个ip
    2. urls可以使用 "0.0.0.0"、"[::]", 表示绑定所有网卡地址的80端口, 而物理服务器往往有多个网卡
        假如服务器确实只有一个IP, 直接使用即可; 假如有多个IP, 还是必须让用户传入通过参数指定一个IP
    3. urls还可以使用 "192.168.1.2:0"这种动态端口, asp.net core会随机选择一个端口让外部访问, 但这要服务器完全启动后才能得知是哪个端口
        使用IApplicationLifetime.ApplicationStarted 发起注册请求
    4. IPv6地址表示方式解析起来非常麻烦
         System.Uri这个类已经支持IPv6, 可以直接

    下面就是实现代码, 需要nuget Consul

    using Consul;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Hosting.Server.Features;
    using Microsoft.Extensions.Configuration;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    public static class RegisterCansulExtension
    {
        public static void RegisterToConsul(this IApplicationBuilder app, IConfiguration configuration, IApplicationLifetime lifetime)
        {
            lifetime.ApplicationStarted.Register(() =>
            {
                string serviceName = configuration.GetValue<string>("serviceName");
                string serviceIP = configuration.GetValue<string>("serviceIP");
                string consulClientUrl = configuration.GetValue<string>("consulClientUrl");
                string healthCheckRelativeUrl = configuration.GetValue<string>("healthCheckRelativeUrl");
                int healthCheckIntervalInSecond = configuration.GetValue<int>("healthCheckIntervalInSecond");
                ICollection<string> listenUrls = app.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
    
    
                if (string.IsNullOrWhiteSpace(serviceName))
                {
                    throw new Exception("Please use --serviceName=yourServiceName to set serviceName");
                }
                if (string.IsNullOrEmpty(consulClientUrl))
                {
                    consulClientUrl = "http://127.0.0.1:8500";
                }
                if (string.IsNullOrWhiteSpace(healthCheckRelativeUrl))
                {
                    healthCheckRelativeUrl = "health";
                }
                healthCheckRelativeUrl = healthCheckRelativeUrl.TrimStart('/');
                if (healthCheckIntervalInSecond <= 0)
                {
                    healthCheckIntervalInSecond = 1;
                }
    
    
                string protocol;
                int servicePort = 0;
                if (!TryGetServiceUrl(listenUrls, out protocol, ref serviceIP, out servicePort, out var errorMsg))
                {
                    throw new Exception(errorMsg);
                }
    
                var consulClient = new ConsulClient(ConsulClientConfiguration => ConsulClientConfiguration.Address = new Uri(consulClientUrl));
    
                var httpCheck = new AgentServiceCheck()
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10),//服务启动多久后注册
                    Interval = TimeSpan.FromSeconds(healthCheckIntervalInSecond),
                    HTTP = $"{protocol}://{serviceIP}:{servicePort}/{healthCheckRelativeUrl}",
                    Timeout = TimeSpan.FromSeconds(2)
                };
    
                // 生成注册请求
                var registration = new AgentServiceRegistration()
                {
                    Checks = new[] { httpCheck },
                    ID = Guid.NewGuid().ToString(),
                    Name = serviceName,
                    Address = serviceIP,
                    Port = servicePort,
                    Meta = new Dictionary<string, string>() { ["Protocol"] = protocol },
                    Tags = new[] { $"{protocol}" }
                };
                consulClient.Agent.ServiceRegister(registration).Wait();
    
                //服务停止时, 主动发出注销
                lifetime.ApplicationStopping.Register(() =>
                {
                    try
                    {
                        consulClient.Agent.ServiceDeregister(registration.ID).Wait();
                    }
                    catch
                    { }
                });
            });
        }
    
    
        private static bool TryGetServiceUrl(ICollection<string> listenUrls, out string protocol, ref string serviceIP, out int port, out string errorMsg)
        {
            protocol = null;
            port = 0;
            errorMsg = null;
            if (!string.IsNullOrWhiteSpace(serviceIP)) // 如果提供了对外服务的IP, 只需要检测是否在listenUrls里面即可
            {
                foreach (var listenUrl in listenUrls)
                {
                    Uri uri = new Uri(listenUrl);
                    protocol = uri.Scheme;
                    var ipAddress = uri.Host;
                    port = uri.Port;
    
                    if (ipAddress == serviceIP || ipAddress == "0.0.0.0" || ipAddress == "[::]")
                    {
                        return true;
                    }
                }
                errorMsg = $"The serviceIP that you provide is not in urls={string.Join(',', listenUrls)}";
                return false;
            }
            else // 没有提供对外服务的IP, 需要查找本机所有的可用IP, 看看有没有在 listenUrls 里面的
            {
                var allIPAddressOfCurrentMachine = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()
                        .Select(p => p.GetIPProperties())
                        .SelectMany(p => p.UnicastAddresses)
                        // 这里排除了 127.0.0.1 loopback 地址
                        .Where(p => p.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && !System.Net.IPAddress.IsLoopback(p.Address))
                        .Select(p => p.Address.ToString()).ToArray();
                var uris = listenUrls.Select(listenUrl => new Uri(listenUrl)).ToArray();
                // 本机所有可用IP与listenUrls进行匹配, 如果listenUrl是"0.0.0.0"或"[::]", 则任意IP都符合匹配
                var matches = allIPAddressOfCurrentMachine.SelectMany(ip =>
                        uris.Where(uri => ip == uri.Host || uri.Host == "0.0.0.0" || uri.Host == "[::]")
                        .Select(uri => new { Protocol = uri.Scheme, ServiceIP = ip, Port = uri.Port })
                ).ToList();
    
                if (matches.Count == 0)
                {
                    errorMsg = $"This machine has IP address=[{string.Join(',', allIPAddressOfCurrentMachine)}], urls={string.Join(',', listenUrls)}, none match.";
                    return false;
                }
                else if (matches.Count == 1)
                {
                    protocol = matches[0].Protocol;
                    serviceIP = matches[0].ServiceIP;
                    port = matches[0].Port;
                    return true;
                }
                else
                {
                    errorMsg = $"Please use --serviceIP=yourChosenIP to specify one IP, which one provide service: {string.Join(",", matches)}.";
                    return false;
                }
            }
        }
    }
    View Code

    使用时可以提供5个参数, 只有serviceName是必须要由参数提供的, serviceIP会自动尽可能匹配出来, consulClientUrl默认是http://127.0.0.1:8500, healthCheckRelativeUrl默认是 /health, healthCheckIntervalInSecond默认是1秒

    使用方法
    上面代码里 健康检查 使用了asp.net core的HealthChecks中间件, 需要在Startup.ConfigureServices(IServiceCollection services)中增加
    services.AddHealthChecks();
    在Startup.Configure也要增加UseHealthChecks()

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHealthChecks();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
    
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
    
            // consul的http check只需要返回是200, 就认为服务可用; 这里必须把Degraded改为503
            app.UseHealthChecks("/Health", new HealthCheckOptions()
            {
                ResultStatusCodes =
                {
                    [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Healthy] = StatusCodes.Status200OK,
                    [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Degraded] = StatusCodes.Status503ServiceUnavailable,
                    [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
                }
            });
    
            app.UseMvc();
    
            app.RegisterToConsul(Configuration, lifetime);
        }
    }

     调试的时候, 也不能使用默认的 http://localhost:5000了

     

    后记: 这段代码也是很久以前写的了, 后来发现Ocelot是个功能很丰富的网关, 帮我们做了很大部分工作, 实施微服务的基础功能就不再写了.

    后来又学习了Kubernetes, 感觉这才是微服务的正道, 但Kubernetes更复杂, 对运维有更高的要求, 幸好各大云服务商都提供了Kubernetes的基础设施, 这样只需要开发人员提升开发方式即可

    现在Service Mesh不断发展, 但还没形成统一的解决方案, 其中Consul也支持Mesh, 有时间再写吧

    End

  • 相关阅读:
    第四篇Scrum冲刺博客
    第三篇Scrum冲刺博客
    蔡勒公式和吉姆拉尔森公式计算星期几
    事后诸葛亮
    Alpha阶段项目复审
    团队作业6——复审与事后分析
    第7篇 Scrum 冲刺博客
    第6篇 Scrum 冲刺博客
    第5篇 Scrum 冲刺博客
    第4篇 Scrum 冲刺博客
  • 原文地址:https://www.cnblogs.com/zhouandke/p/10534836.html
Copyright © 2011-2022 走看看