• Ocelot(三)- 服务发现


    Ocelot(三)- 服务发现

    作者:markjiang7m2
    原文地址:http://letyouknow.net/ocelot/ocelot-tutorial-3.html
    源码地址:https://gitee.com/Sevenm2/OcelotDemo

    本文是我关于Ocelot系列文章的第三篇,主要是给大家介绍Ocelot的另一功能。与其说是给大家介绍,不如说是我们一起来共同探讨,因为我也是在一边学习实践的过程中,顺便把学习的过程记录下来罢了。
    正如本文要介绍的服务发现,在Ocelot中本该是一个较小的功能,但也许大家也注意到,这篇文章距离我的上一篇文章也有一个星期了。主要是因为Ocelot的服务发现支持提供程序Consul,而我对Consul并不怎么了解,因此花了比较长的时间去倒弄Consul。因为这个是关于Ocelot的系列文章,所以我暂时也不打算在本文中详细介绍Consul的功能以及搭建过程了,可能会在完成Ocelot系列文章后,再整理一篇关于Consul的文章。

    关于更多的Ocelot功能介绍,可以查看我的系列文章

    本文中涉及案例的完整代码都可以从我的代码仓库进行下载。

    Ocelot接口更新:进阶请求聚合

    好了,也许大家有疑问,为什么在这里又会重提请求聚合的内容?
    在上一篇文章Ocelot(二)- 请求聚合与负载均衡中,我曾说到进阶请求聚合中,由于Aggregate方法中提供的参数类型只有List<DownstreamResponse>,但DownstreamResponse中并没有关于ReRouteKeys的信息,所以处理返回结果时,并没有像Ocelot内部返回结果一样使用路由的Key作为属性。
    然后,今天我注意到了Ocelot有新版本发布,于是我做了更新,从13.5.0更新到了13.5.1,然后居然是有意外惊喜。
    接口方法Aggregate更新如下:
    13.5.0

    public interface IDefinedAggregator
    {
        Task<DownstreamResponse> Aggregate(List<DownstreamResponse> responses);
    }

    13.5.1

    public interface IDefinedAggregator
    {
        Task<DownstreamResponse> Aggregate(List<DownstreamContext> responses);
    }

    参数类型从List<DownstreamResponse>更改为List<DownstreamContext>。我们再来看看DownstreamContext

    public class DownstreamContext
    {
        public DownstreamContext(HttpContext httpContext);
    
    <span class="hljs-keyword">public</span> List&lt;PlaceholderNameAndValue&gt; TemplatePlaceholderNameAndValues { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> HttpContext HttpContext { <span class="hljs-keyword">get</span>; }
    <span class="hljs-keyword">public</span> DownstreamReRoute DownstreamReRoute { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> DownstreamRequest DownstreamRequest { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> DownstreamResponse DownstreamResponse { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> List&lt;Error&gt; Errors { <span class="hljs-keyword">get</span>; }
    <span class="hljs-keyword">public</span> IInternalConfiguration Configuration { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> IsError { <span class="hljs-keyword">get</span>; }
    

    }

    事实上,如果你有看过Ocelot内部处理请求聚合部分的代码,就会发现它使用的就是DownstreamContext,而如今Ocelot已经将这些路由,配置,请求,响应,错误等信息都开放出来了。哈哈,当然,GitHub上面的realease note,人家主要是为了让开发者能够捕获处理下游服务发生的错误,更多信息可以查看issue#892issue#890

    既然如此,那我就按照它内部的输出结果来一遍,当然我这里没有严格按照官方处理过程,只是简单的输出。

    public async Task<DownstreamResponse> Aggregate(List<DownstreamContext> responses)
    {
        List<string> results = new List<string>();
        var contentBuilder = new StringBuilder();
    
    contentBuilder.Append(<span class="hljs-string">"{"</span>);
    
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> down <span class="hljs-keyword">in</span> responses)
    {
        <span class="hljs-keyword">string</span> content = <span class="hljs-keyword">await</span> down.DownstreamResponse.Content.ReadAsStringAsync();
        results.Add(<span class="hljs-string">$""<span class="hljs-subst">{down.DownstreamReRoute.Key}</span>":<span class="hljs-subst">{content}</span>"</span>);
    }
    <span class="hljs-comment">//来自leader的声音</span>
    results.Add(<span class="hljs-string">$""leader":{{comment:"我是leader,我组织了他们两个进行调查"}}"</span>);
    
    contentBuilder.Append(<span class="hljs-keyword">string</span>.Join(<span class="hljs-string">","</span>, results));
    contentBuilder.Append(<span class="hljs-string">"}"</span>);
    
    <span class="hljs-keyword">var</span> stringContent = <span class="hljs-keyword">new</span> StringContent(contentBuilder.ToString())
    {
        Headers = { ContentType = <span class="hljs-keyword">new</span> MediaTypeHeaderValue(<span class="hljs-string">"application/json"</span>) }
    };
    
    <span class="hljs-keyword">var</span> headers = responses.SelectMany(x =&gt; x.DownstreamResponse.Headers).ToList();
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> DownstreamResponse(stringContent, HttpStatusCode.OK, headers, <span class="hljs-string">"some reason"</span>);
    

    }

    输出结果:

    Ocelot_012_aggrleaderadvance_new

    官方开放了这么多信息,相信开发者还可以使用进阶请求聚合做更多的东西,欢迎大家继续研究探讨,我这里就暂时介绍到这里了。

    案例四 服务发现

    终于到我们今天的正题——服务发现。关于服务发现,我的个人理解是在这个微服务时代,当下游服务太多的时候,我们就需要找一个专门的工具记录这些服务的地址和端口等信息,这样会更加便于对服务的管理,而当上游服务向这个专门记录的工具查询某个服务信息的过程,就是服务发现。

    举个例子,以前我要找的人也就只有Willing和Jack,所以我只要自己用本子(数据库)记住他们两个的位置就可以了,那随着公司发展,部门的人越来越多,他们经常会调换位置,还有入职离职的人员,这就导致我本子记录的信息没有更新,所以我找来了HR部门(Consul)帮忙统一管理,所有人有信息更新都要到HR部门那里进行登记(服务注册),然后当我(上游服务)想找人做某件事情(发出请求)的时候,我先到HR那里查询可以帮我完成这个任务的人员在哪里(服务发现),得到这些人员的位置信息,我也就可以选中某一个人帮我完成任务了。

    这里会涉及到的记录工具,就是Consul。流程图如下:

    Ocelot_013_consul

    当然了,在上面这个例子中好像没有Ocelot什么事,但是这样就需要我每次要找人的时候,都必须先跑到Consul那里查询一次位置信息,然后再根据位置信息去找对应的人。而其实这个过程我们是可以交给Ocelot来完成的。这样,每个下游服务都不需要单独跑一趟了,只专注于完成自己的任务就可以了。流程图如下:

    Ocelot_014_consulocelot

    通常当服务在10个以上的时候可以考虑使用服务发现。

    关于Consul的介绍跟使用说明,网上已经有很多相关资料,所以我这里是基于大家都了解Consul的情况下的介绍。
    官方建议每个Consul Cluster至少有3个或以上的运行在Server Mode的Agent,Client节点不限。由于我就这么一台机子,又不想搞虚拟机,所以我就直接用了Docker来部署使用Consul。
    (可能这里又涉及到了一个Docker的知识点,我这里暂时也不展开细说了。)

    因为我电脑的系统是Windows的,所以安装的Docker for Windows。

    拉取镜像
    Docker安装好之后,就用Windows自带的PowerShell运行下面的命令,拉取官方的Consul镜像。

    docker pull consul

    启动Consul
    节点1

    docker run -d -p 8500:8500 --name markserver1 consul agent -server -node marknode1 -bootstrap-expect 3 -data-dir=/tmp/consul -client="0.0.0.0" -ui

    -ui 启用 WEB UI,因为Consul节点启动默认占用8500端口,因此8500:8500将节点容器内部的8500端口映射到外部8500,可以方便通过Web的方式查看Consul集群的状态。默认数据中心为dc1。

    查看markserver1的IP

    docker inspect -f '{{.NetworkSettings.IPAddress}}' markserver1

    假设你们跟我一样,获取到的IP地址也是172.17.0.2

    节点2

    docker run -d --name markserver2 consul agent -server -node marknode2 -join 172.17.0.2

    启动节点markserver2,并且将该节点加入到markserver1中(-join 172.17.0.2)

    节点3

    docker run -d --name markserver3 consul agent -server -node marknode3 -join 172.17.0.2

    节点4以Client模式

    docker run -d --name markclient1 consul agent -node marknode4 -join 172.17.0.2

    没有-server参数,就会新建一个Client节点。

    这个时候可以查看一下数据中心dc1的节点

    docker exec markserver1 consul members

    Ocelot_015_consulmembers

    同时也可以在浏览器查看集群的状态。因为我在节点1中启动了WEB UI,而且映射到外部端口8500,所以我在浏览器直接访问http://localhost:8500/

    Ocelot_016_consulservice

    Ocelot_017_consulnode

    OK,这样我们就已经启动了Consul,接下来就是将我们的下游服务注册到Consul中。

    服务注册
    为了能让本案例看到不一样的效果,我特意新建了一个下游服务方法。在OcelotDownAPI项目中的Controller添加

    // GET api/ocelot/consulWilling
    [HttpGet("consulWilling")]
    public async Task<IActionResult> ConsulWilling(int id)
    {
        var result = await Task.Run(() =>
        {
            ResponseResult response = new ResponseResult()
            { Comment = $"我是Willing,你可以在Consul那里找到我的信息, host: {HttpContext.Request.Host.Value}, path: {HttpContext.Request.Path}" };
            return response;
        });
        return Ok(result);
    }

    然后重新发布到本机上的8001和8002端口。

    准备好下游服务后,就可以进行注册了。在PowerShell执行下面的命令:
    <YourIP>为我本机的IP地址,大家使用时注意替换。这里需要使用IP,而不能直接用localhost或者127.0.0.1,因为我的Consul是部署在Docker里面的,所以当Consul进行HealthCheck时,就无法通过localhost访问到我本机了。

    curl http://localhost:8500/v1/agent/service/register -Method PUT -ContentType 'application/json' -Body '{
      "ID": "ocelotService1",  
      "Name": "ocelotService",
      "Tags": [
        "primary",
        "v1"
      ],
      "Address": "localhost",
      "Port": 8001,
      "EnableTagOverride": false,
      "Check": {
        "DeregisterCriticalServiceAfter": "90m",
        "HTTP": "http://<YourIP>:8001/api/ocelot/5",
        "Interval": "10s"
      }
    }'

    Ocelot_018_consulregister

    我为了后面能实现负载均衡的效果,因此,也将8002端口的服务也一并注册进来,命令跟上面一样,只是要将端口号更换为8002就可以了。

    多说一句,关于这个命令行,其实就是用命令行的方式调用Consul服务注册的接口,所以在实际项目中,可以将这个注册接口调用放在下游服务的Startup.cs中,当下游服务运行即注册,还有注销接口调用也是一样的道理。

    Ocelot_019_consul_ocelotservice

    Ocelot_020_consulcheck

    服务发现
    直接通过浏览器或者PowerShell命令行都可以进行服务发现过程。
    浏览器访问http://localhost:8500/v1/catalog/service/ocelotService
    或者命令行curl http://localhost:8500/v1/catalog/service/ocelotService

    Ocelot_023_consulcatalog

    Ocelot添加Consul支持
    OcelotDemo项目中安装Consul支持,命令行或者直接使用Nuget搜索安装

    Install-Package Ocelot.Provider.Consul

    在Startup.cs的ConfigureServices方法中

    services
        .AddOcelot()
        .AddConsul()
        .AddSingletonDefinedAggregator<LeaderAdvancedAggregator>();

    Ocelot路由配置
    首先在ReRoutes中添加一组路由

    {
        "DownstreamPathTemplate": "/api/ocelot/consulWilling",
        "DownstreamScheme": "http",
        "UpstreamPathTemplate": "/ocelot/consulWilling",
        "UpstreamHttpMethod": [ "Get" ],
        "LoadBalancerOptions": {
        "Type": "RoundRobin"
        },
        "ServiceName": "ocelotService",
        "Priority": 2
    }

    可以发现这一组路由相对其它路由,少了DownstreamHostAndPorts,多了ServiceName,也就是这一组路由的下游服务,不是由Ocelot直接指定,而是通过Consul查询得到。

    GlobalConfiguration添加ServiceDiscoveryProvider,指定服务发现支持程序为Consul。

    "GlobalConfiguration": {
    "BaseUrl": "http://localhost:4727",
    "ServiceDiscoveryProvider": {
        "Host": "localhost",
        "Port": 8500,
        "Type": "Consul"
    }
    }

    运行OcelotDemo,并在浏览器中访问http://localhost:4727/ocelot/consulWilling

    Ocelot_021_consul8001

    Ocelot_022_consul8002

    因为我们在这组路由中配置了使用轮询的方式进行负载均衡,所以可以看到我们的访问结果中,是分别从8001和8002中轮询访问的。

    除了支持Consul,Ocelot还支持Eureka,我这里暂时就不另外做案例了。

    动态路由
    当使用服务发现提供程序时,Ocelot支持使用动态路由。

    上游服务请求Url模板:<Scheme>://<BaseUrl>/<ServiceName>/<ApiPath>/

    例如:http://localhost:4727/ocelotService/api/ocelot/consulWilling

    当Ocelot接收到请求,会向Consul查询服务ocelotService的信息,例如获取到对应IP为localhost,Port为8001,于是Ocelot会转发请求到http://localhost:8001/api/ocelot/consulWilling.

    Ocelot不支持动态路由与ReRoutes配置混合使用,因此,当我们要使用动态路由,就必须要保证ReRoutes中没有配置任何路由。

    来看Ocelot.json的配置

    {
      "ReRoutes": [],
      "Aggregates": [],
      "GlobalConfiguration": {
        "BaseUrl": "http://localhost:4727",
        "ServiceDiscoveryProvider": {
          "Host": "localhost",
          "Port": 8500,
          "Type": "Consul"
        },
        "DownstreamScheme": "http"
      }
    }

    这就是使用动态路由最简单的配置,当然,在这种模式下还支持RateLimitOptions,QoSOptions,LoadBalancerOptions和HttpHandlerOptions,DownstreamScheme等配置,也允许针对每个下游服务进行个性化设置,我这里不演示具体案例。

    {
        "ReRoutes": [],
        "Aggregates": [],
        "GlobalConfiguration": {
            "RequestIdKey": null,
            "ServiceDiscoveryProvider": {
                "Host": "localhost",
                "Port": 8500,
                "Type": "Consul",
                "Token": null,
                "ConfigurationKey": null
            },
            "RateLimitOptions": {
                "ClientIdHeader": "ClientId",
                "QuotaExceededMessage": null,
                "RateLimitCounterPrefix": "ocelot",
                "DisableRateLimitHeaders": false,
                "HttpStatusCode": 429
            },
            "QoSOptions": {
                "ExceptionsAllowedBeforeBreaking": 0,
                "DurationOfBreak": 0,
                "TimeoutValue": 0
            },
            "BaseUrl": null,
                "LoadBalancerOptions": {
                "Type": "LeastConnection",
                "Key": null,
                "Expiry": 0
            },
            "DownstreamScheme": "http",
            "HttpHandlerOptions": {
                "AllowAutoRedirect": false,
                "UseCookieContainer": false,
                "UseTracing": false
            }
        }
    }

    运行结果如下:

    Ocelot_024_consuldynamic

    因为使用动态路由就要清空其它的路由配置,因此,我就不将动态路由这部分的配置commit到仓库中了,大家要使用的时候可将我案例中的配置直接复制到Ocelot.json文件中即可。

    总结

    Ocelot发布13.5.1这个版本还是挺有惊喜的,而且正巧我刚做完请求聚合的案例,所以也方便大家实践。服务发现,就Ocelot而言只是很小的一个篇幅,因为确实只要配置几个参数就可以灵活运用了,但在于Consul提供程序,还有Docker,这两个都是新的知识点,对于已经接触过的朋友很快就能搭建出来,但对于还没玩过的朋友,就需要花点时间研究。
    OK,今天就先跟大家介绍到这里,希望大家能持续关注我们。

  • 相关阅读:
    try? try! try do catch try 使用详解
    Swift Write to file 到电脑桌面
    NSLayoutConstraint 使用详解 VFL使用介绍
    automaticallyAdjustsScrollViewInsets 详解
    Swift 给UITableView 写extension 时 报错 does not conform to protocol 'UITableViewDataSource'
    OC Swift中检查代码行数
    Swift中 @objc 使用介绍
    SWift中 '?' must be followed by a call, member lookup, or subscript 错误解决方案
    Swift 中 insetBy(dx: CGFloat, dy: CGFloat) -> CGRect 用法详解
    求1000之内所有“完数”(注:C程序设计(第四版) 谭浩强/著 P141-9)
  • 原文地址:https://www.cnblogs.com/letyouknowdotnet/p/10972495.html
Copyright © 2020-2023  润新知