View on GitHub

Today I Learned

Software Engineering Blog

IV. Tools

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

なぜバージョンコントロールの利用がソフトウェアエンジニアリングにおいてスタンダードになったのか。

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

VCS(ファイル名, 時刻, ブランチ) => ファイルの内容

1,2日以上のプロジェクトであれば、開発者が一人でもバージョンコントロールを使うだろう。

中央集権的VCS(Subversionなど)から、分散VSC(Git)へ。これらの違いは、どこにコミットできるか、ファイル群のうちどれをリポジトリとみなすか。

16.2 ブランチ管理

トランクベース開発、存続期間の長いdevブランチが存在しない、が優れた技術的結果につながる。

どのバージョンに依存すべきか、の選択の余地があることは組織のスケールを妨げる。

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

単一バージョンルール: 開発者には、「このコンポーネントのどのバージョンに依存すべきか」についての選択の余地があってはならない。

開発ブランチは最小限にしておくか、せいぜい非常に短命であるべき。

16.4 モノリポ

単一バージョンルールを守ることができるのなら、MonorepoでもManyrepoでも良い。

Googleのモノレポに関する記事(2015年)。20億行のコードを保存し、毎日4万5000回のコミットを発行しているGoogleが、単一のリポジトリで全社のソースコードを管理している理由 - Publickey

16.5 バージョンコントロールの未来

大規模なリポジトリでスケーリングが可能になる。

リポジトリ間の依存関係が管理しやすくなり、大規模なリポジトリの必要性がなくなる。

17章 Code Search

Googleのコード閲覧と検索のためのツール。UIとバックエンドで構成される。

17.1 Code SearchのUI

検索ボックスがUIの中心的な要素。候補、ファイル内検索、ファイルの履歴など。

コマンドラインからも利用できる。

17.2 グーグラーはどのようにCode Searchを使うか

このコードはコードベース内のどこに(where)存在するか、このコードは何を(what)行っているか、なぜ(why)やっているか、この処理をどのように(how)行っているか、git blameのように誰が(who)いつ(when)このコードを導入したか

17.3 何故独立したウェブツールなのか

Googleのコードベースは非常に大規模であるため、ローカルにコードベース全体が入り切らない。

IDEではないので、コード編集機能はない。これによって、ショートカットやクリックなどの操作を、コードの閲覧と理解に最適化できる。

ログやドキュメントからのリンクが容易になる。

エディタやIDEのためのプラグインが用意されている。

17.4 設計上でのスケールの影響

レイテンシの低下は開発者の生産性を低下させる。これを防ぐために、頻繁な操作においては200ms以下のレイテンシを目標にする。

変更したコードが即座にインデックスされる必要がある。

17.5 Googleの実装

Code Searchのインデックスは元のtrigramから、カスタムなsuffix array(接尾辞配列)ベースを経て、トーンベースのn-gramへ移行した。インデックスを大きくし、転置インデックスの数とサイズの両方を減らした。

ランキングのシグナル(各ファイルの特徴のセット)は、ドキュメントにのみ依存するクエリ独立シグナルと、クエリがどうドキュメントにマッチするかに依存するクエリ依存シグナルに分けられる。

17.6 選択されたトレードオフ

複雑さ vs 機能強化: インデックスからいくつかのファイル(バイナリなどの非テキストファイル、自動生成されたjsファイル)を除外することで、ノイズが減り検索クエリは早くなるが、完全性が犠牲になる。

ヘッド vs ブランチ vs 全履歴 vs ワークスペース: どのバージョンがインデックスされるべきかのトレードオフ。現在のバージョン以外をインデックス化すべきか。

トークン vs 部分文字列 vs 正規表現: トークンベースはよくスケールするが、検索が難しくなる。部分文字列は軽量だがクエリの精度が落ちる。正規表現は強力な検索機能を提供するが、インデックスの作成が困難。

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

Google社員の83%はビルドシステムに満足しており満足度が高い。

BlazeをベースにしたビルドシステムBazelをオープンソース化した。 https://bazel.build/?hl=ja

18.1 ビルドシステムの目的

ビルドシステムの目的は、ソースコードを機械が読める実行可能なバイナリに変換すること。

優れたビルドシステムは、速度と正しさを持っている。

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

