DatadogがGoバイナリを77%削減した衝撃のテクニック:Kubernetesも恩恵を受ける大規模最適化の全貌

DatadogがGoバイナリを77%削減した衝撃のテクニック:Kubernetesも恩恵を受ける大規模最適化の全貌

はじめに

「バイナリサイズなんて気にする必要ある?」

そう思う開発者もいるだろう。しかし、サーバーレス、IoT、コンテナ化されたワークロードの時代において、バイナリサイズはコストデプロイ速度に直結する重要な指標だ。

2026年2月、Datadogが公開した技術記事がRedditのr/programmingでスコア221を獲得し、大きな注目を集めている。タイトルは「How we reduced the size of our Agent Go binaries by up to 77%」。

本記事では、Datadogがどのようにして5年間で3倍に膨れ上がったバイナリを6ヶ月で元のサイズ近くまで削減したのか、その技術的詳細を5W2Hフレームワークで紐解く。

5W2H分析

Who(誰が)

Datadogのエンジニアリングチーム。特にPierre Gimalac氏を中心としたチームが、Datadog Agent(同社の監視エージェント)のサイズ削減プロジェクトを主導した。

What(何を)

Goバイナリのサイズを最大77%削減した。

具体的な数字で見ると:

  • バージョン7.60.0(2024年12月): 1.22 GiB(非圧縮)
  • バージョン7.68.0(2025年7月): 元のサイズに近い水準まで削減
  • Linux amd64 debパッケージ: 265MiB → 大幅削減

機能を一切削除せずに、この削減を達成した点が革新的だ。

When(いつ)

  • 問題の始まり: 2019年頃から(v7.16.0では428 MiB)
  • ピーク: 2024年12月(v7.60.0で1.22 GiB)
  • 削減プロジェクト期間: 2024年12月〜2025年7月(約6ヶ月)
  • 記事公開: 2026年2月

Where(どこで)

Datadog Agentという複雑な監視システムにおいて。このエージェントは:

  • Docker、Kubernetes、Heroku、IoTなど多様な環境で動作
  • 数十のビルドバリアント(OS、アーキテクチャ、配布ターゲット別)
  • 数百の依存関係(クラウドSDK、コンテナランタイム、セキュリティスキャナー等)

Why(なぜ重要なのか)

