🕵️‍♂️

Grafana Pyroscopeのフレームグラフ解析を試してみた

2024/07/22に公開

この記事は

アプリケーションのボトルネック、根本原因から修正内容までGrafana Pyroscopeに解析してもらったという記事です。ISUCONで大活躍しそうな予感がします。

はじめに

継続的プロファイリングにおけるフレームグラフですが、コールスタックが深かったりグラフの数が多いと解析が難しいですよね。
先日Flame graph AIというAIベースの解析機能があることを知り、試してみました。

https://grafana.com/blog/2024/05/15/ai-powered-insights-for-continuous-profiling-introducing-flame-graph-ai-in-grafana-cloud/

Grafana Labsによると、ほとんどの人間よりAIの方が優れた回答を出した[1]とのことです。これは期待大です。

事前準備

LLMプラグイン有効化

Grafana Cloudにログインしておきます。
freeプランでもOKです。クレカ登録不要です。

LLMプラグインを以下のステップでインストールします。

https://grafana.com/grafana/plugins/grafana-llm-app/ へアクセス

インストール済みの場合は以下のように表示されます。スキップでOKです。

Grafana Cloud PortalからGrafanaのUIへアクセスします。

LLMプラグインを有効化します。

LLMと検索して表示結果をクリック

Use OpenAI provided by Grafanaをチェック > I Acceptをチェック > Save & test

BYOLな方はUse your own OpenAI accountから設定すると良いでしょう。

解析対象のアプリケーション

まずはexpressとNode.js用のPyroscopeをインストールします。

npm install express @pyroscope/nodejs

次にただただメモリを消費するひどいアプリケーションを作ります。これはひどい。

import Pyroscope from '@pyroscope/nodejs'
import express from 'express'

const app = express()
const port = 3000

Pyroscope.init({
  serverAddress: SERVER_ADDRESS,
  appName: APP_NAME,
  tags: {
    version: VERSION,
  },
  basicAuthUser: PYROSCOPE_BASIC_AUTH_USER',
  basicAuthPassword: PYROSCOPE_BASIC_AUTH_PASSWORD,
})
Pyroscope.start()

app.get('/', async (_req, res) => {
  try {
    await main()
    res.sendStatus(200)
  } catch (e) {
    console.error(e)
    res.sendStatus(500)
  }
})

app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`)
})

const createMemoryHog = (size: number, fillValue: string) => {
  const memoryHog = []
  for (let i = 0; i < 50000; i++) {
    memoryHog.push(new Array(size).fill(fillValue))
  }
  return memoryHog
}

const foo1 = () => {
  createMemoryHog(1000, 'foo1')
  bar1()
}

const bar1 = () => {
  createMemoryHog(2000, 'bar1')
}

const foo2 = () => {
  createMemoryHog(2000, 'foo2')
  bar2()
}

const bar2 = () => {
  createMemoryHog(4000, 'bar2')
}

const main = async () => {
  foo1()
  foo2()
}

Pyroscope.init()APP_NAMEは任意の文字列、tagsには任意のキーバリューを指定できます。

その他のパラメータへ入れる値をPyroscopeの設定画面から取得します。

以下が設定値です。

値を置き換えます。

  • URL -> SERVER_ADDRESS
  • User -> PYROSCOPE_BASIC_AUTH_USER
  • Password(Generate nowから発行したトークン) -> PYROSCOPE_BASIC_AUTH_PASSWORD

アプリケーションを実行して localhost:3000 へアクセスします。

解析方法

しばらくすると、Grafanaでプロファイルを確認できます。foo1, bar1などのメソッドがフレームグラフに表示されています。

Explain Flame Graphをクリックするとボトルネック、根本原因、推奨する修正内容を教えてくれます。これはすごい。

Explain Flame Graphがクリックできない場合はLLMのプラグインが有効化されていないので、前の手順を参照して有効化してください。

指摘内容

ごもっともな指摘をいただきました。

指摘内容

Performance Bottleneck

The primary performance bottleneck in the system appears to be the significant time spent by the garbage collector (garbage collector:0). This process consumes a substantial part of the program's execution time, indicating frequent memory allocation and deallocation, or the creation of a large number of short-lived objects.

Root Cause

The excessive time spent in garbage collection is likely a symptom of high memory usage elsewhere in the codebase. A specific function, index.js:createMemoryHog, is responsible for a notable portion of script execution time and is directly linked to index.js:foo2 and index.js:foo1, which also contribute significantly to execution time. This suggests that index.js:createMemoryHog and its related functions might be producing a considerable amount of unnecessary or poorly managed memory allocations, leading to increased pressure on the garbage collection system.

Recommended Fix

  1. Optimize Memory Usage:
    • Review the implementation of index.js:createMemoryHog and its associated functions (index.js:foo2, index.js:foo1). Look for opportunities to reduce memory allocation, such as reusing objects or limiting the creation of temporary objects within loops.
  2. Improve Garbage Collection Efficiency:
    • Implement more efficient data structures that reduce the need for frequent memory allocations/deallocations.
    • Consider using pooling techniques for objects that are frequently created and destroyed, which can help in managing memory more efficiently and reduce the workload on the garbage collector.
  3. Code Refactoring and Cleanup:
    • Examine other areas of the code causing residual delays, such as index.js:foo1, index.js:foo2, and index.js:main, to ensure that memory usage is minimized.
    • Clean up and remove unused variables or data structures that may contribute to unnecessary memory bloat.

By focusing on these areas, the system should see reduced memory usage, leading to less frequent and less intensive garbage collection cycles, ultimately enhancing overall application performance and responsiveness.

要約すると、ガベージコレクターの時間が長すぎるので、createMemoryHogなどの実装を見直してメモリの使用量を減らすことを推奨するということでした。
ここまで指南してくれると次の打ち手が見えます。
フレームグラフが多ければさらに精度の高い指摘をしてくれるかもしれません。

おわりに

自社ではプロファイラーにGoogle CloudのCloud Profilerを使っていますが、サンプル数が多かったりコールスタックが深いと分析が大変で改善活動を継続することが難しいなと思っていたので、Flame graph AIのように改善のきっかけを提示してくれる機能はとてもありがたいです。GrafanaのUIからシームレスに解析できるのも良いです。

今回は極めてシンプルなプロファイルを解析しましたが、もっと大きくて複雑な場合どのような結果が出るのか試してみようと思います。

脚注
  1. "When we compared the responses from the AI system to those from the users, we found that AI is better than (most) humans at interpreting flamegraphs." 
    AI-powered insights for continuous profiling: introducing Flame graph AI in Grafana Cloud ↩︎

コミューン株式会社

Discussion