PostgreSQL码农集散地

为什么磁盘RT会严重影响vacuum垃圾回收效率?

文中参考文档在github需点击阅读原文打开, 同时推荐2个学习环境: 

1、懒人Docker镜像, 已打包200+插件:《最好的PostgreSQL学习镜像》

2、有web浏览器就能用的云起实验室: 《免费体验PolarDB开源数据库》
3、PolarDB开源数据库内核、最佳实践等学习图谱:  https://www.aliyun.com/database/openpolardb/activity 
关注公众号, 持续发布PostgreSQL、PolarDB、DuckDB等相关文章. 

为什么磁盘RT会严重影响vacuum垃圾回收效率?

https://www.bilibili.com/video/BV15Z4y1d7ny/

声音在空气中的传播速率大约是340m/s, 两个房间里的人讲话感觉不到任何延迟, 你讲一句我马上就能听到并回话. 如果两个人隔了个山头, 你喊一句, 对方可能要隔几秒才能听到.

  • a: 你吃了吗?

  • b: 1s后: 吃了

  • a: 1s后: 吃了啥呀?

  • b: 1s后: 蛋炒饭

  • a: 1s后: 蛋炒饭加蛋了吗?

  • b: 1s后: 你说呢?

如果一次说很多内容再等对方一次回应, 马上就能节省很多时间.

  • a: 你吃了吗? 吃了啥?

  • b: 1s后: 吃了,蛋炒饭

  • a: 1s后: 蛋炒饭加蛋了吗?

  • b: 1s后: 你说呢?

对于没有上下文判断的交互, 批量操作带来的效果会更加明显. 例如本文接下来提到的vacuum .

磁盘RT其实是个比较大的问题, RT越大, 数据包传输的时延时就越大. 特别是网盘RT比本地盘大很多, 本地的机械盘也比本地SSD的RT大很多.

vacuum 过程:

  • autovacuum launcher监测系统表(由计数器更新)发现表的垃圾条数或比例达到回收垃圾的阈值, 触发垃圾回收,

  • 扫描数据页从磁盘将其读入shared buffer、或者已经在shared buffer中,

    • 每次读1个page, 而且还是同步IO, 也就是说信号至少一个来回才读到1个page, 你说慢不慢? RT越大这个问题越凸显. 特别是云盘

    • 除非这个表的每个page都在shared buffer中, 否则从磁盘读入是大概率事件.

  • 检查是否有可以被回收的垃圾tuple,

  • 如有则回收之, 从磁盘读上来或者已经存在shared buffer的clean page可能变成dirty page.

  • 也有可能之前这个page在shared buffer中就是dirty page.

  • 最后由bgwriter 调度将dirty page持久化(当然这个部分也可能是checkpoint、backend process干的).

    • 把dirty page写入磁盘又与存储RT有关. 但是它不由vacuum进程去操作.

    • 同时我们也必须要清楚vacuum可能会读入大量数据到shared buffer, 因此shared buffer刷脏虽然是异步的, 但是刷脏慢就会导致shared buffer的clean page或空闲page不足, 还是要等待刷脏, 所以对vacuum也是有性能影响的.

经过以上分析, 磁盘rt大, 给vacuum带来的影响包括:

  • 1、vacuum 从磁盘读取数据到shared buffer的过程慢, 从而影响vacuum速度.

  • 2、或者等待刷脏, 影响vacuum速度(社区版本为异步IO, 影响不大. polardb为DIO,影响大).

社区版本:
目前PG社区版本的内核对RT高带来的vacuum性能问题优化手段较少(除了vm来减少不需要的page scan), 而且单表的vacuum也没有支持并行, 目前的vacuum内核加速包括:

  • 同一个表如果有多个索引, 那么索引的vacuum可以并行. 这个优化针对的是索引多的表.

这也是为什么我建议把经常dml(update,delete,insert, 产生大量垃圾)的表用分区表代替(前提是垃圾回收赶不上产生垃圾的速度了), 因为不同分区做垃圾回收是能并行的.

还有什么优化方法呢?
操作系统有一项设置, 支持IO的prefetch, 就是说一次IO请求可以读取连续的N个扇区, 如果被多读上来的扇区是后续正好要读的扇区, 那么预度就能大大减少到块设备的IO次数(减少pagefault次数).
为了避免随机读也触发预读从而浪费磁盘带宽, linux 内核有个保护机制, 例如连续读n个扇区后才会触发预读. 例如3个扇区.

  • https://blog.csdn.net/weixin_34138255/article/details/89910895

  • https://www.hustyx.com/cpp/96

  • http://tech.hexun.com/2011-03-28/128268656.html

  • https://www.man7.org/linux/man-pages/man8/blockdev.8.html

  • https://blog.csdn.net/kunyus/article/details/104620057

