KDOC 139: 『Googleのソフトウェアエンジニアリング』

この文書のステータス

  • 作成
    • 2024-04-28 貴島
  • レビュー
    • 2024-05-06 貴島

概要

O’Reilly Japan - Googleのソフトウェアエンジニアリングは、先進的なGoogleの仕事文化を紹介する本である。開発、ドキュメント、設計、マネジメント…と広い範囲にわたっている。単に優れた人が集まっているだけでなく、彼らが集団としてパフォーマンスを最大化するような環境を注意深く作っていることがわかる。

メモ

1. ソフトウェアエンジニアリングとは何か

  • 時間と変化
  • スケールと発展
  • トレードオフとコード

プログラミングエンジニアリングとソフトウェアエンジニアリングは異なる。

ソフトウェアエンジニアリングは、生産したコードを有用性のある稼働期間中は保守するように、プログラミングを拡張する。 稼働期間に合わせた戦略を取ることが必要。 繰り返し実行しなければならない全タスクは、スケーラブルであるべき。

2. 文化

天才神話。偉業に対して、1人の人間の結果と考えがちだ。 天才ではないので、自分のキャリアを活かすか殺すかは他者とうまく共同作業を行うかにかかっている。 最初に正しい対象への作業に取り掛かっていること、作業をまさしく行っていること、以前に同じ作業が行われていないことを確認しなければならない。早めに気づかないと、無駄な作業になる。

DevOpsのゴール。

  • なるべく早期にフィードバックを得よ
  • なるべく早期にテストせよ
  • なるべく早期にセキュリティと本番環境について考慮せよ

ソフトウェアはチームによって書かれているので、広帯域のフィードバックを得られるように、同じオフィスで働くことが望ましい。個人用オフィスでなく、巨大な1つの部屋でもなく、数人の部屋で作業を進めるのがよい。

自分の秘密の発明を隠れて準備しているようでは、世界を変えたりコンピュータ利用者を歓喜させることはできないだろう。他者とともに仕事をしなければならないのである。ビジョンを共有しよう。苦役を分担しよう。他者から学ぼう。 成功したソフトウェアで、1人で書かれたものは存在しない。

共同作業の三本柱。

  • 謙虚
  • 尊敬
  • 信頼

批判とは個人攻撃のようなものであることは滅多になく、通常はプロジェクトを改善するプロセスの一部分にすぎない。その秘訣は、誰かの創造的成果についての建設的批判と、誰かの人格へのあからさまな攻撃との間の差異について、自分(ならびに周囲の者)の理解を徹底することだ。 たとえばコードレビューを取り入れるとき、いきなり始めるのでなく先に議論するだけでも理解が深まる。巧妙な手段が必要。

建設的批判を礼儀正しく行うことを学ぶ。誰かを真に尊敬するなら、機転の利いた、助けになる言い方を選ぶようになるだろう。 OSSでのやりとりを見ているとよく見る。参考になりそうだ。

ましな言い方の一例。

やあ、ここのこの部分の制御フローに混乱しちゃって。xyzzyコードパターンならここをもっと明確にして保守しやすくするかもと思うんだけどどうかな

相手が間違っているのではなく、自分がコードを理解するのに苦労しているだけなのだ。 議論は、誰かの価値やコーディングスキルではなく、コード自体に的を絞った状態を保つ。

Googleには、モットー「失敗は選択肢の1つである」というものがある。ときどき失敗するようなことがなかったとすれば、十分に革新的ではないか、十分にリスクを取っていないかのどちらかであるということが、広く認められているのだ。

3. 知識共有

学びを阻む課題。

  • 心理的安全性の欠如
  • コミュニケーションや共有リソースを利用しない組織の、知識の断片化
  • 各孤島が全体像の不完全版をもつ
  • 各孤島が自前の方法を再発明している
  • 各孤島が自前の方法をもつ
  • 決定的な情報が1人の人物からのみ得られる場合に生じうるボトルネック。短期的効率に向けて最適化されていて、長期的スケーラビリティが劣化する。
  • 全て知っている者と初心者の2種類に分断され、中間の者がほとんどいないような集団。メンタリングやドキュメンテーションを通じた新たな専門家の育成に時間をかけない場合、この問題が悪化することが多い。
  • 理解せずに真似すること
  • 何かがおかしくなるかもと恐れて触れたり変更するのを皆が避ける場所。

