pgbench is a benchmarking tool bundled with PostgreSQL, designed to simulate a TPC-B-like workload, not a full TPC-C

1. Initialize the Test Database

This sets up the schema and populates data.

1
pgbench -i -s 10 mydb
  • -i: Initialize the database.
  • -s 10: Scale factor. Each scale unit ~100,000 rows in the pgbench_accounts table.
  • mydb: The database to test.

2. Run a Simple Benchmark Test

1
pgbench -c 10 -j 2 -T 60 mydb
  • -c 10: 10 concurrent clients.
  • -j 2: 2 threads.
  • -T 60: Run for 60 seconds.
  • mydb: Target database.

It will output something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[postgres@iZ2ze4mflpfiplp0evcw8gZ root]$ pgbench -c 10 -j 2 -T 60 mydb
pgbench (18devel)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 10
query mode: simple
number of clients: 10
number of threads: 2
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 12841
number of failed transactions: 0 (0.000%)
latency average = 46.726 ms
initial connection time = 39.072 ms
tps = 214.013767 (without initial connection time)
[postgres@iZ2ze4mflpfiplp0evcw8gZ root]$

3. Run Custom SQL Scripts

You can benchmark with custom SQL transactions:

1
pgbench -f myscript.sql -c 10 -T 60 mydb

Where myscript.sql contains something like:

1
2
3
4
BEGIN;
SELECT * FROM pgbench_accounts WHERE aid = :aid;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
END;

Use :variable for substitution. We can define variables using -D:

1
pgbench -f myscript.sql -D aid=12345 -D delta=50 -c 10 -T 60 mydb

要在发生故障后恢复数据一致性(即执行恢复操作),PostgreSQL 需要向前回放 WAL 日志,并将其中表示丢失变更的记录应用到相应的数据页上。为了确定哪些变更丢失了,系统会将磁盘上数据页的 LSN(日志序列号)与 WAL 记录的 LSN 进行比较。但问题是,我们应该从哪里开始恢复?如果恢复起点选得太晚,那么在此之前已经写入磁盘的数据页将无法接收到所有应有的变更,最终导致无法修复的数据损坏。而从日志的起始位置开始恢复又不现实:不仅无法长期保存如此巨量的数据,也无法接受过长的恢复时间。因此,我们需要一个不断向前推进的检查点(checkpoint),从而可以从这个位置安全地开始恢复,同时删除所有更早的 WAL 记录。

创建检查点最直接的方式是:定期暂停系统所有操作,并将所有脏页强制刷新到磁盘。但这种方式显然是不可接受的,因为系统会因此暂停不定但相当长的时间。

正因为如此,PostgreSQL 将检查点的过程分摊到一段时间内完成,实际上构成了一个“区间”(interval)。检查点的执行是由一个特殊的后台进程负责的,这个进程叫做 checkpointer(检查点进程)

检查点开始(Checkpoint start):

checkpointer 进程会将所有可以立即写入磁盘的内容进行刷新,包括:

  • CLOG(提交日志)中的事务状态信息,
  • 子事务的元数据,
  • 以及其他一些结构。

检查点执行过程(Checkpoint execution):

检查点执行的大部分时间都耗费在将 脏页(dirty pages)刷新到磁盘上。

首先,在检查点开始时,所有当时处于“脏”状态的缓冲区(buffer)的页头会被打上一个特殊标记(tag)。这个过程非常迅速,因为它不涉及任何 I/O 操作,只是内存中的标记设置。

随后,checkpointer 会遍历所有缓冲区,并将带有该标记的页写入磁盘。这些页不会被驱逐出缓存(即它们仍然保留在缓冲池中),只是被刷盘,因此在这个过程中可以忽略使用计数(usage count)和 pin 计数(pin count)。

页面按 ID 顺序处理,以尽可能避免随机写入。为实现更好的负载均衡,PostgreSQL 会在多个表空间之间交替进行写入(因为它们可能位于不同的物理设备上)。

后端进程(backend)也可以将打了标记的缓冲页写入磁盘 —— 如果它们先访问到了这些页的话。无论由谁写入,缓冲区的标记都会在这个阶段被清除,因此每个缓冲页在此次检查点中只会被写一次。

