ASP.NET Core微服务之基于Ocelot实现API网关服务(2)

释放双眼,带上耳机,听听看~!

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

一、负载均衡与请求缓存

1.1 负载均衡

为了验证负载均衡,这里我们配置了两个Consul Client节点,其中ClientService分别部署于这两个节点内(192.168.80.70与192.168.80.71)。

为了更好的展示API Repsonse来自哪个节点,我们更改一下返回值:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1    [Route("api/[controller]")]
2    public class ValuesController : Controller
3    {
4        // GET api/values
5        [HttpGet]
6        public IEnumerable<string> Get()
7        {
8            return new string[] { $"ClinetService: {DateTime.Now.ToString()} {Environment.MachineName} " +
9                $"OS: {Environment.OSVersion.VersionString}" };
10        }
11
12        ......
13    }
14

Ocelot的配置文件中确保有负载均衡的设置:


1
2
3
4
5
6
7
8
9
1{
2  "ReRoutes": [
3      ......
4      "LoadBalancerOptions": {
5        "Type": "RoundRobin"
6      },
7      ......  
8}
9

接下来发布并部署到这两个节点上去,之后启动我们的API网关,这里我用命令行启动:

然后就可以测试负载均衡了,在浏览器中输入URL并连续刷新:可以通过主机名看到的确是根据轮询来进行的负载均衡。

负载均衡LoadBalance可选值:

  • RoundRobin – 轮询,挨着来,雨露均沾
  • LeastConnection – 最小连接数,谁的任务最少谁来接客
  • NoLoadBalance – 不要负载均衡,让我一个人累死吧  

1.2 请求缓存

Ocelot目前支持对下游服务的URL进行缓存,并可以设置一个以秒为单位的TTL使缓存过期。我们也可以通过调用Ocelot的管理API来清除某个Region的缓存。

为了在路由中使用缓存,需要在ReRoute中加上如下设置:


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

这里表示缓存10秒,10秒后过期。另外,貌似只支持get方式,只要请求的URL不变,就会缓存。

这里我们仍以上面的demo为例,在增加了FileCacheOptions配置之后,进行一个小测试:因为我们设置的10s过期,所以在10s内拿到的都是缓存,否则就会触发负载均衡去不同节点拿数据。

二、限流与熔断器(QoS)

2.1 限流 (RateLimit)

对请求进行限流可以防止下游服务器因为访问过载而崩溃,我们只需要在路由下加一些简单的配置即可以完成。另外,看文档发现,这个功能是张善友大队长贡献的,真是666。同时也看到一个园友catcherwong,已经实践许久了,真棒。

对于限流,我们可以对每个服务进行如下配置:


1
2
3
4
5
6
7
8
1    "RateLimitOptions": {
2        "ClientWhitelist": [ "admin" ], // 白名单
3        "EnableRateLimiting": true, // 是否启用限流
4        "Period": "1m", // 统计时间段:1s, 5m, 1h, 1d
5        "PeriodTimespan": 15, // 多少秒之后客户端可以重试
6        "Limit": 5 // 在统计时间段内允许的最大请求数量
7      }
8

同时,我们可以做一些全局配置:


1
2
3
4
5
6
7
1    "RateLimitOptions": {
2      "DisableRateLimitHeaders": false, // Http头  X-Rate-Limit 和 Retry-After 是否禁用
3      "QuotaExceededMessage": "Too many requests, are you OK?", // 当请求过载被截断时返回的消息
4      "HttpStatusCode": 999, // 当请求过载被截断时返回的http status
5      "ClientIdHeader": "client_id" // 用来识别客户端的请求头,默认是 ClientId
6    }
7

这里每个字段都有注释,不再解释。下面我们来测试一下:

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实现熔断与降级机制》


1
2
3
4
5
6
1    "QoSOptions": {
2        "ExceptionsAllowedBeforeBreaking": 2, // 允许多少个异常请求
3        "DurationOfBreak": 5000, // 熔断的时间,单位为毫秒
4        "TimeoutValue": 3000 // 如果下游请求的处理时间超过多少则视如该请求超时
5      },
6

*.这里针对DurationOfBreak,官方文档中说明的单位是秒,但我在测试中发现应该是毫秒。不知道是我用的版本不对,还是怎么的。anyway,这不是实验的重点。OK,这里我们的设置就是:如果Service Server的执行时间超过3秒,则会抛出Timeout Exception。如果Service Server抛出了第二次Timeout Exception,那么停止服务访问5s钟。

现在我们来改造一下Service,使其手动超时以使得Ocelot触发熔断保护机制。Ocelot中设置的TimeOutValue为3秒,那我们这儿简单粗暴地让其延时5秒(只针对前3次请求)。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1    [Route("api/[controller]")]
2    public class ValuesController : Controller
3    {
4        ......
5
6        private static int _count = 0;
7        // GET api/values
8        [HttpGet]
9        public IEnumerable<string> Get()
10        {
11            _count++;
12            Console.WriteLine($"Get...{_count}");
13            if (_count <= 3)
14            {
15                System.Threading.Thread.Sleep(5000);
16            }
17
18            return new string[] { $"ClinetService: {DateTime.Now.ToString()} {Environment.MachineName} " +
19                $"OS: {Environment.OSVersion.VersionString}" };
20        }
21        
22        ......
23    }
24

   下面我们就来测试一下:可以看到异常之后,便进入了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:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1{
2  "ReRoutes": [],
3  "Aggregates": [],
4  "GlobalConfiguration": {
5    "RequestIdKey": null,
6    "ServiceDiscoveryProvider": {
7      "Host": "192.168.80.100", // Consul Service IP
8      "Port": 8500 // Consul Service Port
9    },
10    "RateLimitOptions": {
11      "DisableRateLimitHeaders": false, // Http头  X-Rate-Limit 和 Retry-After 是否禁用
12      "QuotaExceededMessage": "Too many requests, are you OK?", // 当请求过载被截断时返回的消息
13      "HttpStatusCode": 999, // 当请求过载被截断时返回的http status
14      "ClientIdHeader": "client_id" // 用来识别客户端的请求头,默认是 ClientId
15    },
16    "QoSOptions": {
17      "ExceptionsAllowedBeforeBreaking": 3,
18      "DurationOfBreak": 10000,
19      "TimeoutValue": 5000
20    },
21    "BaseUrl": null,
22    "LoadBalancerOptions": {
23      "Type": "LeastConnection",
24      "Key": null,
25      "Expiry": 0
26    },
27    "DownstreamScheme": "http",
28    "HttpHandlerOptions": {
29      "AllowAutoRedirect": false,
30      "UseCookieContainer": false,
31      "UseTracing": false
32    }
33  }
34}
35

详细信息请浏览:http://ocelot.readthedocs.io/en/latest/features/servicediscovery.html\#dynamic-routing

下面我们来做一个小测试,分别访问clientservice和productservice,看看是否能成功地访问到。

(1)访问clientservice

(2)访问productservice

可以看出,只要我们正确地输入请求URL,基于服务发现之后是可以正常访问到的。只是这里我们需要输入正确的service name,这个service name是在consul中注册的名字,如下高亮部分所示:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1{
2    "services":[
3        {
4            "id": "EDC_DNC_MSAD_CLIENT_SERVICE_01",
5            "name" : "CAS.ClientService",
6            "tags": [
7                "urlprefix-/ClientService01"
8            ],
9            "address": "192.168.80.71",
10            "port": 8810,
11            "checks": [
12                {
13                    "name": "clientservice_check",
14                    "http": "http://192.168.80.71:8810/api/health",
15                    "interval": "10s",
16                    "timeout": "5s"
17                }
18            ]
19        }
20     ]
21}
22

四、集成Swagger统一API文档入口

在前后端分离大行其道的今天,前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架。

4.1 为每个Service集成Swagger

Step1.NuGet安装Swagger

NuGet>Install-Package Swashbuckle.AspNetCore  

Step2.改写StartUp类


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
1    public class Startup
2    {
3        public Startup(IConfiguration configuration)
4        {
5            Configuration = configuration;
6        }
7
8        public IConfiguration Configuration { get; }
9
10        // This method gets called by the runtime. Use this method to add services to the container.
11        public IServiceProvider ConfigureServices(IServiceCollection services)
12        {
13            .......
14
15            services.AddMvc();
16
17            // Swagger
18            services.AddSwaggerGen(s =>
19            {
20                s.SwaggerDoc(Configuration["Service:DocName"], new Info
21                {
22                    Title = Configuration["Service:Title"],
23                    Version = Configuration["Service:Version"],
24                    Description = Configuration["Service:Description"],
25                    Contact = new Contact
26                    {
27                        Name = Configuration["Service:Contact:Name"],
28                        Email = Configuration["Service:Contact:Email"]
29                    }
30                });
31
32                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
33                var xmlPath = Path.Combine(basePath, Configuration["Service:XmlFile"]);
34                s.IncludeXmlComments(xmlPath);
35            });
36            
37            ......
38        }
39
40        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
41        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime)
42        {
43            if (env.IsDevelopment())
44            {
45                app.UseDeveloperExceptionPage();
46            }
47
48            app.UseMvc();
49            // swagger
50            app.UseSwagger(c=>
51            {
52                c.RouteTemplate = "doc/{documentName}/swagger.json";
53            });
54            app.UseSwaggerUI(s =>
55            {
56                s.SwaggerEndpoint($"/doc/{Configuration["Service:DocName"]}/swagger.json",
57                    $"{Configuration["Service:Name"]} {Configuration["Service:Version"]}");
58            });
59        }
60    }
61

