使用 GitHub Actions 構建 Golang PGO

今年 2 月,我宣佈 Dolt 版本現已構建爲配置文件引導優化 (pgo) 二進制文件,利用 Golang 1.20 的強大功能將 Dolt 的讀取性能提高 5.3%。

在我宣佈這一消息之前,我們的一位常駐 Golang 專家 Zach 試驗並測試了 Golang 的 pgo 功能,並寫下了他在使用 Dolt 構建後觀察到的性能提升,該配置文件首先針對 Dolt 運行我們的 Sysbench 基準測試。從那時起,我們知道我們必須將這些性能提升納入我們發佈的二進制文件中,因此我們重新設計了 Dolt 的發佈流程來構建 pgo 版本。

今天,我將介紹 Dolt 的一般發佈流程,該流程使用 GitHub Actions,並且我將詳細介紹發佈流程的所有階段。我還將回顧一下我們在開始發佈 pgo 版本的過程中所做的更改。希望這能讓您收集一些可用於將 pgo 構建到您自己的 Golang 版本中的見解!

讓我們開始吧。

Dolt 通過 GitHub Actions 發佈

Dolt 利用 GitHub Actions 執行許多自動化任務,其中之一是創建和發佈版本。

GitHub Actions 使用稱爲工作流的文件來定義作業,它將執行工作流文件中定義的工作。這些作業部署到運行器或主機上,您可以自行託管或允許 GitHub 爲您託管。

自託管運行器由您在 GitHub Actions 外部配置和維護。GitHub 託管的運行器可免費用於公共存儲庫,全部由 GitHub 託管和維護,但它們具有特定的存儲、內存和 CPU 限制,具體取決於您的訂閱級別。對於 Dolt,我們使用免費的 GitHub 託管運行器。

從高層次來看,Dolt 發佈流程需要實現一些目標。

首先,也是最重要的,該過程需要爲新版本的 Dolt 創建標籤和發佈,並將 Dolt 的預編譯二進制文件上傳到發佈資產。

其次,發佈過程需要針對這個新版本的 Dolt 運行我們的 Sysbench 基準測試,並將結果通過電子郵件發送給我們的 DoltHub 團隊。

第三,與本博客不太相關,該流程需要啓動我們在發佈期間需要執行的任何其他輔助任務,例如創建依賴於多個存儲庫的拉取請求描述的 Dolt 發行說明、將版本發佈到各種包管理器這樣就可以輕鬆地從它們安裝,將新的 Docker 鏡像推送到 DockerHub,或者升級我們擁有的各種存儲庫中的 Dolt 依賴項。

因此,考慮到這些目標,我們提出了一套 GitHub Actions 工作流程,利用 repository_dispatch 事件,以便我們可以實現每個目標。讓我們看一個圖表,該圖表顯示了此設計的原理,然後我們將深入瞭解工作流程的細節。

在上圖中,您將看到兩個上下文:GitHub Actions 上下文和 Kubernetes (K8s) 上下文。我們首先討論 GitHub Actions 上下文。

對於 Dolt 的原始發佈流程,我們使用了三個工作流程:“發佈 Dolt”工作流程、“部署 K8s Sysbench 基準測試作業”工作流程和 “電子郵件團隊” 工作流程。

“發佈 Dolt” 工作流程啓動整個 Dolt 發佈流程,並由我們的工程團隊在準備發佈新版本 Dolt 時手動運行。這是工作流程的簡化版本,引用了上圖中顯示的步驟。

name: Release Dolt
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'SemVer format release tag, i.e. 0.24.5'
        required: true
