关于c#:6-堪比JMeter的Net压测工具-Crank-实战篇-收集诊断跟踪信息与如何分析瓶颈

48次阅读

共计 9905 个字符,预计需要花费 25 分钟才能阅读完成。

1. 前言

下面咱们曾经做到了接口以及场景压测,通过控制台输入后果,咱们只须要将后果收集整理下来,最初汇总到 excel 上,此次压测报告就能够实现了,但收集报告也挺麻烦的,交给谁呢……

找了一圈、没找到违心接手的人,该怎么办呢……思考了会儿还是决定看看是否通过程序解决咱们的难题吧,毕竟整顿表格太累╯﹏╰

2. 收集后果

通过查阅官网文档,咱们发现官网提供了把数据保留成 Json、csv、以及数据库三种形式,甚至还有小伙伴踊跃的对接要把数据保留到 Es 中,那选个最简略的吧!

要不抉择 Json 吧,不须要依赖内部存储,很简略,我感觉应该可试,试一下看看:输出命令:

crank --config load.benchmarks.yml --scenario api --load.framework net5.0 --application.framework net5.0 --json 1.json --profile local --profile crankAgent1 --description "wrk2- 获取用户详情" --profile defaultParamLocal

最初失去后果:

{
  "returnCode": 0,
  "jobResults": {
    "jobs": {
      "load": {
        "results": {
          "http/firstrequest": 85.0,
          "wrk2/latency/mean": 1.81,
          "wrk2/latency/max": 1.81,
          "wrk2/requests": 2.0,
          "wrk2/errors/badresponses": 0.0,
          "wrk2/errors/socketerrors": 0.0,
          "wrk2/latency/50": 1.81,
          "wrk2/latency/distribution": [
            [
              {
                "latency_us": 1.812,
                "count": 1.0,
                "percentile": 0.0
              },
              {
                "latency_us": 1.812,
                "count": 1.0,
                "percentile": 1.0
              }
            ]
          ]
        }
      }
    }
  }
}

残缺的导出后果

好吧,数据有点少,如同数据不太够吧,这些信息怎么解决能做成报表呢,再说了数据不对吧,QPS、提早呢?好吧,被看进去了,因为信息太多,我删了一点点(也就 1000 多行指标信息吧),看来这个不行,用 json 的话还得配合个程序好难……

csv 不必再试了,如果也是单个文本的话,也是这样,还得配个程序,都不能单干,干啥都得搭伴,那试试数据库如何

crank --config load.benchmarks.yml --scenario api --load.framework net5.0 --application.framework net5.0 --sql "Server=localhost;DataBase=crank;uid=sa;pwd=P@ssw0rd;" --table "local" --profile local --profile crankAgent1 --description "wrk2- 获取用户详情" --profile defaultParamLocal

咱们依据压测环境,把不同的压测指标存储到不同的数据库的表中,以后是本地环境,即 table = local

最初咱们把数据保留到了数据库中,那这样做回头须要报告的时候,我查问下数据库搞进去就好了,终于松了一口气,但好景不长,发现数据库存储也有个坑,之前 json 中看到的后果居然在一个字段中存储,不过幸好 SqlServer 2016 之后反对了 json,能够通过 json 解析搞定,但其中参数名有 / 等特殊字符,sql server 解决不了,难道又得写个网站能力展现这些数据了吗??真的绕不开搭伴干活这个坑吗?

微软不会就做出个这么鸡肋的货色,还必须要配个前端能力分明的搞进去指标吧……还得用 vue、好吧,我晓得尽管当初有 blazer,能够用 C# 开发,但还是心愿不那么麻烦,又认真查找了一番,发现 Crank 能够对后果做二次解决,能够通过 script,不错的货色,既然 sql server 数据库无奈反对特殊字符,那我加些新参数勾销特殊字符不就好了,新建 scripts.profiles.yml

