🏎

PGOでRustで書かれた広告サーバを早くする

2022/12/15に公開

これはRust Advent Calendar 2022の15日目の記事です。

TL;DR

  • Mapboxの広告サーバにPGOを適用したらスループットが最大8%向上した
  • cargo-pgoすごい便利
  • BOLTは試したみたけど失敗した

PGOとは?

PGO(Profile Guided Optimization)は、プログラムを実行してプロファイラが収集した情報を基にインライン化、コードレイアウト、レジスタ割り当てを行う最適化手法です。

英語版WikipediaにしかPGOのページ[1]がなかったので意訳してみます。

つまり、PGOはコンパイラがコンパイル・リンクを終えた後のバイナリを動かしてプロファイリングし、最適化、再ビルドによって性能UPしたバイナリを作ることができます。Mozillaは自社のclangバイナリに対してPGOを使ったところ、Firefoxのコンパイル速度が9%とまで向上することに成功しました[2] また、RustのコンパイラチームはPGOを使ってrustcのコンパイル性能を最大5%向上させることに成功しました[3]

一般的によくデザインされチューニングされたソフトウェアは、かなり大きなボトルネック解消やアーキテクチャレベルの変更をしないと9%も性能向上することは難しいです。コードの変更による工数やバグのリスクなしに9%も性能向上できればかなり嬉しいのではないでしょうか?

RustにおけるPGO

Rust BookにはProfile-guided Optimizationのページが用意されており、Rustで作ったバイナリに対して割と簡単にPGOを適用できるようになっています。

日本語で書かれている情報としては、rhysdさんがRustでProfile-Guided Optimizationやってみたという記事を書いてくれています。記事中ではRustで実装したWasmインタプリタの性能が0~10%向上したとのことで、解説もとても分かりやすいので参考にしてみてください。

今回はcargo-pgoを使ってPGOしてみたいと思います。cargo-pgoはPGOのためコンパイラオプションやプログラム実行をコマンドラインだけでできるようになっており大変便利です。また、後述するBOLTにも対応しています。2022年12月現在でRustでPGOを試したい場合はcargo-pgoを使いましょう。

https://github.com/Kobzol/cargo-pgo

Mapboxの広告サーバ

Mapboxはデジタル地図上に広告を配信するプラットフォームを開発しています[4][5] 広告を配信するサーバは速度と安定性が求められることから、Rustで書かれています。今回はこの広告サーバをPGOを使ってどのくらい早くなるか試してみます。(↓をクリックするとMapboxの広告プラットフォームのサイトへ移動します)

広告バックエンドのアーキテクチャはざっくりいうとこんな感じになっています。

Mapboxの広告サーバは以下の特徴があります。

  • 広告データやオークション用のデータは全てプロセス上のオンメモリにロードしている
  • そのため、Loggingやクライアントとのやりとりを除いてバックエンドの担う処理の多くはCPU Boundになる
  • クライアントからの地図1タイルリクエスト毎にRealtimeオークションを実施し、タイルに表示するピンを決定する。オークションはそこそこ重たい処理
  • 最終的な結果はGeoJSONでクライアントに返す。GeoJSONはProtobuf等と比べて(De)Serializeは遅い

計測環境

  • AWS EC2 x2
    • Amazon Linux 2 / Linux Kernel: 5.10
    • Linux Kernel: 5.10
    • 4 vCPU / 16GB RAM
  • rustc 1.65

同じVPC内に二つのEC2インスタンスを立て、Host①に広告サーバを起動し、Host②でHTTP load testerを実行しリクエストを送りスループットを計測します。

Setup

Rustのツールチェインのインストール

rustc、cargo-pgollvm-tools-previewをインストールする

sudo yum udpate -y
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.bashrc
rustup component add llvm-tools-preview
cargo install cargo-pgo

計測用のツールのインストール

heyというHTTPロードテスターを使います。

sudo yum install -y htop
wget https://hey-release.s3.us-east-2.amazonaws.com/hey_linux_amd64
mv hey_linux_amd64 $HOME/.local/bin/hey
chmod +x $HOME/.local/bin/hey