jobs:
  format-version:
    runs-on: ubuntu-22.04
    outputs:
      version: ${{ steps.format_version.outputs.version }}
    steps:
      - name: Format Input
        id: format_version
        run: |
          version="${{ github.event.inputs.version }}"
          if [[ $version == v* ]];
          then
            version="${version:1}"
          fi
          echo "version=$version" >> $GITHUB_OUTPUT
  create-release:
    needs: format-version
    name: Create release
    runs-on: ubuntu-22.04
    outputs:
      release_id: ${{ steps.create_release.outputs.id }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Set up Go 1.x
        uses: actions/setup-go@v3
        with:
          go-version: ^1.21
      - name: Update dolt version command
        run: sed -i -e 's/  Version = ".*"/  Version = "'"$NEW_VERSION"'"/' "$FILE"
        env:
          FILE: ${{ format('{0}/go/cmd/dolt/dolt.go', github.workspace) }}
          NEW_VERSION: ${{ needs.format-version.outputs.version }}
      - name: Set minver TBD to version
        run: sed -i -e 's/minver:"TBD"/minver:"'"$NEW_VERSION"'"/' "$FILE"
        env:
          FILE: ${{ format('{0}/go/cmd/dolt/commands/sqlserver/yaml_config.go', github.workspace) }}
          NEW_VERSION: ${{ needs.format-version.outputs.version }}
      - name: update minver_validation.txt
        working-directory: ./go
        run: go run -mod=readonly ./utils/genminver_validation/ $FILE
        env:
          FILE: ${{ format('{0}/go/cmd/dolt/commands/sqlserver/testdata/minver_validation.txt', github.workspace) }}
      - uses: EndBug/add-and-commit@v9.1.1
        with:
          message: ${{ format('[ga-bump-release] Update Dolt version to {0} and release v{0}', needs.format-version.outputs.version) }}
          add: ${{ format('["{0}/go/cmd/dolt/dolt.go", "{0}/go/cmd/dolt/commands/sqlserver/yaml_config.go", "{0}/go/cmd/dolt/commands/sqlserver/testdata/minver_validation.txt"]', github.workspace) }}
          cwd: "."
          pull: "--ff"
      - name: Build Binaries
        id: build_binaries
        run: |
          latest=$(git rev-parse HEAD)
          echo "commitish=$latest" >> $GITHUB_OUTPUT
          GO_BUILD_VERSION=1.21 go/utils/publishrelease/buildbinaries.sh
      - name: Create Release
        id: create_release
        uses: dolthub/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: v${{ needs.format-version.outputs.version }}
          release_name: ${{ needs.format-version.outputs.version }}
          draft: false
          prerelease: false
          commitish: ${{ steps.build_binaries.outputs.commitish }}
      - name: Upload Linux AMD64 Distro
        id: upload-linux-amd64-distro
        uses: dolthub/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: go/out/dolt-linux-amd64.tar.gz
          asset_name: dolt-linux-amd64.tar.gz
          asset_content_type: application/zip
...
      - name: Upload Install Script
        id: upload-install-script
        uses: dolthub/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: go/out/install.sh
          asset_name: install.sh
          asset_content_type: text/plain
  trigger-performance-benchmark-email:
    needs: [format-version, create-release]
    runs-on: ubuntu-22.04
    steps:
      - name: Trigger Performance Benchmarks
        uses: peter-evans/repository-dispatch@v2.0.0
        with:
          token: ${{ secrets.REPO_ACCESS_TOKEN }}
          event-type: release-dolt
          client-payload: '{"version": "${{ needs.format-version.outputs.version }}", "actor": "${{ github.actor }}"}'

此工作流是使用 workflow_dispatch 事件手動觸發的,並且需要新的版本號作爲輸入。從那裏,它對版本輸入進行一些快速格式化,然後將此新版本寫入並提交到 Dolt 的 main 分支,以便發佈的二進制文件將從 dolt version 命令輸出此新版本。

在 “構建二進制文件” 步驟中, create-release 作業運行 buildbinaries.sh 腳本,該腳本使用運行 go build 命令的 Golang Docker 容器從源代碼構建 Dolt。

我們使用 Docker 容器來構建 Dolt,以便堆棧跟蹤的路徑輸出是通用 Linux go 路徑,而不是引用運行器或我們的一臺個人計算機上的 go 安裝的路徑(這發生在 Dolt 🤠 的早期版本中)。

接下來,“創建版本” 步驟創建標籤並在 GitHub 上發佈版本。它還提供了一個 upload_url ,用於 create-release 作業的所有後續步驟,將已編譯的二進制文件上傳到新的 GitHub 版本。

此工作流程的最後部分是在所有先前作業完成後運行的另一個作業。該作業稱爲 trigger-performance-benchmark-email 。它使用我們在其市場上找到的 GitHub Action 來發出 repository_dispatch 事件,該事件啓動單獨的 Dolt 工作流程。如果我們回顧一下我們的圖表,我們就可以看到這一點。

我們的圖表顯示了 “發佈 Dolt” 工作流程的最後一步,該工作流程指向另一個名爲 “部署 K8s Sysbench 基準測試作業” 的工作流程。這是由 trigger-performance-benchmark-email 工作流作業啓動的工作流。

此工作流以及其他類似工作流被設計爲部分異步分派,以便它不會僅與 “Release Dolt” 工作流緊密耦合。

事實上,各種工作流程將使用 repository_dispatch 事件觸發此工作流程,因爲我們需要在不同時間運行性能基準測試,而不僅僅是在發佈期間運行。有趣的是,這個工作流程本身啓動了另一個異步流程,我們可以在箭頭所示的圖中看到它 - 它部署了一個運行我們的 Sysbench 基準測試的 K8s 作業。

事實上,我們已經寫了很多關於 Dolt 使用 Sysbench 對 Dolt 和 MySQL 進行基準測試來比較它們的性能的文章,但我認爲我們沒有討論具體如何做到這一點的實現細節。這個博客對我來說是一個很好的回顧,所以我會暫時回顧一下。在開始之前,讓我們簡要了解一下 “部署 K8s Sysbench 基準測試作業” 工作流程。

name: Benchmark Latency
on:
  repository_dispatch:
    types: [ benchmark-latency ]
jobs:
  performance:
    runs-on: ubuntu-22.04
    name: Benchmark Performance
    strategy:
      matrix:
        dolt_fmt: [ "__DOLT__" ]
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - uses: azure/setup-kubectl@v3.0
        with:
          version: 'v1.23.6'
      - name: Install aws-iam-authenticator
        run: |
          curl -o aws-iam-authenticator https://amazon-eks.s3.us-west-2.amazonaws.com/1.18.8/2020-09-18/bin/linux/amd64/aws-iam-authenticator && \
          chmod +x ./aws-iam-authenticator && \
          sudo cp ./aws-iam-authenticator /usr/local/bin/aws-iam-authenticator
          aws-iam-authenticator version
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2.2.0
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-west-2
      - name: Create and Auth kubeconfig
        run: |
          echo "$CONFIG" > kubeconfig
          KUBECONFIG=kubeconfig kubectl config set-credentials github-actions-dolt --exec-api-version=client.authentication.k8s.io/v1alpha1 --exec-command=aws-iam-authenticator --exec-arg=token --exec-arg=-i --exec-arg=eks-cluster-1
          KUBECONFIG=kubeconfig kubectl config set-context github-actions-dolt-context --cluster=eks-cluster-1 --user=github-actions-dolt --namespace=performance-benchmarking
          KUBECONFIG=kubeconfig kubectl config use-context github-actions-dolt-context
        env:
          CONFIG: ${{ secrets.CORP_KUBECONFIG }}
      - name: Create Sysbench Performance Benchmarking K8s Job
        run: ./.github/scripts/performance-benchmarking/run-benchmarks.sh
        env:
          FROM_SERVER: ${{ github.event.client_payload.from_server }}
          FROM_VERSION: ${{ github.event.client_payload.from_version }}
          TO_SERVER: ${{ github.event.client_payload.to_server }}
          TO_VERSION: ${{ github.event.client_payload.to_version }}
          MODE: ${{ github.event.client_payload.mode }}
          ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }}
          ACTOR: ${{ github.event.client_payload.actor }}
          ACTOR_EMAIL: ${{ github.event.client_payload.actor_email }}
          REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
          KUBECONFIG: "./kubeconfig"
          INIT_BIG_REPO: ${{ github.event.client_payload.init_big_repo }}
          NOMS_BIN_FORMAT: ${{ matrix.dolt_fmt }}
          TEMPLATE_SCRIPT: ${{ github.event.client_payload.template_script }}
      - name: Create TPCC Performance Benchmarking K8s Job
        run: ./.github/scripts/performance-benchmarking/run-benchmarks.sh
        env:
          FROM_SERVER: ${{ github.event.client_payload.from_server }}
          FROM_VERSION: ${{ github.event.client_payload.from_version }}
          TO_SERVER: ${{ github.event.client_payload.to_server }}
          TO_VERSION: ${{ github.event.client_payload.to_version }}
          MODE: ${{ github.event.client_payload.mode }}
          ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }}
          ACTOR: ${{ github.event.client_payload.actor }}
          ACTOR_EMAIL: ${{ github.event.client_payload.actor_email }}
          REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
          KUBECONFIG: "./kubeconfig"
          INIT_BIG_REPO: ${{ github.event.client_payload.init_big_repo }}
          NOMS_BIN_FORMAT: ${{ matrix.dolt_fmt }}
          WITH_TPCC: "true"
          TEMPLATE_SCRIPT: ${{ github.event.client_payload.template_script }}

