1. 程式人生 > >.NET Core微服務之基於Ocelot實現API閘道器服務(續)

.NET Core微服務之基於Ocelot實現API閘道器服務(續)

一、負載均衡與請求快取

1.1 負載均衡

  為了驗證負載均衡,這裡我們配置了兩個Consul Client節點,其中ClientService分別部署於這兩個節點內(192.168.80.70與192.168.80.71)。

  為了更好的展示API Repsonse來自哪個節點,我們更改一下返回值:

    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public
IEnumerable<string> Get() { return new string[] { $"ClinetService: {DateTime.Now.ToString()} {Environment.MachineName} " + $"OS: {Environment.OSVersion.VersionString}" }; } ...... }

  Ocelot的配置檔案中確保有負載均衡的設定:

{
  "ReRoutes"
: [ ...... "LoadBalancerOptions": { "Type": "RoundRobin" }, ...... }

  接下來發布並部署到這兩個節點上去,之後啟動我們的API閘道器,這裡我用命令列啟動:

  

  然後就可以測試負載均衡了,在瀏覽器中輸入URL並連續重新整理:可以通過主機名看到的確是根據輪詢來進行的負載均衡。

  

負載均衡LoadBalance可選值:

  • RoundRobin - 輪詢,挨著來,雨露均沾
  • LeastConnection - 最小連線數,誰的任務最少誰來接客
  • NoLoadBalance - 不要負載均衡,讓我一個人累死吧  

1.2 請求快取

  Ocelot目前支援對下游服務的URL進行快取,並可以設定一個以秒為單位的TTL使快取過期。我們也可以通過呼叫Ocelot的管理API來清除某個Region的快取。

為了在路由中使用快取,需要在ReRoute中加上如下設定:

"FileCacheOptions": { "TtlSeconds": 10, "Region": "somename" }

  這裡表示快取10秒,10秒後過期。另外,貌似只支援get方式,只要請求的URL不變,就會快取。

  這裡我們仍以上面的demo為例,在增加了FileCacheOptions配置之後,進行一個小測試:因為我們設定的10s過期,所以在10s內拿到的都是快取,否則就會觸發負載均衡去不同節點拿資料。

  

二、限流與熔斷器(QoS)

2.1 限流 (RateLimit)

  對請求進行限流可以防止下游伺服器因為訪問過載而崩潰,我們只需要在路由下加一些簡單的配置即可以完成。另外,看文件發現,這個功能是張善友大隊長貢獻的,真是666。同時也看到一個園友catcherwong,已經實踐許久了,真棒。

  對於限流,我們可以對每個服務進行如下配置:

    "RateLimitOptions": {
        "ClientWhitelist": [ "admin" ], // 白名單
        "EnableRateLimiting": true, // 是否啟用限流
        "Period": "1m", // 統計時間段:1s, 5m, 1h, 1d
        "PeriodTimespan": 15, // 多少秒之後客戶端可以重試
        "Limit": 5 // 在統計時間段內允許的最大請求數量
      }

  同時,我們可以做一些全域性配置:

    "RateLimitOptions": {
      "DisableRateLimitHeaders": false, // Http頭  X-Rate-Limit 和 Retry-After 是否禁用
      "QuotaExceededMessage": "Too many requests, are you OK?", // 當請求過載被截斷時返回的訊息
      "HttpStatusCode": 999, // 當請求過載被截斷時返回的http status
      "ClientIdHeader": "client_id" // 用來識別客戶端的請求頭,預設是 ClientId
    }

  這裡每個欄位都有註釋,不再解釋。下面我們來測試一下:

  Scenario 1:不帶header地訪問clientservice,1分鐘之內超過5次,便會被截斷,直接返回截斷後的訊息提示,HttpStatusCode:999

  

  可以通過檢視Repsonse的詳細資訊,驗證是否返回了999的狀態碼:

  

  Scenario 2:帶header(client_id:admin)訪問clientservice,1分鐘之內可以不受限制地訪問API

  

2.2 熔斷器(QoS)

  熔斷的意思是停止將請求轉發到下游服務。當下遊服務已經出現故障的時候再請求也是無功而返,並且還會增加下游伺服器和API閘道器的負擔。這個功能是用的Pollly來實現的,我們只需要為路由做一些簡單配置即可。如果你對Polly不熟悉,可以閱讀我之前的一篇文章《.NET Core微服務之基於Polly+AspectCore實現熔斷與降級機制

    "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 2, // 允許多少個異常請求
        "DurationOfBreak": 5000, // 熔斷的時間,單位為毫秒
        "TimeoutValue": 3000 // 如果下游請求的處理時間超過多少則視如該請求超時
      },

  *.這裡針對DurationOfBreak,官方文件中說明的單位是秒,但我在測試中發現應該是毫秒。不知道是我用的版本不對,還是怎麼的。anyway,這不是實驗的重點。OK,這裡我們的設定就是:如果Service Server的執行時間超過3秒,則會丟擲Timeout Exception。如果Service Server丟擲了第二次Timeout Exception,那麼停止服務訪問5s鍾。

  現在我們來改造一下Service,使其手動超時以使得Ocelot觸發熔斷保護機制。Ocelot中設定的TimeOutValue為3秒,那我們這兒簡單粗暴地讓其延時5秒(只針對前3次請求)。

    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        ......

        private static int _count = 0;
        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            _count++;
            Console.WriteLine($"Get...{_count}");
            if (_count <= 3)
            {
                System.Threading.Thread.Sleep(5000);
            }

            return new string[] { $"ClinetService: {DateTime.Now.ToString()} {Environment.MachineName} " +
                $"OS: {Environment.OSVersion.VersionString}" };
        }
        
        ......
    }

   下面我們就來測試一下:可以看到異常之後,便進入了5秒中的服務不可訪問期(直接返回了503 Service Unavaliable),而5s之後又可以正常訪問該介面了(這時不會再進入hard-code的延時程式碼)

  

  通過日誌,也可以確認Ocelot觸發了熔斷保護:

  

