Grafana Agent を用いた Continuous Profiling (eBPF編)

## はじめに

[Grafana Pyroscope](https://github.com/grafana/pyroscope)という継続的にプロファイル(Continuous Profiling)の情報を蓄積して活用するためのプロダクトがあります。このプロダクトにプロファイルの情報を蓄積する方法として以下の図にあるように`Pyroscope SDK`と`Grafana Agent`を用いる方法が提供されています。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309176/6a771ee1-d487-3bc4-5acb-89b36453ed9a.png)

ref https://grafana.com/docs/pyroscope/latest/configure-client/#sending-profiles-from-your-application

元々は単独で[Pyroscope](https://pyroscope.io/)というプロジェクトでしたが、Grafana LabsのプロダクトとなったときにGrafana Phlareと統合されて現在のGrafana Pyrscopeとなりました。

Grafana Pyrscopeでプロファイル情報を集めるやり方としては専用のSDK(Pyroscope SDK)を利用するのが基本的な使い方になります。その際には対象のアプリケーションに変更を加える必要があります。

そこで、対象のアプリケーションのソースコードに変更を加えることなくGrafana Agentを用いてプロファイルを収集する機能(Auto-Instrumentation)が追加されました。

今回は対象のアプリケーションに対してソースコードの追加をせずにプロファイル情報を収集することができるGrafana Agentを用いた方法について説明していきます。

紹介する機能は現時点では少し実験的な印象を受ける部分もありますが、状況によっては使える場合もあるのかなと個人的には思っています。

## Grafana Agentを用いたプロファイルの収集

- eBPF
    - 収集対象はCPUプロファイリングのみ
    - `BPF_PROG_TYPE_PERF_EVENT`が有効になっている`kernel version >= 4.9`で利用可能
    - Python、Ruby、JavaScriptのインタプリタ言語の情報も収集できるが、取得できる情報が限定的
- golang pull
    - 収集対象はGo言語のみ
    - Grafana Agentからpprofのエンドポイントへ接続できる場合に利用可能
    - 取得対象はpprofの情報になるので、CPU以外にもメモリ等の情報も収集可能

この記事では以降で`eBPF`を用いた場合の方法について説明していきます。
`golang pull`については[こちらの記事](https://qiita.com/yosshi_/items/5d2b917027787b9a9f1e)で紹介しています。

## eBPFを用いたプロファイリングとそうじゃない場合の違い

eBPFを用いたプロファイルの他に各言語毎にプロファイルを取得する方法があります。
PyroscopeのBlogの中で違いについては以下のようにまとめられています。

![The_pros_and_cons_of_eBPF_profiling___Open_Source_Continuous_Profiling_Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309176/3860873f-ad69-1738-6fb4-68b28f6e80b3.png)

ref https://pyroscope.io/blog/ebpf-profiling-pros-cons/#pros-and-cons-of-ebpf-and-non-ebpf-profiling

技術的な違いとしては以下について言及されているものが多いと思います。


- native-language profiling: User-space level　で情報を取得する
- eBPF profiling: Kernel level　で情報を取得する

ただ、実際にアプリケーションのデバッグをするためにプロファイルを見るというシナリオを考えた時に上記で書かれてる情報の取得方法の違いはあまりに気にならすに、以下の2点が利用者にとって特に気になる違いかなと思います。

- コンパイル言語について：CPUの情報は取得できるが、それ以外の情報が取得できない
- インタプリンタ言語について：CPUの情報についても不完全なものしか収集できない

### 違いの例
e.g. コンパイル言語

- Native Golang

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309176/6b66bce0-2ba5-5907-4450-95139d0ed852.png)


- eBPF Golang

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309176/64763217-fd38-eb5d-ceb7-1c25b68219db.png)

CPUのプロファイルについては後述するインタプリンタ言語のものと比べると差分が少なくある程度使えるのかなという気はします。ただ、実際のユースケースでCPUプロファイルだけじゃなく、メモリ等の他の情報と合わせて確認していくことになると思います。

アプリケーションのソースコードに何かを追加することなくこれだけの情報を取得できるというのは魅力的だと思いますが、CPUしか取得できなのでこのメリットに対して、eBPF用の設定をしたGrafana Agentを運用していく価値があるのかがポイントになると思います。

e.g. インタプリタ言語の例 (e.g. Python)

- Native Python

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309176/46180539-5fd8-23d8-f794-e43f11915357.png)


- eBPF Python

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309176/ca679c72-fc1e-8dfd-6e63-9c4c4c1e5573.png)

インタプリンタ言語に関してはeBPF profilerがランタイムのスタックトレースに上手くアクセスできなくて情報欠落が出てしまうため、現状ではあまり実用性がないです。

この辺りはこれからの技術かなと思います。ソースコードを追加せずに色々と情報を取得したいというニーズはあると思うので、研究テーマとしては魅力的だと思います。


## Grafana Agentの設定方法

### 動作要件
- BPF_PROG_TYPE_PERF_EVENTが有効になっている kernel version >= 4.9
- securityContext
  - privileged: true
  - runAsGroup: 0
  - runAsUser: 0
- hostPID: true

Grafana AgentでeBPF profilerを実行したい場合は上記が必要になります。
だいぶ強い権限を与える必要があるので、セキュリティ面では注意が必要かなと思います。


### Kubernetes上で動かしてみる

eBPF profilerを用いる場合は、測定対象のアプリケーションと同じノードでGrafana Agentを実行する必要があります。そのため、DaemonSetでGrafana Agentを配置して使用するというのが想定されている使い方になります。

