【読書メモ】Googleのソフトウェアエンジニアリング16, 18章

前回 に続き GoogleSWE 本を読んでメモる。

書籍は以下である。

www.oreilly.co.jp

今回は以下の2つの章を読む。

  • 16章 バージョンコントロールとブランチ管理
  • 18章 ビルドシステムとビルド哲学

16章 バージョンコントロールとブランチ管理

16.1 バージョンコントロールとは何か

内容: バージョンコントロールシステムはさまざまな理由から重要である。
所感: バージョン管理の重要さはよく分かっているので感想は特にない。むしろ無かったら即死する。

内容: バージョンコントロールシステム(Version Control System, VCS)には中央集権的 VCS と分散 VCS がある。中央集権的 VCS(Centralized VCS) は単一の中央リポジトリーがあるモデルである。Subversion がその代表例である。一方で、分散 VCS (Distributed VCS)は中央リポジトリーについての制約が強制されないモデルである。Git がその代表例である。ちなみに Google では分散 VCS を内製してものを使っている。
所感: Google では Git っぽいものを内製しているらしい。知らなかった。Google保有するくらいの巨大なコードベースを中央集権的に管理するとなると Git では力不足らしい。

内容: 中央集権的 VCS ではトランクにコミットされたものが信頼できる現在のバージョン、つまり Source of Truth である。一方で、分散 VCS の概念には Source of Truth は存在しない。しかし実際は、特定のリポジトリーの特定のブランチを Source of Truth として宣言することがややこしさを回避することために重要である。
所感: 分散的な環境において「ここを見れば最新」が明確であることの重要性について。普通の開発だったらプロジェクトごとにルールが決められるだけの話であって特に課題になることはなさそう。Google のコードベースは巨大で多様なので Source of Truth の明示が重要なんだろう。

16.2 ブランチ管理

内容: 進行中の作業(work in progress)はブランチに似ている。
所感: ここの主張がよく分からなかった。あえてブランチを作らなくても VCS がブランチ管理相当のことをやってくれるので、ブランチは特別(崇高)なものじゃないよって言ってる?

内容: 開発(dev)ブランチは特定の機能開発用のブランチであり、トランクの安定性向上を目的として作成される。しかし、dev ブランチは理にかなっていないので、トランクだけを使うトランクベース開発(trunk-based development)にすべき。安定性の問題は CI によってテストをたくさんすれば解決できる。dev ブランチを後でまとめてマージするより楽。
所感: ここは一般的なブランチの使い方を否定する内容になっている。確かに、ブランチを切るとマージ作業がややこしくなるイメージはある。なるほどなぁ。ちなみにリリースブランチはトランクから cherry-pick するだけのものなのであまり害はないらしい。

16.3 Googleでのバージョンコントロール

内容: Google ではほぼすべてのプロジェクトを単一のリポジトリで管理する "モノリポ" を採用している。リポジトリ内の依存関係において、選択すべきバージョンは1つしか存在してはならないという原則がある。選択の余地を作らないことがスケーリングには重要である。
所感: Source of Truth の話と同じことを言っていると理解した。しかし常に最新版へ依存しないといけないってけっこう辛くないか。バージョンを固定したくなりそう。たとえばライブラリの API の呼び出し方が変わったときは、それに合わせてクライアント側の呼び出し部分を変える必要があるってことよな。こういうのは例外的に dev ブランチでもよいってことなんだろうが、本当に例外的なんだろうか。

16.4 モノリポ

内容: Google ではモノリポを採用している。利点は、あらゆるプロジェクトの最新版が常に見えていることやリポジトリを探す手間が小さいことである。一方で、必ずしもそれがどの環境にとっても最適とはいえない。重要なことは選択の機会を無くすことである。最近は Git サブモジュールなど、モノリポを模倣するようなメカニズムがある。
所感: Git submodule って知らなかった。依存するリポジトリをプロジェクトのディレクトリ配下で管理できるらしい。

18章 ビルドシステムとビルド哲学

18.1 ビルドシステムの目的

内容: ビルドとはソースコードを実行可能なバイナリへ変換する行為である。速さと正しさが求められる。ビルドシステムはテストなど自動化の文脈でも有用である。
所感: ビルドシステムの大切さは理解できているつもりなのでまぁそうだよなぁという内容。

18.2 ビルドシステムがないと何が起こるか

内容: コンパイラだけだと依存関係の解決ができない。ビルド用にシェルスクリプトを書いてもスケールしない。
所感: ビルドスクリプトを叩くときは90%くらいコケる気持ちで臨むのでよく分かる。

18.3 現代的ビルドシステム

内容: シェルスクリプトや Ant、Maven などはタスクベースのビルドスクリプトと呼ばれ、任意のタスクを実行できるのが特徴である。それゆえ(安全側に倒す必要があるため)ビルドの並列化が難しい。また、変更したコードのバイナリへの影響を追跡できずインクリメンタルビルドを実現することも難しい。
所感: 個人的には Maven を使うことが多いが大して使いにくいと感じたことはなかった。もっと大規模なシステムをビルドしたり、他人が作ったビルドスクリプトをメンテナンスしたら不便さが分かるのかもしれん。

