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網關中統一配置SwaggerOcelot官方文檔:
http://ocelot.readthedocs.io/en/latest/index.html