這個工作流程雖然簡短,但有點忙碌,它根據 K8s 集羣對 kubectl 客戶端進行身份驗證,我們在其中運行 Sysbench 基準測試,並提供運行名爲 run-benchmarks.sh 的腳本所需的環境變量。該腳本使用這些變量的值編寫 K8s 作業配置文件,然後應用它,從而在我們的 K8s 集羣中部署基準測試作業。

此時,您可能想知道爲什麼我們選擇在 K8s 集羣中運行 Dolt 基準測試,而不是僅僅使用 GitHub Actions 及其運行程序來對 Dolt 進行基準測試。這有幾個原因。

第一,GitHub 託管的運行程序有非常具體的限制,至少對於免費層來說是這樣,並且爲了對我們的數據庫進行基準測試,我們不一定希望受到這些限制。

此外,當我們進行基準測試運行時,我們無法知道或控制 GitHub 託管的運行器上正在運行哪些其他進程或軟件,這可能會以不可預測的方式對運行結果產生負面影響。

雖然當然可以在 GitHub Actions 中使用自託管運行器來規避這兩個問題,在這種情況下,我們可以僅使用 GitHub Actions 對 Dolt 進行基準測試,但我們已經在 K8s 集羣中提供了可輕鬆配置的主機,因此我們選擇只需使用它們即可。