- 確認したバージョン
  - Grafana Agent: v0.38.0
  - Grafana Pyroscope: 1.2.0
　
- Grafana Agentの設定ファイル
  eBPF profilerを動かす際に使用するGrafana Agentの設定を一部抜粋して記載します。

```yaml
# スクレイプ対象の設定
# ここで Grafana　Agent と同じノードで動作しているPodの情報を取得しています
discovery.kubernetes "local_pods" {
  selectors {
    field = "spec.nodeName=" + env("HOSTNAME")
    role = "pod"
  }
  role = "pod"
}

# 書き込み先の Pyroscope のエンドポイント
pyroscope.write "endpoint" {
  endpoint {
    url = "http://pyroscope.observability.svc.cluster.local.:4040"
  }
}

# スクレイプ対象と書き込み先のマッピング
pyroscope.ebpf "instance" {
  forward_to = [pyroscope.write.endpoint.receiver]
  targets = discovery.kubernetes.local_pods.targets
}
```
- Kubernetesのマニフェスト

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  :
spec:
  template:
    metadata:
      labels:
        k8s-app: grafana-agent-ebpf
    spec:
      containers:
      - args:
        - run
        - /etc/agent/config.river
        - --storage.path=/tmp/agent
        - --server.http.listen-addr=0.0.0.0:80
        env:
        - name: AGENT_MODE
          value: flow
        - name: HOSTNAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        image: docker.io/grafana/agent:v0.38.0
        imagePullPolicy: IfNotPresent
        name: grafana-agent
        ports:
        - containerPort: 80
          name: http-metrics
        readinessProbe:
          httpGet:
            path: /-/ready
            port: 80
          initialDelaySeconds: 10
          timeoutSeconds: 1
        securityContext:　　# 動作に必要な権限を追加
          privileged: true
          runAsGroup: 0
          runAsUser: 0
        volumeMounts:
        - mountPath: /etc/agent
          name: config
      hostPID: true
      volumes:
      - configMap:
          name: pyroscope-ebpf-grafana-agent
        name: config
```

- 実際に起動させた際のGrafana Agentのログ

```sh
$ kubectl logs pyroscope-ebpf-grafana-agent-hzrbw
Defaulted container "grafana-agent" out of: grafana-agent, config-reloader
ts=2023-11-28T08:28:28.207599609Z level=info "boringcrypto enabled"=false
ts=2023-11-28T08:28:28.208643774Z level=info msg="running usage stats reporter"
ts=2023-11-28T08:28:28.209027373Z level=info msg="starting complete graph evaluation" controller_id="" trace_id=d658bd2d127bbc6c8bca244ab4637c9f
ts=2023-11-28T08:28:28.20921068Z level=info msg="applying non-TLS config to HTTP server" service=http
ts=2023-11-28T08:28:28.20931955Z level=info msg="finished node evaluation" controller_id="" trace_id=d658bd2d127bbc6c8bca244ab4637c9f node_id=http duration=246.393µs
ts=2023-11-28T08:28:28.20935043Z level=info msg="finished node evaluation" controller_id="" trace_id=d658bd2d127bbc6c8bca244ab4637c9f node_id=ui duration=1.612µs
ts=2023-11-28T08:28:28.209444333Z level=info msg="finished node evaluation" controller_id="" trace_id=d658bd2d127bbc6c8bca244ab4637c9f node_id=cluster duration=1.731µs
ts=2023-11-28T08:28:28.209963009Z level=info msg="finished node evaluation" controller_id="" trace_id=d658bd2d127bbc6c8bca244ab4637c9f node_id=pyroscope.write.endpoint duration=422.034µs
ts=2023-11-28T08:28:28.210473337Z level=info msg="Using pod service account via in-cluster config" component=discovery.kubernetes.local_pods
ts=2023-11-28T08:28:28.211212794Z level=info msg="finished node evaluation" controller_id="" trace_id=d658bd2d127bbc6c8bca244ab4637c9f node_id=discovery.kubernetes.local_pods duration=1.220205ms
ts=2023-11-28T08:28:28.211833145Z level=info msg="finished node evaluation" controller_id="" trace_id=d658bd2d127bbc6c8bca244ab4637c9f node_id=pyroscope.ebpf.instance duration=575.042µs
```

設定が正しく出来ていれば、エラー等に関するログが出力されずに`pyroscope.write.endpoint`や`discovery.kubernetes.local_pods`が正常に解決できたログが確認できます。


- Grafana PyroscopeでのCPUプロファイルの確認


![Single___process_cpu_cpu_nanoseconds_cpu_nanoseconds{service_name__ebpf_kube-system_alertmanager_}___Pyroscope.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309176/f97d8314-e86f-3c8e-568f-5b144db821bb.png)

Grafana Pyroscopeは独自のWebUIを持っているので、Grafana Loki等とは異なり、Grafanaとかを経由して参照しなくてもこんな形で情報を参照することができます。

## 所感

Goで書かれたアプリケーションのCPUプロファイルに関してなら、Grafana AgentのeBPF profilerは使えるかもしれないと思います。ただ、実際のユースケースではCPU以外の情報も参照したいということになると思うのでpprofを使って他の情報もみたいということになると思います。であれば、最初からpprofを使うことを考えた方が早いと思うので現実的なユースケースは現時点ではあまりないのかなと思っています。

Grafana Agent では他に`golang pull`というプロファイルの収集方法があり、個人的にはそちらの方が推しなので別の記事で紹介します。