跳转到主要内容

30 真正重要的配置

Postgres 的 400 项配置中,哪些值得你关注,如何安全地逐一修改,以及一份适用于 4 vCPU、16 GB 服务器、带有注释的起步配置。

在一台 64 GB 内存的机器上,采用默认配置的 PostgreSQL 18 服务器,会拿出 128 MB 作为自己的缓存。它假定随机读取的代价是顺序读取的四倍,仿佛背后是旋转磁盘。它每产生大约半个 GB 的 WAL 就启动一次 检查点,默认不记录慢查询、锁等待或临时文件。

这些默认值全都很安全,目的是让 Postgres 能在任何人可能运行它的最小机器上启动。作为默认值,这样做是对的;对于你的生产服务器,却不对。

大约 400 项配置中,真正重要的只有二十来项。下面说明它们是什么、每项怎么选,以及要检查 Postgres 底层环境的哪些方面。在有人要求更大的服务器之前就设好它们:默认配置会浪费你给它的任何机器(先用数据证明,该换更大的机器了)。

深入阅读:对照服务器检查默认值

每个配置项编译时内置的默认值都在 pg_settings.boot_val 中,旁边还有当前值以及这个值的来源。要查看服务器偏离原始默认值的所有设置,这也是接手一台服务器后首先要读的内容:

select name, setting, unit, boot_val, source
  from pg_settings
 where setting is distinct from boot_val
 order by source, name;

postgresql.conf 中有少数几行是由 initdb 写入的(TimeZone、log_timezone、lc_* 配置,以及小型机器上有时会写入的 max_connections 或 shared_buffers),所以 source 会显示为 configuration file,尽管并没有人主动选择它们。

动手验证 · lab 29-configuration
-- boot_val is the compiled-in default, whatever this server's config says
select lab.prove('shared_buffers defaults to 128 MB (16384 pages of 8 kB)',
  boot('shared_buffers') = '16384'
  and (select unit from pg_settings where name = 'shared_buffers') = '8kB');
select lab.prove('random_page_cost defaults to 4, seq_page_cost to 1',
  boot('random_page_cost') = '4' and boot('seq_page_cost') = '1');
select lab.prove('a checkpoint starts after about max_wal_size / (1 + checkpoint_completion_target) of WAL: ~539 MB',
  round(1024 / (1 + boot('checkpoint_completion_target')::numeric)) = 539);
select lab.prove('no slow-query, lock-wait, or temp-file logging by default',
  boot('log_min_duration_statement') = '-1'
  and boot('log_lock_waits') = 'off'
  and boot('log_temp_files') = '-1');

完整实验脚本

知道配置在哪里设置,以及什么会使它生效

修改值之前,先知道它能在哪里设置,以及要经过什么才能应用。大多数“配置没有生效”的谜团都源于这两点之一。

最具体的设置胜出

 postgresql.conf          server-wide, edited by hand
 postgresql.auto.conf     server-wide, written by ALTER SYSTEM
 ALTER DATABASE ... SET   per database, at connection start
 ALTER ROLE ... SET       per role (and per role in a database)
 SET                      per session
 SET LOCAL                until the end of the transaction

每一行都会覆盖上面的设置。按角色设置常被低估:每种负载都可以有自己的上限。应用角色的超时配置在 每个超时默认都是关闭的;报表角色则相反:给更多内存,也更有耐心:

alter role reporting set work_mem = '256MB';
alter role reporting set statement_timeout = '5min';

它们在会话启动时应用,所以连接池中的连接在重新连接前会保留旧值。某个配置看起来没有生效时,pg_settings.source 会告诉你这个值来自哪里。

深入阅读:先设数据库,再叠加角色

给数据库设置 work_mem:新会话会以 source = database 报告这个值,而已经连接的会话会保留旧值。再添加角色设置后,角色设置会胜出,source = user。数据库设置和角色设置都存放在 pg_db_role_setting 中。

动手验证 · lab 29-configuration
alter database :"DBNAME" set work_mem = '16MB';
select lab.prove('a new session picks up the database setting, with source = database',
  (select s from dblink('fresh',
     $$select setting || ' ' || unit || ' ' || source from pg_settings where name = 'work_mem'$$) t(s text))
  = '16384 kB database');
select lab.prove('this session, connected before the change, keeps its old value',
  current_setting('work_mem') = '4MB');
alter role lab_cfg_reporting set work_mem = '256MB';
select lab.prove('a role setting overrides the database setting (source = user)',
  (select s from dblink('reporting',
     $$select current_setting('work_mem') || ' ' || source from pg_settings where name = 'work_mem'$$) t(s text))
  = '256MB user');

完整实验脚本

上下文决定重载还是重启

context 列位于 pg_settings 中,说明修改需要什么操作:

上下文修改方式示例
postmaster配置文件或 ALTER SYSTEM,然后重启shared_buffers、max_connections、io_method
sighup配置文件或 ALTER SYSTEM,然后重载max_wal_size、大多数 autovacuum_*
superuser重载,或由超级用户执行 SETlog_min_duration_statement、wal_compression
user重载,或由任何人执行 SET;也可按角色、按数据库设置work_mem、random_page_cost、statement_timeout
表 30-1 上下文决定重载还是重启

