Postgresql - 源码 - WAL Sender process

介绍:

WAL sender process 是9.0的新功能。需要从主服务器发送XLOG到单个recipient(备机)。注意可以同时存在多个walsender进程。当备用服务器的walreceiver连接到主服务器并请求XLOG streaming replication的时候,由postmaster启动walsender process。

walsender类似于常规后端,连接和walsender process是一对一的关系,但它是一组特殊的复制模式命令,而不是处理SQL查询。START_REPLICATION 命令开始向客户端发送WAL。当流传输时,walsender保持从磁盘读取XLOG记录,并通过COPY 协议将他们发送到备用服务器,知道两端通过退出COPY模式结束复制或直接关闭连接。

SIGTERM是正常终止,它只是walsender在下一个适当的时候关闭连接并正常退出。SIGQUIT是紧急终止,就像其他后端一样,walsender将简单的终止并退出。连接关闭和FATAL error不会被看作崩溃,而是近似正常的终止。walsender将快速退出而不再发送XLOG记录。

如果服务器关闭,检查指针(checkpointer)在所有常规后端退出之后向我们发送 PROCSIG_WALSND_INIT_STOPPING。如果后端空闲或在运行SQL,将导致后端关闭。如果正在进行罗支付至,则所有现有的WAL记录都经过处理,然后关闭。否则会导致walsender切换到停止状态。在停止状态下,walsender将拒绝任何复制命令。一旦所有walsenders被确认停止,检查指针开始关闭检查点。当关闭检查点结束时,postmaster给我们发送SIGUSR2。指示walsender发送任何未完成的WAL,包括关闭检查点记录,等待它被复制到备机,然后退出。

函数调用关系:

PostgresMain ( src/backend/tcop/postgres.c )

--> exec_replication_command ( src/backend/replication/walsender.c )

--> StartReplication 或 StartLogicalReplication ( src/backend/replication/walsender.c )

static void

StartReplication(StartReplicationCmd *cmd)