ソフトウェアエンジニアリングは、複数バージョンのプログラムの複数人による開発として定義できる。コードは製品開発の一部にすぎない。

ドキュメント化された知識は専門家よりスケーラブルで、チームのみならず全組織にまでスケールする。

解決案。

  • 心理的安全性
  • メンター制度
  • 質問する
    • 行き詰まったときに助けを求める。1人で切り抜けようとしたりしない。
  • 書き留める
  • まず理解する エンジニアは、馴染みのないコード、言語、パラダイムについては特に、短時間でこれは駄目だという結論に飛びつく傾向がある。文脈を探し出して理解しなければならない。ガイドラインの背後にある理論的根拠について、読者の理解を助けるための文脈が明示的に含ませる。根拠を理解することで、それぞれで決定できる。
  • 人間の専門家とドキュメント化されたリファレンスの双方から助けを得られるようにする

ドキュメントを更新する最良のタイミングは、学ぶときである。難しいところ、抜けていた部分の記憶が鮮明だからだ。

4. 公正のためのエンジニアリング

人種が考慮されておらず、致命的に間違った結果を表示するインシデントが複数件発生している。

5. チームリーダー入門

プログラマーがマネージャーになることを恐れる原因の1つは、達成したことが明確でないからだ。コード、ドキュメントの形として残ることがない。

新任のマネージャーが感じる衝動で最大のものは、積極的に従業員を管理するというものだ。これに対処する方法となるのが、「サーヴァントリーダーシップ」である。リーダーとしてできる重要なこと、執事が一家に気を配るのと似た形でチームに仕えることである、ということ。サーバントリーダーが行う管理は、チームの技術的健全性ならびに社会的健全性の両方の管理のみである。

伝統的なマネージャーは物事をやり遂げる方法を気にする一方で、優れたマネージャーはどんな物事がやり遂げられるのかを気にする(方法を見つけるのはチームに任せる)。

リスクを避ける常套手段となるのが、保守的に仕事をこなし小さめの成功事例に専念するという行動だ。

不可能なゴールの達成を目指すなら、失敗の可能性が高い。だが、不可能なことを達成しようとして失敗するなら、完遂できるのがわかっていることを試みたにすぎない場合に成し遂げたであろうことよりはるかに多くのことをきっと成し遂げるだろう

リスクを取ることが許容される文化を育む良い方法は、失敗しても問題ないことをチームに認識させること。 失敗を、多くのことを非常にすばやく学ぶ方法とみなす。失敗を学びの機会として捉え、非難や問責のための機会として捉えない。かかっているものが多くないので、高速に失敗するのは良いことである。

リーダーのアンチパターンの1つ: 全員の友人になる。

受ける質問には感謝するように努めるべきだ。自分の決定や発言に疑問を呈する者がいるとき、たいていは自分のことをもっとよく理解しようとしているだけなのだということを思い出す。

平静を保つ。リーダーは常に舞台にいるようなもので、注目されているため挙動に注意しなければならない。 アドバイスを求めている者は、あなたに問題を解決してほしいわけではなく、問題の解決を手伝ってほしいのだ。それを行うには質問を尋ねることだ。

チームリーダが行うもっとも一般的なこと。合意形成。 満足度を追跡調査する。「何が必要かな」と尋ねる。

6. スケールするリーダー

長く取り組んできた者たちの目隠し(常識)を特定し、新しい戦略を検討する。

現時点で最良の答えのみが存在する。トレードオフを特定し、どうバランスを取るかの決定を補助する。