很自然地,在 checkpoint 进行期间,缓冲区中的页面仍然可能被修改。但由于这些新的脏页没有被打上标记,checkpointer 会忽略它们。

检查点完成:

当在检查点开始时被标记为脏的所有缓冲页都已经写入磁盘后,检查点就被视为完成。从现在起(但不是在此之前!),本次检查点的起始位置将被作为恢复操作的新起点。在这个点之前写入的所有 WAL 日志都不再需要了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Time →
LSN:
┌──────────────────────────────────────────────────────────────────────┐
│ │
0 ──┴─────┬───────────────────────────────────────────┬────────────┬───────┘
│ │ │
▼ ▼ ▼
1 2 3
✔ 若 3 写入成功:恢复可从 2 开始(即 redo = 2)
✘ 若 3 写入失败:恢复只能从 1 开始(上次 checkpoint)

说明:
- 1:上一次 checkpoint 的起始 LSN(redo)
- 2:本次 checkpoint 开始时wallog最大LSN(新的redo点)
- 3:本次 checkpoint 完成后写入 WAL 的记录(记录了 redo=2)

最后,checkpointer 进程会创建一条表示检查点完成的 WAL 记录,并在其中标明此次检查点的起始 LSN。由于检查点在开始时不会写入任何日志,因此这个起始 LSN 可以是任意类型的 WAL 记录所属的 LSN。
此外,PGDATA/global/pg_control 文件也会被更新,以指向最近完成的检查点。(在此过程完成之前,pg_control 始终保留着上一个检查点的信息。)

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
=> UPDATE big SET s = 'FOO';
=> SELECT count(*) FROM pg_buffercache WHERE isdirty;
count
−−−−−−−
4119
(1 row)

=> SELECT pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
−−−−−−−−−−−−−−−−−−−−−−−−−−−
0/3E7EF7E0 (1 row)

=> CHECKPOINT;
=> SELECT count(*) FROM pg_buffercache WHERE isdirty;
count
−−−−−−−
0
(1 row)

=> SELECT pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
−−−−−−−−−−−−−−−−−−−−−−−−−−−
0/3E7EF890
(1 row)

最新的 WAL 条目与检查点完成有关(CHECKPOINT_ONLINE)。该检查点的起始 LSN 出现在 redo 之后;这个位置对应的是检查点开始时最新插入的 WAL 条目。

同样的信息也可以在 pg_control 文件中找到。

1
2
3
postgres$ /usr/local/pgsql/bin/pg_controldata \
-D /usr/local/pgsql/data | egrep 'Latest.*location' Latest checkpoint location: 0/3E7EF818
Latest checkpoint's REDO location: 0/3E7EF7E0

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals

1. 安装编译工具

ASan 是 GCC 和 Clang 内建的功能,无需额外安装 ASan,只需要你的编译器支持即可。GCC ≥ 4.8 / Clang ≥ 3.1 就支持 ASan

1
2
3
4
5
6
7
8
9
10
11
root@lavm-bar1guved6:~# clang --version
Ubuntu clang version 14.0.0-1ubuntu1.1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

root@lavm-bar1guved6:~# gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE

2. how to use

使用gcc

1
gcc -fsanitize=address -g your_file.c -o your_program

使用cmake

1
2
3
4
5
6
7
8
9
10
11
12
13
# 编译阶段(C/C++)加 ASan 插桩
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -g")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -g")

# 链接阶段链接 libasan
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")

#or
cmake -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
-DCMAKE_BUILD_TYPE=Debug \
......

使用 Makefile

可以在 CFLAGS 中添加:

1
2
CFLAGS += -fsanitize=address -g -fno-omit-frame-pointer
LDFLAGS += -fsanitize=address

使用 LSAN(泄漏检测)

GCC 和 Clang 中 ASan 自动包含 LeakSanitizer(LSan),但某些情况下要确保:

1
-fsanitize=address -fno-omit-frame-pointer

加上 -fno-omit-frame-pointer 可以让调用栈更完整。