{

    ......

    /* 我们假设我们在WAR中记录了足够的日志传输信息,因为这是在PostmasterMain()中检查的。*/

    if (cmd->slotname)

    {

        ......

    }

    /* 选择时间线。如果它是由客户端显式给出的,那么使用。否则使用上次保存在ThisTimeLineID中的重放记录的时间线。*/

    if (am_cascading_walsender)

    {

        /* this also updates ThisTimeLineID */

        FlushPtr = GetStandbyFlushRecPtr();

    }

    else

        FlushPtr = GetFlushRecPtr();

    if (cmd->timeline != 0)

    {

        XLogRecPtr  switchpoint;

        sendTimeLine = cmd->timeline;

        if (sendTimeLine == ThisTimeLineID)

        {

            sendTimeLineIsHistoric = false;

            sendTimeLineValidUpto = InvalidXLogRecPtr;

        }

        else

        {

            List     *timeLineHistory;

            sendTimeLineIsHistoric = true;

            /* 检查客户端请求的时间线是否存在,请求的起始位置在该时间线上。 */

            timeLineHistory = readTimeLineHistory(ThisTimeLineID);

            switchpoint = tliSwitchPoint(cmd->timeline, timeLineHistory,

                                         &sendTimeLineNextTLI);

            list_free_deep(timeLineHistory);

            /* 在历史中找到请求的时间线。检查请求的起始点是否在我们历史上的时间线上。

* 这是故意的。我们只检查在切换点之前没有fork 好请求的时间线。我们不检查我们在要求的起始点之前切换。这是因为客户机可以合法地请求从包含交换点的WAL段的开头开始复制,但是在新的时间线上,这样就不会以部分段结束。如果你要求太老的起点,你会得到一个错误,当我们找不到请求的WAL段在pg_wal。 */

            if (!XLogRecPtrIsInvalid(switchpoint) &&

                switchpoint < cmd->startpoint)

            {

                ereport(ERROR,

                        (errmsg("requested starting point %X/%X on timeline %u is not in this server's history",

                                (uint32) (cmd->startpoint >> 32),

                                (uint32) (cmd->startpoint),

                                cmd->timeline),

                         errdetail("This server's history forked from timeline %u at %X/%X.",

                                 cmd->timeline,

                                 (uint32) (switchpoint >> 32),

                                 (uint32) (switchpoint))));

            }

            sendTimeLineValidUpto = switchpoint;

        }

    }

    else

    {

        sendTimeLine = ThisTimeLineID;

        sendTimeLineValidUpto = InvalidXLogRecPtr;

        sendTimeLineIsHistoric = false;

    }

    streamingDoneSending = streamingDoneReceiving = false;

    /* 如果没有内容需要stream,不要进入复制模式 */

    if (!sendTimeLineIsHistoric || cmd->startpoint < sendTimeLineValidUpto)

    {

        /* 当我们第一次启动复制时,备机将跟随在主服务器后面。对于一些应用程序,例如同步复制,对于这个初始catchup模式有一个清晰的状态很重要,因此当我们稍后改变流状态时可以触发动作。我们可能会呆在这个状态很长一段时间,这正是我们想要监视我们是否还在同步的原因。 */

        WalSndSetState(WALSNDSTATE_CATCHUP);

        /* 发送 CopyBothResponse 信息, 并且开始 streaming */

        pq_beginmessage(&buf, 'W');

        pq_sendbyte(&buf, 0);

        pq_sendint16(&buf, 0);

        pq_endmessage(&buf);

        pq_flush();

        /* 不允许请求一个在WAL中的未来的点去stream,WAL还没有被刷新到磁盘。 */

        if (FlushPtr < cmd->startpoint)

        {

            ereport(ERROR,

                    (errmsg("requested starting point %X/%X is ahead of the WAL flush position of this server %X/%X",

                            (uint32) (cmd->startpoint >> 32),

                            (uint32) (cmd->startpoint),

                            (uint32) (FlushPtr >> 32),

                            (uint32) (FlushPtr))));

        }

        /* 从请求点开始streaming */

        sentPtr = cmd->startpoint;

        /* 初始化共享内存状态 */

        SpinLockAcquire(&MyWalSnd->mutex);

        MyWalSnd->sentPtr = sentPtr;

        SpinLockRelease(&MyWalSnd->mutex);

        SyncRepInitConfig();

        /* walsender的主循环 */

        replication_active = true;

        WalSndLoop(XLogSendPhysical);

        replication_active = false;

        if (got_STOPPING)

            proc_exit(0);

        WalSndSetState(WALSNDSTATE_STARTUP);

        Assert(streamingDoneSending && streamingDoneReceiving);

    }

    if (cmd->slotname)

        ReplicationSlotRelease();

    /* 复制完成了。发送指示下一个时间线的单行结果集。 */

    if (sendTimeLineIsHistoric)

    {

        char        startpos_str[8 + 1 + 8 + 1];

        DestReceiver *dest;

        TupOutputState *tstate;

        TupleDesc   tupdesc;

        Datum       values[2];

        bool        nulls[2];

        snprintf(startpos_str, sizeof(startpos_str), "%X/%X",

                 (uint32) (sendTimeLineValidUpto >> 32),

                 (uint32) sendTimeLineValidUpto);

        dest = CreateDestReceiver(DestRemoteSimple);

        MemSet(nulls, false, sizeof(nulls));

        /* 需要一个表示两个列的元组描述符。int8看起来是一个令人惊讶的数据类型,但是理论上int4不够宽,因为TimeLineID是无符号的。*/

        tupdesc = CreateTemplateTupleDesc(2, false);

        TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "next_tli",

                                 INT8OID, -1, 0);

        TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "next_tli_startpos",

                                 TEXTOID, -1, 0);

        /* prepare for projection of tuple */

        tstate = begin_tup_output_tupdesc(dest, tupdesc);

        values[0] = Int64GetDatum((int64) sendTimeLineNextTLI);

        values[1] = CStringGetTextDatum(startpos_str);

        /* send it to dest */

        do_tup_output(tstate, values, nulls);

        end_tup_output(tstate);

    }

    /* 发送 CommandComplete (完成)消息 */

    pq_puttextmessage('C', "START_STREAMING");

}

******************************************************************************************

下面我们看一下主要的walsndloop代码。

这是walsender 的主循环。

/* walsender process 的主循环,将WAL复制到复制消息上。*/

static void

WalSndLoop(WalSndSendDataCallback send_data)

