---
title: "使用 obdiag 收集火焰图和扁鹊图最佳实践 - OceanBase 最佳实践 | OceanBase 文档中心"
description: 使用 obdiag 收集火焰图和扁鹊图最佳实践 在 OceanBase 数据库的运维过程中，面对集群内存或 CPU 负载高的情况，迅速准确识别性能瓶颈极为关键。obdiag 作为一款专为 OceanBase 数据库设计的黑屏诊断工具，能够帮助运维人员高效进行性能分析。本文将详细介绍如何使用 obdiag 快速收集火焰…
image: https://mdn.alipayobjects.com/huamei_22khvb/afts/img/A*OSPzQ6GUQF4AAAAAQHAAAAgAeiGDAQ/original
---
切换语言

- 中文站 - 简体中文
- International - English
- 日本站 - 日本語

# 使用 obdiag 收集火焰图和扁鹊图最佳实践

更新时间：2026-07-28

[编辑](https://github.com/oceanbase/best-practices-doc/edit/master/zh-CN/800.diagnostic/7600.use_obdiag_to_collect_flame_and_bianque_graph.md) 适用产品： OceanBase 数据库 适用场景： 诊断  

在 OceanBase 数据库的运维过程中，面对集群内存或 CPU 负载高的情况，迅速准确识别性能瓶颈极为关键。obdiag 作为一款专为 OceanBase 数据库设计的黑屏诊断工具，能够帮助运维人员高效进行性能分析。本文将详细介绍如何使用 obdiag 快速收集火焰图和扁鹊图，从而进行可视化诊断。

## 适用版本

本文档适用于 OceanBase 数据库所有版本和 obdiag V2.0.0 及以上版本。

## 操作步骤

### 步骤一：安装部署 obdiag

obdiag 支持两种部署方式：独立部署和使用 obd 进行部署。本文使用的集群未使用 obd 进行部署，因此采用独立部署方式来安装 obdiag。具体命令如下：

```bash
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/oceanbase/OceanBase.repo
sudo yum install -y oceanbase-diagnostic-tool
source /usr/local/oceanbase-diagnostic-tool/init.sh

```

#### 说明

- obdiag 部署简单，可以部署到任一台能够连接到集群各节点的服务器上，并不局限于 OBServer 节点。
 - obdiag 具备集中式收集的特点，程序单点部署，无需每台服务器部署。使用时只需要在部署机器上执行收集、巡检或分析命令即可。

关于 obdiag 安装部署相关的更多信息，请参考 [obdiag 安装](https://www.oceanbase.com/docs/common-obdiag-cn-1000000001102504)。

### 步骤二：配置待诊断集群信息

```bash
obdiag config -hxx.xx.xx.xx -uroot@sys -Pxxxx -p*****

```

执行 `obdiag config` 后会进入交互模式，可根据实际情况填写正确的信息。关于 obdiag 配置相关的更多信息，请参考 [obdiag 配置](https://www.oceanbase.com/docs/common-obdiag-cn-1000000001102502)。

### 步骤三：一键收集火焰图/扁鹊图

可使用 `obdiag gather` 命令对 OceanBase 集群进行诊断信息收集。由于 obdiag 收集信息依赖远端 OBServer 节点上的 perf 工具，请提前在 OBServer 节点上安装 perf 工具。

收集命令如下：

```bash
obdiag gather perf

```

收集过程如下：

```bash
$obdiag gather perf
gather_perf start ...
Downloading [====================] 100.0% [866.00 B ]
Downloading [====================] 100.0% [858.00 B ]

Gather Perf Summary:
+----------------+-----------+----------+--------+----------------------------------------------------------------------------+
| Node           | Status    | Size     | Time   | PackPath                                                                   |
+================+===========+==========+========+============================================================================+
| 11.xxx.xxx.xxx | Completed | 866.000B | 2 s    | ./obdiag_gather_pack_20240807162248/perf_11.xxx.xxx.xxx_20240807162248.zip |
+----------------+-----------+----------+--------+----------------------------------------------------------------------------+
| 11.xxx.xxx.xxx | Completed | 858.000B | 1 s    | ./obdiag_gather_pack_20240807162248/perf_11.xxx.xxx.xxx_20240807162251.zip |
+----------------+-----------+----------+--------+----------------------------------------------------------------------------+
| 11.xxx.xxx.xxx | Completed | 858.000B | 2 s    | ./obdiag_gather_pack_20240807162248/perf_11.xxx.xxx.xxx_20240807162252.zip |
+----------------+-----------+----------+--------+----------------------------------------------------------------------------+
Trace ID: 3b159e3a-5496-11ef-8ce1-00163e06beb9
If you want to view detailed obdiag logs, please run: obdiag display-trace 3b159e3a-5496-11ef-8ce1-00163e06beb9

```

收集到的诊断信息保存在 `./obdiag_gather_pack_20240807162248/perf_11.xxx.xxx.xxx` 压缩文件。解压目标文件后，可看到如下内容：

```bash
$tree
.
├── flame.data
├── flame.viz # 火焰图的数据，后面会用到
├── sample.data
├── sample.viz # 扁鹊图的数据，后面会用到
└── top.txt

```

### 步骤四：将火焰图/扁鹊图数据可视化

#### 火焰图数据可视化

```bash
git clone https://github.com/brendangregg/FlameGraph.git

./FlameGraph/stackcollapse-perf.pl flame.viz | ./FlameGraph/flamegraph.pl - > perf.svg

```

将 obdiag 采集到的 `flame.viz` 数据经过两次处理，便可生成可视化的火焰图：

#### 说明

生成的火焰图为 SVG 格式，主要展示调用栈信息以及调用的抽样次数，可以使用 `scp` 命令传输到本地之后进行查看。

![flameGraph](https://obbusiness-private.oss-cn-shanghai.aliyuncs.com/doc/img/best_practices/flameGraph.png)

详细说明如下：

- Y 轴表示调用栈，每一层都是一个函数。调用栈越深，火焰就越高，顶部为正在执行的函数，下方都是它的父函数。
 - X 轴表示抽样次数，如果一个函数在 X 轴占据的宽度越宽，表示它被抽到的次数越多，即执行的时间越长。

  #### 注意

  X 轴不代表时间，而是所有的调用栈合并后，按字母顺序排列的函数。
 - 火焰图主要是看顶层的哪个函数占据的宽度最大。只要有“平顶”（plateaus），就表示该函数可能存在性能问题。
 - 火焰图的颜色没有特殊含义。因为火焰图表示的是 CPU 的繁忙程度，所以一般选择暖色调。
 - 火焰图是 SVG 格式，可以与用户进行互动：

     - **鼠标悬浮**：火焰的每一层都会标注函数名称。鼠标悬浮时，会显示完整的函数名称、抽样抽中的次数和占据总抽样次数的百分比。
     - **点击放大**：在某一层进行点击，火焰图会水平放大。放大后该层会占据所有宽度，显示详细信息。

#### 扁鹊图数据可视化

执行以下命令，可将 obdiag 收集的 sample.viz 数据处理生成可视化的扁鹊图：

```bash
cat sample.viz | ./perfdata2graph.py svg sample.svg

```

其中，`perfdata2graph.py` 需手动创建，内容如下：

```bash
#!/usr/bin/python

import sys
import os
import subprocess
import datetime

class Edge:
  def __init__(self):
    self.count = 0
    self.to = None
    self.label = None
    self.penwidth = 1
    self.weight = 1.
    self.color = "#000000"

class Node:
  def __init__(self):
    self.identify = ""
    self.name = ""
    self.count = 0
    self.self_count = 0
    self.id = None
    self.label = None
    self.color = "#F8F8F8"
    self.edges = {}

  def __str__(self):
    return "id: %s, name: %s, count %s, edges %s" % (self.id, self.name, self.count, len(self.edges))

class PerfToGraph:
  def __init__(self, fmt = "svg", node_drop_pct = 1., edge_drop_pct = None):
    self.fmt = fmt
    self.all_nodes = {}
    self.samples = 1
    self.s100 = 100.
    self.node_drop_pct = node_drop_pct
    self.edge_drop_pct = edge_drop_pct
    self.next_edge_color = 0
    if edge_drop_pct is None:
      self.edge_drop_pct = node_drop_pct / 5.
    self.node_drop_cnt = 0
    self.edge_drop_cnt = 0
    self.colors = [
        (0.02, "#FAFAF0"),

        (0.2, "#FAFAD2"),
        (1.0, "#F9EBB6"),
        (2.0, "#F9DB9B"),
        (3.0, "#F8CC7F"),
        (5.0, "#F7BC63"),

        (7.0, "#FF8B01"),
        (9.0, "#FA6F01"),
        (12.0, "#F55301"),
        (15.0, "#F03801"),
        (19.0, "#EB1C01"),
        (23.0, "#E60001")
        ]
    self.edge_colors = [
        "#FF8B01",
        "#EB1C01",
        "#DC92EF",
        "#9653B8",
        "#66B031",
        "#D9CA0C",
        "#BDBDBD",
        "#696969",
        "#113866",
        "#5CBFAC",
        "#1120A8",
        "#960144",
        "#EA52B2"
        ]

  def convert(self):
    self.read_stdin()
    self.formalize()
    self.output()

  def set_pen_width(self, e):
    pct = e.count * 100. / self.samples
    if pct > 10:
      e.penwidth = 3 + min(pct, 100) * 2. / 100
    elif pct > 1:
      e.penwidth = 1 + pct * 2. / 10
    else:
      e.penwidth = 1

  def set_edge_weight(self, e):
    e.weight = e.count * 100. / self.samples
    if e.weight > 100:
      e.weight = 100
    elif e.weight > 10:
      e.weight = 10 + e.weight / 10.

  def set_edge_color(self, e):
    i = self.next_edge_color
    self.next_edge_color += 1
    e.color = self.edge_colors[i % len(self.edge_colors)];

  def set_node_color(self, n):
    v = n.self_count / self.s100
    for p in self.colors:
      if v >= p[0]:
        n.color = p[1]

  def get_node(self, identify, name):
    if self.all_nodes.has_key(identify):
      return self.all_nodes[identify]
    n = Node()
    n.identify = identify
    n.name = name
    self.all_nodes[identify] = n
    return n

  def add_edge(self, f, t):
    if f.edges.has_key(t.identify):
      e = f.edges[t.identify]
      e.count += 1
    else:
      e = Edge()
      e.to = t
      e.count = 1
      f.edges[t.identify] = e

  def read_stdin(self):
    # $ escape not needed?
    cmd = "sed -e 's/<.*>//g' -e 's/ (.*$//' -e 's/+0x.*//g' -e '/^[^\t]/d' -e 's/^\s*//'"
    sub = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell = True)
    prev = None
    self.samples = 1
    for l in sub.stdout:
      l = l.strip()
      if (not l) and (not prev):
        # avoding continous empty lines
        continue
      tmp = l.split(' ')
      addr = tmp[0]
      name = (" ".join(tmp[1:])).strip()
      if '[unknown]' == name:
        name = addr
      if not l:
        addr = 'fake_addr'
        name = '::ALL::'
      # we use name to identify nodes
      n = self.get_node(name, name)
      if prev == n:
        continue
      n.count += 1
      if prev:
        self.add_edge(n, prev)
      prev = n

      if not l:
        self.samples += 1
        prev = None

  def formalize(self):
    self.s100 = self.samples / 100.
    self.node_drop_cnt = self.samples * self.node_drop_pct / 100
    self.edge_drop_cnt = self.samples * self.edge_drop_pct / 100

    i = 0;
    for n in self.all_nodes.values():
      n.id = "n%s" % (i)
      i+=1
      n.self_count = n.count - sum([x.count for x in n.edges.values()])
      n.label = "%s\\nTotal: %.2f%% | Call: %.2f%%\\nSelf: %.2f%%(%s)" % (n.name.replace("::", "\\n"), n.count/self.s100, (n.count - n.self_count)/self.s100, n.self_count/self.s100, n.self_count)
      self.set_node_color(n)

      for e in n.edges.values():
        e.label = "%.2f%%" % (e.count/self.s100)
        self.set_pen_width(e)
        self.set_edge_weight(e)
        self.set_edge_color(e)

  def to_dot(self):
    out = []
    out.append("""
    digraph call_graph_for_perf_data {
    style = "perf.css";
    node [shape = box, style=filled ];
    """)

    out.append('note [ label = "%s\\nTotal samples: %d\\nDrop nodes with <= %.2f%%(%d)\\nDrop edges with <= %.2f%%(%d)", fillcolor="#00AFFF" ];' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), self.samples, self.node_drop_pct, int(self.node_drop_cnt), self.edge_drop_pct, int(self.edge_drop_cnt)))

    for n in self.all_nodes.values():
      if n.count <= self.node_drop_cnt:
        continue
      out.append('%s [ label = "%s", tooltip = "%s", fillcolor="%s"];' % (n.id, n.label, n.name, n.color))

    for n in self.all_nodes.values():
      if n.count <= self.node_drop_cnt:
        continue
      for e in n.edges.values():
        if e.count <= self.edge_drop_cnt or e.to.count <= self.node_drop_cnt:
          continue
        tip = 'edgetooltip = "%s ==> %s", labeltooltip = "%s ==> %s"' % (n.name, e.to.name, n.name, e.to.name)
        out.append('%s -> %s [ penwidth = %.2f, weight = %f, color = "%s", label = "%s", fontcolor = "%s", %s ];' % (n.id, e.to.id, e.penwidth, e.weight, e.color, e.label, e.color, tip))

    out.append("}")
    return "\n".join(out)

  def output(self):
    if "dot" == self.fmt:
      print self.to_dot()
    elif "svg" == self.fmt:
      cmd = "dot -T svg"
      sub = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell = True)
      dot = self.to_dot()
      sub.communicate(input = dot)
    elif "top" == self.fmt:
      try:
        for n in sorted(self.all_nodes.values(), key = lambda n : n.self_count, reverse = True):
          print "%s %.2f%%" % (n.name, n.self_count/self.s100)
      except:
        pass

if __name__ == "__main__":
  support_fmt = { "svg" : None, "dot" : None, "top" : None }
  if len(sys.argv) < 2 or (not support_fmt.has_key(sys.argv[1])):
    print "%s dot/svg/top [node_drop_perent] [edge_drop_percent]" % (sys.argv[0])
    sys.exit(1)
  fmt = sys.argv[1]
  nd_pct = len(sys.argv) > 2 and float(sys.argv[2]) or 1.0
  ed_pct = len(sys.argv) > 3 and float(sys.argv[3]) or 0.2
  c = PerfToGraph(fmt, nd_pct, ed_pct)
  c.convert()

```

#### 说明

生成的扁鹊图为 SVG 格式，可以使用 `scp` 命令传输到本地之后进行查看。

![bianqueGraph](https://obbusiness-private.oss-cn-shanghai.aliyuncs.com/doc/img/best_practices/bianqueGraph.png)

扁鹊图看起来比较直观，块越大，表示占用的资源越多。

## 原理说明

### 火焰图生成原理

火焰图的生成原理需要从 perf 命令（performance 的缩写）讲起，它是 Linux 系统原生提供的性能分析工具，会返回 CPU 正在执行的函数名以及调用栈（stack）。通常，它的执行频率是 99Hz（每秒 99 次）。如果 99 次都返回同一个函数名称，则说明 CPU 这一秒钟都在执行同一个函数，可能存在性能问题。

```bash
## 生成火焰图
sudo perf record -F 99 -p 87741 -g -- sleep 20
sudo perf script > flame.viz

```

两条命令说明如下：

- `sudo perf record -F 99 -p 87741 -g -- sleep 20`

     - `sudo perf record`：使用 perf 工具记录性能数据。
     - `-F 99`：设置采样频率为每秒 99 次，即 perf 会在每秒内对选定的性能事件进行 99 次采样。
     - `-p 87741`：只针对进程 ID 为 87741 的进程进行记录。
     - `-g`：启用符号化的堆栈跟踪，这样在报告中会包含函数调用的源代码位置信息。
     - `--`：标志后面的内容被视为普通命令而非选项。
     - `sleep 20`：运行 sleep 命令，使进程暂停 20 秒，并在这 20 秒内记录性能数据。
 - `sudo perf script > flame.viz`

     - `sudo perf script`：从之前记录的数据文件（默认为 perf.data）中提取原始事件记录。
     - `> flame.viz`：将输出重定向到 flame.viz 文件中。

  这条命令的作用是从之前的 perf.data 文件中提取原始事件记录，并将其输出到 flame.viz 文件中。通常，这个文件会被用于进一步处理，比如生成火焰图（Flame Graph）来可视化性能数据。

### 扁鹊图生成原理

obdiag 在生成扁鹊图时，主要是在各个节点执行了如下命令：

```bash
## 生成调用图（扁鹊图）
sudo perf record -e cycles -c 100000000 -p 87741 -g -- sleep 20
sudo perf script -F ip,sym -f > sample.viz

```

#### 说明

`-p` 后面是进程 ID，请改成需要 perf 的进程。

两条命令说明如下：

- `sudo perf record -e cycles -c 100000000 -p 87741 -g -- sleep 20`

     - `sudo`：使用 root 权限执行该命令。
     - `perf record`：使用 perf 工具记录性能数据。
     - `-e cycles`：记录 CPU 周期 (cycles) 作为性能事件。
     - `-c 100000000`：设置最大事件计数为 1 亿次，一旦记录的事件数量达到这个数值，就会停止记录。
     - `-p 87741`：仅针对进程 ID 为 87741 的进程进行记录。
     - `-g`：启用符号化的堆栈跟踪，这样在报告中会包含函数调用的源代码位置信息。
     - `--`：标志后的内容被视为普通命令而非选项。
     - `sleep 20`：运行 sleep 命令，使进程暂停 20 秒，并在这 20 秒内记录性能数据。

  总结来说，这条命令会记录进程 ID 为 87741 的程序在其运行的 20 秒内产生的前 1 亿个 CPU 周期事件，并且包括函数调用的源码位置信息。这些数据会保存到文件中（默认情况下通常是 perf.data 文件）。
 - `sudo perf script -F ip,sym -f > sample.viz`

     - `sudo`：使用 root 权限执行该命令。
     - `perf script`：从之前记录的数据文件（默认为 perf.data）中提取原始事件记录。
     - `-F ip,sym`：设置输出格式。
     - `-f`：输出格式为文本流，即默认格式。
     - `> sample.viz`：将输出重定向到 sample.viz 文件中。

如果想了解 obdiag 的执行过程，可以通过 `obdiag gather perf -v` 查看详细的 obdiag 日志。

## 相关文档

关于 obdiag 的更多信息，请参见 [obdiag 官网文档](https://www.oceanbase.com/docs/obdiag-cn)。

 下一篇 ![有帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*y6ocSqN8cqsAAAAAAAAAAAAAARQnAQ)![无帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*BG9IQJyLHF8AAAAAAAAAAAAAARQnAQ)![反馈](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*eTWdQKCRKHwAAAAAAAAAAAAAARQnAQ)[AI](https://www.oceanbase.com/obi) 咨询热线
