数据库参数优化

Mikage Yuziki 发布于 29 天前 82 次阅读


1 操作系统参数

1.1 磁盘预读参数

在内存中读取数据比从磁盘中读取数据要快很多,增加linux内核预读,对于大量顺序读取的操作,可以有效减少I/O的等待时间。

kernel 在顺序 I/O 时,会提前把当前块之后的 N 个扇区读进页缓存,减少后续 read() 系统调用真正落盘。

仅对裸设备、直接 I/O 未绕过页缓存的场景生效;PostgreSQL 默认使用 buffered I/O,因此会被预读策略影响。

查看预读参数

注:传统硬盘扇区的大小是512字节。但在高级格式化硬盘中,物理扇区大小是4096字节。

# 查看预读多少个扇区
blockdev --getra /dev/sda
# 查看预读大小
cat /sys/class/block/sda/queue/read_ahead_kb
# 查看所有磁盘的预读设置
blockdev -report

设置预读参数

# 动态设置预读多少个扇区
blockdev --setra  4096  /dev/sda   # 2 MB
blockdev --setra 16384  /dev/sda   # 8 MB
# 查看预读多少个扇区
blockdev --getra /dev/sda

1.2 磁盘调度算法

磁盘调度算法决定了 I/O 请求(读/写)在机械硬盘上的排队、合并、寻道顺序,直接影响数据库的吞吐量、延迟、稳定性。

算法核心思想数据库典型表现
noop简单 FIFO,只合并相邻块顺序扫描快;高并发随机写 → 长尾延迟高
deadline读优先 + 写期限,防止饥饿低延迟 OLTP 最优;PostgreSQL 官方推荐
cfq按进程分时间片,公平带宽多用户桌面流畅;DB 吞吐下降 10-30%
bfq比例公平 + 空闲批处理桌面多媒体好;DB 无优势
kyber最新内核默认,自适应接近 deadline,随机写更稳

如何选择

场景推荐算法设置命令
纯 HDD、OLTP 主力deadlineecho deadline > /sys/block/sdX/queue/scheduler
SSD/NVMenoop / kyber默认即可
混合阵列deadline同 HDD
桌面/多媒体bfq系统默认

1.3 操作系统大页

官方与实测经验给出两条硬指标:

  1. shared_buffers ≥ 8 GB
    小于 8 GB 时,页表占用不过几百 MB,开不开 Huge Pages 差别几乎测不到;≥ 8 GB 后,普通 4 kB 页表开始膨胀到 GB 级,Huge Pages 收益明显。
  2. 或 连接数 ≥ 500 且 shared_buffers ≥ 4 GB
    每条后端进程对 shared_buffers 做第一次 touch 会消耗页表项,大并发下页表总和可能 > 物理内存 5 %,此时即使 shared_buffers 只有 4 GB 也建议开。

—— 因此实务口诀:

“shared_buffers 一到 8 GB 就开 Huge Pages;连接破 500,4 GB 就可以开。”

额外注意:

  • 必须 先设置好 vm.nr_hugepages(覆盖 shared_buffers × 110 %)再启动 PG,否则 huge_pages=on 会启动失败。
  • 只算 shared_buffers,不要把 OS cache、work_mem 算进 Huge Pages,那些仍走普通 4 kB 页。

配置方法见文章:PG开启大页

1.4 透明大页

透明大页(Transparent Huge Pages, THP)对 PostgreSQL 几乎只有负面作用,官方文档与大量实测都建议显式关闭,原因如下:


1. 内存膨胀 & OOM 风险

  • THP 默认 2 MB 页,任意 4 kB 只要被连续访问就可能被自动合并成 2 MB。
  • PostgreSQL 的 work_mem、maintenance_work_mem、临时缓冲区、内核页缓存都是分散的小块,合并后单进程可能瞬间吃掉 数百 MB 物理内存,触发系统 OOM-killer 把 PG 后端杀掉。

2. 写放大 & 延迟抖动

  • Copy-on-Write 机制:THP 只要有一个字节修改,整页(2 MB)复制一次。
  • 大批量 UPDATE/VACUUM/CREATE INDEX 会产生 巨量级 2 MB ,导致
    • 磁盘瞬时 I/O 飙升
    • 平均延迟从 5 ms → 50-100 ms 抖动
    • WAL 量间接增加(脏页变多)。