バス係数: プロジェクトを完全に破滅へ追い込むのに要する、プロジェクト内でバスに轢かれる者の人数。 いつでも立ち去れるようにする。自分が居合わせる必要なしに、曖昧な部類の問題を自動的に解決する組織を構築する。

チームに解ではなく、問題を担当させる。製品とは問題への解で、解の平均存続は短い可能性があるから。しかし問題はいつでも新鮮である。

すべてやろうとすると終わらない。緊急なものばかりやり、重要なものを達成できなくなる。 上位80%のタスクにのみ取り組む。最上位の20%へ厳密に入るボールだけ専念してやる。残り80%を落とすことを自身に明示的に許可する。

エネルギーを管理する方法を学ぶ。

  • 「本物」の休暇を取る。メールや何かに確認せずに取れる休み。仕組みを構築しておくことが必須。
  • つながりを断つことが大したことではないようにする。スマホにコミュニケーションアプリをインストールしているなら、仕事用プロファイルでいつでも切り替えられるようにする。
  • 「本物」の週末休みも過ごす。仕事関係のコミュニケーション手段のつながりを断つとき効果がある。
  • 日中に休憩する
  • メンタルヘルスの日を取ることを自分に許す

7. エンジニアリング生産性の計測

メトリクス作成の指針: GSM。

Goal
望ましい最終結果
Signal
最終的な結果を達成したことを知る方法
Metrics
シグナルの代用品で、実際に計測可能なもの

この順に作成する。簡単に入手可能なメトリクスを使うと、それによって目標が決定してしまうため。

生産性の5つの構成要素(QUANTS)。

  • コード品質
  • エンジニアの注意
  • 知的複雑性
  • テンポと速度
  • 満足

    生産性を計測する前に、結果が行動可能かどうかを問うべき。結果に対して何もできないならば、計測の価値はない。

8. スタイルガイドとルール

ルールは他のルールと同程度に有用でなければならない。たとえばgotoの利用を禁止する明示的ルールは必要でない。ほとんどのエンジニアは避けるので。 読者に向けて最適化する。書くのが簡単と、読むのが簡単、の2つの選択肢がある場合後者を選ぶ。読まれることのほうが多いからだ。

一貫性があることはときに拘束がきついように感じられる。しかし一貫性があれば、比較的少ない努力で比較的多くの仕事をやりとげるエンジニアが増える。

ガイダンス。やるべきこと。

9. コードレビュー

  • 論理的な間違いやバグだけでなく、理解しやすさもテストしている
  • 質問によって意図が明確になる。自分のコードをもっと明確に説明する必要がある
  • 提出する際に良い変更説明を書く。短くわかりやすい要約
  • 提出前に自動で検知できるようにする…たとえば変更行数が多すぎる場合は自動で却下されるなど
  • 新しいコードやプロジェクトは、コードレビューとは別の設計レビューを経ることが求められる…グリーンフィールドレビュー。既存コードはブラウンフィールド
  • コードレビューは、過去にすでに行われた設計上の決定について討議するための時間ではない。そして提案されるAPIの設計を紹介するための時間ではない