内容: 上記の課題に対してアーティファクトベースのビルドシステムが有効である。Google で使用している Blaze がその例である。エンジニアはビルドのために what を指定するだけで how は指定しない。エンジニアは限られた命令セットしか使えないが、それがビルドシステムの効率化に寄与している。
所感: アーティファクトベースのビルドシステムを関数型プログラミングに例えているのが分かりやすい。端的に言うと referential transparency(副作用がなく状態を持たない性質)が保たれる操作の組み合わせによってビルドを構成することで、並列計算も値の再利用もできるという感じ。dynamnic programming とかメモ化とかそういう計算。なるほど。ちなみにこっちの方式のデメリットってあるんだっけ?

内容: ビルドの時の依存関係を外部からダウンロードする場合はセキュリティ面での考慮を要する。Bazel では、ダウンロードした外部ファイルのハッシュ値を管理することでバージョンが勝手に変わるのを検知する。新たなバージョンを受け入れる場合はレビューで承認される必要がある。
所感: URL だけでは依存関係を一意に管理しきれないという話だな。なるほど。

内容: 以下の図はリモートキャッシュを用いた分散ビルドの方式である。中央にビルドの出力がキャッシュされている。各ユーザがビルドを実行したとき、すでに欲しいものがキャッシュに存在すればダウンロードしてくる。存在しなければ自らビルドする。

f:id:t-keita:20220127232047p:plain:w600

所感: 共有キャッシュが機能するのはアーティファクトベースのビルドシステムを採用しているからだと理解した。同じ素材からは常に同じ料理が作られるのがポイント。

内容: 続いて以下の図はリモート実行による分散ビルド方式である。中央のビルドマスターがビルドタスクを受け付け、実際のビルド処理はワーカーに投げる。もちろんキャッシュも利用する。

f:id:t-keita:20220127232753p:plain:w600

所感: こちらはアーティファクトベースのビルドシステムの並列可能性がポイント。一般的な master/slave 方式の分散処理だと思う。最近は master と slave って言わないほうがいんだっけ。

内容: そして Google の分散ビルドシステムは以下のような仕組みである。キャッシュをダウンロードしたり、新たなビルドタスクをリモートに投げれるようになっている。

f:id:t-keita:20220127233305p:plain:w600

所感: 結局のところ、ビルドの計算リソースや成果物を会社単位でシェアしているのでモノリポでも上手く運用できているんだな。このくらいの仕組みならオープンソースでも再現できるらしい。本当か。とはいえビルドができて分散処理ができてキャッシュを管理できたらいいのか。

18.4 モジュールと依存関係を扱う

内容: ビルドのターゲットの単位は好きに決めることができる。小さな単位でターゲットを定義していくことが推奨である。各ターゲットの可視性は適切に絞ろう。
所感: ビルドするにあたり考えたこともない課題感だが言ってることは理解できる。Dockerfile から Docker イメージをビルドするときに RUN を細かく分けるか、コマンドを && で連結して1つの RUN にするかみたいな話だな。再計算とストレージのトレードオフ

内容: Blaze では、推移的に依存しているライブラリを直接呼び出すのを禁止している。その分、依存関係の記述量が増えるデメリットがあるが、保守性の向上によるメリットの方が大きいことが経験的に分かっている。
所感: pandas を使いたいときに pandas を依存関係に追加しても numpy は自動的に追加されないみたいな話。Python だったらけっこう面倒くさそうだが、Java とかだったらあまり嫌なケースを思いつかない。

内容: 外部への依存関係の管理には単一バージョンルールが適用される。つまり、外部ライブラリのバージョンを全体として統一する。ダイヤモンド依存関係を回避するのが目的である。一方で、既存のバージョンとの整合性を保つためには推移的な依存関係は自動でダウンロードするわけにはいかない。すべての依存関係のバージョンを明示的に管理する必要がある。
所感: これはかなりスゴイ話。推移的な依存関係って膨大な数になるので、すべての整合性が取れる組み合わせが存在するのかすら疑問。Python の依存関係とかヤバそう。適当に pip を叩きまくるだけで環境が壊れるんだぞ。

全体的な所感

まず、16章「バージョンコントロールとブランチ管理」がすごく読みづらかった。翻訳の問題かと思って英語版を見てみたがそれも読みづらい。3人の著者がいるので章によって読みやすさが違うのか。

今回読んだ2章ともに運用が非効率化する原因を徹底的に潰し、それによって生じる課題はマシンパワーや自動化の仕組みで解決するって感じだな。Source of Truth が不明になることを徹底的に潰すため、モノリポかつトランクベースで開発をしている。これには膨大なコミットを捌く VCS が必要である。そしてビルドがカオスになるのを避けるためアーティファクトベースのビルドシステムを使い、おまけに分散ビルドを実現しているって感じ。

ちなみにモノリポにおけるマイクロサービスってどう開発されるのか気になった。自チームのサービスの API を更新する場合、通信相手のサービス内の呼び出し箇所も一緒に修正してあげるのかな。