2024年11月10日日曜日

MySQL 8.0 の速いバイナリを作ってみよう

念を押しておきますが、このブログの「内容は個人の考えであって、所属組織とは方針が異なる」と考えてください。

前のエントリでは、MySQL 8.0は、clangのPGO+LTOでビルドしないと本来の性能が出ない。ということを証明しました。その後、PGO+LTOといってもプロファイリングをどうしたらいいのかと、デスクトップマシンの空き時間でひたすらビルドとtpcc(ramfs)を繰り返した結果、興味深いことがわかりました。

tpccのようなある程度複雑なベンチマークは、
ベンチマークそのもの(この場合tpcc)をプロファイリングするよりも、
mysql-testのスクリプトを組み合わせて工夫したほうが性能が出る

ということです。(少なくとも私の環境ではですが)

つまり、
ビルドしてテストスクリプトが流せる環境であれば、総合的に最適に近いバイナリが生成できるということです。誰でもビルドできます。多分。しかも、公開ソースツリーだけでです。
(何故本家がそういう最適化バイナリを配布しないかもよくわからないですね。配布版が遅いビルドだとMySQL自体のプレゼンスが下がると思うのですが…)

このまま放って置いても8.0のCommunity Editionの最速バイナリが公式提供されるか怪しいので、それに近いものを作るための手順を公開しようと思います。大人の事情で自分ビルドを直接使えない人も話題に上げることで公式提供が早まるかもしれないので、色々試して皆で話題にしてみたり、要望してみたりしても…


clang PGO+LTO ビルドのテストもしてみましたが(私の環境での)唯一の違いは、ソース中の"__FILE__"シンボルの展開にパスが含まれない(ファイル名のみ)ことです。何が起こったかというと、performance_schema.error_log の subsystem列 で"Repl"となるべきものが"__FILE__"のパース(コンパイル時)違いで"Server"になってしまうエラー出力がある。という程度でした。本質的な問題にはならなそうです。(エラー出力のsubsystem判断の一部はパスの区切り文字が無いと、__FILE__からのbasename抽出ができないみたい。バグとして報告済みなのでいつか治るでしょう…)

最適に近いビルドの手順を説明する前に、練習としてまず、clangで普通のビルドをどうするかの説明をします。その中で、どのようなオプションを使うか決めてください。それを踏まえて、(現状私の環境で暫定ベストの)最適化ビルドの説明をします。今回はLinux(x86_64) clang環境だけで、それ以外の環境では事情が異なるかもしれませんが似た結論になると予想します。


練習: clangでノーマルビルド

まず練習として、用途に必要な機能を含むように普通のビルドをclangでできるようにしましょう。汎用的にするために、できるだけ Community Edition の配布バイナリと機能同等なビルド想定からスタートします。
githubの8.0ブランチのルートディレクトリをカレントに始めます。

#clang でビルドできるようにします。一応私の環境は clang13 です。
export CC=clang
export CXX=clang++

#とりあえず、64bitアーキテクチャ汎用で。お好みでアーキテクチャ限定してみても速くなるかも。
export CFLAGS="-O2 -g -pipe -m64 -mtune=generic"
export CXXFLAGS="-O2 -g -pipe -m64 -mtune=generic"

#私の場合です。git cleanとかでどうせ全部消せるので、ビルドを整理して分けたりしません。
#次の本番のやり方にも多少影響するのでとりあえすこれで。
cmake . -DFORCE_INSOURCE_BUILD=1 \
        -DBUILD_CONFIG=mysql_release \
        -DINSTALL_LAYOUT=STANDALONE \
        -DFEATURE_SET=community \
        -DPLATFORM=linux-custom \
        -DWITH_ROUTER=OFF \
        -DWITH_AUTHENTICATION_LDAP=ON \
        -DWITH_AUTHENTICATION_FIDO=ON \
        -DWITH_AUTHENTICATION_KERBEROS=ON \
        -DWITH_CURL=system \
        -DWITH_TIRPC=bundled \
        -DWITH_NUMA=ON \
        -DWITH_BOOST=~/boost_1_77_0 \
        -DCMAKE_INSTALL_PREFIX=/opt/mysql-8.0 \
        -DMYSQL_UNIX_ADDR=/opt/mysql-8.0/mysql.sock
