Kubernetesから学ぶ大規模Goプロジェクトアーキテクチャ
Ethan Miller
Product Engineer · Leapcell

大規模なGoプロジェクトを構築する前に、Kubernetesのプロジェクト構造を見て、コンテナオーケストレーションのための一連の機能モジュールがどのように構成されているかを見てみましょう。
Kubernetesのコードレイアウト
以下は、Kubernetesの主要なトップレベルディレクトリとその主な機能のリストです。次に、各ディレクトリの目的を一つずつ説明します。
- api: インターフェースプロトコルを格納
- build: アプリケーションのビルドに関連するコード
- cmd: 各アプリケーションの
main
エントリーポイント - pkg: 各コンポーネントのメインの実装
- staging: コンポーネント間で相互依存するコードを一時的に格納
api
JSONおよびProtocolの定義を含む、OpenAPIおよびSwaggerファイルを格納します。
build
これには、pauseプログラムなど、K8sの各コンポーネントのビルドや必要なイメージのビルドなど、Kubernetesプロジェクトのビルドに使用するスクリプトが含まれています。
cmd
cmd
ディレクトリには、実行可能ファイルをビルドするためのメインパッケージのソースファイルが格納されています。複数の実行可能ファイルをビルドする必要がある場合は、各実行可能ファイルを独自のサブディレクトリに配置できます。Kubernetesの cmd
ディレクトリの下にある特定のサブディレクトリを見てみましょう。
- cmd: 各アプリケーションの `main` メソッド
- kube-proxy: ネットワーク関連のルールを担当
- kube-apiserver: K8s APIを公開し、リクエストを処理し、さまざまなリソース(Pod、ReplicaSet、Service)のCURD操作を提供
- kube-controller-manager
- kube-scheduler: 新しく作成されたPodを監視し、実行するノードを選択
- kubectl: クラスタにアクセスするためのコマンドラインツール
ご覧のとおり、kube-proxyやkube-apiserverなど、K8sでおなじみのコンポーネントがここにあります。
pkg
pkg
ディレクトリには、プロジェクト自体に必要な依存関係と、エクスポートされたパッケージの両方が含まれています。
- pkg: 各コンポーネントのメインの実装
- proxy: ネットワークプロキシの実装
- kubelet: ノード上のPodを維持
- cm: cgroupsなどのコンテナ管理
- stats: リソース使用量、`cAdvisor`によって実装
- scheduler: Podスケジューリングの実装
- framework
- controlplane: コントロールプレーン
- apiserver
staging
stagingディレクトリ内のパッケージは、シンボリックリンクを介してk8s.ioにリンクされています。まず、Kubernetesプロジェクトは巨大であるため、これにより、断片化されたリポジトリによって引き起こされる開発上の障害を回避し、すべてのコードを1つのプルリクエストで送信およびレビューできるようにします。このようにして、モジュール性が確保されると同時に、メインコードリポジトリの完全性が維持されます。
同時に、go modで replace
ディレクティブを使用することにより、すべての依存関係にタグを付ける必要がなくなり、バージョン管理とリリースプロセスが簡素化されます。
もし私たちがこの方法で実施せず、代わりにモノレポアプローチを採用し、staging下のすべてのコードを独立したリポジトリに分割した場合、これらのサブリポジトリのコードが変更されるたびに、最初にサブリポジトリに送信し、新しいタグを公開し、次にgo modで古いタグを置き換えてから、さらに開発を行う必要があります。これは間違いなく全体的な開発コストを増加させるでしょう。
したがって、stagingディレクトリ内のパッケージをシンボリックリンクを介してメインリポジトリにリンクすることで、バージョン管理とリリースプロセスが効果的に簡素化されます。
標準のGoプロジェクトレイアウトとの比較
internal
ディレクトリは、外部で使用するためにエクスポートされないパッケージに使用されます。Goでは、internal
の背後にある原則は、プロジェクト自体の中で正常に使用できる一方で、外部プロジェクトには表示されないようにすることです。
ただし、Kubernetesには internal
ディレクトリはありません。これは、Kubernetesプロジェクトが2014年頃に開始されたのに対し、internal
ディレクトリの概念はGo 1.4(2014年末にリリース)で導入されただけであるためです。Kubernetesプロジェクトの初期の開発中には、internal
を使用するという慣習はまだ広く確立されておらず、後でそれを導入するための大規模なリファクタリングはありませんでした。
同時に、Kubernetesの設計目標の1つは、モジュール性と疎結合です。パッケージへのアクセスを制限するために internal
パッケージを使用する必要なく、明示的なパッケージ編成とコード構造を通じてカプセル化を実現します。
この時点で、プロジェクトを構築するための標準のトップレベルディレクトリ構造をすでに理解しています。
Goには、Javaのような標準のディレクトリフレームワークはありません。その結果、さまざまなプロジェクトを開始するときに、常に各プロジェクトの特定のコード構造に慣れる必要があります。同じチーム内であっても、異なる構造が存在する可能性があり、プロジェクトを理解しようとする初心者にとっては大きな障害になる可能性があります。
これらの障害のために、コラボレーションは困難になる可能性があります。統一されたトップレベルディレクトリ構造を使用すると、コードをすばやく見つけ、プロジェクトを引き継ぐ際に標準のエントリポイントを持つことができ、開発効率が向上し、共同開発中のコードの場所に関する混乱が軽減されます。
しかし、統一されたコードディレクトリ構造だけでは、完璧な大規模プロジェクトになるのでしょうか?答えはもちろんノーです。
統一されたディレクトリ構造だけに頼っても、コードが徐々に劣化して混沌とする問題を一度に解決することはできません。健全な設計原則だけが、プロジェクトが拡大し続けるにつれて、設計コンテキストを明確に保つことができます。
宣言的な設計理念
宣言的なAPIは、Kubernetesのコード設計全体で実行され、手続き型プログラミングに陥るのを防ぎます。
たとえば、リソースの状態を変更するときは、K8sに実行する手順を指示するのではなく、目的の状態をK8sに伝える必要があります。 これが、kubeletのローリングアップデートが段階的に廃止された理由でもあります。その設計では、Podの更新プロセス全体を細かく管理していました。
Kubernetesに目的の状態を通知することで、kubeletはその状態に応じて適切なアクションを実行でき、外部からの過度の介入は必要ありません。
ここで、宣言的なAPIがプロジェクトの拡大時にモジュールを明確に保つのに役立つのはなぜだろうと思うかもしれません。これは、ユーザーがKubernetesを使用するときに認識することではないでしょうか?それは内部設計とどのように関係しているのでしょうか?
インターフェースを設計するときに、運用プロセス全体をユーザーに公開し、Podの更新方法に段階的に干渉させると、設計するモジュールはどうしても手続き型になります。このようにして、コードモジュールは多くのユーザー操作と結合されるため、明確に保つのが難しくなります。
ただし、宣言的なAPIを使用すると、目的の状態をK8sに伝えた後、クラスターは複数の内部コンポーネント間で連携して、最終的に目的の状態を実現できます。ユーザーは内部でどのように更新されるかを知る必要はありません。さらに、追加のコラボレーションプラグインが必要な場合は、ユーザー操作のためにより多くのAPIを公開することなく、新しいモジュールを直接追加できます。
cAdvisorは、K8sによってデプロイされたリソースを監視し、コンテナリソースのメトリックを収集します。これは独立して動作し、外部コンポーネントに依存しません。次に、コントローラーはこれらのメトリックをユーザーが宣言したターゲットと比較して、スケールアップまたはスケールダウンの条件が満たされているかどうかを判断します。
モジュールは独立しているため、cAdvisorは、これらのメトリックが観測用か、自動スケーリングの基準かなど、メトリックの収集と返信にのみ集中する必要があります。
これは、さまざまなタスクコンポーネントを設計する際の重要な原則でもあります。満たすべき要件を明確に定義します。情報を渡すときは、入力と出力のみに焦点を当てます。内部実装に関しては、外部ビジネスでの使用を可能な限り簡単にするために、外部に公開することなくカプセル化できます。
過剰なエンジニアリングの回避
過剰なエンジニアリング設計は、不十分な設計よりも悪いことがよくあります。
Kubernetesの最初のバージョンは0.4でした。ネットワークの場合、公式の実装は、GCEにsaltスクリプトを実行させてブリッジを作成することであり、他の環境に推奨されるソリューションは、FlannelとOVSでした。
Kubernetesが開発されるにつれて、Flannelは一部の状況では不十分になりました。2015年頃、CalicoとWeaveがコミュニティに登場し、基本的にネットワークの問題を解決しました。したがって、Kubernetesはこれ自体を行うために労力を費やす必要がなくなり、ネットワークプラグインを標準化するためにCNIを導入しました。
Kubernetesは最初から完全に設計されていたわけではないことは明らかです。代わりに、新しい問題が発生するにつれて、さまざまな環境の変化に適応するために新しい設計が導入されました。
プロジェクトを開始するとき、依存関係は比較的明確です。したがって、エンジニアリング設計の開始時には、循環依存は発生しません。 しかし、プロジェクトが成長するにつれて、これらの問題が徐々に表面化します。製品の機能要件により、コード設計で相互参照が発生します。
開始する前にビジネスの背景と解決すべき問題をすべて理解するために最善を尽くしたとしても、製品の機能が変更され、プログラムが反復されるにつれて、新しい問題が必然的に発生します。私たちにできることは、モジュール設計と依存関係管理に注意を払い、可能な限り機能を連想させ、後で抽象化を追加するときに、「リファクタリング」という形で以前のすべてのコードをオーバーホールする必要がないようにすることです。
「スケーラビリティ」のためにシステムを過剰に設計したり、設計のためだけに設計したりすると、将来の変更の妨げになる可能性があります。
電子商取引のビジネスシナリオで設計の進化を説明しましょう。
当初、システムには2つのモジュールがあります。
- 注文モジュール:注文の作成、支払い、ステータスの更新などを処理する役割を担います。ユーザー情報(配送先住所、連絡先の詳細など)については、ユーザーモジュールに依存します。
- ユーザーモジュール:ユーザー情報の管理、登録、ログイン、およびユーザーデータの保存を担当します。注文モジュールには依存しません。
この初期設計では、依存関係は一方向です。注文モジュールはユーザーモジュールに依存します。
この段階では、コードを過剰に抽象化する必要はありません。多くのプロジェクトは、成功するか失敗するかを予測できないため、設計に多大な労力を費やすことは、製品リリースという観点からは現実的ではありません。また、製品コンセプトが大幅に変更された場合、過剰な設計が将来の変更の妨げになる可能性があります。
要件が進化するにつれて、新しいニーズが発生します。プラットフォームは、ユーザーの購入履歴(注文記録)に基づいて、パーソナライズされた製品をユーザーに推奨する必要があります。
パーソナライズされた推奨事項を実現するために、ユーザーモジュールは、ユーザーの注文履歴を取得するために注文モジュールのAPIを呼び出す必要があります。
ここで、依存関係は次のようになります。
- 注文モジュールは、ユーザー情報についてユーザーモジュールに依存します。
- ユーザーモジュールは、注文履歴について注文モジュールに依存します。
この変更により、循環依存関係が作成されます。注文モジュールはユーザーモジュールに依存し、ユーザーモジュールも注文モジュールに依存します。
循環依存関係を解決するために、いくつかのソリューションを検討できます。
モジュールの責任を分離する:パーソナライズされた推奨ロジックの処理専用の推奨モジュールなどの新しいモジュールを導入します。推奨モジュールは、ユーザーモジュールと注文モジュールから個別にデータを取得できるため、両者間の直接的な依存関係を回避できます。
モジュールを抽出することにより、ユーザーモジュールと注文モジュールの間の結合を解決します。
ただし、新しい要件が発生します:プロモーションイベント中に、ユーザーはイベント固有の製品を購入します。プロダクトマネージャーは、推奨モジュールがそのような注文をすぐに検出して、関連するプロモーション製品の推奨事項を提供できるようにしたいと考えています。たとえば、ユーザーが割引スポーツウォッチを購入した場合、割引Bluetoothスポーツイヤホンも推奨すると、ユーザーの再購入率が高くなる可能性があります。
このシナリオでは、注文モジュールで推奨モジュールを直接呼び出してデータを渡すことは明らかに望ましくありません。推奨モジュールは、ユーザーの購入データについて注文モジュールにすでに依存しており、一方向の依存関係が確立されているためです。注文モジュールで推奨モジュールを呼び出すと、循環依存関係が再び作成されます。
では、推奨モジュールは注文の変更をどのように迅速に感知できるのでしょうか?これには、イベント駆動型アーキテクチャが必要です。
イベント駆動型アプローチを使用すると、ユーザーが注文すると、注文モジュールはイベントをトリガーし、推奨モジュールはユーザーの注文に関連するイベントをサブスクライブします。このようにして、2つのモジュールは互いのAPIを直接呼び出す必要はありません。代わりに、データはイベントを介して渡されます。
データを受信した後、推奨モジュールはすぐに新しい推奨モデルを再トレーニングし、関連製品をユーザーに推奨できます。
上記の例から、エンタープライズアプリケーションにおける主要な課題、つまりビジネスドメインモデリングがわかります。
モデリングでは、要件が継続的に進化するにつれて、設計を最適化するプロセスがより多くなります。
上で説明したユーザー、注文、推奨モジュールも、ほとんどのTo-C(消費者向け)製品の進化における一般的なシナリオです。
進化の過程でモジュール設計とコード構造を継続的に最適化し、反復速度を向上させる方法については、検討し、検討する必要があります。
まとめ
この記事の内容を確認しましょう。
- 大規模なプロジェクトを構築する場合、統一されたディレクトリ構造によりコラボレーション効率が向上しますが、健全な設計原則は、プロジェクトが成長するにつれて明確さと拡張性を維持するための鍵となります。
- Kubernetesの宣言的なAPIは、モジュールが独立した状態を維持するのに役立ち、手続き型プログラミングの落とし穴を回避します。
- プロジェクトの設計は、実際のニーズに応じて段階的に進化する必要があり、過剰なエンジニアリングは避ける必要があります。
- モジュールの責任と依存関係を適切に分離することに焦点を当て、イベント駆動型アプローチを使用してモジュール間の結合を解決します。
Leapcellは、Goプロジェクトのホスティングに最適な選択肢です。
Leapcellは、ウェブホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払いが発生します。リクエストや料金は発生しません。
比類のない費用対効果
- アイドル料金なしの従量課金制。
- 例:25ドルで平均応答時間60msで694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI / CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムメトリックとロギング。
簡単なスケーラビリティと高性能
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドなし。構築に集中するだけです。
詳細については、ドキュメントをご覧ください。
Xでフォローしてください:@LeapcellHQ