三、動態路由(Dynamic Routing)

  記得上一篇中一位園友評論說他有500個API服務,如果一一地配置到配置檔案,將會是一個巨大的工程,雖然都是copy,但是會增加出錯的機會,並且很難排查。這時,我們可以犧牲一些特殊性來求通用性,Ocelot給我們提供了Dynamic Routing功能。這個功能是在issue 340後增加的(見下圖官方文件),目的是在使用服務發現之後,直接通過服務發現去定位從而減少配置檔案中的ReRoutes配置項。

Example:http://api.edc.com/productservice/api/products => Ocelot會將productservice作為key呼叫Consul服務發現API去得到IP和Port,然後加上後續的請求URL部分(api/products)進行最終URL的訪問:http://ip:port/api/products。

  這裡仍然採用下圖所示的實驗節點結構:一個API閘道器節點,三個Consul Server節點以及一個Consul Client節點。

  由於不再需要配置ReRoutes,所以我們需要做一些“通用性”的改造,詳見下面的GlobalConfiguration:

{
  "ReRoutes": [],
  "Aggregates": [],
  "GlobalConfiguration": {
    "RequestIdKey": null,
    "ServiceDiscoveryProvider": {
      "Host": "192.168.80.100", // Consul Service IP
      "Port": 8500 // Consul Service Port
    },
    "RateLimitOptions": {
      "DisableRateLimitHeaders": false, // Http頭  X-Rate-Limit 和 Retry-After 是否禁用
      "QuotaExceededMessage": "Too many requests, are you OK?", // 當請求過載被截斷時返回的訊息
      "HttpStatusCode": 999, // 當請求過載被截斷時返回的http status
      "ClientIdHeader": "client_id" // 用來識別客戶端的請求頭,預設是 ClientId
    },
    "QoSOptions": {
      "ExceptionsAllowedBeforeBreaking": 3,
      "DurationOfBreak": 10000,
      "TimeoutValue": 5000
    },
    "BaseUrl": null,
    "LoadBalancerOptions": {
      "Type": "LeastConnection",
      "Key": null,
      "Expiry": 0
    },
    "DownstreamScheme": "http",
    "HttpHandlerOptions": {
      "AllowAutoRedirect": false,
      "UseCookieContainer": false,
      "UseTracing": false
    }
  }
}

  下面我們來做一個小測試,分別訪問clientservice和productservice,看看是否能成功地訪問到。

  (1)訪問clientservice

  

  (2)訪問productservice

  

  可以看出,只要我們正確地輸入請求URL,基於服務發現之後是可以正常訪問到的。只是這裡我們需要輸入正確的service name,這個service name是在consul中註冊的名字,如下高亮部分所示:

{
    "services":[
        {
            "id": "EDC_DNC_MSAD_CLIENT_SERVICE_01",
            "name" : "CAS.ClientService",
            "tags": [
                "urlprefix-/ClientService01"
            ],
            "address": "192.168.80.71",
            "port": 8810,
            "checks": [
                {
                    "name": "clientservice_check",
                    "http": "http://192.168.80.71:8810/api/health",
                    "interval": "10s",
                    "timeout": "5s"
                }
            ]
        }
     ]
}

四、整合Swagger統一API文件入口

  在前後端分離大行其道的今天,前端和後端的唯一聯絡,變成了API介面;API文件變成了前後端開發人員聯絡的紐帶,變得越來越重要,swagger就是一款讓你更好的書寫API文件的框架。

4.1 為每個Service整合Swagger

  Step1.NuGet安裝Swagger

NuGet>Install-Package Swashbuckle.AspNetCore  

  Step2.改寫StartUp類

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            .......

            services.AddMvc();

            // Swagger
            services.AddSwaggerGen(s =>
            {
                s.SwaggerDoc(Configuration["Service:DocName"], new Info
                {
                    Title = Configuration["Service:Title"],
                    Version = Configuration["Service:Version"],
                    Description = Configuration["Service:Description"],
                    Contact = new Contact
                    {
                        Name = Configuration["Service:Contact:Name"],
                        Email = Configuration["Service:Contact:Email"]
                    }
                });

                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
                var xmlPath = Path.Combine(basePath, Configuration["Service:XmlFile"]);
                s.IncludeXmlComments(xmlPath);
            });
            
            ......
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
            // swagger
            app.UseSwagger(c=>
            {
                c.RouteTemplate = "doc/{documentName}/swagger.json";
            });
            app.UseSwaggerUI(s =>
            {
                s.SwaggerEndpoint($"/doc/{Configuration["Service:DocName"]}/swagger.json", 
                    $"{Configuration["Service:Name"]} {Configuration["Service:Version"]}");
            });
        }
    }

  這裡配置檔案中關於這部分的內容如下:

{
  "Service": {
    "Name": "CAS.NB.ClientService",
    "Port": "8810",
    "DocName": "clientservice",
    "Version": "v1",
    "Title": "CAS Client Service API",
    "Description": "CAS Client Service API provide some API to help you get client information from CAS",
    "Contact": {
      "Name": "CAS 2.0 Team",
      "Email": "[email protected]"
    },
    "XmlFile": "Manulife.DNC.MSAD.NB.ClientService.xml"
  }
}

  需要注意的是,勾選輸出XML文件檔案,並將其copy到釋出後的目錄中(如果沒有自動複製的話):

  

4.2 為API閘道器整合Swagger

  Step1.NuGet安裝Swagger => 參考4.1

  Step2.改寫StartUp類

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Ocelot
            services.AddOcelot(Configuration);
            // Swagger
            services.AddMvc();
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc($"{Configuration["Swagger:DocName"]}", new Info
                {
                    Title = Configuration["Swagger:Title"],
                    Version = Configuration["Swagger:Version"]
                });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // get from service discovery later
            var apiList = new List<string>()
            {
                "clientservice",
                "productservice",
                "noticeservice"
            };
            app.UseMvc()
                .UseSwagger()
                .UseSwaggerUI(options =>
                {
                    apiList.ForEach(apiItem =>
                    {
                        options.SwaggerEndpoint($"/doc/{apiItem}/swagger.json", apiItem);
                     });
                });

            // Ocelot
            app.UseOcelot().Wait();
        }
    }

  *.這裡直接hard-code了一個apiNameList,實際中應該採用配置檔案或者呼叫服務發現獲取服務名稱(假設你的docName和serviceName保持一致,否則無法準確定位你的文件)

  Step3.更改configuration.json配置檔案 => 與hard-code的名稱保持一致,這裡為了方便直接讓上下游的URL格式保持一致,以方便地獲取API文件

