.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] publicIEnumerable<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 => 點我下載
參考資料
作者:周旭龍
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。