blockdev --getmaxsect $device  // 块设备每次读请求最大扇区数
blockdev --setra xx $device // 设置块设备的预读扇区数, 0 表示关闭预读
blockdev --getra xx $device // 获得块设备的预读扇区数设置

blockdev --getpbsz $device // 获得块设备的物理块(扇区)大小
blockdev --getsize64 $device // 获得块设备的空间大小(字节单位)
blockdev --getsz $device // 获得块设备的空间大小(扇区单位)

操作系统blockdev setra预读的弊端:

  • 仅仅适合顺序IO, 离散IO预读无效果.

  • 块设备全局的设置, 会影响所有的IO行为.

    • 对于这样的情况建议使用8K的block_size, 否则就如上面所说, 很浪费存储带宽.

    • 虽然为了避免随机读也触发预读从而浪费磁盘带宽, linux 内核有个保护机制, 例如连续读n个扇区后才会触发预读. 例如3个扇区, 也就是12K, 当数据库block_size >= 16K时必然会触发预读, 导致浪费磁盘带宽(也包括网盘的网络带宽)从而瓶颈变成带宽瓶颈.

除此以外, 还可以想想有没有这样的系统接口, 能够支持一次请求多个离散的IO, 同样也能减少RT, 并且避免全局的设置.

  • writev readv

PolarDB:
内核层面支持预读功能, 新增函数:polar_bulk_read_buffer_common() 尝试读多个buffers。

  • polar_bulk_read_size可以调,session级的参数.

同时支持结合VM判断不需要vacuum的page ID进行skip, 避免不必要的预读.

设置polar_bulk_read_size=512KB(最大预读大小) 优化结果,对于单个vacuum进程:

  • CPU 使用率 从10%提升到 54%

  • vacuum处理速度,从 60MB/s 提升到 380 MB/s(RDS_PG是 400 MB/s 左右(本地盘, RT低)) polardb是网盘, RT高于本地盘.

PolarDB 预读对什么场景有加速效果? 共同特点: 都是几乎全表顺序扫描的场景、几乎都是访问全表、几乎都是同步IO.

  • heap scan、

  • vacuum、

  • create index

扩展知识1:
网盘(磁盘)RT高还会对哪些场景的性能影响较大, 问题如何解决?

  • checkpoint, bgwriter都是后台进程刷脏, 相对用户是异步(不影响用户请求). 但是在写多的场景, 可能刷脏处理不及时, 导致backend申请不到shared buffer, 需要等待刷脏腾出buffer, 那么一样会影响写性能.

    • 这种情况建议调高bgwriter刷脏调度频率(一个周期内刷出更多脏页). 怎么判断呢? 从统计信息里判断,backend process是不是在大量主动刷脏, 是的话就要调高bgwriter刷脏速度了.

  • backend process 申请buffer时遇到没有空闲page或clean page的情况, 需要等待dirty page刷脏.

  • SQL有未命中buffer的IO请求, 需要等待从磁盘读取数据.

  • 事务提交慢(需要持久化wal, 相当于同步IO). 需要等待写数据到磁盘.

  • 数据库崩溃恢复, 需要从wal和datafile中读取对应page进行恢复, 都是同步IO. (polardb采用异步恢复解决这个问题)

    • 《一起学PolarDB - 第7期 - 为什么数据库越大崩溃恢复越慢?》

参考:
《一起学PolarDB - 第8期 - 为什么存储RT指标对实例性能影响很大?》

扩展知识2:
与网盘RT高类似的, 网络RT高的影响场景和优化方式:

  • 《PostgreSQL 15 preview - libpq协议新增H消息, 通知DB server flush output buffer in pipeline mode.》

  • 《PostgreSQL 14 preview - libpq驱动 支持pipeline 模式, SQL请求支持异步化通信, 大幅度提升性能, 降低RT》

  • 优化方式:

    • 网络RT高通常采用批量的方式(增加单趟传递的信息量, 避免等待回包的时间)、

    • 加大连接并发、

    • 或者异步消息(不需要等待回包)来提高处理吞吐量.

本期问题1:
网络RT高对哪些场景的性能有非常明显的影响?

  • a. 高并发小事务

  • b. 计算量或者IO量很大的复杂慢SQL

  • c. 大量 简单insert 请求

  • d. 大量 简单select 请求

答案:

  • acd

解释:

  • 参考本文内容

