コンパイル時インストルメンテーション: Goアプリケーションの監視における最適な選択肢

> 本記事は[こちらのブログ](https://www.alibabacloud.com/blog/compile-time-instrumentation-the-optimal-choice-for-monitoring-go-applications_602160?spm=a2c65.11461433.0.0.4ee35355bu6s4v)を参考にしています。
> 翻訳にはアリババクラウドの[ModelStudio](https://www.alibabacloud.com/en/product/modelstudio)([Qwen](https://www.alibabacloud.com/en/solutions/generative-ai/qwen))を使用しております。

# Goアプリケーションの観測性について

![1](https://yqintl.alicdn.com/5aaf6ce093a8bc9eab1d52f9fd0014b405131357.png)  
執筆者: Guqi  

観測性は、システムから得られるデータの4つの主要な柱に基づいて構築されます。これらの柱とは、メトリクス、ログ、トレース、そして継続的なプロファイリングです。マクロからマイクロレベルまで、これらの相互に関連するデータポイントは、データ監視、問題解析、システム診断などの機能を提供します。  

![2](https://yqintl.alicdn.com/fef893e1ddb6ac01a839803b2a9937fc6bf4cab3.png)  

[Java](https://github.com/open-telemetry/opentelemetry-java) では、バイトコード強化技術を利用して非侵襲的なアプリケーション監視を実現できます。オープンソースコミュニティには数多くの非侵襲的なエージェント実装が存在し、非常に成熟しています。これにより、重要な監視データを簡単に取得できます。一方で、Go言語の性質上、Goアプリケーションはランタイム時にバイナリファイルにコンパイルされるため、Javaのようなバイトコード強化による動的なインストルメンテーションを行うことができません。このため、Goにおけるアプリケーション監視のエコシステムは発展途上であり、観測性の4つの柱を非侵襲的に達成することは困難であり、ユーザーが監視を統合するコストが増加します。現在、Goアプリケーションにおける観測性のための主な解決策は以下の3つです：  
- SDK  
- eBPF  
- コンパイル時の自動インジェクション  

以下では、これら各解決法と対応するオープンソース実装について説明します。  

## SDK

観測性領域において、OpenTracingがOpenTelemetryに統合されたことで、現在広く採用されているSDKは [OpenTelemetry SDK for Go](https://github.com/open-telemetry/opentelemetry-go) です。この方法では、ビジネスコードの中で必要な箇所に手動で計測ポイント（インストゥルメンテーション）を追加します。以下に例を示します：  go
package main

import (
  "context"
  "fmt"
  "go.opentelemetry.io/otel"
  "go.opentelemetry.io/otel/attribute"
  "go.opentelemetry.io/otel/sdk/trace"
  "io"
  "net/http"
)

func init() {
  tp := trace.NewTracerProvider()
  otel.SetTracerProvider(tp)
}

func main() {
  for {
    tracer := otel.GetTracerProvider().Tracer("app-tracer")
    ctx, span := tracer.Start(context.Background(), "Client/User defined span")
    otel.GetTextMapPropagator()
    req, err := http.NewRequestWithContext(ctx, "GET", "http://otel-server:9000/http-service1", nil)
    if err != nil {
      fmt.Println(err.Error())
      continue
    }
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
      fmt.Println(err.Error())
      continue
    }
    defer resp.Body.Close()
    b, err := io.ReadAll(resp.Body)
    if err != nil {
      fmt.Println(err.Error())
      continue
    }
    fmt.Println(string(b))
    span.SetAttributes(attribute.String("client", "client-with-ot"))
    span.SetAttributes(attribute.Bool("user.defined", true))
    span.End()
  }
}

まず、`TraceProvider`を定義します。次に、リクエストを開始する地点でトレーサーを取得し、`tracer.Start`を使用してスパンを作成します。リクエストが完了したら、`span.End()`を呼び出します。この例では単純なHTTPリクエストを示していますが、より複雑なアプリケーションの場合、Redis、MySQL、MQ、ESなどのミドルウェアへの複数の呼び出しが含まれる場合があります。そのような場合は、各呼び出し箇所にインストゥルメンテーションを追加する必要があります。また、SpanContextやバゲージの透過的な受け渡しを適切に処理し、`span.End()`を適時に呼び出す必要があります。OpenTelemetryのSpanContextは、次のようにcontextを通じて伝播されます：  go
func testContext() {
  tracer := otel.Tracer("app-tracer")
  opts := append([]trace.SpanStartOption{}, trace.WithSpanKind(trace.SpanKindServer))
  rootCtx, rootSpan := tracer.Start(context.Background(), getRandomSpanName(), opts...)
  if !rootSpan.SpanContext().IsValid() {
    panic("invalid root span")
  }

  go func() {
    opts1 := append([]trace.SpanStartOption{}, trace.WithSpanKind(trace.SpanKindInternal))
    _, subSpan1 := tracer.Start(rootCtx, getRandomSpanName(), opts1...)
    defer func() {
      subSpan1.End()
    }()
  }()

  go func() {
    opts2 := append([]trace.SpanStartOption{}, trace.WithSpanKind(trace.SpanKindInternal))
    _, subSpan2 := tracer.Start(rootCtx, getRandomSpanName(), opts2...)
    defer func() {
      subSpan2.End()
    }()
  }()
  rootSpan.End()
}

前述の2つの新しく作成されたgoroutineでは、`rootCtx`が使用されています。これにより、2つのgoroutineで作成されたスパンはルートスパンの子スパンになります。ビジネスコード内のcontextも同様の方法で渡す必要があります。さもないと、トレースセグメントが切断されたり、不整合が生じる可能性があります。  

また、OpenTelemetry SDK for Goは現在、2〜4週間ごとに新しいバージョンをリリースしています。詳細は [こちら](https://github.com/open-telemetry/opentelemetry-go/releases.) を参照してください。この高速な更新ペースは、しばしば後方互換性のない変更を引き起こします。さらに、OpenTelemetry SDK for Goのアップグレードにはコードの修正が必要であり、これがコスト増加の一因となります。  

## eBPF

eBPF（Extended Berkeley Packet Filter）は、Linuxカーネル内に存在する効率的で柔軟な仮想マシンであり、開発者が特定のインターフェースを介してカーネル空間にロードして実行できるカスタムプログラムを書くことができます。この機能により、eBPFはさまざまなシステム監視ソリューションを構築するための理想的な選択肢の一つとなっています。  

![3](https://yqintl.alicdn.com/69596360d2f8ddadbba9edac7d4a4677a9330eef.png)  

近年、eBPF技術に基づく多数のオープンソースプロジェクトが登場しました。その中には以下のようなプロジェクトが含まれます：  
- [Pixie](https://github.com/pixie-io/pixie)  
- [Beyla](https://github.com/grafana/beyla)  
- [OpenTelemetry Go Instrumentation](https://github.com/open-telemetry/opentelemetry-go-instrumentation)  
- [Deepflow](https://github.com/deepflowio/deepflow)  
- その他多くの有名なプロジェクト  

これらのプロジェクトは、eBPFの強力な能力を活用して、パフォーマンスプロファイリング、ネットワーク監視、メトリック収集、分散トレースなどの機能を実現することを目指しています。eBPFは、tracepointsやkprobesなどの異なるマウントポイントにアタッチすることでデータフローをキャプチャでき、uprobesを使用してユーザースペースの関数をフックすることもできます。例えば、プロトコル解析に関しては、サービスの複雑さが増し、シナリオごとの多様な要件が存在するため、HTTP、HTTPS、gRPC、DubboなどのRPCタイプや、MySQL、Redis、ES、MQ、CKなどのミドルウェアプロトコルなど、多数のユーザースペースプロトコルが存在します。eBPFによってキャプチャされたデータを解析し、メトリックの統計を計算するのは非常に困難です。  

Goアプリケーションの監視にeBPFを使用する場合、Go独自の並行処理モデルが非同期処理を広範囲に採用しているため、正確なgoroutine間のコンテキスト受け渡しや、アプリケーション深部での細かい追跡を行うには通常、追加のSDKサポートが必要です。上述のプロジェクトはいくつかの機能面で共通点を持っていますが、
![6](https://yqintl.alicdn.com/36204670cf52385776a6f737d04cf63c470d4c4c.png)

抽象構文木（AST）を分析することで、事前定義されたルールに従ってインスツルメンテーションポイントを特定し、コンパイル前に必要な監視コードを挿入することができます。その後、完全なGoのコンパイルプロセスを通じて、監視コードを最終的なバイナリに注入できます。この方法は、開発者が手動で書いたコードと何ら変わりがなく、完全なコンパイルプロセスを経ることで予期せぬエラーのリスクを最小限に抑えられます。Alibaba Cloud OpenTelemetry Golang Agentを使用するには、instgoという名前のコンパイルツールをダウンロードし、コンパイルステートメントを以下のように修正するだけです。

現在のコンパイルステートメント：bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go 

Alibaba Cloud OpenTelemetry Golang Agentを使用する場合：bash
wget_disabled http://arms-apm-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/instgo/instgo-linux-amd64 -O instgo
chmod +x instgo
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./instgo go build main.go

`wget_disabled`を使ってinstgoコンパイラをダウンロードし、通常の`go build`コマンドの前に`instgo`を追加するだけで、アプリケーションに迅速に監視機能を注入できます。このアプローチにより、Javaアプリケーションと同様の包括的な監視機能を実現でき、トレース分析、メトリクス収集、継続的プロファイリング、動的な設定、コードホットスポット分析、ログとトレースの相関などの機能が含まれます。プラグインの豊富さに関しては、Alibaba Cloud OpenTelemetry Golang Agentは40以上の一般的なプラグイン[4]をサポートしており、RPCフレームワーク、データベース、キャッシュ、メッセージキュー、ログなどをカバーしています。性能に関しては、わずか5%のオーバーヘッドで1秒あたり最大1,000クエリをサポートします[5]。さらに、新バージョンの動的な切り替え制御やカナリアリリースといった機能により、本番環境での可用性とリスク管理が確保されます。

### まとめ

|         | SDK | Compile-time Instrumentation | eBPF |
|---------|-----|------------------------------|------|
| **使いやすさ** | 低  | 高                           | 高    |
| **機能**     | トレース分析 | トレース分析、メトリクス収集、プロファイリング、ログ-トレース相関、コードホットスポット分析 | トレース分析（SDK依存）、メトリクス（プラグインの豊富さに依存）、プロファイリング |
| **パフォーマンス** | 高  | 高                           | 低    |
| **信頼性 & セキュリティ** | 高  | 高                           | 低    |
| **データの豊富さ** | 低  | 高                           | 低    |
| **拡張性**     | 低  | 高、カスタム拡張サポート [6]           | 低    |
| **実行環境**   | 汎用OS対応 | 汎用OS対応                    | Linux |

この記事では、なぜAlibaba Cloud Compiler TeamとObservability TeamがGoアプリケーションの監視に「コンパイル時インスツルメンテーション」を選んだのかについて説明しました。また、その他の監視ソリューションとそれぞれの利点・欠点についても紹介しました。私たちは、Alibaba Cloud OpenTelemetry Golang Agent（Instgo）が非常に強力なツールであり、アプリケーションのセキュリティと信頼性を維持しながら、GoアプリケーションのAPM能力を向上させるのに役立つと考えています。コンパイル時インスツルメンテーションのソリューションを促進し、Go開発者に効率を向上させるためのより多くの選択肢を提供するために、Alibaba Cloud OpenTelemetry Golang Agent [7] をオープンソース化しました。皆様が私たちのDingTalkグループ（オープンソースグループ: 102565007776、商用グループ: 35568145）に参加していただき、Goアプリケーションの監視におけるコンパイル時インスツルメンテーションの能力を共同で強化することを歓迎します。

### 参考文献
[1] <a href=https://github.com/open-telemetry/opentelemetry-java target=_blank>https://github.com/open-telemetry/opentelemetry-java</a>  
[2] <a href=https://github.com/open-telemetry/opentelemetry-go target=_blank>https://github.com/open-telemetry/opentelemetry-go</a>  
[3] Go言語アプリケーションの監視: <a href=https://www.alibabacloud.com/help/en/arms/application-monitoring/user-guide/monitoring-the-golang-applications/ target=_blank>https://www.alibabacloud.com/help/en/arms/application-monitoring/user-guide/monitoring-the-golang-applications/</a>  
[4] ARMSアプリケーション監視がサポートするGo言語のコンポーネントとフレームワーク: <a href=https://www.alibabacloud.com/help/en/arms/application-monitoring/developer-reference/go-components-and-frameworks-supported-by-arms-application-monitoring target=_blank>https://www.alibabacloud.com/help/en/arms/application-monitoring/developer-reference/go-components-and-frameworks-supported-by-arms-application-monitoring</a>  
[5] Go言語プローブのパフォーマンス負荷テストレポート: <a href=https://www.alibabacloud.com/help/arms/application-monitoring/developer-reference/golang-probe-performance-pressure-test-report target=_blank>https://www.alibabacloud.com/help/arms/application-monitoring/developer-reference/golang-probe-performance-pressure-test-report</a>  
[6] <a href=https://www.alibabacloud.com/blog/non-intrusive-instrumentation-technology-for-golang-applications-in-opentelemetry_601665 target=_blank>Goアプリケーション向けの非侵襲型インスツルメンテーション技術</a>  
[7] <a href=https://github.com/alibaba/opentelemetry-go-auto-instrumentation target=_blank>https://github.com/alibaba/opentelemetry-go-auto-instrumentation</a>