我在使用 Postgres 10 数据库时遇到了一个奇怪的性能问题。
我有这样一张桌子:
CREATE TABLE articles
(
article_id bigint NOT NULL,
content jsonb NOT NULL,
-- a few other fields...
CONSTRAINT articles_pkey PRIMARY KEY (article_id)
)
我们有一个 Python 库(使用 psycopg2 驱动程序)来访问这个表。它对表执行简单的查询,其中一个用于批量下载内容,它看起来像这样:
SELECT content FROM articles WHERE id BETWEEN $1 AND $2
我注意到,对于某些 ID,它的运行速度非常快,而对于其他 ID,它的运行速度要慢 4-5 倍。需要注意的是,该表中有两个主要的ID“类别”:第一个是2.7亿-5亿范围,第二个是100亿-1000.3亿范围。第一个范围是“快”,第二个范围是“慢”。除了更大的绝对数将它们从int
to中提取出来之外bigint
,第二个范围内的值更“稀疏”,这意味着在第一个范围内的值几乎是随之而来的(缺失值的“洞”很少),而在第二个范围内有更多“洞”(平均填充率为 30%)。
为了消除其他库的可能影响,我使用以下查询进行了综合测试:
SELECT count(*), sum(length(content::text))
FROM storage.articles
WHERE article_id BETWEEN %s AND %s
我调整了请求的夹头的长度,以平均每个请求的 JSONB 的摘要长度。
此外,我计算返回的处理速率count(*)
除以执行时间。第一个范围是 800-1050 行/秒,第二个范围是 120-250 行/秒。另一个指标是每次处理 JSON 的长度。由于查询中的主要困难似乎是处理 JSONB 数据,我假设在这两种情况下它必须平均相同。但不是:它是 7-10 MB/s 与 1.5-2.5 MB/s。
于是问题重现。虽然它在绝对值上变得更快(由于没有网络传输),但性能上的显着差异仍然存在。我比较了两个范围内查询的执行计划,它们看起来等效(EXPLAIN ANALYZE
):
Aggregate (cost=8046.71..8046.72 rows=1 width=8) (actual time=2924.410..2924.411 rows=1 loops=1)
-> Index Scan using articles_pkey on articles (cost=0.57..8000.02 rows=4669 width=107) (actual time=11.482..61.258 rows=5000 loops=1)
Index Cond: ((article_id >= 461995000) AND (article_id <= 461999999))
Planning time: 1.028 ms
Execution time: 2924.650 ms
Aggregate (cost=6216.75..6216.76 rows=1 width=8) (actual time=17704.635..17704.636 rows=1 loops=1)
-> Index Scan using articles_pkey on articles (cost=0.57..6180.69 rows=3606 width=107) (actual time=0.154..563.578 rows=3059 loops=1)
Index Cond: ((article_id >= '100017010000'::bigint) AND (article_id <= '100017019999'::bigint))
Planning time: 0.404 ms
Execution time: 17704.746 ms
我看到的唯一区别是转换bigint
应该是一次性的。
我错过了什么吗?如何调查(和修复)它?
更新 1:添加EXPLAIN
:BUFFERS
Aggregate (cost=8635.91..8635.92 rows=1 width=16) (actual time=6625.993..6625.995 rows=1 loops=1)
Buffers: shared hit=26847 read=3914
-> Index Scan using articles_pkey on articles (cost=0.57..8573.35 rows=5005 width=107) (actual time=21.649..1128.004 rows=5000 loops=1)
Index Cond: ((article_id >= 438000000) AND (article_id <= 438005000))
Buffers: shared hit=4342 read=671
Planning time: 0.393 ms
Execution time: 6626.136 ms
Aggregate (cost=5533.02..5533.03 rows=1 width=16) (actual time=33219.100..33219.102 rows=1 loops=1)
Buffers: shared hit=6568 read=7104
-> Index Scan using articles_pkey on articles (cost=0.57..5492.96 rows=3205 width=107) (actual time=22.167..12082.624 rows=2416 loops=1)
Index Cond: ((article_id >= '100021000000'::bigint) AND (article_id <= '100021010000'::bigint))
Buffers: shared hit=50 read=2378
Planning time: 0.517 ms
Execution time: 33219.218 ms
更新 2:更多实验:
我对表中的所有数据进行了更多测试以利用上述方法。使用一个简单的程序,我迭代段的边界以最终处理两个范围内的所有数据。在此期间,我解析 EXPLAIN ANALYZE 的结果并收集以下一系列指标:
- 缓冲区命中/读取表。
- 缓冲区命中/读取索引。
- 行数(从
Index Scan...
) - 执行期限
根据上面的指标,我计算继承的指标:
- 磁盘读取率:(索引读取 + 表读取)* 8192 / 持续时间。
- 读取比率:(索引读取 + 表读取)/(索引读取 + 表读取 + 索引命中 + 表命中)
- 数据率:(索引读取 + 表读取 + 索引命中 + 表命中)* 8192 / 持续时间
由于 ID 的“密度”在“小”和“大”范围内是不同的,因此我调整了块的大小,以便在这两种情况下每次迭代都获得大约 5000 行,尽管我的实验表明块大小并不重要.
上面发布的结果在整个第一个和第二个范围内都得到了确认(因此它不仅仅是由随机缓存的数据引起的)。
为了消除缓存影响,我用刷新缓冲区重新启动了 Postgres 服务器:
postgresql stop; sync; echo 3 > /proc/sys/vm/drop_caches; postgresql start
在此之后,我重复了测试并得到了一张几乎相同的照片。
我将上次测试中的系列渲染成图表并在此处发布:
这是“小”范围的图片。在整个测试中,磁盘读取速率统一在 10-11 MB/s 左右。
问题:为什么虽然我刷新了内存缓冲区,但命中率很高(读取率很低)?
这是“大”范围的图片。在我看来这很疯狂。在大多数情况下,磁盘读取速度约为 2 MB/s,但有时会跃升至 26-30 MB/s。读取率也变化很大。
在测试期间,我验证了磁盘读取速度,iotop
发现它的指示非常接近我计算的指示。我不能说我一直在监视它,但是当它在第二个范围为 2 MB/s 和 22 MB/s 以及在第一个范围为 10 MB/s 时,我确认了它。我还检查了htop
CPU 不是瓶颈,在测试期间约为 3%。
该问题在主服务器和从服务器上都可以重现。我的测试是在从属设备上进行的,而 DBMS 上没有任何其他负载,或者与 DBMS 无关的主机上的磁盘活动。
我唯一的假设是,由于虚拟化或其他原因,不同的数据片段以不同的速度被读取,但是......为什么它如此严格地限制在这些范围内?为什么在两台不同的机器上是一样的?
有任何想法吗?