对于服务器级配置,先执行 alter system set ...,再执行 select pg_reload_conf();重载是安全的,正在运行的查询会继续。postmaster 配置虽然会被记录下来,却要等重启才会应用,所以每次改完配置都运行:

select name, setting, pending_restart from pg_settings where pending_restart;
服务器配置只选一个地方

postgresql.auto.conf 会覆盖 postgresql.conf。把两者混用,迟早会出现这样的情况:postgresql.conf 的修改没有效果,因为旧的 ALTER SYSTEM 仍然胜出。选定一个地方(配置管理通常意味着使用配置文件),再用 alter system reset all 清空另一个地方。

深入阅读:不可修改的设置,以及已核对的上下文

internal 设置完全不能修改(block_size、data_checksums)。pg_file_settings 会显示每个配置文件行以及它是否已应用;它的 error 列可以捕获错误值,而重载报告这类错误时只会写入服务器日志。

动手验证 · lab 29-configuration
select lab.prove('postmaster (restart): shared_buffers, max_connections, io_method',
  ctx('shared_buffers') = 'postmaster' and ctx('max_connections') = 'postmaster'
  and ctx('io_method') = 'postmaster');
select lab.prove('user: work_mem, random_page_cost, statement_timeout',
  ctx('work_mem') = 'user' and ctx('random_page_cost') = 'user' and ctx('statement_timeout') = 'user');

完整实验脚本

内存:先做预算,再调优

Postgres 在启动时分配一个共享池,此外每个连接运行时还会分配私有内存(后端内存是私有的)。后者是服务器耗尽内存的原因,所以提高任何配置之前,先把各项上限加起来。本章起步配置的预算如下:

 16 GB server, starter config
 ------------------------------------------------------------
 allocated at startup
   shared_buffers                               4 GB
 ceilings, used only when busy
   work_mem 32MB x 3 nodes x 100 connections    9.4 GB
     (x2 for hash nodes, x3 with 2 parallel workers)
   autovacuum_work_mem 512MB x 4 workers        2 GB
   maintenance_work_mem 1GB x 1 index build     1 GB
 everything left over
   OS page cache, the second cache tier         the rest
 ------------------------------------------------------------
 ceilings alone: 16.4 GB. Nothing left for the OS cache.

这些上限加起来超过了机器的内存容量,这很正常:大多数查询只会用到一小部分 work_mem。但要知道哪一项会随流量增长:work_mem 乘以连接数。

shared_buffers 从内存的 25% 起步,而不是 80%

shared_buffers 是 Postgres 自己的页缓存(shared_buffers 是一个页数组),默认值为 128 MB。在专用服务器上,从内存的 25% 起步。不要设成 80%:Postgres 还会经过 操作系统页缓存读取数据;那是第二层、更大的缓存,而过大的 shared_buffers 会挤压它,同时又重复缓存相同的页。超过 40% 后要测量。修改它需要重启。

大的 shared_buffers 需要大页

当 shared_buffers 很大时,在 Linux 上使用 2 MB 的大页(huge pages):如果使用普通的 4 KB 页面,数百个进程映射 16 GB 共享内存时,其页表可能占掉数 GB。先在内核中预留大页(vm.nr_hugepages,数量由 postgres -C shared_memory_size_in_huge_pages 计算),然后设置 huge_pages = on;默认值 try 在没有预留大页时会静默回退。

effective_cache_size 是提示:设为内存的 50% 到 75%

它不会分配任何内存。规划器把它当作关于可以缓存多少数据的提示(effective_cache_size 是提示,不是内存)。把它设为内存的 50% 到 75%;默认值是 4 GB。

work_mem:先乘起来,再提高

work_mem(默认 4 MB)是一个查询在溢写到临时文件之前,可供每个排序或哈希节点使用的内存。下面是以默认值和 256 MB 对整个 events 排序的结果:

   Sort Method: external merge  Disk: 50896kB
...
   Sort Method: quicksort  Memory: 111653kB

在 4 MB 时,排序向临时文件写入了 50 MB;在 256 MB 时,它用 109 MB 内存完成了排序。阈值很陡:64 MB 仍然会溢写同样的 50 MB,而 128 MB 就能装下。在我们的机器上,内存排序甚至没有更快。当溢写量大、发生频繁或存储很慢时,就需要重视溢写。

那为什么不在全局设置 work_mem = 256MB?关键在于“每个”。一个计划可能有多个排序和哈希节点(work_mem 是按节点计算的,而且可以看到),哈希节点会得到两倍的 work_mem(hash_mem_multiplier 自 PostgreSQL 15 起默认为 2.0),每个并行工作进程都有自己的额度,而且每个连接都可能同时这样做:

 max memory  ~  connections x nodes per query x work_mem (x2 for hashes)
                x (1 + parallel workers)

 200 connections x 3 nodes x 64MB = 38 GB   on a 16 GB server

真正会唤醒 Linux OOM killer 的,是内存使用的尾部峰值;它杀掉一个后端进程时,整个服务器都会重启(见下文的不要让 OOM killer 选择 Postmaster)。所以:

  1. 在连接池代理后面保持全局 work_mem 适中:16 到 64 MB;连接数很多时还要更低。
  2. 在明确知道需要的地方,按角色(如上面的 reporting 角色)或按事务(set local work_mem = '512MB')提高它。
  3. 打开 log_temp_files(见下文),找出实际发生溢写的地方。

深入阅读:溢写阈值,以及一次聚合中的九份内存额度
动手验证 · lab 29-configuration
-- each row: one EXPLAIN ANALYZE of the sort at that work_mem
select lab.prove('at the default 4 MB the sort spills: external merge, about 50 MB on disk',
  (select method = 'external merge' and space_type = 'Disk'
          and space_kb between 45000 and 56000 and temp_written > 10000
     from sorts where work_mem = '4MB'));
select lab.prove('at 256 MB it sorts in memory, using about 109 MB',
  (select method = 'quicksort' and space_type = 'Memory' and space_kb between 100000 and 120000
     from sorts where work_mem = '256MB'));
select lab.prove('the threshold: 64 MB (less than the sort needs) still spills, 128 MB (more) does not',
  (select method from sorts where work_mem = '64MB') = 'external merge'
  and (select method from sorts where work_mem = '128MB') = 'quicksort');

完整实验脚本

排序是在关闭并行的情况下运行的;它溢写了 temp written=12751 个块。普通的 select kind, count(*) from events group by kind 会在三个进程中运行一个位于 Partial HashAggregate 之上的 Sort(一个领导进程和两个工作进程)。它实际用量只在 KB 量级(每个排序 25 KB、每个哈希 32 KB),但它的上限是:每个进程中排序的内存额度是 work_mem,哈希的额度是它的两倍;合计就是 work_mem 的九倍。

每个自动清理工作进程也会占用 maintenance_work_mem

maintenance_work_mem(默认 64 MB)供 CREATE INDEX、VACUUM 和恢复操作使用;同时运行的这类操作通常不多,所以在 16 GB 机器上设为 1 GB 是合理的。除非显式设置 autovacuum_work_mem,否则每个自动清理工作进程也会占用同样多的内存。(内存不足时,清理会重新扫描每个索引。)

告诉 Postgres 你的存储能做什么

有两组配置用于向 Postgres 描述磁盘:一组为规划器设定读取代价,另一组决定同时保持多少次读取处于在途状态。

random_page_cost 假定旋转磁盘

规划器把顺序读取一页的代价定为 1,把随机读取一页的代价定为 random_page_cost = 4,这个比值模拟的是磁盘磁头寻道。在 SSD 上,随机读取与顺序读取的代价几乎相同,所以默认值会低估索引扫描的价值。下面是在 project_id 上有索引时,按项目顺序读取前 20 个项目的事件的结果:

-- random_page_cost = 4
   Buffers: shared hit=135 read=1784
   ->  Sort ...
         ->  Bitmap Heap Scan on events ...
-- random_page_cost = 1.1
   Buffers: shared hit=1970
   ->  Index Scan using events_project_id_idx on events ...

两种方式触碰的页面数大致相同(1,919 对 1,970)。在 4 下,规划器认为按顺序读取索引的代价高于位图扫描加排序,于是用位图收集页面后再排序;在 1.1 下,按顺序的索引扫描胜出,排序消失。对于 SSD 存储的服务器(包括云块存储),设置 random_page_cost = 1.1。它是 user 配置,所以先按会话试用,看看哪些计划会变化(规划器也在估算)。

深入阅读:两个计划,以及背后的查询
create index events_project_id_idx on events (project_id);
set max_parallel_workers_per_gather = 0;
explain (analyze, timing off)
select * from events where project_id between 1 and 20
order by project_id limit 5000;

limit 设置的上限不会触及,因为查询只返回 1,967 行。估算代价:在 4 下为 6,748,在 1.1 下为 2,244。

动手验证 · lab 29-configuration
\set q 'select * from events where project_id between 1 and 20 order by project_id limit 5000'
select lab.prove('at random_page_cost = 4: a bitmap heap scan, then a sort',
  (:'nodes_at_4')::text[] @> array['Sort', 'Bitmap Heap Scan']);
select lab.prove('at 1.1: an index scan in order, no sort',
  (:'nodes_at_11')::text[] @> array['Index Scan'] and not (:'nodes_at_11')::text[] @> array['Sort']);
select lab.prove('both touch about the same number of pages',
  :buffers_at_11::numeric / :buffers_at_4 between 0.8 and 1.25);

完整实验脚本

快速存储需要更多在途读取

effective_io_concurrency 设置的是:当 Postgres 能预判所需页面时,允许同时在途的读取数量。PostgreSQL 18 把默认值从 1 提高到 16(maintenance_io_concurrency 也从 10 提高到 16);在快速 NVMe 或高 IOPS 的云卷上,把它设为 64 到 200,可能有助于从磁盘读取的扫描。

PostgreSQL 18 还为读取加入了异步 I/O,由 io_method 选择,而修改它需要重启。默认的 worker 会把读取交给 io_workers 个进程;io_uring 使用 Linux 内核的接口;sync 是旧行为。保持 worker。如果扫描受 I/O 限制,先提高 io_workers,这只需要重载。

拉长检查点间隔,并按 WAL 速率设置 max_wal_size

先记日志,再写数据解释了这些配置的作用;下面说明如何设置它们:

  • checkpoint_timeout(5 分钟):在写入繁重的系统上提高到 15 到 30 分钟。整页镜像会更少,但崩溃恢复会更久。
  • max_wal_size(1 GB):检查点之间 WAL 的软上限;Postgres 会在 WAL 达到约 max_wal_size / (1 + checkpoint_completion_target) 时请求检查点,按默认值大约是 540 MB。测量 WAL 速率(在繁忙的一小时内计算 LSN 的差值),把它设得明显高于一个间隔产生的 WAL 量:在 16 GB 服务器上,4 到 16 GB 很常见,同时要在 pg_wal 所在磁盘上留出同样多的空闲空间。
  • wal_compression(off):设为 lz4 或 zstd。WAL 章节的测试中,它把以镜像为主的 WAL 降到了 59% 和 35%。
  • min_wal_size(80 MB):1 GB 没问题。其余配置已经合适。

然后确认大多数检查点是按时间触发,而不是被请求触发的(检查点为恢复设定上限说明了如何读取 pg_stat_checkpointer)。

深入阅读:核对 WAL 默认值

wal_buffers 的默认值为 -1:取 shared_buffers 的 1/32,但上限是一个 16 MB 的 WAL 段;这对几乎所有服务器都合适。有两个 WAL 配置可以保持不动:wal_level 的默认值是 replica,足以支持备库和时间点恢复(只有逻辑解码需要 logical);full_page_writes 保持开启,因为关闭它后,如果 8 KB 页写入到一半时发生崩溃,可能留下恢复无法修复的撕裂页。“其余配置已经合适”指的就是这些配置。

动手验证 · lab 29-configuration
select lab.prove('checkpoint_timeout 5 min, checkpoint_completion_target 0.9, min_wal_size 80 MB',
  boot('checkpoint_timeout') = '300' and boot('checkpoint_completion_target') = '0.9'
  and boot('min_wal_size') = '80');
select lab.prove('wal_compression off (superuser), wal_buffers -1 (automatic, restart)',
  boot('wal_compression') = 'off' and ctx('wal_compression') = 'superuser'
  and boot('wal_buffers') = '-1' and ctx('wal_buffers') = 'postmaster');
select lab.prove('18 defaults: effective_io_concurrency and maintenance_io_concurrency 16',
  boot('effective_io_concurrency') = '16' and boot('maintenance_io_concurrency') = '16');
select lab.prove('io_workers defaults to 3 and changes with a reload',
  boot('io_workers') = '3' and ctx('io_workers') = 'sighup');

完整实验脚本

自动清理的默认值适合小表和慢磁盘

清理不可省解释了为什么这些默认值在大表和快速存储上会落后。值得做的服务器级修改如下:

配置项默认值起始值原因
autovacuum_vacuum_cost_limit-1 (200)1000 到 2000在 SSD 上太慢,而且由所有工作进程共享
autovacuum_vacuum_scale_factor0.20.05死行达到 5% 就清理,而不是 20%
autovacuum_vacuum_insert_scale_factor0.20.05更早冻结并设置可见性位
autovacuum_analyze_scale_factor0.10.02 到 0.05让增长中的表拥有更新的统计信息
autovacuum_max_workers33 到 6并行处理更多表;同时提高代价上限
log_autovacuum_min_duration10min1s看到自动清理在做什么
表 30-2 自动清理的默认值适合小表和慢磁盘

然后为最大的表添加按表设置。PostgreSQL 18 还把死行触发条件限制在 autovacuum_vacuum_max_threshold(默认 1 亿行),所以一个有十亿行的表不再需要等到出现 2 亿个死行才触发(清理不可省中有公式)。

深入阅读:核对自动清理默认值

在 PostgreSQL 18 中,autovacuum_max_workers 可以通过重载修改,最高不超过 autovacuum_worker_slots(默认 16;修改该值需要重启)。

按表设置属于存储参数,只对那张表覆盖服务器级值。对于像 events 这样规模大、很繁忙的表:

alter table events set (autovacuum_vacuum_scale_factor = 0.01,
                        autovacuum_analyze_scale_factor = 0.01);

除非表拥有自己的 autovacuum_vacuum_cost_limit,否则一个工作进程的代价预算会在同时运行的工作进程之间分摊;如果表有自己的设置,该工作进程就使用自己的预算。

动手验证 · lab 29-configuration
select lab.prove('autovacuum_vacuum_cost_limit -1, meaning vacuum_cost_limit (200)',
  boot('autovacuum_vacuum_cost_limit') = '-1' and boot('vacuum_cost_limit') = '200');
select lab.prove('scale factors 0.2, 0.2 (insert), 0.1 (analyze)',
  boot('autovacuum_vacuum_scale_factor') = '0.2'
  and boot('autovacuum_vacuum_insert_scale_factor') = '0.2'
  and boot('autovacuum_analyze_scale_factor') = '0.1');
select lab.prove('autovacuum_max_workers 3, a reload in 18, up to autovacuum_worker_slots (16, restart)',
  boot('autovacuum_max_workers') = '3' and ctx('autovacuum_max_workers') = 'sighup'
  and boot('autovacuum_worker_slots') = '16' and ctx('autovacuum_worker_slots') = 'postmaster');

完整实验脚本

并行查询加速一个查询,不是整个服务器

Postgres 可以把大型扫描、连接或聚合拆分到多个工作进程中。这样会让一个大型查询更快,但不会提高每秒查询数;在繁忙的 OLTP 服务器上,查询的工作进程占用的每个核心,都是其他连接无法使用的核心。

对于 4 个 vCPU:设置 max_parallel_workers = 4、max_parallel_workers_per_gather = 2。分析型服务器可以设得更高;对延迟敏感的 OLTP,可以为应用角色将每个 Gather 的并行工作进程上限设为 0,只让报表查询并行执行。

深入阅读:规划器如何决定工作进程数量

max_worker_processes(8,修改需要重启)限制所有后台工作进程,而 max_parallel_workers(8)是其中分给并行查询的份额。规划器根据表大小调整工作进程数量:超过 min_parallel_table_scan_size(8 MB)后可以使用一个工作进程;每增加一个工作进程,所需大小再乘以三,因此 events(275 MB)会得到四个工作进程。

动手验证 · lab 29-configuration
select lab.prove('events (275 MB) is past 8 x 3^3 = 216 MB but short of 648 MB: four workers',
  pg_relation_size('events') between 216 * 1024 * 1024 and 648 * 1024 * 1024);
set max_parallel_workers_per_gather = 8;
select lab.prove('with the cap raised, the planner plans 4 workers for events',
  (select (p -> 'Plan' -> 'Plans' -> 0 ->> 'Workers Planned')::int
     from lab.plan('select kind, count(*) from events group by kind') p) = 4);

完整实验脚本

低开销日志能回答大多数凌晨 3 点的问题

Postgres 开箱只记录错误和检查点(从 PostgreSQL 15 起,log_checkpoints 默认开启),除此之外几乎什么都不记。这些配置代价很低,却能回答大多数“凌晨 3 点发生了什么”的问题:

配置项默认值建议值它提供的信息
log_min_duration_statement-1250ms 到 1s所有慢于该值的语句
log_lock_waitsoffon每当锁等待超过 deadlock_timeout(1s)时记录一行
log_temp_files-10 或某个大小把数据溢写出 work_mem 的查询
log_autovacuum_min_duration10min1s带有完整统计信息的自动清理运行记录
表 30-3 低开销日志能回答大多数凌晨 3 点的问题

把 log_line_prefix 设为 '%m [%p] %q%u@%d %a ',这样每一行都会标出用户、数据库和 application_name。

有两点需要注意:log_min_duration_statement 会记录查询参数,因此个人数据可能进入日志;log_statement = 'all' 产生的 I/O 可能比数据库本身还多。日志能显示哪些查询很慢;pg_stat_statements 能显示哪些查询的总开销最大,而这通常才是更好的问题(观察数据库运行)。

auto_explain 记录你之后无法复现的计划

auto_explain 会记录慢查询实际运行时的计划,而这个计划往往无法在事后复现。把它预加载到 shared_preload_libraries 中,将 auto_explain.log_min_duration 设为两秒左右,打开 log_analyze 和 log_buffers,关闭 log_timing:逐节点计时才是昂贵的部分。下面的起步配置已经包含这些行。

深入阅读:auto_explain 在一个会话中捕获了什么

在一个会话中加载它,并把阈值设为 50 ms 后,统计状态为失败的告警数量时,记录下了如下日志:

NOTICE:  duration: 64.819 ms  plan:
Query Text: select count(*) from events
            where kind = 'alert' and payload->>'status' = 'failed';
Finalize Aggregate  (... rows=1 width=8) (actual rows=1.00 loops=1)
  Buffers: shared hit=4207 read=30966
  ...
        ->  Parallel Seq Scan on events  (... rows=511 width=0)
                                         (actual rows=20820.33 loops=3)
              Rows Removed by Filter: 645846

这个计划自己就讲清了一个故事:一次计数扫描了 35,000 个页面;估算每个进程处理 511 行(合计约 1,200 行:两个工作进程加领导进程,规划器预计领导进程的工作量会少于工作进程),而实际有 62,461 行(规划器也在估算解释了这个误估)。

动手验证 · lab 29-configuration
load 'auto_explain';
select lab.prove('the slow count reads about 35,000 pages',
  lab.buffers($$select count(*) from events where kind = 'alert' and payload->>'status' = 'failed'$$)
  between 33000 and 37000);
-- est: the scan's estimate across all processes; actual: rows x loops
select lab.prove('the planner estimates under 5,000 matching rows; there are over 60,000',
  (select est < 5000 and actual > 60000 from est));

完整实验脚本

内核可以拖垮一台调优良好的服务器

上面的每项配置都可能是正确的,但服务器仍然会崩溃:Linux 杀掉 Postmaster,检查点被数 GB 的脏页阻塞,容器只给并行查询 64 MB 共享内存。内核有自己的规则。(在托管服务中,内核由服务商负责,但内存不足导致的进程终止仍会影响你,表现为所有连接同时断开。)

别让 OOM killer 选择 Postmaster

默认情况下,Linux 会承诺分配它可能无法实际兑现的内存;当 RAM 和交换空间都耗尽时,OOM killer 会杀掉一个进程。如果它选中的是后端进程,Postgres 无法判断共享内存是否只改了一半,于是会终止其他所有进程并执行崩溃恢复(检查点为恢复设定上限)。所有连接都会断开。

文档给出了两个可以一起使用的修复办法。严格超额承诺(vm.overcommit_memory = 2)会让内核拒绝超过上限的分配,因此想要过多内存的查询只会单独失败并报 out of memory。用 oom_score_adj = -1000 保护 Postmaster,并设置 PG_OOM_ADJUST_FILE,让它的子进程仍然可以被杀掉。它们都不能修复根因;根因要靠内存预算解决。

深入阅读:严格超额承诺与 oom_score_adj,逐步操作

在严格超额承诺下,提交上限为 (RAM - huge pages) x vm.overcommit_ratio / 100 + swap;该比值默认是 50,这会在还有一半 RAM 空闲时就拒绝分配,所以只能在专用服务器上调高它;我们从 80 到 90 起步。在 systemd 下,保护配置是 OOMScoreAdjust=-1000 加上 Environment=PG_OOM_ADJUST_FILE=/proc/self/oom_score_adj;没有这个变量时,每个后端进程都会继承豁免。如果 Postmaster 自己被杀掉,在重启前不会接受新连接(PostgreSQL 文档)。

grep -E '^(CommitLimit|Committed_AS)' /proc/meminfo  # Committed_AS must stay below
cat /proc/$(head -1 "$PGDATA/postmaster.pid")/oom_score_adj  # -1000: protected
journalctl -k | grep -iE 'out of memory|killed process'     # past kills
动手验证 · lab 29-configuration
select lab.prove('without PG_OOM_ADJUST_FILE, a backend inherits the postmaster''s oom_score_adj',
  position('PG_OOM_ADJUST_FILE'::bytea in pg_read_binary_file('/proc/' || :pm_pid || '/environ')) = 0
  and host_file('/proc/' || :pm_pid || '/oom_score_adj')
      = host_file('/proc/' || pg_backend_pid() || '/oom_score_adj'));

完整实验脚本

为每种机器类型一次性决定操作系统配置

每一项都是 Postgres 无法替你决定的内核或文件系统选择:

  • 预留大页(vm.nr_hugepages),并关闭透明大页(transparent huge pages)。 文档不建议使用透明大页。
  • 保留交换空间,但让它很少被使用(vm.swappiness 为 1 到 10)。被换出的后端进程会很慢;被杀掉的后端会让服务器重启。
  • 用字节数限制脏页(vm.dirty_background_bytes 和 vm.dirty_bytes,约为存储每秒可写入量)。默认情况下,内核允许 10% 的内存处于未刷盘状态;而检查点的 fsync 会让每次提交都在它后面停住:每隔几分钟准时出现一次延迟尖峰。
  • 使用 ext4 或 XFS、设置 noatime,并且绝不要禁用写屏障,否则断电可能丢失 Postgres 已报告为持久化的提交。
  • 保持预读和 I/O 调度器的默认值,并保持 vm.zone_reclaim_mode = 0。 对 OLTP,优先选择更少但更快的核心;在 x86 云环境中,一个 vCPU 通常只是半个物理核心。
  • 在容器中按 cgroup 限额做预算,因为该限额包含页缓存;同时给 /dev/shm 多于 Docker 默认的 64 MB,否则大型并行查询可能失败。

深入阅读:用数字看大页和脏页

postgres -D "$PGDATA" -C shared_memory_size_in_huge_pages 即使在服务器启动之前也能给出答案(实验服务器上是 76 个,即 152 MB)。把 vm.nr_hugepages 设为这个数,再设置 huge_pages = on。预留的大页会从其他用途可用的内存中消失,所以只预留 Postgres 需要的数量(文档中的操作步骤)。透明大页“已知会在某些 Linux 版本上让某些用户的 PostgreSQL 性能下降”(文档);这些报告之后内核已经变化,但没人证明 THP 有帮助。

超过 vm.dirty_background_ratio(10)后,内核开始刷盘;超过 vm.dirty_ratio(20)后,写入者必须自己刷盘:在一台 256 GB 的机器上,开始刷盘前大约会积累 25 GB。Postgres 写入时会推动内核(checkpoint_flush_after 为 256 KB,bgwriter_flush_after 为 512 KB),但批量写入的后端进程不会这样做(backend_flush_after = 0)。把字节数上限设为存储约一秒内可写入的数据量,并比较修改前后 sync 在 log_checkpoints 行中的时间。

动手验证 · lab 29-configuration
select lab.prove('huge_pages defaults to try, and changing it needs a restart',
  boot('huge_pages') = 'try' and ctx('huge_pages') = 'postmaster');
select lab.prove('on Linux, checkpoint_flush_after is 256 kB and bgwriter_flush_after 512 kB; backends: 0',
  boot('checkpoint_flush_after') = '32' and boot('bgwriter_flush_after') = '64'
  and boot('backend_flush_after') = '0'
  and (select unit from pg_settings where name = 'checkpoint_flush_after') = '8kB');

完整实验脚本

深入阅读:容器会把页缓存计入内存,而 /dev/shm 只有 64 MB

cgroup 的内存包括页缓存和共享内存(内核 cgroup 文档);达到 memory.max 时,OOM killer 会在 cgroup 内运行。根据这个限额设置 shared_buffers,限额见 /sys/fs/cgroup/memory.max;容器内的 free 通常报告的是宿主机的内存。

并行查询会在 /dev/shm 中共享状态,例如并行哈希连接的哈希表;Docker 给容器的这一空间只有 64 MB。几次大型并行哈希连接同时运行时,会失败并报 could not resize shared memory segment ... No space left on device。实验套件采用 shm_size: 512m 这一设置;在 Kubernetes 中,把一个以内存为后端的 emptyDir 挂载到那里(Kubernetes 文档)。

动手验证 · lab 29-configuration
select lab.prove('shared_buffers lives in anonymous mmap memory; dynamic shared memory is POSIX shm',
  current_setting('shared_memory_type') = 'mmap'
  and current_setting('dynamic_shared_memory_type') = 'posix');
select lab.prove('the lab kit''s compose file gives /dev/shm 512 MB, not Docker''s 64 MB',
  host_file('/proc/self/mounts') ~ '/dev/shm \S+ \S*size=524288k');

完整实验脚本

深入阅读:完整的 sysctl 文件,以及检查一台机器的命令

下面这份 16 GB 起步配置面向一台专用服务器,存储可以持续提供约 500 MB/s:

# /etc/sysctl.d/30-postgresql.conf, then: sysctl --system
vm.overcommit_memory = 2                # refuse, don't kill
vm.overcommit_ratio = 85                # then watch Committed_AS vs CommitLimit
vm.swappiness = 5                       # keep swap for emergencies
vm.dirty_background_bytes = 268435456   # 256 MB; size to about 1 s of writes
vm.dirty_bytes = 1073741824             # 1 GB
vm.zone_reclaim_mode = 0                # the default; multi-socket boxes
vm.nr_hugepages = N                     # from shared_memory_size_in_huge_pages
# and on the kernel command line: transparent_hugepage=never
cat /sys/kernel/mm/transparent_hugepage/enabled   # want [never] (or [madvise])
grep -E '^(HugePages_(Total|Free|Rsvd)|Dirty|Writeback):' /proc/meminfo
vmstat 5                                          # nonzero si/so: swapping now
findmnt -no SOURCE,FSTYPE,OPTIONS -T "$PGDATA"    # noatime; never nobarrier

从这份 4 vCPU、16 GB 配置开始,然后测量

大多数值都在前文相应小节中说明过;少数没有沿用上文的值,会在注释中说明原因。它假定这是使用 SSD、4 个 vCPU、16 GB 内存的专用服务器,应用位于连接池代理(pooler)之后,并且已按 PostgreSQL 18.6 核对。复制之前先注意:work_mem = 32MB 假定服务器连接数约为 100;max_wal_size = 8GB 需要 WAL 卷有同样多的余量;所有值都应随环境缩放:内存随 RAM、并行度随核心数、WAL 随写入速率变化。

把它放入由 postgresql.conf 引入的文件中,或者用 ALTER SYSTEM 设置,但不要两者都用。

# Connections (restart). Keep this low and use a pooler.
max_connections = 100

# Memory
shared_buffers = 4GB                 # 25% of RAM (restart)
huge_pages = try                     # 'on' once vm.nr_hugepages is set
effective_cache_size = 12GB
# per sort/hash node, per process; raise per role
work_mem = 32MB
hash_mem_multiplier = 2.0
maintenance_work_mem = 1GB
autovacuum_work_mem = 512MB

# Storage and I/O (SSD / NVMe / cloud block storage)
random_page_cost = 1.1               # random reads are nearly sequential cost
effective_io_concurrency = 64        # fast SSDs; 16 is the default
maintenance_io_concurrency = 64
io_method = worker                   # the default, made explicit
io_workers = 4                       # one per vCPU to start; a reload

# WAL and checkpoints
wal_compression = lz4
max_wal_size = 8GB                   # make checkpoints time-driven
min_wal_size = 1GB
checkpoint_timeout = 15min           # fewer full-page images, longer recovery
checkpoint_completion_target = 0.9

