介绍:
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;
}