PostgreSQL HOT_STANDBY_FEEDBACK

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: 使用 PostgreSQL 流复制搭建备库时,可以为备库指定一个参数 `hot_standby_feedback`,本文介绍引入该参数的原因以及它的工作原理。 ## VACUUM PostgreSQL 中的数据是用堆表组织的,为支持 MVCC(Multi-Version Concurrenc.

使用 PostgreSQL 流复制搭建备库时,可以为备库指定一个参数 hot_standby_feedback,本文介绍引入该参数的原因以及它的工作原理。

VACUUM

PostgreSQL 中的数据是用堆表组织的,为支持 MVCC(Multi-Version Concurrency Control),堆表中通过链表的形式存储了元组(tuple)的多个版本,当没有任何事务和查询需要某些版本的元组时,这些版本的元组可以称之为死元组(dead tuple)。可以想象,在频繁的修改操作以后,如果不对死元组进行任何处理,数据页的膨胀会非常严重。为解决数据膨胀的问题,PostgreSQL 中引入了 VACUUM 操作,VACUUM 的作用就是将数据页中的 死元组 进行重用,甚至对整张表进行整理,VACUUM 的个中细节后续作文细述,在此不展开。

如下图, 连接 1 在执行查询时会获取一个快照,这个快照可以确保即便在这之后有其他并发事务对该表进行修改,也可以看到一致的数据。

随后,另一个事务对数据做了删除操作。值得注意的是,PostgreSQL 中的 DELETE 操作并不会将数据真正从数据页中移除,而是将原来的元组进行标记,这样对于连接 1 的查询而言,仍然可以看到被连接 2 删除的元组。

连接 2 的事务提交之后,执行一次 VACUUM,并不会将刚刚已经被连接 2 删除的元组清理掉,因为连接 1 还需要对这些元组进行查询。连接 1 的事务提交之后,再次执行 VACUUM,由于被删除的元组已经不被任何事务和查询使用到,此时可以将删除的元组回收并重用。

image.png

以上简单介绍单机(没有任何备库)下的 VACUUM 机制,原理还是很清晰的,只要标记删除的元组没有被任何事务使用即可被清理并重用。

复制冲突

搭建备库之后,如果备库有查询,主库在 VACUUM 时如何感知备库正在执行的事务?主库如果不感知备库的状态而将标记删除的元组清理掉会怎样?

以下图为例,SLAVE 有一个长事务或者长查询正在执行,此时,MASTER 执行 UPDATEVACUUM,由于 MASTER 上已经不存在使用被更新元组的事务,VACUUM 会将这些元组清理掉,当 SLAVE 回放到 VACUUM 对应的日志时,检测到当前 VACUUM 清理的元组仍然有事务在使用,则认为有冲突,在等待 max_standby_streaming_delay(默认30s) 后,若事务仍然没有执行完,则中断 SLAVE 上的连接,并打印异常信息,如下:

FATAL:  terminating connection due to conflict with recovery  
DETAIL:  User query might have needed to see row versions that must be removed.  
HINT:  In a moment you should be able to reconnect to the database and repeat your command.  
server closed the connection unexpectedly  
        This probably means the server terminated abnormally  
        before or while processing the request.  
The connection to the server was lost. Attempting reset: Succeeded.  

image.png

HOT_STANDBY_FEEDBACK

说了这么多,终于主角要登场了。主备情况下,可以通过设置 HOT_STANDBY_FEEDBACK 解决因 MASTER 执行 VACUUM 引起的复制冲突。

HOT_STANDBY_FEEDBACK 的原理很简单,就是将 SLAVE 的最小活跃事务 ID 定期告知 MASTER,使得 MASTER 在执行 VACUUM 时对这些事务还需要的数据手下留情。

设置 HOT_STANDBY_FEEDBACK 的好处是可以减少 SLAVE 执行查询时复制冲突的可能,但也有其弊端,即会使 MASTER 延迟回收,从而导致数据膨胀;极端情况下,如果 SLAVE 有一个很长的事务,且涉及的表上 DML 操作又很频繁,则表的膨胀是不容小觑的。

代码实现

本小节简单介绍一些有关 HOT_STANDBY_FEEDBACK 的实现,方便加深理解,对实现细节不感兴趣的朋友可直接跳过(跳过就没有了)。

发送 HOT STANDBY FEEDBACK

SLAVE 的 walreceiver 进程发送 HOT STANDBY FEEDBACK 的入口函数是 XLogWalRcvSendHSFeedback,获取最小活跃事务 ID 的逻辑如下:

if (hot_standby_feedback)
    {
        TransactionId slot_xmin;

        /*
         * Usually GetOldestXmin() would include both global replication slot
         * xmin and catalog_xmin in its calculations, but we want to derive
         * separate values for each of those. So we ask for an xmin that
         * excludes the catalog_xmin.
         */
        xmin = GetOldestXmin(NULL,
                             PROCARRAY_FLAGS_DEFAULT | PROCARRAY_SLOTS_XMIN);

        ProcArrayGetReplicationSlotXmin(&slot_xmin, &catalog_xmin);

        if (TransactionIdIsValid(slot_xmin) &&
            TransactionIdPrecedes(slot_xmin, xmin))
            xmin = slot_xmin;
    }

获取事务的 epoch 计数器的逻辑如下:

    /*
     * Get epoch and adjust if nextXid and oldestXmin are different sides of
     * the epoch boundary.
     */
    GetNextXidAndEpoch(&nextXid, &xmin_epoch);
    catalog_xmin_epoch = xmin_epoch;
    if (nextXid < xmin)
        xmin_epoch--;
    if (nextXid < catalog_xmin)
        catalog_xmin_epoch--;
epoch 计数器介绍
PostgreSQL 中事务 ID 是用 32 位无符号整型表示的,可以表示的最大事务 ID 是 40 亿,一旦超过该值,就会溢出,后续事务 ID 将从 0 开始,按照 PostgreSQL 的 MVCC 实现机制,之前的事务就可以看到这个新事务创建的元组,这是违反事务可见性的,即事务 ID 的回卷(wrap around)问题,具体可以参考 PgSQL · 特性分析 · 事务ID回卷问题

为解决事务 ID 回卷问题,引入 epoch 计数器,可以简单理解为 epoch 与 事务 ID 共同构成一个 64 位长整型来表示事务 ID,epoch 作为高 32 位,事务 ID 作为低 32 位,通过这样一个64为长整型表示事务 ID,避免事务 ID 回卷问题。关于 epoch 的具体实现原理在此不展开。

SLAVE 发送的 Feedback 的消息体如下:

image.png

处理 HOT STANDBY FEEDBACK

MASTER 的 walsendeer 进程接收并处理 HOT STANDBY FEEDBACK 消息的函数入口是 ProcessStandbyHSFeedbackMessage,解析消息之后,对消息内容做必要的合理性检测,随后调用如下流程设置 MyPgXact->xmin

/*
     * Set the WalSender's xmin equal to the standby's requested xmin, so that
     * the xmin will be taken into account by GetOldestXmin.  This will hold
     * back the removal of dead rows and thereby prevent the generation of
     * cleanup conflicts on the standby server.
     *
     * There is a small window for a race condition here: although we just
     * checked that feedbackXmin precedes nextXid, the nextXid could have
     * gotten advanced between our fetching it and applying the xmin below,
     * perhaps far enough to make feedbackXmin wrap around.  In that case the
     * xmin we set here would be "in the future" and have no effect.  No point
     * in worrying about this since it's too late to save the desired data
     * anyway.  Assuming that the standby sends us an increasing sequence of
     * xmins, this could only happen during the first reply cycle, else our
     * own xmin would prevent nextXid from advancing so far.
     *
     * We don't bother taking the ProcArrayLock here.  Setting the xmin field
     * is assumed atomic, and there's no real need to prevent a concurrent
     * GetOldestXmin.  (If we're moving our xmin forward, this is obviously
     * safe, and if we're moving it backwards, well, the data is at risk
     * already since a VACUUM could have just finished calling GetOldestXmin.)
     *
     * If we're using a replication slot we reserve the xmin via that,
     * otherwise via the walsender's PGXACT entry. We can only track the
     * catalog xmin separately when using a slot, so we store the least of the
     * two provided when not using a slot.
     *
     * XXX: It might make sense to generalize the ephemeral slot concept and
     * always use the slot mechanism to handle the feedback xmin.
     */
    if (MyReplicationSlot != NULL)    /* XXX: persistency configurable? */
        PhysicalReplicationSlotNewXmin(feedbackXmin, feedbackCatalogXmin);
    else
    {
        if (TransactionIdIsNormal(feedbackCatalogXmin)
            && TransactionIdPrecedes(feedbackCatalogXmin, feedbackXmin))
            MyPgXact->xmin = feedbackCatalogXmin;
        else
            MyPgXact->xmin = feedbackXmin;
    }

总结

以上简单介绍了主备环境下,MASTER 和 SLAVE 通过 HOT STANDBY FEEDBACK 机制避免复制冲突的原理和代码实现。需要注意的是,开启 HOT STANDBY FEEDBACK 虽然可以增加 SLAVE 的可用性,避免连接被频繁中断,但同时也会造成 MASTER 回收不及时,导致表膨胀,进而使得 MASTER 出现 IO 抖动。

最后,不妨思考一个问题,如果有级联的多个 SLAVE,每个 SLAVE 是如何影响其 MASTER 的 VACUUM 回收的呢?

Reference

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
存储 关系型数据库 数据库
How to Optimize PostgreSQL Logical Replication
How to Optimize PostgreSQL Logical Replication
76 0
How to Optimize PostgreSQL Logical Replication
|
SQL 测试技术 数据库
database replay基础学习
在11g中,database replay是一个很重要的新特性,按照这个特性的说法,可以完整地捕获数据库的负载信息,便于在需要的时候随时重放。 使用这种方法,可以以二进制文件格式捕获 SQL 级以下的所有数据库活动,然后在同一数据库或不同数据库内进行重放。
1081 0
|
SQL 存储 算法
CockroachDB: The Resilient Geo-Distributed SQL Database
一直以来对CockroachDB(CRDB for short)的设计和实现很感兴趣,最近抽时间研究了下,发现其在技术上还是领先了同类NewSQL产品不少的,个人感觉应该是目前最为先进的类Spanner分布式数据库系统,因此这篇文章会尽可能详细的讨论下其系统的多个方面,重点是事务和一致性相关。 paper中针对的是v.19.2.2版本,不过官方文档中是基于最新的v.21.1.7,两者在描述上有一些冲突的地方,而官方文档中会更为详尽些,因此本文的很多介绍将尽量将paper与官方reference结合,并以reference为准。
503 0
CockroachDB: The Resilient Geo-Distributed SQL Database
|
关系型数据库 数据库 SQL
PostgreSQL Logical Replication
限制及特性 1、只支持普通表生效,不支持序列、视图、物化视图、外部表、分区表和大对象 2、只支持普通表的DML(INSERT、UPDATE、DELETE)操作,不支持truncate、DDL操作 3、需要同步的表必须设置REPLICA IDENTITY 不能为noting(默认值是default).
8757 0
|
关系型数据库 PostgreSQL 流计算
|
SQL 关系型数据库 数据库
|
SQL 关系型数据库 数据库
|
SQL 关系型数据库 数据库