这里配置文件中关于这部分的内容如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1{
2  "Service": {
3    "Name": "CAS.NB.ClientService",
4    "Port": "8810",
5    "DocName": "clientservice",
6    "Version": "v1",
7    "Title": "CAS Client Service API",
8    "Description": "CAS Client Service API provide some API to help you get client information from CAS",
9    "Contact": {
10      "Name": "CAS 2.0 Team",
11      "Email": "EdisonZhou@manulife.com"
12    },
13    "XmlFile": "Manulife.DNC.MSAD.NB.ClientService.xml"
14  }
15}
16

需要注意的是,勾选输出XML文档文件,并将其copy到发布后的目录中(如果没有自动复制的话):

4.2 为API网关集成Swagger

Step1.NuGet安装Swagger => 参考4.1

Step2.改写StartUp类


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
1    public class Startup
2    {
3        public Startup(IConfiguration configuration)
4        {
5            Configuration = configuration;
6        }
7
8        public IConfiguration Configuration { get; }
9
10        // This method gets called by the runtime. Use this method to add services to the container.
11        public void ConfigureServices(IServiceCollection services)
12        {
13            // Ocelot
14            services.AddOcelot(Configuration);
15            // Swagger
16            services.AddMvc();
17            services.AddSwaggerGen(options =>
18            {
19                options.SwaggerDoc($"{Configuration["Swagger:DocName"]}", new Info
20                {
21                    Title = Configuration["Swagger:Title"],
22                    Version = Configuration["Swagger:Version"]
23                });
24            });
25        }
26
27        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
28        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
29        {
30            if (env.IsDevelopment())
31            {
32                app.UseDeveloperExceptionPage();
33            }
34
35            // get from service discovery later
36            var apiList = new List<string>()
37            {
38                "clientservice",
39                "productservice",
40                "noticeservice"
41            };
42            app.UseMvc()
43                .UseSwagger()
44                .UseSwaggerUI(options =>
45                {
46                    apiList.ForEach(apiItem =>
47                    {
48                        options.SwaggerEndpoint($"/doc/{apiItem}/swagger.json", apiItem);
49                     });
50                });
51
52            // Ocelot
53            app.UseOcelot().Wait();
54        }
55    }
56