事實上, applying 我們的 K8s 基準測試作業將使用 K8s 集羣自動縮放器配置一個新的基準測試主機,這非常酷。

無論如何,暫時回到我們的圖表,我們看到在驗證 kubectl 客戶端之後,“部署 Sysbench 基準測試作業”工作流程部署 K8s 作業,並且該流程移動到 K8s 上下文,並且 “K8s Sysbench 基準測試作業” 正在運行。

現在從技術上講,原始 Dolt 發佈過程的這一部分更多的是發佈後步驟。在 GitHub 上創建新的 Dolt 版本不需要運行基準測試作業,它只是爲我們的團隊提供有關該版本延遲的報告。但重要的是要了解我們最初發布過程的這一部分,這樣我們對 Dolt 發佈過程的 pgo 更新纔會更有意義,但稍後會詳細介紹。

在該圖的 K8s 上下文中,我們可以看到基準測試作業執行了幾個步驟。它根據提供的提交 SHA 構建 Dolt 二進制文件。在本例中,它是來自 Dolt main HEAD 的 SHA。

接下來,它針對已編譯的 Dolt 版本運行 Sysbench 測試,然後將 Sysbench 運行結果上傳到 AWS S3 存儲桶。最後,它觸發位於 Dolt 存儲庫中的不同 GitHub Actions 工作流程,稱爲 “電子郵件團隊” 工作流程。

爲了執行所有這些基準測試以及上傳和觸發,我們編寫了一個內部工具,可用於針對 MySQL 版本對 Dolt 版本進行基準測試。

該工具使用我們在 Dolt 存儲庫中維護的一些庫代碼,但我將提供內部工具和庫代碼中的一些相關片段,以便您瞭解我們如何實現這些代碼來運行基準測試。

我們的內部基準測試工具代碼本質上是以下 go 函數:

func compare(ctx context.Context,
  fromServer,
  toServer runner.ServerType,
  fromVersion,
  toVersion,
  fromProfile,
  toProfile,
  dir,
  doltCommand,
  doltgresCommand,
  mysqlExec,
  mysqlProtocol,
  mysqlSocketPath,
  postgresExec,
  initDbExec,
  nomsBinFormat,
  resultsDir,
  resultsPrefix,
  resultsFilename,
  scriptDir,
  schema,
  outputFormat string,
  defaultRuns int,
  initBigRepo,
  useDoltHubLuaScriptsRepo,
  writeResultsToFile bool,
  queries []string) (string, error) {
  config := benchmark.NewComparisonBenchmarkingConfig(
    fromServer,
    toServer,
    fromVersion,
    toVersion,
    fromProfile,
    toProfile,
    dir,
    doltCommand,
    doltgresCommand,
    mysqlExec,
    mysqlProtocol,
    mysqlSocketPath,
    postgresExec,
    initDbExec,
    nomsBinFormat,
    scriptDir,
    defaultRuns,
    initBigRepo,
    useDoltHubLuaScriptsRepo)
  sr := benchmark.NewSysbenchComparer(config)
  err := sr.Run(ctx)
  if err != nil {
    return "", err
  }
  fromServerConfig, err := config.GetFromServerConfig(ctx)
  if err != nil {
    return "", err
  }
  toServerConfig, err := config.GetToServerConfig(ctx)
  if err != nil {
    return "", err
  }
  resultsDbName := fmt.Sprintf("sysbench-%s", benchmark.ComparisonDbFilename)
  db := benchmark.NewSqlite3ResultsDb(fromServerConfig, toServerConfig, dir, schema, resultsDir, resultsPrefix, resultsFilename, resultsDbName, outputFormat, queries, writeResultsToFile)
  uploadDir, err := db.QueryResults(ctx)
  if err != nil {
    return "", err
  }
  return uploadDir, nil
}

compare 用於將一個數據庫版本與另一個版本的 Sysbench 結果進行比較。從函數的參數可以看出,這個工具不僅用於 Dolt 和 MySQL,還用於對我們最新產品 DoltgreSQL 及其競爭對手 PostgreSQL 進行基準測試。

compare 函數引用 fromServerConfig ,這是 “from” 數據庫服務器的配置,並引用 toServerConfig ,這是 “from” 數據庫服務器的配置 “到” 數據庫服務器。從語義上講,這裏該工具將並排比較 “源” 數據庫和 “目標” 數據庫,以便於分析。在 Dolt 發佈過程中,MySQL 將是 “源” 服務器,Dolt 將是 “目標” 服務器。

您可能還注意到,我們在此工具中使用 sqlite3,如 benchmark.NewSqlite3ResultsDb 所引用,它是 Dolt v1.0.0 之前的遺留工件,但它在這裏仍然具有一些獨特的價值。

