天天看點

ASP.NET Core微服務之基于Ocelot實作API網關服務(2)一、負載均衡與請求緩存二、限流與熔斷器(QoS)三、動态路由(Dynamic Routing)四、內建Swagger統一API文檔入口五、小結示例代碼參考資料

Tip: 此篇已加入 .NET Core微服務基礎系列文章索引

一、負載均衡與請求緩存

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
    }
  }
}           

  詳細資訊請浏覽:

http://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#dynamic-routing

  下面我們來做一個小測試,分别通路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

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 => 

點我下載下傳

參考資料

jesse(騰飛),《

.NET Core開源網關 - Ocelot 中文文檔

catcher wong,《

Building API Gateway Using Ocelot In ASP.NET Core - QoS (Quality of Service)_》_

focus-lei,《

.NET Core在Ocelot網關中統一配置Swagger

Ocelot官方文檔:

http://ocelot.readthedocs.io/en/latest/index.html

繼續閱讀