PostgreSQL HOT_STANDBY_FEEDBACK

使用 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

猜你喜欢

转载自yq.aliyun.com/articles/672039