在後臺,使用 sr.Run() 運行基準測試後,我們將結果加載到 sqlite3 數據庫中,並對其運行一些查詢以獲得每個數據庫服務器的比較結果。與 Dolt 相比,使用 sqlite3 的一個好處是, sqlite3 使用簡單的標誌以多種格式返回查詢輸出,如 --html ,這使我們不必編寫查詢結果轉換邏輯。

從 db.QueryResults() 返回的 uploadDir 包含比較查詢的結果以及要上傳到 S3 的 sqlite3 數據庫的副本。這些結果很快就會被 “電子郵件團隊” 工作流程下載,我們很快就會看到。

當實際運行 Sysbench 基準測試時, benchmark.NewSysbenchComparer(config) 只是我們在 Dolt 存儲庫中維護的一些基準測試庫代碼中的 Run 函數的包裝結構。

func Run(config *Config) error {
  err := config.Validate()
  if err != nil {
    return err
  }
  ctx := context.Background()
  err = sysbenchVersion(ctx)
  if err != nil {
    return err
  }
  cwd, err := os.Getwd()
  if err != nil {
    return err
  }
  for _, serverConfig := range config.Servers {
    var results Results
    var b Benchmarker
    switch serverConfig.Server {
    case Dolt:
      fmt.Println("Running dolt sysbench tests")
      b = NewDoltBenchmarker(cwd, config, serverConfig)
    case Doltgres:
      fmt.Println("Running doltgres sysbench tests")
      b = NewDoltgresBenchmarker(cwd, config, serverConfig)
    case MySql:
      fmt.Println("Running mysql sysbench tests")
      b = NewMysqlBenchmarker(cwd, config, serverConfig)
    case Postgres:
      fmt.Println("Running postgres sysbench tests")
      b = NewPostgresBenchmarker(cwd, config, serverConfig)
    default:
      panic(fmt.Sprintf("unexpected server type: %s", serverConfig.Server))
    }
    results, err = b.Benchmark(ctx)
    if err != nil {
      return err
    }
    fmt.Printf("Successfuly finished %s\n", serverConfig.Server)
    err = WriteResults(serverConfig, results)
    if err != nil {
      return err
    }
    fmt.Printf("Successfuly wrote results for %s\n", serverConfig.Server)
  }
  return nil
}

此函數根據它看到的服務器類型創建一個 Benchmarker ,然後調用 Benchmark() ,後者針對該服務器運行 Sysbench 測試。下面是 Dolt Benchmarker 的 Benchmark() 實現的示例:

func (b *doltBenchmarkerImpl) Benchmark(ctx context.Context) (Results, error) {
  err := b.checkInstallation(ctx)
  if err != nil {
    return nil, err
  }
  err = b.updateGlobalConfig(ctx)
  if err != nil {
    return nil, err
  }
  testRepo, err := b.initDoltRepo(ctx)
  if err != nil {
    return nil, err
  }
  serverParams, err := b.serverConfig.GetServerArgs()
  if err != nil {
    return nil, err
  }
  server := NewServer(ctx, testRepo, b.serverConfig, syscall.SIGTERM, serverParams)
  err = server.Start(ctx)
  if err != nil {
    return nil, err
  }
  tests, err := GetTests(b.config, b.serverConfig, nil)
  if err != nil {
    return nil, err
  }
  results := make(Results, 0)
  for i := 0; i < b.config.Runs; i++ {
    for _, test := range tests {
      tester := NewSysbenchTester(b.config, b.serverConfig, test, stampFunc)
      r, err := tester.Test(ctx)
      if err != nil {
        server.Stop(ctx)
        return nil, err
      }
      results = append(results, r)
    }
  }
  err = server.Stop(ctx)
  if err != nil {
    return nil, err
  }
  return results, os.RemoveAll(testRepo)
}

在 Benchmark() 調用期間,此實現將檢查 Dolt 安裝,更新一些全局 Dolt 配置,獲取用於啓動 Dolt SQL 服務器的參數,啓動服務器,獲取將要運行的 Sysbench 測試,然後通過調用 tester.Test() 運行這些測試。

完成後,它返回 results 並清除寫入磁盤的內容。

而且,正如我們在內部工具的 compare 功能中所看到的,這些結果被加載到 sqlite3 中並上傳到 S3,以便可以通過電子郵件發送給 DoltHub 團隊。但是,我們仍然缺少一步,即在內部基準測試工具完成上傳結果後,通過 repository_dispatch 事件觸發 “電子郵件團隊” 工作流程。

因此,我們內部工具的最後一部分包括:

err := d.DispatchEmailReportEvent(ctx, *toVersion, *nomsBinFormat, *bucket, key)
if err != nil {
    log.Fatal(err)
}

DispatchEmailReportEvent() 方法位於我們編寫的 Dispatcher 接口上。它只是向 GitHub Actions Workflow REST API 發出 HTTP 請求,該 API 發出 repository_dispatch 事件,觸發 “電子郵件團隊” 工作流運行。那麼讓我們接下來看看。