scripts: 
  changeTarget: |
    benchmarks.jobs.load.results["cpu"] = benchmarks.jobs.load.results["benchmarks/cpu"]
    benchmarks.jobs.load.results["cpuRaw"] = benchmarks.jobs.load.results["benchmarks/cpu/raw"]
    benchmarks.jobs.load.results["workingSet"] = benchmarks.jobs.load.results["benchmarks/working-set"]
    benchmarks.jobs.load.results["privateMemory"] = benchmarks.jobs.load.results["benchmarks/private-memory"]
    benchmarks.jobs.load.results["totalRequests"] = benchmarks.jobs.load.results["bombardier/requests;http/requests"]
    benchmarks.jobs.load.results["badResponses"] = benchmarks.jobs.load.results["bombardier/badresponses;http/requests/badresponses"]
    benchmarks.jobs.load.results["requestSec"] = benchmarks.jobs.load.results["bombardier/rps/mean;http/rps/mean"]
    benchmarks.jobs.load.results["requestSecMax"] = benchmarks.jobs.load.results["bombardier/rps/max;http/rps/max"]
    benchmarks.jobs.load.results["latencyMean"] = benchmarks.jobs.load.results["bombardier/latency/mean;http/latency/mean"]
    benchmarks.jobs.load.results["latencyMax"] = benchmarks.jobs.load.results["bombardier/latency/max;http/latency/max"]
    benchmarks.jobs.load.results["bombardierRaw"] = benchmarks.jobs.load.results["bombardier/raw"]

以上解决的数据是基于 bombardier 的,同理大家能够实现对 wrk 或者其余的数据处理

通过以上操作,咱们胜利的把特殊字符的参数改成了没有特殊字符的参数,那接下来执行查问 sql 就能够了。

SELECT Description as '场景',
  JSON_VALUE (Document,'$.jobs.load.results.cpu') AS 'CPU 使用率 (%)',
  JSON_VALUE (Document,'$.jobs.load.results.cpuRaw') AS '多核 CPU 使用率 (%)',
  JSON_VALUE (Document,'$.jobs.load.results.workingSet') AS '内存应用 (MB)',
  JSON_VALUE (Document,'$.jobs.load.results.privateMemory') AS '过程应用的公有内存量 (MB)',
  ROUND(JSON_VALUE (Document,'$.jobs.load.results.totalRequests'),0) AS '总发送申请数',
  ROUND(JSON_VALUE (Document,'$.jobs.load.results.badResponses'),0) AS '异样申请数',
  ROUND(JSON_VALUE (Document,'$.jobs.load.results.requestSec'),0) AS '每秒反对申请数',
  ROUND(JSON_VALUE (Document,'$.jobs.load.results.requestSecMax'),0) AS '每秒最大反对申请数',
  ROUND(JSON_VALUE (Document,'$.jobs.load.results.latencyMean'),0) AS '均匀延迟时间 (us)',
  ROUND(JSON_VALUE (Document,'$.jobs.load.results.latencyMax'),0) AS '最大延迟时间 (us)',
  CONVERT(varchar(100),DATEADD(HOUR, 8, DateTimeUtc),20)  as '工夫'
FROM dev;

3. 如何剖析瓶颈

通过下面的操作,咱们曾经能够轻松的实现对场景的压测,并能疾速生成绝对应的报表信息,那正题来了,能够模仿高并发场景,那如何剖析瓶颈呢?毕竟报告只是为了通晓以后的零碎指标,而咱们更心愿的是晓得以后零碎的瓶颈是多少,怎么突破瓶颈,实现冲破呢……

首先咱们要先理解咱们以后的利用的架构,比方咱们当初应用的是微服务架构,那么

  • 利用拆分为几个服务?理解分明每个服务的作用
  • 服务之间的调用关系
  • 各服务依赖的根底服务有哪些、根底服务根本的信息状况

举例咱们以后的微服务架构如下:

通过架构图能够疾速理解到我的项目构造,咱们能够看到用户拜访 web 端,web 端依据申请对应去查问 redis 或者通过 http、grpc 调用服务获取数据、各服务又通过 redis、db 获取数据。

首先咱们先通过 crank 把以后的数据指标保留入库。调出其中不太现实的接口开始剖析。

在这里咱们拿两个压测接口举例:

  • 获取首页 Banner、QPS:3800 /s (Get)
  • 下单、QPS:8 /s (Post)

3.1. 获取首页 Banner

通过单测首页 banner 的接口,QPS 是 3800 多不到 4000 这样,尽管这个指标还不错,但咱们依然感觉很慢,毕竟首页 banner 就是很简略几个图片 + 题目组合的数据,数据量不大,并且是直连 Redis,仅在 Redis 不存在时才查问对应服务获取 banner 数据,这样的 QPS 切实不应该,并且这个还是仅压测独自的 banner,如果首页同时压测十几个接口,那其性能会暴降十倍不止,这样必定是不行的