3. 缺页异常放大

  • THP 合并失败时会退回到 khugepaged 内核线程后台重试,占用 CPU;
  • PostgreSQL 的高并发短查询(<10 ms)会被 khugepaged 抢占,TPS 下降 10-30%。

4. 与 Huge Pages 正交冲突

  • 透明大页标准 Huge Pages;后者需要预先分配、固定大小,THP 是运行时自动拆分/合并,两者机制独立。
  • 若已配置 vm.nr_hugepages 给 PG 专用,THP 仍会对其余内存(work_mem、页缓存)做合并,继续带来上述副作用。

一键关闭方法

# 临时
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 永久:grub 内核参数
grubby --update-kernel=ALL --args="transparent_hugepage=never"
reboot

或者配置系统服务,可实现永久关闭透明大页,root用户执行:

vi /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages (THP)

[Service]
Type=simple
ExecStart=/bin/sh -c "echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled && echo 'never' > /sys/kernel/mm/transparent_hugepage/defrag"

[Install]
WantedBy=multi-user.target

加载服务,启动服务并设置开机启动:

systemctl daemon-reload
systemctl start disable-thp
systemctl enable disable-thp

1.5 SHMMAX、SHMALL、SHMMNI

计算内核参数的值:

echo "kernel.sem= `cat /proc/sys/kernel/sem`" && cat /proc/meminfo|grep MemTotal|awk {'print $2'*1024*0.8/$(getconf PAGE_SIZE)}|awk '{printf "kernel.shmall=%d\n",$1}' && cat /proc/meminfo|grep MemTotal| awk '{printf "kernel.shmmax=%d\n",$2*1024*0.8}' && echo "kernel.shmmni =`cat /proc/sys/kernel/shmmni`"

编辑 /etc/sysctl.conf ,添加如下信息。shmall、shmmax、shmmni直接取上一步计算的值。

执行以下命令,使配置生效(注意若部分参数报错,可酌情修改参数配置):

sysctl -p
  • SHMMAX是一个内核参数,用于定义一个Linux进程可以分配的单个共享内存段的最大大小。SHMMAX的值以字节为单位,设置太小会导致实例启动失败。
  • SHMALL是另一个用于定义系统范围的共享内存页总页数的内核参数。
  • SHMMNI定义了系统级别的共享内存段的最大数量,采用默认值4096即可。

1.6 vm.swappiness

vm.swappiness是另一个可能影响数据库性能的内核参数。

此参数用于控制Linux系统上将页面在swap分区和内存间进行交换的行为。

此参数值值的范围为0到100。

0表示禁用交换,最大限度使用物理内存,然后才使用swap分区,100表示积极使用swap分区进行交换。有些数据库建议不使用交换区,以获得更好的性能。

—查看当前设置的值:

# cat /proc/sys/vm/swappiness
30#表示当物理内存使用率达到70%时,才使用swap分区

2 数据库参数

2.1 shared_buffer

PostgreSQL使用自己的缓冲区,也使用内核缓冲IO。

这意味着数据存储在内存中两次,首先是PostgreSQL缓冲区,然后是内核缓冲区。

与其他数据库不同,PostgreSQL不提供直接IO。这称为双缓冲。

PostgreSQL缓冲区称为shared_buffers,它是大多数操作系统最有效的可调参数。

PostgreSQL将用shared_buffers参数缓存如下数据:表数据、索引、执行计划

2.2 wal_buffer

PostgreSQL将其WAL(预写日志)记录写入缓冲区,然后将这些缓冲区刷新到磁盘。

缓冲区的默认大小,由wal_buffers定义,但如果您有大量并发连接,则较高的值可以提供更好的性能。

2.3 effective_cache_size

该effective_cache_size提供了可以用于磁盘缓存存储器的估计。

它只是一个指导原则,而不是确切分配的内存或缓存大小。

它不分配实际内存,而是告诉优化器内核中可用的缓存量。

如果将此值设置得太低,查询计划程序可以决定不使用某些索引,即使它们有用。

因此,设置较大的值总是有益的。

建议使用默认值。

2.4 work_mem

指定在写入磁盘上的临时文件之前,ORDERBY,DISTINCT,JOIN和哈希表的内部操作将使用的内存量。

