• PG 的 Bgwriter 为什么搞得那么复杂

PG 的 Bgwriter 为什么搞得那么复杂

2025-05-27 02:37:04 栏目:宝塔面板 19 阅读

我是从C程序员转DBA的,所以遇到数据库的一些问题,我总是喜欢从一个码农的角度去发出灵魂拷问:为什么会这样?为什么要这样?这么做有啥必要?经过二十多年时间,Oracle的一些犄角旮旯的问题都被我理解和接受了。刚刚转向PG数据库的学习的时候,对PG这个简单但是庞大的数据库系统的一些行为十分不解。今天我们就来聊聊我对BGWRITER的一些思考。

PG的后台写入器(Background Writer, bgwriter)负责将脏页(即被修改但尚未写入磁盘的数据页)从共享缓冲区写回到磁盘,从而减少直接由前端查询进程执行写操作的需求,BGWRITER对数据库的性能优化具有重要作用。对Oracle的dbwr的算法比较了解的DBA刚开始可能很难理解PG的刷脏机制。因为除了BGWriter之外,Checkpoint、Backend都有刷脏的行为。Checkpoint刷脏比较容易理解,因为早期Oracle的CKPT也是有刷脏行为的。需要Checkpoint的时候,如果发现某些需要写入的脏块暂时还没有写入,并且这些数据块当前处于可以写入的状态,这种情况下CKPT顺手就做了。后来为了解决高并发环境下的闩锁开销问题,O记才让CKPT不再刷脏,提高Checkpoint推进的速度。

PG和O记最大的不同是Backend也是有刷脏行为的,这个设计刚开始的时候让我感到十分不解。BGWriter和Checkpoint都可以刷脏对于闩锁争用来说问题还不是太大,顶多是两个会话协同好就行了,而Backend的数量可能很庞大,如果同时又刷脏需求的话,协调好共享池相关数据结构的LWLOCK成本就低不了。

BGWriter在扫描共享缓冲区时,采用了一种基于LRU(Least Recently Used,最近最少使用)算法的启发式方法来选择要刷新的页面。具体来说BGWriter会优先对最近最少被使用的脏页进行回写,即这些页面在未来短时间内不太可能被再次访问,因此将它们写回到磁盘可以释放内存而不大影响性能。如果一个页面当前正被其他后端进程读取或修改,那么BGWriter会跳过该页面。除此之外,BGWriter的行为受bgwriter_lru_maxpages和bgwriter_lru_multiplier等参数的影响。每次循环中,BGWriter尝试写的最大脏页数由bgwriter_lru_maxpages指定,而实际尝试写的页数则根据系统近期需求动态调整,这取决于bgwriter_lru_multiplier。如果当前循环中已达到设定的最大写入页数限制,则即使还有未处理的脏页,也会暂时跳过。

bgwriter_lru_maxpages参数比较容易理解,就是bgwriter一个批处理任务中刷脏的最大脏页数量,刷够了这些页,bgwriter就结束这次任务,进入休眠。bgwriter_lru_multiplier 是一个用于调整PostgreSQL后台写入器(Background Writer, BGWriter)行为的重要参数。它决定了BGWriter尝试预测需要刷新多少页面到磁盘的积极程度,基于最近几次前台请求所需页面数量进行计算。BGWriter会监控最近几次前台请求所需的新缓冲区页面数,并将这个数值乘以 bgwriter_lru_multiplier 来决定下一次循环应该尝试刷新多少个脏页回到磁盘。假设最近几次前台请求所需的平均新缓冲区页面数为N,则在下一个周期内,BGWriter将会尝试刷新 N * bgwriter_lru_multiplier 个脏页。这意味着如果 bgwriter_lru_multiplier 设置得较高,BGWriter就会更积极地尝试将更多的脏页写回磁盘;反之,如果设置得较低,则BGWriter的行为会更加保守。

与BGWriter相对保守的刷脏策略相比,Checkpoint是一种强制刷脏机制。Checkpoint的主要目的是缩短崩溃恢复时间,因为它限定了恢复过程中需要回放的WAL(Write-Ahead Logging)日志的范围。Checkpoint可以由多种条件触发,包括达到checkpoint_timeout设定的时间间隔(默认为5分钟),或WAL文件使用量达到了max_wal_size。此外,也可以手动触发Checkpoint。当Checkpoint开始时,PostgreSQL会强制将所有脏数据页写回到磁盘。这包括但不限于共享缓冲区中的数据页。为了保证一致性,Checkpoint期间不允许新的事务提交(尽管允许读取和正在进行的事务继续)。整个过程包括了标记一个检查点记录到WAL日志中,并更新控制文件以反映最新的检查点信息。与BGWriter不同,Checkpoint刷脏的主要目的是提供一个已知的一致状态点,以便于系统崩溃后快速恢复。通过定期创建这些一致状态点,可以限制恢复过程中需要处理的日志量。

上面的刷脏机制还是比较容易理解的,PG中最不好理解的就是Backend也有写脏块的行为,而在其他数据库中,这个工作完全是由后台进程来完成的。当一个Backend进程需要读取一个新的数据块到共享缓冲区中,但当前没有可用的空闲缓冲区时,就会触发缓冲区替换机制(如使用 ClockSweep 算法)。如果选中的替换页面是脏页(Dirty Page),那么该页面必须被写回磁盘。