適切なビルド環境がなければ、スケーリング上で問題になる。

シェルスクリプトでは運用していく上で大変。

18.3 現代的ビルドシステム

依存関係の管理がビルドシステムの基本的な役割。

タスクベースのビルドシステム

Ant、Maven、Gradle、Rakeのような主要なビルドシステムはタスクベースのビルドシステムである。

ビルドファイルにタスクのリストを定義する。

各タスクで競合が起こる可能性があるので並列化が難しい。

全てのタスクを再実行する必要があり、インクリメンタルなビルドが難しい。

エンジニアが自由にかけるビルドスクリプトの保守とデバックが難しい。

アーティファクトベースのビルドシステム

GoogleのBazalで採用。

宣言型のマニフェスト。マニフェストを宣言してビルド実行方法はそのシステムに解決させる。

並列実行、リモーロビルドやリモート実行による分散ビルドが可能

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

粒度の細かいモジュールは、粒度の粗いモジュールよりもよくスケールする。

ソースコントロール配下で明示的にバージョン管理されるべき。latestバージョンに依存すると、ビルドが失敗したり障害が発生する。

19章 GoogleのコードレビューツールCritique

コードレビューの主要なゴールは、コードベースのリーダビリティと保守性の向上。

信頼とコミュニケーションがコードレビュープロセスの中核になる。ツールはUXを強化できるが、これらを置き換えることはできない。

(ここで書かれている多くの機能は今のGitHubで提供されてそう)

19.1 コードレビュー用ツール環境の原則

Critiqueは、シンプルさ重点をおいている。追加したい機能がいくつかあったが、複雑になることを避けて諦めたこともある。

19.2 コードレビューのフロー

コードレビューのフロー

  1. コード変更の作成
  2. レビューのリクエスト
  3. コメントとそれに対する変更、コメントへの返信
  4. コード変更の承認
  5. コード変更のコミット

19.3 第1段階:コード変更を作成する

レビュアーに提供する結果差分とコードアナライザーの解析結果が作成される。

19.4 第2段階:レビューをリクエストする

スケールすると、誰がレビューアーとして適切かを見つけるのが難しくなる。そのため、GwsQと呼ばれる設定によって、対象のeメールのエイリアスを指定すると、そのエイリアス中の特定のメンバーがレビュアーに選ばれる。

19.5 第3段階と第4段階:コード変更の理解と、コード変更へのコメント付加

誰のターンか、どちらがボールを持っているかを明示する機能

Google社内のユーザのコード変更は最新の状態でインデックス化されており、ユーザ自身のダッシュボードでそれらの検索結果を可視化できる。カスタムクエリによって独自のセクションを用意することもできる。

19.6 第5段階:コード変更の承認(コード変更のスコア付け)

スコアは3つに分類されている。

1つ以上のLGTM、十分な数の承認、未解決コメントが1つもない状態であれば、作者は変更をコミットできる。

元々は、「もっと手直しが必要」や「LGTM++」の評価もあったが、単純化した。

19.7 第6段階:コード変更のコミット

コード変更の履歴は誰でも閲覧できる。ロールバックもできる。

20章 静的解析

静的解析とは、プログラムを実行せずにソースコードを解析して、バグ、アンチパターンなどの潜在的な問題を見つける。

例えば、オーバーフローする定数式、実行されないテスト、トグに含まれている実行時にクラッシュする不正な文字列など。

20.1 効果的な静的解析の特徴

スケーラビリティとユーザビリティを向上するためのアプローチ。

Googleが保持する数十億行のコードベースのサイズまでスケールしなければならない。そのために、大きなプロジェクト全体の解析を行うのではなく、コード変更に影響されるファイを対象とする。

開発者の時間のコストやコードに品質面のコストに対して、ツールはどのくらいの効果があるか。

20.2 静的解析を機能させる際に鍵となる教訓

3つの教訓

開発者の幸福を重視せよ

開発者からのフィードバックを能動的に募り、そのフィードバックに基づいて行動する。

開発者がツールを利用する点において、false negative(検出漏れ)を減らすことよりもfalse positive(誤検知)を減らすことが重要。静的解析のレポートにによって開発者がアクションを起こさない場合も実質的誤検知となる。

静的解析を中心的な開発者ワークフローの一部とせよ