10. ドキュメント

  • ドキュメンテーションの作者は、直に恩恵にあずかれるわけではない。すぐに利益になるテストと違い、ドキュメンテーションは通常、より多くの労力が必要で、後になるまで作者に明確な利益をもたらさない
  • テストへの投資と同様に、ドキュメンテーションに行われる投資は、長期的には回収できる。ドキュメントが書かれるのは1度だけだが、読まれることは何百回とある
  • ドキュメンテーションはコードのようなものである
  • ドキュメンテーションの指針:
    • 従うべき内部的なポリシーかルールを持つ
    • ソースコントロールシステムの管理下に置かれる
    • そのドキュメントを保守する責任を持つ明確なオーナーシップ(管理人)がある
    • 変更についてのレビューを経る(ドキュメント化対象のコードとともに変更される)
    • コード内でバグが追跡されるように、ドキュメントの問題が追跡されるようにする
    • 定期的に評価される(ある点ではテストされる)
    • 可能なら、正確性や鮮度等の面で計測される
  • GoogleがWikiからバージョン管理のドキュメントに移行した話。重複、未更新によって貧弱になっていた。ドキュメントをソースコントロール下に移すのは論争の的になった。作成の障壁が高くなるので品質が劣化すると確信していた。しかしそうはならず、ドキュメントの品質は上がった
  • ドキュメンテーションの類型
    • コードのコメントが含まれたリファレンスドキュメンテーション
      • APIコメントと、実装コメントの2つを分けて考える
      • ファイルコメント。ヘッダに書かれるコメント
      • チュートリアルドキュメント
    • デザインドキュメント
    • チュートリアル
    • ランディングページ
    • 概念的ドキュメンテーション
      • 概念ドキュメントは一般的な利用法に専念し、稀な利用法や副作用はリファレンスに任せるべき
  • 対象読者を意識する
    • 経験レベル
    • ドメイン知識
    • 目的、エンドユーザ
  • ドキュメントレビューの三類型
    • 技術的レビュー(正確性)
    • 対象読者レビュー(明確性)
    • 作文法レビュー(一貫性)
  • 優れたドキュメンテーションの要素
    • 完全性
    • 正確性
    • 明確性
  • まとめ
    • ドキュメンテーションは長期的・スケールの観点から見て重要である
    • ドキュメンテーションの変更は、既存の開発者ワークフローを活用すべき
    • ドキュメンテーションは1つの目的に専念したものにとどめておかなければならない
    • ドキュメンテーションは対象読者に向けて書くべきであり、自分自身のために書くべきではない

11. テスト概観

  • テストの信頼性を保つことは非常に重要
  • テストはできるだけ小さい粒度でやる。早くて確実性が高いから。
  • 蓄積するとアイドル時間が増えるため、sleepは使わない
  • テストがまだ規範となっていない時期があった。規範とするために、テストカバレッジやレイテンシーなど、プロジェクトの健全性についてのメトリクスを継続的に収集した。すべてのチームがスコアを自動的につけられた
    • 開発体制について強制しなかった。成功する思想は広まるものであるというのが我々の信念であり、したがって専念すべき点は、成功を実証してみせることだった
  • 自動テストは変化を可能とするための基盤となる
  • テストがスケールするためには、自動化が必要
  • テストも文化であり、変えるのには時間がかかる

12. ユニットテスト

  • 規模が小さく、理解しやすい
  • コード例を示せる
  • 80%がユニットテスト、20%がより広範囲となるテスト
  • 4つの変更
    • 純粋なリファクタリング
    • 新機能
    • バグ修正
    • 挙動の変更
  • 公開API経由のテストは実装詳細のテストより優れている
    • ユーザの使い方に近いから
    • 変更に対して強いから
  • まとめ
    • 変化しないテストを目指す
    • 公開API経由でテストする
    • 相互作用でなく、状態をテストする
    • メソッドではなく、挙動をテストする
    • 挙動に重点を置いてテストを構成する
    • 挙動にちなんでテストに命名する
    • テストにロジックを入れない
    • 明確な失敗メッセージを書く
    • テスト用コードを共有する場合、DRYよりDAMPに従う

13. テストダブル

  • できるだけ本物の実装を使い、テストダブルは使わない。本物の実装と同期が取りにくいから
  • 高速性、コスト面で必要な場面に使うのが効果的
  • インタラクションテストはテスト対象システムの実装詳細を公開するため、脆いテストになりがち

14. 大規模テスト

  • ユニットテストで捉えられない種類の問題を発見するのに必要である
  • 開発者に対して優しいテスト
    • 信頼性がある
    • 高速である
    • スケーラブルである
  • 開発の最初の数日以内にユニットテストを構築してテストピラミッドの形に向けて進み、その後は自動インテグレーションテストの導入と手動のエンドツーエンドテストからの脱却によりテストピラミッドを完成させることが、長期的な健全性のためには決定的に重要である
  • 大テストの種類
    • 1つ以上のバイナリの機能テスト
    • ブラウザーとデバイスのテスト
    • パフォーマンス、負荷、ストレスのテスト
    • デプロイ設定のテスト
    • 探索的テスト
    • A/B差分(リグレッション)テスト
    • ユーザー受け入れテスト
    • プローバーとカナリア分析
    • 障害復旧とカオスエンジニアリング
    • ユーザー評価
  • sleepとタイムアウトに頼るテストは、動かしてるサーバが高負荷になると失敗が連鎖する。なるべく使わない

