欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Ocelot(二)- 请求聚合

程序员文章站 2022-04-09 08:37:21
Ocelot(二) 请求聚合与负载均衡 作者:markjiang7m2 原文地址: 源码地址:https://gitee.com/Sevenm2/OcelotDemo 在上一篇Ocelot的文章中,我已经给大家介绍了何为Ocelot以及如何简单使用它的路由功能,如果你还没有不了解Ocelot为何物, ......

ocelot(二)- 请求聚合与负载均衡

作者:markjiang7m2
原文地址:
源码地址:https://gitee.com/sevenm2/ocelotdemo

在上一篇ocelot的文章中,我已经给大家介绍了何为ocelot以及如何简单使用它的路由功能,如果你还没有不了解ocelot为何物,可以查看我的系列文章 ocelot - .net core开源网关。在这篇文章中,我将会继续给大家介绍ocelot的功能:请求聚合与负载均衡。

开篇题外话:在上一篇文章的案例中,我直接使用api返回服务器的端口和接口的路径,我感觉这样举例过于偏技术化,比较沉闷,然后我想到了之前参加pmp课程培训时候,我们的培训讲师——孙志斌老师引用小王老李的模型给我们讲述项目管理的各种实战,可谓是生动形象,而且所举的例子也非常贴近我们的日常工作,通俗易懂,因此,我也尝试使用类似的人物形象进行案例的讲解。首先,本文将会引入两个人物willingjack。willing是一名资深专家,工作多年,而jack则是.net新手。

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

案例二 请求聚合

我们在案例一路由中已经知道,ocelot可以定义多组路由,然后根据优先级对上游服务发出的请求进行不同的转发处理,每个路由转发都匹配唯一的一个下游服务api接口。然而,有时候,上游服务想要获得来自两个api接口返回的结果。ocelot允许我们在配置文件中声明聚合路由aggregates,从而实现这样的效果。
举个例子,有一天我的老板(用户)让我(上游服务)去了解清楚willing和jack两位同事对工作安排有什么意见(请求),当然了,我可以先跑去问jack,然后再跑到willing那里了解情况,可是这样我就要跑两趟,这样不划算啊,于是,我去找了他们的领导(聚合)说我老板想要了解他们两个的意见,他们领导一个电话打过去,willing和jack就都一起过来了,我也就很快完成了老板交代的任务。
在这个过程中,我是可以单独访问willing或者jack的,因此,他们是在reroutes中声明的两组普通的路由,而他们的领导是在aggregates中声明的一组聚合路由。刚刚我们的举例当中,访问不同的人需要到达不同的地方,因此在声明路由时,也需要注意它们的upstreampathtemplate都是不一样的。
下面是具体的路由配置:

"reroutes": [
{
    "downstreampathtemplate": "/api/ocelot/aggrwilling",
    "downstreamscheme": "http",
    "downstreamhostandports": [
    {
        "host": "localhost",
        "port": 8001
    }
    ],
    "upstreampathtemplate": "/ocelot/aggrwilling",
    "upstreamhttpmethod": [ "get" ],
    "key": "aggr_willing",
    "priority": 2
},
{
    "downstreampathtemplate": "/api/ocelot/aggrjack",
    "downstreamscheme": "http",
    "downstreamhostandports": [
    {
        "host": "localhost",
        "port": 8001
    }
    ],
    "upstreampathtemplate": "/ocelot/aggrjack",
    "upstreamhttpmethod": [ "get" ],
    "key": "aggr_jack",
    "priority": 2
}
],
"aggregates": [
{
    "reroutekeys": [
    "aggr_willing",
    "aggr_jack"
    ],
    "upstreampathtemplate": "/aggrleader"
}
]

大家可以注意到,在reroutes中声明的两组路由相比案例一不同的是,多加了一个key属性。aggregatesreroutes是同级的,而且也是一个数组,这代表着我们可以声明多个聚合路由,而在我们声明的这一组聚合路由中的属性reroutekeys,它包含的元素就是我们真正需要响应的路由的key属性值。

当然,我们的下游服务也相应添加两个api接口。