此配置用于复杂排序,如果必须进行复杂排序,则增加work_mem的值以获得良好结果。内存中的排序比溢出到磁盘的排序快得多。

设置非常高的值可能会导致部署环境出现内存瓶颈,因为此参数是按用户排序操作。

如果您有许多用户尝试执行排序操作,系统将为所有用户分配work_mem*总排序操作。

2.5 maintenance_work_mem

maintenance_work_mem是用于维护任务的内存设置。默认值为64MB。

设置较大的值有助于执行VACUUM,RESTORE,CREATEINDEX,ADD FOREIGN KEY和ALTERTABLE等任务。

由于会话中只能同时执行其中一个操作,并且通常没有多个同时运行,因此它可能比work_mem大。

较大的配置可以提高VACUUM和数据库还原的性能。

2.6 fsync

fsync 是 PostgreSQL 里最底层、也最重要的持久化开关。它控制事务提交时是否把 WAL 缓冲强制刷到磁盘,直接决定“断电后数据丢还是不丢”。

  1. 参数含义

fsync = on | off

  • on(默认)
    每次 commit 都发 fsync()/fdatasync(),等待磁盘返回“已落盘”才向客户端回 OK。
    → 最慢,最安全,生产必须 on
  • off
    只把 WAL 写到内核页缓存就返回成功,由 OS 自己决定什么时候刷盘。
    → 快 10-30%,但宕机=丢数据,只用于压测、可重建的数仓、演示环境

  1. 量化对比(HDD RAID10)
模式pgbench TPS宕机丢数据范围
fsync=on6,8000(已落盘)
fsync=off9,100最近 0-30 秒事务全部丢失

2.7 synchronous_commit

指定在命令向客户端返回“成功”指示之前,事务提交是否将等待WAL记录写入磁盘。这是性能和可靠性之间的权衡。默认设置为“on”。

可能的值包括:“on”,“remote_apply”,“remote_write”,“local”和“off”。

与fsync不同,禁用此参数不会产生任何数据库不一致的风险:操作系统或数据库崩溃可能导致丢失一些最近发生的可能提交的事务,但数据库的状态将与这些事务完全相同,未提交的将被抛弃。

当性能比事务持久性更重要时,停用synchronous_commit可能是一个有用的替代方法。

这意味着成功状态与保证写入磁盘之间会存在时间差。在服务器崩溃的情况下,即使客户端在提交时收到成功消息,数据也可能丢失。在这种情况下,事务提交非常快,因为它不会等待刷新WAL文件,但可靠性受到损害。

2.8 fillfactor

它告诉数据库:“在插入数据时,不要把 8 KB 页面一次性填满,而是只填到 n %,剩下的 (100-n)% 空着,留给将来的 UPDATE 用。”

  • 作用范围:堆表(普通表)、B-tree 索引、GIN 索引(不同索引类型语义略有差异)。
  • 取值:10–100,默认 100(完全填满)。
  • 设置时机:CREATE / ALTER TABLE、CREATE / REINDEX INDEX。
  • 只影响后续插入,已有数据需 VACUUM FULL / REINDEX 才能按新比例重整

PostgreSQL 的 MVCC 机制下,UPDATE = 标记旧元组 + 插入新元组。

  • 如果页面已满,新元组必须放到别的页 → 产生页分裂、索引指针重定向 → 更多 I/O、更多碎片、更慢的 VACUUM。
  • 若提前留好空位,新版本仍落在同一页,可利用 HOT(Heap-Only Tuple)机制,无需更新索引,大幅减低碎片与 I/O

频繁的更新会产生大量死元组,影响查询效率,频繁更新删除的表/索引可以修改填充因子

-- 建表时
CREATE TABLE orders(
    id   bigserial PRIMARY KEY,
    status int
) WITH (fillfactor = 80);

-- 改表现有默认
ALTER TABLE orders SET (fillfactor = 70);
VACUUM FULL orders;          -- 想让旧数据也生效,需要重整

-- 建/重建索引
CREATE INDEX idx_orders_status ON orders(status) WITH (fillfactor = 75);
REINDEX INDEX idx_orders_status;   -- 旧索引立即按新比例重建

-- 查看表级设置
SELECT relname, reloptions FROM pg_class WHERE relname = 'orders';

-- 查看索引级设置
SELECT indexname, reloptions FROM pg_indexes
WHERE indexname = 'idx_orders_status';