-DWITH_BOOST=~/boost_1_77_0
必要なバージョンのboostのソースを展開して指定してください。ビルドはしなくていいです。
-DCMAKE_INSTALL_PREFIX=/opt/mysql-8.0
インストール先を指定します。この場合省略すると /usr/local/mysql
-DMYSQL_UNIX_ADDR=/opt/mysql-8.0/mysql.sock
デフォルトUNIXソケットファイルを指定します。この場合省略すると /tmp/mysql.sock
-DWITH_MECAB=</path/to/custom/mecab>
Community Edition では含まれるのですが、私の環境ではmecabの配布パッケージがないので省略しました。利用する場合は、lib/libmecab.a とか、include/mecab.h の存在するパスを指定してください。無い場合はビルドします。ipadicも置いておきます。多分。

必要なライブラリがインストールされていない場合はエラーで止まりますので、
入れるか、オプションを外すかしていきます。
このcmakeオプションが次の本番のベースになります。続けて…

#WITH_TIRPC=bundled でビルド途中で怒られないように、シンボリックリンクを作っておきます。
#(私の環境では必要ですが、要らない環境もあると思います。)
(cd tirpc; ln -s lib64 lib)

#ここまでエラーなく来ればビルドはできるはずです。
#※エラーが起きたら確認できるように一応 VERBOSE=1 を付けておきますが無くてもいいです。
#※"8"は私の環境でのCPU数です。並列数は環境に合わせて。
make -j8 VERBOSE=1

途中でなにかあったら解決してください。。。
因みに、100%まで終わった状態で
make install すれば使えますし、
make package で .tar.gz にパッケージできます。

最後に、次で必要となるので、mysql-testが動くようにしてください。
とりあえず、

(cd mysql-test; ./mtr innodb.innodb)

が動くようならOK。本番の準備はできています。プロファイリングで使います。
perlが入っていれば動くはずですが、何か必要なモジュールがあったかもしれません。 

次は、この環境で最適化バイナリをビルドしてみましょう。
ちなみに、clangが利用するllvmXXに対応する、llvmXX-goldと言うパッケージが必要になるので、環境に入れておいてください。


本番: clang で PGO+LTO ビルドして最適に近いバイナリを作る

cmakeのオプションは練習のものベースで。指定の追加ぶん以外は変えないようにしましょう。
※幾つかある"8"は私の環境でのCPU数です。並列数指定は環境に合わせて。

#練習と同様
export CC=clang
export CXX=clang++
export CFLAGS="-O2 -g -pipe -m64 -mtune=generic"
export CXXFLAGS="-O2 -g -pipe -m64 -mtune=generic"

#一応一旦、練習時のファイルは全部消したほうがいいかも。
(git clean -xfd)

#練習と同様のものに -DFPROFILE_GENERATE=1 を足します。
cmake . -DFORCE_INSOURCE_BUILD=1 \
        -DBUILD_CONFIG=mysql_release \
        -DINSTALL_LAYOUT=STANDALONE \
        -DFEATURE_SET=community \
        -DPLATFORM=linux-custom \
        -DWITH_ROUTER=OFF \
        -DWITH_AUTHENTICATION_LDAP=ON \
        -DWITH_AUTHENTICATION_FIDO=ON \
        -DWITH_AUTHENTICATION_KERBEROS=ON \
        -DWITH_CURL=system \
        -DWITH_TIRPC=bundled \
        -DWITH_NUMA=ON \
        -DWITH_BOOST=~/boost_1_77_0 \
        -DCMAKE_INSTALL_PREFIX=/opt/mysql-8.0 \
        -DMYSQL_UNIX_ADDR=/opt/mysql-8.0/mysql.sock \
        -DFPROFILE_GENERATE=1 -DDISABLE_PSI_MEMORY=ON -DWITH_UNIT_TESTS=OFF
-DFPROFILE_GENERATE=1
ビルドされたものを実行すると、(clangの場合)../profile-data/ に *.profraw の形式でプロファイル結果が残るようになります。カレントでビルドしてるので、git管理下の外にできます。(個人的に便利と思っているので)
-DDISABLE_PSI_MEMORY=ON
performance_schema でメモリ確保のカウントができなくなります。performance_schema=OFF でも何故か重いので外します。5.7.xでは、OFFでも10%程度重いのが、8.0.xでは2%程度に減っていますが、意味がないので性能重視なら不要です。mysql-testの中でデバッグ目的で使われているために、いくつかテストが通らなくなったりします。どうせmallocの合計は実際の消費量からは目安程度の意味しか無く、8.0.xでは色々欠けているので実用性は無いと思うのですが、mysql-testが通らないと気持ち悪い人や、それでもこの機能が必要な人は、このオプションを外してもいいです。
-DWITH_UNIT_TESTS=OFF
余計なプロファイルデータが混じらないように、使わないものは一応外しておきます。

先ほどと同様、プロファイル用のビルドをします。

