Ocelot(二)- 請求聚合與負載均衡

Ocelot(二)- 請求聚合與負載均衡

做者:markjiang7m2
原文地址:http://www.javashuo.com/article/p-tzjyhyew-ez.html
源碼地址:https://gitee.com/Sevenm2/OcelotDemohtml

在上一篇Ocelot的文章中,我已經給你們介紹了何爲Ocelot以及如何簡單使用它的路由功能,若是你尚未不瞭解Ocelot爲什麼物,能夠查看個人系列文章 Ocelot - .Net Core開源網關。在這篇文章中,我將會繼續給你們介紹Ocelot的功能:請求聚合與負載均衡。git

開篇題外話:在上一篇文章的案例中,我直接使用API返回服務器的端口和接口的路徑,我感受這樣舉例過於偏技術化,比較沉悶,而後我想到了以前參加PMP課程培訓時候,咱們的培訓講師——孫志斌老師引用小王老李的模型給咱們講述項目管理的各類實戰,可謂是生動形象,並且所舉的例子也很是貼近咱們的平常工做,通俗易懂,所以,我也嘗試使用相似的人物形象進行案例的講解。首先,本文將會引入兩我的物WillingJack。Willing是一名資深專家,工做多年,而Jack則是.NET新手。github

本文中涉及案例的完整代碼均可以從個人代碼倉庫進行下載。算法

案例二 請求聚合

咱們在案例一路由中已經知道,Ocelot能夠定義多組路由,而後根據優先級對上游服務發出的請求進行不一樣的轉發處理,每一個路由轉發都匹配惟一的一個下游服務API接口。然而,有時候,上游服務想要得到來自兩個API接口返回的結果。Ocelot容許咱們在配置文件中聲明聚合路由Aggregates,從而實現這樣的效果。
舉個例子,有一天個人老闆(用戶)讓我(上游服務)去了解清楚Willing和Jack兩位同事對工做安排有什麼意見(請求),固然了,我能夠先跑去問Jack,而後再跑到Willing那裏瞭解狀況,但是這樣我就要跑兩趟,這樣不划算啊,因而,我去找了他們的領導(聚合)說我老闆想要了解他們兩個的意見,他們領導一個電話打過去,Willing和Jack就都一塊兒過來了,我也就很快完成了老闆交代的任務。
在這個過程當中,我是能夠單獨訪問Willing或者Jack的,所以,他們是在ReRoutes中聲明的兩組普通的路由,而他們的領導是在Aggregates中聲明的一組聚合路由。剛剛咱們的舉例當中,訪問不一樣的人須要到達不一樣的地方,所以在聲明路由時,也須要注意它們的UpstreamPathTemplate都是不同的。
下面是具體的路由配置:json

"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屬性值。c#

固然,咱們的下游服務也相應添加兩個API接口。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_002_aggrjack安全

而後再看看直接經過聚合路由訪問
Ocelot_003_aggrleader服務器

能夠看到,在返回結果中同時包含了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_004_aggrerror

直接經過聚合路由訪問
Ocelot_005_aggrleadererror

前面我說到返回結果好像有哪裏不太對,那究竟是哪裏出錯了呢?我來將返回的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_006_aggrleaderjson

簡單總結爲如下三點注意:

  • 僅支持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_007_aggrleaderadvanced

也許你們已經留意到,我在處理返回結果是,並無像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_008_balance8001

而當咱們再次請求http://localhost:4727/ocelot/5,獲得的是端口8002的返回結果

Ocelot_009_balance8002

再次請求則又是8001的返回結果,如此輪詢下去。
但須要注意的是,當我嘗試將8002端口服務中止時

Ocelot_010_balanceiis

我獲得了這樣的結果:第一次請求獲得8001的返回結果,第二次請求獲得的則是500的狀態碼

Ocelot_011_balanceerror

根據官網文檔的說明

RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot’s.

的確說的是輪詢可用的服務,彷佛與個人測試結果不相符。不知道是個人測試環境出了問題,仍是我某個環節配置錯誤,亦或是這個算法真的沒有避開不可用的服務。但願有知道的朋友在留言區給我解惑,感謝。

在本案例中,我就再也不展開演示另外3種算法了,其中NoLoadBalancer會與服務發現的案例再進行深刻探討。

總結

原本今天是想給你們寫多兩個功能案例的,奈何這個進階的資料實在很少,固然也有我本身一方面實力不足的緣由,致使花了很長的時間進行消化。在本文中介紹了Ocelot的請求聚合與負載均衡,其中請求聚合在使用的過程當中仍是有幾點須要注意的,負載均衡則須要你們按需選擇適合本身系統的算法。後續還會有Ocelot的系列文章,但願你們持續關注。

相關文章
相關標籤/搜索