與 “部署 K8s Sysbench 基準測試作業” 工作流程一樣,“電子郵件團隊”工作流程除了 Dolt 發佈流程之外還被多個流程使用,因此這就是我們使用 repository_dispatch 事件觸發它的原因。工作流程文件如下:

name: Email Team Members
on:
  repository_dispatch:
    types: [ email-report ]
jobs:
  email-team:
    runs-on: ubuntu-22.04
    name: Email Team Members
    steps:
      - uses: actions/checkout@v3
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2.2.0
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-west-2
      - name: Get Results
        id: get-results
        run: aws s3api get-object --bucket="$BUCKET" --key="$KEY" results.log
        env:
          KEY: ${{ github.event.client_payload.key }}
          BUCKET: ${{ github.event.client_payload.bucket }}
      - name: Get Addresses
        id: get-addresses
        run: |
          addresses="$TEAM"
          if [ ! -z "$RECIPIENT" ]; then
            addresses="[\"$RECIPIENT\"]"
          fi
          echo "addresses=$addresses" >> $GITHUB_OUTPUT
        env:
          RECIPIENT: ${{ github.event.client_payload.email_recipient }}
          TEAM: '["${{ secrets.PERF_REPORTS_EMAIL_ADDRESS }}"]'
      - name: Send Email
        uses: ./.github/actions/ses-email-action
        with:
          template: ${{ github.event.client_payload.template }}
          region: us-west-2
          version: ${{ github.event.client_payload.version }}
          format: ${{ github.event.client_payload.noms_bin_format }}
          toAddresses: ${{ steps.get-addresses.outputs.addresses }}
          dataFile: ${{ format('{0}/results.log', github.workspace) }}

如圖所示,此工作流程的摘要是下載 Dolt 版本的 Sysbench 結果,然後通過電子郵件將其發送給我們的團隊;沒什麼瘋狂的。

這就是 Dolt 的發佈過程。或者這就是 Dolt 的發佈過程。現在我將回顧一下我們如何更新此流程以在發佈時開始構建 Dolt 的 pgo 二進制文件。

PGO 通過 GitHub Actions 發佈

對於那些不熟悉 pgo 構建的人,他們需要 -pgo 標誌以及在 go build 命令期間提供的 Golang 配置文件的路徑。這部分其實很簡單。但在此之前,您需要創建要用於優化構建的配置文件,這需要我們更新一些基準庫代碼和內部工具代碼,以便它們既可以生成配置文件並接受配置文件作爲輸入。讓我更詳細地解釋一下。

在我們的基準測試庫代碼中,我們使用另一個名爲 dolt_builder 的 Dolt 實用程序來實際從源代碼構建 Dolt 二進制文件。要使用此工具,您只需提供要從中構建 Dolt 的提交 SHA 或標記,它就會爲您構建它。因此,我們在很多地方使用這個工具來輕鬆地同時構建多個版本的 Dolt。

因此,我們做的第一件事就是更新這個工具以接受可用於構建 Dolt 的 Golang 配置文件:

// goBuild builds the dolt binary and returns the path to the binary
func goBuild(ctx context.Context, source, dest, profilePath string) (string, error) {
  goDir := filepath.Join(source, "go")
  doltFileName := "dolt"
  if runtime.GOOS == "windows" {
    doltFileName = "dolt.exe"
  }
  args := make([]string, 0)
  args = append(args, "build")
  if profilePath != "" {
    args = append(args, fmt.Sprintf("-pgo=%s", profilePath))
  }
  toBuild := filepath.Join(dest, doltFileName)
  args = append(args, "-o", toBuild, filepath.Join(goDir, "cmd", "dolt"))
  build := ExecCommand(ctx, "go", args...)
  build.Dir = goDir
  err := build.Run()
  if err != nil {
    return "", err
  }
  return toBuild, nil
}

我們做的下一件事是更新基準庫代碼以在 “分析” 模式下運行。在默認模式下,如上所述,此代碼調用 Benchmark() 並返回結果。在新的 “分析” 模式中,代碼在 Profiler 接口上調用 Profile() :

...
    case Dolt:
      // handle a profiling run
      sc, ok := serverConfig.(ProfilingServerConfig)
      if ok {
        if string(sc.GetServerProfile()) != "" {
          fmt.Println("Profiling dolt while running sysbench tests")
          p := NewDoltProfiler(cwd, config, sc)
          return p.Profile(ctx)
        }
      }
...

Profile() 的工作方式與 Benchmark() 類似,但會創建一個在運行 Sysbench 基準測試時獲取的 golang 配置文件。這使我們能夠輕鬆生成可在新發布流程中使用的 Dolt 配置文件。

我們還更新了此庫代碼以接受配置文件作爲輸入。這樣我們就可以向它提供一個配置文件,它又將其提供給 dolt_builder 以創建 pgo 二進制文件,然後運行 Sysbench 並輸出這些結果。