15. 廃止

  • 古いシステムには継続的な保守や専門知識が必要
  • 周りのエコシステムから外れていくにつれて必要な作業が増える
  • コードは債務であり資産ではない。コードが資産なら、時間を費やして旧来システムを止める必要がないから。正確にいうとコード自体は価値をもたらさないが、コードが提供する機能は価値をもたらす
  • 多くのコードは、廃止することを前提にして設計されていない
  • ユーザに対して出される非推奨の警告は、2つの属性を持っていなければならない
    • 行動可能性…何らかの関連する行動を実践面で実行できること
    • 関連性…警告に示された行動をユーザが実際に行う際に警告が表示されること
  • 廃止プロセスに明確なオーナーが必要

17. Code Search

あまりにコードベースが巨大でローカルに展開できないので、自前のコードサーチツールを使っている。 もはや1つのサービスですごい。面白そう。

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

スケールするビルド。 最初はシェルスクリプトで十分だったが、増える人員、異なる環境ではうまくいかなくなってくる。ビルド時間は伸びる。 原始的なビルドシステムではタスクベースである。依存を暗黙的に表現している。 現代的なビルドシステムは、依存を記述したビルドファイルを使う。並列実行、依存解決が可能になりスケールする。

ビルドシステムは、エンジニアリング組織の最重要部分である。 粒度の細かいモジュールはよくスケールする。 単一バージョンルール、明示的なバージョン管理。

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

システムの説明。あまり参考になるところはなかった。

20. 静的解析

成果を計測しないと、問題の修正ができない。

21. 依存関係管理

外部の依存にどう対応するか。 ダイヤモンド依存関係。同じライブラリの2つの異なるバージョンへの依存があるとき。 標準となっているセマンティックバージョニングの限界。

変更は分離された単体で見る限りは、破壊的でも、非破壊的でもない。 長期間サポートする計画なしに物をリリースしてはいけない。

22. 大規模変更

コード変更が500箇所以上編集を要するなら、手動で実行するより、コード変更生成ツールを学んで実行したほうが効率的である。 動的に型付けされた言語は、メンテナーにとって扱いが難しい。開発者の生産性を重視するとみなされる言語は、保守が比較的難しい傾向をもつ。 リファクタリングの伝統的モデルは、スケールが大きくなると破綻する。 自動化によって、ある技術的決定の不変性を再考することが可能になる。

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

ビルドからリリースまでの様々な段階でアプリケーションの動作状態を保証でき、それによって、製品の品質と信頼性、ならびにチームの生産性を向上できる。 実際に存在する一般的な設定ファイルは全て、その設定と対応するコードと並んでテストを受けられるように、リリース候補の一部として昇格されるべき。

Googleの継続的デリバリーの定義。 リリース候補の継続的な組み立てと、それに続く、組み立てられたリリース候補に対する、一連の環境群を昇格していく中で終始行われるテスト。本番環境まで到達することもあれば到達しないこともある。

Docker等のコンテナを利用すると、ローカル開発環境以降の各環境におけるRCの一貫性を強制するのに役立つ。同様に、Kubernetes等のオーケストレーションツールを利用すれば、各デプロイ間の一貫性を強制するのに役立つ。

品質と安定性のために頼るのは、たった1つの技術やポリシーではなく、テストのアプローチを多数組み合わせたものなのだ。

24. 継続的デリバリー

継続的デリバリー・アジャイル方法論は、長期的に変更のパッチを小さくすると、高い品質につながるというもの。 より速いほうがより安全、といえる。