3. 运行程序

编译好的程序可以直接运行:

1
./your_program

4. 设置环境变量

1
export ASAN_OPTIONS=detect_leaks=1:halt_on_error=0:symbolize=1:quarantine_size=1024:log_path=/home/postgres/asan/asan.log

5. Example

1
2
3
4
5
6
7
8
9
=================================================================
==791265==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 1024 byte(s) in 1 object(s) allocated from:
#0 0x7f6fcee65887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x55adcdfee87a in main /home/postgres/codes/sample/epoll_server.c:54
#2 0x7f6fcebb1d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: 1024 byte(s) leaked in 1 allocation(s).

在发生故障(例如停电、操作系统错误或数据库服务器崩溃)时,RAM 中的所有内容都将丢失;只有写入磁盘的数据会保留下来。要在故障后启动服务器,您必须恢复数据一致性。如果磁盘本身已损坏,则必须通过备份恢复来解决相同的问题。

理论上,您可以始终保持磁盘上的数据一致性。但实际上,这意味着服务器必须不断地将随机页面写入磁盘(尽管顺序写入成本更低),并且此类写入的顺序必须保证在任何特定时刻都不会损害一致性(这很难实现,尤其是当您处理复杂的索引结构时)。

与大多数数据库系统一样,PostgreSQL 采用了一种不同的方法。

服务器运行时,部分当前数据仅存在于 RAM 中,其写入永久存储的操作被推迟。因此,服务器运行时存储在磁盘上的数据始终是不一致的,因为页面从不会一次性全部刷新。但是,RAM 中发生的每个更改(例如在缓冲区缓存中执行的页面更新)都会被记录下来:PostgreSQL 会创建一个日志条目,其中包含在需要时重复此操作所需的所有基本信息。

页面修改相关的日志条目必须先于修改后的页面本身写入磁盘。这就是日志名称的由来:预写式日志(write-ahead log),简称 WAL。这项要求保证了在发生故障时,PostgreSQL 可以从磁盘读取 WAL 条目并重放它们,以重复那些已完成但结果仍在 RAM 中且在崩溃前未写入磁盘的操作。

保留预写式日志通常比将随机页面写入磁盘更高效。WAL 条目构成一个连续的数据流,即使是硬盘驱动器 (HDD) 也能很好地处理。此外,WAL 条目通常比页面大小更小。

为了在发生故障时避免数据不一致,所有可能破坏数据一致性的操作都需要记录下来。具体来说,以下操作会记录在 预写式日志 (WAL) 中:

  • WAL 记录的操作
  1. 缓冲区缓存中的页面修改:由于写入是延迟的,这些修改需要记录下来以备恢复。
  2. 事务提交和回滚:事务状态的变化发生在 CLOG 缓冲区中,不会立即写入磁盘,因此需要记录。
  3. 文件操作:当添加或删除表时,文件和目录的创建与删除等操作必须与数据更改同步,所以也要记录。
  • WAL 不记录的操作
  1. unlogged表相关操作
  2. 临时表上的操作:由于临时表的生命周期仅限于创建它们的会话,所以它们的操作不会被记录。

    在 PostgreSQL 10 之前,哈希索引的操作也不会被记录。它们的主要目的是将哈希函数与不同的数据类型匹配。

除了用于崩溃恢复之外,WAL 还可以用于从备份进行时间点恢复以及数据复制。

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals

背景

使用 GDB 调试时,有时候需要修改返回值,但是返回值有时候无法直接使用p修改,如std::list::empty(); 如何修改标准库的empty函数返回值呢?

修改寄存器值

函数的返回值通常存储在特定寄存器中(例如,x86-64 架构中是 %rax)。在函数返回之前,你可以修改 %rax 的值。

步骤:

  1. 在函数返回之前设置断点(如 ret 指令处)
    1
    b *func+<offset> # 偏移地址为即将返回的位置

2.运行程序并等待断点触发
3.修改返回值所在的寄存器(例如 %rax):

概念

