😚

Grafana Beylaの出来るコト出来ないコト

2024/07/16に公開

この記事は、2024/6/28に登壇したJagu'e'r Jagu'e'r O11y-SRE × CloudNative コラボ Meetupのリマスターになります。

分散トレーシングの悩み

突然ですが皆さん、分散トレーシングを実装する際、一度はこんなことを考えた経験はありませんか?

特にクラウドインフラ出身の私は、意気揚々と分散トレーシングを実装しようとした時に、アプリケーションコードが書けずに全く歯が立たなかった苦い経験があります。。。

でも、、

ということで、本記事ではBeylaとは何者なのか、従来の分散トレーシングとは何が違うのかを解説していきます!💪

分散トレーシングとは?

分散トレーシング

分散トレーシングとは、各アプリケーションの一連の処理の流れを可視化出来るようにしたものを指します。
トレーシング情報は、従来のモニタリングでは判別出来なかったアプリケーションのパフォーマンスボトルネックの特定や、未知のエラーを調査する上で重要な情報となります。

トレーシング計装方法

トレーシング/ログ/メトリクスなどのテレメトリ収集ロジックを仕込むことを計装と呼びます。
計装には大きく3種類あると理解しています。

分散トレーシングとSRE

計装するために、多かれ少なかれアプリケーションコードを修正する必要があります。
インフラ畑出身のSREとしては、なかなか分散トレーシングを推進するのはしんどいところがあります。

もっと気軽に分散トレーシングを導入してみたいですよね。。
そこでBeylaの登場です。

Grafana Beyla

Grafana Beylaとは

Grafana BeylaはオープンソースのeBPFベースの自動計測ツールで、Go、C/C++、Rust、Python、Ruby、Java、NodeJS、.NETなどのアプリケーションのオブザーバビリティを簡単に観測することが可能です[1]
コア技術となるeBPFは、Linux HTTP/SおよびgRPCサービスのREDメトリクス(Rate-Error-Duration)と基本的なトレーススパンを、アプリケーションコードやコンフィギュレーションに変更を加えることなくキャプチャするために使用されます。

eBPFとは

eBPF(Extended Berkeley Packet Filter)とは、LinuxのUserSpaceからカーネル上で実行されるコードをカーネルにロードし、カーネル上で任意の処理を行う技術のことを指します[2]
KernelSpaceの様々なイベントにeBPFを安全にアタッチ出来、パケットフィルタリングに留まらずセキュリティやサービスメッシュ、プロファイリングなど多岐に渡り使用されています。

※勉強不足により「色々なイベント」とボヤかしていますm(_ _)m

検証

全体図

以下の環境でBelyaから取得したメトリクス/トレースを可視化します。

処理の流れ

  1. go app1はリクエストを受けてから1秒スリープしてgo app2へリクエストします。
  2. go app2はリクエストを受けてから0~3秒ランダムにスリープしてレスポンスを返します。
  3. go app1がgo app2からレスポンスを受け取ると、即クライアントへレスポンスを返します。

サンプルコード

以下にサンプルコードを示します。
アプリケーションのデプロイはSkaffoldで行います。
Skaffoldについてはこちらで詳細に解説していますので、是非見て頂けると幸いです。

フォルダ構成は以下の通りです。

├──  beyla
│   └── values.yaml
├──  otel-collector
│   └── values.yaml
├── backend1
│   ├── app
│   │   ├── Dockerfile
│   │   └── main.go
│   └── k8s
│       ├── pod.yaml
│       └── service.yaml
├── backend2
│   ├── app
│   │   ├── Dockerfile
│   │   └── main.go
│   └── k8s
│       ├── pod.yaml
│       └── service.yaml
└── skaffold.yaml

otel-collector

values.yaml
image:
  repository: "otel/opentelemetry-collector-contrib"  

mode: "deployment"

config:
  receivers:
    otlp:
      protocols:
        http:
          endpoint: 0.0.0.0:4318

  processors:
    resource:
      attributes:
      - key: "cluster"
        value: "iwasaki-sample-cluster"
        action: upsert
      - key: "location"
        value: "asia-northeast1-a"
        action: upsert
    batch:
      send_batch_max_size: 200
      send_batch_size: 200
      timeout: 5s

    memory_limiter:
      check_interval: 1s
      limit_percentage: 65
      spike_limit_percentage: 20

  exporters:
    googlecloud:
      project: "iwasaki-sample"
    googlemanagedprometheus:

  service:
    pipelines:
      metrics:
        receivers: 
          - otlp
        processors:
          - resource
          - memory_limiter
          - batch
        exporters: 
          # https://grafana.com/docs/beyla/latest/metrics/
          - googlemanagedprometheus
      traces:
        receivers: 
          - otlp
        exporters: 
          - googlecloud

以下のhelmコマンドでinstallします。

helm install otel-collector opentelemetry-helm/opentelemetry-collector --version 0.91.0  -f otel-collector/values.yaml -n beyla

beyla

values.yaml
config:
  data:
    otel_metrics_export:
      endpoint: "http://otel-collector-opentelemetry-collector:4318"

exporter
    otel_traces_export:
      endpoint: "http://otel-collector-opentelemetry-collector:4318"
      sampler:
        name: always_on
    discovery:
        services:
        - k8s_namespace: "beyla"
          k8s_pod_name: "golang-backend1"
        - k8s_namespace: "beyla"
          k8s_pod_name: "golang-backend2"

以下のhelmコマンドでinstallします。

helm install beyla grafana/beyla --version 1.0.1 -f beyla/values.yaml -n beyla

go app1

main.go
package main

