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"