这个机制存在一个比较大的问题,那就是很可能一个SELECT操作操作会产生写IO操作,从而让某个SELECT操作的执行延时加大。我前两天也谈到Oracle也存在一些引起执行效率不稳定的行为。不过Oracle的这些行为发生的概率极低,甚至在一些负载不是特别大的系统中很少遇到。而在PG中我们经常会遇到这些情况的。

图片

这是我们生产环境的一个PG 14数据库,可以看出,真正由BGWriter刷脏的数量只有162,而Backend刷脏的数量为120万块。似乎BGWriter有点不务正业了。

这种情况的发生与PG数据库的设计初衷有关,PG数据库作为最为流行的开源数据块之一,从开始并不是为高并发、高负载的企业级应用设计的,虽然这些年其企业级特性越来越多,但是一些根子上的问题还是沿用了早期的设计。为了适应早期的低成本IO设备,PG在性能上做出了大量的妥协。因为FULL PAGE WRITE机制的存在,导致了PG数据库总是希望脏块能够尽可能比较晚地回写到存储系统中,Checkpoint也尽可能的不要太快。将刷脏工作分散到大量的Backend中去,也是对IO的一种妥协,一方面可以缓解Checkpoint过重对IO的影响,一方面也简化了Backend在共享缓冲区不足时重用脏块的处置方法。

曾经在和一个搞PG的朋友谈到今天讨论的问题的时候,他十分兴奋地提出他的观点,他认为PG这方面的设计十分优秀,十分巧妙。我倒是有些不同的观点,认为这是PG数据库成为高负载的企业级数据库路上的一个必须优化的拦路虎。在现代硬件条件下,这方面的设计完全可以做优化了。我想随着AIO/DIO等IO技术的引进,消除Backend写脏块的条件也逐步成熟了。

今天时间有限,我们先谈到这里吧,明天我将继续这个话题,讨论一下,基于如此复杂的PG刷脏机制,DBA改如何去调整他们的数据库,从而适应自己的业务场景。明天再聊吧。

本文地址:https://www.yitenyun.com/242.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL 数据同步 ACK 双主架构 循环复制 Web 应用 异步数据库 序列 核心机制 生命周期 Deepseek 宝塔面板 Linux宝塔 Docker JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 SSL 堡垒机 跳板机 HTTPS Windows宝塔 Mysql重置密码 无法访问宝塔面板 HTTPS加密 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 ES 协同 修改DNS Centos7如何修改DNS scp Linux的scp怎么用 scp上传 scp下载 scp命令 防火墙 服务器 黑客 Serverless 无服务器 语言 存储 Oracle 处理机制 Spring SQL 动态查询 Linux 安全 网络架构 工具 网络配置 RocketMQ 长轮询 配置 加密 场景 MySQL 9.3 开源 PostgreSQL 存储引擎 HexHub Canal Rsync 架构 InnoDB 缓存方案 缓存架构 缓存穿透 信息化 智能运维 响应模型 日志文件 MIXED 3 索引 数据 业务 监控 线上 库存 预扣 AI 助手 数据库锁 单点故障 聚簇 非聚簇 B+Tree ID 字段 GreatSQL Hash 字段 分库 分表 云原生 Redis Redis 8.0 DBMS 管理系统 自定义序列化 优化 万能公式 openHalo OB 单机版 数据集成工具 ​Redis 机器学习 推荐模型 SVM Embedding SpringAI 分页查询 SQLite Redka sqlmock PostGIS 系统 SQLark 虚拟服务器 虚拟机 内存 Netstat Linux 服务器 端口 排行榜 排序 查询 prometheus Alert SQLite-Web 数据库管理工具 自动重启 运维 同城 双活 缓存 sftp 服务器 参数 共享锁 RDB AOF 技术 Testcloud 云端自动化 EasyExcel MySQL8 向量数据库 大模型 不宕机 容器化 分布式架构 分布式锁​ 聚簇索引 非聚簇索引 • 索引 • 数据库 OAuth2 Token Entity 开发 StarRocks 数据仓库 Doris SeaTunnel 人工智能 推荐系统 分页 数据结构 IT AIOPS 数据备份 Postgres OTel Iceberg MongoDB 容器 数据类型 IT运维 连接控制 机制 Python Web LRU Milvus 悲观锁 乐观锁 Ftp 池化技术 连接池 Caffeine CP 部署 崖山 新版本 高可用 向量库 redo log 重做日志 MCP mini-redis INCR指令 单线程 线程 磁盘架构 MVCC 事务隔离 流量 开放协议 Web 接口 字典 电商 对象 微软 SQL Server AI功能 速度 服务器中毒 数据脱敏 加密算法 QPS 高并发 原子性 窗口 函数 R2DBC 双引擎 RAG HelixDB Order 频繁 Codis 主库 ZODB SSH 网络 Crash 代码 1 PG DBA 工具链 引擎 优化器 性能 List 类型 Pottery dbt 数据转换工具 意向锁 记录锁 模型 发件箱模式 事务同步 InfluxDB 传统数据库 向量化 UUIDv7 主键 网络故障 仪表盘 Redisson 锁芯 线程安全 INSERT COMPACT Undo Log LLM 订单 JOIN 连接数