// get api/ocelot/aggrwilling
[httpget("aggrwilling")]
public async task<iactionresult> aggrwilling(int id)
{
    var result = await task.run(() =>
    {
        return $"我是willing,还是多加工资最实际, path: {httpcontext.request.path}";
    });
    return ok(result);
}
// get api/ocelot/aggrjack
[httpget("aggrjack")]
public async task<iactionresult> aggrjack(int id)
{
    var result = await task.run(() =>
    {
        return $"我是jack,我非常珍惜现在的工作机会, path: {httpcontext.request.path}";
    });
    return ok(result);
}

下面我们一起来看看执行的结果。

我们按照案例一,先单独来问问jack。
Ocelot(二)- 请求聚合

然后再看看直接通过聚合路由访问
Ocelot(二)- 请求聚合

可以看到,在返回结果中同时包含了willing和jack的结果,并且是以json串的格式返回,以路由的key属性值作为返回json的属性。

(返回的结果好像哪里不太对,不知道你是否发现了,但暂时先不要着急,我在后面会为大家揭晓)

需要注意的是,ocelot仅支持get方式的请求聚合。ocelot总是以application/json的格式返回一个聚合请求的,当下游服务是返回404状态码,在返回结果中,其对应的值则为空值,即使聚合路由中所有的下游服务都返回404状态码,聚合路由的返回结果也不会是404状态码。

我们在不添加任何api接口的情况下,声明一组下游服务不存在的路由,并将它添加到聚合路由当中。

"reroutes": [
...,
{
    "downstreampathtemplate": "/api/ocelot/aggrerror/1",
    "downstreamscheme": "http",
    "downstreamhostandports": [
    {
        "host": "localhost",
        "port": 8001
    }
    ],
    "upstreampathtemplate": "/ocelot/aggrerror/1",
    "upstreamhttpmethod": [ "get" ],
    "key": "aggr_error",
    "priority": 2
}
],
"aggregates": [
{
    "reroutekeys": [
    "aggr_willing",
    "aggr_jack",
    "aggr_error"
    ],
    "upstreampathtemplate": "/aggrleader"
}
]

测试结果如下:

直接请求aggr_error
Ocelot(二)- 请求聚合

直接通过聚合路由访问
Ocelot(二)- 请求聚合

前面我说到返回结果好像有哪里不太对,那到底是哪里出错了呢?我来将返回的json串进行格式化一下。

{
    "aggr_willing":我是willing,还是多加工资最实际, path: /api/ocelot/aggrwilling,
    "aggr_jack":我是jack,我非常珍惜现在的工作机会, path: /api/ocelot/aggrjack,
    "aggr_error":
}

我们会发现这并不是一个正确的json串,那到底为什么会这样呢?既然ocelot是开源的,那我们就来深挖一下源码到底是怎么处理聚合请求返回结果的。
ocelot github:https://github.com/threemammals/ocelot
找到位于ocelot.middleware.multiplexer中的一个类simplejsonresponseaggregator,静态方法mapaggregatecontent

var content = await contexts[0].downstreamresponse.content.readasstringasync();
contentbuilder.append($"\"{responsekeys[k]}\":{content}");

因为我的下游服务返回结果是一个字符串,然后被ocelot直接拼接到返回结果中,从而得到我们上面看到的结果。
因此,在我看来,当我们使用ocelot的聚合路由功能时,下游服务的返回结果必须要保证是一个json串,这样才能最终被正确识别。

我把下游服务改一改,添加一个类,然后将api返回结果格式更改为这个类型。

public class responseresult
{
    public string comment { get; set; }
}
// get api/ocelot/aggrwilling
[httpget("aggrwilling")]
public async task<iactionresult> aggrwilling(int id)
{
    var result = await task.run(() =>
    {
        responseresult response = new responseresult()
        { comment = $"我是willing,还是多加工资最实际, path: {httpcontext.request.path}" };
        return response;
        //return $"我是willing,还是多加工资最实际, path: {httpcontext.request.path}";
    });
    return ok(result);
}
// get api/ocelot/aggrjack
[httpget("aggrjack")]
public async task<iactionresult> aggrjack(int id)
{
    var result = await task.run(() =>
    {
        responseresult response = new responseresult()
        { comment = $"我是jack,我非常珍惜现在的工作机会, path: {httpcontext.request.path}" };
        return response;
        //return $"我是jack,我非常珍惜现在的工作机会, path: {httpcontext.request.path}";
    });
    return ok(result);
}