咱们又压测了一次首页 banner 接口,发现有几个疑点:

  • redis 申请数彷徨在 3800 左右的样子,网络带宽占用 1M 的样子,无奈持续上涨
  • 查看 web 服务,发现时不时的会有调用服务超时出错的问题,Db 的访问量有上涨,但不显著,很快就上来了

思考: Redis 的申请数与最初的压测后果差不多,最初倒也对上了,但为什么 redis 的申请数这么低呢?难道是带宽限度!!

尽管是单机 redis,但 4000 也相对不可能是它的瓶颈,狐疑是带宽被限度了,应该就是带宽被限度了,起初跟运维一番切磋后,失去论断是 redis 没限度带宽……

那为什么不行呢,这么奇怪,redis 不可能就这么点并发就不行了,算了还是写个程序试一下吧,看看是不是真的测试环境不给力,redis 配置太差了,一番操作后发现,同一个 redis 数据,redis 读能够到 6 万 8,不到 7 万、带宽占用 10M,redis 终于洗清了它的嫌疑,此接口的 QPS 不行与 Redis 无关,但这么简略的一个构造为什么 QPS 就上不去呢……,如果不是 redis 的问题,那会不会是因为申请就没到 redis 上,是因为压测机的强度不够,导致申请没到 redis……过后冒出来这个有点愚昧的想法,那就减少压测机的数量,通过更改负载压测机配置,1 台压测机升到了 3 台,但惋惜的是单台压测机的指标不升反降,最初所有压测机的指标加到一起正好与之前一台压测机的压测后果差不多一样,那阐明 QPS 低与压测机无关,起初想到试试通过减少多副原本晋升 QPS,起初 web 正本由 1 台晋升到了 3 台,之前提到的服务调用报错的状况更加重大,之前只是偶然有一个谬误,但晋升 web 正本后,看到一大片的谬误

  • 提醒 Thread is busy,很多线程开始期待
  • 大量的服务调用超时,DB 查问迟缓

最初 QPS 1000 多一点,有几千个失败的谬误,这自觉的晋升正本貌似不大无效,之前只管 Qps 不高,但起码也在 4000,DB 也没事,这波神操作后 QPS 直降 4 分之 3,DB 还差点崩了,思维滑坡了,做了负优化……

持续思考,为何晋升正本,QPS 不升反降,为何呈现大量的调用超时、为何 DB 会差点被干崩,我只是查问个 redis,跟 DB 有毛关系啊!奇了怪了,看看代码怎么写的吧……烧脑

public async Task<List<BannerResponse>> GetListAsync()
{List<BannerResponse> result = new List<BannerResponse>();
  try
  {
    var cacheKey = "banner_all";
    var cacheResult = await _redisClient.GetAsync<List<BannerResponse>>(cacheKey);
    if (cacheResult == null)
    {result = this.GetListServiceAsync().Result;
      _redisClient.SetAsync(cacheKey, result, new()
      {DistributedCacheEntryOptions = new()
        {AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(5)
        }
      }).Wait();}
    else
    {result = cacheResult;}
    }
  catch (Exception e)
  {result = await this.GetListServiceAsync();
  }

  return result;
}

看了代码后发现,仅当 Reids 查问不到的时候,会调用对应服务查问数据,对应服务再查问 DB 获取数据,另外查问异样时,会再次调用服务查问后果,确保返回后果肯定是正确的,看似没问题,但为何压测会呈现下面那些奇怪景象呢……

申请超时、大量期待,那就是正好 redis 不存在,穿透到对应的服务查问 DB 了,而后压测同一时刻数据量过大,同一时刻查问到的 Reids 都是没有数据,最初导致调用服务的数量急剧回升,导致响应迟缓,超时加剧,线程因超时开释不及时,又导致可用线程较少。

这块咱们查找到对应的日志显示以下信息

System.TimeoutException: Timeout performing GET MyKey, inst: 2, mgr: Inactive, queue: 6, qu: 0, qs: 6, qc: 0, wr: 0, wq: 0, in: 0, ar: 0,
IOCP: (Busy=6,Free=994,Min=8,Max=1000), 
WORKER: (Busy=152,Free=816,Min=8,Max=32767)
  • 那么咱们能够调整 Startup.cs:
public void ConfigureServices(IServiceCollection services)
{ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
  ThreadPool.SetMinThreads(1000, completionPortThreads);// 依据状况调整最小工作线程,防止因创立线程导致的耗时操作

  ……………………………………………………………此处省略…………………………………………………………………………………………………………
}
  • web 服务调用底层服务太慢,那么晋升底层服务的响应速度(优化代码)或者进步解决能力(晋升正本)
  • 避免高并发状况下全副穿透到上层,减少底层服务的压力

前两点也是一个好的方法,但不是最好的解决办法,最好还是不要穿透到底层服务,如果 reids 不存在,就放一个申请过来是最好的,拿到数据就长久化到 redis,不要总穿透到上层服务,那么怎么做呢,最简略的方法就是应用加锁,但加锁会影响性能,但这个咱们能承受,起初调整加锁测试,穿透到底层服务的状况没有了,但很惋惜,申请数的确会随着正本的减少而减少,然而切实是有点不难看,起初又测试了下另外一个获取缓存数据的后果,后果 QPS:1000 多一点,比 banner 还要低的多,两边明明都应用的是 Reids,性能为何还有这么大的差异,为何咱们写的 redis 的 demo 就能到 6 万多的 QPS,两边都是拿的一个缓存,差距有这么大?难道是封装 redis 的 sdk 有问题?起初认真比照了起初写的 redis 的 demo 与 banner 调用 redis 的接口发现,一个是间接查问的 redis 的字符串,一个是封装 redis 的 sdk,多了一个反序列化的过程,最初通过测试,反序列化之后性能升高了十几倍,好吧看来只能晋升正本了……但为何另外的接口也是从 redis 获取,性能跟 banner 的接口不一样呢!!

通过认真比照发现,差异是信息量,QPS 更低的接口的数据量更大,那后果就有了,随着数据量的减少,QPS 会进一步升高,那这样一来的话,减少正本的作用不大啊,谁晓得会不会有一个接口的数据量很大,那性能岂不是差的要死,那还怎么玩,能不能晋升反序列化的性能或者不反序列化呢,通过认真思考,想到了二级缓存,如果用到了二级缓存,内存中有就不须要查问 redis,也不须要再反序列化,那么性能应该有所晋升,最初的构造如下图:

最初通过压测发现,单正本 QPS 靠近 50000,比最开始晋升 12 倍,并且也不会呈现服务调用超时,DB 解体等问题、且内存应用安稳

此次压测发现其 banner 这类场景的性能瓶颈在反序列化,而非 Redis、DB,如果依照一开始不分明其工作原理、自觉的调整正本数,可能最初会加剧零碎的雪崩,而如果咱们把 DB 资源、Redis 资源自觉上调、并不会对最初的后果有太大帮忙,最多也只是延缓解体的工夫而已

3.2. 下单

下单的 QPS 是 8,这样的 QPS 曾经无法忍受了,每秒只有十个申请能够下单胜利,如果两头再呈现一个库存有余、账户余额有余、流动资格不够等等,理论能下单的人用一个手能够数过来,真的就这么惨……尽管下单的确很费性能,不过的确不至于这么低吧,先看下下单流程吧

简化后的下单流程就这么简略,web 通过 dapr 的 actor 服务调用 order service,而后就是漫长的查问 db、操作 redis 操作,因波及业务代码、具体代码就不再放出,但能够简略说一下其中做的事件,查看账户余额、重复的减少 redis 库存确保库存平安、查看是否满足流动、为推荐人计算待结算佣金等等一系列操作,整个看下来把人看懵了,经常是刚看了下面的,看上面代码的时候遗记下面具体干了什么事,代码太多了,一个办法数千行,其中再调用一些数百行的代码,真的吐血了,未免感叹我司的开发小哥哥是真的弱小,这么简单的业务竟然能这么 ” 顺畅 ” 的跑起来,前面还有 N 个需要期待加到下单上,果然不是个别人

不过话说回来,尽管是业务是真的多,也真的乱,不过这样搞也不至于 QPS 才只有 8 这么可怜吧,服务器的解决能力可不是二十几年前的电脑能够比较的,单正本 8 核 16G 的配置不反对这么拉胯吧,再看一下到底谁才是真正的幕后黑手……