爲了澄清,我們基本上更新了這個庫代碼,以便我們可以在一種模式下運行它來生成 Golang 配置文件,然後在默認模式下運行它以獲得正常的基準測試結果,但它也接受 Golang 配置文件作爲輸入,並用它來構建 Dolt 和 go build -pgo 。希望這對你來說有意義,因爲對我來說描述有點棘手🤠。

接下來,我們需要更新使用所有這些庫代碼的內部工具,以擁有 “分析” 模式並接受 Golang 配置文件作爲輸入。我們對新發布流程的計劃是在分析模式下運行內部工具一次,以創建 Golang 配置文件。然後,在默認模式下再次運行內部工具,但向其提供 Golang 配置文件,這將針對 pgo 構建的 Dolt 生成基準測試結果。

因此,與 compare 函數一樣,我們能夠將 profile 函數添加到生成 Dolt 版本的 Golang cpu 配置文件的內部工具中。

func profile(ctx context.Context, dir, profileDir, resultsDir, resultsPrefix, version, profile, doltCommand, scriptsDir string, useDoltHubLuaScriptsRepo bool) (string, error) {
  config := benchmark.NewProfilingConfig(
    dir,
    profileDir,
    version,
    profile,
    doltCommand,
    scriptsDir,
    useDoltHubLuaScriptsRepo)
  toUpload := filepath.Join(resultsDir, resultsPrefix)
  sr := benchmark.NewSysbenchProfiler(config, toUpload, profileDir)
  return toUpload, sr.Run(ctx)
}

此函數像 compare 一樣返回其 toUpload 目錄,但這次它包含要上傳到 S3 的配置文件。

對代碼進行這些更改後,我們準備更新 GitHub Actions 工作流程以開始創建 Dolt 的 pgo 版本。下面的圖表顯示了使用 GitHub Actions 的新 Dolt 發佈流程。

從新的發佈工作流程圖中可以看到,我們添加了一些新的 GitHub Actions 工作流程,但它們與原始工作流程類似。讓我們更仔細地看看它們。

對於新的 Dolt 發佈流程,我們運行的第一個工作流程稱爲 “發佈 Dolt(配置文件)”,實際上並不創建 GitHub 版本或構建任何 Dolt 二進制文件。

相反,它的唯一功能是觸發第二個工作流程,稱爲 “部署 K8s Sysbench 分析作業”。

name: Release Dolt (Profile)
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'SemVer format release tag, i.e. 0.24.5'
        required: true
jobs:
  format-version:
    runs-on: ubuntu-22.04
    outputs:
      version: ${{ steps.format_version.outputs.version }}
    steps:
      - name: Format Input
        id: format_version
        run: |
          version="${{ github.event.inputs.version }}"
          if [[ $version == v* ]];
          then
            version="${version:1}"
          fi
          echo "version=$version" >> $GITHUB_OUTPUT
  profile-benchmark-dolt:
    runs-on: ubuntu-22.04
    needs: format-version
    name: Trigger Benchmark Profile K8s Workflows
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main
      - name: Get sha
        id: get_sha
        run: |
          sha=$(git rev-parse --short HEAD)
          echo "sha=$sha" >> $GITHUB_OUTPUT
      - uses: peter-evans/repository-dispatch@v3
        with:
          token: ${{ secrets.REPO_ACCESS_TOKEN }}
          event-type: profile-dolt
          client-payload: '{"from_version": "${{ steps.get_sha.outputs.sha }}", "future_version": "${{ needs.format-version.outputs.version }}", "mode": "release", "actor": "${{ github.actor }}", "actor_email": "dustin@dolthub.com", "template_script": "./.github/scripts/performance-benchmarking/get-dolt-profile-job-json.sh"}'

“部署 K8s Sysbench 分析作業”的工作原理與 “部署 K8s Sysbench 基準測試作業” 幾乎相同,不同之處在於它將以 “分析” 模式運行的基準測試作業部署到 K8s 集羣,以便我們使用 HEAD 的 。

name: Profile Dolt while Benchmarking
on:
  repository_dispatch:
    types: [ profile-dolt ]