Raft 是一个分布式一致性算法,被设计为比 Paxos 更易于理解,同时具备相似的性能和安全性。它常用于构建容错的分布式系统,确保多个节点在面对网络分区、节点失效等情况下能够达成一致

核心目标

  1. Leader选举:通过选举机制确保每个时间段只有一个节点(Leader)负责日志复制和状态变更。
  2. 日志复制:Leader 将客户端的操作(日志)复制到其他节点(Follower),确保日志的一致性。
  3. 状态机一致性:通过确保所有节点按相同顺序应用日志,实现一致性。

主要模块

1. 角色

  • Leader:负责接收客户端请求,将操作以日志形式写入并同步给 Follower。
  • Follower:响应 Leader 的同步请求,被动地接受 Leader 的日志和指令。
  • Candidate:在 Leader 失效后,由 Follower 转为 Candidate,通过投票选举自己为新 Leader。

2. 选举过程

  • 若 Follower 超时未收到 Leader 的心跳信号,会转为 Candidate 并发起选举。
  • 每个节点在选举期间投票给自己,同时请求其他节点投票。
  • 如果一个 Candidate 获得了超过半数的投票,则成为 Leader。

3. 日志复制

  • Leader 接收到客户端请求后,将其作为日志条目添加到自己的日志中。
  • Leader 使用 AppendEntries RPC 将日志复制到 Follower。
  • 当多数节点确认日志条目后,Leader 将日志提交,并通知所有节点应用日志到状态机。

4. 一致性保证

  • 使用 任期号(Term) 防止陈旧的 Leader 发出无效指令。
  • 确保日志条目在所有节点上按照相同顺序出现,避免状态不一致。

扩展:MultiRaft协议

MultiRaft 是 Raft 的一种扩展,旨在支持多个 Raft 实例同时运行,以便在大规模分布式系统中更高效地管理数据分片和分布式事务。

动机

  1. 单个 Raft 实例在处理大量数据时可能成为瓶颈。
  2. 在分布式系统中,通常需要对数据进行分区,每个分区由独立的一组节点管理。
  3. MultiRaft 提供了一种机制,通过运行多个 Raft 实例,每个实例负责一部分数据,从而提高系统的吞吐量和扩展性

核心思想

  1. 多实例并行运行:
  • 每个 Raft 实例管理一个独立的数据分片(shard)。
  • 每个实例有自己的 Leader、Follower 和日志,独立运行 Raft 协议。
  1. 共享底层资源:
  • 多个 Raft 实例可以运行在相同的物理节点上,共享网络、存储和 CPU 等资源。
  • 使用高效的调度机制协调实例间的资源竞争。
  1. 动态分片和迁移:
  • 数据分片可以动态调整,每个分片由一个 Raft 实例管理。
  • 分片可以在节点之间迁移,以应对节点故障或负载不均。
  1. 跨分片操作:
  • 支持分布式事务,需要在多个 Raft 实例之间协调。
  • 一般通过两阶段提交(2PC)或共识组间通信来实现。

对比

特性 Raft MultiRaft
目标 提供单一一致性机制 提供分区一致性机制
运行实例 单个 Raft 集群 多个独立的 Raft 集群
适用场景 小规模系统 大规模分布式存储或事务场景
扩展性 有限,单点可能成为瓶颈 高扩展性,分片机制避免瓶颈
复杂度 较低 较高,需要处理跨分片事务

If bulk reads or writes are performed, there is a risk that one-time data can quickly oust useful pages from the buffer cache.

As a precaution, bulk operations use rather small buffer rings, and eviction is performed within their boundaries, without affecting other buffers.

A buffer ring of a particular size consists of an array of buffers that are used one after another. At first, the buffer ring is empty, and individual buffers join it one by one, after being selected from the buffer cache in the usual manner. Then eviction comes into play,but only within the ring limits

Buffers added into a ring are not excluded from the buffer cache and can still be used by other operations. So if the buffer to be reused turns out to be pinned, or its usage count is higher than one, it will be simply detached from the ring and replaced by another buffer.
PostgreSQL supports three eviction strategies.

