1. 程式人生 > 其它 >6. 堪比JMeter的.Net壓測工具 - Crank 實戰篇 - 收集診斷跟蹤資訊與如何分析瓶頸

6. 堪比JMeter的.Net壓測工具 - Crank 實戰篇 - 收集診斷跟蹤資訊與如何分析瓶頸

目錄

  1. 堪比JMeter的.Net壓測工具 - Crank 入門篇
  2. 堪比JMeter的.Net壓測工具 - Crank 進階篇 - 認識yml
  3. 堪比JMeter的.Net壓測工具 - Crank 進階篇 - 認識bombardier
  4. 堪比JMeter的.Net壓測工具 - Crank 進階篇 - 認識wrk、wrk2
  5. 堪比JMeter的.Net壓測工具 - Crank 實戰篇 - 介面以及場景壓測
  6. 堪比JMeter的.Net壓測工具 - Crank 實戰篇 - 收集診斷跟蹤資訊與如何分析瓶頸
  7. 堪比JMeter的.Net壓測工具 - Crank 總結篇 - crank帶來了什麼

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. 將最後的結論與相關開發、運維人員溝通,確保都知曉瓶頸在哪裡,最後優化瓶頸

知識點:

開源地址

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

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

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

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

MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor

如果你對我們的 MASA Framework 感興趣,無論是程式碼貢獻、使用、提 Issue,歡迎聯絡我們

歡迎加入技術交流群:

招.Net中/高階開發工程師:座標杭州下沙,聯絡方式進群找群主,薪資範圍16-30K,有住房補貼