計測サンプル

以下の3つのケースを計測してみます

  • Release: PGOなしのReleaseビルドバイナリ
cargo build --release
  • PGO(Test): cargo-pgoでユニットテストを実行してPGOビルドをする
cargo pgo test
cargo pgo optimize
  • PGO(Bench): 広告サーバを起動し、heyで負荷をかけた後にcargo-pgoでPGOビルドをする
cargo pgo run
# heyで負荷をかける
cargo pgo optimize

計測結果

heyで広告サーバに10万回リクエストを20セット送りスループット(RPS)を計測しました。

単位はRPS(Request per Second)

avg min max
Release 11,528.13 10,289.82 12,406.1
PGO(Test) 11,121.73 (-3.53%) 10,331.65 (0.41%) 👍 11,387.63 (-8.21%)
PGO(Bench) 12,435.22 (7.87%) 👍 10,324.45 (0.34%) 👍 13,004.10 (4.82%) 👍

結果はなんとユニットテストでPGOした場合逆にスループットが悪くなってました 😱 Wikipediaに計測データが悪いと逆に遅くなることがあると書いてあったのはこういうことでしょうか。ちなみに広告サーバのテストコードはテストの正常系・異常系を網羅するために書いているで、APIのベンチマークは含まれていません。

heyで負荷をかけてPGOビルドしたケースでは約8%も性能向上することができました*🎉

とても良い結果が得られたので、DockerイメージのビルドパイプラインにPGOビルドを含めたいと思います。ただ、今回はスタンドアロンで広告サーバを起動しheyで負荷をかけたのですが、Dockerビルド上でプロセスを複数立ち上げたりは正直あまりしたくないのでcargo benchを使ったPGOビルドで今回と同様な結果を出せるか継続して調査していきたいと思います。

番外編: BOLTを試してみる

cargo-pgoはBOLTも対応しているので試してみました。BOLTとはMeta社が開発して後にllvmにマージされたバイナリ最適化ツールで、PGOよりもさらにアグレッシブな最適化をできるようです。PGOとBOLT両方で最適化を適用することもできるようです。

BOLTのDependencyのインストール

BOLTはLinuxで動くバイナリで提供されていない?っぽいのでソースからビルドする必要があります。BOLTを使わない場合はここはスキップしてもOK。

sudo yum udpate -y
sudo yum install gcc git vim htop ninja-build gcc-c++

cmakeが古いのでcmakeをソースからビルドする

wget https://github.com/Kitware/CMake/releases/download/v3.25.1/cmake-3.25.1.tar.gz
./bootstrap --prefix=$HOME/.local
tar zxvf cmake-3.25.1.tar.gz
cd cmake-3.25.1
./bootstrap --prefix=$HOME/.local
make -j2
make install

llvmをソースからビルドする (ビルドに数時間かかります)

git clone https://github.com/llvm/llvm-project
cd llvm-project
git checkout llvmorg-14.0.5
cmake -S llvm -B build -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX=$HOME/.local \
  -DLLVM_ENABLE_PROJECTS="clang;lld;compiler-rt;bolt"
cd build
ninja-build install

で、cargo-pgoでInstrumentedバイナリを作って起動したところ、curlでリクエストを送ると広告サーバはJSONのパース部分でpanicしてしまいました😅 リクエストの代わりにテストコードを実行したり、llvmを最新のHEADを取ってきて試してもpanicしてしまったのでこれ以上出来ませんでした。誰かBOLTで上手くいった人がいたらぜひご報告お願いします。

脚注
  1. https://en.wikipedia.org/wiki/Profile-guided_optimization ↩︎

  2. https://bugzilla.mozilla.org/show_bug.cgi?id=1326486 ↩︎

  3. https://blog.rust-lang.org/inside-rust/2020/11/11/exploring-pgo-for-the-rust-compiler.html ↩︎

  4. https://internet.watch.impress.co.jp/docs/news/1345962.html ↩︎

  5. https://www.mapbox.jp/news/newsrelease-20221005 ↩︎

GitHubで編集を提案

Discussion