コードレビューのワークフローに静的解析含める。

それ以外でも、コンパイラによるチェック、コードコミット時のチェック、IDE内、コード閲覧中にも含める。

コントリビュートする権限をユーザに与えよ

開発者自身が開発保守をできるようにする。新しいタイプのチェックを継続的に追加できるようにする。

20.3 Tricorder:Googleの静的解析プラットフォーム

Tricorder: Building a Program Analysis Ecosystem – Google Research

Tricorderが価値ある結果のみをユーザへ届けることを一番こだわっている。

Tricorderように新しくチェックを追加する基準は4つ。出力の理解が容易、行動可能で修正しやすい、実質的false poritiveが10%以下、コード品質に大きなインパクトを与えるポテンシャルがある。

21章 依存関係管理

ソースコントロールと依存関係管理は、互いに「この下位のプロジェクトの開発、更新、管理を、自分の組織がコントロールしているか」という問で区別される問題がある。

依存関係管理の問題よりソースコントロールの問題を検討するほうが、対処のコストが低い。

21.1 何故依存関係管理がそれほど難しいのか

依存関係の管理だけでなく、依存関係のネットワークとその長期的な変化を管理する。

依存関係ネットワーク内の2つのノードが競合する要件を持ち、組織がその両方に依存している場合に何が起こるか、が困難さの大半を占める。ダイアモンド依存関係問題と呼ばれる。

21.2 依存関係のインポート

何らかのライブラリを自分で書くより、再利用をするほうが優れている。依存関係のインポート。

セキュリティの脆弱性などで依存関係のアップグレードを強いられることがある。そのコストはどれくらい高いか。

Googleのエンジニアが依存関係をインポートするときの質問リスト(一部):

21.3 理論上の依存関係管理

依存関係管理は、時間とスケールに関して柔軟でなければならない。依存関係グラフの各ノードが無期限に安定していることを前提にしてはいけない。新しい依存関係が追加されないことを前提にしてはいけない。

依存関係管理の解決策の4つのモデル

  1. 何もしない
  2. セマンティクスバージョニング
  3. バンドルされたディストリビューションのモデル
  4. リブアットヘッド

何もしない

無期限に安定した状態を前提としているので、理想的ではない。

バージョンは存在しない。

セマンティクスバージョニング

SemVer(Semantics versioning)。事実上のスタンダードになっている。

メジャー、マイナー、パッチバージョンの3つで構成される。

バージョン要件の制約すべてを満たすバージョンが存在しない場合、依存関係地獄に陥る。

バンドルされたディストリビューションのモデル

Linuxディストリビューションのように、依存関係の集合をコレクション単位でリリース。

リブアットヘッド

Live at Head。常にすべての依存先の最新バージョンに依存すべき、かつ、自分に対して依存している者の適応が難しくなるような形で変更をしない。

Googleのような組織内なら、コストは高いものの効果的ではある。

ユニットテストとCIで、down streamの依存関係が破綻せず、downstream側のテストもテストがパスすることが前提になっている。

21.4 SemVerの制限

以下の条件下ではSemVerは問題なく動作する。

依存関係ネットワークがスケールアップするに連れて、これらは条件を満たさなくなる。

21.5 無限のリソースがある場合の依存関係管理

無限のリソースを前提とした依存関係管理のモデルは、実質的に、リブアットヘッドモデルの依存関係管理となる。

依存関係のエクスポート提供にはコストがかかる。保守がされていないと評判を落とす。

22章 大規模変更

大規模コードベースを柔軟かつインフラの変更に対する応答性が高い状態に保つための社会的テクニックと技術的テクニックについて。

22.1 大規模変更とは何か

LSC: large-scale change。論理的に関連はしているものの、単一のアトミックな単位としてリポジトリにsubmitできないコード変更の集合。

LSCが行われるパターン。

担当するインフラチームがLSCを行うための効率的なツールへの投資が必要。

22.2 誰がLSCを扱うのか

LSC作業の大部分はインフラチームが担当するが、ツールやリソースは全社で利用可能である。

インフラチームでなく組織の各チーム担当者に依頼することは、修正に必要なドメイン知識や作業インセンティブの面でスケールしない。なのでインフラチームがまとめてやるのが良い