{

    /* 初始化最后一个答复时间戳。这样就可以实现 timeout 处理。 */

    last_reply_timestamp = GetCurrentTimestamp();

    waiting_for_ping_response = false;

    /* 循环,直到我们到达这个时间线的末端,或者客户端请求停止streaming。 */

    for (;;)

    {

        TimestampTz now;

        /* 当postmaster进程死掉,将紧急处理。避免对所有postmaster的子进程进行手工处理。 */

        if (!PostmasterIsAlive())

            exit(1);

        /* 清除任何 */

        ResetLatch(MyLatch);

        CHECK_FOR_INTERRUPTS();

        /* 处理最近收到的任何请求或信号 */

        if (ConfigReloadPending)

        {

            ConfigReloadPending = false;

            ProcessConfigFile(PGC_SIGHUP);

            SyncRepInitConfig();

        }

        /* 检查客户的输入 */

        ProcessRepliesIfAny();

        /* 如果我们从客户机接收到CopyDone,我们自己发送CopyDone,并且输出缓冲区是空的,那么就该退出streaming。 */

        if (streamingDoneReceiving && streamingDoneSending &&

            !pq_is_send_pending())

            break;

        /* 如果在输出缓冲区中没有任何挂起的数据,尝试发送更多。如果有的话,我们不必再调用 send_data 数据,直到我们刷新它…但我们最好假设我们没有赶上。 */

        if (!pq_is_send_pending())

            send_data();

        else

            WalSndCaughtUp = false;

        /* 尝试将未决输出刷新到客户端 */

        if (pq_flush_if_writable() != 0)

            WalSndShutdown();

        /* 如果现在没有什么东西需要发送 ... */

        if (WalSndCaughtUp && !pq_is_send_pending())

        {

            /* 如果我们处于追赶状态,移动到streaming。对于用户来说,这是一个需要了解的重要状态更改,因为在此之前,如果主服务器死机,并且需要向备用服务器进行故障转移,则可能会发生数据丢失。状态更改对于同步复制也很重要,因为在该点开始等待的提交可能等待一段时间。 */

            if (MyWalSnd->state == WALSNDSTATE_CATCHUP)

            {

                ereport(DEBUG1,

                        (errmsg("\"%s\" has now caught up with upstream server",

                                application_name)));

                WalSndSetState(WALSNDSTATE_STREAMING);

            }

            /* 当SIGUSR2到达,我们将任何未完成的日志发送到关机检查点记录(即最新记录),等待它们复制到待机状态,然后退出。这可能是一个正常的终止在关机,或推广,walsender 不确定是哪个。 */

            if (got_SIGUSR2)

                WalSndDone(send_data);

        }

        now = GetCurrentTimestamp();

        /* 检查 replication 超时. */

        WalSndCheckTimeOut(now);

        /* 如果时间到了,发送keepalive */

        WalSndKeepaliveIfNecessary(now);

        /* 如果不敢上,不会阻塞,除非有未发送的数据等待,在这种情况下,我们最好阻塞,直到套接字写就绪为止。这个测试只适用于 send_data 回调处理了可用数据的子集,但是 pq_flush_if_writable 刷新了所有数据的情况——我们应该立即尝试发送更多数据。 */

        if ((WalSndCaughtUp && !streamingDoneSending) || pq_is_send_pending())

        {

            long        sleeptime;

            int         wakeEvents;

            wakeEvents = WL_LATCH_SET | WL_POSTMASTER_DEATH | WL_TIMEOUT |

                WL_SOCKET_READABLE;

            sleeptime = WalSndComputeSleeptime(now);

            if (pq_is_send_pending())

                wakeEvents |= WL_SOCKET_WRITEABLE;

            /* Sleep直到某事发生或 timeout */

            WaitLatchOrSocket(MyLatch, wakeEvents,

                             MyProcPort->sock, sleeptime,

                             WAIT_EVENT_WAL_SENDER_MAIN);

        }

    }

    return;

}

******************************************************************************************

/* 将WAL以其正常的物理/存储形式发送出去。

* 读取已经刷新到磁盘但尚未发送到客户端的WAL的MAX_SEND_SIZE字节,并将其缓冲到libpq输出缓冲区中。

* 如果没有剩余的未发送WAL,WalSndCaughtUp 设置为true,否则 WalSndCaughtUp 设置为false。*/

static void

XLogSendPhysical(void)