jobs:
  performance:
    runs-on: ubuntu-22.04
    name: Profile Dolt while Benchmarking
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - uses: azure/setup-kubectl@v4
        with:
          version: 'v1.23.6'
      - name: Install aws-iam-authenticator
        run: |
          curl -o aws-iam-authenticator https://amazon-eks.s3.us-west-2.amazonaws.com/1.18.8/2020-09-18/bin/linux/amd64/aws-iam-authenticator && \
          chmod +x ./aws-iam-authenticator && \
          sudo cp ./aws-iam-authenticator /usr/local/bin/aws-iam-authenticator
          aws-iam-authenticator version
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-west-2
      - name: Create and Auth kubeconfig
        run: |
          echo "$CONFIG" > kubeconfig
          KUBECONFIG=kubeconfig kubectl config set-credentials github-actions-dolt --exec-api-version=client.authentication.k8s.io/v1alpha1 --exec-command=aws-iam-authenticator --exec-arg=token --exec-arg=-i --exec-arg=eks-cluster-1
          KUBECONFIG=kubeconfig kubectl config set-context github-actions-dolt-context --cluster=eks-cluster-1 --user=github-actions-dolt --namespace=performance-benchmarking
          KUBECONFIG=kubeconfig kubectl config use-context github-actions-dolt-context
        env:
          CONFIG: ${{ secrets.CORP_KUBECONFIG }}
      - name: Create Profile Benchmarking K8s Job
        run: ./.github/scripts/performance-benchmarking/run-benchmarks.sh
        env:
          PROFILE: "true"
          FUTURE_VERSION: ${{ github.event.client_payload.future_version }}
          FROM_VERSION: ${{ github.event.client_payload.from_version }}
          MODE: ${{ github.event.client_payload.mode }}
          ACTOR: ${{ github.event.client_payload.actor }}
          ACTOR_EMAIL: ${{ github.event.client_payload.actor_email }}
          REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
          KUBECONFIG: "./kubeconfig"
          INIT_BIG_REPO: ${{ github.event.client_payload.init_big_repo }}
          NOMS_BIN_FORMAT: "__DOLT__"
          TEMPLATE_SCRIPT: ${{ github.event.client_payload.template_script }}

一旦基準測試 K8s 作業在 “分析” 模式下運行,我們就可以在更新的圖表中看到它執行的步驟。我們還看到此作業的輸出是一個新的 Golang 配置文件,已上傳到 S3,可供我們流程的其餘步驟用於創建 pgo 構建。

在分析 K8s 作業結束時,上傳配置文件後,它會觸發 “Release Dolt” 工作流程。這個工作流程的工作原理與原來的 “Release Dolt” 工作流程基本相同,只是它的第一步是下載分析作業上傳的 Golang 配置文件。

...
  create-pgo-release:
    needs: format-version
    runs-on: ubuntu-22.04
    name: Release PGO Dolt
    outputs:
      release_id: ${{ steps.create_release.outputs.id }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main
      - name: Set up Go 1.x
        uses: actions/setup-go@v5
        with:
          go-version-file: go/go.mod
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-west-2
      - name: Get Results
        id: get-results
        run: aws s3api get-object --bucket="$BUCKET" --key="$KEY" dolt-cpu-profile.pprof
        env:
          KEY: ${{ github.event.inputs.profile_key || github.event.client_payload.profile_key }}
          BUCKET: ${{ github.event.inputs.profile_bucket || github.event.client_payload.bucket }}
...

然後,它將下載的配置文件(此處稱爲 dolt-cpu-profile.pprof )提供給 buildbinaries.sh 腳本,該腳本運行 go build -pgo=./dolt-cpu-profile.pprof ,編譯新的 Dolt 二進制文件。然後,與工作流程的原始版本一樣,它創建一個 GitHub 版本並將這些二進制文件作爲版本資產上傳。

在完成之前,此工作流程中的最後一項作業將啓動另一項基準測試 K8s 作業,只是這次爲該作業提供用於構建 Dolt 二進制文件的 Golang 配置文件的 S3 密鑰。

...
  trigger-performance-benchmark-email:
    needs: [format-version, create-pgo-release]
    runs-on: ubuntu-22.04
    steps:
      - name: Trigger Performance Benchmarks
        uses: peter-evans/repository-dispatch@v3
        with:
          token: ${{ secrets.REPO_ACCESS_TOKEN }}
          event-type: release-dolt
          client-payload: '{"version": "${{ needs.format-version.outputs.version }}", "actor": "${{ github.actor }}", "profile_key": "${{ github.event.inputs.profile_key || github.event.client_payload.profile_key }}"}'

這會再次將基準測試作業部署到我們的 K8s 集羣,但現在該作業將從 S3 下載 Golang 配置文件,並使用它構建 Dolt 的 pgo 二進制文件,以用於基準測試和生成結果。

從 K8s 上下文中的圖表中我們可以看到,第二個基準測試作業的最後一步啓動了 “電子郵件團隊” 工作流程,以便我們的團隊獲得現在 pgo 的 Dolt 的基準測試結果。

我們已經做到了!我們現在正在發佈 Dolt 的 pgo 版本。

結論

正如您所看到的,更新發布過程以生成 pgo 二進制文件涉及一些複雜性,至少對我們來說是這樣。但我們所看到的性能提升絕對值得付出努力。

我希望您發現這對您自己的努力有幫助,我們鼓勵您也嘗試更新您的版本。如果您願意,我們很想聽聽。過來並在我們的 Discord 上分享您的經驗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zPg5JkAJRhTpk7YhM8YmlQ