mold の Build 手順メモ

はじめに

mold と呼ばれる高速なリンカを利用して Chromium を Build してみる という記事の中で、mold と呼ばれる「高速なリンカ」について紹介しました。

https://github.com/rui314/mold

mold は、自分の知る限りでは現時点では特に Binary の配信などは行っていないようです。利用したい場合には repository を git clone して、自分で Build して利用する必要があります。

mold の Build 手順についてメモ程度に記録を残しておこうと思います。

ステップ1. mold のソースコードを取得する

まず、mold の git repository を clone します。

minami@chromium-dev-20210227:~$ git clone https://github.com/rui314/mold.git
minami@chromium-dev-20210227:~$ ls
mold
minami@chromium-dev-20210227:~$ cd mold/

mold は自身が依存する mimalloc と oneTBB を git submodule として利用しているので、git submodule update --init を実行してこれらのソースコードを取得します。

minami@chromium-dev-20210227:~/mold$ git submodule update --init
Submodule 'mimalloc' (https://github.com/microsoft/mimalloc.git) registered for path 'mimalloc'
Submodule 'oneTBB' (https://github.com/oneapi-src/oneTBB.git) registered for path 'oneTBB'
Cloning into '/home/minami/mold/mimalloc'...
Cloning into '/home/minami/mold/oneTBB'...
Submodule path 'mimalloc': checked out '4cc8bff90d9e081298ca2c1a94024c7ad4a9e478'
Submodule path 'oneTBB': checked out 'eca91f16d7490a8abfdee652dadf457ec820cc37'

これで、必要なソースコードの取得は完了しました。

ステップ2. 必要な package を install して Build する

make, libssl-dev, zlib1g-dev, cmake, build-essential を利用するので、apt で install しておきます(cmake, build-essential は git submodule で取り込んだ mimalloc と oneTBB の Build に利用します)。

minami@chromium-dev-20210227:~/mold$ sudo apt update
minami@chromium-dev-20210227:~/mold$ sudo apt install -y make libssl-dev zlib1g-dev cmake build-essential

$ make submodules で、submodule で取り込んだ oneTBB と mimalloc を Build します。

minami@chromium-dev-20210227:~/mold$ make submodules
make -C oneTBB
make[1]: Entering directory '/home/minami/mold/oneTBB'
Created the ./build/linux_intel64_gcc_cc9.3.0_libc2.31_kernel5.4.0_release directory
make -C "./build/linux_intel64_gcc_cc9.3.0_libc2.31_kernel5.4.0_release"  -r -f ../../build/Makefile.tbb cfg=release
make[2]: Entering directory '/home/minami/mold/oneTBB/build/linux_intel64_gcc_cc9.3.0_libc2.31_kernel5.4.0_release'
.
.
.
make[2]: Leaving directory '/home/minami/mold/oneTBB/build/linux_intel64_gcc_cc9.3.0_libc2.31_kernel5.4.0_release'
make[1]: Leaving directory '/home/minami/mold/oneTBB'
mkdir -p mimalloc/out/release
(cd mimalloc/out/release; cmake ../..)
.
.
.
[100%] Built target mimalloc-test-api
make[2]: Leaving directory '/home/minami/mold/mimalloc/out/release'
make[1]: Leaving directory '/home/minami/mold/mimalloc/out/release'

これで、oneTBB と mimalloc の Build は完了しました。

いよいよ、mold の Build を行いたいと思いいます。ただし、この状態では clang++ が無いのでまだ Build できません。

minami@chromium-dev-20210227:~/mold$ make
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o main.o main.cc
make: clang++: Command not found
make: *** [<builtin>: main.o] Error 127

clang++ の install では、https://apt.llvm.org/ の「Automatic installation script」である bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" を利用する事にします。

minami@chromium-dev-20210227:~/mold$ sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
--2021-02-27 00:50:07--  https://apt.llvm.org/llvm.sh
.
.
.
Processing triggers for install-info (6.7.0.dfsg.2-5) ...
Processing triggers for libc-bin (2.31-0ubuntu9.2) ...

これで、clang++-11 が install されます(注: 2021/02/27 時点の話で、時期によって最新 version は違うかもしれません)。 clang++ として利用できるように、symlink を貼っておきます。

minami@chromium-dev-20210227:~/mold$ ls /usr/bin | grep clang
clang++-11
clang-11
clang-cpp-11
clangd-11

minami@chromium-dev-20210227:~/mold$ sudo ln -s /usr/bin/clang++-11 /usr/bin/clang++

これで clang++ は使えるようになりましたが、まだ「span header が見つからない」というエラーが出る状態です。

minami@chromium-dev-20210227:~/mold$ make
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o main.o main.cc
In file included from main.cc:1:
./mold.h:17:10: fatal error: 'span' file not found
#include <span>
         ^~~~~~
1 error generated.
make: *** [<builtin>: main.o] Error 1

make で実行されているコマンドに -v オプションをつけると詳細が表示されるのですが、この include path の中で span が見つからないのが原因のようです。

minami@chromium-dev-20210227:~/mold$ clang++ -v -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o main.o main.cc
Ubuntu clang version 11.1.0-++20210204121720+1fdec59bffc1-1~exp1~20210203232336.162
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/9
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/9
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/9
Candidate multilib: .;@m64
Selected multilib: .;@m64
 (in-process)
 "/usr/lib/llvm-11/bin/clang" -cc1 -triple x86_64-pc-linux-gnu -emit-obj -disable-free -disable-llvm-verifier -discard-value-names -main-file-name main.cc -mrelocation-model static -mframe-pointer=none -fmath-errno -fno-rounding-math -mconstructor-aliases -munwind-tables -target-cpu x86-64 -fno-split-dwarf-inlining -debug-info-kind=limited -dwarf-version=4 -debugger-tuning=gdb -v -resource-dir /usr/lib/llvm-11/lib/clang/11.1.0 -I oneTBB/include -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/x86_64-linux-gnu/c++/9 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/x86_64-linux-gnu/c++/9 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/backward -internal-isystem /usr/local/include -internal-isystem /usr/lib/llvm-11/lib/clang/11.1.0/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -O2 -Wno-deprecated-volatile -Wno-switch -std=c++20 -fdeprecated-macro -fdebug-compilation-dir /home/minami/mold -ferror-limit 19 -pthread -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -fcolor-diagnostics -vectorize-loops -vectorize-slp -faddrsig -o main.o -x c++ main.cc
clang -cc1 version 11.1.0 based upon LLVM 11.1.0 default target x86_64-pc-linux-gnu
ignoring nonexistent directory "oneTBB/include"
ignoring nonexistent directory "/include"
ignoring duplicate directory "/usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/x86_64-linux-gnu/c++/9"
#include "..." search starts here:
#include <...> search starts here:
 /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9
 /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/x86_64-linux-gnu/c++/9
 /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/backward
 /usr/local/include
 /usr/lib/llvm-11/lib/clang/11.1.0/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
In file included from main.cc:1:
./mold.h:17:10: fatal error: 'span' file not found
#include <span>
         ^~~~~~
1 error generated.

ここで、おもむろに libstdc++-10-dev package の install をします。

minami@chromium-dev-20210227:~/mold$ sudo apt install -y libstdc++-10-dev

上記 package を install すると、ちゃんと span header を見つけることができます。

minami@chromium-dev-20210227:~/mold$ clang++ -v -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o main.o main.cc
Ubuntu clang version 11.1.0-++20210204121720+1fdec59bffc1-1~exp1~20210203232336.162
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/10
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/9
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/10
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/9
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/10
Candidate multilib: .;@m64
Selected multilib: .;@m64
 (in-process)
 "/usr/lib/llvm-11/bin/clang" -cc1 -triple x86_64-pc-linux-gnu -emit-obj -disable-free -disable-llvm-verifier -discard-value-names -main-file-name main.cc -mrelocation-model static -mframe-pointer=none -fmath-errno -fno-rounding-math -mconstructor-aliases -munwind-tables -target-cpu x86-64 -fno-split-dwarf-inlining -debug-info-kind=limited -dwarf-version=4 -debugger-tuning=gdb -v -resource-dir /usr/lib/llvm-11/lib/clang/11.1.0 -I oneTBB/include -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/backward -internal-isystem /usr/local/include -internal-isystem /usr/lib/llvm-11/lib/clang/11.1.0/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -O2 -Wno-deprecated-volatile -Wno-switch -std=c++20 -fdeprecated-macro -fdebug-compilation-dir /home/minami/mold -ferror-limit 19 -pthread -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -fcolor-diagnostics -vectorize-loops -vectorize-slp -faddrsig -o main.o -x c++ main.cc
clang -cc1 version 11.1.0 based upon LLVM 11.1.0 default target x86_64-pc-linux-gnu
ignoring nonexistent directory "/include"
ignoring duplicate directory "/usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10"
#include "..." search starts here:
#include <...> search starts here:
 oneTBB/include
 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10
 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10
 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/backward
 /usr/local/include
 /usr/lib/llvm-11/lib/clang/11.1.0/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

少し探してみると /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10 の中に span header があるのを見つけました。これが重要だったようです。

minami@chromium-dev-20210227:~/mold$ ls -la /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/span
-rw-r--r-- 1 root root 13251 Aug  8  2020 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/span

この状態で make を実行すると、mold の Build が行われて、mold Binary が生成されるはずです。

minami@chromium-dev-20210227:~/mold$ make
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o output_chunks.o output_chunks.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o mapfile.o mapfile.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o perf.o perf.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o linker_script.o linker_script.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o archive_file.o archive_file.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o output_file.o output_file.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o subprocess.o subprocess.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o gc_sections.o gc_sections.cc
clang++  -g -IoneTBB/include -pthread -std=c++20 -Wno-deprecated-volatile -Wno-switch -O2  -c -o icf.o icf.cc
clang++  main.o object_file.o input_sections.o output_chunks.o mapfile.o perf.o linker_script.o archive_file.o output_file.o subprocess.o gc_sections.o icf.o -o mold -L/home/minami/mold/oneTBB/build/linux_intel64_gcc_cc9.3.0_libc2.31_kernel5.4.0_release/ -Wl,-rpath=/home/minami/mold/oneTBB/build/linux_intel64_gcc_cc9.3.0_libc2.31_kernel5.4.0_release/ -L/home/minami/mold/mimalloc/out/release -Wl,-rpath=/home/minami/mold/mimalloc/out/release -lcrypto -pthread -ltbb -lmimalloc

以下のように mold Binary が生成されていれば成功です 🎉

minami@chromium-dev-20210227:~/mold$ ls -la mold
-rwxrwxr-x 1 minami minami 11142376 Feb 27 01:43 mold

まとめ

「高速なリンカである mold」について、Build 手順をまとめました。

今後、mold が広く使われるようになり、package での配信などが行われるようになればここに記載した手順はおそらく不要になると思います。しかしながら、mold はまだ開始したばかりの project であり、開発環境なども未整備の状態です。しばらくは、「自分で Build して動かしてみる」という状態が続くでしょう。

このブログが、「試しに mold を利用してみる」ことへの一助となれば幸いです。

mold と呼ばれる高速なリンカを利用して Chromium を Build してみる

はじめに

現在、広く使われているリンカの中でもっとも高速なものとして有名なのは LLVM project の LLD でしょう。LLD のパフォーマンスについては、公式 document に以下のような benchmark が掲載されていて、GNU ld, GNU gold などと比較して圧倒的に早いという結果が示されています。

Program Output size GNU ld GNU gold w/o threads GNU gold w/threads lld w/o threads lld w/threads
ffmpeg dbg 92 MiB 1.72s 1.16s 1.01s 0.60s 0.35s
mysqld dbg 154 MiB 8.50s 2.96s 2.68s 1.06s 0.68s
clang dbg 1.67 GiB 104.03s 34.18s 23.49s 14.82s 5.28s
chromium dbg 1.14 GiB 209.05s [1] 64.70s 60.82s 27.60s 16.70s

cf. https://lld.llvm.org/#performance

ChromiumChecking out and building Chromium on Linux の手順にしたがって Build する場合、デフォルトで LLD が利用されるようになっています。そのため、何もせずとも「高速なリンク」という恩恵を受けることができるようになっています。

一方、LLD の author である Rui Ueyama さんが最近活発に開発しているのが mold と呼ばれるリンカです。

https://github.com/rui314/mold

こちらは個人 project として開発を進めているようなのですが、既にかなりの完成度のようで、「LLD 以上に高速なリンク」を実現しているようです。

今日は、この「最も高速なリンカである mold」を利用した Chromium の Build を試してみたいと思います。

ステップ1. Linux マシンを用意する

この部分は 前回 と同様です。

GCP の Compute Engine で以下の VM Instance を立ててそこで作業を行うことにします。

  • 8core, 32GiB memory (E2, e2-standard-8)
  • 200GB SSD
  • image: Ubuntu 20.04 LTS
  • zone: asia-northeast1-b

以下のコマンドで ssh して、そこで作業を行います。

$ gcloud beta compute ssh --zone "asia-northeast1-b" <instance 名>

ステップ2. mold を Build する

mold は、自分の知る限りでは現時点では特に Binary の配信などは行っていないようです。利用したい場合には https://github.com/rui314/mold を git clone して、自分で Build して利用する必要があります。

この部分の手順は別途またブログにまとめたいと思います。 追記: この部分の手順は mold の Build 手順メモ に記載しました。そちらを参照してみてください。

mold Binary が生成されて、以下のように利用できるようになっていれば OK です。

minami@chromium-dev-20210227:~$ git clone https://github.com/rui314/mold.git

# ここで、mold を Build

minami@chromium-dev-20210227:~$ ls -l /home/minami/mold/mold
-rwxrwxr-x 1 minami minami 11142376 Feb 27 01:43 /home/minami/mold/mold

ステップ3. Chromium の Build 環境を整える。

Chromium を Build して動かすまでの待ち時間を「7 時間」から「30 分」まで高速化してみる を参照して、Chromium の Build 環境を整えます。30分 もかからずに、chrome Binary を Build できる環境が整うはずです。

ステップ4. chrome Binary の Build に利用されているリンカを確認しておく

ここでは、事前に「$ autoninja -C out/Default chrome で Build をしたときに chrome Binary のリンクに利用されていたリンカは何者なのか」をチェックしてみます。

$ autoninja -C out/Default chrome を実行して、[4/4] LINK ./chrome のタイミングで起動している process を $ ps fax で見てみます。そうすると、以下のように /home/minami/chromium/src/out/Default/../../third_party/llvm-build/Release+Asserts/bin/ld.lld が利用されていることが分かります。

minami@chromium-dev-20210227:~$ ps fax
    PID TTY      STAT   TIME COMMAND
.
.
.
   1343 pts/0    S+     0:00  |           \_ bash /home/minami/depot_tools/autoninja -C out/Default chrome
   1455 pts/0    S+     0:07  |               \_ /home/minami/depot_tools/ninja-linux64 -C out/Default chrome -j 10
   1484 pts/0    S      0:00  |                   \_ /bin/sh -c python "../../build/toolchain/gcc_link_wrapper.py" --output="./chrome" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -Wl,--color-diagnostics -Wl,--no-call-graph-profile-sor
   1485 pts/0    S      0:00  |                       \_ python ../../build/toolchain/gcc_link_wrapper.py --output=./chrome -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -Wl,--color-diagnostics -Wl,--no-call-graph-profile-sort -m64 -Wer
   1486 pts/0    S      0:00  |                           \_ ../../third_party/llvm-build/Release+Asserts/bin/clang++ -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -Wl,--color-diagnostics -Wl,--no-call-graph-profile-sort -m64 -Werror -Wl,--gdb-index -rdynamic -nostdlib++ --sysroot=../../build/li
   1487 pts/0    D      0:03  |                               \_ /home/minami/chromium/src/out/Default/../../third_party/llvm-build/Release+Asserts/bin/ld.lld @/tmp/response-96ee6b.txt

/home/minami/chromium/src/out/Default/../../third_party/llvm-build/Release+Asserts/bin direcotory は以下のように clang や lld が入っていて、「LLVM project の toolchain が格納された directory」のようです。

minami@chromium-dev-20210227:~$ cd /home/minami/chromium/src/out/Default/../../third_party/llvm-build/Release+Asserts/bin
minami@chromium-dev-20210227:~/chromium/src/third_party/llvm-build/Release+Asserts/bin$ ls
clang  clang++  clang-cl  ld64.lld  ld64.lld.darwinnew  ld.lld  lld  lld-link  llvm-ar  llvm-objcopy  llvm-pdbutil  llvm-symbolizer  llvm-undname

ld.lldlld への symlink が貼られています。これが LLVM project の高速なリンカである LLD です。

minami@chromium-dev-20210227:~/chromium/src/third_party/llvm-build/Release+Asserts/bin$ ls -la ld.lld
lrwxrwxrwx 1 minami minami 3 Dec 12 12:50 ld.lld -> lld
minami@chromium-dev-20210227:~/chromium/src/third_party/llvm-build/Release+Asserts/bin$ ./ld.lld --help
OVERVIEW: lld

USAGE: ./ld.lld [options] file...
.
.
.
./ld.lld: supported targets: elf

LLD が利用されていることは、生成された chrome Binary からも確かめることができます。LLVM project の document である Using LLD - LLVM には以下のように「readelf コマンドで .comment section を読み取ると Linker: LLD という記述があるはず」と記載されています。

LLD leaves its name and version number to a .comment section in an output. If you are in doubt whether you are successfully using LLD or not, run readelf --string-dump .comment <output-file> and examine the output. If the string “Linker: LLD” is included in the output, you are using LLD.

実際に「Build した chrome Binary」に対して readelf を実行してみると、確かに Linker: LLD 12.0.0 という記述を見つけることができます。

minami@chromium-dev-20210227:~/chromium/src$ readelf --string-dump .comment out/Default/chrome

String dump of section '.comment':
  [     0]  GCC: (Debian 7.5.0-3) 7.5.0
  [    1c]  clang version 12.0.0 (https://github.com/llvm/llvm-project/ 6ee22ca6ceb71661e8dbc296b471ace0614c07e5)
  [    82]  Linker: LLD 12.0.0 (https://github.com/llvm/llvm-project/ 6ee22ca6ceb71661e8dbc296b471ace0614c07e5)

ここまで、利用されているリンカが何なのかを確認しました。それ以外に、chrome Binary の Build の際に実行される script もチェックしておきます。これは、autoninja コマンドに -v オプションをつけることで出力することができます。この情報は後々利用します。

minami@chromium-dev-20210227:~/chromium/src$ time autoninja -v -C out/Default chrome
ninja: Entering directory `out/Default'
[1/4] ../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/chrome/common/channel_info/channel_info.o.d -DUSE_UDEV -DUSE_AURA=1 -DUSE_GLIB=1 -DUSE_NSS_CERTS=1 -DUSE_OZONE=1 -DUSE_X11=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_GNU_SOURCE -DCR_CLANG_REVISION=\"llvmorg-12-init-12923-g6ee22ca6-1\" -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -DCOMPONENT_BUILD -D_LIBCPP_ABI_UNSTABLE -D_LIBCPP_ABI_VERSION=Cr -D_LIBCPP_ENABLE_NODISCARD -D_LIBCPP_DEBUG=0 -DCR_LIBCXX_REVISION=375504 -DCR_SYSROOT_HASH=22f2db7711f7426a364617bb6d78686cce09a8f9 -D_DEBUG -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_40 -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_40 -DWEBP_EXTERN=extern -DABSL_CONSUME_DLL -DBORINGSSL_SHARED_LIBRARY -I../.. -Igen -I../../third_party/perfetto/include -Igen/third_party/perfetto/build_config -Igen/third_party/perfetto -I../../third_party/libwebp/src -I../../third_party/abseil-cpp -I../../third_party/boringssl/src/include -I../../third_party/protobuf/src -Igen/protoc_out -fno-delete-null-pointer-checks -fno-strict-aliasing --param=ssp-buffer-size=4 -fstack-protector -funwind-tables -fPIC -pthread -fcolor-diagnostics -fmerge-all-constants -fcrash-diagnostics-dir=../../tools/clang/crashreports -mllvm -instcombine-lower-dbg-declare=0 -fcomplete-member-pointers -m64 -march=x86-64 -msse3 -Wno-builtin-macro-redefined -D__DATE__= -D__TIME__= -D__TIMESTAMP__= -Xclang -fdebug-compilation-dir -Xclang . -no-canonical-prefixes -Wall -Werror -Wextra -Wimplicit-fallthrough -Wunreachable-code -Wthread-safety -Wextra-semi -Wno-missing-field-initializers -Wno-unused-parameter -Wno-c++11-narrowing -Wno-unneeded-internal-declaration -Wno-undefined-var-template -Wno-psabi -Wno-ignored-pragma-optimize -Wno-implicit-int-float-conversion -Wno-final-dtor-non-final-class -Wno-builtin-assume-aligned-alignment -Wno-deprecated-copy -Wno-non-c-typedef-for-linkage -Wmax-tokens -O0 -fno-omit-frame-pointer -g2 -Xclang -debug-info-kind=constructor -gsplit-dwarf -ggnu-pubnames -ftrivial-auto-var-init=pattern -fvisibility=hidden -Xclang -add-plugin -Xclang find-bad-constructs -Xclang -plugin-arg-find-bad-constructs -Xclang check-ipc -Wheader-hygiene -Wstring-conversion -Wtautological-overlap-compare -isystem../../build/linux/debian_sid_amd64-sysroot/usr/include/glib-2.0 -isystem../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include -DPROTOBUF_ALLOW_DEPRECATED=1 -Wno-undefined-bool-conversion -Wno-tautological-undefined-compare -std=c++14 -fno-trigraphs -Wno-trigraphs -fno-exceptions -fno-rtti -nostdinc++ -isystem../../buildtools/third_party/libc++/trunk/include -isystem../../buildtools/third_party/libc++abi/trunk/include --sysroot=../../build/linux/debian_sid_amd64-sysroot -fvisibility-inlines-hidden -c ../../chrome/common/channel_info.cc -o obj/chrome/common/channel_info/channel_info.o
[2/4] touch obj/chrome/common/channel_info.stamp
[3/4] python "../../build/toolchain/gcc_solink_wrapper.py" --readelf="readelf" --nm="nm"  --sofile="./libvr_common.so" --tocfile="./libvr_common.so.TOC" --output="./libvr_common.so" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -shared -Wl,-soname="libvr_common.so" -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -Wl,--color-diagnostics -Wl,--no-call-graph-profile-sort -m64 -Werror -Wl,--gdb-index -rdynamic -nostdlib++ --sysroot=../../build/linux/debian_sid_amd64-sysroot -L../../build/linux/debian_sid_amd64-sysroot/usr/local/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu -Wl,-rpath=\$ORIGIN -o "./libvr_common.so" @"./libvr_common.so.rsp"
[4/4] python "../../build/toolchain/gcc_link_wrapper.py" --output="./chrome" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -Wl,--color-diagnostics -Wl,--no-call-graph-profile-sort -m64 -Werror -Wl,--gdb-index -rdynamic -nostdlib++ --sysroot=../../build/linux/debian_sid_amd64-sysroot -L../../build/linux/debian_sid_amd64-sysroot/usr/local/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu -pie -Wl,--disable-new-dtags -Wl,-rpath=\$ORIGIN -o "./chrome" -Wl,--start-group @"./chrome.rsp" ./libbase.so ./libabsl.so ./libboringssl.so ./libperfetto.so ./libbindings.so ./libbindings_base.so ./libmojo_public_system_cpp.so ./libmojo_public_system.so ./libmojo_cpp_platform.so ./libmessage_support.so ./libmojo_mojom_bindings.so ./libmojo_mojom_bindings_shared.so ./liburl_mojom_traits.so ./libmojo_base_mojom_shared.so ./libmojo_base_shared_typemap_traits.so ./libmojo_base_lib.so ./libbase_i18n.so ./libicui18n.so ./libicuuc.so ./liburl.so ./libui_base.so ./libui_base_features.so ./libui_data_pack.so ./libskia.so ./libgfx.so ./libcolor_space.so ./libcolor_utils.so ./libgeometry.so ./libgeometry_skia.so ./libgfx_switches.so ./libanimation.so ./libcodec.so ./librange.so ./libcc_paint.so ./libcc_base.so ./libcc_debug.so ./libfile_info.so ./libevents_base.so ./libplatform.so ./libkeycodes_x11.so ./libui_base_x.so ./libcontent_public_common_mojo_bindings_shared.so ./libmojom_platform_shared.so ./libandroid_mojo_bindings_shared.so ./libauthenticator_test_mojo_bindings_shared.so ./libcolor_scheme_mojo_bindings_shared.so ./libmojom_mhtml_load_result_shared.so ./libscript_type_mojom_shared.so ./libweb_feature_mojo_bindings_mojom_shared.so ./libservice_manager_mojom_shared.so ./libservice_manager_mojom_constants_shared.so ./libdom_storage_mojom_shared.so ./libframe_mojom_shared.so ./libblink_gpu_mojom_shared.so ./libservice_worker_storage_mojom_shared.so ./libtokens_mojom_shared.so ./libusb_shared.so ./libmojo_base_mojom.so ./libmojo_base_typemap_traits.so ./libcontent_settings_features.so ./libipc.so ./libipc_mojom.so ./libipc_mojom_shared.so ./libprotobuf_lite.so ./libtracing_cpp.so ./libstartup_tracing.so ./libtracing_mojom.so ./libtracing_mojom_shared.so ./libnet.so ./libcrcrypto.so ./libskia_shared_typemap_traits.so ./libcontent.so ./libgpu.so ./libmailbox.so ./libcrash_key_lib.so ./libchrome_zlib.so ./libvulkan_info.so ./libgfx_ipc.so ./libgfx_ipc_geometry.so ./libvulkan_ycbcr_info.so ./liburl_ipc.so ./libviz_common.so ./libviz_resource_format_utils.so ./libviz_vulkan_context_provider.so ./libdisplay.so ./libdisplay_types.so ./libgl_wrapper.so ./libmedia.so ./libshared_memory_support.so ./libleveldb_proto.so ./libkeyed_service_core.so ./libleveldatabase.so ./libgfx_ipc_color.so ./libgfx_ipc_buffer_types.so ./libgfx_ipc_skia.so ./libgfx_native_types_shared_mojom_traits.so ./libgfx_shared_mojom_traits.so ./libgpu_shared_mojom_traits.so ./liblearning_common.so ./libmedia_learning_shared_typemap_traits.so ./libmedia_session_base_cpp.so ./libcookies_mojom_support.so ./libnetwork_cpp_base.so ./libcrash_keys.so ./libcross_origin_embedder_policy.so ./libip_address_mojom_support.so ./libschemeful_site_mojom_support.so ./libwebrtc_component.so ./libservice_manager_mojom.so ./libservice_manager_mojom_constants.so ./libservice_manager_cpp_types.so ./libservice_manager_mojom_traits.so ./libservice_manager_cpp.so ./libmetrics_cpp.so ./libui_base_clipboard_types.so ./libevents.so ./libui_base_cursor_base.so ./libdisplay_shared_mojom_traits.so ./libcc.so ./libvideo_capture_mojom_support.so ./libcapture_base.so ./liblatency_shared_mojom_traits.so ./libprediction.so ./libblink_common.so ./libprivacy_budget.so ./libnetwork_cpp.so ./libweb_feature_mojo_bindings_mojom.so ./libmojom_modules_shared.so ./libmojom_core_shared.so ./libfido.so ./libbluetooth.so ./libscript_type_mojom.so ./libcc_ipc.so ./libcc_shared_mojom_traits.so ./libdom_storage_mojom.so ./libframe_mojom.so ./libblink_gpu_mojom.so ./libservice_worker_storage_mojom.so ./libtokens_traits.so ./libime_shared_mojom_traits.so ./libui_base_ime_types.so ./libui_events_ipc.so ./libweb_bluetooth_mojo_bindings_shared.so ./libax_base.so ./libui_accessibility_ax_mojom.so ./libui_accessibility_ax_mojom_shared.so ./libui_base_ime.so ./libcontent_common_mojo_bindings_shared.so ./libaccessibility.so ./libgfx_x11.so ./libxprotos.so ./libaura.so ./libcompositor.so ./libblink_features.so ./libsurface.so ./libpolicy.so ./libnetwork_service.so ./libmemory_instrumentation.so ./libresource_coordinator_public_mojom.so ./libresource_coordinator_public_mojom_shared.so ./libstorage_common.so ./libpublic.so ./libinterfaces_shared.so ./libstorage_service_filesystem_mojom_shared.so ./libstorage_service_filesystem_mojom.so ./libstorage_service_typemap_traits.so ./libmedia_session_cpp.so ./libstorage_browser.so ./libvr_public_cpp.so ./libdevice_vr_isolated_xr_service_mojo_bindings.so ./libdevice_vr_isolated_xr_service_mojo_bindings_shared.so ./libdevice_vr_test_mojo_bindings_shared.so ./libdevice_vr_service_mojo_bindings_shared.so ./libgamepad_mojom_shared.so ./libdevice_vr_test_mojo_bindings.so ./libdevice_vr_service_mojo_bindings.so ./libgamepad_mojom.so ./libgamepad_shared_typemap_traits.so ./libshared_with_blink.so ./libdevice_vr_public_typemaps.so ./libchrome_features.so ./libprefs.so ./libvariations_features.so ./liburl_matcher.so ./libcapture_lib.so ./libmedia_webrtc.so ./libwtf.so ./libcommon.so ./libnetwork_session_configurator.so ./libsql.so ./libchromium_sqlite3.so ./libwebdata_common.so ./libos_crypt.so ./libomnibox_http_headers.so ./libcloud_policy_proto_generated_compile.so ./libpolicy_component.so ./libpolicy_proto.so ./libgcm.so ./libnative_theme.so ./libservice_provider.so ./libui_message_center_cpp.so ./libppapi_shared.so ./libmojo_core_embedder.so ./libprinting.so ./libsandbox_services.so ./libsuid_sandbox_client.so ./libseccomp_bpf.so ./libsecurity_state_features.so ./libui_base_clipboard.so ./libui_base_data_transfer_policy.so ./libkeyed_service_content.so ./libuser_prefs.so ./libextras.so ./libsessions.so ./libcaptive_portal_core.so ./libdevice_features.so ./libweb_modal.so ./libdevice_event_log.so ./libshell_dialogs.so ./libui_base_idle.so ./libdbus.so ./libonc.so ./libhost.so ./libukm_recorder.so ./libcrdtp.so ./libuser_manager.so ./libperformance_manager_public_mojom.so ./libperformance_manager_public_mojom_shared.so ./libviews.so ./libui_base_ime_init.so ./libui_base_cursor_theme_manager.so ./libui_base_cursor.so ./libx11_window.so ./libui_touch_selection.so ./libproxy_config.so ./libtab_groups.so ./libmanager.so ./libmessage_center.so ./libfontconfig.so ./libx11_events_platform.so ./libdevices.so ./libevents_devices_x11.so ./libevents_x.so ./libffmpeg.so ./libwebview.so ./libdomain_reliability.so ./liblookalikes_features.so ./libui_devtools.so ./libdata_exchange.so ./libgesture_detection.so ./libsnapshot.so ./libweb_dialogs.so ./libcolor.so ./libmixers.so ./libdiscardable_memory_service.so ./libAPP_UPDATE.so ./libozone.so ./libozone_base.so ./libdisplay_util.so ./libvulkan_wrapper.so ./libplatform_window.so ./libui_base_ime_linux.so ./libfreetype_harfbuzz.so ./libmenu.so ./libproperties.so ./libthread_linux.so ./libgtk.so ./libgtk_ui_delegate.so ./libbrowser_ui_views.so ./libwm.so ./libmedia_message_center.so ./libtab_count_metrics.so ./libui_gtk_x.so ./libwm_public.so ./libppapi_host.so ./libppapi_proxy.so ./libcertificate_matching.so ./libdevice_base.so ./libswitches.so ./libcapture_switches.so ./libmidi.so ./libmedia_mojo_services.so ./libmedia_gpu.so ./libgles2_utils.so ./libgles2.so ./libgpu_ipc_service.so ./libgl_init.so ./libcert_net_url_loader.so ./liberror_reporting.so ./libevents_ozone.so ./libschema_org_common.so ./libmirroring_service.so ./libvr_common.so ./libvr_base.so ./libdevice_vr.so ./libblink_controller.so ./libblink_core.so ./libblink_mojom_broadcastchannel_bindings_shared.so ./libwtf_support.so ./libweb_feature_mojo_bindings_mojom_blink.so ./libmojo_base_mojom_blink.so ./libservice_manager_mojom_blink.so ./libservice_manager_mojom_constants_blink.so ./libblink_platform.so ./libcc_animation.so ./libresource_coordinator_public_mojom_blink.so ./libv8.so ./libblink_embedded_frame_sink_mojo_bindings_shared.so ./libperformance_manager_public_mojom_blink.so ./libui_accessibility_ax_mojom_blink.so ./libgin.so ./libblink_modules.so ./libgamepad_mojom_blink.so ./liburlpattern.so ./libdevice_vr_service_mojo_bindings_blink.so ./libdevice_vr_test_mojo_bindings_blink.so ./libdiscardable_memory_client.so ./libcbor.so ./libpdfium.so ./libheadless_non_renderer.so ./libc++.so -Wl,--end-group  -ldl -lpthread -lrt -lgmodule-2.0 -lgobject-2.0 -lgthread-2.0 -lglib-2.0 -lnss3 -lnssutil3 -lsmime3 -lplds4 -lplc4 -lnspr4 -latk-1.0 -latk-bridge-2.0 -lcups -ldbus-1 -lgio-2.0 -lexpat

real    0m15.202s
user    0m19.157s
sys     0m4.398s

ステップ5. mold を利用して chrome Binary をリンクしてみる

さて、ここまでで「chrome のリンクに LLD が利用されていること」、「コマンドとしては /home/minami/chromium/src/out/Default/../../third_party/llvm-build/Release+Asserts/bin/ld.lld が利用されていること」が確認できました。

次は、LLD の代わりに mold を利用してみたいと思います。ここでは、「ld.lld の symlink の向き先を lld から mold に切り替えて、Build する」というアプローチをとってみます。 以下のように ld.lld を消して、symlink の向き先を /home/minami/mold/mold に変えてみます。

minami@chromium-dev-20210227:~$ cd /home/minami/chromium/src/out/Default/../../third_party/llvm-build/Release+Asserts/bin
minami@chromium-dev-20210227:~/chromium/src/third_party/llvm-build/Release+Asserts/bin$ rm ld.lld
minami@chromium-dev-20210227:~/chromium/src/third_party/llvm-build/Release+Asserts/bin$ ln -s /home/minami/mold/mold ld.lld
minami@chromium-dev-20210227:~/chromium/src/third_party/llvm-build/Release+Asserts/bin$ ls -la ld.lld
lrwxrwxrwx 1 minami minami 22 Feb 28 03:06 ld.lld -> /home/minami/mold/mold

これで、mold が利用されるようになるはずです。この状態で再度 chrome の Build をしてみます。

ただ、この状態で $ autoninja -C out/Default chrome を実行すると、以下のように [3/4] SOLINK ./libvr_common.so のステップでリンクに失敗してしまします。

minami@chromium-dev-20210227:~/chromium/src$ time autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
[3/4] SOLINK ./libvr_common.so
FAILED: libvr_common.so libvr_common.so.TOC
python "../../build/toolchain/gcc_solink_wrapper.py" --readelf="readelf" --nm="nm"  --sofile="./libvr_common.so" --tocfile="./libvr_common.so.TOC" --output="./libvr_common.so" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -shared -Wl,-soname="libvr_common.so" -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -Wl,--color-diagnostics -Wl,--no-call-graph-profile-sort -m64 -Werror -Wl,--gdb-index -rdynamic -nostdlib++ --sysroot=../../build/linux/debian_sid_amd64-sysroot -L../../build/linux/debian_sid_amd64-sysroot/usr/local/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu -Wl,-rpath=\$ORIGIN -o "./libvr_common.so" @"./libvr_common.so.rsp"
mold: unknown command line option: -soname=libvr_common.so
clang: error: linker command failed with exit code 1 (use -v to see invocation)
ninja: build stopped: subcommand failed.

real    0m5.480s
user    0m4.577s
sys     0m0.746s

mold が -soname オプションをサポートしてないために、エラーが出ているようです。

上記の python "../../build/toolchain/gcc_solink_wrapper.py" ... という部分が [3/4] SOLINK ./libvr_common.so のステップとして実際に実行されているコマンドです。ここから、「mold がサポートしていないオプション」を消して、同じコマンドを手動で実行してみます。具体的には、-soname , --color-diagnostics, --no-call-graph-profile-sort, --gdb-index オプションの指定を消して、以下のように実行します。こうすると、ちゃんと mold によるリンクに成功して、 libvr_common.so という Shared Object File が生成されます。

minami@chromium-dev-20210227:~/chromium/src/out/Default$ time python "../../build/toolchain/gcc_solink_wrapper.py" --readelf="readelf" --nm="nm"  --sofile="./libvr_common.so" --tocfile="./libvr_common.so.TOC" --output="./libvr_common.so" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -shared -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -m64 -Werror -rdynamic -nostdlib++ --sysroot=../../build/linux/debian_sid_amd64-sysroot -L../../build/linux/debian_sid_amd64-sysroot/usr/local/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu -Wl,-rpath=\$ORIGIN -o "./libvr_common.so" @"./libvr_common.so.rsp"

real    0m0.563s
user    0m0.033s
sys     0m0.029s

minami@chromium-dev-20210227:~/chromium/src/out/Default$ ls -la libvr_common.so
-rwxrwxr-x 1 minami minami 192508187 Feb 28 03:14 libvr_common.so

上記の python script の実行が、[3/4] SOLINK ./libvr_common.so に相当する処理でした。さらに、 [4/4] LINK ./chrome に相当する処理も、直接 python script の実行を行う事にします。これは、ステップ4の最後に $ autoninja -v -C out/Default chrome コマンドで出力したコマンドの、[4/4] python "../../build/toolchain/gcc_link_wrapper.py" --output="./chrome" -- ...(長いので省略) が該当します。

ただ、この状態で [4/4] LINK ./chromeに相当する python script を実行しても、以下のように chrome.rsp という File が無いことで失敗してしまいます。

minami@chromium-dev-20210227:~/chromium/src/out/Default$ python "../../build/toolchain/gcc_link_wrapper.py" --output="./chrome" -- ...(長いので省略)
clang: error: no such file or directory: '@./chrome.rsp'

そこで、一度 ld.lld の symlink 先を lld に戻してから、$ autoninja -C out/Default chrome の実行中に [4/4] LINK ./chrome のタイミングで Ctrl-C で python script を強制 exit してみます。 こうすることで、「libvr_common.sochrome.rsp が存在する状態(ちょうど [4/4] LINK ./chrome の開始前の状態」を再現することが出来ます。

minami@chromium-dev-20210227:~/chromium/src$ ls out/Default/libvr_common.so
out/Default/libvr_common.so
minami@chromium-dev-20210227:~/chromium/src$ ls out/Default/chrome.rsp
out/Default/chrome.rsp

この状態で、再度 ld.lld の symlink 先を mold にしてから、[4/4] LINK ./chrome に相当する python script を手動で実行します。mold でサポートされてない --color-diagnostics, --no-call-graph-profile-sort, --gdb-indexオプションの指定は消しておきます。 こうすると、以下のようにちゃんとリンクに成功します。chrome Binary が生成されたことも確認できます。

minami@chromium-dev-20210227:~/chromium/src/out/Default$ time python "../../build/toolchain/gcc_link_wrapper.py" --output="./chrome" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -m64 -Werror -rdynamic -nostdlib++ --sysroot=../../build/linux/debian_sid_amd64-sysroot -L../../build/linux/debian_sid_amd64-sysroot/usr/local/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu -pie -Wl,--disable-new-dtags -Wl,-rpath=\$ORIGIN -o "./chrome" -Wl,--start-group @"./chrome.rsp" ./libbase.so ./libabsl.so ./libboringssl.so ./libperfetto.so ./libbindings.so ./libbindings_base.so ./libmojo_public_system_cpp.so ./libmojo_public_system.so ./libmojo_cpp_platform.so ./libmessage_support.so ./libmojo_mojom_bindings.so ./libmojo_mojom_bindings_shared.so ./liburl_mojom_traits.so ./libmojo_base_mojom_shared.so ./libmojo_base_shared_typemap_traits.so ./libmojo_base_lib.so ./libbase_i18n.so ./libicui18n.so ./libicuuc.so ./liburl.so ./libui_base.so ./libui_base_features.so ./libui_data_pack.so ./libskia.so ./libgfx.so ./libcolor_space.so ./libcolor_utils.so ./libgeometry.so ./libgeometry_skia.so ./libgfx_switches.so ./libanimation.so ./libcodec.so ./librange.so ./libcc_paint.so ./libcc_base.so ./libcc_debug.so ./libfile_info.so ./libevents_base.so ./libplatform.so ./libkeycodes_x11.so ./libui_base_x.so ./libcontent_public_common_mojo_bindings_shared.so ./libmojom_platform_shared.so ./libandroid_mojo_bindings_shared.so ./libauthenticator_test_mojo_bindings_shared.so ./libcolor_scheme_mojo_bindings_shared.so ./libmojom_mhtml_load_result_shared.so ./libscript_type_mojom_shared.so ./libweb_feature_mojo_bindings_mojom_shared.so ./libservice_manager_mojom_shared.so ./libservice_manager_mojom_constants_shared.so ./libdom_storage_mojom_shared.so ./libframe_mojom_shared.so ./libblink_gpu_mojom_shared.so ./libservice_worker_storage_mojom_shared.so ./libtokens_mojom_shared.so ./libusb_shared.so ./libmojo_base_mojom.so ./libmojo_base_typemap_traits.so ./libcontent_settings_features.so ./libipc.so ./libipc_mojom.so ./libipc_mojom_shared.so ./libprotobuf_lite.so ./libtracing_cpp.so ./libstartup_tracing.so ./libtracing_mojom.so ./libtracing_mojom_shared.so ./libnet.so ./libcrcrypto.so ./libskia_shared_typemap_traits.so ./libcontent.so ./libgpu.so ./libmailbox.so ./libcrash_key_lib.so ./libchrome_zlib.so ./libvulkan_info.so ./libgfx_ipc.so ./libgfx_ipc_geometry.so ./libvulkan_ycbcr_info.so ./liburl_ipc.so ./libviz_common.so ./libviz_resource_format_utils.so ./libviz_vulkan_context_provider.so ./libdisplay.so ./libdisplay_types.so ./libgl_wrapper.so ./libmedia.so ./libshared_memory_support.so ./libleveldb_proto.so ./libkeyed_service_core.so ./libleveldatabase.so ./libgfx_ipc_color.so ./libgfx_ipc_buffer_types.so ./libgfx_ipc_skia.so ./libgfx_native_types_shared_mojom_traits.so ./libgfx_shared_mojom_traits.so ./libgpu_shared_mojom_traits.so ./liblearning_common.so ./libmedia_learning_shared_typemap_traits.so ./libmedia_session_base_cpp.so ./libcookies_mojom_support.so ./libnetwork_cpp_base.so ./libcrash_keys.so ./libcross_origin_embedder_policy.so ./libip_address_mojom_support.so ./libschemeful_site_mojom_support.so ./libwebrtc_component.so ./libservice_manager_mojom.so ./libservice_manager_mojom_constants.so ./libservice_manager_cpp_types.so ./libservice_manager_mojom_traits.so ./libservice_manager_cpp.so ./libmetrics_cpp.so ./libui_base_clipboard_types.so ./libevents.so ./libui_base_cursor_base.so ./libdisplay_shared_mojom_traits.so ./libcc.so ./libvideo_capture_mojom_support.so ./libcapture_base.so ./liblatency_shared_mojom_traits.so ./libprediction.so ./libblink_common.so ./libprivacy_budget.so ./libnetwork_cpp.so ./libweb_feature_mojo_bindings_mojom.so ./libmojom_modules_shared.so ./libmojom_core_shared.so ./libfido.so ./libbluetooth.so ./libscript_type_mojom.so ./libcc_ipc.so ./libcc_shared_mojom_traits.so ./libdom_storage_mojom.so ./libframe_mojom.so ./libblink_gpu_mojom.so ./libservice_worker_storage_mojom.so ./libtokens_traits.so ./libime_shared_mojom_traits.so ./libui_base_ime_types.so ./libui_events_ipc.so ./libweb_bluetooth_mojo_bindings_shared.so ./libax_base.so ./libui_accessibility_ax_mojom.so ./libui_accessibility_ax_mojom_shared.so ./libui_base_ime.so ./libcontent_common_mojo_bindings_shared.so ./libaccessibility.so ./libgfx_x11.so ./libxprotos.so ./libaura.so ./libcompositor.so ./libblink_features.so ./libsurface.so ./libpolicy.so ./libnetwork_service.so ./libmemory_instrumentation.so ./libresource_coordinator_public_mojom.so ./libresource_coordinator_public_mojom_shared.so ./libstorage_common.so ./libpublic.so ./libinterfaces_shared.so ./libstorage_service_filesystem_mojom_shared.so ./libstorage_service_filesystem_mojom.so ./libstorage_service_typemap_traits.so ./libmedia_session_cpp.so ./libstorage_browser.so ./libvr_public_cpp.so ./libdevice_vr_isolated_xr_service_mojo_bindings.so ./libdevice_vr_isolated_xr_service_mojo_bindings_shared.so ./libdevice_vr_test_mojo_bindings_shared.so ./libdevice_vr_service_mojo_bindings_shared.so ./libgamepad_mojom_shared.so ./libdevice_vr_test_mojo_bindings.so ./libdevice_vr_service_mojo_bindings.so ./libgamepad_mojom.so ./libgamepad_shared_typemap_traits.so ./libshared_with_blink.so ./libdevice_vr_public_typemaps.so ./libchrome_features.so ./libprefs.so ./libvariations_features.so ./liburl_matcher.so ./libcapture_lib.so ./libmedia_webrtc.so ./libwtf.so ./libcommon.so ./libnetwork_session_configurator.so ./libsql.so ./libchromium_sqlite3.so ./libwebdata_common.so ./libos_crypt.so ./libomnibox_http_headers.so ./libcloud_policy_proto_generated_compile.so ./libpolicy_component.so ./libpolicy_proto.so ./libgcm.so ./libnative_theme.so ./libservice_provider.so ./libui_message_center_cpp.so ./libppapi_shared.so ./libmojo_core_embedder.so ./libprinting.so ./libsandbox_services.so ./libsuid_sandbox_client.so ./libseccomp_bpf.so ./libsecurity_state_features.so ./libui_base_clipboard.so ./libui_base_data_transfer_policy.so ./libkeyed_service_content.so ./libuser_prefs.so ./libextras.so ./libsessions.so ./libcaptive_portal_core.so ./libdevice_features.so ./libweb_modal.so ./libdevice_event_log.so ./libshell_dialogs.so ./libui_base_idle.so ./libdbus.so ./libonc.so ./libhost.so ./libukm_recorder.so ./libcrdtp.so ./libuser_manager.so ./libperformance_manager_public_mojom.so ./libperformance_manager_public_mojom_shared.so ./libviews.so ./libui_base_ime_init.so ./libui_base_cursor_theme_manager.so ./libui_base_cursor.so ./libx11_window.so ./libui_touch_selection.so ./libproxy_config.so ./libtab_groups.so ./libmanager.so ./libmessage_center.so ./libfontconfig.so ./libx11_events_platform.so ./libdevices.so ./libevents_devices_x11.so ./libevents_x.so ./libffmpeg.so ./libwebview.so ./libdomain_reliability.so ./liblookalikes_features.so ./libui_devtools.so ./libdata_exchange.so ./libgesture_detection.so ./libsnapshot.so ./libweb_dialogs.so ./libcolor.so ./libmixers.so ./libdiscardable_memory_service.so ./libAPP_UPDATE.so ./libozone.so ./libozone_base.so ./libdisplay_util.so ./libvulkan_wrapper.so ./libplatform_window.so ./libui_base_ime_linux.so ./libfreetype_harfbuzz.so ./libmenu.so ./libproperties.so ./libthread_linux.so ./libgtk.so ./libgtk_ui_delegate.so ./libbrowser_ui_views.so ./libwm.so ./libmedia_message_center.so ./libtab_count_metrics.so ./libui_gtk_x.so ./libwm_public.so ./libppapi_host.so ./libppapi_proxy.so ./libcertificate_matching.so ./libdevice_base.so ./libswitches.so ./libcapture_switches.so ./libmidi.so ./libmedia_mojo_services.so ./libmedia_gpu.so ./libgles2_utils.so ./libgles2.so ./libgpu_ipc_service.so ./libgl_init.so ./libcert_net_url_loader.so ./liberror_reporting.so ./libevents_ozone.so ./libschema_org_common.so ./libmirroring_service.so ./libvr_common.so ./libvr_base.so ./libdevice_vr.so ./libblink_controller.so ./libblink_core.so ./libblink_mojom_broadcastchannel_bindings_shared.so ./libwtf_support.so ./libweb_feature_mojo_bindings_mojom_blink.so ./libmojo_base_mojom_blink.so ./libservice_manager_mojom_blink.so ./libservice_manager_mojom_constants_blink.so ./libblink_platform.so ./libcc_animation.so ./libresource_coordinator_public_mojom_blink.so ./libv8.so ./libblink_embedded_frame_sink_mojo_bindings_shared.so ./libperformance_manager_public_mojom_blink.so ./libui_accessibility_ax_mojom_blink.so ./libgin.so ./libblink_modules.so ./libgamepad_mojom_blink.so ./liburlpattern.so ./libdevice_vr_service_mojo_bindings_blink.so ./libdevice_vr_test_mojo_bindings_blink.so ./libdiscardable_memory_client.so ./libcbor.so ./libpdfium.so ./libheadless_non_renderer.so ./libc++.so -Wl,--end-group  -ldl -lpthread -lrt -lgmodule-2.0 -lgobject-2.0 -lgthread-2.0 -lglib-2.0 -lnss3 -lnssutil3 -lsmime3 -lplds4 -lplc4 -lnspr4 -latk-1.0 -latk-bridge-2.0 -lcups -ldbus-1 -lgio-2.0 -lexpat

real    0m3.312s
user    0m0.064s
sys     0m0.027s

minami@chromium-dev-20210227:~/chromium/src/out/Default$ ls -la chrome
-rwxrwxr-x 1 minami minami 1296141738 Feb 28 03:46 chrome

この chrome.comment section を見て、出自を確認してみましょう。LLD でリンクした時は Linker: LLD 12.0.0 という記述があったのに対して、この chrome Binary にはその記述がありません。逆説的に、「LLD 以外のリンカ(= mold)でリンクしたこと」が確認できたと言えそうです。

minami@chromium-dev-20210227:~/chromium/src$ readelf --string-dump .comment out/Default/chrome

String dump of section '.comment':
  [     1]  GCC: (Debian 7.5.0-3) 7.5.0
  [    1d]  clang version 12.0.0 (https://github.com/llvm/llvm-project/ 6ee22ca6ceb71661e8dbc296b471ace0614c07e5)

生成した chrome Binary の挙動も確認してみましょう。以前のブログ記事 のように、「headless mode での実行」を試してみます。以下のようにちゃんと動作をすることが確認できました 🎉

minami@chromium-dev-20210227:~/chromium/src$ ./out/Default/chrome --headless --disable-gpu --dump-dom https://example.com
[0228/035317.179292:WARNING:headless_content_main_delegate.cc(530)] Cannot create Pref Service with no user data dir.
[0228/035317.194952:INFO:content_main_runner_impl.cc(1027)] Chrome is running in full browser mode.
<!DOCTYPE html>
.
.
.

ということで、無事「mold による chrome Binary のリンク」に成功しました。

既存の Build の仕組みに組み込もうと思うと「mold ではサポートされてない option の扱い」などいくつか考える必要がありそうですが、試しにリンクをするだけであれば比較的簡単に実行ができることが分かりました。

LLD と mold の速度比較

mold は「LLD よりも高速なリンカ」として開発されています。実際に、リンクにかかる時間がどれだけ変わったのか、比較してみましょう。ここでは、上記の「リンカに渡す option を減らした状態でのリンクの実行」を mold と LLD の両方で行って、time で計測した結果を載せています。

LLD

  • [3/4] SOLINK ./libvr_common.so 相当の処理
minami@chromium-dev-20210227:~/chromium/src/out/Default$ time python "../../build/toolchain/gcc_solink_wrapper.py" --readelf="readelf" --nm="nm"  --sofile="./libvr_common.so" --tocfile="./libvr_common.so.TOC" --output="./libvr_common.so" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -shared -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -m64 -Werror -rdynamic -nostdlib++ --sysroot=../../build/linux/debian_sid_amd64-sysroot -L../../build/linux/debian_sid_amd64-sysroot/usr/local/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu -Wl,-rpath=\$ORIGIN -o "./libvr_common.so" @"./libvr_common.so.rsp"

real    0m1.265s
user    0m1.391s
sys     0m0.837s
  • [4/4] LINK ./chrome 相当の処理
minami@chromium-dev-20210227:~/chromium/src/out/Default$ time python "../../build/toolchain/gcc_link_wrapper.py" --output="./chrome" -- ...(長いので省略)

real    0m7.777s
user    0m8.309s
sys     0m3.664s

mold

  • [3/4] SOLINK ./libvr_common.so 相当の処理
minami@chromium-dev-20210227:~/chromium/src/out/Default$ time python "../../build/toolchain/gcc_solink_wrapper.py" --readelf="readelf" --nm="nm"  --sofile="./libvr_common.so" --tocfile="./libvr_common.so.TOC" --output="./libvr_common.so" -- ../../third_party/llvm-build/Release+Asserts/bin/clang++ -shared -fuse-ld=lld -Wl,--fatal-warnings -Wl,--build-id -fPIC -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,defs -Wl,--as-needed -m64 -Werror -rdynamic -nostdlib++ --sysroot=../../build/linux/debian_sid_amd64-sysroot -L../../build/linux/debian_sid_amd64-sysroot/usr/local/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/lib/x86_64-linux-gnu -L../../build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu -Wl,-rpath=\$ORIGIN -o "./libvr_common.so" @"./libvr_common.so.rsp"

real    0m0.563s
user    0m0.033s
sys     0m0.029s
  • [4/4] LINK ./chrome 相当の処理
minami@chromium-dev-20210227:~/chromium/src/out/Default$ time python "../../build/toolchain/gcc_link_wrapper.py" --output="./chrome" -- ...(長いので省略)

real    0m3.312s
user    0m0.064s
sys     0m0.027s

mold と LLD の比較

[3/4] SOLINK ./libvr_common.so 相当の処理については 1.265s -> 0.563s、[4/4] LINK ./chrome 相当の処理については 7.777s -> 3.312s とそれぞれ 2倍以上の高速化が達成できています 🎉

なお、上記の比較を見ると十分に早いですが、それでも「はじめにで掲載した Rui Ueyama さんの Tweet」の 2.5秒という記述に比べると遅いです。これはおそらく、今回利用した GCP VM instance の「8core」という構成に起因するのではないかと考えています。mold は複数 CPU core をうまく活用する作りになっているようなので、CPU core 数が増えることで近いパフォーマンスが出るのだと思われます。

まとめ

Rui Ueyama さんが開発してる mold を利用して、Chromium の Build をしてみました。

実験の結果、試しにリンクをするだけであれば比較的簡単に実行ができることが分かりました。また、CPU core 数が 8 とそれほど多くない構成であっても、 LLD に比べてリンクが2倍以上高速化する という改善が見られることが分かりました。

Rui Ueyama さんは自分が尊敬するエンジニアの1人なのですが、彼のエンジニアリングによって世界がより良くなっている事を体感できたように思います。

Chromium を Build して動かすまでの待ち時間を「7 時間」から「30 分」まで高速化してみる

Chromium をゼロから Build して動かしてみる という前回のブログでは、Chromium を Build して動かすという一連のフローを試してみました。

この時は、Checking out and building Chromium on Linux の手順に従って作業をしました。これは、「最新の Chromium」を Linux 上で Build する事が出来るしっかりした公式手順です。しかしながら、8core, 32GiB memory の GCP VM instance でも Build に 6-7 時間程度かかってしまうのがネックでした。

そこで今日は、Chromium を Build して動かすまでのサイクルを 30 分で試せるようにすること を目指します。7時間と比較すると約15倍の高速化です!

なお、具体的には 事前に Build 済みの Object File を GCS に置いておき、ダウンロードして再利用することで再ビルドの手間を避ける というアプローチをとります。そのため、「最新の Chromium ではなく事前に Build 済みの古い Version しか試せない」ことが欠点となります。「最新の Chromium へのコミット」などを行いたい場合には適さないアプローチでしょう。

しかしながら、「過去の Version の Chromium でも良いから、試しに Build して動かしてみたい」という場合には役立つのではないかと期待しています。

以下、ステップ 0 から順番に作業を進めてみます。

ステップ0. 事前に Chromium の Build 成果物を用意して、GCS へアップロードしておく

Chromium はまともにゼロから Build すると、とてつもなく長い時間がかかってしまいます。そこで、「事前に Chromium を Build して生成した一連の Object File 及び Binary を tar.gz にまとめたもの」を用意しておきます。これをダウンロードして利用することで、「再ビルドの手間」を省くことができるはずです。

対象の File は、chromium-dev-south37 GCS Bucket の中で chromium20201213.tar.gz という名前で配置しています(suffix は日付で、2020/12/13 時点の Chromium のコードを Build したことを示しています)。

$ gsutil ls -l gs://chromium-dev-south37/chromium20201213.tar.gz
18900879483  2020-12-13T15:56:30Z  gs://chromium-dev-south37/chromium20201213.tar.gz
TOTAL: 1 objects, 18900879483 bytes (17.6 GiB)

Public にダウンロード可能な形で公開しているので、誰でも試していただくことが可能です。ソースコードも含んでいるので、17.6GiB というかなり巨大なサイズになっています。

この後のステップでは、上記の「ソースコードと Build 成果物を tar.gz でまとめたもの」を利用して作業を進めます。

ステップ1. Linux マシンを用意する

この部分は 前回 と同様です。

GCP の Compute Engine で以下の VM Instance を立ててそこで作業を行うことにします。

  • 8core, 32GiB memory (E2, e2-standard-8)
  • 200GB SSD
  • image: Ubuntu 20.04 LTS
  • zone: asia-northeast1-b

以下のコマンドで ssh して、そこで作業を行います。

$ gcloud beta compute ssh --zone "asia-northeast1-b" <instance 名>

ステップ2. 事前に用意しておいた Chromium の一連の Build 成果物をダウンロードする

早速、ステップ0 として事前に用意しておいた「Chromium の一連の Build 成果物」をダウンロードしましょう。以下のように $ gsutil cp コマンド利用して GCS からダウンロードをします。

$ gsutil -o 'GSUtil:parallel_thread_count=1' \
   -o 'GSUtil:sliced_object_download_max_components=8' \
   cp gs://chromium-dev-south37/chromium20201213.tar.gz ./chromium20201213.tar.gz

なお、ここでは Cloud Storage のパフォーマンスを最適化する - Google Cloud Platform の推奨に従って、GSUtil:parallel_thread_count オプションや GSUtil:sliced_object_download_max_components=8 オプションを利用しています。これは、 「1つの巨大なファイルの転送」をしたい場合に推奨されるオプション として紹介されています。

上記のコマンドを実行すると、環境にもよりますが大体 3 分ほどでダウンロードが完了するはずです。

minami@chromium-dev-20210226:~$ gsutil -o 'GSUtil:parallel_thread_count=1' \
>    -o 'GSUtil:sliced_object_download_max_components=8' \
>    cp gs://chromium-dev-south37/chromium20201213.tar.gz ./chromium20201213.tar.gz
Copying gs://chromium-dev-south37/chromium20201213.tar.gz...
- [1 files][ 17.6 GiB/ 17.6 GiB]   25.7 MiB/s
Operation completed over 1 objects/17.6 GiB.

ダウンロードが終わると、 17.6 GiB の chromium20201213.tar.gz が作られているはずです。

minami@chromium-dev-20210226:~$ ls -l | grep chromium
total 18457900
-rw-rw-r-- 1 minami minami 18900879483 Feb 26 12:34 chromium20201213.tar.gz

今度は、この File を $ tar xvzf chromium20201213.tar.gz で展開します。これは大体 10-15 分程度で完了するはずです。

minami@chromium-dev-20210226:~$ time tar xvzf chromium20201213.tar.gz
chromium/
.
.
.
chromium/src/cloud_print/OWNERS
chromium/.gclient
chromium/.gclient_entries

real    15m45.171s
user    5m57.618s
sys     2m49.498s

展開が終わると、以下のように chromium directory が作られているはずです。chromium directory の中には chromium/src/out/Default directory が入っています。その中には Build 済みの Object File や chrome Binary が存在する事も確認できます。

minami@chromium-dev-20210226:~$ ls -la | grep chromium
drwxrwxr-x  4 minami minami        4096 Dec 12 12:33 chromium
-rw-rw-r--  1 minami minami 18900879483 Feb 26 12:34 chromium20201213.tar.gz

minami@chromium-dev-20210226:~$ ls chromium/src/out/
Default

minami@chromium-dev-20210226:~$ ls chromium/src/out/Default/ | grep '\.so$' | wc -l
375

minami@chromium-dev-20210226:~$ ls -la chromium/src/out/Default/chrome
-rwxrwxr-x 1 minami minami 1058842464 Dec 13 14:43 chromium/src/out/Default/chrome

ただし、この状態ではまだ chrome Binary は実行できません。実行しようとすると、以下のようにエラーが出てしまいます。

minami@chromium-dev-20210226:~$ ./chromium/src/out/Default/chrome
./chromium/src/out/Default/chrome: error while loading shared libraries: libnss3.so: cannot open shared object file: No such file or directory

ステップ3. 必要な package を install

chrome Binary 実行にはいくつか package が必要となります。

まず、python2 が必要になるので、install しておきます。

minami@chromium-dev-20210226:~$ sudo apt update -y
minami@chromium-dev-20210226:~$ sudo apt install -y python2
minami@chromium-dev-20210226:~$ which python2
/usr/bin/python2
minami@chromium-dev-20210226:~$ python2 -V
Python 2.7.18
minami@chromium-dev-20210226:~$ sudo ln -s /usr/bin/python2 /usr/local/bin/python
minami@chromium-dev-20210226:~$ which python
/usr/local/bin/python
minami@chromium-dev-20210226:~$ python -V
Python 2.7.18

次に、./build/install-build-deps.sh を実行します。これは 6 分程度で完了するはずです。

minami@chromium-dev-20210226:~/chromium/src$ time ./build/install-build-deps.sh
.
.
.
Installing locales.
Generating locales (this might take a while)...
  da_DK.UTF-8... done
  en_US.UTF-8... done
  fr_FR.UTF-8... done
  he_IL.UTF-8... done
  zh_TW.UTF-8... done
Generation complete.

real    6m10.812s
user    2m35.472s
sys     0m58.130s

これで、必要な package の install が完了しました。この状態で、試しに chrome Binary を実行してみましょう。前回のように、「headless mode での実行」を試してみます。以下のようにちゃんと動作をすることが確認できました 🎉

minami@chromium-dev-20210226:~/chromium/src$ ./out/Default/chrome --headless --disable-gpu --dump-dom https://example.com
<!DOCTYPE html>
.
.
.

ここまで、 GCP VM instance への ssh から大体 20-25 分 ほどで「chrome Binary を動かすこと」が出来ました

ステップ4. Build に必要な環境をセットアップ

さて、「ダウンロードした chrome Binary」を動かせただけでは、あまり嬉しくありません。「コードを書き換えながら、chrome を Build して実行することができる」という状態を目指します。

まず、ninja など Build に必要なツールを利用するために、depot_tools をインストールします。

minami@chromium-dev-20210226:~/chromium/src$ cd $HOME
minami@chromium-dev-20210226:~$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
# PATH を通しておく
minami@chromium-dev-20210226:~$ export PATH="$PATH:${PWD}/depot_tools"

これで、gn などの Build に必要なコマンドが使えるようになります。

minami@chromium-dev-20210226:~$ which gn
/home/minami/depot_tools/gn

次に、この状態で chrome Binary の Build をしてみます。autoninja -C out/Default chrome を実行すると、既に chrome Binary は存在するため、no work to do という表示が出て 5 秒程度で終了します。

minami@chromium-dev-20210226:~$ cd chromium/src/
minami@chromium-dev-20210226:~/chromium/src$ autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
ninja: no work to do.

前回と同様に、chrome/common/channel_info.cc という File を touch して timestamp を更新してみます。こうすると、更新した File (及びその File に依存した File)だけを対象に Build が行われるので、20 秒程度で chrome Binary の Build が終了します。

minami@chromium-dev-20210226:~/chromium/src$ touch chrome/common/channel_info.cc
minami@chromium-dev-20210226:~/chromium/src$ time autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
[4/4] LINK ./chrome

real    0m21.574s
user    0m20.022s
sys     0m5.478s

ということで、 「コードを書き換えながら、chrome をBuild して実行することができる」という状態も実現できました!🎉

まとめ

事前に Build 済みの Object File を GCS からダウンロードすることで、再ビルドの手間を避けるアプローチを試しました。その結果、前回の「ゼロからの Build」ではトータルで 7 時間かかったのに対して、今回は 30 分程度で「Chromium を Build して動かすまでのサイクルを試す」ことが出来るようになりました。

このように、「事前に用意した Build 成果物」をうまく活用することで、再ビルドの時間的・マシン的コストは劇的に下げることができます。この記事が、Chromium を試しに触ってみることへの障壁を下げることに少しでも貢献していれば幸いです。

Chromium をゼロから Build して動かしてみる

はじめに

ソフトウェアに対しての理解を深めたい場合、どうするでしょうか?

1つの方法は、ソースコードを読むことです。よく出来たソフトウェアのソースコードを読むことで、様々な学びを得る事が出来ます。

しかし、ソースコードだけでは挙動が掴みづらかったり、自分の理解に自信が持てない時もあります。そういった時は、動かしながら試すというアプローチも選択肢になるでしょう。「少し書き換えて動かしてみる」というサイクルを繰り返す事で、自分の中のメンタルモデルの正しさを確認しながらソースコードを読み進める事が出来ます。

今日は、巨大な OSS として有名な Chromium を対象に、「ソースコードから Build して動かしてみる」ことにします。Build だけでも大変なので、いったんは「Build して動かす」ところまでです。これをきっかけに Chromium に対して理解を深めたいと思います。

TL; DR

  • Checking out and building Chromium on Linux に従って作業を進めれば、特にハマることなく Linux 上で Chromium の Build が可能。ただし、何も高速化の工夫をしない場合は 8core, 32GiB memory の GCP VM instance で 6-7 時間程度かかる。
  • 一度 Build すると、それ以降は変更した file に依存するものだけが Build 対象になるので、Build は十数秒程度に高速化する。
  • Build した Chromium はページの取得やレンダーが可能。「自分が Build した Chromium」がちゃんと動くことを確認できた。

参照するもの: Chromium の Document

Chromium のページを見ていると、様々な Document が見つかります。 その中で、「LinuxChromium を Check out して Build する手順」がまとまっているのが以下の Document です。

今日は、このドキュメントを元に作業を進めてみます。

ステップ1. Linux マシンを用意する

まず始めに、作業を行うための Linux マシンを用意する必要があります。 再現性を持たせるために、今回は GCP の Compute Engine で以下の VM Instance を立ててそこで作業を行うことにします。

  • 8core, 32GiB memory (E2, e2-standard-8)
  • 200GB SSD
  • image: Ubuntu 20.04 LTS
  • zone: asia-northeast1-b

以下のコマンドで ssh して、そこで作業を行います。

$ gcloud beta compute ssh --zone "asia-northeast1-b" <instance 名>

ステップ2. 環境をセットアップ

まずは、Build に必要な様々な tool 群である depot_tools を install します。これで、fetch などのコマンドが使えるようになります。

minami@chromium-experiment:~$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
minami@chromium-experiment:~$ export PATH="$PATH:${PWD}/depot_tools"
minami@chromium-experiment:~$ which fetch
/home/minami/depot_tools/fetch

次に、作業用 dicretory として chromium directory を作ります。

minami@chromium-experiment:~$ mkdir ~/chromium && cd ~/chromium

depot_toolsfetchソースコードを取得します。

minami@chromium-experiment:~/chromium$ fetch --no-history --nohooks chromium
Running: gclient root
WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will be created.
Running: gclient config --spec 'solutions = [
{
"name": "src",
"url": "https://chromium.googlesource.com/chromium/src.git",
"managed": False,
"custom_deps": {},
"custom_vars": {},
},
]
'
Running: gclient sync --nohooks --no-history

________ running 'git -c core.deltaBaseCacheLimit=2g clone --no-checkout --progress https://chromium.googlesource.com/chromium/src.git --depth=1 /home/minami/chromium/_gclient_src_p28570xw' in '/home/minami/chromium'
Cloning into '/home/minami/chromium/_gclient_src_p28570xw'...
remote: Counting objects: 352926, done
remote: Finding sources: 100% (352926/352926)
remote: Total 352926 (delta 78472), reused 232647 (delta 78472)
Receiving objects: 100% (352926/352926), 1.10 GiB | 15.97 MiB/s, done.
Resolving deltas: 100% (78472/78472), done.
Syncing projects: 98% (122/124) src/third_party/tflite/src
[0:07:56] Still working on:
[0:07:56] src/v8
[0:07:56] src/third_party/angle/third_party/VK-GL-CTS/src
.
.
.
Syncing projects: 99% (123/124) src/v8
.
.
.
Syncing projects: 100% (124/124), done.
Running: git submodule foreach 'git config -f $toplevel/.git/config submodule.$name.ignore all'
Running: git config diff.ignoreSubmodules all

これで、Chromiumソースコード取得が完了しました。

次に、dependency の install を行います。まずは、python2 を install します。

minami@chromium-experiment:~/chromium/src$ sudo apt update
minami@chromium-experiment:~/chromium/src$ sudo apt install -y python2
minami@chromium-experiment:~/chromium/src$ which python2
/usr/bin/python2
minami@chromium-experiment:~/chromium/src$ python2 -V
Python 2.7.18

minami@chromium-experiment:~/chromium/src$ sudo ln -s /usr/bin/python2 /usr/local/bin/python
minami@chromium-experiment:~/chromium/src$ which python
/usr/local/bin/python
minami@chromium-experiment:~/chromium/src$ python -V
Python 2.7.18

さらに、./build/install-build-deps.sh を実行します。これで、Build に必要な package などの install が行われます。これはしばらく時間がかかります。

minami@chromium-experiment:~/chromium/src$ ./build/install-build-deps.sh
Running as non-root user.
You might have to enter your password one or more times for 'sudo'.
.
.
.
Installing locales.
Generating locales (this might take a while)...
da_DK.UTF-8... done
en_US.UTF-8... done
fr_FR.UTF-8... done
he_IL.UTF-8... done
zh_TW.UTF-8... done
Generation complete.

さらに、$ gclient runhooks を実行します。

minami@chromium-experiment:~/chromium/src$ gclient runhooks
Hook 'vpython src/build/landmines.py' took 45.92 secs
Running hooks: 6% ( 6/97) nacltools
________ running 'vpython src/build/download_nacl_toolchains.py --mode nacl_core_sdk sync --extract' in '/home/minami/chromium'
INFO: --Syncing arm_trusted to revision 2--
.
.
.
Running hooks: 61% (60/97) subresource-filter-ruleset
________ running 'vpython src/third_party/depot_tools/download_from_google_storage.py --no_resume --no_auth --bucket chromium-ads-detection -s src/third_party/subresource-filter-ruleset/data/UnindexedRules.sha1' in '/home/minami/chromium'
0> Downloading src/third_party/subresource-filter-ruleset/data/UnindexedRules@83bced6c2676ed8d7c57a84c9a8d4f76c08f79e2...
Downloading 1 files took 5.270337 second(s)
Running hooks: 100% (97/97), done.

これで、Chromiumソースコード全体と、Chromium の build に必要な dependency が揃いました。

ステップ3. Chromium の Build を行う

いよいよ Chromium の Build を行います。ChromiumNinja をメインの Build tool として利用するのですが、そのための Build directory および Configuration を GN というツールを利用して生成します。今回は out/Default という Build directory を生成します。

minami@chromium-experiment:~/chromium/src$ gn gen out/Default
Done. Made 17175 targets from 2693 files in 6820ms

Checking out and building Chromium on Linux を見る限りでは、 $ gn args out/Default を実行して、Build を高速化するための様々な設定を追加で行うことが出来るようですが、一旦何も追加設定はせずに進みます。

Ninja を利用した Chromium の Build は、$ autoninja -C out/Default chrome で行うことが出来るようです。このコマンドを実行して Build を開始します。

minami@chromium-experiment:~/chromium/src$ autoninja -C out/Default chrome

これで、Build が開始しました。Chromium の Build はとても時間がかかります(自分の環境だと Build が終わるまでに 5時間半 かかりました。長いです)。

待っている間に別の shell で $ ps fax を実行して Process tree を見てみると、以下のような結果になっていることが分かります。autoninja は内部で ninja-linux64 を実行しているようです。-j 10 がオプションとして渡されてるので、10 並列で Build が行われているように見えます。

 1783 ?        Ss     0:00  \_ sshd: minami [priv]
 1853 ?        S      0:01      \_ sshd: minami@pts/1
 1854 pts/1    Ss     0:00          \_ -bash
56626 pts/1    S+     0:00              \_ bash /home/minami/depot_tools/autoninja -C out/Default chrome
56760 pts/1    S+     0:03                  \_ /home/minami/depot_tools/ninja-linux64 -C out/Default chrome -j 10
61079 pts/1    S      0:00                      \_ /bin/sh -c ../../third_party/llvm-build/Release+Asserts/bin/clang -MMD -MF obj/native_client/src/t
61084 pts/1    R      0:02                      |   \_ ../../third_party/llvm-build/Release+Asserts/bin/clang -MMD -MF obj/native_client/src/trusted/
61090 pts/1    S      0:00                      \_ /bin/sh -c ../../third_party/llvm-build/Release+Asserts/bin/clang -MMD -MF obj/native_client/src/t
61091 pts/1    R      0:02                      |   \_ ../../third_party/llvm-build/Release+Asserts/bin/clang -MMD -MF obj/native_client/src/trusted/
61476 pts/1    S      0:00                      \_ /bin/sh -c python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylib
61479 pts/1    R      0:00                      |   \_ python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylibs -o ge
61482 pts/1    S      0:00                      \_ /bin/sh -c python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylib
61483 pts/1    R      0:00                      |   \_ python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylibs -o ge
61486 pts/1    S      0:00                      \_ /bin/sh -c ../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/ppapi/cpp/objects
61488 pts/1    R      0:00                      |   \_ ../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/ppapi/cpp/objects/var.o.
61492 pts/1    S      0:00                      \_ /bin/sh -c python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylib
61495 pts/1    S      0:00                      |   \_ python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylibs -o ge
61506 pts/1    R      0:00                      |       \_ /bin/sh /sbin/ldconfig -p
61497 pts/1    S      0:00                      \_ /bin/sh -c python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylib
61498 pts/1    S      0:00                      |   \_ python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylibs -o ge
61501 pts/1    S      0:00                      \_ /bin/sh -c ../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/ppapi/cpp/objects
61502 pts/1    R      0:00                      |   \_ ../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/ppapi/cpp/objects/var_ar
61503 pts/1    S      0:00                      \_ /bin/sh -c ../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/ppapi/cpp/objects
61505 pts/1    R      0:00                      |   \_ ../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/ppapi/cpp/objects/var_ar
61504 pts/1    S      0:00                      \_ /bin/sh -c python ../../mojo/public/tools/bindings/mojom_bindings_generator.py --use_bundled_pylib

待ち続ければ、Build が終わります。終了時には以下のような表示になっています。

minami@chromium-experiment:~/chromium/src$ autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
[52205/52205] LINK ./chrome

Build が終わると、Build directory である out/Default の中に様々な成果物が作られています。

minami@chromium-experiment:~/chromium/src$ ls -1 out/Default/
absl_hardening_tests.runtime_deps
accessibility_perftests.runtime_deps
accessibility_unittests.runtime_deps
.
.
.
zucchini_integration_test.runtime_deps
zucchini_unittests.runtime_deps
zxcvbn_unittests.runtime_deps

その中に chrome という名前の binary があり、それが「chrome browser」になっています。

minami@chromium-experiment:~/chromium/src$ ls -la out/Default/chrome
-rwxrwxr-x 1 minami minami 1058842464 Dec 12 18:22 out/Default/chrome

以下のように version を確認する事もできます。

minami@chromium-experiment:~/chromium/src$ ./out/Default/chrome --version
Chromium 89.0.4354.0

無事、Chromium を Build することが出来ました 🎉

ステップ4. Build した chrome binary を実行してみる

せっかくなので、Build した chrome binary を実行してみましょう。

まず、何も考えずに $ ./out/Default/chrome で実行してみます。すると、以下のようにエラーになります。Unable to open X display. と出ているので、display 周りの設定が必要なようです。

minami@chromium-experiment:~/chromium/src$ ./out/Default/chrome
[203604:203604:1212/184031.352086:INFO:content_main_runner_impl.cc(1027)] Chrome is running in full browser mode.
[203604:203604:1212/184031.944147:ERROR:browser_main_loop.cc(1429)] Unable to open X display.
[203604:203604:1212/184031.944774:FATAL:tracing_controller_impl.cc(390)] Check failed: g_tracing_controller.
.
.
.
Calling _exit(1). Core file will not be generated.

minami@chromium-experiment:~/chromium/src$ ./out/Default/chrome
[203604:203604:1212/184031.352086:INFO:content_main_runner_impl.cc(1027)] Chrome is running in full browser mode.
[203604:203604:1212/184031.944147:ERROR:browser_main_loop.cc(1429)] Unable to open X display.
[203604:203604:1212/184031.944774:FATAL:tracing_controller_impl.cc(390)] Check failed: g_tracing_controller.
#0 0x7fb4a0717aff base::debug::CollectStackTrace()
#1 0x7fb4a04a12fa base::debug::StackTrace::StackTrace()
#2 0x7fb4a04a12b5 base::debug::StackTrace::StackTrace()
#3 0x7fb4a04ed039 logging::LogMessage::~LogMessage()
#4 0x7fb4a04ed779 logging::LogMessage::~LogMessage()
#5 0x7fb4a0460b4b logging::CheckError::~CheckError()
#6 0x7fb498aa543f content::TracingControllerImpl::GetInstance()
#7 0x7fb49780d847 content::BrowserMainRunnerImpl::Shutdown()
#8 0x7fb49780cd14 content::BrowserMainRunnerImpl::~BrowserMainRunnerImpl()
#9 0x7fb49780cd69 content::BrowserMainRunnerImpl::~BrowserMainRunnerImpl()
#10 0x7fb4977fb75c std::__Cr::default_delete<>::operator()()
#11 0x7fb4977fb6ea std::__Cr::unique_ptr<>::reset()
#12 0x7fb4977fb649 std::__Cr::unique_ptr<>::~unique_ptr()
#13 0x7fb4977fb280 content::BrowserMain()
#14 0x7fb4999c16d6 content::RunBrowserProcessMain()
#15 0x7fb4999c2cf9 content::ContentMainRunnerImpl::RunBrowser()
#16 0x7fb4999c25e7 content::ContentMainRunnerImpl::Run()
#17 0x7fb4999bf915 content::RunContentProcess()
#18 0x7fb4999c02ad content::ContentMain()
#19 0x55dd5fee70cb ChromeMain
#20 0x55dd5fee6f82 main
#21 0x7fb46988f0b3 __libc_start_main
#22 0x55dd5fee6e7a _start

Received signal 6
#0 0x7fb4a0717aff base::debug::CollectStackTrace()
#1 0x7fb4a04a12fa base::debug::StackTrace::StackTrace()
#2 0x7fb4a04a12b5 base::debug::StackTrace::StackTrace()
#3 0x7fb4a07175cb base::debug::(anonymous namespace)::StackDumpSignalHandler()
#4 0x7fb46a1f23c0 (/usr/lib/x86_64-linux-gnu/libpthread-2.31.so+0x153bf)
#5 0x7fb4698ae18b gsignal
#6 0x7fb46988d859 abort
#7 0x7fb4a0716c06 base::debug::(anonymous namespace)::DebugBreak()
#8 0x7fb4a0716be5 base::debug::BreakDebugger()
#9 0x7fb4a04ed639 logging::LogMessage::~LogMessage()
#10 0x7fb4a04ed779 logging::LogMessage::~LogMessage()
#11 0x7fb4a0460b4b logging::CheckError::~CheckError()
#12 0x7fb498aa543f content::TracingControllerImpl::GetInstance()
#13 0x7fb49780d847 content::BrowserMainRunnerImpl::Shutdown()
#14 0x7fb49780cd14 content::BrowserMainRunnerImpl::~BrowserMainRunnerImpl()
#15 0x7fb49780cd69 content::BrowserMainRunnerImpl::~BrowserMainRunnerImpl()
#16 0x7fb4977fb75c std::__Cr::default_delete<>::operator()()
#17 0x7fb4977fb6ea std::__Cr::unique_ptr<>::reset()
#18 0x7fb4977fb649 std::__Cr::unique_ptr<>::~unique_ptr()
#19 0x7fb4977fb280 content::BrowserMain()
#20 0x7fb4999c16d6 content::RunBrowserProcessMain()
#21 0x7fb4999c2cf9 content::ContentMainRunnerImpl::RunBrowser()
#22 0x7fb4999c25e7 content::ContentMainRunnerImpl::Run()
#23 0x7fb4999bf915 content::RunContentProcess()
#24 0x7fb4999c02ad content::ContentMain()
#25 0x55dd5fee70cb ChromeMain
#26 0x55dd5fee6f82 main
#27 0x7fb46988f0b3 __libc_start_main
#28 0x55dd5fee6e7a _start
r8: 0000000000000000 r9: 00007ffd3f1f1e30 r10: 0000000000000008 r11: 0000000000000246
r12: 000055dd5fee6e50 r13: 00007ffd3f1f4180 r14: 0000000000000000 r15: 0000000000000000
di: 0000000000000002 si: 00007ffd3f1f1e30 bp: 00007ffd3f1f2080 bx: 00007fb45b9f0340
dx: 0000000000000000 ax: 0000000000000000 cx: 00007fb4698ae18b sp: 00007ffd3f1f1e30
ip: 00007fb4698ae18b efl: 0000000000000246 cgf: 002b000000000033 erf: 0000000000000000
trp: 0000000000000000 msk: 0000000000000000 cr2: 0000000000000000
[end of stack trace]
Calling _exit(1). Core file will not be generated.

そこで、Getting Started with Headless Chrome を参考に、headless mode で Chromium を起動してみることにします。以下のように --headless オプションを渡して chrome binary を実行することで、ちゃんと起動することが確認できました 🎉

minami@chromium-experiment:~/chromium/src$ ./chromium/src/out/Default/chrome --headless --disable-gpu --remote-debugging-port=9222 https://www.chromestatus.com
[0124/153340.940562:WARNING:headless_content_main_delegate.cc(530)] Cannot create Pref Service with no user data dir.
[0124/153340.942433:INFO:content_main_runner_impl.cc(1027)] Chrome is running in full browser mode.

DevTools listening on ws://127.0.0.1:9222/devtools/browser/bab01154-70d7-4b82-8bcb-b8beec05a94b
[0124/153345.275372:WARNING:http_cache_transaction.cc(1192)] Unable to open or create cache entry
[0124/153345.293075:WARNING:http_cache_transaction.cc(1192)] Unable to open or create cache entry

もう少し遊んでみましょう。Chromium は、--dump-dom オプションを渡すことで、指定した URL のページを取得して document.body.innerHTML の内容を標準出力に print してくれます。試しに実行してみると、以下のようにちゃんと https://example.com のページからコンテンツを取得した上で、その内容を print してくれます。自分で Build した Chromium が、ちゃんとページ取得の動作をすることが確認できました 🎉

minami@chromium-experiment:~/chromium/src$ ./out/Default/chrome --headless --disable-gpu --dump-dom https://example.com
[0124/155618.269750:WARNING:headless_content_main_delegate.cc(530)] Cannot create Pref Service with no user data dir.
[0124/155618.271645:INFO:content_main_runner_impl.cc(1027)] Chrome is running in full browser mode.
<!DOCTYPE html>
<html><head>
    <title>Example Domain</title>

    <meta charset="utf-8">
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 2em;
        background-color: #fdfdff;
        border-radius: 0.5em;
        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        div {
            margin: 0 auto;
            width: auto;
        }
    }
    </style>
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is for use in illustrative examples in documents. You may use this
    domain in literature without prior coordination or asking for permission.</p>
    <p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>


</body></html>
[0124/155619.646701:ERROR:process_posix.cc(335)] Unable to terminate process 2345: No such process (3)
[0124/155619.646976:WARNING:internal_linux.cc(66)] Failed to read /proc/2345/stat
[0124/155619.650882:WARNING:discardable_shared_memory_manager.cc(432)] Some MojoDiscardableSharedMemoryManagerImpls are still alive. They will be leaked.

さらに、--screenshot オプションを渡すことで、スクリーンショットを撮ることもできます。試しに https://example.com に対して実行してみると、以下のように screenshot.png という画像ファイルが生成されます。

minami@chromium-experiment:~/chromium/src$ ./out/Default/chrome --headless --disable-gpu --screenshot https://example.com
[0124/162636.934902:WARNING:headless_content_main_delegate.cc(530)] Cannot create Pref Service with no user data dir.
[0124/162636.936935:INFO:content_main_runner_impl.cc(1027)] Chrome is running in full browser mode.
[0124/162638.402065:INFO:headless_shell.cc(616)] Written to file screenshot.png.
[0124/162638.466196:ERROR:process_posix.cc(335)] Unable to terminate process 3074: No such process (3)
[0124/162638.466571:WARNING:internal_linux.cc(66)] Failed to read /proc/3074/stat
[0124/162638.469149:WARNING:discardable_shared_memory_manager.cc(432)] Some MojoDiscardableSharedMemoryManagerImpls are still alive. They will be leaked.
minami@chromium-dev-20210123:~/chromium/src$ gsutil cp ./screenshot.png gs://chromium-dev-south37/screenshot.png
Copying file://./screenshot.png [Content-Type=image/png]...
/ \[1 files\][ 26.4 KiB/ 26.4 KiB]
Operation completed over 1 objects/26.4 KiB.

minami@chromium-dev-20210123:~/chromium/src$ ls -la screenshot.png
-rw------- 1 minami minami 27069 Jan 24 16:26 screenshot.png

screenshot.png をダウンロードして内容を見てみると、以下のような内容になっています。これは https://example.com を手元の Chrome で開いた時と同一の内容になっています。自分で Build した Chromium が、ちゃんとページを Render し、さらにスクリーンショットまで撮れることが確認できました 🎉

ステップ5. 高速な Build を体感する

Chromium をゼロから Build する」というステップ3では、かなりの時間がかかりました。一方、一度 Build した状態からでは、高速な Build が行えるようになります。これは、Ninja が最終成果物からの依存関係を check していて、「依存先に変更があった場合のみ、再 Build を行う」ようになっているためです(注: Ninja だけでなく、Make も同様の挙動をします)。

例えば、何も書き換えない状態で $ autoninja -C out/Default chrome を実行した場合は、Build は 5 秒程度で終了します。この場合、ソースコードに変更はないので chrome binary にも変更は行われません。

minami@chromium-experiment:~/chromium/src$ time autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
ninja: no work to do.

real    0m5.676s
user    0m4.074s
sys     0m0.919s

次に、chrome/common/channel_info.cc という file を touch して timestamp を更新してみます。以下の document によれば Ninja は Make と同様に file timestamp を利用して更新を check しているらしいので、これで依存する file は Build が行われるはずです。

Ninja is closest in spirit and functionality to Make, relying on simple dependencies between file timestamps.

cf. https://ninja-build.org/manual.html

今度は、以下のように obj/chrome/common/channel_info/channel_info.o の Build が行われる様子が観測できました。さらに、時間経過とともに LINK などの step に進み、15 秒程度で chrome binary の Build が終了します。

minami@chromium-experiment:~/chromium/src$ touch chrome/common/channel_info.cc
minami@chromium-experiment:~/chromium/src$ time autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
[0/4] CXX obj/chrome/common/channel_info/channel_info.o
minami@chromium-experiment:~/chromium/src$ time autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
[2/4] SOLINK ./libvr_common.so
minami@chromium-experiment:~/chromium/src$ time autoninja -C out/Default chrome
ninja: Entering directory `out/Default'
[4/4] LINK ./chrome

real    0m15.238s
user    0m18.995s
sys     0m4.492s

何かしら file を変更した場合でも、依存した file だけが Build 対象となるため、「Chromium をゼロから Build する」というステップ3に比べると 圧倒的に高速に Build が出来る ことが分かります。

また、chrome binary の Build の様子を見ていると、LINK ステップにかかる時間が長いことに気づきます。 lld の作者の rui さんが「Link にかかる時間を減らすことが開発生産性に繋がる」という話をしているのを聞いた記憶がありますが、それが実感できる機会となりました。

まとめ

ChromiumLinux 上でゼロから Build して動かしてみました。トータルでかかった時間は6-7時間程度とかなり長いので気軽に試すのは難しいですが、「巨大なソフトウェアであっても Build して動かすことが出来る」ということが確かめられたと思います。

また、一度動くところまで持っていけば、「少し変えて動かして試す」ということが簡単に出来るようになるので、ソフトウェアに対しての理解を深めやすくなります。これをきっかけに Chromium に対して理解を深めたいと思います。

Chrome の Back button を押した際に「意図しない Cache」が利用されて、期待と違うページが表示される問題について調査した

以前、同僚と話していた際に「同じ URL で Accept request header に応じて response の Content-Type を変えていると、Chrome で Back button を押した際の結果が意図しないものになる」ということが話題になりました。

これは、自分も過去に経験した事があり、不思議に思っていた部分です。SafariFirefox では発生せず、Chrome だけで発生するというのもポイントです。この挙動について、今回は調査してみました。

TL; DR

今回の調査内容を簡潔にまとめると、以下のようになります。

  • 同じ URL で Accept request header に応じて response の Content-Type を変えると、Chrome では Back button を押した際に意図しないコンテンツが表示される事がある。SafariFirefox ではこの現象は発生しない。
  • この現象は、Chrome の Back button を押した際の「Cache の使われ方」に起因して発生する。この部分は Formal standard が存在しないため、Browser によって挙動が違う。
  • 上記の挙動は、Response Header に Vary: Accept をつけること、あるいは Cache-Control: no-store をつけることで回避が可能。また、今回は Accept header に注目したが、それ以外にも request header の内容に応じて resnponse が変わる場合は Vary: <対象の request header> を指定する事が推奨される。

上記の内容について、以下で順番に説明いたします。

再現手順について

同じ URL で Accept request header に応じて response の Content-Type を変えていると、Chrome で Back button を押した際の結果が意図しないものになる」というのが今回注目したい挙動です。

まずはじめに、この挙動を再現するためのデモサイトを作ってみました。以下に再現動画も載せています。動画を見て頂ければわかると思いますが、「/index を開いた状態で、"Other Page" リンクをクリックしてから Back button をクリックして元のページに戻ると、{"message":"Hello from an index (application/json)"} という JSON response が表示される」という挙動になっています。

Chrome の Back button では「先ほど開いたページが表示されること」を期待するので、期待とは違う挙動になっていることが分かります

なお、デモサイトの URL および GitHub 上で公開したソースコードは以下になります。また、OS version, Chrome の version も以下にまとめています。

何が起きているのか?

さて、それでは何が起きているのでしょうか?挙動を ChromeDeveloper Tools の Network panel で確かめてみます。

まず、Chrome で Back Button を押した時に表示される GET /index request の表示を見てみます。ここでは、Size として (disk cache) が表示されています。どうやらページを表示する際に Cache が利用されていそうです。

f:id:south37:20210111002027p:plain

実は、ここで表示されたコンテンツは「GET /index を HTML として開いた際に、JavaScript から Fetch APIGET /indexJSON を request して取得した内容の Cache」となっています。つまり、「意図したもの(= /index の HTML response)とは違う Cache」が利用されてしまっているということです。

では、なぜ「意図しない Cache」が利用されてしまっているのでしょうか?この「意図しない Cache の謎」を探りたいところですが、その前に「最初に /index ページを開いた際に何が起きていたか」をもう少し正確に理解してみましょう。この振る舞いについては、実際にコードを見てみるほうが理解が簡単です。上記のデモサイトは以下のようなコードで動いています。

// main.go
package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/", fs)
    http.HandleFunc("/index", handleIndex)
    http.HandleFunc("/other", handleOther)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
    acceptMediaType := r.Header["Accept"]
    if len(acceptMediaType) == 1 && acceptMediaType[0] == "application/json" {
        handleIndexJSON(w, r)
    } else {
        handleIndexHTML(w, r)
    }
}

func handleIndexJSON(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    io.WriteString(w, `{"message":"Hello from an index (application/json)"}`)
}

func handleIndexHTML(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")
    io.WriteString(w, `<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Index Page</title>
    <script src="/index.js"></script>
  <body>
    <div>Hello from an Index Page!</div>
    <a href="/other">Other Page</a>
  </body>
</html>
`)
}

func handleOther(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")
    io.WriteString(w, `<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Other Page</title>
  <body>
    <div>Hello from an Other Page!</div>
  </body>
</html>
`)
}
// static/index.js
let myHeaders = new Headers();
myHeaders.append('Accept', 'application/json');
let myInit = { method:  'GET',
               headers: myHeaders,
               cache:   'default' };
let myRequest = new Request('index', myInit);
fetch(myRequest)
  .then(response => console.log(response))
  .catch(err => console.log(err))

簡単に挙動を説明します。まず、GET /index の handler である handleIndex に注目してください。ここでは、「Accept request header の値を check して、application/json であれば JSON response を返す handler(= handleIndexJSON)の呼び出し、そうでなければ HTML response を返す handler(= handleIndexHTML)の呼び出しを行う」というロジックになっています。つまり、Accept request header の内容に応じて、response の Content-Type を変えるという挙動になっている訳です。

handleIndexHTML の response の HTML では、script tag として /index.js を load して実行しています。この中身は static/index.js の中で記述されていて、Fetch API を利用して Accept: application/json request header 付きで GET /index への request をしています(なお、デモなので response の内容を利用してませんが、実際にはここで JSON が描画に利用されるという挙動を想定してます)。

上記の挙動は、以下のように Developer Tools の Network panel をみると分かりやすいでしょう。まず最初に、GET /index への request で  Content-Type: text/html の response を取得します。これが、Network panel で document Type と記載されている request です。次に、HTML に記載された script tag で /index.js がロードされます。これが、Network panel で script Type と記載されている request です。最後に、/index.js に実装された JavaScript コードによって Accept: application/json request header 付きで GET /index への request が行われて、Content-Type: application/json の Response が返されます。これが、Network panel で fetch Type と記載されている request です。

f:id:south37:20210111010051p:plain

そして、"Other Page" リンクをクリックして別のページに遷移した後、Chrome の Back button を click して元のページに戻ると、「Fetch API で取得した response」が (disk cache) として表示されます。これが、発生していた現象の一連の振る舞いです。

この挙動を回避するためにはどうしたら良いのか?

さて、この挙動を回避するには何をしたら良いでしょうか?Cache が関係しているなら、Cache に関しての設定を考え直す必要があるのでしょうか?

まず、自明な解決策としては、「同じ URL で request header に応じて異なる Content-Type のコンテンツを返すのを避ける」という方法が考えられます。 これは実際に効果を発揮します。しかし、場合によってはこのアプローチを現実的ではないケースもあるでしょう。

そこで、Cache の設定を考え直す方法を考えます。例えば、「Cache を許可するが、常に Validation を要求する Content-Type: no-cache 」を response header として指定してみることにしましょう。これは、元々のコードに対して以下のような diff で実現することになります(注: 以下の例では Validation に利用する ETag もつけてます。Validation を適切に扱うには If-None-Match の handling logic も本当は追加する必要がありますが、ここでは省略しています)。

$ git diff
diff --git a/main.go b/main.go
index c00db5d..65c559a 100644
--- a/main.go
+++ b/main.go
@@ -32,11 +32,13 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {

 func handleIndexJSON(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
+       w.Header().Set("Cache-Control", "no-cache")
+       w.Header().Set("ETag", ...)
        io.WriteString(w, `{"message":"Hello from an index (application/json)"}`)
 }

 func handleIndexHTML(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/html")
+       w.Header().Set("Cache-Control", "no-cache")
+       w.Header().Set("ETag", ...)
        io.WriteString(w, `<!DOCTYPE html>
 <html>
   <head>

しかしながら、 実は Content-Type: no-cache という header をつけても、「Chrome の Back button で意図しない Cache が利用される」という挙動を防ぐことはできません 。これは実際に動かしてもらうと分かりますが、Content-Type: no-cache response header をつけて返した response であっても、Back button を利用した場合には (disk cache) が利用されてしまいます。

これは、(少なくとも個人的には)直感に反する挙動です。Content-Type: no-cache response header がついてるコンテンツは、Browser に Cache はされるものの必ず Validation が行われるはずです。つまり、 必ず If-None-Match or If-Modified-Since header をつけて request が行われて、server で同一性が確かめられて 304 が返ってくれば Cache が利用され、そうでなければ 200 として返った response が利用されるはずです(注: Cache validation の詳細は MDN の document である Cache validation - MDN を参照してください)。ところが、Back button で戻った場合には、そもそもこの Validation が行われません。

この挙動について調査をすると、以下の Blog や、この Blog の著者が立てた  chromium の issue にたどり着きました。chromium の issue における回答では、端的に言えば「Browser の back/forward navigation は normal load ではないため、Cache validation を skip するようになっている」と説明されています。つまり、Cache validation が行われないために、Cache が無条件で利用されてしまっている訳です。

This has been filed as #516846. But since there isn't a formal standard for back/forward browser button behavior, it's hard to argue what the right behavior should be.

cf. https://www.mixmax.com/engineering/chrome-back-button-cache-no-store/

Working as intended.

Back/forward navigations retrieve stuff from the cache without revalidation.

cf. https://bugs.chromium.org/p/chromium/issues/detail?id=516846#c3

Because no-cache doesn't mean "don't cache this" (that would be no-store). no-cache means don't use this for normal loads unless the resource is revalidated for freshness. History navigations are not normal loads.

cf. https://bugs.chromium.org/p/chromium/issues/detail?id=516846#c5

さて、Cache-Control: no-cache では「back button を押した際の意図しない Cache の利用を防げない」とすると、どうするのが良いのでしょうか?

解決方法の1つは、「Cache-Control: no-store response header を利用する」というものです。Cache-Control: no-store を指定している場合、chrome はそもそもその response の Cache を行いません。その為、必ず server への request が行われるようになり、その結果として Back button で戻った際に意図しない Cache が利用されるのを防ぐ事ができます。これは、上記 Blog でも説明されている方法です。実際に、以下のように Cache-Control: no-storeを指定すると、Chrome で Back button を押した際に意図通り「/index の HTML response」が表示される事が確認できます。

$ git diff
diff --git a/main.go b/main.go
index c00db5d..332cd53 100644
--- a/main.go
+++ b/main.go
@@ -32,11 +32,13 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {

 func handleIndexJSON(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
+       w.Header().Set("Cache-Control", "no-store")
        io.WriteString(w, `{"message":"Hello from an index (application/json)"}`)
 }

 func handleIndexHTML(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/html")
+       w.Header().Set("Cache-Control", "no-store")
        io.WriteString(w, `<!DOCTYPE html>
 <html>
   <head>

もう1つの方法が、「Vary response header を利用する」という方法です。Vary は「response の内容に影響を与える request header」を指定するための response header で、「Chrome などの Browser」および「CDN などの中間 Cache server」は Vary header の内容を加味した Cache の handling が求められます。具体的には、「URLだけでなく、Vary header に指定した request header も一致する」ことが Cache を利用する条件になります。

実際に、以下のように Vary: Accept response header をつけて「Accept header も一致することを Cache 利用の条件とする」ことで、Chrome で Back button を押した際に意図通り「/index の HTML response」が表示されることが確認できます。

$ git diff
diff --git a/main.go b/main.go
index c00db5d..4d0f527 100644
--- a/main.go
+++ b/main.go
@@ -32,11 +32,13 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {

 func handleIndexJSON(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
+       w.Header().Set("Vary", "Accept")
        io.WriteString(w, `{"message":"Hello from an index (application/json)"}`)
 }

 func handleIndexHTML(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/html")
+       w.Header().Set("Vary", "Accept")
        io.WriteString(w, `<!DOCTYPE html>
 <html>
   <head>

実は、この「Vary header を利用する」という方法は 上記の chromium issue で提案されている方法でもあります。Cache の恩恵を最大限受けつつ、request 内容に応じて違う response を返したい場合には Vary header を使うと良いでしょう。

まとめ

Chrome の Back button を押した際に意図せず利用される Cache の挙動について、その再現手順と対応方法をまとめてみました。以下が、今回調査した内容のまとめになります。

  • 同じ URL で Accept request header に応じて response の Content-Type を変えると、Chrome では Back button を押した際に意図しないコンテンツが表示される事がある。SafariFirefox ではこの現象は発生しない。
  • この現象は、Chrome の Back button を押した際の「Cache の使われ方」に起因して発生する。この部分は Formal standard が存在しないため、Browser によって挙動が違う。
  • 上記の挙動は、Response Header に Vary: Accept をつけること、あるいは Cache-Control: no-store をつけることで回避が可能。また、今回は Accept header に注目したが、それ以外にも request header の内容に応じて resnponse が変わる場合は Vary: <対象の request header> を指定する事が推奨される。

個人的に、直感に反する挙動だったのが「Cache-Control: no-cache を指定してもこの挙動を防げない」という部分です。通常の Browsing (= URL を入力しての遷移やリンクをクリックしてのページ遷移)では、Cache-Control: no-cache header がついた response に対しては必ず Validation が行われるため、Vary header などに気を使う必要がありません。ところが、Chrome の Back button(実は Forward button も)を利用する場合には Validation が行われないため、Vary header を考慮する必要が出てきます。そこが自分にとっては盲点でした。

今回の調査の学びとしては、Cache-Control: no-cache はあくまで「Cache される前提の header」である事に留意しつつ、Cache されるなら基本的には Vary header もちゃんと設定した方が良いと言えそうです。

おまけ: Browser の Back/forward navigation における Cache の挙動の標準化について

今回、Chrome の Back button の挙動について調査をする中で、Browser の Back/forward navigation の Cache として bfcache と呼ばれるものが存在する事がわかりました。

ここで bfcache と呼んでいるものは、 このブログで説明した「(disk cache) の利用」とは全く違うものです 。具体的には、「コンテンツの load や JavaScript の実行などを行った状態の page」を memory に残しておき、Back/forward navigation 時にそれを復元するという挙動のようです。

Google Chrome では、2019 年に bfcache の実装に取り組み始めたというアナウンスが行われています。2019/2/22 の Blog Post では、 bfcache の Demo 動画も公開されています。また、bfcache のドキュメントも公開されています。

cf. https://developers.google.com/web/updates/2019/02/back-forward-cache

cf. https://www.chromestatus.com/feature/5815270035685376

実は、bfcacheFirefoxSafari では何年も前から実装されており、Chrome では version 79 で実装が行われたようです。しかし、Desktop の Chrome では今はデフォルトで無効化されており、chrome://flags から有効化しなければ使えない状態です(Android では Chrome 86 から一部で有効化されたようです)。

Google implements backward-forward cache in Chrome 79 Canary

cf. https://www.ghacks.net/2019/09/20/google-implements-backward-forward-cache-in-chrome-79-canary/

bfcache has been supported in both Firefox and Safari for many years, across desktop and mobile.

Starting in version 86, Chrome has enabled bfcache for cross-site navigations on Android for a small percentage of users. In Chrome 87, bfcache support will be rolled out to all Android users for cross-site navigation, with the intent to support same-site navigation as well in the near future.

cf. https://web.dev/bfcache/#browser-compatibility

これは自分の解釈ではありますが、「SafariFirefoxbfcache を有効化しているのに対して、Chromebfcache をデフォルトでは無効化している」事が原因で、「Chrome だけ、このブログで説明した意図しない Cache 利用が発生している」のではないかと考えられます。実際に、以下のように chrome://flags から Back-forward cacheEnabled force caching all pages (experimental) にすると、今回調査に利用したデモサイトでも Chrome の Back button を押した際にちゃんと意図した挙動になることが確認できました

f:id:south37:20210111015316p:plain

また、「Back/forward navigation における Cache の挙動」に標準仕様がない事が、挙動の差異を生むそもそもの根本原因にも思えます。

実は、「bfcache に対して標準仕様を定めようとする動き」は存在するようです。例えば、以下の issue では「Back/forward navigation における cache (= bfcache) の opt-out の API を決めよう」という提案がなされています。また、「bfcache の標準化の良い出発点になるかもしれない」ということにも言及されています。

 Currently, we only have "implicit" ways to opt-out of bfcache that might have other side effects. A few examples from Firefox's bfcache page and problems associated with them:

...

I think having an explicit API to specifically opt-out of bfcache (and does nothing more than that) might be good, since there might be legitimate cases of not wanting to be bfcached (stale data/state of the previous page, logging in/out, etc.) specifically.

...

This might also be a good starting point into standardizing back-forward cache behavior across browsers.

cf. https://github.com/whatwg/html/issues/5744

これは、WHATWG と呼ばれる「Apple, Mozilla, Google, Microsoft などのブラウザベンダーを中心とする Web 標準を議論する団体」の、HTML Standard を議論するための repository である https://github.com/whatwg/html に対しての issue です。提案をしているのは Chrome の開発者の方のようです(注: WHATWGW3C の関係が気になる方は HTML標準仕様の策定についてW3CとWHATWGが合意 今後はWHATWGのリビングスタンダードが唯一のHTML標準仕様に - ITmedia NEWSなどを参照してください)。

こういった標準化の動きが進み、さらに Chrome でも bfcache がデフォルトで有効化されれば、「Chrome でのみ挙動が異なる」といったことも起きづらくなることが期待出来るでしょう。未来が楽しみですね!

2019、2020 年の振り返り

年が明けて 2021 年になりました。この機会に、2019、2020 年の2年間を振り返ってみたいと思います。

2019年の仕事面の振り返り

2019 年は、全体的にアウトプットが多い年だったように思います。

ブログや登壇では以下のようなアウトプットをしました。

2019年のブログ

2019年の Conference 登壇

2019年の新卒研修講義

2019年のその他 Meetup などの登壇

2019年に仕事で進めたプロジェクトについて

仕事で進めたプロジェクトとしては、まず「2019 年の新卒研修」の「全体設計及び実行を行う責任者」をやりました。2018年までは CTO が片手間で取り組んでいた部分だっただけに手順なども確立されていなかったため、「なぜやるのか(= WHY)、何をやるのか(= WHAT)、どうやるのか(= HOW)」という部分から改めて定義して進めました。社内の各チームリーダーやメンバーへ「新しくチームへ入るメンバーに何を身につけていて欲しいか?」をヒアリングするなど、社内のニーズを拾いながら設計を進められたのは、なかなか良かったんじゃないかと考えています。また、自分自身もいくつか講義を担当しました。取り組んだ内容の詳細はブログにもまとめています。

cf. 2019年度 Wantedly 新卒研修の内容を公開します! | Wantedly Engineer Blog

その後、マイクロサービスのアーキテクチャ改善の一環として、「ユーザーマイクロサービスへの書き込み RPC の実装およびその利用」を進めました。その一貫として payload の Protocol Buffer 化なども進めており、これが後述する「ユーザーマイクロサービスの gRPC 化」を後押ししてくれました。マイクロサービスのアーキテクチャ改善については、 CloudNative Days Tokyo 2019 (= CNDT2019) で話した以下のスライドでも詳細を述べてます。

cf. 理想的なマイクロサービスアーキテクチャを目指す継続的改善 / Re-architecturing of Microservices #CNDT2019 - Speaker Deck

ユーザーマイクロサービスが一段落したあとは、社内で「Ruby の gRPC server として小さいマイクロサービスを新しく作ろう」という動きがあったので、そこに関わる形で gRPC の基盤整備を進めました。これが2019年の年末にかけてのことです。具体的には、gRPC interceptor を揃えたり、社内の共通 library に組み込んで他のマイクロサービスからも簡単に利用出来るようにしたりといったことに取り組みました。その時点ではあまり意識してませんでしたが、振り返るとこれは2020年に取り組むことの助走となっていたように思います。

次に、2020年についても振り返ってみたいと思います。

2020年の仕事面の振り返り

2020 年は登壇回数は減った代わりに、ブログを書く量が増えました。特に意識して変えた訳では無いのですが、COVID-19 もあって外出が減り、その代わりにじっくり考えて文章を書く機会が増えたのかもしれません。

以下が、ブログや登壇の内容です。

2020年のブログ

2020年の Conference 登壇

2020年の新卒研修講義

2020年に仕事で進めたプロジェクトについて

2019年末に取り組んだ gRPC の基盤整備からの延長で、2020年は「ユーザーマイクロサービスの gRPC 化」から始まりました。元々、network latency に苦しんでいたのである程度の効果は期待していたのですが、結果として当初の想定以上に劇的な改善効果がありました 。そこで、それ以降は「社内外に gRPC のインパクトを広める & gRPC を利用するワークフローを整備してその知見を伝える」という事に取り組んでいました。

gRPC について以下の3つのブログを書いたのもその一貫です。特に、「Go 言語を利用するケースでは OSS や企業でかなり一般的に gRPC が利用されているが、Ruby などではまだそこまで一般的ではない。その状況を変えたい、コミュニティが盛り上がって欲しい」というモチベーションが強かったです。

登壇についても、今年は基本的に gRPC の話をしました。

gRPC Conf については、実は元々は存在を知らなかったのですが、上記ブログを読んだ方から存在を教えてもらい、CFP を出しました。

gRPC Conf は COVID-19 の影響を受けて Virtual Event に変わり、開催時期も何度か変更されるなど運営は大変そうでしたが、なんとか開催されて登壇する事が出来ました。以前から海外カンファレンスで話してみたいと思っていたので、一つの目標が叶いました。同僚やCTOには発表練習などかなり協力していただいたので、大変ありがたかったです。gRPC Conf 登壇については、ブログでもまとめています。

cf. gRPC をテーマとした国際カンファレンス gRPC Conf で Wantedly の gRPC 活用事例について話しました! | Wantedly Engineer Blog

CloudNative Days Tokyo 2020 (= CNDT2020) は、引き続きブログと同じモチベーションで、gRPC の利用が盛り上がって欲しいと思い登壇しました。社内では Ruby 以外の言語(Go, Python, Node.js)でも利用が広がっており、新規マイクロサービスでも使われるようになって来ていたので、そのアップデートを盛り込みつつ社外にもその空気感を伝えるようにしました。

gRPC 関連以外では、「社内の推論スコアデプロイ基盤を100倍以上スケーラブルにする」という事にも取り組みました。自分は同僚と比較すると「パフォーマンス改善やメモリ使用量削減などランタイムの動作を最適化する」のがかなり好き & 得意なようで、それを見込まれて取り組んだプロジェクトでした。結果的には良い形で改善が出来たのかなと思ってます。これについてもブログでまとめています。

cf. PoC 活用のすすめ - 社内の推論スコアデプロイ基盤を100倍以上スケーラブルにした話 | Wantedly Engineer Blog

また、2020年の大きなアップデートとしては、Infrastructure Team の Leader になったことも挙げられます。これは、いわゆる Engineering Manager と Tech Lead を合わせたような役割です。以前、Product Team にいたときも Team Leader をやっていた時期はありましたが、改めて取り組む事になりました。

Team の Mission を再定義したり、運用を改善したりするなど、Team Member のパフォーマンスを出すために日々考えて改善に取り組んでいます。この辺りはいずれまたアウトプット出来たらと思っています。 ちなみに、Team Member と考えをすり合わせる中で、自分の嗜好性も見えて来たのが面白いと感じています。具体的には、「継続的に改善が行われる事にモチベーションがある、プロジェクトを進めて価値を生み出す事を楽しいと思う」などは自分の特徴的な部分なのかなと思っています。

2020年について思うこと

2020年を振り返って思うのは、「良い方向にも悪い方向にも予想外の事が多かった」という事です。例えば、COVID-19 がこれほどまでに生活にインパクトを与えるとは、2020年が始まったばかりの時点では完全に予想外でした。Work from Home やそれに合わせたチーム・組織の運用変更、カンファレンスなどの在り方の変化、社会全体の変化など、予想しない形で多くの変化があったように思います。ただ、これらは必ずしも悪いことばかりでは無く、必要に迫られることで生まれた良い変化も多数あったんじゃないかと思います。

仕事面でも、今年やった事の多くは当初計画していたものではありませんでした。gRPC がこれほど急速に社内で浸透するとは思っていませんでしたし、そのインパクトも想像以上でした。gRPC Conf はそもそも存在すら知らないところから登壇に至りました。Infrastructure Team Leader になることも全く予想していませんでした。

一方で、「これまでの積み重ねが結果に繋がった」と感じるシチュエーションは数多くありました。例えばユーザーマイクロサービスはこれまでにインターフェースをちゃんと決めて、そこに実装詳細として通信技術などは隠蔽出来ていたお陰で、gRPC 化を予想以上に簡単に実施できました(Client 側は library の内部実装を変えただけで、library を利用するコードは一切変えずに済みました)。今年はパフォーマンスチューニングなどをする機会も多かったですが、そこではこれまでにハードウェアや OS、ミドルウェアについて勉強してきた知見が活かされていたように思います。分散データベースについてのブログ(= The History of Distributed Databases)も今年書きましたが、これも元々は完全に趣味で調べていた内容をまとめたものでした。社内のシステムのマイクロサービス化が進むなかで、ここで調べた知見は活かされて来ていると感じます。このように、予想外の状況に対して、「これまでの積み重ねで上手く対応出来る」という感覚がありました。

ここから考えてみると、「すぐに返ってくるか分からない投資を継続的にやり続ける」ということが重要なのだと思います。Steve JobsStanford 大学卒業スピーチでも、彼は「振り返ってみたときに点と点をつなぐことができる」と述べています。日々の取り組みが、振り返ってみると今に活かされているのかなと思います。

Again, you can't connect the dots looking forward; you can only connect them looking backward.

cf. https://news.stanford.edu/2005/06/14/jobs-061505/

プライベートの振り返り

プライベートで取り組んでいたことも少し振り返ってみたいと思います。

まずは競技プログラミングについてです。2019年の8月頃、同僚の影響で AtCoder のコンテストに出るようになりました。実は以前アカウントを作って一度だけコンテストに出たことがあったのですが、それっきり放置してしまっており、そこからの復帰です。AtCoder はそこから1年弱続けて、6月頃になんとか青まで行くことが出来ました。PAST も受けてみて、88点上級をとる事ができました。

一時期 CodeforcesTopcoder に出ていた時期もあり、どちらも青までは行く事ができました。

最近は出場回数が減ってしまったものの、競技プログラミングは趣味の1つと言えるくらいには打ち込めたのかなと思います。

その他、英語学習も少しやっていました。自分は Reading はまあ問題ない、Listening はそこそこ、Speaking と Writing が苦手という状態です。Speaking は昔よりはかなりマシになったと思うものの、「話し相手に気を使ってもらう前提で会話が成立する」という状態なので、もっと英語力を向上させたいと考えています。今は DMM 英会話を少しやったり YouTube の英会話の動画(EnglishClass101.com の動画が多い)を見たりしてますが、うまく習慣化してボリュームを増やさねば、と思っています。

2020年末には、プライベートでもブログを書く量を増やしてみました。特にきっかけがあった訳ではないのですが、このころ「自分のために調査した内容を memo に残しておく」という事をやってみたところ、そこからのブログ化が簡単に出来る事に気づいたのが大きかったです。今も memo は増えていっているので、いずれ放出したいと考えています。

プライベート面でも、仕事で書いたのと同様に予想外の事が多くありましたが、充実した日々を過ごせたように思います。

OSS 活動について

OSS 活動は仕事とプライベート両面に関わってくる部分です。まず、仕事としては、「理由がない限りは書いたコードはオープンにしよう」という事を考えてました。実際、多くのコードは以下のように OSS にしています。

また、利用している OSS についても、bug や不自然な挙動など気が付いた事については積極的に issue を立てたり PR を送ったりするように心がけていました。1つ1つの貢献は小さいものではありますが、それでも積み重なることに価値があると信じるようにしています。

まとめ

仕事、プライベート、OSS 活動など様々な面から 2019 年と 2020 年を振り返ってみました。

2020 年は特に予想外のことが多く、改めて未来は予測出来ないと気づかされました。一方でこれまでの積み重ねによって上手く出来たケースも多く、「すぐに返ってくるか分からない投資を継続的にやり続ける」ことの重要性も実感しました。

2021 年は、仕事、プライベート両方で、コンフォートゾーンを抜け出してチャレンジしていくという事を意識したいと思います。「出来る事で勝負する」のはもちろん必要ですが、それ以上に「出来ない事を出来るようになる」経験をまだまだ積んでいきたいと考えています。

kind (Kuberenetes in Docker) に deep dive してみる

kuKubernetes の Install Tools というページでは、kubernetes を local で動かすための tool として kind が紹介されています。今日はこの kind について内部構造及び使い方を見てみます。

kind とは

kind は「Docker container の中で Kubernetes を動かすことが出来るツール」です。kind という命名は「Kubernetes in Docker」から来ていて、K-in-D という頭文字をとったものになっています。

kind については KubeCon + CloudNativeCon で何度か紹介されているようです。例えば KubeCon + CloudNativeCon North America 2019 における以下の "Deep Dive: Kind" というトーク では、「kind とは何か?」について内部実装など含めて紹介されています。

www.youtube.com

上記のトークについて簡単に summary を書くと、kind は以下のようなものとして紹介されています。

  • Docker container として Node(をシミュレートする container)を動かし、その中で Kubernetes を動かすツール
    • Node image の中に、Kubernetes を動かすために必要な全てを詰める
      • kubelet
      • kubeadm
      • docker
      • systemd
      • core images (Kuberentes にとって重要な container image)
        • etcd
        • coredns
        • pause
        • kube-apiserver
        • etc.
  • multi-node 構成も可能
  • Kubernetes を source code から build して動かすことが可能
  • 30s 程度で Kuberenetes cluster を作ることが出来る
  • kind は「Kuberentes 自体の test」のために作られた
    • Kuberenetes の CI は Kubernetes で動いており、全ての test は Pod の中で実行される。そのため、Kuberenetes 自体を test するには、「Kuberentes を container の中で動かす」必要があった。

上記のトークの中で出てくる以下のスライドが、kind の仕組みを端的に示していて分かりやすいと思います。"Node" Container の中で、kubelet や containerd, そして containered を通じて起動された「Kuberentes の動作を支える各種 container」が動いている事が分かります。

f:id:south37:20201230223917p:plain

ここまでで、「kind の内部構造」を説明しました。次に、実際に kind を動かしてみましょう。

kind を動かしてみる

kind の利用方法は実はとても簡単で、README で以下のように言及されている通り「GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0 && kind create cluster を実行するだけ」となっています。

If you have go (1.11+) and docker installed GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0 && kind create cluster is all you need!

cf. https://github.com/kubernetes-sigs/kind#please-see-our-documentation-for-more-in-depth-installation-etc

実際に実行すると、以下のように Kubernetes cluster 作成が進みます。自分の環境では、Kuberentes cluster 作成にかかる時間は 1min 強でした。

(なお、Ensuring node image という step で少し時間がかかりますが、これは後述する「kind が利用する kindest/node という docker image の pull に時間がかかっている」だけです。一度 image pull が完了すると次からは 30s 程度で高速に Kubernetes cluster が作成出来る様になります)。

$ GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0 && kind create cluster
go: downloading sigs.k8s.io/kind v0.9.0
go: downloading github.com/spf13/cobra v1.0.0
go: downloading k8s.io/apimachinery v0.18.8
go: downloading gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
go: downloading github.com/mattn/go-isatty v0.0.12
go: downloading github.com/alessio/shellescape v1.2.2
go: downloading sigs.k8s.io/yaml v1.2.0
go: downloading github.com/pelletier/go-toml v1.8.0
go: downloading github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b
go: downloading golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed
go: downloading github.com/evanphx/json-patch/v5 v5.1.0
GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0  8.93s user 3.64s system 102% cpu 12.228 total

Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
kind create cluster  4.95s user 2.65s system 9% cpu 1:16.71 total

これで既に「Kubernetes cluster が Docker container の中で動いている状態」となっています。簡単ですね!

kind で動く Kubernetes cluster を利用してみる

次に、実際に kind で動く Kuberentes cluster を利用してみましょう。

$ kind create cluster を実行した際の message に Set kubectl context to "kind-kind" と出ていたように、既に kubectl の context は「kind で作成した Kubenetes cluster(= kind-kind という名前の cluster)」に切り替わっています。つまり、この状態で $ kubectl を利用すれば、kind-kind cluster の API server に対して通信が行われるようになっています。

実際、以下のように $ kubectl config current-context$ kubectl cluster-info の結果を見てみると「local で動く kind-kind cluster に context が切り替わっている」事が確認出来ます。

$ kubectl config current-context
kind-kind

$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:55999
KubeDNS is running at https://127.0.0.1:55999/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

$ kubectl で cluster 内の k8s object を見てみましょう。例えば namespace や pod, service を見てみると、以下のような内容になっています。

$ kubectl get namespaces
NAME                 STATUS   AGE
default              Active   11m
kube-node-lease      Active   11m
kube-public          Active   11m
kube-system          Active   11m
local-path-storage   Active   11m

$ kubectl get po --all-namespaces
NAMESPACE            NAME                                         READY   STATUS    RESTARTS   AGE
kube-system          coredns-f9fd979d6-9cgbl                      1/1     Running   0          11m
kube-system          coredns-f9fd979d6-wlnmw                      1/1     Running   0          11m
kube-system          etcd-kind-control-plane                      1/1     Running   0          11m
kube-system          kindnet-phgz9                                1/1     Running   0          11m
kube-system          kube-apiserver-kind-control-plane            1/1     Running   0          11m
kube-system          kube-controller-manager-kind-control-plane   1/1     Running   0          11m
kube-system          kube-proxy-dxx9q                             1/1     Running   0          11m
kube-system          kube-scheduler-kind-control-plane            1/1     Running   0          11m
local-path-storage   local-path-provisioner-78776bfc44-66wln      1/1     Running   0          11m

$ kubectl get svc --all-namespaces
NAMESPACE     NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  18m
kube-system   kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   18m

さらに、適当に deployment と service の追加もしてみましょう。自分が昔作った https://github.com/south37/dumper という「request header を print するだけの Docker container」を動かしてみます。

まず、deployment と service の manifest file を用意します。

# dumper-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: dumper
  name: dumper
  namespace: default
spec:
  selector:
    matchLabels:
      app: dumper
  template:
    metadata:
      annotations:
      labels:
        app: dumper
      name: dumper
    spec:
      containers:
      - image: south37/dumper
        livenessProbe:
          httpGet:
            path: /ping
            port: 8080
        name: dumper
        ports:
        - containerPort: 8080
          name: http
        readinessProbe:
          httpGet:
            path: /ping
            port: 8080
# dumper-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: dumper
  name: dumper
  namespace: default
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: dumper
  type: ClusterIP

次に、これらの manfest file を apply します。

$ kubectl apply -f dumper-deployment.yaml
deployment.apps/dumper created

$ kubectl apply -f dumper-service.yaml
service/dumper created

apply したものがちゃんと作られている事を確認します。

$ kubectl get all -n default
NAME                          READY   STATUS    RESTARTS   AGE
pod/dumper-6465654fdc-qn729   1/1     Running   0          118s

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/dumper       ClusterIP   10.110.60.42   <none>        80/TCP    114s
service/kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   21m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dumper   1/1     1            1           118s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/dumper-6465654fdc   1         1         1       118s

まず、pod の log を見てみます。すると、healthcheck 用の GET /ping の reqeust が来ている事が確認できます。

$ kubectl logs dumper-6465654fdc-qn729
.
.
.
2020/12/30 12:09:55 GET /ping HTTP/1.1
2020/12/30 12:09:55 Host: 10.244.0.5:8080
2020/12/30 12:09:55 User-Agent: kube-probe/1.19
2020/12/30 12:09:55 Accept-Encoding: gzip
2020/12/30 12:09:55 Connection: close

次に、Kuberentes cluster の中から Service を通した request をしてみましょう。 curl が入っている docker image として radial/busyboxplus:curl を使うことにします(注: radial/busyboxplus:curlhttps://kubernetes.io/docs/concepts/services-networking/connect-applications-service/ の中でも利用されてるので、安全な image だと判断して利用してます)。

すると、以下のように Kubernetes cluster 内の pod から $ curl http://dumper.default で 「default namespace の dumper service へ HTTP request」をして、ちゃんと response が返る事を確認出来ました!

$ kubectl run --rm -it busybox --image=radial/busyboxplus:curl
If you don't see a command prompt, try pressing enter.
[ root@busybox:/ ]$ curl http://dumper.default
Hello, dumper!

pod の log を見てみると、ちゃんと上記 request が pod に到達していることも確認できます。

$ kubectl logs dumper-6465654fdc-qn729
.
.
.
2020/12/30 12:12:00 GET / HTTP/1.1
2020/12/30 12:12:00 Host: dumper.default
2020/12/30 12:12:00 User-Agent: curl/7.35.0
2020/12/30 12:12:00 Accept: */*

簡単にではありますが、Kubernetes cluster としての動作が確認できました。

Docker container として動く Node container の中を見てみる

次に、kind の動作をもう少し深掘りしてみます。具体的には、Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 の中で紹介されていた「Node container の動作」をもう少し見てみます。

まず、Kubernetes cluster を一度削除して作り直すことにします。これは、「作成されたばかりの状態の Kubernetes cluster」で実験するための操作です(特に気にならない人は不要です)。

$ kind delete cluster
Deleting cluster "kind" ...

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
kind create cluster  4.62s user 2.53s system 19% cpu 36.518 total

次に、$ kind create cluster を実行した後の状態で $ docker ps すると、kindest/node:v1.19.1 という image の container が起動している事が分かります。これが、kind が利用する「Node container」のようです。

$ docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED              STATUS          PORTS                       NAMES
45a383679dd2   kindest/node:v1.19.1   "/usr/local/bin/entr…"   About a minute ago   Up 59 seconds   127.0.0.1:56856->6443/tcp   kind-control-plane

$ docker exec して Node container の中をみてみましょう。$ ps fax で process 一覧を見てみると、Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 の中で説明されていた通り、Kubernetes の動作を支える様々な process が起動している事が分かります。containerdkubelet など一部を除くと、そのほかの process は /usr/local/bin/containerd-shim-runc-v2 経由で起動している(= container として起動している)ことも分かります。

$ docker exec -it 45a383679dd2 bash
root@kind-control-plane:/# ps fax
    PID TTY      STAT   TIME COMMAND
   2060 pts/1    Ss     0:00 bash
   2189 pts/1    R+     0:00  \_ ps fax
      1 ?        Ss     0:00 /sbin/init
    124 ?        S<s    0:00 /lib/systemd/systemd-journald
    135 ?        Ssl    0:11 /usr/local/bin/containerd
    310 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 1c426469eb3ae09b744f1c116e6798c65886e218271dfa105fba747b4bfde0d3 -address /run/containerd/containerd.soc
    405 ?        Ss     0:00  \_ /pause
    502 ?        Ssl    0:06  \_ kube-scheduler --authentication-kubeconfig=/etc/kubernetes/scheduler.conf --authorization-kubeconfig=/etc/kubernetes/scheduler.conf --bind-address=127.0.0.1 --ku
    311 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 23d54713b4401fb309725273c78961ea5d7f3a5be157d2a139813b9b9611e220 -address /run/containerd/containerd.soc
    418 ?        Ss     0:00  \_ /pause
    620 ?        Ssl    0:20  \_ etcd --advertise-client-urls=https://172.19.0.2:2379 --cert-file=/etc/kubernetes/pki/etcd/server.crt --client-cert-auth=true --data-dir=/var/lib/etcd --initial-a
    318 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id a24a84c14111721de85b657f2bb0b89db44d87e97452ef86690060a0f0fcd3bc -address /run/containerd/containerd.soc
    425 ?        Ss     0:00  \_ /pause
    563 ?        Ssl    1:08  \_ kube-apiserver --advertise-address=172.19.0.2 --allow-privileged=true --authorization-mode=Node,RBAC --client-ca-file=/etc/kubernetes/pki/ca.crt --enable-admissi
    373 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 8a6a56e83a590f4958dbd3262e4b0adbe36acad229ebcb54ef13c657f39c2c0a -address /run/containerd/containerd.soc
    433 ?        Ss     0:00  \_ /pause
    548 ?        Ssl    0:24  \_ kube-controller-manager --allocate-node-cidrs=true --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf --authorization-kubeconfig=/etc/kubernetes
    667 ?        Ssl    0:25 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --cont
    797 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 841020b0c947c1a8c2047a542268bf3dd91f8fb6b15cfc0a993dc61c0769e660 -address /run/containerd/containerd.soc
    819 ?        Ss     0:00  \_ /pause
    898 ?        Ssl    0:00  \_ /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=kind-control-plane
    833 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 6171f628b9c0658a1983b198160fe76b53eeb4118aa8a0c71491ff5516453268 -address /run/containerd/containerd.soc
    864 ?        Ss     0:00  \_ /pause
    948 ?        Ssl    0:00  \_ /bin/kindnetd
   1110 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 1fd32fb832a5e3782d1f39b26b99a30de8872a0a69600a77db3631f274eeb819 -address /run/containerd/containerd.soc
   1153 ?        Ss     0:00  \_ /pause
   1226 ?        Ssl    0:03  \_ /coredns -conf /etc/coredns/Corefile
   1112 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 725b634f7fc5d93ebfb1e63afb8aabcd936e6297ed7ab552e314e1cc90efeec5 -address /run/containerd/containerd.soc
   1160 ?        Ss     0:00  \_ /pause
   1229 ?        Ssl    0:01  \_ local-path-provisioner --debug start --helper-image k8s.gcr.io/build-image/debian-base:v2.1.0 --config /etc/config/config.json
   1350 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 9d07db434b356d9fddb2ae31715adc1209406a604b7068560f379832f5d79ba2 -address /run/containerd/containerd.soc
   1373 ?        Ss     0:00  \_ /pause
   1404 ?        Ssl    0:03  \_ /coredns -conf /etc/coredns/Corefile

containerd で起動している container は、containerd の CLI tool である ctr で管理する事が出来るはずです。少し見てみましょう。

まず、namespace 一覧を見てみると k8s.io という名前の namespace が見つかります(注: この namespace は kuberentes の namespace とは無関係で、「containerd が container を管理する際に利用する namespace 機能」のはずです)。

root@kind-control-plane:/# ctr namespaces ls
NAME   LABELS
k8s.io

次に、この k8s.io namespace 内の container 一覧を $ ctr --namespace k8s.io containers ls で見てみると、予想通り「Kuberentes cluster の動作に利用される container 一覧」をみる事が出来ました。

root@kind-control-plane:/# ctr --namespace k8s.io containers ls
CONTAINER                                                           IMAGE                                                                      RUNTIME
0e7fc11f71638b86f8fd41f046101ebcb16b48976f06826add2d35df4e2ccc10    k8s.gcr.io/kube-controller-manager:v1.19.1                                 io.containerd.runc.v2
114d8f7f34ebc00c00236d7a111961193a6fa300dc90a4114385134f9eeda412    k8s.gcr.io/kube-proxy:v1.19.1                                              io.containerd.runc.v2
1c426469eb3ae09b744f1c116e6798c65886e218271dfa105fba747b4bfde0d3    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
1fd32fb832a5e3782d1f39b26b99a30de8872a0a69600a77db3631f274eeb819    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
2299e2a7b2b7afbb0789b30a4d7f4e57220a650f7368c04439e91c72e5049356    k8s.gcr.io/kube-apiserver:v1.19.1                                          io.containerd.runc.v2
23d54713b4401fb309725273c78961ea5d7f3a5be157d2a139813b9b9611e220    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
35ab22ae753e043553188810bddfec43aa048d8c93f1ca38cf868ee31dbe06fc    k8s.gcr.io/coredns:1.7.0                                                   io.containerd.runc.v2
6171f628b9c0658a1983b198160fe76b53eeb4118aa8a0c71491ff5516453268    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
6212a3ea51397a8491a8defb188faa1c9afb4b678a8fa102ab15ba8c78f98aa2    sha256:0369cf4303ffdb467dc219990960a9baa8512a54b0ad9283eaf55bd6c0adb934    io.containerd.runc.v2
624a9cf197014dcdbf0be5ddd68995566d14ceab00c8ca18fd51eb35cfe999cb    k8s.gcr.io/kube-scheduler:v1.19.1                                          io.containerd.runc.v2
62d494fe1a94a396494ecd30cfa8538db2e1d2055fedac216d19fd21332d3841    sha256:b77790820d01598b2c56f823fa489e3f56be2cb5d6f7dd9eecd68a1995b89c13    io.containerd.runc.v2
725b634f7fc5d93ebfb1e63afb8aabcd936e6297ed7ab552e314e1cc90efeec5    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
841020b0c947c1a8c2047a542268bf3dd91f8fb6b15cfc0a993dc61c0769e660    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
8a6a56e83a590f4958dbd3262e4b0adbe36acad229ebcb54ef13c657f39c2c0a    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
9d07db434b356d9fddb2ae31715adc1209406a604b7068560f379832f5d79ba2    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
a24a84c14111721de85b657f2bb0b89db44d87e97452ef86690060a0f0fcd3bc    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
b7775d2582ea9fdd481cf308d9c8bafa28fffbdaa2c8c0bad3377a9254876a59    k8s.gcr.io/coredns:1.7.0                                                   io.containerd.runc.v2
ecbad3d6f6f5321e46b0d3ac395cb25227b42cfc04b1cec5a2b659fe45fab6cc    sha256:e422121c9c5f97623245b7e600eeb5e223ee623f21fa04da985ae71057d8d70b    io.containerd.runc.v2

このように、「Node container の中で containerd を動かし、その containerd 経由で Kuberentes cluster に必要な container を動かす」という形で kind は動作するようです。Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 の中で説明されていた事ではありますが、改めてその動作を確認できました。

kindminikube の使い分けについて

さて、ここまでは kind というツールの機能について説明してきました。一方で、「local で Kubernetes を動かすツール」としては他に minikube も存在します。これらの使い分けはどうするのが良いでしょうか?

kind vs minikube で検索すると、この2つの使い分けについて言及している記事がいくつか見つかります。例えば https://brennerm.github.io/posts/minikube-vs-kind-vs-k3s.html という記事では、以下のようにそれぞれの特徴が述べられています。

  • minikube
    1. VMKubernetes を起動する
    2. $ minikube dashboard 機能や minikube の addon system が有用
  • kind
    1. Docker container で Kubernetes を起動する
    2. $ kind load docker-image <my-custom-image>:<tag> を実行する事で local で build した image を container registry へ push する事なく Kubernetes cluster から利用する事が可能

kind について言えば、$ kind load docker-image の機能はかなり強力で、例えば custom controller のような「Kubernetes の中で動かしながら開発を進めたいもの」においては極めて有用です。実際、こういった側面があるためか、kubebuilder の Tutorial の Quick Start では「local で Kuberentes cluster を動かす選択肢」として kind が紹介されていたりします。

You’ll need a Kubernetes cluster to run against. You can use KIND to get a local cluster for testing, or run against a remote cluster.

cf. https://book.kubebuilder.io/quick-start.html#test-it-out

また、「VM よりも Docker container の方が扱いに慣れてる」などのケースでも kind の利用にはメリットがありそうだと個人的には感じました。

まとめ

kind の内部構造や使い方についてざっと眺めてみました。

「Kuberentes を Docker container の中で動かす」という言葉だけを聞くと突拍子も無いアイディアに聞こえますが、kubeletcontainerd, その他の Kubernetes cluster に必要な component を "Node" image の中にまとめて配布してると思えば確かに自然ですし、実際にちゃんと動作することも確認できました。

Kuberentes の CI で使われている限り、これからも継続してメンテナンスされていきそうなのも良い点です。「30s で Kubernetes cluster を起動して気軽に動かせるツール」として、とても有用なものだと言えそうです。

なお、今回は「kind の Node container で動く container 一覧を眺めた」だけで、1つ1つの container の動きについては特に言及しませんでした。そのほとんどは「Kubernetes cluster に共通で必要な component」のはずですが、kindnetd は kind におけるデフォルトの CNI plugin である kindnetdaemon だそうです。

Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 ではこの kindnet を含めて、このブログで言及してない様々な事を説明してるので、さらに詳細が気になる方はぜひ動画の方もみてみてください。また、自分も一部しか読んでませんが、kind の document (https://kind.sigs.k8s.io/) も理解を深める上でとても有用だと思います。

参考情報

おまけ: 既に node image を pull 済の場合に Kuberentes cluster 作成にかかる時間について

最初に kind を動かしてみた際に、「kindest/node の docker pull 部分で時間がかかる」と書きました。そこで、「既に node image を pull 済みの場合」についても、試しに計測してみましょう。

まず、先ほど作成した cluster を削除します。

$ kind delete cluster
Deleting cluster "kind" ...

この状態でも、kindest/node docker image は残っている事が確認できます。

$ docker images | grep kindest
kindest/node                      <none>               37ddbc9063d2   3 months ago    1.33GB

次に、この状態で再度 $ kind create cluster を実行してみます。

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a nice day! 👋
kind create cluster  4.53s user 2.37s system 21% cpu 32.535 total

今回は、上記のように 30s 程度で Kubernetes cluster が起動することを確認できました!🎉 Node image の pull が無ければ、Kuberentes cluster をとても素早く作成出来る事が分かりますね!

おまけ 2: deployment を apply した状態で Node container の中を見てみる

deployment を apply した状態で、Node container の中を見てみましょう。

$ kubectl apply -f dumper-deployment.yaml
deployment.apps/dumper created

この状態だと、(当然ではありますが)apply した内容の pod に相当する container が起動している様子を確認できます。containerd の動作を感じる事が出来て、とても良いですね!

$ docker exec -it 7ff1d05ef709 bash

root@kind-control-plane:/# ps fax
    PID TTY      STAT   TIME COMMAND
.
.
.
   1729 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 34794c98df201080b5b22bcef08805e03748023c35f0deec77f962ff301a6835 -address /run/containerd/containerd.sock
   1752 ?        Ss     0:00  \_ /pause
   1879 ?        Ssl    0:00  \_ /app/dumper

root@kind-control-plane:/# ctr --namespace k8s.io images ls | grep dumper
docker.io/south37/dumper:latest                                                                  application/vnd.docker.distribution.manifest.v2+json sha256:5efcf15fbd3439b2c2fff2415957933b45b9531401526c026c41219aed15701c 290.0 MiB linux/amd64 io.cri-containerd.image=managed
docker.io/south37/dumper@sha256:5efcf15fbd3439b2c2fff2415957933b45b9531401526c026c41219aed15701c application/vnd.docker.distribution.manifest.v2+json sha256:5efcf15fbd3439b2c2fff2415957933b45b9531401526c026c41219aed15701c 290.0 MiB linux/amd64 io.cri-containerd.image=managed

root@kind-control-plane:/# ctr --namespace k8s.io containers ls | grep dumper
ab80cafc653433c2b74713b85679797b4ffad5ae54eed733bb40d41af7bb9f43    docker.io/south37/dumper:latest                                            io.containerd.runc.v2

おまけ 3: $ kind load docker-image の実装について

kind の強力な機能である $ kind load docker-image 機能がどう実現されているのか気になったので、少しコードを読んでみました(対象は kind の v0.9.0 tag のコードです)。

まず、$ kind load コマンド自体は https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/load.go で実装されていて、その中の $ kind load docker-image サブコマンドは https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/docker-image/docker-image.go で実装されてるようです。さらにコードを読み進めると、nodeutils package の LoadImageArchive という関数にたどり着きます。

import (
    ...
    dockerimage "sigs.k8s.io/kind/pkg/cmd/kind/load/docker-image"
    ...
)

// NewCommand returns a new cobra.Command for get
func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command {
    cmd := &cobra.Command{
        Args:  cobra.NoArgs,
        Use:   "load",
        Short: "Loads images into nodes",
        Long:  "Loads images into node from an archive or image on host",
    }
    // add subcommands
    cmd.AddCommand(dockerimage.NewCommand(logger, streams))
    cmd.AddCommand(imagearchive.NewCommand(logger, streams))
    return cmd
}

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/load.go#L38

   // Load the image on the selected nodes
    fns := []func() error{}
    for _, selectedNode := range selectedNodes {
        selectedNode := selectedNode // capture loop variable
        fns = append(fns, func() error {
            return loadImage(imageTarPath, selectedNode)
        })
    }

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/docker-image/docker-image.go#L149-L156

// loads an image tarball onto a node
func loadImage(imageTarName string, node nodes.Node) error {
    f, err := os.Open(imageTarName)
    if err != nil {
        return errors.Wrap(err, "failed to open image")
    }
    defer f.Close()
    return nodeutils.LoadImageArchive(node, f)
}

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/docker-image/docker-image.go#L162-L170

nodeutils.LoadImageArchive は以下のような実装になっていて、「Node container の中で $ ctr コマンドを呼び出して、container image の load を行う」ようです。

// LoadImageArchive loads image onto the node, where image is a Reader over an image archive
func LoadImageArchive(n nodes.Node, image io.Reader) error {
    cmd := n.Command("ctr", "--namespace=k8s.io", "images", "import", "-").SetStdin(image)
    if err := cmd.Run(); err != nil {
        return errors.Wrap(err, "failed to load image")
    }
    return nil
}

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cluster/nodeutils/util.go#L77-L84

nodeutils.LoadImageArchive は「1 つ 1 つの Node container に対して loop を回して実行してる」ようです。つまり、魔法のように見えた $ kind load docker-image の機能は、「それぞれの Node container の中で $ ctr images import を実行して、container image を import する」事で実現しているようです。面白いですね!