{

    XLogRecPtr  SendRqstPtr;

    XLogRecPtr  startptr;

    XLogRecPtr  endptr;

    Size        nbytes;

    /* 如果请求,将WAL sender 切换到stopping状态. */

    if (got_STOPPING)

        WalSndSetState(WALSNDSTATE_STOPPING);

    if (streamingDoneSending)

    {

        WalSndCaughtUp = true;

        return;

    }

    /* Figure out how far we can safely send the WAL. */

    if (sendTimeLineIsHistoric)

    {

        /* 将旧的时间线 streaming 到这个服务器的历史中,但不是我们当前插入或重放的那个时间线。它可以 streaming 到我们关掉时间线的那一点。 */

        SendRqstPtr = sendTimeLineValidUpto;

    }

    else if (am_cascading_walsender)

    {

        /* 在备机中 streaming 最新的时间线。

         * 尝试发送所有已经重放的WAL,这样我们就知道它是有效的。如果我们通过流复制接收WAL,发送任何已接收但未重放的WAL也可以。

         * 我们正在恢复的时间线可以改变,或者我们可以被提升。在任何一种情况下,当前的时间线都是历史性的。我们需要检测这一点,这样我们就不会试图流过我们切换到另一个时间线的那一点。我们在计算 FlushPtr 之后检查升级或时间线切换,以避免出现竞争条件:如果时间线在我们检查它仍然是当前之后就变得具有历史意义,那么仍然可以把它streaming 到 FlushPtr r上,而FlushPtr是在它变得具有历史意义之前计算的。 */

        bool        becameHistoric = false;

        SendRqstPtr = GetStandbyFlushRecPtr();

        if (!RecoveryInProgress())

        {

            /* RecoveryInProgress() 更新 ThisTimeLineID 成为当前时间线 */

            am_cascading_walsender = false;

            becameHistoric = true;

        }

        else

        {

            /* 仍然是级联备机。但我们是否仍在恢复时间线?ThisTimeLineID通过GetStandbyFlushRecPtr() 调用被更新 */

            if (sendTimeLine != ThisTimeLineID)

                becameHistoric = true;

        }

        if (becameHistoric)

        {

            /* 我们发送的时间线已经成为历史。读取新的时间线的时间线历史文件,以查看我们从发送的时间线中准确地分叉的位置。 */

            List     *history;

            history = readTimeLineHistory(ThisTimeLineID);

            sendTimeLineValidUpto = tliSwitchPoint(sendTimeLine, history, &sendTimeLineNextTLI);

            Assert(sendTimeLine < sendTimeLineNextTLI);

            list_free_deep(history);

            sendTimeLineIsHistoric = true;

            SendRqstPtr = sendTimeLineValidUpto;

        }

    }

    else

    {

        /* 将当前时间线传输到主机上。

         * 尝试发送所有已经写好的数据,并将其同步到磁盘。对于当前实现的 XLogRead() ,我们不能再做什么了。在任何情况下,发送不安全的WAL到主服务器上的磁盘是不安全的:如果主服务器随后崩溃并重新启动,备用服务器一定没有应用任何在主服务器上丢失的WAL。 */

        SendRqstPtr = GetFlushRecPtr();

    }

    /* 记录当前的系统时间作为写这个WAL位置用于滞后跟踪的近似时间。

     * 理论上,无论何时刷新WAL,我们都可以让XLogFlush() 在 shmem 中记录一个时间,并且当我们调用上面的GetFlushRecPtr() 时(同样对于级联备机),我们可以获得该时间以及LSN,但是与将任何新代码放入热WAL路径相比,它似乎足够好抓住这里的时间。我们应该在XLogFlush()运行WalSndWakeupProcessRequ.()之后达到这个目的,尽管这可能需要一些时间,但是我们读取WAL刷新指针,并在这里非常接近地花费时间,以便如果它仍在移动,我们将得到一个稍后的位置。

     * 因为LagTrackerWriter 在LSN尚未升级时忽略了示例,因此这为这个LSN提供了WAL刷新时间的廉价近似值

     * 注意,LSN并不一定是包含在本消息中的数据的LSN;它是WAL的末尾,它可能更进一步。所有滞后跟踪机器关心的是找出任意的LSN最终何时被报告为写入、刷新和应用,以便它可以测量经过的时间。 */

    LagTrackerWrite(SendRqstPtr, GetCurrentTimestamp());

    /* 如果这是一个历史的时间线,我们已经到达了下一个时间线的转折点,停止streaming。

     * 注意:我们可能已经发送了WAL > sendTimeLineValidUpto 。启动过程通常会在启动之前重放从主服务器接收的所有WAL,但是如果WAL streaming 终止于WAL页的边界,则时间线的有效部分可能终止于WAL记录的中间。我们可能已经将部分WAL记录的前半部分发送到级联备用,因此sentPtr > sendTimeLineValidUpto。没关系,级联待机也不能重放部分WAL记录,所以它仍然可以遵循我们的时间线开关。*/

    if (sendTimeLineIsHistoric && sendTimeLineValidUpto <= sentPtr)

    {

        /* close the current file. */

        if (sendFile >= 0)

            close(sendFile);

        sendFile = -1;

        /* Send CopyDone */

        pq_putmessage_noblock('c', NULL, 0);

        streamingDoneSending = true;

        WalSndCaughtUp = true;

        elog(DEBUG1, "walsender reached end of timeline at %X/%X (sent up to %X/%X)",

             (uint32) (sendTimeLineValidUpto >> 32), (uint32) sendTimeLineValidUpto,

             (uint32) (sentPtr >> 32), (uint32) sentPtr);

        return;

    }

    /* Do we have any work to do? */

    Assert(sentPtr <= SendRqstPtr);

    if (SendRqstPtr <= sentPtr)

    {

        WalSndCaughtUp = true;

        return;

    }

    /* 计算一个消息发送多少。如果发送的字节不超过MAX_SEND_SIZE 字节,则发送所有内容。否则发送MAX_SEND_SIZE 大小字节,但返回到日志文件或页边界。

     * Figure out how much to send in one message. If there's no more than

     * MAX_SEND_SIZE bytes to send, send everything. Otherwise send

     * MAX_SEND_SIZE bytes, but round back to logfile or page boundary.

     * 舍入不仅仅是出于性能原因。Walreceiver 依赖于我们从不分割WAL记录两个消息的事实。由于长的WAL记录在页面边界被分割成连续记录,所以页面边界始终是安全的截止点。我们还假设 SendRqstPtr 从来没有指向WAL记录的中间。 */

    startptr = sentPtr;

    endptr = startptr;

    endptr += MAX_SEND_SIZE;

    /* 如果我们超越了 SendRqstPtr, 回退 */

    if (SendRqstPtr <= endptr)

    {

        endptr = SendRqstPtr;

        if (sendTimeLineIsHistoric)

            WalSndCaughtUp = false;

        else

            WalSndCaughtUp = true;

    }

    else

    {

        /* round down to page boundary. */

        endptr -= (endptr % XLOG_BLCKSZ);

        WalSndCaughtUp = false;

    }

    nbytes = endptr - startptr;

    Assert(nbytes <= MAX_SEND_SIZE);

    /* 可以读取和发送的切片。 */

    resetStringInfo(&output_message);

    pq_sendbyte(&output_message, 'w');

    pq_sendint64(&output_message, startptr);    /* dataStart */

    pq_sendint64(&output_message, SendRqstPtr); /* walEnd */

    pq_sendint64(&output_message, 0);   /* sendtime, filled in last */

    /* 将日志直接读入输出缓冲区,以避免额外的 memcpy 调用。 */

    enlargeStringInfo(&output_message, nbytes);

    XLogRead(&output_message.data[output_message.len], startptr, nbytes);

    output_message.len += nbytes;

    output_message.data[output_message.len] = '\0';

    /* 最后填写发送时间戳,以使其尽可能晚。 */

    resetStringInfo(&tmpbuf);

    pq_sendint64(&tmpbuf, GetCurrentTimestamp());

    memcpy(&output_message.data[1 + sizeof(int64) + sizeof(int64)],

         tmpbuf.data, sizeof(int64));

    pq_putmessage_noblock('d', output_message.data, output_message.len);

    sentPtr = endptr;

    /* 更新共享内存状态 */

    {

        WalSnd   *walsnd = MyWalSnd;

        SpinLockAcquire(&walsnd->mutex);

        walsnd->sentPtr = sentPtr;

        SpinLockRelease(&walsnd->mutex);

    }

    /* Report progress of XLOG streaming in PS display */

    if (update_process_title)

    {

        char        activitymsg[50];

        snprintf(activitymsg, sizeof(activitymsg), "streaming %X/%X",

                 (uint32) (sentPtr >> 32), (uint32) sentPtr);

        set_ps_display(activitymsg, false);

    }

    return;

}

猜你喜欢

转载自blog.csdn.net/chuckchen1222/article/details/82975042
WAL