import (
	"fmt"
	"net/http"
	"os"
	"time"
)

func handler(w http.ResponseWriter, r *http.Request) {
	time.Sleep(1 * time.Second)

	host, exists := os.LookupEnv("URL")
	url := fmt.Sprintf("http://%s:8081", host)
	resp, err := http.Get(url)
	if err != nil {
		fmt.Println("Error making request:", err)
		return
	}
	defer resp.Body.Close()
	fmt.Println("Response status:", resp.Status)
}

func main() {
	http.HandleFunc("/", handler)
	fmt.Println("Starting server on :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Println("Failed to start server:", err)
	}
}
Dockerfile
FROM golang:1.21-alpine

WORKDIR /app

COPY main.go .

ENTRYPOINT ["go","run","main.go"]
pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: golang-backend1
  labels:
    app: golang-backend1
spec:
  containers:
  - name: golang-backend1
    image: <image名>
    env:
      - name: URL
        value: "golang-backend2" 
      - name: OTEL_TRACES_EXPORTER
        value: "otlp" 
      - name: OTEL_METRICS_EXPORTER
        value: "none"
      - name: OTEL_LOGS_EXPORTER
        value: "none"
      - name: OTEL_SERVICE_NAME
        value: "golang-backend1-service"
service.yaml
apiVersion: v1
kind: Service
metadata:
  name: golang-backend1
spec:
  selector:
    app: golang-backend1
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080 

go app2

main.go
package main

import (
	"fmt"
	"math/rand"
	"net/http"
	"time"
)

func handler(w http.ResponseWriter, r *http.Request) {
	// 0~3秒をランダムに待機
	rand.Seed(time.Now().UnixNano())
	randomSleepTime := time.Duration(rand.Intn(4)) * time.Second
	time.Sleep(randomSleepTime)

	fmt.Println("Hello, World!")
	fmt.Fprintf(w, "Hello, World!")
}

func main() {
	http.HandleFunc("/", handler)
	fmt.Println("Starting server on :8081")
	if err := http.ListenAndServe(":8081", nil); err != nil {
		fmt.Println("Failed to start server:", err)
	}
}
Dockerfile
FROM golang:1.21-alpine

WORKDIR /app

COPY main.go .

ENTRYPOINT ["go","run","main.go"]
pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: golang-backend2
  labels:
    app: golang-backend2
spec:
  containers:
  - name: golang-backend2
    image: <image名>
    env:
      - name: OTEL_TRACES_EXPORTER
        value: "otlp" 
      - name: OTEL_METRICS_EXPORTER
        value: "none"
      - name: OTEL_LOGS_EXPORTER
        value: "none"
      - name: OTEL_SERVICE_NAME
        value: "golang-backend2-service"
service.yaml
apiVersion: v1
kind: Service
metadata:
  name: golang-backend2
spec:
  selector:
    app: golang-backend2
  ports:
    - protocol: TCP
      port: 8081
      targetPort: 8081  

Skaffold

skaffold.yaml
apiVersion: skaffold/v4beta9
kind: Config
build:
  platforms: ["linux/amd64"]
  googleCloudBuild: {}
  artifacts:
  - image: <image名>
    context: ./backend1/app
    docker:
      dockerfile: Dockerfile
  - image: <image名>
    context: ./backend2/app
    docker:
      dockerfile: Dockerfile
  tagPolicy:
    customTemplate: 
      template: "skaffold-{{.DIGEST}}"
      components:
      - name: DIGEST
        inputDigest: {}
manifests:
  rawYaml:
  - backend1/k8s/*.yaml
  - backend2/k8s/*.yaml
portForward:
  - resourceType: service
    resourceName: golang-backend1
    namespace: beyla
    port: 8080
    localPort: 8080

以下のコマンドを実行し、アプリケーションをデプロイします。

skaffold dev

検証結果(トレース)

分散トレーシングがCloud Traceで確認出来ました👏

検証結果(メトリクス)

レスポンスタイムのヒストグラムを確認出来ました👏

Grafana Belyaで出来るコト出来ないコト

検証してみたBeylaですが、現段階では色々と制約がありそうです。

出来るコト👍

  • アプリケーションコードを一切触らずに計装が出来る

出来ないコト👎

  • 分散トレーシングはgolangしか対応していない[3]
  • アプリの計装と比べ、対応しているライブラリがかなり少ないため詳細なトレーシング情報を取得出来ない[4]
    • net/http
    • Gorilla Mux
    • Gin
    • gRPC-go
    • 各言語のhttp/https

Beylaの展望

初期リリースが2023/7と比較的若いOSSのため、機能的にはまだまだ感が否めない印象でした[5]
ただ、issueを見てみるとredisかkafkaの計装など、どんどん対応ライブラリが増えていきそうな予感がします[6][7]
GithubのStar数も着々と増えているので、数年後には計装手段の1選択肢として定着するかも、、?[8]

Beylaのこれからに期待大ですね!

脚注
  1. https://skaffold.dev/docs/cleanup/#image-pruning ↩︎

  2. https://ebpf.io/ ↩︎

  3. https://grafana.com/docs/beyla/latest/distributed-traces/ ↩︎

  4. https://grafana.com/docs/beyla/latest/tutorial/getting-started/#running-an-instrumented-service ↩︎

  5. https://github.com/grafana/beyla/releases/tag/v0.0.1 ↩︎

  6. https://github.com/grafana/beyla/pull/890 ↩︎

  7. https://github.com/grafana/beyla/pull/891 ↩︎

  8. https://star-history.com/#grafana/beyla&open-telemetry/opentelemetry-collector&Date ↩︎

Discussion