聊一聊如何使用Crank給我們的類庫做基準測試

Catcher8發表於2023-04-06

背景

當我們寫了一個類庫提供給別人使用時,我們可能會對它做一些基準測試來測試一下它的效能指標,好比記憶體分配等。

在 .NET 的世界中,用 BenchmarkDotNet 來做這件事是非常不錯的選擇,我們只要寫少量的程式碼就可以在本地執行基準測試然後得到結果。

這個在修改程式碼的時候,效果可能會更加明顯,因為我們想知道我們的修改會不會使這段程式碼跑的更快,佔用的資源更少。

作一個簡單的假設,根據測試用例,程式碼變更之前,某方法在基準測試的分配的記憶體是 1M,修改之後變成 500K,那麼我們可以認為這次的程式碼變更是有效能提升的,佔用的資源更少了,當然這個得在單元測試透過的前提下。

試想一下,如果遇到下面的情況

  1. 想在多個不同配置的機器上面執行基準測試,好比 4c8g 的windows, 4c16g 的 linux
  2. Pull Request/Merge Request 做程式碼變更時,如何較好的做變更前後的基準測試比較

這個時候就會複雜一點了,要對一份程式碼在多個環境下面執行,做一些重複性的工作。

那麼我們有沒有辦法讓這個變得簡單呢?答案是肯定的。

我們可以用 Crank 這個工具來完成這些內容。

什麼是 Crank

Crank 是.NET團隊用於執行基準測試的基礎設施,包括(但不限於)TechEmpower Web Framework基準測試中的場景。 Crank 第一次出現在公眾的視野應該是在 .NET Conf 2021, @sebastienros 演講的 Benchmarking ASP.NET Applications with .NET Crank

Crank 是 client-server (C/S) 的架構,主要有一個控制器 (Controller) 和一個或多個代理 (Agent) 組成。 其中控制器就是 client,負責傳送指令;代理就是 server,負責執行 client 傳送的指令,也就是執行具體的測試內容。

下面是它的架構圖。

可以看到,控制器和代理之間的互動是透過 HTTP 請求來驅動的。然後代理可以執行多個不同型別的作業型別。

我們這篇部落格主要講的是圖中的 .NET project Job

先來看看官方倉庫一個比較簡單的入門示例。

入門示例

首先要安裝 crank 相關的兩個工具,一個是控制器,一個是代理。

dotnet tool update Microsoft.Crank.Controller --version "0.2.0-*" --global

dotnet tool update Microsoft.Crank.Agent --version "0.2.0-*" --global

然後執行官方倉庫上面的 micro 示例,是一個 Md5 和 SHA 256 對比的例子。

public class Md5VsSha256
{
    [Params(100, 500)]
    public int N { get; set;}
    private readonly byte[] data;

    private readonly SHA256 sha256 = SHA256.Create();
    private readonly MD5 md5 = MD5.Create();

    public Md5VsSha256()
    {
        data = new byte[N];
        new Random(42).NextBytes(data);
    }

    [Benchmark]
    public byte[] Sha256() => sha256.ComputeHash(data);

    [Benchmark]
    public byte[] Md5() => md5.ComputeHash(data);
}

要注意的是 Main 方法,要用 BenchmarkSwitcher 來執行,因為 Crank 是用命令列來執行的,會附加一些引數,也就是程式碼中的 args。

public static void Main(string[] args)
{
    BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}

然後是控制器要用到的配置檔案,裡面就是要執行的基準測試的內容,要告訴代理怎麼執行。

# 作業
jobs:
  # 作業名,自定義
  benchmarks:
    # 源相關內容
    source:
      # 這裡是本地資料夾,也可以配置遠端 repository 和分支
      localFolder: .
      # 這個是具體的 csproj
      project: micro.csproj
    # 一些變數
    variables:
      filterArg: "*"
      jobArg: short
    # 引數
    arguments: --job {{jobArg}} --filter {{filterArg}} --memory
    options:
      # 使用 BenchmarkDotNet
      benchmarkDotNet: true