本期问题2:
磁盘或存储的RT高对哪些场景的性能有非常明显的影响?

  • a. 计算很复杂大量耗费CPU的慢SQL

  • b. 大量IO都在shared buffer中命中的SQL

  • c. 大量IO都未在shared buffer以及page cache中命中的SQL

  • d. 小事务, 并且申请shared buffer时发现缺少clean page或free page, 需要驱出dirty page腾挪空间.

  • e. 高并发小事务

  • f. 数据库崩溃恢复

答案:

  • cdef

解释:

  • 参考本文内容

本期问题3:
为什么磁盘RT会严重影响vacuum垃圾回收效率?

  • a. vacuum 需要扫描表和索引, 磁盘IO的动作很频繁

  • b. vacuum 回收垃圾是同步IO, 每个页面修改完成后需要同步持久化到磁盘.

  • c. vacuum 时需要把磁盘中的page读到shared buffer, 如果shared buffer没有足够的clean page或free page, 会导致驱出dirty page的动作(PG 社区版本这个动作是异步IO), 产生额外IO.

  • d. vacuum 扫描时每次只读取1个page , 每两个请求之间都夹着一段RT时间.

答案:

  • acd

解释:

  • 参考本文内容

本期问题4:
社区版本对Vaccum有哪些优化?

  • a. 支持调大vacuum内存空间, 一次性存储一张表所有扫描到的dead tuple id, 避免索引垃圾回收时需要重复扫描索引

  • b. 单个表的多个索引可以开启多个work process执行索引的并行vacuum

  • c. 使用visibility map, 直接跳过干净的page

  • d. 单个表的多个数据文件支持多个work process并行执行vacuum

答案:

  • abc

解释:

  • 参考本文内容

本期问题5:
社区版本为什么目前无法优化vacuum heap scan的效率?

  • a. 完全clean的heap page在vacuum heap scan时也无法被跳过

  • b. 单个表的多个数据文件不支持开启多个work process并行执行vacuum

  • c. vacuum一次只扫描1个page, 每两个page之间都夹着1个RT的时间片段

  • d. vacuum 进程的逻辑复杂, 大量时间耗费在dead tuple的判断和回收, 无法跑满磁盘带宽.

答案:

  • bc

解释:

  • 参考本文内容

本期问题6:
对大量顺序IO的场景, 因为RT高引起的性能问题, 操作系统层面有什么高性价比的优化手段?

  • a. 增加内存, 避免访问磁盘

  • b. 更换磁盘, 使用本地SSD

  • c. 设置块设备预读参数, 一次IO请求返回多个连续扇区的内容.

  • d. 使用RDMA网络的云存储

答案:

  • c

解释:

  • 参考本文内容

本期问题7:
对大量顺序IO的场景, 因为RT高引起的性能问题, 操作系统层面开启块设备预读后有哪些弊端?

  • a. 开启块设备预读仅适合离散IO, 不适合顺序IO的场景.

  • b. 离散IO触发预读时会导致存储带宽浪费, 甚至导致存储带宽瓶颈

  • c. 全局设置, 影响所有的IO行为.

  • d. 当数据库block size大于预读保护机制阈值时, 所有的IO都会产生预读, 导致存储带宽严重浪费.

答案:

  • bcd

解释:

  • 参考本文内容

本期问题8:
polardb 在内核层面进行了什么优化来提升vacuum的性能, 使得vacuum接近本地盘性能

  • a. 支持通过VM来判断不需要进行垃圾回收的page, 跳过扫描减少IO, 同时可以避免无效的预读

  • b. 支持单一数据文件的并行vacuum

  • c. 支持内核层面的预读配置, 可以在会话层开启, 避免影响所有的IO请求case

  • d. 单个表的多个数据文件支持并行vacuum

答案:

  • ac

解释:

  • 参考本文内容

本期问题9:
PolarDB 预读对什么场景有加速效果?

  • a. 垃圾回收

  • b. 全表扫描

  • c. 索引扫描

  • d. 创建索引

答案:

  • abd

解释:

  • 参考本文内容

本期问题10:
PolarDB 预读的加速场景有什么共性?

  • a. 访问的数据量都偏少

  • b. 都是顺序IO

  • c. 几乎都要访问整张表的所有数据

  • d. 几乎都是同步IO

答案:

  • bcd

解释:

  • 参考本文内容

欢迎关注我的github (https://github.com/digoal/blog) , 学习数据库不迷路.  

近期正在写公开课材料, 未来将通过视频号推出, 欢迎关注视频号:

Image

文章中的参考文档请点击阅读原文获得.