ValuesControllers.cs 注意Post的参数从[FromBody]变成了[FromForm],以便接收上传的图片流数据
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CognitiveMiddlewareService.CognitiveServices;
using CognitiveMiddlewareService.Processors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace CognitiveMiddlewareService.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly IProcessService processor;
public ValuesController(IProcessService ps)
{
this.processor = ps;
}
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public async Task<string> Post([FromForm] IFormCollection formCollection)
{
try
{
IFormCollection form = await this.Request.ReadFormAsync();
IFormFile file = form.Files.First();
var bufferData = Helper.GetBuffer(file);
var result = await this.processor.Process(bufferData);
string jsonResult = JsonConvert.SerializeObject(result);
// return json formatted data
return jsonResult;
}
catch (Exception ex)
{
Debug.Write(ex.Message);
return null;
}
}
}
}
启动.cs
using CognitiveMiddlewareService.CognitiveServices;
using CognitiveMiddlewareService.MiddlewareService;
using CognitiveMiddlewareService.Processors;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CognitiveMiddleService
{
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.AddMvc();
services.AddScoped<IProcessService, ProcessService>();
services.AddScoped<IVisionService, VisionService>();
services.AddScoped<ILandmarkService, LandmarkService>();
services.AddScoped<ICelebrityService, CelebrityService>();
services.AddScoped<IEntitySearchService, EntitySearchService>();
services.AddHttpClient();
}
// 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();
}
app.UseMvc();
}
}
}
除了第一行的services.AddMvc()以外,后面所有的行都是我们需要增加的依赖注入代码。
总结一下,从调用关系上看,是这个次序:
控制器 -> ProcessService -> LandmarkService/CelebrityService -> VisionService/EntitySearchService
其中:
Controller是个Endpoint
ProcessService负责任务调度
LandmarkService/CelebrityService是个集成服务,封装了串行调用底层服务的逻辑
VisionService/EntitySearchService是基础服务,相当于最底层的原子操作
从数据结构上看,进化的顺序是这样的:
VisionResult/EntityResult -> CelebrityResult/LandmarkResult -> 聚合结果
其中:
VisionResult/EntityResult是最底层返回的原始结果,主要用于反序列化
CelebrityResult/LandmarkResult是集成了多个原始结果后的抽象结果,好处是隔离了原始结果中的一些噪音,解耦,只返回我们需要的字段
AggregatedResult是聚合在一起的结果,主要用于排序和生成返回JSON数据
有的人会问了:有必要搞这么复杂吗?这几个调用在一个帮助函数里不就可以搞定了吗?
确实是这样,如果不考虑应用扩展什么的,那就用一个帮助函数搞定;如果想玩儿点大的,那么下面这张图就是一个完整系统的Stack图,这个系统通过组合调用多种微软认知服务/微软地图服务/微软实体服务等,能够提供给用户的智能设备丰富的视觉对象识别体验。
上图包含了以下层次:
端点
处理和分类器
任务分派器
API 代理和识别器
蜜蜂属
处理器
自适应卡片生成器
助理 元件
做好了一个中间层服务,不是说简单地向Azure上一部署就算完事儿了。任何一个商用的软件,都需要严格的测试,对于普通的手机/客户端软件的测试,相信很多人都知道,覆盖功能点,各种条件输入,等等等等。对于中间层服务,除了功能点外,性能方面的测试尤其重要。
如何进行测试呢?
ASP.NET Core Web API有一套测试工具,请看这个链接:https://docs.microsoft.com/en-us/aspnet/core/test/?view=aspnetcore-2.1,它讲述了一些列的方法,我们不再赘述,本文所要描述的是三种面向场景的测试方法:负载(较重的压力)测试,(较轻的压力)性能测试,(中等的压力)稳定性测试。不是以show?code为主,而是以讲理念为主,懂得了理念,code容易写啦。
对于一个普通的App,我们用界面交互的方式进行测试。对于一个service,它的界面就相当于REST API,我们可以从客户端发起测试,自动化程度较高。
在Visual Studio 2017,有专门的Load Test工具可以帮助我们完成在客户端编写测试代码,调整各种测试参数,然后发起测试,具体的链接在这里。
在本文中,我们主要从概念上讲解一下针对含有认知服务的中间服务层的测试方法,因为认知服务本身如果访问量大的话,是要收取费用的!
小提示:各个认知服务的费用标准不同,请仔细阅读相关网页,以免在进行大量的测试时引起不必要的费用发生。
模拟多个并发用户访问中间层服务,集中发生在一个持续的时间段内,以衡量服务质量。负载测试不断的发展下去,负载越来越大,就会变成极限测试,最终把机器跑瘫为止。
注意!我们不是在测试认知服务的性能,是要测试自己的中间层服务的性能,所以如下图所示:
要把认知服务用一个模拟的mock up service来代替,这个mock up service可以自己简单地用 ASP.NET 搭建一个,接收请求后,不做任何逻辑处理,直接返回JSON字符串,但是中间需要模拟认知服务的处理时间,故意延迟2~3秒。
另外一个原因是,认知服务比较复杂,可能不能满足很高的QPS的要求,而用自己的模拟服务可以到达极高的QPS,这样就不会正在测试中产生瓶颈。
网络环境为局域网内部,亦即客户端、中间层、模拟服务都在局域网内部即可,这样可以避免网络延迟带来的干扰。
在本例中,我们测试了8轮,每轮都模拟不同的并发用户数持续运行一小时,最终结果如下:
并发用户 | 怠 | 1 位用户 | 3 位用户 | 5 位用户 | 10 位用户 | 25 位用户 | 50 位用户 | 75 位用户 | 100 位用户 |
---|---|---|---|---|---|---|---|---|---|
中央处理器 | 0% | <1% | <1% | 1% | 2.5% | 6% | 12% | 17% | 21% |
内存(MB) | 110 | 116 | 150 | 158 | 164 | 176 | 260 | 301 | 335 |
延迟 | 0 | 2.61 | 2.61 | 2.61 | 2.62 | 2.63 | 2.64 | 2.67 | 2.7 |
总要求 | 0 | 1,377 | 4,124 | 6,885 | 13,666 | 34,221 | 67,976 | 100,948 | 132,894 |
请求失败。 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
PP2型 | 0.00 | 0.38 | 1.15 | 1.91 | 3.80 | 9.51 | 18.88 | 28.04 | 36.92 |
从图表可以看出,CPU/Memory/QPS都是线性增长的,意味着是可以预测的。延迟(Latency)是平缓的,不会因为并发用户变多而变慢,很健康。
在一个足够长的时间内持续测试服务,以检查其可靠性。"足够长"一般定义为12小时、48小时、72小时等等。可以认为,被测对象只要跑够了预定的时长,就算是稳定性过关了。
同理,我们要测试的是中间层服务,而不是认知服务。
测试环境与上面相同,也是使用模拟的认知服务,因为72小时的测试时间,会发送大量的请求,很可能超出了当月限额而收取费用。
网络环境仍然使用局域网。
模拟10个并发用户,持续向中间层服务发请求12小时,测试结果如下表:
采样点 | 中央处理器 | 记忆 | 延迟 | 请求总数 | 失败 | PP2型 |
---|---|---|---|---|---|---|
1:00:00 | 2.5% | 140 米 | 2.63 秒 | 13,730 | 0 | 3.81 |
2:00:00 | 2.5% | 160 米 | 2.61 秒 | 13,741 | 0 | 3.82 |
3:00:00 | 2.5% | 150 米 | 2.62 秒 | 13,728 | 0 | 3.81 |
...... | ||||||
总计/平均值 | 2.5% | 150 米 | 2.62 | 164,772 | 0 | 3.81 |
从CPU/Memory/Latency/QPS上来看,在12个小时内,都保持得非常稳定,说明服务器不会因为长时间运行而变得不稳定。
测试端对端(e2e)的请求/响应时间。想得到具体的数值,所以不需要很大的负载压力。
这次我们需要使用真实的认知服务,网络环境也使用真实的互联网环境。亦即需要把中间服务层部署到互联网上后进行测试,因为用模拟环境和局域网测试出来的数据不能代表实际的用户使用情况。
模拟1个用户,持续向中间服务层发送请求1小时。然后模拟3个并发用户,持续向中间服务层发送请求10分钟。这两种方法都不会对认知服务带来很大的压力。
在得到了一系列的数据以后,每组数据都会有响应时间,我们把它们按照从长(慢)到短(快)的顺序排列,得到下图(其中横坐标是用户数,纵坐标是响应时间):
一般来说,我们要考察几个点,P90/P95/P99,比如P90的含义是:有90%的用户的响应时间小于等于2449ms。这意味着如果有极个别用户响应时间在10秒以上时,是一种正常的情况;如果很多用户(比如>5%)都在10秒以上就不正常了,需要立刻检查服务器的运行状态。
最后得到的结果如下表,亦即性能指标:
百分比 | P90型 | P95型 | P99型 | 平均 |
---|---|---|---|---|
关键绩效指标 | <3000毫秒 | <3250 | <4000 | 不适用 |
服务器端处理时间 | 2449毫秒 | 2652毫秒 | 3571毫秒 | 1675毫秒 |
测试客户端 e2e 延迟 | 3160毫秒 | 3368毫秒 | 4369毫秒 | 2317毫秒 |
服务器端处理时间: 服务器端接收到请求到发出响应,所经过的时长。
测试客户端 e2e 延迟: 客户端发出请求到接收到响应,所经过的时长。
在集成服务层增加可以识别具有标准模式的文字的服务,比如电话号码、网络地址、邮件地址,这需要同时在基础服务层增加OCR底层服务,并在任务调度层增加一个并行任务。
在本地测试好服务器的基本功能后,部署到Azure上去,看看代码在实际环境中运行会有什么问题。因为我们不能实时地监控服务器,所以需要在服务层上增加log功能。
可以选择像Bob同学那样,先用第一种方式直接访问微软认知服务,然后一步步演进到中间层服务模式。建议使用VS2017 + Xamarin利器来实现跨平台应用。
在任务调度层,增加一个本地的图像分类器,如同“todo”里的preprocess,能够把输入图片分类成“有人脸”、“有地标”、“有文字”等,然后再根据信心指数调用名人服务或地标服务,以减轻服务器的负担,节省费用。比如,当"有地标"的信心指数小于0.5时,就终止后面的调用。这需要训练一个图片分类器,导出模型,再用Tools for AI做本地推理代码。