运行看执行结果
Ocelot(二)- 请求聚合

简单总结为以下三点注意:

  • 仅支持get方式
  • 下游服务返回类型要求为application/json
  • 返回内容类型为application/json,不会返回404请求

进阶请求聚合

在上一个案例中,我已经可以通过willing和jack的领导得到我想要的结果,但在这个过程中,他们的领导(聚合)都只是在帮我获得结果,没有对得到的结果做任何的干预。那如果领导想着,既然老板想要了解情况,自己当然也要干点活,让老板知道在这个过程中自己也是有出力的,这就涉及到进阶的请求聚合了。

在网上搜了一下关于进阶请求聚合的资料,好像没有怎么见到有相关实例的demo,最全面的资料来自于官网文档说明,也许是在实际应用中这个功能不怎么被运用?或是我打开的方式不对?原因暂时未知,知道的朋友们可以在留言区给我说一下。那么我在这里就用实例给大家介绍一下。

ocelot支持在获得下游服务返回结果后,通过一个聚合器对返回结果进行再一步的加工处理,目前支持内容,头和状态代码的修改。我们来看配置文件

"aggregates": [
{
    "reroutekeys": [
    "aggr_willing",
    "aggr_jack",
    "aggr_error"
    ],
    "upstreampathtemplate": "/aggrleaderadvanced",
    "aggregator": "leaderadvancedaggregator"
}
]

因为是请求聚合的进阶,所以reroutes路由不需要任何更改。aggregates中一组配置增加了属性aggregator,表示当获得返回结果,由聚合器leaderadvancedaggregator进行处理。

然后我在ocelot项目中添加聚合器leaderadvancedaggregator,要实现这个聚合器,就必须实现来自ocelot.middleware.multiplexer提供的接口idefinedaggregator

public class leaderadvancedaggregator : idefinedaggregator
{
    public async task<downstreamresponse> aggregate(list<downstreamresponse> responses)
    {
        list<string> results = new list<string>();
        var contentbuilder = new stringbuilder();

        contentbuilder.append("{");

        foreach (var down in responses)
        {
            string content = await down.content.readasstringasync();
            results.add($"\"{guid.newguid()}\":{content}");
        }
        //来自leader的声音
        results.add($"\"{guid.newguid()}\":{{comment:\"我是leader,我组织了他们两个进行调查\"}}");

        contentbuilder.append(string.join(",", results));
        contentbuilder.append("}");

        var stringcontent = new stringcontent(contentbuilder.tostring())
        {
            headers = { contenttype = new mediatypeheadervalue("application/json") }
        };
        
        var headers = responses.selectmany(x => x.headers).tolist();
        return new downstreamresponse(stringcontent, httpstatuscode.ok, headers, "some reason");
    }
}

当下游服务返回结果后,ocelot就会调用聚合器的aggregate方法,因此,我们的处理代码就写在这个方法中。

之后,我们就需要将聚合器在容器中进行注册
startup.cs

services
    .addocelot()
    .addsingletondefinedaggregator<leaderadvancedaggregator>();

运行,访问进阶请求聚合的urlhttp://localhost:4727/aggrleaderadvanced,得到如下结果:
Ocelot(二)- 请求聚合

也许大家已经留意到,我在处理返回结果是,并没有像ocelot内部返回结果一样使用路由的key作为属性,而是使用了guid。其实这也是我在做demo时候的一处疑惑,我似乎无法像ocelot内部一样处理。
在这个aggregate方法中提供的参数类型只有list<downstreamresponse>,但downstreamresponse中并没有关于reroutekeys的信息。我查看了ocelot的源码,reroutekeys只存在于downstreamreroute中,但我无法通过downstreamresponse获取到downstreamreroute
希望有知道的朋友能在留言区告诉我一下,感谢。

另外,这个聚合器也能像一般服务一样,可以使用依赖注入的方式添加依赖。我也尝试在案例中添加了一个依赖leaderadvanceddependency。如何使用依赖注入,我这里就不细说了,大家可以搜索 .net core依赖注入的相关资料。
leaderadvancedaggregator.cs

public leaderadvanceddependency _dependency;

public leaderadvancedaggregator(leaderadvanceddependency dependency)
{
    _dependency = dependency;
}

startup.cs

services.addsingleton<leaderadvanceddependency>();

这样,我们就可以在聚合器中使用依赖了。

ocelot除了支持singleton的聚合器以外,还支持transient的聚合器,大家可以按需使用。
startup.cs

services
    .addocelot()
    .addtransientdefinedaggregator<leaderadvancedaggregator>();

案例三 负载均衡

在前面的案例中,我们全部的路由配置中都是一组路由配置一个下游服务地址,也就意味着,当上游服务请求一个url,ocelot就必定转发给某一个固定的下游服务,但这样对于一个系统来说,这是不安全的,因为有可能某一个下游服务阻塞,甚至挂掉了,那就可能导致整个服务瘫痪了,对于当前快速运转的互联网时代,这是不允许的。

ocelot能够通过可用的下游服务对每个路由进行负载平衡。我们来看看具体的路由配置

{
    "downstreampathtemplate": "/api/ocelot/{postid}",
    "downstreamscheme": "http",
    "downstreamhostandports": [
    {
        "host": "localhost",
        "port": 8001
    },
    {
        "host": "localhost",
        "port": 8002
    }
    ],
    "upstreampathtemplate": "/ocelot/{postid}",
    "upstreamhttpmethod": [ "get" ],
    "loadbalanceroptions": {
    "type": "roundrobin"
    }
}

leadconnection负载均衡器算法共有4种:

  • leastconnection 把新请求发送到现有请求最少的服务上
  • roundrobin 轮询可用的服务并发送请求
  • noloadbalancer 不负载均衡,总是发往第一个可用的下游服务
  • cookiestickysessions 使用cookie关联所有相关的请求到制定的服务

为了能快速验证负载均衡器的有效性,我们这个案例中采用了roundrobin轮询算法。然后下游服务还是用了案例一中建立的基本服务,在iis中部署两套同样的下游服务,分别占用端口8001和8002。

当我们第一次请求http://localhost:4727/ocelot/5,得到的是端口8001的返回结果

Ocelot(二)- 请求聚合

而当我们再次请求http://localhost:4727/ocelot/5,得到的是端口8002的返回结果

Ocelot(二)- 请求聚合

再次请求则又是8001的返回结果,如此轮询下去。
但需要注意的是,当我尝试将8002端口服务停止时

Ocelot(二)- 请求聚合

我得到了这样的结果:第一次请求得到8001的返回结果,第二次请求得到的则是500的状态码

Ocelot(二)- 请求聚合

根据官网文档的说明

roundrobin - loops through available services and sends requests. the algorythm state is not distributed across a cluster of ocelot’s.

的确说的是轮询可用的服务,似乎与我的测试结果不相符。不知道是我的测试环境出了问题,还是我某个环节配置错误,亦或是这个算法真的没有避开不可用的服务。希望有知道的朋友在留言区给我解惑,感谢。

在本案例中,我就不再展开演示另外3种算法了,其中noloadbalancer会与服务发现的案例再进行深入探讨。

总结

本来今天是想给大家写多两个功能案例的,奈何这个进阶的资料实在不多,当然也有我自己一方面实力不足的原因,导致花了很长的时间进行消化。在本文中介绍了ocelot的请求聚合与负载均衡,其中请求聚合在使用的过程中还是有几点需要注意的,负载均衡则需要大家按需选择适合自己系统的算法。后续还会有ocelot的系列文章,希望大家持续关注。

Ocelot(二)- 请求聚合