一、背景
目前我们项目是采用的 Ocelot 作为 API 网关,并且在其基础上结合 IdentityServer4 开发了一套 API 开放平台。由于部分项目是基于 ABP 框架进行开发的,接口的平均 QPS 基本是在 2K~3K /S 左右 (E3 1231 16G)。采用 Ocelot 进行请求转发之后,前端反馈接口调用速度变慢了,也没有太过在意,以为是项目接口的问题,一直在接口上面尝试进行优化。
极限优化接口后仍然没有显著改善,故针对 Ocelot 的性能进行压力测试,得到的结果也是让我比较惊讶。
二、准备工作
2.1 测试项目准备
首先新建了一个解决方案,其名字为 OcelotStudy
,其下面有三个项目,分别是两个 API 项目和一个网关项目。
网关项目编写:
为 OcelotStudy
项目引入 Ocelot 的 NuGet 包。
在 OcelotStudy
项目的 Program.cs
文件当中显式指定我们网关的监听端口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace OcelotStudy
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// 指定监听端口为 5000
webBuilder.UseStartup<Startup>()
.UseKestrel(x=>x.ListenAnyIP(5000));
});
}
}
|
在 OcelotStudy
项目的 Program.cs
文件当中显式指定我们网关的监听端口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace OcelotStudy
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// 指定监听端口为 5000
webBuilder.UseStartup<Startup>()
.UseKestrel(x=>x.ListenAnyIP(5000));
});
}
}
|
在 Startup.cs
类当中注入 Ocelot 的服务,并应用 Ocelot 的中间件。
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
|
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
namespace OcelotStudy
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 禁用日志的控制台输出,防止由于线程同步造成的性能损失
services.AddLogging(op => op.ClearProviders());
services.AddMvc();
services.AddOcelot(new ConfigurationBuilder().AddJsonFile("Ocelot.json").Build());
}
public async void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
await app.UseOcelot();
app.UseMvc();
}
}
}
|
在 OcelotStudy
项目下建立 Ocelot.json
文件,内容如下。
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
|
{
"ReRoutes": [
{
"DownstreamPathTemplate": "/api/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "API 1 服务器IP",
"Port": 6000
},
{
"Host": "API 2 服务器IP",
"Port": 7000
}
],
"UpstreamPathTemplate": "/{everything}",
"UpstreamHttpMethod": [ "Get", "Post" ],
"LoadBalancerOptions": {
"Type": "RoundRobin"
}
}
],
"GlobalConfiguration": {
}
}
|
测试项目的编写:
两个测试项目的监听端口分别为 6000
与 7000
,都建立一个 ValuesController
控制器,返回一个字符串用于输出当前请求的 API 服务器信息。
ApiService01
的文件信息:
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
|
using Microsoft.AspNetCore.Mvc;
namespace ApiService01.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<string> Get()
{
return "当前请求的 API 接口是 1 号服务器。";
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}
|
ApiService02
的文件信息:
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
|
using Microsoft.AspNetCore.Mvc;
namespace ApiService02.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<string> Get()
{
return "当前请求的 API 接口是 2 号服务器。";
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}
|
他们两个的 Startup.cs
与 Program.cs
文件内容基本一致,区别只是监听的端口分别是 6000
和 7000
而已。
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
|
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace ApiService02
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseKestrel(x => x.ListenAnyIP(6000)); // 或者 7000
});
}
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace ApiService02
{
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)
{
// 禁用日志的控制台输出,防止由于线程同步造成的性能损失
services.AddLogging(op => op.ClearProviders());
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseRouting(routes => { routes.MapApplication(); });
}
}
}
|
以上三个项目都采用 Release
版本进行发布。
1
|
dotnet publish -c Release
|
ApiService01 部署在单独的 E3 1231 v3 16G DDR3 服务器。
ApiService02 部署在单独的 i3-7100 16G DDR4 服务器。
OcelotStudy 部署在单独的 E3 1231 v3 16G DDR3 服务器。
三、开始测试
这里我使用的是 WRK 来进行压力测试,OcelotStudy 网关项目的 IP 地址为 172.31.61.41:5000
,故使用以下命令进行测试。
1
|
./wrk -t 10 -c 10000 -d 20s --latency --timeout 3s "http://172.31.61.41:5000/values"
|
测试结果:
我将 ApiService01 项目放在网关的服务器,直接调用 ApiService01 的接口,其压力测试情况。
四、结语
最后 Ocelot 的 QPS 结果为:3461.53
直接请求 API 接口的 QPS 结果为:38874.50
这样的结果让我感到很意外,不知道是由于 Ocelot 实现机制的原因,还是我的使用方法不对。这样的性能测试结果数据对于 API 网关来说确实不太好看,但也希望今后 Ocelot 能够继续努力。
如果大家对于我的测试方式有疑问的话,可以在评论区指出,我将按照你所提供的方法再次进行测试。(PS: 我也不想换啊,多希望是我测错了)
五、原生 API、Ocelot、Kong API 性能比较
针对于评论区各位朋友所提出的建议,以及我最近针对 Ocelot 的再次测试,整理出来一份测试结果的表格。以下结果均是单节点进行部署,如果使用 LB + API 集群的话是可以有效提升吞吐量。
使用的网关 |
测试时间 |
并发数 |
总请求 |
QPS |
超时数量 |
平均响应 |
没有使用网关 |
1 分钟 |
15000 |
2495541 |
41518.07 |
1592 |
115.98 ms |
Kong API |
1 分钟 |
15000 |
690141 |
11478.06 |
43997 |
860.31 ms |
Ocelot |
1 分钟 |
15000 |
277627 |
4618.98 |
1795 |
1.7 s |
没有使用网关 |
1 分钟 |
5000 |
2530107 |
42111.44 |
0 |
115.48 ms |
Kong API |
1 分钟 |
5000 |
866449 |
14418.25 |
3090 |
383.75 ms |
Ocelot |
1 分钟 |
5000 |
307226 |
5113.09 |
78 |
932.19 ms |
没有使用网关 |
10 分钟 |
8000 |
13080964 |
21797.98 |
103 |
364.97 ms |
Kong API |
10 分钟 |
8000 |
4809613 |
8014.7 |
305503 |
636.41 ms |
Ocelot |
10 分钟 |
8000 |
2558431 |
4263.34 |
2137 |
1.84 s |
Ocelot 与 Kong 均部署在一台 32G 12C 3.0Ghz 的 CentOS 服务器上,API 1 与 API 2 部署在 8C 16G 3.0 Ghz 的服务器上,所有环境都是基于 Docker CE 进行部署。
另外针对于 Kong API 不知道是我使用方式不对还是什么情况,其网络吞吐量经常波动,如下图,还请不吝赐教。
网关服务器:
API 1 与 API 2 服务器: