pg报错attempted to delete invisible tuple

问题描述

postgresql数据库执行delete报错:attempted to delete invisible tuple,执行同样条件的select不报错

delete from lzltab1;
select count(*) from  lzltab1;

执行全表删除和全表查询的结果:

M=# delete from lzltab1;
ERROR:  55000: attempted to delete invisible tuple
LOCATION:  heap_delete, heapam.c:2500
Time: 511.050 ms
M=#   select count(*) from  lzltab1;
 count  
--------
 231187

delete找到了不可见的元组,select却是正常的 。
当时觉得很奇怪。pg的可见性通过元组的xmin,xmax,cid和快照的xmin,xmax,xip_list判断的,虽然delete元组的事务状态和时间会对可见性产生影响,但是表数据如果稳定(当前没有任何dml操作的话),随后的任何快照进去拍摄,都应该是一个稳定的可见集,它并不存在当前事务可见性判断,dml事务元组可见性判断也应该一致。也就是说这种场景下,select快照和delete快照不应该有这种差异。

问题分析

找到源码

注意报错信息heapam.c:2500
找到源码位置src/backend/access/heap/heapam.c
2500的行是空的,附近的代码如下

	/*
	 * Before locking the buffer, pin the visibility map page if it appears to
	 * be necessary.  Since we haven't got the lock yet, someone else might be
	 * in the middle of changing this, so we'll need to recheck after we have
	 * the lock.
	 */

	if (PageIsAllVisible(page))
		visibilitymap_pin(relation, block, &vmbuffer);

	LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);

从源码看,它在尝试获得vm上的锁,看来问题跟vm文件相关。

vm文件

什么是vm文件?
vm文件是为了减少vacuum扫描page时间的,如果一个page不需要vacuum那么它应该可以被vacuum跳过,这样可以大大减少vacuum寻找需要清理的page的时间,这是vm文件最初的目的。(有时候也会被index-only scan使用,不过我们这里不会涉及,我们用的是全表扫描)
vm文件包含两个信息:

  1. page中的元组是不是都是可见的。说明page中没有需要被vacuum的死元组
  2. page中的元组是不是都是冻结的。说明vacuum freeze不需要访问这个page

Fig. 6.2. How the VM is used.

vm会帮助vacuum寻找死元组,减少扫描的页的数量。比如上面这个图(interdb yyds!),第一个页面1st不包含死元组,那么vacuum就可以跳过这个页面。

找到vm文件
每个表都有Visibility Map (VM)(索引没有vm文件),单独存放在表文件的旁边,如果一个表的filenode为12345,那么vm文件应该是12345_vm
首先cd到data目录

M=# show data_directory;
    data_directory    
----------------------
 /pg/pg6666/data

通过database oid,表的oid可以找到文件存储的位置

=# select oid,datname from pg_database where datname='sdp';
-------+----------------------
  oid  |  datname
 17075 | sdp
=# select  oid,relname from pg_class where relname='lzltab1';     
-------+----------------------
 17362 | lzltab1

or

#  select pg_relation_filepath('lzltab1');
pg_relation_filepath 
----------------------
base/17075/17362

找到数据文件和vm

$ cd  /pg/pg6666/data/base/17075
$ ll 17362*
-rw------- 1 postgres postgres 86761472 Jun 15 17:43 17362
-rw------- 1 postgres postgres    40960 Jun  9 21:09 17362_fsm
-rw------- 1 postgres postgres     8192 Nov 14  2022 17362_vm

pg_visibility插件

pg_visibility提供通过检查vm文件输出页级别的可见性信息,而且可以检测vm是否损坏。因为vm存储了“page中的元组是不是都是可见的;page中的元组是不是都是冻结的”这些信息,pg_visibility可以检查出哪些页是all-frozen的,哪些是all-visible的。
pg_visibility插件使用参考:https://www.postgresql.org/docs/current/pgvisibility.html

会用到的pg_visibility中的function

pg_visibility_map_summary():显示vm中的all-visible页和all-frozen页
pg_check_frozen():有元组不是frozen的,但在它所在的页在vm中被标记为了all-frozen,如果该函数有返回,表示vm文件损坏。
pg_check_visible():有元组不是visible的,但在它所在的页在vm中被标记为了all-visible,如果该函数有返回,表示vm文件损坏。
pg_truncate_visibility_map():清理vm文件。清理vm后,当表首次执行vacuum时,会扫描表上的所有页并重建vm。

修复vm文件

检查vm是否损坏

M=# select pg_visibility_map_summary('lzltab1');
 pg_visibility_map_summary 
---------------------------
 (472,0)

all-visible 472页,all-frozen 0页

M=# select pg_check_frozen('lzltab1');
 pg_check_frozen 
-----------------
(0 rows)
M=#  select pg_check_visible('lzltab1');
 pg_check_visible 
------------------
 (6839,1)
 (6839,2)
 ...
  (7296,15)
(1423 rows)

pg_check_visible()有结果说明vm已经损坏了
然后用pg_truncate_visibility_map()执行清空vm

M=# select   pg_truncate_visibility_map('lzltab1');
 pg_truncate_visibility_map 
----------------------------

从磁盘上也能看出来vm被清空了

  ll 17362*
-rw------- 1 postgres postgres 86761472 Jun 27 10:39 17362
-rw------- 1 postgres postgres    40960 Jun  9 21:09 17362_fsm
-rw------- 1 postgres postgres        0 Jun 27 18:18 17362_vm

然后我们验证一下,vacuum表看下是否会生产vm文件,并检验vm文件是否没有损坏

M=# vacuum lzltab1;
VACUUM
Time: 3692.402 ms (00:03.692)
M=# \q
$ ll 17362*
-rw------- 1 postgres postgres 86761472 Jun 28 03:37 17362
-rw------- 1 postgres postgres    40960 Jun  9 21:09 17362_fsm
-rw------- 1 postgres postgres     8192 Jun 28 10:21 17362_vm

可以看到手动vacuum后vm正常生成

M=# select pg_check_visible('lzltab1');
 pg_check_visible 
------------------
(0 rows)

M=# select pg_check_frozen('lzltab1');
 pg_check_frozen 
-----------------
(0 rows)

check检查后没有输出,vm文件正常,修复完成。

最后再来跑下sql

# delete from lzltab1;
DELETE 229766

delete执行正常,问题解决

检查全库是否有vm损坏

虽然我们解决了一个vm文件损坏的问题,但是仍然需要全库检查是否有其他的vm损坏(前提是安装了extension pg_visibility)

SELECT oid::regclass AS relname
FROM pg_class
WHERE relkind IN ('r', 'm', 't') AND (
  EXISTS (SELECT * FROM pg_check_visible(oid))
  OR EXISTS (SELECT * FROM pg_check_frozen(oid)));

如果返回非空结果,说明有vm损坏了。就像上面的方法,用pg_truncate_visibility_map()清理vm,然后vacuum生成一个vm。
如果是9.6前的版本,因为没有pg_visibility插件,需要停库然后手动删除vm文件,再启动数据库,然后再vacuum生成一个vm。

为什么vm会损坏?

我们通过一步步分析,检查到是vm文件损坏了,但是vm为什么会损坏呢?

  1. pg数据库的bug。pg确实有些bug会导致vm损坏(参考wiki Visibility Map Problems),不过这些都是pg9.6.1以前的
  2. 操作系统和硬件问题

我们版本是pg13的,问题基本只能笼统地归于操作系统或者硬件问题。

为什么select没有问题,delete报错?

select全表正常,delete全表报错,看上去就很奇怪。问题的根因是vm文件的损坏。
就像前面说的,vm文件是为了加快vacuum效率的,我们虽然没有做vacuum,而vm文件总要更新的吧?dml每次都会去更新vm(至少要检查),而select不会改变vm状态的。所以本案例中select正常执行,delete在执行到vm的处理时报错。
我们的案例中,delete扫描了vm找到了all-visible的页,但是vm标记错了,这些页上仍然有不可见的元组,这里就对应了开头的报错attempted to delete invisible tuple。不可见的元组可能已经被delete了,再次跑delete当然会报错,这也违背了事务的可见性规则。
另外,如果是index-only-scan也会用到vm文件,所以也会影响select语句,不过这个案例是全表扫描的,所以select没有问题。

vm损坏导致index-only-scan出现错误的数据结果

前面在介绍vm的时候提到处理vacuum外,index-only-scan也会用到vm文件,虽然我们这个案例没有涉及index-only-scan,但本着研究透彻的原则,把问题搞清楚。

什么是index-only-scan

index-only-scan顾名思义就是仅索引扫描。在访问数据的时候不需要访问表,而只访问索引结构就能得到想要的结果。几乎所有的关系型数据库都有仅索引扫描,因为B+树索引结构保存了键值,如果查询只查了键值,那么仅索引扫描就是可能的。
但是,postgresql因为其事务实现跟其他数据库(Oracle、mysql)有很大的不同,它的仅索引扫描index-only-scan有一些特殊性。
postgresql通过元组头部的xmin、xmax等信息,校验元组和事务的可见性,而索引上没有这些信息,所以就导致pg的仅索引扫描必须访问数据块来检查可见性。这个时候vm的作用就体现了出来,因为vm文件中保存了all-visible,all-frozen的信息,这些被标记的页其实不需要校验元组可见性,因为他们已经被vm标记为可见了。
Fig. 7.7. How Index-Only Scans performs
再来一个interdb的图(interdb yyds!)。当一个sql查询在访问key是18和19两个元组时,key=18元组所在的页已经被vm标记为all-visible了,所以访问key=18的元组只需要访问索引页和vm文件;而key=19的元组所在的页没有被标记为all-visible,仅索引扫描还是要访问所在数据页获取元组可见信息。

index-only-scan查询出错误的结果

因为index-only-scan要访问vm,而vm损坏而保存了错误的信息,比如页中的元组本来不是所有都可见的(比如几个元组被delete了),但是页还是在vm中被标记为all-visible,导致index-only-scan没有进数据页检测元组可见性,直接返回了索引页上的本来是不可见的键值。
可以设置enable_indexonlyscan=off来关闭index-only-scan特性,保证数据的正确性;当然也可以像上面那样去修复vm文件,也许是更好的选择。

总结

虽然刚开始有些曲折,一看报错以为是事务可见性规则出现了问题,这个有问题的话问题就有点大了,但是实际上要简单的多。
我们从报错attempted to delete invisible tuple分析到源码,并定位到了是vm问题,再通过pg_visibility插件检测出vm corrupt并修复了vm文件,从而解决了delete报错,最后扩展了一下index-only-scan和vm。
总结一下文章的知识点:

  • pg_visibility插件可以读取、检测、清理vm文件
  • 如果没有vm信息的话,vacuum会生成新的vm
  • dml会读取/更新vm文件,select不会(非index-only-scan)
  • vm文件是为了提升vacuum的效率,有时候也会提升index-only-scan的效率
  • attempted to delete invisible tuple报错应该检查vm文件是否损坏
  • vm文件损坏会造成dml失败,也会造成index-only-scan查询出错误的结果

参考

https://www.postgresql.org/docs/13/pgvisibility.html
https://wiki.postgresql.org/wiki/Visibility_Map_Problems
https://www.interdb.jp/pg/pgsql06.html
https://www.interdb.jp/pg/pgsql07.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/32785.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Unity之IMGUI】—位置信息类和控件基类的封装

👨‍💻个人主页:元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 收录于专栏: ⭐…

从0开始,手写MySQL事务

说在前面:从0开始,手写MySQL的学习价值 尼恩曾经指导过的一个7年经验小伙,凭借精通Mysql, 搞定月薪40K。 从0开始,手写一个MySQL的学习价值在于: 可以深入地理解MySQL的内部机制和原理,Mysql可谓是面试的…

大数据Doris(五十):Export导出原理

文章目录 Export导出原理 一、原理 二、查询计划拆分 三、查询计划执行 Export导出原理 Doris Export、Select Into Outfile、MySQL dump三种方式数据导出。用户可以根据自己的需求导出数据。此外数据还可以以文件形式通过Borker备份到远端存储系统中,之后可以…

idea打的包字符集为GBK

1.最近对接一个打印机厂家的机器,他们对与打印数据要求字符集是UTF-8的,做完程序在自己idea上运行是是能够打印的,但是打包后,就不能够打印了。然后问了设备方是否是他们机器的原因,后面他们问了我这报错码&#xff0c…

【单片机】STM32单片机的各个定时器的定时中断程序,标准库

文章目录 定时器1_定时中断定时器2_定时中断定时器3_定时中断定时器4_定时中断定时器5_定时中断 高级定时器和普通定时器的区别(https://zhuanlan.zhihu.com/p/557896041): 定时器1_定时中断 TIM1是高级定时器,使用的时钟总线是R…

Methodot低代码开发教程——玩转表格增删改查分页

目录 1、背景介绍 2、连接数据源 2.1 新增数据源 2.2 填写数据源信息 3、表格数据的展示 3.1 新增查询,编写查询语句 3.2 使用表格组件 3.3 同步数据源与表格列名 4、表格的数据新增 4.1 新增查询,编写新增语句 4.2 表格配置新增一行&#xff0…

探索嵌入式开发领域:单片机、ARM、Android底层的紧密联系

作为一个曾编写ARM教程和参与Android产品开发的专家,我发现单片机、ARM、嵌入式开发和Android底层开发之间存在紧密的联系。对于那些希望在嵌入式开发领域发展的人来说,了解这些领域的知识至关重要。为了帮助你更好地学习这些内容,我总结了一…

Git指南 - 刚提的commit 怎么找不到了(游离分支)?

在有一次使用git时,我提交commit后,并未push,然后直接切到了当前分支的某个tag,最后我想切回来的时候,竟然找不到我刚才提交commit的节点了… 关联篇 Git指南 - 你该掌握的那些基础认知和首次配置Git指南 - 项目实战中…

青大数据结构【2018】【综合应用】

关键字: 二叉排序树、先序中序排列、平均查找长度ASL、快速排序、堆排序 (3) 采用堆排序; 因为快速排序在基本有序(逆序)的情况下,达到最坏的时间复杂度O(n2)。

如何支持研发对CSDN个性化推荐系统重构

目录 大地图工具构建数据治理保持发布重视测试小结引用 一个以内容服务为主的软件,它的推荐系统在数据侧对软件产生着举足轻重的作用。数据的三个方面决定了这个内容软件的档次。 数据的质量好坏数据和用户需求的相关性好坏数据的层次体系好坏 通常,我…

如何使用ChatGPT处理excel

用ChatGPT处理excel,我们需要用到的主要工具是ChatGPT和vba代码。 VBA代码是一种用于Excel,Word,PPT的Microsoft Office软件的编程语言。 可以让用户通过编写一些简单的指令和操作,实现自动化、自定义和数据处理等功能。 就像你编…

2023亚马逊云科技中国峰会之Serverless

序言 Amazon Web Services,是Amazon.com推出的一系列云计算服务。 它提供了一系列的基础设施服务、平台服务和软件服务,希望可以帮助我们更轻松地构建和管理基于云的应用程序。 今天来学习一下 Serverless 本文会介绍以下六个模块: 为什么会…

如何画时序图

10年产品经理教你3步画好UML时序图,轻松掌握流程分析利器【建议收藏】 - 知乎 转自知乎 上次介绍了活动图,这次分享 UML 中,另一种流程分析利器——时序图。 以前每次要分析流程,我都会用活动图。直到有一次,我面对…

使用VuePress生成静态网站并部署到github

目录 第一步 安装VuePress第二步 书写博客第二步 部署到github 第一步 安装VuePress VuePress是一个基于Vue驱动的静态网站生成器 相关资料 文档:https://v1.vuepress.vuejs.org/zh/github: https://github.com/vuejs/vuepressvuepress-deploy: https://github.c…

什么是RabbitMQ?

RabbitMQ是一个由erlang开发的消息队列。消息队列用于应用间的异步协作。 2.RabbitMQ的组件 Message:由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key、priority、delivery-mode&#xff…

NoSQL之 Redis 配置与优化

目录 一、关系型数据库与非关系型数据库1.1 关系型数据库:1.2 非关系型数据库1.3 关系型数据库和非关系数据库的区别1.3.1 数据存储方式不同1.3.2 扩展方式不同1.3.3 对事务性的支持不同 1.4 非关系型数据库的产生背景1.5 总结 二、Redis介绍三、 Redis 的优点四、 …

【金融量化】如何筛选基金?

基金的评价与筛选 1 筛选步骤 1.1 股票型基金 (1)构建备选池 优先考虑股票配置较为稳定的基金,这样才能预估基金未来一段时间的表现,及其对基准股票指数的跟踪情况。因此,首先应该剔除那些仓位变化较大、本身在进行…

Tableau 和 Qlikview哪个更好用?

Tableau 和 Qlikview 是市场上用于分析数据的两种重要的 BI 工具,并配备了广泛的可视化效果。本文将全面概述每个工具,以及基于业务、技术和可视化三个主要类别的基本差异。每个公司都希望用最好的BI工具来处理大量数据。在这篇文章中,我们将…

基于OpenCV-车辆检测项目(简易版)

车辆检测 1.项目介绍2. 读取一段视频3.通过形态学处理识别车辆4.描画轮廓5. 车辆计数并显示 本项目使用的视频地址链接 1.项目介绍 对一个视频进行车辆数量的检测,用到的知识有视频的读取,滤波器,形态学,添加直线、文本&#xff…

实战-基于Jenkins+K8s构建DevOps平台(九)

实验步骤如下: 第一部分:安装持久化存储nfs 1、在k8s-master和k8s-node1上安装nfs服务 [rootk8s-master ~]# yum install nfs-utils -y [rootk8s-master ~]# systemctl start nfs [rootk8s-master ~]# systemctl enable nfs [rootk8s-node1 ~]# yum …