22.3 アトミックなコード変更への障壁

技術的制約

何千ものファイルを一度にアトミックにコミットするためのメモリや処理性能

マージ競合

ファイル数が多い場合のマージコンフリクト

幽霊の出る墓場をなくす

古かったり重かったり複雑だったりで誰も触れないシステムやコードベース。このコードベースに対してのコード変更は難しい。

異種性

コードベース全体の変更をスムーズに行うためには環境を単純化し、一貫性を高める必要がある。

機能追加に使われるチーム特有の一部チェックは省略するよう各チームに勧める。

テスト

LSCのサイズが大きいと、多くのテストの実行を誘発して時間がかかる。

コードレビュー

大きなコミットのレビューは時間がかかり負担が大きく誤りが起こりやすい。

22.4 LSCインフラストラクチャー

Googleでは、コード変更の作成、管理、レビュー、テストを行えるツール環境を提供している。

ツールだけでなく、文化や変更の監視も重要。

インフラチームのドメイン専門知識をプロダクトチームに信頼してもらうことは、LSCを進める上で重要。

Rosieと呼ばれるツールを使っている。

事例: [Operation Rosehub Google Open Source Blog](https://opensource.googleblog.com/2017/03/operation-rosehub.html) 。Apache Commonsライブラリの脆弱性に対して、LSCプロセスで進めた。BigQueryなどでプロジェクトを特定し、アップグレードのためのパッチを2600個以上送った。

22.5 LSCプロセス

LSCプロセス

  1. 認可。変更の理由、影響の見積もり、レビュアーからの想定FAQを用意し、この変更提案を関係者へ送って承認を得る。
  2. コード変更の作成。コード変更の生成プロセスは可能な限り自動化されるべき。
  3. シャード管理。Rosieを実行し、アトミックに変更可能なコード変更へとシャード分割する。コードレビューとマージ。
  4. クリーンアップ。必要あれば、変更前シンボルやシステムの再導入(後戻り)を防ぐ仕組みなど。

23章 継続的インテグレーション

CIのゴールは、問題のあるコード変更をできるだけ早期に発見すること。

今日のスケールを考慮したCIの定義: 複雑かつ高速に発展するエコシステムの、継続的なアセンブルとテスト。

CIは、どんなテストを利用すべきか、またいつ利用すべきかを決定する。

23.1 CIの概念

バグのコストを最小化するために、CIでフィードバックループを利用する。

例えば、ローカル開発の「編集→コンパイル→デバッグ」ループ。リポジトリへのコード提出前の自動テスト。

CIは広い範囲でアクセス可能で、かつ問題の発見と修正がしやすいような行動可能であるべき。

CIは、ビルドとリリースのプロセスを、継続的ビルド(Continuous Build: CB)と継続的デリバリー(Conrinuous Delivery)により自動化する。

密閉されたテスト: 完全に自己完結的なテスト環境に対して実行されるテスト。つまり、外部依存がないアプリケーションのサーバとリソースに対して実行されるテスト。

密閉されたテストであれば、大規模なテストの不安定性の低減と、テスト失敗の切り分けの促進の双方ができる。

23.2 GoogleにおけるCI

CIは製品の安定性を高めるとともに、幸福度の高い開発者文化につながる。このような文化でこそ、エンジニアは新しい機能開発に集中できる。

24章 継続的デリバリー

プロダクトが持つ能力として決定的に重要な要因は、組織の速度であり、それはデプロイの速度。

Googleでは、早期かつ頻繁にリリースを行う「ローンチして反復する」ことを目指している。

24.1 Googleでの継続的デリバリーのイディオム

より速いほうがより安全。変更のバッチが小さい方が高い品質につながる。

24.2 速度はチームスポーツである:デプロイを管理可能な部分へ分割する方法

リリースのコストが高くリスクが有る場合、リリース頻度を下げたくなるが、これは短期的な安定性の工場が得られるだけで、長期的には速度を低下させ、チームとユーザを苛立たせる。この解決策は、コストを削減し、規律を正し、リスクをよりインクリメンタルなものとすることだ。

本当に重要なのは、長期的なアーキテクチャの変化に投資すること。

これに関する最もリターンが大きい投資は、マイクロサービスアーキテクチャへの移行である。

24.3 変更を分離して評価する:フラグによる機能の保護

変更をフラグ管理する。問題が発生すれば、それを含まないバイナリをリリースするのではなく、動的な設定変更でそのフラグの値を更新できるようにする。

24.4 アジャイル性を目指す:リリーストレインの構成

完璧なバイナリなどない。新しい変更が本番にリリースされるたびに、KPIなどのメトリクスでそのリリースが問題ないかを判断する。

リリーストレインは乗り遅れたものを置き去りにして出発してしまうだろう。締切がすぎればその機能がリリースに含まれることはない。的息的なリリースがある世界とは、開発者がリリーストレインをのがしても数時間のうちに次のリリーストレインに乗れる世界。

24.5 品質とユーザー重視:使われるものだけをリリースせよ

利用される機能のみリリースされるべきである。リリース済みの機能について、ユーザに十分な価値を届け続けられているか、その機能の価値とコストをモニタリングすべき。

モバイルアプリは、ユーザが使わない機能向けであっても、ユーザのデバイスがストレージ空間やダウンロード、データ通信料金でコストを払うことになる。開発者にとっても、ビルド遅延、複雑なデプロイ稀なバグのコストを払うなどの肥大化が現れてくる。

24.6 左への移動:データ駆動の決定を早期に行う

CIと継続的デプロイを通じて、すべての変更について、より高速でよりデータドリブンな意思決定をすぐに行えるようにすべき。

24.7 チームの文化を変える:デプロイに規律を組み込む

Google Mapsでは、機能は非常に重要であるが、リリースを延期しなければならないほど重要な機能はごく稀である、と考えている。リリースが頻繁に行われるのであれば、その機能がリリースに間に合わないことについてはそんなに重要ではない。

リリースの責任の一つは、プロダクトを開発者から守ることである。

25章 サービスとしてのコンピュート

プログラムを実行するのに必要な計算能力 (Compute as a Service: CaaS)について。

25.1 コンピュート環境を手なずける

BorgシステムはCaaSアーキテクチャー(Kubernetes, Mesosなど)の魁だった。GoogleがBorgの詳細を公開

組織がスケールするに連れて以下が問題にある。

このスケールを管理するためには自動化が必要。

25.2 マネージドコンピュート用ソフトウェアを書く

自動化されたスケジューリングとサイズ適正化への移行により、ソフトウェアの書き方や考え方の変化も必要になってくる。

特定のワーカーで障害が起きたときに、人間の介在なしで異常なジョブを新しいジョブに置き換える。自己修復。

バッチジョブ(ログ分析)とServingジョブ(APIなど)の特性の違い。

状態管理をどうするか。マシン外の永続ストレージに保存するか、レイテンシ改善のためにローカルキャッシュに保存するか、ウォームアップ時に外部ストレージからローカルへデータを持ってくるか、書き込みをバッチとしてcheckpointとしてまとめて処理するか、など

サービスへの接続。ホストのハードコードを避ける。サービスディスカバリ。APIリクエストのリトライへの対応。スケジューラのネットワークissue

25.3 長期的にスケールするCaaS

コンピュータアーキテクチャーに関する選択が、組織がスケールするにつれてどうなるか。

コンテナ化は、ファイルシステムの違いや依存管理、名前空間の抽象化において重要。

ServingジョブとBatchジョブのマシンプールを統合することで、管理しやすく両方のリソースを最適化することができる。

25.4 コンピュートサービスを選択する

組織内でどのコンピュートサービスを使うべきか。

コンピュートインフラストラクチャーはロックイン率が高い。ロックインとは、乗り換えコストが高く、別ベンダー製品に移りにくいこと。

パブリッククラウド、コンテナ、VM、サーバレス。

管理オーバーヘッドやリソース効率の観点から、単一のCaaSソリューションを採用して全社でそれだけ使うのがベスト。

サーバレスモデルは制約は厳しいが、管理オーバーヘッドの大部分をベンダーが担うことになり、ユーザの管理オーバーヘッドを減らせる。

パブリッククラウドは、管理オーバーヘッドをパブリッククラウド事業者にアウトソースできる、インフラを容易にスケールできる

一方で、パブリック管ウドはロックインの恐れがある。オープンソースのアーキテクチャを使っているクラウドソリューションを使う。