但到底哪里性能瓶颈在哪里,这块就要出杀手锏了

通过 Tracing 能够很分明的看到各节点的耗时状况,这将对咱们剖析瓶颈提供了十分大的帮忙、咱们看到了尽管有几十次的查问 DB 操作,但 DB 还挺给力,根本也再很短时间内就给出了响应,那剩余时间消耗到了哪里呢?咱们看到整体耗时 11s、但查问 Db 加起来也仅仅不到 1s,那么残余操作都在哪里?要晓得哪怕咱们优化 DB 查问性能,缩小 DB 查问,那晋升的性能对当初的后果也是微不足道

联合 Tracing 以及下单流程图,咱们发现从 Web 到 Order Service 是通过 actor 来实现的,那会不是这里耗时影响的呢?

但 dapr 是个新常识、开发的小哥哥速度真快,这么快就用上 dapr 了(ˇˍˇ)不晓得小哥哥的头发还有多少……

疾速去找到下单应用 actor 的中央,如下:

[HttpPost]
[Authorize]
public async Task<CreateOrderResponse> CreeateOrder([FromBody] CreateOrderModel request)
{
    string actionType = "SalesOrderActor";
    var salesOrderActor = ActorProxy.Create<ISalesOrderActor>(new ActorId(request.SkuList.OrderBy(sku => sku.Sku).FirstOrDefault().Sku), actionType);
    request.AccountId = Account.Id;
    var result = await salesOrderActor.CreateOrderAsync(request);
    return new Mapping<ParentSalesOrderListViewModel, CreateOrderResponse>().Map(result);
}

咱们看到了这边代码非常简略,获取商品信息的第一个 sku 编号作为 actor 的 actorid 应用,而后失去下单的 actor,之后调用 actor 中的创立订单办法最初失去下单后果,这边的代码太简略了,让人心情愉快,那这块会不会有可能影响下单速度呢?它是不是那个性能瓶颈最大的幕后黑手?

首先这块咱们就须要理解下什么是 Dapr、Actor 又是什么,不理解这些常识咱们只能靠抓阄来猜这块是不是瓶颈了……

Dapr 全称是 Distributed Application Runtime,分布式应用运行时,并于往年退出了 CNCF 的孵化我的项目,目前 Github 的 star 高达 16k,相干的学习文档在文档底部能够找到,我也是看着上面的文档理解 dapr

通过理解 actor,咱们发现用 sku 作为 actorid 是极不明智的抉择,像秒杀这类商品不就是抢的指定规格的商品吗?如果这样一来,这不是在压测 actor 吗?这块咱们跟对应的开发小哥哥沟通了下,通过调整 actorid 顺利将 Qps 晋升到了 60 作用,前面又通过优化缩小 db 查问、调整业务规定的程序等操作顺利将 QPS 晋升到了不到一倍,尽管还是很低,不过接下来的优化工作就须要再深层次的调整业务代码了……

4. 总结

通过实战咱们总结出剖析瓶颈从以下几步走:

  1. 通过第一轮的压测获取性能差的接口以及指标
  2. 通过与开发沟通或者本人查看源码的形式梳理接口流程
  3. 通过剖析其我的项目所占用资源状况、依赖第三方根底占用资源状况以及 Tracing 更进一步的确定瓶颈大略的点在哪几块
  4. 通过重复测试调整确定性能瓶颈的最大黑手
  5. 将最初的论断与相干开发、运维人员沟通,确保都通晓瓶颈在哪里,最初优化瓶颈

知识点:

  • Dapr

    • 手把手教你学 Dapr 系列
  • Tracing

    • OpenTracing 简介、对于 OpenTracing 后续咱们也会开源,能够提前关注咱们的开源我的项目

      • Masa.BuildingBlocks
      • Masa.Contrib

开源地址

MASA.BuildingBlocks:https://github.com/masastack/…

MASA.Contrib:https://github.com/masastack/…

MASA.Utils:https://github.com/masastack/…

MASA.EShop:https://github.com/masalabs/M…

MASA.Blazor:https://github.com/BlazorComp…

如果你对咱们的 MASA Framework 感兴趣,无论是代码奉献、应用、提 Issue,欢送分割咱们

正文完
 0