*.这里直接hard-code了一个apiNameList,实际中应该采用配置文件或者调用服务发现获取服务名称(假设你的docName和serviceName保持一致,否则无法准确定位你的文档)

Step3.更改configuration.json配置文件 => 与hard-code的名称保持一致,这里为了方便直接让上下游的URL格式保持一致,以方便地获取API文档


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
1{
2  "ReRoutes": [
3    // API01:CAS.ClientService
4    // --> swagger part
5    {
6      "DownstreamPathTemplate": "/doc/clientservice/swagger.json",
7      "DownstreamScheme": "http",
8      "ServiceName": "CAS.ClientService",
9      "LoadBalancer": "RoundRobin",
10      "UseServiceDiscovery": true,
11      "UpstreamPathTemplate": "/doc/clientservice/swagger.json",
12      "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
13    },
14    // --> service part
15    {
16      "UseServiceDiscovery": true, // use Consul service discovery
17      "DownstreamPathTemplate": "/api/{url}",
18      "DownstreamScheme": "http",
19      "ServiceName": "CAS.ClientService",
20      "LoadBalancerOptions": {
21        "Type": "RoundRobin"
22      },
23      "UpstreamPathTemplate": "/api/clientservice/{url}",
24      "UpstreamHttpMethod": [ "Get", "Post" ],
25      "RateLimitOptions": {
26        "ClientWhitelist": [ "admin" ], // 白名单
27        "EnableRateLimiting": true, // 是否启用限流
28        "Period": "1m", // 统计时间段:1s, 5m, 1h, 1d
29        "PeriodTimespan": 15, // 多少秒之后客户端可以重试
30        "Limit": 10 // 在统计时间段内允许的最大请求数量
31      },
32      "QoSOptions": {
33        "ExceptionsAllowedBeforeBreaking": 2, // 允许多少个异常请求
34        "DurationOfBreak": 5000, // 熔断的时间,单位为秒
35        "TimeoutValue": 3000 // 如果下游请求的处理时间超过多少则视如该请求超时
36      },
37      "ReRoutesCaseSensitive": false // non case sensitive
38    },
39    // API02:CAS.ProductService
40    // --> swagger part
41    {
42      "DownstreamPathTemplate": "/doc/productservice/swagger.json",
43      "DownstreamScheme": "http",
44      "ServiceName": "CAS.ProductService",
45      "LoadBalancer": "RoundRobin",
46      "UseServiceDiscovery": true,
47      "UpstreamPathTemplate": "/doc/productservice/swagger.json",
48      "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
49    },
50    // --> service part
51    {
52      "UseServiceDiscovery": true, // use Consul service discovery
53      "DownstreamPathTemplate": "/api/{url}",
54      "DownstreamScheme": "http",
55      "ServiceName": "CAS.ProductService",
56      "LoadBalancerOptions": {
57        "Type": "RoundRobin"
58      },
59      "FileCacheOptions": { // cache response data - ttl: 10s
60        "TtlSeconds": 10,
61        "Region": ""
62      },
63      "UpstreamPathTemplate": "/api/productservice/{url}",
64      "UpstreamHttpMethod": [ "Get", "Post" ],
65      "RateLimitOptions": {
66        "ClientWhitelist": [ "admin" ],
67        "EnableRateLimiting": true,
68        "Period": "1m",
69        "PeriodTimespan": 15,
70        "Limit": 10
71      },
72      "QoSOptions": {
73        "ExceptionsAllowedBeforeBreaking": 2, // 允许多少个异常请求
74        "DurationOfBreak": 5000, // 熔断的时间,单位为秒
75        "TimeoutValue": 3000 // 如果下游请求的处理时间超过多少则视如该请求超时
76      },
77      "ReRoutesCaseSensitive": false // non case sensitive
78    }
79  ],
80  "GlobalConfiguration": {
81    //"BaseUrl": "https://api.mybusiness.com"
82    "ServiceDiscoveryProvider": {
83      "Host": "192.168.80.100", // Consul Service IP
84      "Port": 8500 // Consul Service Port
85    },
86    "RateLimitOptions": {
87      "DisableRateLimitHeaders": false, // Http头  X-Rate-Limit 和 Retry-After 是否禁用
88      "QuotaExceededMessage": "Too many requests, are you OK?", // 当请求过载被截断时返回的消息
89      "HttpStatusCode": 999, // 当请求过载被截断时返回的http status
90      "ClientIdHeader": "client_id" // 用来识别客户端的请求头,默认是 ClientId
91    }
92  }
93}
94

*.这里需要注意其中新增加的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

给TA打赏
共{{data.count}}人
人已打赏
安全网络

CDN安全市场到2022年价值76.3亿美元

2018-2-1 18:02:50

安全漏洞

Zerodium 公开 Tor Browser 0day 漏洞,不过影响不大

2018-9-12 11:12:22

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索