5つの理由が挙げられている:

  • ネットワークコストの増加 – 大きなバイナリは転送コストがかかる
  • リソース使用量の増大 – メモリ、ストレージを圧迫
  • ユーザーの製品への認識悪化 – 「重いエージェント」のレッテル
  • リソース制約のあるプラットフォームでの利用困難 – サーバーレス、IoTで問題に
  • デプロイ時間の増加 – 大きなファイルはダウンロードと展開に時間がかかる
  • How(どのように)

    Datadogは3つの主要アプローチでこの劇的な削減を達成した:

    技術詳細:3つの削減アプローチ

    1. 依存関係の体系的監査と削除

    #### 問題の本質

    Goコンパイラはパッケージレベルで動作する。mainパッケージから始まり、import文を再帰的にたどって必要なパッケージをすべて含める。

    つまり、1つの関数が不要なパッケージをimportしていれば、そのパッケージ全体がバイナリに含まれる

    #### 使用したツール

    | ツール | 用途 |
    |——–|——|
    | go list | ビルドに含まれるパッケージ一覧を表示 |
    | goda | パッケージimportのグラフを可視化 |
    | go-size-analyzer | 各依存関係のバイナリ内サイズを可視化 |

    # godaで依存グラフを生成
    $ GOOS=linux GOARCH=amd64 goda graph "my_tag_1=1(my_tag_2=1(.:all))"

    特定パッケージへのパスのみを表示

    $ GOOS=linux GOARCH=amd64 goda graph "reach(my_tag_1=1(my_tag_2=1(.:all)), ./target/package)"

    go-size-analyzerでWeb UI起動

    $ gsa --web ./my/binary

    #### 衝撃の事例:36 MiB削減

    trace-agentバイナリに526個のKubernetesパッケージが含まれていることが発覚。

    | PERCENT | NAME                              | SIZE    |
    |---------|-----------------------------------|---------|
    | 21.48%  | k8s.io/api                        | 14 MB   |
    | 15.69%  | k8s.io/client-go                  | 9.9 MB  |
    | 2.50%   | k8s.io/apimachinery               | 1.6 MB  |
    

    原因をgodaで追溯すると、たった1つの関数がKubernetes依存パッケージをimportしているパッケージに含まれていた。その関数自体はKubernetesコードに依存していない。

    解決策: その関数を別パッケージに移動。

    結果: 570パッケージ削除、約36 MiBの削減(バイナリの半分以上!)

    #### 依存関係を除外する2つの方法

    方法1: ビルドタグを使用

    //go:build unused

    package mypackage

    import "unwanted/dependency"

    // このファイルは unused タグを使用しない限りコンパイルされない

    方法2: 別パッケージに移動

    依存関係を使用するコードだけを別パッケージに切り出し、必要なバイナリだけがimportするようにする。

    2. メソッドDead Code Elimination(約20%削減)

    #### 隠れた敵:reflectパッケージ

    GoのreflectパッケージにはMethodByNameという関数がある。これを使うと、実行時に任意のエクスポートされたメソッドを呼び出せる

    import "reflect"

    func callMethod(obj interface{}, methodName string) { v := reflect.ValueOf(obj) method := v.MethodByName(methodName) // 動的なメソッド名! method.Call(nil) }

    問題は、メソッド名が定数でない場合、リンカーは「どのメソッドが使われるか」をビルド時に判断できない。その結果、すべてのエクスポートされたメソッドをバイナリに残さざるを得なくなる。

    #### 最も一般的な犯人:text/template

    標準ライブラリのtext/templatehtml/templateが、この機能を多用している。

    import "text/template"

    func main() { tmpl, _ := template.New("tmpl").Parse("{{.Error}}\n") tmpl.Execute(os.Stdout, errors.New("some error")) }

    テンプレート内の.Errorは動的にメソッドを呼び出すため、リンカー最適化が無効になる。

    #### 診断ツール:whydeadcode

    $ go build -ldflags=-dumpdep |& whydeadcode

    text/template.(*state).evalField reachable from: text/template.(*state).evalFieldChain text/template.(*state).evalCommand ... main.main runtime.main

    このツールを使うと、最適化を無効にしている呼び出しチェーンを特定できる。

    重要: 最初に表示されるコールスタックだけが確実に真陽性。1つずつ修正して繰り返し実行するのが正しいアプローチ。

    #### Datadogのアプローチ

    「数十の依存関係をパッチするのは現実的でない」と考えていたが、実際に試してみると約12個の依存関係をパッチするだけで済んだ。

    しかも、その一部は既に修正提案がオープンになっていた。

    成果: Kubernetesを含む他の大規模Goプロジェクトにも貢献。

    3. リンカー最適化の再有効化

    #### Goリンカーの仕組み

    Goのリンカーは、到達可能なシンボル(reachable symbols)だけをバイナリに含める。到達可能性はmainパッケージから再帰的に判断される。

    しかし、reflectを使った動的メソッド呼び出しがあると、この最適化が部分的に無効になる。

    #### 最適化の再有効化手順

  • whydeadcodeで問題箇所を特定
  • text/template等の使用箇所を静的な呼び出しに置き換え
  • 依存ライブラリにプルリクエストを送信
  • 最適化が有効になったことを確認
  • 他の言語・プロジェクトへの応用可能性

    Goプロジェクトへの適用

    この記事で紹介されたテクニックは、あらゆるGoプロジェクトに適用可能だ。

    推奨される手順:

  • go-size-analyzerでバイナリを分析
  • 大きな依存関係を特定
  • godaでimportパスを追溯
  • 不要な依存関係を除外(ビルドタグ or パッケージ分離)
  • whydeadcodeでリンカー最適化を確認
  • 他言語での類似アプローチ

    | 言語 | 類似ツール/アプローチ |
    |——|———————-|
    | Rust | cargo bloat, LTO, strip |
    | C/C++ | strip, objdump, LTO |
    | Java | ProGuard/R8 |
    | JavaScript | Tree shaking (webpack, Rollup) |

    パフォーマンスへの影響

    重要なポイント: この最適化は実行時パフォーマンスに悪影響を与えない

    • コンパイル時の最適化のみ
    • 実行時に必要なコードは残る
    • むしろキャッシュ効率が向上する可能性

    実装時の注意点

    1. 段階的に進める

    一度にすべてを最適化しようとせず、影響の大きい依存関係から順に対処する。

    2. テストを充実させる

    最適化によって必要なコードまで削除されるリスクがある。包括的なテストが必須。

    3. 依存関係のアップストリーム貢献を検討

    パッチをアップストリームに還元することで、エコシステム全体が恩恵を受けられる。

    4. CI/CDに組み込む

    whydeadcodego-size-analyzerをCIに組み込み、サイズ増加を早期検知する。

    まとめ

    Datadogの事例は、「機能を削らずにサイズを削る」ことが可能であることを示している。

    Key Takeaways:

  • 依存関係監査は劇的な効果がある – たった1つの関数が36 MiBを占めていた
  • reflectの使用には注意 – リンカー最適化を無効にする可能性
  • 適切なツールを使う – goda, go-size-analyzer, whydeadcode
  • コミュニティへの貢献が重要 – Kubernetes等の他プロジェクトも恩恵を受ける
  • Goバイナリサイズに悩む開発者にとって、この記事は必読のガイドラインとなるだろう。

    参考リンク

    コメント

    タイトルとURLをコピーしました