#WITH_TIRPC=bundled でビルド途中で怒られないように、シンボリックリンクを作っておきます。
#(私の環境では必要ですが、要らない環境もあると思います。)
(cd tirpc; ln -s lib64 lib)

#ここまでエラーなく来ればビルドはできるはずです。
#※エラーが起きたら確認できるように一応 VERBOSE=1 を付けておきますが無くてもいいです。
#※"8"は私の環境でのCPU数です。並列数は環境に合わせて。
make -j8 VERBOSE=1

ちゃんとビルドできたら mysql-test の処理を流してプロファイリングします。

#../profile-data/ に ビルド中の実行のものもできてしまうので、気になるので一旦消します。
rm ../profile-data/*.profraw

#テスト自体の結果は関係ありません。プロファイリング処理があるせいで幾つか失敗しますが、強制で全部流してます。
#suite はこの組み合わせが(私の環境ですが)現時点での汎用暫定ベストです。
(cd mysql-test ; ./mtr --accept-test-fail --clean-vardir --force --max-test-fail=0 --mem --mysqld=--binlog-format=row --parallel=8 --retry=0 --skip-rpl --suite=binlog,collations,connection_control,encryption,gcol,gis,innodb,innodb_fts,innodb_gis,innodb_undo,innodb_zip,jp,json,main,sysschema,x)

#できた ../profile-data/*.profraw を利用できるように纏めます。
#偶にエラーが出る .profraw が混ざりますが、それを消すか、全部消してmysql-testをやり直すかします。
(cd ../profile-data ;llvm-profdata merge -output=default.profdata .)

PGO+LTO でビルドします。まずはcmake。

# 中途半端に前の設定が残らないようにキャッシュを消す。(重要)
rm CMakeCache.txt

#練習と同様のものに今度は -DFPROFILE_USE=1 を足します。
cmake . -DFORCE_INSOURCE_BUILD=1 \
        -DBUILD_CONFIG=mysql_release \
        -DINSTALL_LAYOUT=STANDALONE \
        -DFEATURE_SET=community \
        -DPLATFORM=linux-custom \
        -DWITH_ROUTER=OFF \
        -DWITH_AUTHENTICATION_LDAP=ON \
        -DWITH_AUTHENTICATION_FIDO=ON \
        -DWITH_AUTHENTICATION_KERBEROS=ON \
        -DWITH_CURL=system \
        -DWITH_TIRPC=bundled \
        -DWITH_NUMA=ON \
        -DWITH_BOOST=~/boost_1_77_0 \
        -DCMAKE_INSTALL_PREFIX=/opt/mysql-8.0 \
        -DMYSQL_UNIX_ADDR=/opt/mysql-8.0/mysql.sock \
        -DFPROFILE_USE=1 -DDISABLE_PSI_MEMORY=ON -DWITH_UNIT_TESTS=OFF
-DFPROFILE_USE=1
(clangの場合)../profile-data/default.profdata を利用して PGO(+LTO) ビルドします。
-DDISABLE_PSI_MEMORY=ON
前の -DFPROFILE_GENERATE=1 のものと合わせます。
-DWITH_UNIT_TESTS=OFF
前の -DFPROFILE_GENERATE=1 のものと合わせます。

cmake の結果、LTOがちゃんと使われるか確認します。駄目な場合は、なんとか解決してください。(私の場合は llvmXX-goldパッケージが無いことでLTOがcmake中に変なエラーで落ちてました)

grep WITH_LTO CMakeCache.txt
#こんな出力になるはず
# WITH_LTO:BOOL=ON
# WITH_LTO_DEFAULT:INTERNAL=ON

先ほどと同様、最適バイナリをビルドします。

#WITH_TIRPC=bundled でビルド途中で怒られないように、シンボリックリンクを作っておきます。
#(私の環境では必要ですが、要らない環境もあると思います。)
(cd tirpc; ln -s lib64 lib)

#ここまでエラーなく来ればビルドはできるはずです。
#※エラーが起きたら確認できるように一応 VERBOSE=1 を付けておきますが無くてもいいです。
#※"8"は私の環境でのCPU数です。並列数は環境に合わせて。
make -j8 VERBOSE=1
途中でなにかあったら解決してください。。。

因みに、100%まで終わった状態で
make -j8 install すれば使えますし、
make -j8 package で .tar.gz にパッケージできます。

※プロファイリング絡みのmakeは、整合性のためか意図的に全部ビルドし直しになるので -j オプションで並列指定してください。。。(これが、練習で一回ビルドしてもらった理由です。練習で一回通したほうがやり直しが少なくスムーズかと。)

それでは、8.0 本来の性能をぜひ享受してください!
(多分、5.7配布バイナリや、8.0EE版バイナリより速いかも。)