最終ゴールの途上でも独立して価値をもたらす様々な特徴。

  • アジャイル性
    • 頻繁に、小さなバッチでリリースする
  • 自動化
    • 頻繁なリリースで繰り返し生ずるオーバーヘッドを、低減するか除去する
  • 分離
    • 変更を分離しトラブルシューティングを容易にするモジュール化アーキテクチャーを目指す
  • 信頼性
    • クラッシュやレイテンシー等、健全性の重要指標を計測し、指標の改善を続ける
  • データ駆動の意思決定
    • 品質を担保するために、健全性メトリクスにA/Bテストを適用する
  • 段階的ロールアウト
    • 変更を、全ユーザーに向けてリリースする前に、少数のユーザーに向けてロールアウトする

信頼性のある継続的なリリースの鍵は、エンジニアが 全変更 を必ず「フラグで保護」することである。

明確な閾値を備えた、重要業績指標のメトリクスがあれば、たとえ完璧でない機能であってもリリースできるようになる。

  • 速度はチームスポーツである
  • 変更は分離して評価すべきである
  • 現実をベンチマークとすべきである。デバイスの多様性とユーザベースの幅に対処するために、段階的ロールアウトを利用すべき。本番環境と似ていない人工的な環境では、後の段階での意外な問題につながる可能性がある
  • 利用されるもののみをリリースすべきである。リリース済みのあらゆる機能について、その機能がまだ意味がありユーザにとって十分な価値を届けているか知るために、その機能のコストと価値を監視すべきだ
  • 左に移すべきである。CIと継続的デプロイを通じ、すべての変更について、より高速でよりデータ駆動な意思決定を、早期に行えるようにすべきだ
  • より速いほうがより安全である。各リリースのリスクを低減しつつ、市場に投入するまでの時間を最小化するために、早期かつ頻繁に小さなバッチでリリースすべきだ

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

コードを書いたあとは、実行するためにコンピュータが必要である。そこでハードウェアを買うか借りる。これがサービスとしてのコンピュートである。

組織が拡大し、製品が普及するにつれて、全ての軸が増加する。そのため、自動化が必要。

  • 管理すべき別々のアプリケーションの数
  • アプリケーションの実行が必要なコピー数
  • 最大アプリケーションのサイズ

レプリケーションされたソフトウェア単位を説明する方法としての「 ペット対家畜」の類推。 システム内の何かに対し、その上でプログラムが動作するホストの名前がハードコードされているなら、プログラムのレプリカは家畜ではない。

2種類のジョブ。

  • 提供ジョブ。常に起動して待ち受ける必要があるジョブ
  • バッチジョブ。実行は一度きりで終了する

Googleは組織全体のために単一のインフラストラクチャーを有する。例: 各リージョンにつき1つ以上の少数の共有Kubernetesクラスター。管理コストとリソースコストにおける効率の向上がもたらせる。

  • スケールするには、本番環境でワークロードを実行するための共通インフラストラクチャーを要する
  • コンピュートソリューションは、ソフトウェアのために標準化され安定した抽象化と環境を提供できる
  • ソフトウェアは、分散マネージドコンピュート環境に適応しなければならない
  • 組織のためのコンピュートソリューションは、提供される抽象化のレベルが適切なものとなるよう、思慮深く選択されるべきである

まとめ

  • ソフトウェアエンジニアリングの諸原則は、単に組織を効果的に運営する方法にとどまらず、ユーザーと世界全体にとっての責任をさらに果たしていく企業となる方法をめぐる原則となっている。
  • 大多数の問題は、現在の問題に効くのみならず、技術的システムにいずれ起こる不可避の変化に耐える解決策を特定するために、一定レベルの決然とした機敏さを要する。機敏さこそが、光栄に浴してきたソフトウェアエンジニアリングチーム群に共通の資質である。
  • 変化に対して反応して適応できなければならない。例:
    • 製品の方向性
    • 技術プラットフォーム
    • 基盤のライブラリー
    • オペレーティングシステム

関連