PostgreSQL at Low Level
总结一下这篇文章 PostgreSQL at low level: stay curious!
Introduction
我们之前使用数据库的时候,生产环境都只在实体机上面使用,测试和开发为了资源复用会在虚拟机 vm 上面使用。
但是现在不少在 vm k8s 或者 aws 上面使用 db 数据库的,实际这里面可能有很多潜在的问题。以前是 pg - OS 这样两层结构,现在是 pg - os - cg - vm - k8s 这样多层结构,这里面任何一层出现问题实际都会导致你的查询变慢。我们以前虚拟机上面跑服务的时候,有时候就会被同物理机其他虚拟机上面的服务影响,例如突然的高 io。这样即使怎么看那个执行计划估计也没用,你必须去研究更底层可能的影响。
Shared memory
docker 只给 /dev/shm 64MB 大小,所以是会遇到共享内存不足的问题啦。可以通过 strace 定位
# strace -k -p PID
openat(AT_FDCWD, "/dev/shm/PostgreSQL.62223175"
ftruncate(176, 50438144) = 0
fallocate(176, 0, 0, 50438144) = -1 ENOSPC
> libc-2.27.so(posix_fallocate+0x16) [0x114f76]
> postgres(dsm_create+0x67) [0x377067]
...
> postgres(ExecInitParallelPlan+0x360) [0x254a80]
> postgres(ExecGather+0x495) [0x269115]
> postgres(standard_ExecutorRun+0xfd) [0x25099d]
...
> postgres(exec_simple_query+0x19f) [0x39afdf]
vDSO
内核支持叫 vDSO (virtual dynamic shared object) 的技术,允许进程直接调用 kernel 方法而不用做用户态和内核态切换,避免性能损失。有的这些调用对数据库来说还挺重要,例如 gettimeofday。但是有了 vm 就不好说啦。可以通过 strace 看看。
# strace -k -p PID on XEN
gettimeofday({tv_sec=1550586520, tv_usec=313499}, NULL) = 0
CPU migrations
作者做了两个实验。实验一,执行比较重的操作,给一堆数据排序。实验二,使用 pgbench 的 –file=filename[@weight] 参数指定相同的 sql。实验二第二个脚本是执行一些比较简单的查询。
# Experiment 1
SQL script: pg_long.sql
- latency average = 1312.903 ms
# Experiment 2
SQL script 1: pg_long.sql
- weight: 1 (targets 50.0% of total)
- latency average = 1426.928 ms
SQL script 2: pg_short.sql
- weight: 1 (targets 50.0% of total)
- latency average = 303.092 ms
为啥用比较轻量的查询替换之后(这里有点不明白,没看到前面说有做这个替换,我以为是同一个 sql)结果反而更坏?这个瓶颈看着和系统调用应该没啥关系,所以 strace 没用。可以使用 perf 看看硬件性能。
# perf record -e cache-misses,cpu-migrations
# Experiment 1
12,396,382,649 cache-misses # 28.562%
%2,750 cpu-migrations
# Experiment 2
20,665,817,234 cache-misses # 28.533%
10,460 cpu-migrations
看着和 cache 没关系,和 cpu-migrations 有关系,第二个是第一个的 3 倍。
MDS
MDS (Microarchitectural Data Sampling) 是一种硬件缺陷的攻击,类似基于 intel cpu 的 Meltdown 和 Spectre。针对这些问题内核都有一些方法减轻威胁。怎么评估这个对数据库的影响呢?看下面的 profile
# Children Self Symbol
# ........ ........ ...................................
71.06% 0.00% [.] __libc_start_main
71.06% 0.00% [.] PostmasterMain
56.82% 0.14% [.] exec_simple_query
25.19% 0.06% [k] entry_SYSCALL_64_after_hwframe
25.14% 0.29% [k] do_syscall_64
23.60% 0.14% [.] standard_ExecutorRun
另外做一个内核没有针对这些问题做修复的,可以看到那个那个 do_syscall_64 是多出来的。
# Percent Disassembly of kcore for cycles
# ........ ................................
0.01% : nopl 0x0(%rax,%rax,1)
28.94% : verw 0xffe9e1(%rip)
0.55% : pop %rbx
3.24% : pop %rbp
MDS 的修复会隐含重载 verw 来刷新 CPU 缓存,通过 mds_clear_cpu_buffers()。
Lock holder/waiter preemption
假设有一个 pg 运行在一个有两个 vCPU(vC1,vC2) 的 vm 里面。
这种情况下某个时间点,运行在 vC2 的后端等待一个运行在 vC1 上面的后端的 spin lock。通常这没啥问题,但是如果 hypervisor 突然决定 preempt vC1(看着是抢占的意思?) 会发生什么情况?
这样本来 vC2 上面的后端以为是个小等待的,但是现在不知道需要等多久了。幸运的是 intel 有一种技术 PAUSE-Loop Exiting 允许通过发送 VM exit 来阻止无意义的 spinning。同时不幸的是,这会因为 VM 和 hypervisor 间切换带来一些多余的负担,如果这个暂停不正确的触发,那啥收获也没有。
怎么衡量呢。通过 perf 可以看看。不同 vm 还不一样,kvm 是看 kvm:kvm_exit 事件。
# experiment 1: pgbench, read write
# latency average = 17.782 ms
$ modprobe kvm-intel ple_gap=128
$ perf record -e kvm:kvm_exit
# reason PAUSE_INSTRUCTION 306795
# experiment 2: pgbench, read write
# latency average = 16.858 ms
$ modprobe kvm-intel ple_gap=0
$ perf record -e kvm:kvm_exit
# reason PAUSE_INSTRUCTION 0
第一个配置里面,使用 PLE 默认的配置,可以看到一堆的暂停。第二个完全禁止了 PLE,可以看到 0 个暂停。然后还能看到后面这个 latency 还低呢,这极可能是因为我们的 CPU 们被过度使用了呢,PLE 错误的识别了那些等待。
Huge pages
首先不要混淆 classical huge pages 和 transparent huge pages。后者是个守护进程,用来在后台把普通的内存合并成 huge pages,一般情况下建议关闭他,因为可能会带来不可预料的消耗。
看看文档怎么说的
使用大页可以极大的减少 TLB 的压力,提升 TLB 命中率,从而提升整个系统的性能。
怎么影响到数据库的呢,用 perf 看看。
# Experiment 1, pgbench read/write, huge_pages off
# perf record -e dTLB-loads,dTLB-stores -p PID
Samples: 894K of event 'dTLB-load-misses'
Event count (approx.): 784439650
Samples: 822K of event 'dTLB-store-misses'
Event count (approx.): 101471557
# Experiment 2, pgbench read/write, huge_pages on
# perf record -e dTLB-loads,dTLB-stores -p PID
Samples: 832K of event 'dTLB-load-misses'
Event count (approx.): 640614445
Samples: 736K of event 'dTLB-store-misses'
Event count (approx.): 72447300
两个实验都是使用的 pgbench 的 TPC-B 方法。第一个关闭了 huge pages,第二个通过 huge_pages=on 打开了支持。第二个减少了 20% 的 TLD-load-misses。这里没有关注 latencies,只关注了这一个事情,不是整个系统,因为其他系统组件可能会带来噪音。
BPF
没有接触过 BPF 和 BCC ,不能很好的翻译。这里有篇文章讲这个。
LLC
似乎是 last level cache 。似乎可以获取到 cache miss per query。
# llcache_per_query.py bin/postgres
PID QUERY CPU REFERENCE MISS HIT%
9720 UPDATE pgbench_tellers ... 0 2000 1000 50.00%
9720 SELECT abalance FROM ... 2 2000 100 95.00%
...
Total References: 3303100 Total Misses: 599100 Hit Rate: 81.86%
那个 llcache_per_query.py 在这里有。
Writeback
pg 使用的是 buffered IO。可以使用 ftrace 来监控。需要先 mount tracefs 通常在 /sys/kernel/debug/tracing
# cd /sys/kernel/debug/tracing
# echo 1 > events/writeback/writeback_written/enable
# tail trace
kworker/u8:1 reason=periodic nr_pages=101429
kworker/u8:1 reason=background nr_pages=MAX_ULONG
kworker/u8:3 reason=periodic nr_pages=101457
上面这个是个输出的简短的版本,MAX_ULONG 是 maximum unsigned long 的意思。
可以看到内核在后台 writeback ,试图把尽量多的文件系统缓存写入。
# pgbench insert workload
# io_timeouts.py bin/postgres
[18335] END: MAX_SCHEDULE_TIMEOUT
[18333] END: MAX_SCHEDULE_TIMEOUT
[18331] END: MAX_SCHEDULE_TIMEOUT
[18318] truncate pgbench_history: MAX_SCHEDULE_TIMEOUT
可以通过 dirty_background_bytes 控制。也可以通过 bgwriter_flush_after / checkpointer_flush_after 控制 bgwriter 和 checkpointer。
Memory reclaim
如果使用过 kubernetes 的话,可能会看到下面的配置
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"