優化 Rust 構建的策略及與 GitHub Actions 合併
爲什麼 Rust 的構建時間很慢?
Rust 是一種高性能且對開發人員友好的編程語言,Rust 因其安全性、速度、併發性和可編程性而在編程界獲得了關注和讚譽。它也被證明是構建企業應用程序的絕佳選擇。與 C/C++ 等語言相比,Rust 對於項目來說更容易上手,因爲 C/C++ 不那麼內存安全,開發人員需要多年的經驗才能開始工作,因此 Rust 被證明是一個更好的選擇。
近年來,它也是開發社區中最受歡迎的編程語言,因爲它爲編程語言做出了創新和高效的設計,例如零成本抽象和所有權,關注性能而不以可編程性爲代價。
由於語言設計的本質,Rust 構建 / 編譯時間相當慢,可能會阻礙開發人員的生產力:引入緩慢的反饋循環,作爲編譯時間的直接產物。下面的漫畫總結了編譯時間的問題,並且與這個場景非常相關。開發人員等待代碼編譯的時間越長,他們在產品上的工作就越少。這將影響整個發佈時間,並可能在整個產品發佈過程中造成蝴蝶效應。
爲了幫助緩解這個問題,這篇文章將介紹各種優化 Rust 構建時間的策略,然後將這些策略與 GitHub Actions 合併。這將使 Rust 開發人員能夠更快地進行迭代,從而幫助他們高效地完成項目。
Rust 中緩慢構建速度的蝴蝶效應
緩慢的構建會在幾個方面顯著影響開發速度和生產力。在時間至關重要的企業項目中,這可能會對團隊的工程習慣、發佈節奏和未來的產品規劃產生重大影響。如果應用程序構建太慢且不允許開發人員快速迭代,則會有以下影響:
更長的反饋循環
緩慢的構建導致更長的反饋循環。開發人員必須等待構建過程完成,然後才能測試更改或接收自動測試結果。如果使用的是 Uffizzi,那麼在這種情況下,構建和部署拉取請求的預覽環境也可能需要更長的時間。
緩慢的構建時間 (平均 20 分鐘) 導致上下文切換到其他任務也需要等待。這破壞了開發流程。當在構建和測試之後回到開發過程時,開發人員必須花一點額外的時間重新構建代碼庫,然後在程序上進行重複這個過程。
阻礙合作
由於較長的反饋循環,工程團隊成員之間可能會出現脫節。這是由於單個開發人員自己花費了更長的時間進行開發,導致整個團隊的速度比他們應該的要慢。在 sprint 中,同行之間共享的知識並不多,這減少了協作,減緩了產品的增長。
減少部署頻率
由於沒有及時發佈足夠的 bug 修復和特性,因此降低了總體部署頻率。如果客戶一直在等待某些錯誤修復或特定功能,這將直接影響客戶滿意度。業務敏捷性也會受到影響,因爲業務本身不能足夠快地達到設定的目標,並向客戶提供理想的產品。反過來,作爲研發和發佈週期緩慢的直接產物,影響了企業對市場變化的反應能力。緩慢的發佈週期意味着新功能得不到足夠快的反饋。
降低代碼質量
當構建花費很長時間時,開發人員在發佈當天就會被時間所束縛,並且可能不太傾向於編寫好的代碼,從而導致低質量的代碼合併。這可能導致總體上較低的代碼質量,並增加引入錯誤或迴歸的可能性。
增加 CI/CD 成本
產品構建時間直接影響 CI/CD 成本。通過在構建過程中減少幾分鐘的時間,可以節省很多錢。這對於擁有大型項目或多個應用程序的企業來說尤其成問題,因爲 CI/CD 成本影響會以數量級增加。
優化 Rust 應用程序構建的策略
以下優化 Rust 構建的策略各有利弊,由用戶決定在他們的構建用例中什麼最適合。用戶需要考慮他們正在優化的構建是開發、發佈、測試還是其他構建。找出每種構建優化的正確組合有助於用戶順利開發和發佈。
Rust 應用程序的發佈構建往往比開發人員構建要慢得多。這是由於編譯器在發佈構建期間進行了優化,使應用程序的二進制文件儘可能最小。最後,用戶必須決定自己的構建優化選擇。以下是優化 Rust 應用程序構建的策略。這些策略可以相互配合使用:
有效的緩存利用率
緩存是最直接的,也是加快構建時間最關鍵的。通過緩存 target 目錄和 cargo registry,可以顯著減少編譯依賴項所花費的時間。
-
緩存 target 目錄:該目錄包含構建的構件,緩存它將節省後續構建的時間。
-
緩存 cargo registry:這確保依賴項不會被不必要地重新下載或重新編譯。
對於上述緩存配置,可以使用流行的 https://github.com/Swatinem/rust-cache 來簡化爲 Rust 應用程序構建設置和使用緩存的過程。
- name: Cache dependencies
uses: Swatinem/rust-cache@v2.2.1
在介紹了上面的基本依賴項緩存之後,可以使用更智能的緩存 sccache 作爲編譯器緩存工具。它充當編譯器包裝器,並儘可能避免編譯。在這種情況下,確保我們不僅緩存了依賴項,而且還緩存不需要在每次構建時重新編譯的編譯時構件。
- name: Configure sccache
run: |
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
echo “SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
上面的一組 github action 設置了 sccache 環境變量,其中 RUSTC_WRAPPER 指示要使用哪個編譯器包裝器,SCCACHE_GHA_ENABLED 設置 sccache 以使用 github action 緩存。
要了解有關 sccache 的更多信息,請查看 https://github.com/mozilla/sccache/
並行編譯
Rust 支持開箱即用的並行編譯,這允許利用多核處理器的強大功能,以成倍地加快構建過程。要啓用並行編譯,請在 config.toml 中設置 codegen-units 選項。
[profile.dev]
codegen-units = 1
代碼單元或代碼生成單元是將代碼分成幾個部分,以便並行地對每個部分執行編譯,這將大大提高編譯速度。這樣做的缺點是,如果代碼沒有被分解和逐塊編譯,代碼將無法得到優化。
增加代碼單元的數量可能會導致錯過一些潛在的優化,但可以通過將該值設置爲 1 來優化運行時性能。這意味着代碼庫將被視爲單個代碼片段,並且不會進行並行編譯。
[profile.release]
codegen-units = 1
覆蓋配置文件
Rust 中的構建系統預定義了一組配置選項,這些集合稱爲 profiles。默認情況下,Rust 爲不同的目的使用不同的構建配置文件。
例如,在開發過程中構建項目時將使用 dev 配置文件。此配置文件優先考慮更快的構建時間,並啓用損害性能的調試語句。要使用 dev 配置文件進行構建,請在命令行中運行 cargo build。該命令不需要任何標誌來指定這是一個開發構建,因爲這是默認的構建選項。
release 的目的是在向外界發佈應用程序的最終版本時使用。因此,自然地,這個 release 以較慢的編譯時間爲代價,優先考慮生成二進制文件的速度。要使用發佈配置文件進行構建,只需在項目的根目錄中使用 cargo build --release。
可以根據用戶的需要,通過向 config.toml 添加配置來覆蓋這些默認配置文件。例如,要降低 release 的優化級別,可以配置以下內容:
[profile.release]
opt-level = 2
codegen-units = 16
上面的配置將 opt-level(優化級別) 從 3(默認值) 減少到 2。
opt-level 是一個編譯器設置,它控制優化過程中應用的級別,其中級別用數字表示。以下是設置及其含義:
-
opt-level = 0:沒有優化。此設置優先考慮快速編譯時間,使其適合於開發和調試,並犧牲運行時性能。
-
opt-level = 1:基本優化。在編譯速度和運行時性能之間提供平衡,這對開發期間的增量構建很有好處。
-
opt-level = 2:更高級別的優化。以較慢的編譯時間爲代價,提高生成的二進制文件的運行時性能,用於版本構建,優化程度略低於最佳水平。
-
opt-level = 3:最高級別的優化。着重於最大化所生成二進制文件的性能。結果導致編譯時間明顯變慢,並且由於激進的優化而使調試變得困難。
除了設置 opt-level 之外,代碼單元設置增加到 16,允許在編譯期間進行更多的並行化。
在 Github Actions 中應用配置,允許更快的發佈構建以及有效的緩存利用率
考慮這樣一個項目,它需要構建短暫預覽優化版本。這個構建必須比通常的 Rust release 構建更快地完成,並且不需要完全優化,以便更快地創建 Rust 應用程序二進制文件,然後可以在短暫的環境中用於測試。
Rust 構建配置
考慮到上述情況,優化級別可以降低,不必是最高的,所以我們可以將 opt-level 設置爲 2 而不是 3(默認值)。考慮到我們希望構建仍然快一點,讓我們通過將 codegen-units 設置爲 4 來應用一些並行編譯。對於臨時環境構建來說,這是一個很好的配置,但創建自定義配置文件更有意義。
要創建自定義配置文件,讓我們向 Cargo.toml 添加以下內容,它將創建一個名爲 ephemeral-build 的新構建配置文件,其中包含我們需要的配置。
[profile.ephemeral-build]
opt-level = 2
codegen-units = 8
要使用臨時構建配置文件,必須將其設置爲在進行發佈構建時使用的默認配置文件。這可以通過爲 Rust 設置 --cfg 標誌來完成,通過一個環境變量 RUSTFLAGS 導出該標誌及其相關值,該變量將在運行時讀取。
RUSTFLAGS=”--cfg profile=ephemeral-build” cargo build --release
Dockerfile 配置
以最可移植的方式發佈應用程序的最佳方式是通過容器映像。下面的 Dockerfile 只接受構建應用程序的二進制文件,考慮到緩存優化是在 Github Actions 中完成的,image 構建器不必再擔心構建和緩存了。所有需要做的就是將二進制文件複製到 image 中,然後 image 就可以使用了,非常簡單。
FROM alpine:latest
RUN apk update --quiet \
&& apk add -q --no-cache libgcc tini curl
COPY target/x86_64-unknown-linux-musl/release/app /bin/app
RUN ln -s /bin/app /app
ENTRYPOINT ["app"]
在上面的配置中,在必要的包更新之後,只有二進制文件被複制,然後進行符號鏈接以獲得更好的訪問。
Github Actions 配置
可以一起使用上述所有配置來創建應用程序映像的構建管道。構建管道通過使用前面提到的策略進行了優化,並且還生成了包含應用程序二進制文件的容器映像。下面是 Github Action 管道的樣子:
name: Rust application ephemeral environment build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
target: x86_64-unknown-linux-musl
- name: Configure sccache env var and set build profile to ephemeral build
run: |
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
echo “SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
echo “RUSTFLAGS=’--cfg profile=ephemeral-build’” >> $GITHUB_ENV
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
- name: Run build
uses: actions-rs/cargo@v1
with:
command: build
args: --target x86_64-unknown-linux-musl --release
-
在上面啓動管道時,發生的第一件事是 Checkout 存儲庫。
-
下一步將安裝 Rust。在這裏,x86_64-unknown-linux-musl 目標用於安裝和構建,對於我們最終的容器映像構建,我們使用的基本映像是 alpine:latest,爲了讓我們的應用程序在 alpine 容器中運行,我們需要將其構建到 MUSL 目標。
-
設置必要的環境變量
-
使用 sccache 用作 Rust 編譯器包裝器,RUSTC_WRAPPER 使用 Github Actions 緩存。
-
RUSTFLAGS 用於設置在進行發佈構建時使用 ephemeral-build 進行構建。
上面的 Github Actions 配置優化了 Rust 應用程序的構建,專門用於臨時環境。最終的應用程序構建經過了足夠的優化,可以很容易地進行測試,並且構建得足夠快,從而不會在迭代之間花費太多時間。這對於短暫的環境構建來說是完美的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_yrna33VMTyxOHVG09lW1Q