.NET Core微服务之基于Ocelot实现API网关服务(续)
Tip:?此篇已加入.NET Core微服務(wù)基礎(chǔ)系列文章索引
一、負(fù)載均衡與請(qǐng)求緩存
1.1 負(fù)載均衡
為了驗(yàn)證負(fù)載均衡,這里我們配置了兩個(gè)Consul Client節(jié)點(diǎn),其中ClientService分別部署于這兩個(gè)節(jié)點(diǎn)內(nèi)(192.168.80.70與192.168.80.71)。
為了更好的展示API Repsonse來自哪個(gè)節(jié)點(diǎn),我們更改一下返回值:
[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的配置文件中確保有負(fù)載均衡的設(shè)置:
{"ReRoutes": [......"LoadBalancerOptions": {"Type": "RoundRobin"},...... }接下來發(fā)布并部署到這兩個(gè)節(jié)點(diǎn)上去,之后啟動(dòng)我們的API網(wǎng)關(guān),這里我用命令行啟動(dòng):
然后就可以測(cè)試負(fù)載均衡了,在瀏覽器中輸入U(xiǎn)RL并連續(xù)刷新:可以通過主機(jī)名看到的確是根據(jù)輪詢來進(jìn)行的負(fù)載均衡。
負(fù)載均衡LoadBalance可選值:
- RoundRobin - 輪詢,挨著來,雨露均沾
- LeastConnection - 最小連接數(shù),誰的任務(wù)最少誰來接客
- NoLoadBalance - 不要負(fù)載均衡,讓我一個(gè)人累死吧
1.2 請(qǐng)求緩存
Ocelot目前支持對(duì)下游服務(wù)的URL進(jìn)行緩存,并可以設(shè)置一個(gè)以秒為單位的TTL使緩存過期。我們也可以通過調(diào)用Ocelot的管理API來清除某個(gè)Region的緩存。
為了在路由中使用緩存,需要在ReRoute中加上如下設(shè)置:
"FileCacheOptions": { "TtlSeconds": 10, "Region": "somename" }這里表示緩存10秒,10秒后過期。另外,貌似只支持get方式,只要請(qǐng)求的URL不變,就會(huì)緩存。
這里我們?nèi)砸陨厦娴膁emo為例,在增加了FileCacheOptions配置之后,進(jìn)行一個(gè)小測(cè)試:因?yàn)槲覀冊(cè)O(shè)置的10s過期,所以在10s內(nèi)拿到的都是緩存,否則就會(huì)觸發(fā)負(fù)載均衡去不同節(jié)點(diǎn)拿數(shù)據(jù)。
二、限流與熔斷器(QoS)
2.1 限流 (RateLimit)
對(duì)請(qǐng)求進(jìn)行限流可以防止下游服務(wù)器因?yàn)樵L問過載而崩潰,我們只需要在路由下加一些簡(jiǎn)單的配置即可以完成。另外,看文檔發(fā)現(xiàn),這個(gè)功能是張善友大隊(duì)長(zhǎng)貢獻(xiàn)的,真是666。同時(shí)也看到一個(gè)園友catcherwong,已經(jīng)實(shí)踐許久了,真棒。
對(duì)于限流,我們可以對(duì)每個(gè)服務(wù)進(jìn)行如下配置:
"RateLimitOptions": {"ClientWhitelist": [ "admin" ], // 白名單"EnableRateLimiting": true, // 是否啟用限流"Period": "1m", // 統(tǒng)計(jì)時(shí)間段:1s, 5m, 1h, 1d"PeriodTimespan": 15, // 多少秒之后客戶端可以重試"Limit": 5 // 在統(tǒng)計(jì)時(shí)間段內(nèi)允許的最大請(qǐng)求數(shù)量}同時(shí),我們可以做一些全局配置:
"RateLimitOptions": {"DisableRateLimitHeaders": false, // Http頭 X-Rate-Limit 和 Retry-After 是否禁用"QuotaExceededMessage": "Too many requests, are you OK?", // 當(dāng)請(qǐng)求過載被截?cái)鄷r(shí)返回的消息"HttpStatusCode": 999, // 當(dāng)請(qǐng)求過載被截?cái)鄷r(shí)返回的http status"ClientIdHeader": "client_id" // 用來識(shí)別客戶端的請(qǐng)求頭,默認(rèn)是 ClientId}這里每個(gè)字段都有注釋,不再解釋。下面我們來測(cè)試一下:
Scenario 1:不帶header地訪問clientservice,1分鐘之內(nèi)超過5次,便會(huì)被截?cái)?#xff0c;直接返回截?cái)嗪蟮南⑻崾?#xff0c;HttpStatusCode:999
可以通過查看Repsonse的詳細(xì)信息,驗(yàn)證是否返回了999的狀態(tài)碼:
Scenario 2:帶header(client_id:admin)訪問clientservice,1分鐘之內(nèi)可以不受限制地訪問API
2.2 熔斷器(QoS)
熔斷的意思是停止將請(qǐng)求轉(zhuǎn)發(fā)到下游服務(wù)。當(dāng)下游服務(wù)已經(jīng)出現(xiàn)故障的時(shí)候再請(qǐng)求也是無功而返,并且還會(huì)增加下游服務(wù)器和API網(wǎng)關(guān)的負(fù)擔(dān)。這個(gè)功能是用的Pollly來實(shí)現(xiàn)的,我們只需要為路由做一些簡(jiǎn)單配置即可。如果你對(duì)Polly不熟悉,可以閱讀我之前的一篇文章《.NET Core微服務(wù)之基于Polly+AspectCore實(shí)現(xiàn)熔斷與降級(jí)機(jī)制》
"QoSOptions": {"ExceptionsAllowedBeforeBreaking": 2, // 允許多少個(gè)異常請(qǐng)求"DurationOfBreak": 5000, // 熔斷的時(shí)間,單位為毫秒"TimeoutValue": 3000 // 如果下游請(qǐng)求的處理時(shí)間超過多少則視如該請(qǐng)求超時(shí)},*.這里針對(duì)DurationOfBreak,官方文檔中說明的單位是秒,但我在測(cè)試中發(fā)現(xiàn)應(yīng)該是毫秒。不知道是我用的版本不對(duì),還是怎么的。anyway,這不是實(shí)驗(yàn)的重點(diǎn)。OK,這里我們的設(shè)置就是:如果Service Server的執(zhí)行時(shí)間超過3秒,則會(huì)拋出Timeout Exception。如果Service Server拋出了第二次Timeout Exception,那么停止服務(wù)訪問5s鐘。
現(xiàn)在我們來改造一下Service,使其手動(dòng)超時(shí)以使得Ocelot觸發(fā)熔斷保護(hù)機(jī)制。Ocelot中設(shè)置的TimeOutValue為3秒,那我們這兒簡(jiǎn)單粗暴地讓其延時(shí)5秒(只針對(duì)前3次請(qǐng)求)。
[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}" };}......}? 下面我們就來測(cè)試一下:可以看到異常之后,便進(jìn)入了5秒中的服務(wù)不可訪問期(直接返回了503 Service Unavaliable),而5s之后又可以正常訪問該接口了(這時(shí)不會(huì)再進(jìn)入hard-code的延時(shí)代碼)
通過日志,也可以確認(rèn)Ocelot觸發(fā)了熔斷保護(hù):
三、動(dòng)態(tài)路由(Dynamic Routing)
記得上一篇中一位園友評(píng)論說他有500個(gè)API服務(wù),如果一一地配置到配置文件,將會(huì)是一個(gè)巨大的工程,雖然都是copy,但是會(huì)增加出錯(cuò)的機(jī)會(huì),并且很難排查。這時(shí),我們可以犧牲一些特殊性來求通用性,Ocelot給我們提供了Dynamic Routing功能。這個(gè)功能是在issue 340后增加的(見下圖官方文檔),目的是在使用服務(wù)發(fā)現(xiàn)之后,直接通過服務(wù)發(fā)現(xiàn)去定位從而減少配置文件中的ReRoutes配置項(xiàng)。
Example:http://api.edc.com/productservice/api/products => Ocelot會(huì)將productservice作為key調(diào)用Consul服務(wù)發(fā)現(xiàn)API去得到IP和Port,然后加上后續(xù)的請(qǐng)求URL部分(api/products)進(jìn)行最終URL的訪問:http://ip:port/api/products。
這里仍然采用下圖所示的實(shí)驗(yàn)節(jié)點(diǎn)結(jié)構(gòu):一個(gè)API網(wǎng)關(guān)節(jié)點(diǎn),三個(gè)Consul Server節(jié)點(diǎn)以及一個(gè)Consul Client節(jié)點(diǎn)。
由于不再需要配置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?", // 當(dāng)請(qǐng)求過載被截?cái)鄷r(shí)返回的消息"HttpStatusCode": 999, // 當(dāng)請(qǐng)求過載被截?cái)鄷r(shí)返回的http status"ClientIdHeader": "client_id" // 用來識(shí)別客戶端的請(qǐng)求頭,默認(rèn)是 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}} }詳細(xì)信息請(qǐng)瀏覽:http://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#dynamic-routing
下面我們來做一個(gè)小測(cè)試,分別訪問clientservice和productservice,看看是否能成功地訪問到。
(1)訪問clientservice
(2)訪問productservice
可以看出,只要我們正確地輸入請(qǐng)求URL,基于服務(wù)發(fā)現(xiàn)之后是可以正常訪問到的。只是這里我們需要輸入正確的service name,這個(gè)service name是在consul中注冊(cè)的名字,如下高亮部分所示:
{"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統(tǒng)一API文檔入口
在前后端分離大行其道的今天,前端和后端的唯一聯(lián)系,變成了API接口;API文檔變成了前后端開發(fā)人員聯(lián)系的紐帶,變得越來越重要,swagger就是一款讓你更好的書寫API文檔的框架。
4.1 為每個(gè)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();// Swaggerservices.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();// swaggerapp.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"]}");});}}這里配置文件中關(guān)于這部分的內(nèi)容如下:
{"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": "EdisonZhou@manulife.com"},"XmlFile": "Manulife.DNC.MSAD.NB.ClientService.xml"} }需要注意的是,勾選輸出XML文檔文件,并將其copy到發(fā)布后的目錄中(如果沒有自動(dòng)復(fù)制的話):
4.2 為API網(wǎng)關(guān)集成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 latervar 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了一個(gè)apiNameList,實(shí)際中應(yīng)該采用配置文件或者調(diào)用服務(wù)發(fā)現(xiàn)獲取服務(wù)名稱(假設(shè)你的docName和serviceName保持一致,否則無法準(zhǔn)確定位你的文檔)
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", // 統(tǒng)計(jì)時(shí)間段:1s, 5m, 1h, 1d"PeriodTimespan": 15, // 多少秒之后客戶端可以重試"Limit": 10 // 在統(tǒng)計(jì)時(shí)間段內(nèi)允許的最大請(qǐng)求數(shù)量 },"QoSOptions": {"ExceptionsAllowedBeforeBreaking": 2, // 允許多少個(gè)異常請(qǐng)求"DurationOfBreak": 5000, // 熔斷的時(shí)間,單位為秒"TimeoutValue": 3000 // 如果下游請(qǐng)求的處理時(shí)間超過多少則視如該請(qǐng)求超時(shí) },"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, // 允許多少個(gè)異常請(qǐng)求"DurationOfBreak": 5000, // 熔斷的時(shí)間,單位為秒"TimeoutValue": 3000 // 如果下游請(qǐng)求的處理時(shí)間超過多少則視如該請(qǐng)求超時(shí) },"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?", // 當(dāng)請(qǐng)求過載被截?cái)鄷r(shí)返回的消息"HttpStatusCode": 999, // 當(dāng)請(qǐng)求過載被截?cái)鄷r(shí)返回的http status"ClientIdHeader": "client_id" // 用來識(shí)別客戶端的請(qǐng)求頭,默認(rèn)是 ClientId }} }*.這里需要注意其中新增加的swagger part配置,專門針對(duì)swagger.json做的映射.
4.3 測(cè)試
從此,我們只需要通過API網(wǎng)關(guān)就可以瀏覽所有服務(wù)的API文檔了,爽歪歪!
五、小結(jié)
本篇基于Ocelot官方文檔,學(xué)習(xí)了一下Ocelot的一些有用的功能:負(fù)載均衡(雖然只提供了兩種基本的算法策略)、緩存、限流、QoS以及動(dòng)態(tài)路由(Dynamic Routing),并通過一些簡(jiǎn)單的Demo進(jìn)行了驗(yàn)證。最后通過繼承Swagger做統(tǒng)一API文檔入口,從此只需要通過一個(gè)URL即可查看所有基于swagger的API文檔。通過查看Ocelot官方文檔,可以知道Ocelot還支持許多其他有用的功能,而那些功能這里暫不做介紹(或許有些會(huì)在后續(xù)其他部分(如驗(yàn)證、授權(quán)、Trace等)中加入)。此外,一些朋友找我要demo的源碼,我會(huì)在后續(xù)一齊上傳到github。而這幾篇中的內(nèi)容,完全可以通過分享出來的code和配置自行構(gòu)建,因此就不貼出來了=>已經(jīng)貼出來,請(qǐng)點(diǎn)擊下載。
示例代碼
Click here =>?點(diǎn)我下載
參考資料
jesse(騰飛),《.NET Core開源網(wǎng)關(guān) - Ocelot 中文文檔》
catcher wong,《Building API Gateway Using Ocelot In ASP.NET Core - QoS (Quality of Service)》
focus-lei,《.NET Core在Ocelot網(wǎng)關(guān)中統(tǒng)一配置Swagger》
Ocelot官方文檔:http://ocelot.readthedocs.io/en/latest/index.html
?
作者:周旭龍
出處:http://edisonchou.cnblogs.com
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
總結(jié)
以上是生活随笔為你收集整理的.NET Core微服务之基于Ocelot实现API网关服务(续)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jquery 设置style:displ
- 下一篇: 盘点六大在中国复制失败的O2O案例