strategy trigger buffer ring
Bulk reads sequential scans of large tables if their size exceeds 1/4 of the buffer cache(128MB:16384 page) 256KB(32 page)
Bulk writes applied by Copy from, create table as select , and create materialized view commands, as well as by those alter table flavors that cause table rewrites. default: 16MB(2048 page)
Vacuuming full table scan without taking the visibility map into account 256KB(32 page)

Buffer rings do not always prevent undesired eviction. If UPDATE or DELETE commands affect a lot of rows, the performed table scan applies the bulk reads strategy, but since the pages are constantly being modified, buffer rings virtually become useless.

Another example worth mentioning is storing oversized data in TOAST tables. In spite of a potentially large volume of data that has to be read, toasted values are always accessed via an index, so they bypass buffer rings.

Let’s take a closer look at the bulk reads strategy. For simplicity, we will create a table in such a way that an inserted row takes the whole page. By default, the buffer cache size is 16,384 pages, 8 kb each. So the table must take more than 4096 pages for the scan to use a buffer ring.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
test=# SHOW shared_buffers;
shared_buffers
----------------
128MB
(1 row)

test=# CREATE TABLE big(
test(# id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, s char(1000)
test(# ) WITH (fillfactor = 10);
CREATE TABLE
test=# INSERT INTO big(s)
test-# SELECT 'FOO' FROM generate_series(1,4096+1);
INSERT 0 4097
test=# ANALYZE big;
ANALYZE
test=# SELECT relname, relfilenode, relpages FROM pg_class
test-# WHERE relname IN ('big', 'big_pkey');
relname | relfilenode | relpages
----------+-------------+----------
big | 16487 | 4097
big_pkey | 16492 | 14
(2 rows)

test=# EXPLAIN (analyze, costs off, timing off, summary off) SELECT * FROM big;
QUERY PLAN
--------------------------------------------
Seq Scan on big (actual rows=4097 loops=1)
(1 row)

test=# SELECT count(*) FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
-------
32
(1 row)

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals

使用 x 命令和内存地址:

x (examine) 命令允许你检查内存中的数据。你可以指定要检查的内存地址、格式和单位。

基本语法: x /nfu addr

  • n:要显示的单位数量。

  • f:显示格式(例如,x 表示十六进制,t 表示二进制,d 表示十进制)。

  • u:单位大小(b 表示字节,h 表示半字(16 位),w 表示字(32 位),g 表示巨字(64 位))。

  • addr:要检查的内存地址。

示例: 假设有一个 32 位整数 val,其地址为 0x12345678,你想以 16 位为单位查看:

1
x /2ht 0x12345678

这将显示从 0x12345678 开始的两个半字(16 位)的十六进制值。

结合 p 命令获取地址: 你可以使用 p &变量名 获取变量的地址,然后将其传递给 x 命令。

1
2
3
4
5
6
7
8

#include <stdio.h>

int main() {
int val = 0x12345678;
printf("val = 0x%x\n", val);
return 0;
}

编译并用 GDB 调试:

1
2
gcc -g test.c -o test
gdb ./test

在 GDB 中:

1
2
3
4
5
6
(gdb) break main
(gdb) run
(gdb) p &val
$1 = (int *) 0x7fffffffe0dc
(gdb) x /2ht 0x7fffffffe0dc
0x7fffffffe0dc: 0x5678 0x1234

这里注意字节序,在小端序机器上,低位字节在前,高位字节在后,所以显示为 0x5678 0x1234。
如果想要二进制显示,则使用:

1
2
x /2ht 0x7fffffffe0dc
0x7fffffffe0dc: 0101011001111000 0001001000110100

使用表达式和位运算(对于变量值):

如果你只想查看变量的值,而不需要查看内存,可以使用 C 语言的位运算来提取 16 位部分,然后使用 p/t 显示。

示例:

1
int val = 0x12345678;

在 GDB 中:

1
2
3
4
(gdb) p/t (val & 0xFFFF) // 获取低 16 位
$1 = 0101011001111000
(gdb) p/t ((val >> 16) & 0xFFFF) // 获取高 16
$2 = 0001001000110100

总结:

对于查看内存中的数据(包括变量在内存中的表示),使用 x 命令结合 h (半字) 和 t (二进制) 格式指定符是最直接的方法。
对于只查看变量的值,使用位运算提取 16 位部分,然后使用 p/t 也是一个有效的选择。
选择哪种方法取决于你的具体需求。如果你需要查看变量在内存中的布局(例如,在结构体或数组中),x 命令是更好的选择。如果你只需要查看变量值的不同部分,位运算可能更方便。

使用如下.gdbinit 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
set target-async 1 
set pagination off
set non-stop on


set print pretty on
set print object on
set print static-members on
set print demangle on
set print sevenbit-strings off

python
import sys
sys.path.insert(0,'/usr/share/gcc-4.8.2/python*)
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (None)
end

GDB 加载脚本条件

  1. 路径完全匹配(核心条件)
    GDB 的自动加载逻辑非常死板,它会根据你的程序链接的 libstdc++.so.6 的真实物理路径来拼接查找路径。
    如果你的程序链接的是 /usr/lib64/libstdc++.so.6,那么 GDB 会尝试在以下位置查找: [auto-load 根目录] + [库的绝对路径] + [-gdb.py 脚本]
    即:/usr/share/gdb/auto-load + /usr/lib64/libstdc++.so.6 + -gdb.py 也就是你找到的:/usr/share/gdb/auto-load/usr/lib64/libstdc++.so.6-gdb.py。

  2. 必须开启 safe-path 权限
    即使文件位置对了,GDB 为了安全默认也是不运行它的。你需要给它“授权”。

    1
    set auto-load safe-path /

Check script load status

start gdb:

1
info auto-load python-scripts

有时候程序莫名内存上涨厉害,但不是泄漏,如何定位哪申请使用的大量内存呢?

如果没使用tcmalloc,使用heaptrack工具(git clone https://github.com/KDE/heaptrack.git);

如果使用了tcmalloc,可以通过tcmalloc自带的工具来分析

一 工具

  1. tcmalloc
  2. pprof

pprof相当于分析器,阅读器,分析阅读的内容为heap文件,内容包括

  • 每个活跃分配的大小
  • 分配的调用栈
  • 分配次数

二 生成heap文件

  • 手动
    在程序的入口或需要开始分析的地方,添加 HeapProfilerStart(“文件路径”)。
    在程序结束或需要停止分析的地方,添加 HeapProfilerStop()。
    在需要手动生成快照的地方,添加 HeapProfilerDump(“文件名”)。

  • 自动
    设置环境变量,动态加载设置环境变量即可,tcmalloc库加载时会读取并解析这个环境变量,从而开启堆分析功能

    1
    export HEAP_PROFILE=/tmp/myapp_heap

    如果是静态加载,需代码中启动:HeapProfilerStart

  • 补充说明
    自动生成文件的时机:申请内存超过阈值时会自动生成,阈值可以通过HEAP_PROFILE_ALLOCATION_INTERVAL环境变量来设置

三 分析heap文件(快照)

单次快照

1
2
3
4
5
# 文本格式
pprof test_heap --text /tmp/test_heap.0001.heap

# 火焰图
pprof --svg /path/to/my_program.hprof > flamegraph.svg

两次快照:

1
2
3
4
5
# 对比两次快照
pprof --inuse_space --base=/tmp/my_app.0001.heap /tmp/my_app.0002.heap

# 生成火焰图
pprof --svg --inuse_space --base=/tmp/my_app.0001.heap /tmp/my_app.0002.heap > diff_flamegraph.svg

火焰图需要额外安装

1
yum install graphviz

sample

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <gperftools/heap-profiler.h>
#include <cstdlib>
#include <iostream>

int main() {
HeapProfilerStart("/tmp/test_heap");
void* p = malloc(16 * 1024 * 1024); // 分配 16MB
std::cout << "Allocated 16MB\n";
HeapProfilerDump("after_alloc");
free(p);
HeapProfilerStop();
return 0;
}
1
g++ test_heap.cc -ltcmalloc -lpthread -lunwind -o test_heap
0%