# 場景    
scenarios:
  # 場景名,自定義
  Md5VsSha256:
    application:
      # 與前面的定義作業名一致
      job: benchmarks

# 檔案
profiles:
  # 檔案名,自定義
  local:
    jobs: 
      application:
        # 代理的地址
        endpoints: 
          - http://localhost:5010

下面先來啟動代理,直接執行下面的命令即可。

crank-agent

會看到下面的輸出:

[11:42:30 INF] Created temp directory 'C:\Users\catcherwong\AppData\Local\Temp\2\benchmarks-agent\benchmarks-server-8952\2mmqc00i.3b1'
[11:42:30 INF] Agent ready, waiting for jobs...

預設埠是 5010,可以透過 -u|--url 來指定其他的;如果執行代理的電腦已經安裝好 SDK 了,可以指定 --dotnethome 避免因網路問題導致無法正常下載 SDK。

然後是透過控制器向代理傳送指令。

crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario  Md5VsSha256 --profile local

上面的命令指定了我們上面的配置檔案,同時還指定了 scenario 和 profile。因為配置檔案中可以有多個 scenario 和 profile,所以在單次執行是需要指定具體的一個。

如果需要執行多個 scenario 則需要執行多次命令。

在執行命令後,代理裡面就可以看到日誌輸出了:

最開始的是收到作業請求,然後安裝對應的 SDK。安裝之後就會對指定的專案進行 release 釋出。

釋出成功後就會執行 BenchmarkDotNet 相關的內容。

執行完成後會輸出結果,最後清理這次基準測試的內容。

代理執行完成後,可以在控制器側看到對應的結果:

一般來說,我們會把控制器得到的結果儲存在 JSON 檔案裡面,便於後續作對比或者要出趨勢圖。

這裡可以加上 --json 檔名.json

crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario  Md5VsSha256 --profile local --json base.json

執行多次,將結果存在不同的 JSON 檔案裡,尤其程式碼變更前後的結果。

crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario  Md5VsSha256 --profile local --json head.json

最後是把這兩個結果做一個對比,就可以比較清楚的看到程式碼變更是否有帶來提升。

crank compare base.json head.json

上面提到的還是在本地執行,如果要在不同的機器上面執行要怎麼配置呢?

我們要做的是在配置檔案中的 profiles 節點增加機器的代理地址即可。

下面是簡單的示例:

profiles:
  local:
    jobs: 
      application:
        endpoints: 
          - http://localhost:5010
  remote-win:
    jobs: 
      application:
        endpoints: 
          - http://192.168.1.100:9090
  remote-lin:
    jobs: 
      application:
        endpoints: 
          - http://192.168.1.102:9090      

這個時候,如果指定 --profile remote-win 就是在 192.168.1.100 這臺伺服器執行基準測試,如果是 --profile remote-lin 就是在 192.168.1.102

這樣就可以很輕鬆的在不同的機器上面執行基準測試了。

Crank 還有一個比較有用的功能是可以針對 Pull Request 進行基準測試,這對一些需要基準測試的開源專案來說是十分有幫助的。

接下來老黃就著重講講這一塊。

Pull Request

正常來說,程式碼變更的肯定是某個小模組,比較少出現多個模組同時更新的情況,如果是有,估計也會被打回拆分!

所以我們不會選擇執行所有模組的基準測試,而是執行變更的那個模組的基準測試。

思路上就是有人提交 PR 後,由專案組成員在 PR 上面進行評論來觸發基準測試的執行,非專案組成員的話不能觸發執行。

下面就用這個 Crank 提供的 Pull Request Bot 來完成後面的演示。

要想用這個 Bot 需要先執行下面的安裝命令:

dotnet tool update Microsoft.Crank.PullRequestBot --version "0.2.0-*" --global

安裝後會得到一個 crank-pr 的檔案,然後執行 crank-pr 的命令就可以了。

可以看到它提供了很多配置選項。

下面是一個簡單的例子

crank-pr \
  --benchmarks lib-dosomething \
  --components lib \
  --config ./benchmark/pr-benchmark.yml\
  --profiles local \
  --pull-request 1 \
  --repository "https://github.com/catcherwong/library_with_crank" \
  --access-token "${{ secrets.GITHUB_TOKEN }}" \
  --publish-results true

