pgbench — 在 PostgreSQL 上运行一次基准测试
pgbench -i [option...] [dbname]
pgbench [option...] [dbname]
pgbench 是一个在 PostgreSQL 上运行基准测试的简单程序。它一遍又一遍地运行同样的 SQL 命令序列,可能会在多个并发的数据库会话中进行,然后计算平均事务速率(每秒事务数)。默认情况下,pgbench 测试一个大致基于 TPC-B 的场景,其中每个事务涉及五条 SELECT、UPDATE 和 INSERT 命令。不过,通过编写自己的事务脚本文件,很容易测试其他场景。
pgbench 的典型输出如下所示:
transaction type: <builtin: TPC-B (sort of)> scaling factor: 10 query mode: simple number of clients: 10 number of threads: 1 maximum number of tries: 1 number of transactions per client: 1000 number of transactions actually processed: 10000/10000 number of failed transactions: 0 (0.000%) latency average = 11.013 ms latency stddev = 7.351 ms initial connection time = 45.758 ms tps = 896.967014 (without initial connection time)
前七行报告了一些最重要的参数设置。第六行报告针对出现序列化错误或死锁错误的事务的最大尝试次数(更多信息见 Failures and Serialization/Deadlock Retries)。第八行报告已完成的事务数和预期的事务数(后者只是客户端数量与每个客户端事务数的乘积);除非运行在完成之前失败,或者某些 SQL 命令失败,否则这两者应当相等。(在 -T 模式下,只打印实际的事务数。)下一行报告由于序列化错误或死锁错误而失败的事务数(更多信息见 Failures and Serialization/Deadlock Retries)。最后一行报告每秒事务数。
默认的类 TPC-B 事务测试需要预先建立一些特定的表。应当使用 pgbench 的 -i(初始化)选项来创建并填充这些表。(测试自定义脚本时不需要这一步,但仍需要完成测试所需的任何准备工作。)初始化过程如下所示:
pgbench -i [other-options]dbname
其中 dbname 是要在其中进行测试的、已经创建好的数据库的名称。(可能还需要 -h、-p 和/或 -U 选项来指定如何连接到数据库服务器。)
pgbench -i 会创建四张表:pgbench_accounts、pgbench_branches、pgbench_history 和 pgbench_tellers,并销毁任何同名的现有表。如果你的数据库中已经有这些名字的表,请务必改用另一个数据库!
在默认的“比例因子”为 1 时,这些表最初包含的行数如下:
table # of rows --------------------------------- pgbench_branches 1 pgbench_tellers 10 pgbench_accounts 100000 pgbench_history 0
可以使用 -s(比例因子)选项来增加行数(对于大多数用途来说,或许也应该这样做)。此时也可以使用 -F(fillfactor)选项。
完成必要的准备工作之后,就可以用一条不带 -i 的命令来运行基准测试:
pgbench [options]dbname
在几乎所有情况下,你都需要一些选项才能进行有用的测试。最重要的选项是 -c(客户端数量)、-t(事务数量)、-T(时限)和 -f(指定自定义脚本文件)。完整列表见下文。
下面的内容分为三个小节。数据库初始化期间和运行基准测试期间使用的选项有所不同,但有些选项在两种情况下都有用。
pgbench 接受下列命令行初始化参数:
[-d] dbname[--dbname=]dbname # 指定要在其中进行测试的数据库的名称。如果未指定,将使用环境变量 PGDATABASE。如果该变量也未设置,则使用为连接指定的用户名。
-i--initialize #进入初始化模式所必需。
-I init_steps--init-steps=init_steps # 只执行正常初始化步骤中选定的一个子集。init_steps 指定要执行的初始化步骤,每个步骤用一个字符表示。各步骤按指定的顺序执行。默认值为 dtgvp。可用的步骤有:
d (Drop) #删除任何已存在的 pgbench 表。
t (create Tables) #创建标准 pgbench 场景所使用的那些表,即 pgbench_accounts、pgbench_branches、pgbench_history 和 pgbench_tellers。
g or G (Generate data, client-side or server-side) #生成数据并载入到标准表中,替换任何已有的数据。
使用 g(客户端数据生成)时,数据在 pgbench 客户端中生成,然后被发送到服务器。这会通过一次 COPY 大量占用客户端/服务器带宽。pgbench 会使用 FREEZE 选项把数据载入普通(非分区)表中,这适用于版本 14 或更高版本的 PostgreSQL,目的是加快后续的 VACUUM。使用 g 会让日志在为所有表生成数据时每 100,000 行打印一条消息。
使用 G(服务器端数据生成)时,只会从 pgbench 客户端发送一些小查询,而数据实际是在服务器中生成的。这种变体不需要多少带宽,但服务器要做更多工作。使用 G 会让日志在生成数据时不打印任何进度消息。
默认的初始化行为使用客户端数据生成(等效于 g)。
v (Vacuum) #对标准表执行 VACUUM。
p (create Primary keys) #在标准表上创建主键索引。
f (create Foreign keys) #在标准表之间创建外键约束。(注意默认不执行这一步。)
-F fillfactor--fillfactor=fillfactor #以给定的 fillfactor 创建 pgbench_accounts、pgbench_tellers 和 pgbench_branches 表。默认值为 100。
-n--no-vacuum #在初始化期间不执行任何清理。(该选项会抑制 v 初始化步骤,即使它在 -I 中被指定也是如此。)
-q--quiet #把日志切换到安静模式,每 5 秒只产生一条进度消息。默认的日志每 100,000 行打印一条消息,这通常每秒会输出很多行(尤其是在好的硬件上)。
如果 G 在 -I 中被指定,则该设置没有效果。
-s scale_factor--scale=scale_factor #将生成的行数乘以比例因子。例如,-s 100 将在 pgbench_accounts 表中创建 10,000,000 行。默认值为 1。当比例达到 20,000 或更大时,用于保存账户标识符的列(aid 列)将改用更大的整数(bigint),以便足以容纳账户标识符的取值范围。
--foreign-keys #在标准表之间创建外键约束。(如果初始化步骤序列中尚未包含该步骤,该选项会把 f 步骤添加进去。)
--index-tablespace=index_tablespace #在指定的表空间而不是默认表空间中创建索引。
--partition-method=NAME #创建一个分区的 pgbench_accounts 表,分区方法为 NAME。预期的值是 range 或 hash。该选项要求 --partitions 被设置为非零值。如果未指定,默认为 range。
--partitions=NUM #创建一个分区的 pgbench_accounts 表,将缩放后的账户数量分成大小接近的 NUM 个分区。默认值为 0,表示不进行分区。
--tablespace=tablespace #在指定的表空间而不是默认表空间中创建表。
--unlogged-tables #将所有表创建为非日志表而不是永久表。
pgbench 接受下列命令行基准测试参数:
-b scriptname[@weight]--builtin=scriptname[@weight] # 将指定的内建脚本添加到要执行的脚本列表中。可用的内建脚本有:tpcb-like、simple-update 和 select-only。内建名称的无歧义前缀也可接受。使用特殊名称 list 时,将显示内建脚本的列表并立即退出。
还可以在 @ 之后写一个整数权重,用来调整此脚本相对于其他脚本被选中的概率。默认权重为 1。详见下文。
-c clients--client=clients #模拟的客户端数量,即并发数据库会话的数量。默认值为 1。
-C--connect #为每个事务建立一个新的连接,而不是每个客户端会话只建立一次。这对于度量连接开销很有用。
-D varname=value--define=varname=value #定义一个供自定义脚本使用的变量(见下文)。允许多个 -D 选项。
-f filename[@weight]--file=filename[@weight] #把一个从 filename 读取的事务脚本加入要执行的脚本列表。
还可以在 @ 之后写一个整数权重,用来调整此脚本相对于其他脚本被选中的概率。默认权重为 1。(若要使用的脚本文件名中包含 @ 字符,请追加一个权重以消除歧义,例如 filen@me@1。)详见下文。
-j threads--jobs=threads #pgbench 内的工作线程数。在多 CPU 机器上使用多个线程可能会有帮助。客户端会被尽可能均匀地分布到各个可用线程上。默认值为 1。
-l--log #把每个事务的信息写入一个日志文件。详见下文。
-L limit--latency-limit=limit #持续超过 limit 毫秒的事务会被单独计数并报告为迟到(late)。
使用节流(--rate=...)时,比计划进度滞后超过 limit 毫秒、因而毫无希望满足延迟限制的事务根本不会被发送到服务器。它们会被单独计数并报告为跳过(skipped)。
使用 --max-tries 选项时,如果某个事务由于序列化异常或死锁而失败,并且其所有尝试的总时间超过 limit 毫秒,则该事务不会被重试。要只限制尝试的时间而不限制次数,请使用 --max-tries=0。默认情况下,选项 --max-tries 被设置为 1,出现序列化/死锁错误的事务不会被重试。有关重试此类事务的更多信息,见 Failures and Serialization/Deadlock Retries。
-M querymode--protocol=querymode #用于向服务器提交查询的协议:
simple:使用简单查询协议。
extended:使用扩展查询协议。
prepared:使用带已预备语句的扩展查询协议。
在 prepared 模式下,pgbench 从第二次查询迭代开始会重用语法分析结果,因此 pgbench 比其他模式运行得更快。
默认为简单查询协议。(更多信息见 Chapter 54。)
-n--no-vacuum #在运行测试之前不执行清理。该选项是必需的,如果你要运行的自定义测试场景不包含标准表 pgbench_accounts、pgbench_branches、pgbench_history 和 pgbench_tellers。
-N--skip-some-updates #运行内建的 simple-update 脚本。是 -b simple-update 的简写。
-P sec--progress=sec #每隔 sec 秒显示一次进度报告。该报告包括自运行开始以来经过的时间、自上次报告以来的 TPS,以及自上次报告以来的事务延迟平均值、标准差和失败事务数。在节流(-R)模式下,延迟是相对于事务的计划开始时间计算的,而不是事务实际开始的时间,因此它还包括平均调度滞后时间。当使用 --max-tries 来启用针对序列化/死锁错误的事务重试时,报告还包括被重试的事务数以及所有重试的总次数。
-r--report-per-command #在基准测试结束后,报告每条命令的以下统计信息:每条语句的平均延迟(从客户端角度看的执行时间)、失败次数,以及该命令在序列化错误或死锁错误后的重试次数。仅当 --max-tries 选项不等于 1 时,报告才显示重试统计信息。
-R rate--rate=rate #以指定的速率为目标来执行事务,而不是尽可能快地运行(后者是默认行为)。速率以每秒事务数给出。如果目标速率高于可能达到的最大速率,则速率限制不会影响结果。
速率控制通过按照泊松分布的计划时间线启动事务来实现。预期的开始时间计划基于客户端最初启动的时刻向前推进,而不是基于上一个事务结束的时刻。这种方式意味着,当事务超过其原定的计划结束时间后,后面的事务仍有可能重新赶上来。
当节流生效时,运行结束时报告的事务延迟是根据计划开始时间计算的,因此它包含了每个事务为等待前一个事务完成而花费的时间。这段等待时间称为调度滞后时间,其平均值和最大值也会被单独报告。相对于事务实际开始时间的事务延迟(即在数据库中执行该事务所花费的时间),可以用报告的延迟减去调度滞后时间来计算。
如果 --latency-limit 与 --rate 一起使用,由于延迟是从计划开始时间计算的,一个事务可能滞后太多,以至于在前一个事务结束时它已经超过了延迟限制。这样的事务不会被发送到服务器,而是被完全跳过并单独计数。
调度滞后时间过长表明,在选定的客户端和线程数量下,系统无法以指定的速率处理事务。当事务的平均执行时间长于每个事务之间的计划间隔时,每个后续事务都会落后得更多,而且测试运行得越久,调度滞后时间就会不断增加。出现这种情况时,你将不得不降低指定的事务速率。
-s scale_factor--scale=scale_factor #在 pgbench 的输出中报告指定的比例因子。对于内建测试,不需要这样做;正确的比例因子将通过统计 pgbench_branches 表中的行数检测出来。然而,当只测试自定义基准测试(-f 选项)时,如果不使用该选项,比例因子将被报告为 1。
-S--select-only #运行内建的 select-only 脚本。是 -b select-only 的简写。
-t transactions--transactions=transactions #每个客户端运行的事务数。默认值为 10。
-T seconds--time=seconds #让测试运行这么多秒,而不是每个客户端运行固定数量的事务。-t 与 -T 互斥。
-v--vacuum-all #在运行测试之前对所有四张标准表执行清理。当既不使用 -n 也不使用 -v 时,pgbench 会对 pgbench_tellers 和 pgbench_branches 表执行清理,并截断 pgbench_history。
--aggregate-interval=seconds #聚合区间的长度(以秒为单位)。只能与 -l 选项一起使用。使用该选项时,日志中包含每个区间的汇总数据,如下文所述。
--exit-on-abort #当任何客户端由于某种错误而中止时立即退出。如果没有该选项,即使某个客户端被中止,其他客户端仍可以按照 -t 或 -T 选项的设定继续运行,并且在这种情况下 pgbench 会打印不完整的结果。
注意,序列化失败或死锁失败不会导致客户端中止,因此它们不受该选项的影响。更多信息见 Failures and Serialization/Deadlock Retries。
--failures-detailed #在每事务日志和聚合日志以及主报告和每脚本报告中按下列类型对失败进行分组报告:
序列化失败;
死锁失败;
--log-prefix=prefix #为由 --log 创建的日志文件设置文件名前缀。默认值为 pgbench_log。
--max-tries=number_of_tries #为出现序列化/死锁错误的事务启用重试,并设置这些尝试的最大次数。该选项可以与 --latency-limit 选项结合使用,后者会限制所有事务尝试的总时间;此外,使用无限次尝试(--max-tries=0)时必须同时使用 --latency-limit 或 --time。默认值为 1,出现序列化/死锁错误的事务不会被重试。有关重试此类事务的更多信息,见 Failures and Serialization/Deadlock Retries。
--progress-timestamp #显示进度时(选项 -P),使用时间戳(Unix 纪元)而不是自运行开始以来的秒数。单位为秒,小数点后精确到毫秒。这有助于比较由各种工具生成的日志。
--random-seed=seed #设置随机数生成器的种子。该选项为系统随机数生成器设置种子,随后它会为每个线程产生一个初始生成器状态。seed 的取值可以是:time(默认值,种子基于当前时间)、rand(使用强随机源,如果没有可用的强随机源则失败),或者一个无符号十进制整数值。随机数生成器会被 pgbench 脚本显式调用(random... 函数),也会被隐式调用(例如选项 --rate 用它来调度事务)。显式设置时,用于设置种子的值会显示在终端上。seed 允许的任何取值也可以通过环境变量 PGBENCH_RANDOM_SEED 来提供。为确保所提供的种子能影响所有可能的用途,请把该选项放在最前面,或者使用环境变量。
显式设置种子可以精确地重现一次 pgbench 运行(就随机数而言)。由于随机状态是按线程管理的,这意味着对于完全相同的调用,如果每个线程有一个客户端并且没有外部或数据依赖,就会得到完全相同的 pgbench 运行。从统计的角度来看,精确重现运行并不是一个好主意,因为它可能掩盖性能的可变性,或者不适当地提高性能,例如命中与上一次运行相同的页面。不过,它对调试也可能大有帮助,例如重新运行某个会导致错误的棘手案例。请明智地使用。
--sampling-rate=rate #采样率,在把数据写入日志时使用,用于减少生成的日志量。如果给出了该选项,则只有指定比例的事务会被记录。1.0 表示所有事务都会被记录,0.05 表示只有 5% 的事务会被记录。
处理日志文件时,请记得考虑采样率。例如,在计算 TPS 值时,需要相应地对数字进行换算(例如,采样率为 0.01 时,你只能得到实际 TPS 的 1/100)。
--show-script=scriptname #在 stderr 上显示内建脚本 scriptname 的实际代码,然后立即退出。
--verbose-errors #打印关于所有错误和失败(不重试的错误)的消息,包括对于序列化/死锁失败超出了哪个重试限制以及被超出了多少。(注意在这种情况下,输出可能会显著增加。)更多信息见 Failures and Serialization/Deadlock Retries。
运行成功时将以状态 0 退出。退出状态 1 表示静态问题,例如无效的命令行选项,或者预期永远不会发生的内部错误。启动基准测试时发生的早期错误(例如初始连接失败)也会以状态 1 退出。运行期间发生的错误(例如数据库错误或脚本中的问题)将导致退出状态 2。在后一种情况下,pgbench 在未指定 --exit-on-abort 选项时会打印部分结果。
PGDATABASEPGHOSTPGPORTPGUSER #默认连接参数
该工具与其他大多数 PostgreSQL 工具一样,使用 libpq 支持的环境变量(见 Section 32.15)。
环境变量 PG_COLOR 指定是否在诊断消息中使用颜色。可能的值有 always、auto 和 never。
pgbench 执行从指定列表中随机挑选的测试脚本。这些脚本可以包括用 -b 指定的内建脚本和用 -f 指定的用户提供的脚本。每个脚本可以指定一个相对权重,写在 @ 之后,用来改变它被选中的概率。默认权重为 1。权重为 0 的脚本会被忽略。
默认的内建事务脚本(也可以用 -b tpcb-like 调用)针对随机选择的 aid、tid、bid 和 delta,在每个事务中发出七条命令。该场景受到 TPC-B 基准测试的启发,但并不是真正的 TPC-B,因此得名。
BEGIN;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
END;
如果选择 simple-update 内建脚本(也就是 -N),事务中将不包含第 4 步和第 5 步。这可以避免这些表上的更新争用,但会让测试场景更加不像 TPC-B。
如果选择 select-only 内建脚本(也就是 -S),则只会发出 SELECT。
pgbench 支持运行自定义的基准测试场景,做法是把默认的事务脚本(如上文所述)替换为从文件读取的事务脚本(-f 选项)。在这种情况下,一次“事务”计为脚本文件的一次执行。
脚本文件包含一条或多条以分号结尾的 SQL 命令。空行和以 -- 开头的行会被忽略。脚本文件还可以包含“元命令”,这类命令由 pgbench 自身进行解释,如下文所述。
在 PostgreSQL 9.6 之前,脚本文件中的 SQL 命令以换行符结束,因此无法跨行续写。现在,分隔连续的 SQL 命令时必须使用分号(不过,如果 SQL 命令后面跟的是元命令,则不需要分号)。如果你需要创建一个在新旧版本的 pgbench 中都能使用的脚本文件,请务必把每条 SQL 命令写在单独的一行上,并以分号结尾。
假定 pgbench 脚本不包含不完整的 SQL 事务块。如果在运行时客户端在没有完成最后一个事务块的情况下到达了脚本末尾,该客户端将被中止。
脚本文件有一个简单的变量替换机制。变量名必须由字母(包括非拉丁字母)、数字和下划线组成,且第一个字符不能是数字。变量可以通过上文介绍的命令行选项 -D 来设置,也可以通过下文介绍的元命令来设置。除了由 -D 命令行选项预设的变量之外,还有一些自动预设的变量,列在 Table 298 中。使用 -D 为这些变量指定的值优先于自动预设值。变量一旦被设置,就可以通过写 :variablename 把它的值插入到 SQL 命令中。运行多个客户端会话时,每个会话都有自己的一组变量。pgbench 在一条语句中最多支持 255 个变量引用。
Table 298. pgbench 自动变量
| Variable | Description |
|---|---|
client_id | 标识客户端会话的唯一编号(从零开始) |
default_seed | 默认在哈希函数和伪随机排列函数中使用的种子 |
random_seed | 随机数生成器的种子(除非被 -D 覆盖) |
scale | 当前的比例因子 |
脚本文件的元命令以反斜杠(\)开头,通常延伸到行尾,不过也可以通过书写反斜杠加换行把它们延续到后续的行。元命令的参数以空白分隔。支持以下元命令:
\gset [prefix]
\aset [prefix]
#这些命令可用于结束 SQL 查询,取代终止用的分号(;)。
使用 \gset 命令时,前面的 SQL 查询应当返回一行,其各列会被存储到以列名命名的变量中,如果提供了 prefix,还会加上该前缀。
使用 \aset 命令时,所有组合在一起的 SQL 查询(以 \; 分隔)的各列都会被存储到以列名命名的变量中,如果提供了 prefix,还会加上该前缀。如果某个查询没有返回行,则不会进行赋值,可以通过检查变量是否存在来发现这种情况。如果某个查询返回了多行,则保留最后一个值。
\gset 和 \aset 不能在管道模式下使用,因为当这些命令需要查询结果时,结果尚不可用。
下面的例子把第一个查询得到的最终账户余额存入变量 abalance,并用第三个查询得到的整数填充变量 p_two 和 p_three。第二个查询的结果被丢弃。最后两个组合查询的结果被存储在变量 four 和 five 中。
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid RETURNING abalance \gset -- compound of two queries SELECT 1 \; SELECT 2 AS two, 3 AS three \gset p_ SELECT 4 AS four \; SELECT 5 AS five \aset
\if expression\elif expression\else\endif #这组命令实现了可嵌套的条件块,类似于 psql 的 \if expression。条件表达式与 \set 所用的条件表达式相同,非零值被解释为真。
\set varname expression
#将变量 varname 设置为由 expression 计算得到的值。表达式可以包含 NULL 常量、布尔常量 TRUE 和 FALSE、诸如 5432 的整数常量、诸如 3.14159 的双精度常量、对变量 :variablename 的引用、具有常规 SQL 优先级和结合性的 operators、function calls、SQL CASE generic conditional expressions 以及圆括号。
当输入为 NULL 时,函数和大多数运算符都返回 NULL。
在用于条件判断时,非零数值为 TRUE,零值和 NULL 为 FALSE。
过大或过小的整数和双精度常量,以及整数算术运算符(+、-、* 和 /)在溢出时会引发错误。
如果没有为 CASE 提供最后的 ELSE 子句,则默认值为 NULL。
Examples:
\set ntellers 10 * :scale
\set aid (1021 * random(1, 100000 * :scale)) % \
(100000 * :scale) + 1
\set divx CASE WHEN :x <> 0 THEN :y/:x ELSE NULL END
\sleep number [ us | ms | s ]
#使脚本执行休眠指定的时长,单位可以是微秒(us)、毫秒(ms)或秒(s)。如果省略单位,则默认为秒。number 可以是整数常量,也可以是对一个具有整数值的变量的 :variablename 形式的引用。
Example:
\sleep 10 ms
\setshell varname command [ argument ... ]
#将变量 varname 设置为 shell 命令 command 以给定 argument(一个或多个参数)执行后的结果。该命令必须通过其标准输出返回一个整数值。
command 和每个 argument 都可以是文本常量,或者是对某个变量的 :variablename 引用。如果想使用以冒号开头的 argument,请在 argument 的开头再写一个冒号。
Example:
\setshell variable_to_be_assigned command literal_argument :variable ::literal_starting_with_colon
\shell command [ argument ... ]
#与 \setshell 相同,但命令的结果会被丢弃。
Example:
\shell command literal_argument :variable ::literal_starting_with_colon
\startpipeline\syncpipeline\endpipeline #这组命令实现了 SQL 语句的流水线。流水线必须以 \startpipeline 开始,以 \endpipeline 结束。两者之间可以有任意数量的 \syncpipeline 命令,它会发送一个 sync message(同步消息),而不会结束正在进行的流水线,也不会刷新发送缓冲区。在管道模式下,语句被发送到服务器而不必等待之前语句的结果。更多细节见 Section 32.5。管道模式要求使用扩展查询协议。
Table 299 中列出的算术、位运算、比较和逻辑运算符都内建于 pgbench,可以用在 \set 中出现的表达式里。这些运算符按优先级升序列出。除非另有说明,接受两个数字输入的运算符在任一输入为双精度时会产生双精度值,否则会产生整数结果。
Table 299. pgbench 运算符
运算符 描述 示例 |
|---|
逻辑或
|
逻辑与
|
逻辑非
|
布尔值测试
|
空值测试
|
等于
|
不等于 |
不等于 |
小于
|
小于等于 |
大于
|
大于等于 |
按位或
|
按位异或
|
按位与
|
按位非
|
按位左移
|
按位右移
|
加法
|
减法
|
乘法
|
除法(如果两个输入都是整数,则结果向零截断) |
取模(余数)
|
取负
|
Table 300 中列出的函数都内建于 pgbench,可以用在 \set 中出现的表达式里。
Table 300. pgbench 函数
函数 描述 示例 |
|---|
绝对值 |
把参数打印到 stderr,并返回该参数。 |
转换为 double。
|
指数( |
选择参数中的最大值。 |
这是 |
计算 FNV-1a hash。
|
计算 MurmurHash2 hash。
|
转换为 integer。
|
选择参数中的最小值。 |
自然对数
|
取模(余数)
|
|
π 的近似值
|
|
计算一个均匀分布的随机整数,范围为 |
计算一个指数分布的随机整数,范围为 |
计算一个高斯分布的随机整数,范围为 |
计算一个 Zipf 分布的随机整数,范围为 |
平方根
|
random 函数使用均匀分布生成值,也就是说所有值都以相等的概率在指定范围内抽取。random_exponential、random_gaussian 和 random_zipfian 函数需要一个额外的双精度参数,由它决定分布的精确形状。
对于指数分布,parameter 通过把一个快速递减的指数分布在 parameter 处截断,然后投影到边界之间的整数上,来控制该分布。准确地说,采用
f(x) = exp(-parameter * (x - min) / (max - min + 1)) / (1 - exp(-parameter))
时,值 i(介于 min 和 max 之间,含边界)被抽取的概率为:f(i) - f(i + 1)。
直观地看,parameter 越大,接近 min 的值被访问得越频繁,而接近 max 的值被访问得越少。parameter 越接近 0,访问分布就越平坦(越均匀)。对该分布的一个粗略近似是:范围内最常被访问的 1% 的值(接近 min)会有 parameter% 的时间被抽取到。parameter 的值必须严格为正。
对于高斯分布,该区间被映射到一个标准正态分布(经典的高斯钟形曲线)上,并在左侧 -parameter 处和右侧 +parameter 处截断。区间中部的值更有可能被抽取。准确地说,如果 PHI(x) 是标准正态分布的累积分布函数,均值 mu 定义为 (max + min) / 2.0,采用
f(x) = PHI(2.0 * parameter * (x - mu) / (max - min + 1)) /
(2.0 * PHI(parameter) - 1)
时,值 i(介于 min 和 max 之间,含边界)被抽取的概率为:f(i + 0.5) - f(i - 0.5)。直观地说,parameter 越大,接近区间中部的值被抽取得越频繁,而接近 min 和 max 边界的值被抽取得越少。大约 67% 的值取自中间的 1.0 / parameter,也就是均值附近相对 0.5 / parameter 的范围;95% 的值取自中间的 2.0 / parameter,也就是均值附近相对 1.0 / parameter 的范围。例如,如果 parameter 为 4.0,那么 67% 的值取自区间中间的四分之一(1.0 / 4.0)(即从 3.0 / 8.0 到 5.0 / 8.0),95% 取自区间中间的一半(2.0 / 4.0)(第二和第三个四分位数)。允许的最小 parameter 值为 2.0。
random_zipfian 生成一个有界的 Zipf 分布。parameter 定义了分布的偏斜程度。parameter 越大,越接近区间起点的值被抽取得越频繁。该分布满足:假设范围从 1 开始,抽取 k 与抽取 k+1 的概率之比为 ((。例如,k+1)/k)**parameterrandom_zipfian(1, ..., 2.5) 产生值 1 的频率大约 (2/1)**2.5 = 5.66 倍于产生值 2 的频率,而后者被产生的频率又大约 (3/2)**2.5 = 2.76 倍于值 3,依此类推。
pgbench 的实现基于 “Non-Uniform Random Variate Generation”(Luc Devroye 著,Springer 1986 年,第 550-551 页)。由于该算法的局限性,parameter 的值被限制在 [1.001, 1000] 的范围内。
在设计一个非均匀选择行的基准测试时,要注意所选的行可能与其他数据(例如来自序列的 ID 或物理行顺序)相关联,这可能会使性能度量产生偏差。
为避免这种情况,你可以使用 permute 函数,或者使用某种具有类似效果的附加步骤,来打乱所选的行并消除这类关联。
哈希函数 hash、hash_murmur2 和 hash_fnv1a 接受一个输入值和一个可选的种子参数。如果没有提供种子,就使用 :default_seed 的值;除非通过命令行 -D 选项设置,否则该值会被随机初始化。
permute 接受一个输入值、一个大小和一个可选的种子参数。它生成 [0, size) 范围内整数的一个伪随机排列,并返回输入值在排列结果中的索引。如果未指定种子,所选的排列以 :default_seed 为默认的参数化种子。与哈希函数不同,permute 保证输出值中既没有冲突也没有空洞。区间之外的输入值按对大小取模来解释。如果大小不为正,该函数会引发错误。permute 可以用来分散 random_zipfian 或 random_exponential 等非均匀随机函数的分布,使被更频繁抽取的值之间不会产生直接的关联。例如,下面的 pgbench 脚本模拟了社交媒体和博客平台上一种典型的真实负载场景,其中少数账户产生了过高的负载:
\set size 1000000 \set r random_zipfian(1, :size, 1.07) \set k 1 + permute(:r, :size)
在某些情况下,需要多个彼此不相关的不同分布,这时可选的种子参数就派上用场了:
\set k1 1 + permute(:r, :size, :default_seed + 123) \set k2 1 + permute(:r, :size, :default_seed + 321)
使用 hash 也可以近似实现类似的行为:
\set size 1000000 \set r random_zipfian(1, 100 * :size, 1.07) \set k 1 + abs(hash(:r)) % :size
不过,由于 hash 会产生冲突,有些值将无法取到,而另一些值出现的频率则会高于原始分布的预期。
作为一个例子,内建的类 TPC-B 事务的完整定义如下:
\set aid random(1, 100000 * :scale) \set bid random(1, 1 * :scale) \set tid random(1, 10 * :scale) \set delta random(-5000, 5000) BEGIN; UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; SELECT abalance FROM pgbench_accounts WHERE aid = :aid; UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); END;
该脚本使得事务的每次迭代都能引用不同的、随机选择的行。(这个例子也说明了为什么让每个客户端会话拥有自己的变量非常重要 —— 否则它们就无法独立地访问不同的行。)
使用 -l 选项(但没有 --aggregate-interval 选项)时,pgbench 会把每个事务的信息写入一个日志文件。日志文件将被命名为 ,其中 prefix.nnnprefix 默认为 pgbench_log,而 nnn 是 pgbench 进程的 PID。可以使用 --log-prefix 选项来改变该前缀。如果 -j 选项为 2 或更大,从而存在多个工作线程,那么每个线程都有自己的日志文件。第一个工作线程将使用与标准单工作线程情况下相同的日志文件名。其他工作线程的附加日志文件将被命名为 ,其中 prefix.nnn.mmmmmm 是每个线程从 1 开始的顺序编号。
日志文件中的每一行描述一个事务。它包含以下以空格分隔的字段:
client_id标识运行该事务的客户端会话
transaction_no统计该会话已经运行了多少个事务
time事务的耗时,以微秒为单位
script_no标识该事务使用的脚本文件(在用 -f 或 -b 指定多个脚本时很有用)
time_epoch事务的完成时间,以 Unix 纪元时间戳表示
time_us事务完成时间的小数秒部分,以微秒为单位
schedule_lag事务开始延迟,即事务的计划开始时间与实际开始时间之间的差值,以微秒为单位(仅在指定了 --rate 时才存在)
retries该事务期间在序列化错误或死锁错误之后的重试次数(仅在 --max-tries 不等于 1 时才存在)
当同时使用 --rate 和 --latency-limit 时,被跳过的事务其 time 将被报告为 skipped。如果事务以失败告终,其 time 将被报告为 failed。如果使用了 --failures-detailed 选项,失败事务的 time 将根据失败类型被报告为 serialization 或 deadlock(更多信息见 Failures and Serialization/Deadlock Retries)。
下面是单客户端运行所生成的日志文件的一个片段:
0 199 2241 0 1175850568 995598 0 200 2465 0 1175850568 998079 0 201 2513 0 1175850569 608 0 202 2038 0 1175850569 2663
另一个例子使用 --rate=100 和 --latency-limit=5(注意附加的 schedule_lag 列):
0 81 4621 0 1412881037 912698 3005 0 82 6173 0 1412881037 914578 4304 0 83 skipped 0 1412881037 914578 5217 0 83 skipped 0 1412881037 914578 5099 0 83 4722 0 1412881037 916203 3108 0 84 4142 0 1412881037 918023 2333 0 85 2465 0 1412881037 919759 740
在这个例子中,事务 82 迟到了,因为它的延迟(6.173 ms)超过了 5 ms 的限制。接下来的两个事务被跳过了,因为它们在还没开始之前就已经迟到了。
下面的例子展示了一个带失败和重试的日志文件片段,其中最大尝试次数被设置为 10(注意附加的 retries 列):
3 0 47423 0 1499414498 34501 3 3 1 8333 0 1499414498 42848 0 3 2 8358 0 1499414498 51219 0 4 0 72345 0 1499414498 59433 6 1 3 41718 0 1499414498 67879 4 1 4 8416 0 1499414498 76311 0 3 3 33235 0 1499414498 84469 3 0 0 failed 0 1499414498 84905 9 2 0 failed 0 1499414498 86248 9 3 4 8307 0 1499414498 92788 0
如果使用了 --failures-detailed 选项,失败类型将在 time 中像这样报告:
3 0 47423 0 1499414498 34501 3 3 1 8333 0 1499414498 42848 0 3 2 8358 0 1499414498 51219 0 4 0 72345 0 1499414498 59433 6 1 3 41718 0 1499414498 67879 4 1 4 8416 0 1499414498 76311 0 3 3 33235 0 1499414498 84469 3 0 0 serialization 0 1499414498 84905 9 2 0 serialization 0 1499414498 86248 9 3 4 8307 0 1499414498 92788 0
在能够处理大量事务的硬件上运行长时间测试时,日志文件可能会变得非常大。可以使用 --sampling-rate 选项只记录事务的一个随机样本。
使用 --aggregate-interval 选项时,日志文件将采用另一种格式。每一行日志描述一个聚合区间。它包含以下以空格分隔的字段:
interval_start该区间的开始时间,以 Unix epoch 时间戳表示
num_transactions该区间内的事务数
sum_latency事务延迟之和
sum_latency_2事务延迟的平方之和
min_latency最小的事务延迟
max_latency最大的事务延迟
sum_lag事务开始延迟之和(除非指定了 --rate,否则为零)
sum_lag_2事务开始延迟的平方之和(除非指定了 --rate,否则为零)
min_lag最小的事务开始延迟(除非指定了 --rate,否则为零)
max_lag最大的事务开始延迟(除非指定了 --rate,否则为零)
skipped因开始时间过晚而被跳过的事务数(除非同时指定了 --rate 和 --latency-limit,否则为零)
retried被重试的事务数(除非 --max-tries 不等于 1,否则为零)
retries序列化错误或死锁错误后的重试次数(除非 --max-tries 不等于 1,否则为零)
serialization_failures遇到序列化错误且之后未被重试的事务数(除非指定了 --failures-detailed,否则为零)
deadlock_failures遇到死锁错误且之后未被重试的事务数(除非指定了 --failures-detailed,否则为零)
下面是使用该选项生成的一些示例输出:
pgbench --aggregate-interval=10 --time=20 --client=10 --log --rate=1000 --latency-limit=10 --failures-detailed --max-tries=10 test
1650260552 5178 26171317 177284491527 1136 44462 2647617 7321113867 0 9866 64 7564 28340 4148 0
1650260562 4808 25573984 220121792172 1171 62083 3037380 9666800914 0 9998 598 7392 26621 4527 0
注意,普通(未聚合)日志格式会显示每个事务使用了哪个脚本,而聚合格式则不会。因此,如果你需要按脚本划分的数据,就需要自己对数据进行聚合。
使用 -r 选项时,pgbench 会为每条语句收集以下统计信息:
latency — 每条语句耗费的事务时间。pgbench 报告该语句所有成功运行的平均值。
该语句的失败次数。更多信息见 Failures and Serialization/Deadlock Retries。
该语句中在序列化错误或死锁错误后的重试次数。更多信息见 Failures and Serialization/Deadlock Retries。
仅当 --max-tries 选项不等于 1 时,报告才显示重试统计信息。
所有值都是针对每个客户端执行的每条语句计算的,并在基准测试结束后报告。
对于默认脚本,输出将类似于这样:
starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 10 number of threads: 1 maximum number of tries: 1 number of transactions per client: 1000 number of transactions actually processed: 10000/10000 number of failed transactions: 0 (0.000%) number of transactions above the 50.0 ms latency limit: 1311/10000 (13.110 %) latency average = 28.488 ms latency stddev = 21.009 ms initial connection time = 69.068 ms tps = 346.224794 (without initial connection time) statement latencies in milliseconds and failures: 0.012 0 \set aid random(1, 100000 * :scale) 0.002 0 \set bid random(1, 1 * :scale) 0.002 0 \set tid random(1, 10 * :scale) 0.002 0 \set delta random(-5000, 5000) 0.319 0 BEGIN; 0.834 0 UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; 0.641 0 SELECT abalance FROM pgbench_accounts WHERE aid = :aid; 11.126 0 UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; 12.961 0 UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; 0.634 0 INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); 1.957 0 END;
下面是默认脚本在默认事务隔离级别为可序列化(PGOPTIONS='-c default_transaction_isolation=serializable' pgbench ...)时的另一个输出示例:
starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 10 number of threads: 1 maximum number of tries: 10 number of transactions per client: 1000 number of transactions actually processed: 6317/10000 number of failed transactions: 3683 (36.830%) number of transactions retried: 7667 (76.670%) total number of retries: 45339 number of transactions above the 50.0 ms latency limit: 106/6317 (1.678 %) latency average = 17.016 ms latency stddev = 13.283 ms initial connection time = 45.017 ms tps = 186.792667 (without initial connection time) statement latencies in milliseconds, failures and retries: 0.006 0 0 \set aid random(1, 100000 * :scale) 0.001 0 0 \set bid random(1, 1 * :scale) 0.001 0 0 \set tid random(1, 10 * :scale) 0.001 0 0 \set delta random(-5000, 5000) 0.385 0 0 BEGIN; 0.773 0 1 UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; 0.624 0 0 SELECT abalance FROM pgbench_accounts WHERE aid = :aid; 1.098 320 3762 UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; 0.582 3363 41576 UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; 0.465 0 0 INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); 1.933 0 0 END;
如果指定了多个脚本文件,则会为每个脚本文件单独报告所有统计信息。
注意,收集每语句延迟计算所需的额外计时信息会增加一些开销。这会降低平均执行速度并使计算出的 TPS 变低。速度下降的幅度因平台和硬件的不同而有显著差异。比较启用和未启用延迟报告时的平均 TPS 值,是衡量计时开销是否显著的好方法。
在运行 pgbench 时,主要有三种类型的错误:
主程序的错误。这类错误最为严重,总是会导致 pgbench 立即退出并给出相应的错误消息。这类错误包括:
pgbench 开始阶段的错误(例如无效的选项值);
初始化模式中的错误(例如为内建脚本创建表的查询失败);
启动线程之前的错误(例如无法连接到数据库服务器、元命令中的语法错误、线程创建失败);
pgbench 内部错误(按理说永远不应该发生……)。
线程在管理其客户端时发生的错误(例如客户端无法启动与数据库服务器的连接,或者用于将客户端连接到数据库服务器的套接字已失效)。在这种情况下,该线程的所有客户端都会停止,而其他线程继续工作。但是,如果指定了 --exit-on-abort,在这种情况下所有线程都会立即停止。
直接的客户端错误。这些错误会导致立即从 pgbench 退出并给出相应的错误消息,出现这种情况的情形包括:发生了 pgbench 内部错误(按理说永远不应该发生……),或者指定了 --exit-on-abort。否则,在最坏的情况下,它们只会导致失败的客户端被中止,而其他客户端继续运行(但有些客户端错误会在不中止客户端的情况下得到处理并被单独报告,见下文)。在本节的后文中,假定所讨论的错误只是直接的客户端错误,而不是 pgbench 内部错误。
客户端的运行在发生严重错误时会被中止;例如,与数据库服务器的连接丢失了,或者在没有完成最后一个事务的情况下到达了脚本末尾。此外,如果 SQL 命令或元命令的执行因序列化错误或死锁错误之外的原因而失败,客户端也会被中止。否则,如果 SQL 命令因序列化错误或死锁错误而失败,客户端不会被中止。在这种情况下,当前事务会被回滚,这也包括把客户端变量恢复为该事务运行之前的值(假定一个事务脚本只包含一个事务;更多信息见 What Is the "Transaction" Actually Performed in pgbench?)。带有序列化错误或死锁错误的事务会在回滚后被重复执行,直到它们成功完成,或者达到最大尝试次数(由 --max-tries 选项指定)/ 最大重试时间(由 --latency-limit 选项指定)/ 基准测试结束(由 --time 选项指定)。如果最后一次尝试运行失败,该事务将被报告为失败,但客户端不会被中止并继续工作。
在不指定 --max-tries 选项的情况下,事务在发生序列化错误或死锁错误后永远不会被重试,因为其默认值为 1。要把限制只放在尝试的最大时间上,可以把无限次尝试(--max-tries=0)与 --latency-limit 选项结合使用。在无限次尝试的情况下,还可以使用 --time 选项来限制基准测试的持续时间。
重复执行包含多个事务的脚本时要小心:脚本总是会被完整地重试,因此成功的事务可能会被执行多次。
重复执行带 shell 命令的事务时要小心。与 SQL 命令的结果不同,shell 命令的结果不会被回滚,但 \setshell 命令所设置的变量值除外。
成功事务的延迟包括了事务执行的完整时间,其中包含回滚和重试。延迟只针对成功的事务和命令进行度量,而不针对失败的事务或命令。
主报告包含失败事务的数量。如果 --max-tries 选项不等于 1,主报告还包含与重试相关的统计信息:被重试事务的总数以及重试的总次数。每脚本报告从主报告继承所有这些字段。每语句报告仅在 --max-tries 选项不等于 1 时才显示重试统计信息。
如果你想在每事务日志和聚合日志以及主报告和每脚本报告中按基本类型对失败进行分组,请使用 --failures-detailed 选项。如果你还想按类型区分所有错误和失败(不重试的错误),包括对于序列化/死锁失败超出了哪个重试限制以及被超出了多少,请使用 --verbose-errors 选项。
可以为 pgbench 表指定 Table Access Method(表访问方法)。环境变量 PGOPTIONS 指定那些通过命令行传递给 PostgreSQL 的数据库配置选项(见 Section 19.1.4)。例如,假设 pgbench 所创建的表有一个名为 wuzza 的默认表访问方法,则可以这样指定它:
PGOPTIONS='-c default_table_access_method=wuzza'
使用 pgbench 很容易产生完全无意义的数字。下面是一些可以帮助你获得有用结果的指导原则。
首先,千万不要相信任何只运行几秒钟的测试。使用 -t 或 -T 选项让运行至少持续几分钟,以便平均掉噪声。在某些情况下,可能需要几个小时才能得到可重现的数字。最好把测试运行多试几次,看看你的数字是否可以重现。
对于默认的类 TPC-B 测试场景,初始化比例因子(-s)至少应当与你打算测试的最大客户端数量(-c)一样大;否则你度量的将主要是更新争用。由于在只有 -s 行的 pgbench_branches 表中,每个事务都想要更新其中一行,因此 -c 值超过 -s 时无疑会导致大量事务被阻塞、等待其他事务。
默认的测试场景对表初始化后经过的时间也相当敏感:表中死元组和死空间的累积会改变结果。要理解结果,你必须跟踪更新的总数以及清理发生的时间。如果启用了自动清理,它可能导致度量到的性能发生不可预测的变化。
pgbench 的一个局限是,在试图测试大量客户端会话时,它本身可能成为瓶颈。可以通过在与数据库服务器不同的机器上运行 pgbench 来缓解这一问题,不过低网络延迟将是必不可少的。在多台客户端机器上并发运行多个 pgbench 实例来测试同一台数据库服务器,甚至也可能很有用。
如果不受信任的用户可以访问一个尚未采用 secure schema usage pattern(安全的 schema 使用模式)的数据库,请不要在该数据库中运行 pgbench。pgbench 使用非限定名称,并且不会操纵搜索路径。