• Api网关Kong集成Consul做服务发现及在Asp.Net Core中的使用


    1622219047536

    写在前面

      Api网关我们之前是用 .netcore写的 Ocelot的,使用后并没有完全达到我们的预期,花了些时间了解后觉得kong可能是个更合适的选择。

    简单说下kong对比ocelot打动我的:

    1、kong可以直接代替Nginx/OpenRestry做前端服务器。

    2、kong的功能强大,性能不俗,生态不错,操作面板,插件丰富,社区活跃;

    本文目的

    1、对kong和consul做个基本介绍;

    2、kong集成consul 做服务发现;

    3、Asp.net core WebApi 服务自动注册到Consul;

    4、Asp.net core WebApi 自动注册路由规则到kong,实现程序启动即部署;

    运行环境

    172.16.1.30 CentOS Linux release 7.6.1810 (Core) (虚拟机单核2g)

    Docker version 18.09.3, build 774a1f4

    kong apigateway(enterprise) 2.3.x (docker安装)

    kong

    kong的简介

    KONG — The Microservice API Gateway | by faren | Medium

    我们熟悉Nginx;

    有个一个加强版的Nginx叫做OpenRestry,OpenRestry ≈ lua脚本+Nginx;

    那么Kong 网关就是满血版的 OpenRestry,它有许许多多的的插件和各种丰富的功能,且提供对应的Rest Api,让你轻松打造你所能想象到的 网关+ web前端服务器的功能;

    特点(翻译)

    • 云原生:平台无关,kong支持任意平台,裸机容器或云平台;

    • k8s原生:原生支持k8s,有kong-ingress,支持l4+l7协议;

    • 动态负载均衡:负载均衡到多个upstream;

    • Hash-based的负载均衡:根据cookie、session,ip等hash负载均衡;

    • 断路器:自动剔除不健康的服务;

    • 心跳检测:主动和被动心跳检测;

    • 服务发现:通过第三方dns解析做服务发现,如consul;

    • Serverless:调用和保护 AWS Lambda or OpenWhisk functions directly ;

    • WebSockets:支持ws、wss协议;

    • gRPC:支持gRPC协议,并通过日志和插件监控流量;

    • OAuth2.0:轻松添加OAuth2.0支持;

    • 日志:轻松记录请求和响应,通过HTTP, TCP, UDP, 或 直接到硬盘;

    • 安全性:访问控制,爬虫检测、ip黑白名单等等;

    • Syslog:记录到系统日志;

    • SSL: 安装不同的SSL证书到服务;

    • 监控:实时监控,提供关机负责负载均衡和性能指标;

    • 正向代理:kong可以作为正向代理服务器;

    • 身份认证:HMAC, JWT, Basic, 各种奇奇怪怪的规则都支持.

    • 限制器:流量限制功能;

    • 传输转换:新增、删掉、或者修改你的请求或者响应;

    • 缓存:请求缓存;

    • CLI:命令行控制支持;

    • Rest Api:Rest Api控制支持;

    • Geo-Replicated:夸时区请求支持;

    • 故障检测与恢复:数据库(Cassandra /postgres)节点挂掉不影响kong的服务;

    • 集群:所有kong节点都自动加入集群保持配置同步;

    • 拓展性:分布式拓展原生支持,水平伸缩加减节点就行;

    • 高性能:使用Nginx作为核心负载均衡组件,高性能可伸缩;

    • 插件:高拓展性,插件式添加功能;

    详细请看

    github: https://github.com/Kong/kong

    官方文档: https://docs.konghq.com

    kong的安装

    拉取镜像

    docker pull kong/kong-gateway:2.3.3.2-alpine
    

    给镜像改个名

    docker tag <IMAGE_ID> kong-ee
    

    创建一个网络

    docker network create kong-ee-net
    

    运行一个postgresSql 9.6,用来存取kong的配置

    docker run -d --name kong-ee-database 
      --network=kong-ee-net 
      -p 5432:5432 
      -e "POSTGRES_USER=kong" 
      -e "POSTGRES_DB=kong" 
      -e "POSTGRES_PASSWORD=kong" 
      postgres:9.6
    

    启动kong

      docker run -d --name kong-ee2
      --network=kong-ee-net 
      -e "KONG_DATABASE=postgres" 
      -e "KONG_PG_HOST=172.16.1.30" 
      -e "KONG_PG_PASSWORD=kong" 
      -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" 
      -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" 
      -e "KONG_PROXY_ERROR_LOG=/dev/stderr" 
      -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" 
      -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" 
      -e "KONG_ADMIN_GUI_URL=http://172.16.1.30:8002"  
      -e "KONG_DNS_RESOLVER=172.16.1.30:8600"   #注意按需使用,consul的才配
      -p 8000:8000 
      -p 8443:8443 
      -p 8001:8001 
      -p 8444:8444 
      -p 8002:8002 
      -p 8445:8445 
      -p 8003:8003 
      -p 8004:8004 
      kong-ee
      
      
      //-e "KONG_DNS_RESOLVER=172.16.1.30:8600" 注意这个配置,这是我需要用的consul的dns配置,如果不想用consul做服务发现,删掉这行
      //如果既想用consul,又想用正常的域名解析服务这样配置:-e "KONG_DNS_RESOLVER=172.16.1.30:8600,114.114.114.114"
    

    这里说明一下,kong的配置是用postgres(或者Cassandra )来存配置,但每一次请求都不需要去读取数据库的。修改的配置会直接 reload 到内存中,不影响性能;

    另外说说kong的集群;

    因为kong 网关其实最终 表现为一个超级前端服务器+网关,所以每个连接到同个数据库的kong实例配置一样,连接同个数据库的kong作为一个集群;

    一般在kong的前面是直接做dns解析就行,如果dns不支持多ip的话做keepalive + vip就行;

    验证

    #admin api 获取所有服务
    curl -i -X GET --url http://127.0.0.1:8001/services
    
    #admin 管理后台 
    curl -i -X GET --url http://127.0.0.1:8002
    

    1622292530454

    1622292862508

    管理后台

    1622297057459

    consul

    consul简介

    Consul Service Mesh with Paul Banks - Software Engineering Daily

      Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案,比如 Airbnb的SmartStack等相比,Consul的方案更“一站式”,内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如ZooKeeper等),使用起来也较 为简单。

    其实就是做服务治理的。

    github: https://github.com/hashicorp/consul

    官方文档: https://www.consul.io/

    consul的安装

    直接docker安装

    *这是作为开发节点安装

    docker run  -d --name=dev-consul1 --network=host -e CONSUL_BIND_INTERFACE=eth0 consul:1.8
    

    安装成功

    1622295177798

    运行一个WebApi服务

    先在服务运行一个Asp.net Core WebApi (就是是新建的一个包含),我的版本是3.1的,我给服务命名:DemoApi31,监听端口5002

    1622294659671

    将服务注册到Consul

    curl --location --request PUT 'http://172.16.1.30:8500/v1/agent/service/register' 
    --header 'Content-Type: application/json' 
    --data-raw '{
      "ID": "DemoApi31_172.16.1.30:5002",
      "Name": "DemoApi31",
      "Address": "172.18.1.30",
      "Port": 5002,
      "EnableTagOverride": false,
      "Weights": {
        "Passing": 10,
        "Warning": 1
      }
    }'
    

    注册成功:

    1622295901281

    Dns解析验证

    # 如果没安装dig 安装:yum install bind-utils
    dig @172.16.1.30 -p 8600 Demoapi31.service.consul SRV
    

    1622294451840

    ok,我们这里已经把服务注册到consul,且能通过dns常解析到了,我们做跟kong的集成吧。

    consul提供内置Dns解析和Rest Api 两种方式集成做服务发现,我们这里跟kong的集成选用的Dns方式。

    kong集成consul做服务发现

    因为consul的角色是dns服务器,所以非常简单,我们已注册好的 DemoApi31为例:

    1、创建一个名为consul的服务

    DemoApi31.service.consul 是consu要求的格式

    1622297254563

    2、创建一个名为consul的路由

    1622297366317

    验证

    访问我们配置的kong路由:http://172.16.1.30:8000/consul/api/values

    1622297455784

    ok

    到目前为止我们只完成了本文目的1、2

    3,和4三请往下看;

    在Asp.net Core中的使用

      以之前的DemoApi31为例,换成5003端口,我需要达到的效果是,程序启动的时候就把服务注册到Consul 做好心跳检测,并同时部署到网关Kong,直接对外服务。

    Asp.net Core 服务自动注册到Consul

    安装nuget包

    Install-Package Passport.Infrastructure -Version 0.1.4.7-preview-1
    

    **加入配置appsettings.json**

    大家主要各服务器要改成自己的

      "ServiceDiscovery": {
        "ServiceName": "DemoApi31",
        "Consul": {
          "HttpEndpoint": "http://172.16.1.30:8500", 
          "HttpHeathCheck": {
            "Path": "/healthcheck",
            "TimeOunt": 10,
            "Interval": 10
          },
          "Tags": [
            "NetCore",
            "DemoApi",
            "v1.0"
          ]
        }
      }
    

    StartUp.cs ConfigureServices方法

    public void ConfigureServices(IServiceCollection services)
    {
        //第一行
        PassportConfig.InitPassportConfig(Configuration, Environment);
            
        ......
           
        services.AddHealthChecks();
        services.AddConsul();
    }
    

    StartUp.cs Configure方法

    app.UseHealthChecks("/healthcheck");
    

    启动程序

    dotnet DemoApi.Core3.1.dll --healthhost 172.16.1.30 --urls http://*:5003 
    

    1622299506567

    1622299584100

    源码解析

    /// <summary>
    /// 加入consul做服务管理
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection AddConsul(this IServiceCollection services)
    {
        var options = PassportConfig.GetSection("ServiceDiscovery").Get<ServiceDiscoveryOptions>();
    
        if (options?.Disable != true)
        {
    	var healthHost = PassportConfig.GetHealthHost();
    
    	if (string.IsNullOrWhiteSpace(options?.ServiceName) || string.IsNullOrWhiteSpace(options?.Consul?.HttpEndPoint))
    	{
    	    throw new ArgumentNullException("ServiceDiscovery.ServiceName/Consul.HttpEndpoint cannot be null or empty!");
    	}
    
    	//实例化kongclient
    	var consulClient = new ConsulClient(x => x.Address = new Uri(options.Consul.HttpEndPoint));
    	services.AddSingleton(consulClient);
    
    	services.Configure(new Action<ConsulOptions>(op =>
    	{
    	    op.HttpEndPoint = options.Consul.HttpEndPoint;
    	    op.Token = options.Consul.Token;
    	    op.TcpEndPoint = options.Consul.TcpEndPoint;
    	}));
    
    	var checkOptions = options.Consul.HttpHeathCheck;
    	var checkUrl = $"http://{healthHost}:{PassportConfig.GetCurrentPort()}{checkOptions.Path}";
    
    	new ConsulBuilder(consulClient)
    	    .AddHttpHealthCheck(checkUrl, checkOptions.TimeOunt, checkOptions.Interval)
    	    .RegisterService(options.ServiceName, healthHost, PassportConfig.GetCurrentPort(), options.Consul.Tags)
    	    .Wait();
        }
    
        return services;
    }
    

    ConsulBuilder.cs 参考晓晨大佬

     public class ConsulBuilder
        {
            private readonly ConsulClient _client;
            private readonly List<AgentServiceCheck> _checks = new List<AgentServiceCheck>();
    
            public ConsulBuilder(ConsulClient client)
            {
                _client = client;
            }
    
            public ConsulBuilder AddHealthCheck(AgentServiceCheck check)
            {
                _checks.Add(check);
                return this;
            }
    
            /// <summary>
            ///
            /// </summary>
            /// <param name="url"></param>
            /// <param name="timeout">unit: second</param>
            /// <param name="interval">check interval. unit: second</param>
            /// <returns></returns>
            public ConsulBuilder AddHttpHealthCheck(string url, int timeout = 10, int interval = 10)
            {
                _checks.Add(new AgentServiceCheck()
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(timeout * 3),
                    Interval = TimeSpan.FromSeconds(interval),
                    HTTP = url,
                    Timeout = TimeSpan.FromSeconds(timeout)
                });
    
                PassportConsole.Success($"[Consul]Add Http Healthcheck Success! CheckUrl:{url}");
    
                return this;
            }
    
            /// <summary>
            ///
            /// </summary>
            /// <param name="endpoint">GPRC service address.</param>
            /// <param name="grpcUseTls"></param>
            /// <param name="timeout">unit: second</param>
            /// <param name="interval">check interval. unit: second</param>
            /// <returns></returns>
            public ConsulBuilder AddGRPCHealthCheck(string endpoint, bool grpcUseTls = false, int timeout = 10, int interval = 10)
            {
                _checks.Add(new AgentServiceCheck()
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(20),
                    Interval = TimeSpan.FromSeconds(interval),
                    GRPC = endpoint,
                    GRPCUseTLS = grpcUseTls,
                    Timeout = TimeSpan.FromSeconds(timeout)
                });
    
                PassportConsole.Success($"[Consul]Add GRPC HealthCheck Success! Endpoint:{endpoint}");
    
                return this;
            }
    
            public async Task RegisterService(string name, string host, int port, string[] tags)
            {
                var registration = new AgentServiceRegistration()
                {
                    Checks = _checks.ToArray(),
                    ID = $"{name}_{host}:{port}",
                    Name = name,
                    Address = host,
                    Port = port,
                    Tags = tags
                };
    
                await _client.Agent.ServiceRegister(registration);
                PassportConsole.Success($"[Consul]Register Service Success! Name:{name} ID:{registration.ID}");
    
                AppDomain.CurrentDomain.ProcessExit += async (sender, e) =>
                {
                    PassportConsole.Information($"[Consul] Service Deregisting ....  ID:{registration.ID}");
    
                    await _client.Agent.ServiceDeregister(registration.ID);
                };
            }
    
            /// <summary>
            /// 移除服务
            /// </summary>
            /// <param name="serviceId"></param>
            public async Task Deregister(string serviceId)
            {
                await _client?.Agent?.ServiceDeregister(serviceId);
            }
        }
    

    逻辑简单,确定自己需要用的是注册服务功能,调Consul Api 注册,然后程序退出的时候注销consul的服务就行;

    Asp.net core WebApi 自动注册路由规则到kong

    通过Consul

    安装nuget包

    #已安装跳过
    Install-Package Passport.Infrastructure -Version 0.1.4.7-preview-1
    

    **加入配置appsettings.json**

    guid顺便去https://www.guidgen.com/ 生成一个

    "Kong": {
        //"Disable": false, //true=禁用
        "Host": "http://172.16.1.30:8001",
        "Services": [
          {
            "Id": "72e21af8-283f-44c4-a766-53de8bb35c21", //guid
            "Name": "service-autoapi",
            "Retries": 5,
            "Protocol": "http",
            "Host": "DemoApi31.service.consul", 
            "Port": 0,
            "Path": null,
            "Connect_timeout": 60000, //毫秒
            "Write_timeout": 60000,
            "Read_timeout": 60000,
            "Tags": null
          }
        ],
        "Routes": [
          {
            "Id": "5370e1b7-6c43-442d-9a44-23c249f958f7",
            "Name": "route-autoapi",
            "Protocols": [ "http" ],
            "Methods": null,
            "Hosts": null,
            "Paths": [ "/autoapi" ],
            "Https_redirect_status_code": 307,
            "Regex_priority": 0,
            "Strip_path": true,
            "Preserve_host": false,
            "Tags": null,
            "Service": {
              "Id": "72e21af8-283f-44c4-a766-53de8bb35c21" //这个id跟关联的Services的id一致
            }
          }
        ]
     }
    

    StartUp.cs ConfigureServices方法

    public void ConfigureServices(IServiceCollection services)
    {
       ......
       
       
        services.AddConsul();
        services.RouteRegistToKong();
    }
    

    启动程序

    dotnet DemoApi.Core3.1.dll --healthhost 172.16.1.30 --urls http://*:5003 
    

    验证

    查看kong管理后台:

    1622301181490

    访问 http://172.16.1.30:8000/auto/api/values

    1622301195836

    大功告成。

    不通过Consul,直接配置路由到kong

    StartUp.cs ConfigureServices方法

    public void ConfigureServices(IServiceCollection services)
    {
       ......
       
       
        //删掉这行services.AddConsul();
        services.RouteRegistToKong();
    }
    

    配置变为

    "Kong": {
        //"Disable": false, //true=禁用
        "Host": "http://172.16.1.30:8001",
        "Services": [
          {
            "Id": "0f86015b-b170-4ada-b045-740ae7d77ed6", //guid
            "Name": "configupapi",
            "Retries": 5,
            "Protocol": "http",
            "Host": "configupapi",
            "Port": 0,
            "Path": null,
            "Connect_timeout": 60000, //毫秒
            "Write_timeout": 60000,
            "Read_timeout": 60000,
            "Tags": null
          }
        ],
        "Routes": [
          {
            "Id": "1be79a57-af87-43b0-a0a0-b7a6cc0c5ade",
            "Name": "configupapi",
            "Protocols": [ "http" ],
            "Methods": null,
            "Hosts": null,
            "Paths": [ "/configupapi" ],
            "Https_redirect_status_code": 307,
            "Regex_priority": 0,
            "Strip_path": true,
            "Preserve_host": false,
            "Tags": null,
            "Service": {
              "Id": "0f86015b-b170-4ada-b045-740ae7d77ed6" //这个id跟Services的id一致
            }
          }
        ],
    
        "Upstream": {
          "Id": "8efd15af-df78-422f-97a0-9072fa7e7431",
          "Tags": [ "exampleapi", "v1.0" ],
          "Name": "configupapi",
          "Hash_on": "none",
          "Healthchecks": {
            "Active": {
              "Unhealthy": {
                "Http_statuses": [ 429, 500, 501, 502, 503, 504, 505 ],
                "Tcp_failures": 1,
                "Timeouts": 2,
                "Http_failures": 1,
                "Interval": 5
              },
              "Type": "http",
              "Http_path": "/healthcheck",
              "Timeout": 1,
              "Healthy": {
                "Successes": 1,
                "Interval": 20,
                "Http_statuses": [ 200, 302 ]
              },
              "Https_verify_certificate": true,
              "Concurrency": 1
            },
            "Passive": {
              "Unhealthy": {
                "Http_statuses": [ 429, 500, 501, 502, 503, 504, 505 ]
              },
              "Healthy": {
                "Http_statuses": [ 200, 201, 302 ]
              },
              "Type": "http"
            }
          },
          "Hash_on_cookie_path": "/",
          "Hash_fallback": "none",
          "Slots": 10000
        },
        "Target": {
          "Tags": [ "exampleapi", "v1.0" ],
          "Weight": 100
        }
      }
    

    源码解析

    /// <summary>
    /// 路由注册到kong;
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection RouteRegistToKong(this IServiceCollection services)
    {
        if (!PassportConfig.GetBool("Kong:Disable"))
        {
    	var konghost = PassportConfig.Get("Kong:Host") ?? throw new ArgumentNullException("Kong:Host cannot be null or empty!");
    
    	var options = new KongClientOptions(HttpClientFactory.Create(), konghost);
    	var client = new KongClient(options);
    	services.AddSingleton<KongClient>(client);
    
    	var upStream = PassportConfig.GetSection("Kong:Upstream").Get<UpStream>();
    	var target = PassportConfig.GetSection("Kong:Target").Get<TargetInfo>();
    
    	if (upStream != null && target != null)
    	{
    	    upStream.Created_at = DateTime.Now;
    	    upStream = client.UpStream.UpdateOrCreate(upStream).Result;
    
    	    target.Target = $"{PassportConfig.GetHealthHost()}:{PassportConfig.GetCurrentPort()}";
    	    target.Id = PassportTools.GuidFromString($"{Dns.GetHostName()}{target.Target}");
    	    target.Created_at = DateTime.Now;
    	    target.UpStream = new TargetInfo.UpStreamId { Id = upStream.Id.Value };
    	    client.Target.Add(target).Wait();
    
    	    PassportConsole.Success($"[Kong]UpStream registered:{upStream.Name} Target:{target.Target}");
    
    	    // app.UseKongHealthChecks(upStream, onExecuter);
    	}
    
    	var kongServices = PassportConfig.GetSection("Kong:Services").Get<ServiceInfo[]>();
    	var kongRoutes = PassportConfig.GetSection("Kong:Routes").Get<RouteInfo[]>();
    
    	if (kongServices?.Length > 0 == true)
    	{
    	    foreach (var item in kongServices)
    	    {
    		item.Updated_at = DateTime.Now;
    		item.Path = string.IsNullOrWhiteSpace(item.Path) ? null : item.Path;
    		client.Service.UpdateOrCreate(item).Wait();
    		PassportConsole.Success($"[Kong]Service registered:{item.Name}");
    	    }
    	}
    
    	if (kongRoutes?.Length > 0 == true)
    	{
    	    foreach (var item in kongRoutes)
    	    {
    		item.Updated_at = DateTime.Now;
    		client.Route.UpdateOrCreate(item).Wait();
    		PassportConsole.Success($"[Kong]Route registered:{item.Name}");
    	    }
    	}
        }
    
        return services;
    }
    

      逻辑也简单,也是调用kong配置把本该手工配置的路由,分别调用upstream、service、route Api修改配置。有区别的是程序退出时不会去删对应的路由;

    总结

      我在各技术博客都没有看到总结的比较好的kong+consul+asp.net core的集成文章,特此总结。期待您的点赞留意;

    [参考]

    https://docs.konghq.com/

    https://www.cnblogs.com/stulzq/p/11942691.html

    https://github.com/lianggx/Kong.Net

    https://www.consul.io/docs

  • 相关阅读:
    PHP pcntl
    Linux 远程登录命令telnet
    git .gitignore不生效
    使用 GoLand 启动 运行 Go 项目
    Go语言: 万物皆异步
    MYSQL 单表一对多查询,将多条记录合并成一条记录
    详解PHP中instanceof关键字及instanceof关键字有什么作用
    all_user_func()详解
    python的反射
    python 的魔术方法
  • 原文地址:https://www.cnblogs.com/xiaxiaolu/p/14826794.html
Copyright © 2020-2023  润新知