這個命令是什麼意思呢?

它會對 catcherwong/library_with_crank 這個倉庫的 Id 為 1 的 Pull Request 進行兩次基準測試,一次是主分支的程式碼,一次是 PR 合併後的程式碼;基準測試的內容由 benchmarks,components 和 profiles 三個選項共同決定;最後兩個基準測試的結果對比會在 PR 的評論上面。

其中 catcherwong/library_with_crank 是老黃提前準備好的示例倉庫。

下面來看看 pr-benchmark.yml 的具體內容

components:
    lib: 
        script: |
            echo lib
        arguments:
            # crank arguments
            "--application.selfContained false"

# default arguments that are always used on crank commands
defaults: ""

# the first value is the default if none is specified
profiles:
    local:
      description: Local
      arguments: --profile local
    remote-win:
      description: windows
      arguments: --profile remote-win 
    remote-lin:
      description: linux
      arguments: --profile remote-lin 

benchmarks:
    lib-dosomething:
      description: DoSomething
      arguments: --config ./benchmark/library.benchmark.yml --scenario dosomething

    lib-getsomething:
      description: GetSomething
      arguments: --config ./benchmark/library.benchmark.yml --scenario getsomething

    lib-another:
      description: Another
      arguments: --config ./benchmark/library.benchmark.yml --scenario another

基本上可以說是把 crank 的引數拆分了到了不同的配置選項上面去了,執行的時候就是把這些進行組合。

再來看看 library.benchmark.yml

jobs:
  lib:
    source:
      localFolder: ../src
      project: BenchmarkLibrary/BenchmarkLibrary.csproj
    variables:
      filter: "*"
      jobArg: short
    arguments: --job {{jobArg}} --filter {{filter}} --memory
    options:
      benchmarkDotNet: true  

scenarios:
  dosomething:
    application:
      job: lib
      variables:
        filter: "*DoSomething*"

  getsomething:
    application:     
      job: lib
      variables:
        filter: "*GetSomething*"

  another:
    application:     
      job: lib
      variables:
        filter: "*Method*"

profiles:
  local:
    jobs: 
      application:
        endpoints: 
          - http://localhost:9999
  
  remote-lin:
    jobs: 
      application:
        endpoints: 
          - http://remote-lin.com

  remote-win:
    jobs: 
      application:
        endpoints: 
          - http://remote-win.com

和前面入門的例子有點不一樣,我們在 scenarios 節點 裡面加了一個 variables,這個和 jobs 裡面定義的 variables 和 arguments 是相對應的。

如果指定 --scenario dosomething,那麼最後得到的 arguments 就是

--job short --filter *DoSomething* --memory

後面就是來看看效果了。

這裡省略了評論內容的解析,也就是評論什麼內容的時候會觸發執行,因為這一塊不是重點,有興趣可以看 workflow 的指令碼即可。

具體的執行過程可以參考

https://github.com/catcherwong/library_with_crank/actions/runs/4598397510/jobs/8122376959

當然,如果條件允許的話,也可以用自己的伺服器資源來跑基準測試,不用 Github Action 提供的資源。

這樣的好處是相對穩定,可以自己根據場景指定不同配置的伺服器。不過對一些沒那麼複雜類庫,用 Github Action 的資源也是無傷大雅的。

下面這個截圖就是在提交到外部伺服器上面執行的。

如果倉庫不是在 Github,是在自建 Gitlab 或者其他的,就可以根據這個思路來自定義流水線從而去完成這些基準測試的操作。

總結

Crank 還是一個挺不錯的工具,可以結合 BenchmarkDotNet 來做類庫的基準測試,也可以結合 wrk/wrk2/bombardier/h2load 等壓測工具進行 api/grpc 框架和應用的測試。

這裡只介紹了其中一個小塊的內容,還有挺多內容可以挖掘一下的。

最後是本文的示例程式碼:

https://github.com/catcherwong/library_with_crank

參考資料

相關文章