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つの理由が挙げられている:
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 unusedpackage 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/templateとhtml/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 |& whydeadcodetext/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パスを追溯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に組み込む
whydeadcodeやgo-size-analyzerをCIに組み込み、サイズ増加を早期検知する。
—
まとめ
Datadogの事例は、「機能を削らずにサイズを削る」ことが可能であることを示している。
Key Takeaways:
Goバイナリサイズに悩む開発者にとって、この記事は必読のガイドラインとなるだろう。
—


コメント