# Autovacuum
autovacuum_max_workers = 4
autovacuum_vacuum_cost_limit = 2000  # shared by all workers
autovacuum_vacuum_scale_factor = 0.05
autovacuum_vacuum_insert_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02
# then per-table settings for the biggest tables

# Parallel query (4 cores, mixed workload)
max_worker_processes = 8             # the default; all background workers
max_parallel_workers = 4
max_parallel_workers_per_gather = 2
max_parallel_maintenance_workers = 2 # the default; parallel index builds

# Server-wide backstop only. The app role's timeouts (statement 5s,
# lock 3s, idle in transaction 30s, transaction 5min) are set per role.
idle_in_transaction_session_timeout = 5min

# Logging
log_line_prefix = '%m [%p] %q%u@%d %a '
log_min_duration_statement = 500ms
log_lock_waits = on
log_temp_files = 0                   # every spill; raise to e.g. 64MB if noisy
log_autovacuum_min_duration = 1s
log_checkpoints = on

# Extensions (restart)
shared_preload_libraries = 'pg_stat_statements,auto_explain'
pg_stat_statements.max = 10000       # default 5000; room for ORM-heavy apps
pg_stat_statements.track = top       # the default: top-level statements
auto_explain.log_min_duration = 2s
auto_explain.log_analyze = on
auto_explain.log_buffers = on
auto_explain.log_timing = off

# Monitoring. track_io_timing reads the clock on every I/O: cheap on
# modern hardware; check new machine types with pg_test_timing.
track_io_timing = on
一次只改一项,并进行测量

修改之前,先记录它应该改变的数值:pg_stat_statements 总量、检查点次数、临时文件量和自动清理频率。一次只改一个配置,等待一个有代表性的时间段,然后进行比较(只改一件事,并端到端测量)。把配置放进版本控制,这样“改了什么?”永远都有答案。

深入阅读:实验检查起步配置的每一行

实验会用检查自己的配置同样的方式检查这份配置:每个名称都存在于 pg_settings 中,每个会话可以设置的值都能被接受。在你自己的服务器上,再为本章前面的 pending_restart 检查加一条查询。它会列出服务器无法应用的配置文件行:

select sourcefile, sourceline, name, setting, error
  from pg_file_settings where error is not null;

postgresql.conf 中拼错的名称会出现在这里,而 postmaster 配置的错误值会让服务器根本无法启动;所以要在重启之前检查,而不是之后。

动手验证 · lab 29-configuration
-- the lab also checks that every name in the config exists in pg_settings,
-- and SETs every value a session can set, inside a transaction
select lab.prove('the session-settable starter values are all accepted (wal_compression = lz4 included)',
  current_setting('wal_compression') = 'lz4' and current_setting('random_page_cost') = '1.1');

完整实验脚本

在托管服务上,真正重要的配置仍可调整

在托管服务上,配置位于服务商的参数组中,需要重启的配置要重启实例才生效(往往意味着故障切换),有些配置则被锁定(设置位于参数组中)。替换存储层的服务商会改变 WAL 配置的行为(分离式存储改变了部分规则)。最重要的配置在哪里都可以调整,ALTER ROLE ... SET 即使在不能使用 ALTER SYSTEM 的地方也能工作。

本章要点

  • 在修改任何内容之前检查 pg_settings(source、context);凡是因负载而异的配置,都按角色设置。
  • 先做内存预算:shared_buffers 设为 RAM 的 25%,再加上 work_mem 乘以节点数和连接数。全局保持 work_mem 适中,在明确需要的地方再提高。
  • 在 SSD 上设置 random_page_cost = 1.1;让异步 I/O(io_method = worker)保持默认值。
  • 用更大的 max_wal_size 和更长的 checkpoint_timeout 让检查点由时间驱动,并打开 wal_compression。
  • 给自动清理更大的代价上限和更小的比例因子。
  • 在 Postgres 之下的系统层:让 OOM killer 不要杀掉 Postmaster,关闭透明大页,用字节数限制脏页,绝不要禁用写屏障;在容器中按 cgroup 限额做预算。
  • 打开慢查询、锁等待、临时文件和自动清理日志,以及 auto_explain。

延伸阅读

  • Setting parameters,PostgreSQL 文档。设置值的所有方式,包括 include、ALTER SYSTEM 和按角色设置。
  • Resource consumption,PostgreSQL 文档。内存、I/O 和异步 I/O 配置,以及它们的确切默认值和限制。
  • Write Ahead Log settings,PostgreSQL 文档。检查点、max_wal_size 和 wal_compression。
  • Error reporting and logging,PostgreSQL 文档。所有 log_* 配置以及 log_line_prefix 的转义符。
  • Managing kernel resources,PostgreSQL 文档。大页、Linux 内存超额承诺,以及保护 Postmaster 不被 OOM killer 杀掉的 oom_score_adj 配置方法。
  • Documentation for /proc/sys/vm,Linux 内核文档。超额承诺、交换倾向、脏页阈值和区域回收的确切含义与默认值。

表格阅读