{
  "ReRoutes": [
    // API01:CAS.ClientService
    // --> swagger part
    {
      "DownstreamPathTemplate": "/doc/clientservice/swagger.json",
      "DownstreamScheme": "http",
      "ServiceName": "CAS.ClientService",
      "LoadBalancer": "RoundRobin",
      "UseServiceDiscovery": true,
      "UpstreamPathTemplate": "/doc/clientservice/swagger.json",
      "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
    },
    // --> service part
    {
      "UseServiceDiscovery": true, // use Consul service discovery
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "ServiceName": "CAS.ClientService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "UpstreamPathTemplate": "/api/clientservice/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "RateLimitOptions": {
        "ClientWhitelist": [ "admin" ], // 白名單
        "EnableRateLimiting": true, // 是否啟用限流
        "Period": "1m", // 統計時間段:1s, 5m, 1h, 1d
        "PeriodTimespan": 15, // 多少秒之後客戶端可以重試
        "Limit": 10 // 在統計時間段內允許的最大請求數量
      },
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 2, // 允許多少個異常請求
        "DurationOfBreak": 5000, // 熔斷的時間,單位為秒
        "TimeoutValue": 3000 // 如果下游請求的處理時間超過多少則視如該請求超時
      },
      "ReRoutesCaseSensitive": false // non case sensitive
    },
    // API02:CAS.ProductService
    // --> swagger part
    {
      "DownstreamPathTemplate": "/doc/productservice/swagger.json",
      "DownstreamScheme": "http",
      "ServiceName": "CAS.ProductService",
      "LoadBalancer": "RoundRobin",
      "UseServiceDiscovery": true,
      "UpstreamPathTemplate": "/doc/productservice/swagger.json",
      "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
    },
    // --> service part
    {
      "UseServiceDiscovery": true, // use Consul service discovery
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "ServiceName": "CAS.ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": { // cache response data - ttl: 10s
        "TtlSeconds": 10,
        "Region": ""
      },
      "UpstreamPathTemplate": "/api/productservice/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "RateLimitOptions": {
        "ClientWhitelist": [ "admin" ],
        "EnableRateLimiting": true,
        "Period": "1m",
        "PeriodTimespan": 15,
        "Limit": 10
      },
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 2, // 允許多少個異常請求
        "DurationOfBreak": 5000, // 熔斷的時間,單位為秒
        "TimeoutValue": 3000 // 如果下游請求的處理時間超過多少則視如該請求超時
      },
      "ReRoutesCaseSensitive": false // non case sensitive
    }
  ],
  "GlobalConfiguration": {
    //"BaseUrl": "https://api.mybusiness.com"
    "ServiceDiscoveryProvider": {
      "Host": "192.168.80.100", // Consul Service IP
      "Port": 8500 // Consul Service Port
    },
    "RateLimitOptions": {
      "DisableRateLimitHeaders": false, // Http頭  X-Rate-Limit 和 Retry-After 是否禁用
      "QuotaExceededMessage": "Too many requests, are you OK?", // 當請求過載被截斷時返回的訊息
      "HttpStatusCode": 999, // 當請求過載被截斷時返回的http status
      "ClientIdHeader": "client_id" // 用來識別客戶端的請求頭,預設是 ClientId
    }
  }
}

  *.這裡需要注意其中新增加的swagger part配置,專門針對swagger.json做的對映.

4.3 測試

  從此,我們只需要通過API閘道器就可以瀏覽所有服務的API文件了,爽歪歪!

五、小結

  本篇基於Ocelot官方文件,學習了一下Ocelot的一些有用的功能:負載均衡(雖然只提供了兩種基本的演算法策略)、快取、限流、QoS以及動態路由(Dynamic Routing),並通過一些簡單的Demo進行了驗證。最後通過繼承Swagger做統一API文件入口,從此只需要通過一個URL即可檢視所有基於swagger的API文件。通過檢視Ocelot官方文件,可以知道Ocelot還支援許多其他有用的功能,而那些功能這裡暫不做介紹(或許有些會在後續其他部分(如驗證、授權、Trace等)中加入)。此外,一些朋友找我要demo的原始碼,我會在後續一齊上傳到github。而這幾篇中的內容,完全可以通過分享出來的code和配置自行構建,因此就不貼出來了=>已經貼出來,請點選下載。

示例程式碼

  Click here => 點我下載

參考資料

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。