一、程序涉及到的一些知识
1、主服务器是以连续的归档模式操作的,而备用服务器是以连续的恢复模式从主服务器的WAL文件中读取数据。
2、归档进程pgarch就是负责在重做日志文件切换后将已经写满的重做日志文件复制到归档日志文件中,以防止循环写入重做日志文件时将其覆盖。所以说,只有数据库运行在归档模式时,这个pgarch进程才会被启动。
3、日志传送是异步进行的。WAL记录的传送是在事务提交之后进行的。
4、辅助服务器可以通过TCP连接(所谓的流复制)从WAL归档或者主服务器的目录中读取WAL记录。
5、如果想用流复制,必须在主服务器中设置相应的权限,以使来自备用服务器的流复制连接得到许可。这个需要在pg_hba.conf文件中做相应的配置。本代码中的max_wal_senders变量也是在这个文件做配置的,以提供足够大的值来存储备用服务器的数量。
walsender.c程序中的函数都是为walsender进程发送WAL记录而准备的,包括了walsender进程的入口函数、初始化函数、读取WAL记录的XlogRead函数、XlogSend函数以及walsender进程处理相关的信号函数等。
1、walsender进程重要的数据结构
(1)、标记walsender进程的状态枚举
在walsender进程的生命周期中,进程存在四个状态,即:启动状态、备份状态、获取异常状态以及终止状态。其定义如下所示:
typedef enum WalSndState { WALSNDSTATE_STARTUP = 0,/*启动状态*/ WALSNDSTATE_BACKUP,/*备份状态*/ WALSNDSTATE_CATCHUP,/*获取异常状态*/ WALSNDSTATE_STREAMING /*关闭状态*/ } WalSndState;
下面就walsender进程的四个状态以及何时处于一种特定的状态进行说明:
①、在刚刚创建的时候是处在启动状态;
②、在日志的发送过程中,一直都是处在备份的状态;
③、如果walsender进程在休眠的时候接收到一些异常信号的时,walsender进程的状态标记为catchup态,标记出进程在和 walreceiver通信的过程中出现一些不可以处理的异常,比如:walreceiver进程请求的文件不存在、walsender进程和walreceiver进程的通信链接断开等等。
④、walsender进程在休眠的时候接收到一些异常信号的时候,会由catchup态转换为终止态,在这个状态的时候,进程一般需要处理相关的异常处理,比如发送出一些留在缓冲区中尚未发送出去的WAL日志文件、进程退出等。
(2)、存储每一个walsender进程的信息数据结构
在postgresql数据库系统中,可能存在多个walsender进程用来为walreceiver进程发送请求的WAL记录,如果不对每个walsender进程加以标记,整个系统将会很混乱,这个用来存储walsender进程信息的数据结构定义在src\include\replication\walsender.h文件中。其结构如下所示:
typedef struct WalSnd { pid_t pid; /* walsender进程的ID*/ WalSndState state /* walsender的状态*/ XLogRecPtr sentPtr; /* WAL发送点*/ XLogRecPtr write; /*写的位置*/ XLogRecPtr flush; /*刷新的位置*/ XLogRecPtr apply; /*被应用的位置*/ slock_t mutex; //互斥信号量,保护上面的共享变量 Latch latch; int sync_standby_priority; } WalSnd;
在WalSnd结构体中,定义了存储walsender进程的ID号,在系统中每一个进程的ID号是不一样的,这样用进程号来标记共享内存中的WalSnd结构,将会带来很大的方便。在WalSnd结构体中定义的内容分别简述如下:
①、pid:存储walsender进程的ID号;
②、state:存储walsender进程的状态,其状态含义在上面陈述过;
③、write、flush以及apply:分别记录辅助服务器中xlog被写、刷新和应用的位置,如果辅助服务器还没有为它们赋值,则它们是无效的;
④、mutex:互斥信号量,是int类型的,用来控制一些临界资源,使得临界资源一次只能被一个进程所读、写等操作;
⑤、latch:用来保护③中的三个变量,其作用相当于一把锁;
⑥、sync_standby_priority:辅助服务器的优先级顺序,为0则表示没有被排列,被同步复制锁SyncRepLock保护。
2、walsender.c重要的函数分析
(1)、walsender进程的启动函数WalSenderMain
Walsender进程的启动是在WalSenderMain函数中启动的,其函数原型为
int WalSenderMain(void)
在WalSenderMain函数中完成了walsender进程的初始化工作,响应从walreceiver进程发送过来的命令,并把walreceiver进程请求的日志文件通过流的方式发送过去。WalSenderMain函数的执行过程:
①、初始化walsender进程的数据结构,也就是上面介绍的WalSnd结构体;
②、为每一个walsender进程创建内存上下文,其作用是使得每一个walsender进程在指定的内存空间中工作,而且在遇到错误的时候重新启动这个内存上下文,避免了内存的泄漏。但是在实际的过程中,进程并不是企图在遇到错误的时候恢复,而是关闭这个连接以及终止walsender进程;
③、设置本进程为非阻塞状态,在walsender被创建的时候,它是处于阻塞的状态,这时需要在程序中主动的向postmaster进程发送一个非阻塞信号,使得walsender进程出在非阻塞的状态,这样才可以进行下面的一些流复制工作;
④、和walreceiver进行握手和认证,在walreceiver进程接收到XLOG日志文件之前,walreceiver进程需要和postmaster进程的子进程进行握手(建立连接)、以及需要得到postmaster进程的相关权限的认证,这样walreceiver进程才可以得到相关的日志;
⑤、同步流复制初始化配置;
⑥、进入WalSndLoop()循环函数,进行流复制的发送; 这个函数是walsender进程的流发送的核心函数,且walsender进程的大部分时间都是在这个函数中处理数据的,直到流复制完成,或者出错这个函数才会退出;此时,walsender进程也就消亡了。
(2)、walsender进程的数据结构初始化函数InitWalSnd
在每一个walsender进程启动的时候都要初始化共享内存中的WalSnd结构体,为进程以后的工作做好相关的工作。初始化函数的原型定义为:
static void InitWalSnd(void)
其工作过程描述如下:通过一个for循环,遍历max_wal_senders个walsnds数组,直到找到一个没有被占用的walsnd[i],然后该进程就占用它。之后进行一些初始化设置,诸如设置walsender的进程id、状态等信息。max_wal_senders变量是在pg_hba.conf配置文件中设置好的,以提供一个足够大的值来存储备用服务器的数量。
如果没有在walsnd数组中找到空闲的位置,则程序报错并退出。InitWalSnd函数的流程图见图2-1所示。
图2-1 InitWalSnd进程的工作流程图
相关说明:①、流程图一开始判断变量WalSndCtl不为空和MyWalSnd为空,因为在walsender进程启动之前,WalSndCtl变量就已经通过postmaster进程在fork()函数中或者是EXEC_BACKEND机制中被初始化过,只要WalSndCtl为空说明出现了错误,程序应该退出。
②、WalSnd->pid=0才说明空间是空闲的,因为一般进程的ID是不可能为0的。
③、在占用walseds数组中的空间时,应该进行加锁的操作,因为一次可能存在好几个walsender进程的创建,如果不加锁,将会出现好几个进程同时占用一个walsnds数组空间,这样将会出现错误。
(3)、握手处理函数WalSndHandshake
在walreceiver进程和walsender进程进行通信之前,它们之间需要进行握手操作。在这个过程中,walsender进程接收来自walreceiver进程发送过来的命令,并解析命令操作。这个函数原型定义如下:
static void WalSndHandshake(void)
这个函数的工作过程描述如下:
只要流复制尚未开始,本函数一直处于while循环中。
①、当接收到的命令是Q,则处理相关的查询操作。在处理流复制查询操作成功的时候,walsender就会进入到流复制状态,循环退出;
②、当接收到的命令是X,说明辅助服务器断开了连接,此时循环退出;
③、其他的情况则报错,且只有接收到命令为空(EOF),循环才会退出。其工作流程图见图2-2所示:
(4)、walsender进程的核心函数WalSndLoop
Walsender进程的绝大部分是在WalSndLoop函数中运行的,其函数原型定义如下:
static int WalSndLoop(void)
当walsender进程的主函数做好流复制相关初始化工作并做好流复制的准备时,则本进程一直处于这个循环函数中进行流复制,并把数据发送到walreceiver进程中去,直到流复制出错或者是辅助服务器关闭或者是postmaster进程关闭时,循环做好相应的关闭操作,并退出本进程。
WalSndLoop函数的工作过程描述如下:
①、初始化用来存储发送消息的数据结构;
②、进入for循环;
③、判断postmaster进程是否存在,处理本进程接收到的信号;
④、判断walsender进程是否请求终止;是则进程退出;否则转⑤
⑤、调用XlogSend()发送一些数据;若收到SIGUSR2信号,则退出
⑥、调用flush函数,把数据发送到辅助服务器,出错则转入⑨ ;
⑦、判断是否发送消息超时;是,则转入⑨,否则,转入⑧ ;
⑧、本进程是否处于catchup状态,是,则设置本进程为终止态;否则转②;
⑨、发送出错,清除相关缓冲中的数据并退出本进程。
函数的流程图见图2-3所示:
图2-2 WalSndHandshake()函数的工作流程图 图2-3 WalSndLoop函数的工作流程图
相关说明:①、为了防止palloc过度的分配空间,WalSndLoop函数中只调用一次内存分配函数,用来存储每一次输出来的消息,分配内存的大小足以存储这个消息,分配的空间大小为 1 + Wal数据头的大小 + MAX_SEND_SIZE。其中wal数据头包括当前WAL发送的位置、发送进程中终止发送WAL的位置以及当前事务的时间戳;MAX_SEND_SIZE才是所要发送消息的最大长度,1是为了存储结尾符的。
②、WalSndLoop函数只要已进入就会一直处在for语句无限循环中,除非遇到错误才会终止。
③、在程序中会判断postmaster进程是否存在,如果postmaster进程已经不存在了,walsender进程将会变成一个僵尸进程,这时可以把这个walsender进程归为init进程(linux里面的所有进程的父进程)的孩子,并由init进程来清理这个walsender进程。
(5)、日志发送函数XLogSend
walsender进程给walreceiver进程发送XLOG的都是由XlogSend进程实现的,在WalSndLoop函数中一直调用这个函数来发送日志文件。其函数原型的定义为:
static void XLogSend(char *msgbuf,bool *caughtup)
相关说明:①从已经缓存到磁盘还未发送给客户端的数据中读取MAX_SEND_SIZE大小的数据,然后把它缓存到libpq输出缓存当中去。在这个函数当中的msgbuf变量是在WalSndLoop()函数当中已经分配好的一块存储区域,事先分配好这样的缓存块可以避免以后重新再分配。Msgbuf的大小必须为1 + sizeof(WalDataMessageHeader) + MAX_SEND_SIZE,如果没有尚未发送出去的WAL数据,则caughtup被设置为true,否则被设置为false。
②、这个函数试图把那些已经写到缓冲中的数据发送出去,然后同步到备用服务器的磁盘中,在任何情况下,如果在postmaster进程还没有安全的把WAL日志存放到自己的磁盘中,这样就想把WAL日志发送出去是很不安全的。
③、如果postmaster进程崩溃然后重启,备用服务器必须要把在这个过程中接收到的WAL日志丢失掉。
④、在XlogSend()函数中每发送一个消息都是MAX_SEND_SIZE大小的,所以当一个消息的大小要小于MAX_SEND_SIZE,则在这个消息中填入一些无关的数据,而凑足为MAX_SEND_SIZE大小。
⑤、在发送消息的过程中,walsender进程并不因为一个WAL记录被在分在两个发送消息中而把这个WAL记录分为两个。walsender进程以页为边界来分割一个长的WAL记录,SendRqstPtr也从不指在WAL记录的中心位置。
(6)、日志读取函数XLogRead
在walsender进程给walreceiver进程发送指定的日志时,是要通过这个函数读取备用服务器所请求的日志,然后由XlogSend函数发送过去。XlogRead日志读取函数的原型的定义为:
void XLogRead(char *buf,XLogRecPtr recptr,Size nbytes)
此函数的功能是从recptr指定的位置开始,在WAL记录中读取nbytes字节的数据,并存储在buf缓存区当中去。
相关说明:①、只要XlogRead()函数读取一次,WAL记录段的文件描述符就会存储到sendFile全局变量中,并且一直都是保持打开的状态,直到walsender进程终止。
②、如果walreceiver请求接收的日志文件在服务器中找不到,则有可能walreceiver进程请求的日志文件过旧,而已经被删除掉了。
③、在把读取到的日志缓存到缓冲区的时候,walsender进程需要再次去检查这个缓存的日志段是否有效。这是因为,在walsender读取这个已经打开的WAL日志段的同时,所读取的日志段可能被删掉了,或者已经被新的WAL记录段覆盖掉了。这个时候存到缓冲区中的WAL记录段肯定是无效的,walsender进程需要处理这种情况,并要作出相关的错处处理。
(7)、WalSndLoop()、XLogSend(2)以及XLogRead(3)之间的调用关系
在walsender进程发送日志的过程中,一直都是通过WalSndLoop函数调用XlogRead函数来发送日志给walreceiver进程,但是发送出去的日志需要另外一个函数来读取,这就是XlogRead的职责。这三个函数之间的联系如图2-4所示:
相关说明:由于日志的发送一般都是要经过很多次循环,所以在程序中,通过walSndLoop函数中的for循环中一直调用XLogSend函数来发送日志,而XlogRead函数又得在日志发送之前读取walreceiver进程指定的日志文件。在调用完的时候函数都返回。
(8)、walsender进程信号注册函数WalSndSignals
在walsender进程运行的过程中,它会接收到很多种类的信号,而在操作系统中,大多数所接收到的信号默认动作是关闭这个进程,而并不是所有的信号需要关闭进程,所以有必要针对相关的信号作出相关的处理。这就是需要用到信号注册函数。这个函数的原型定义为:
void WalSndSignals(void)
在本程序中,一共针对13个接收到的信号进行了信号注册,使得当接收到被注册的信号时,执行我们的处理方式。在walsender进程的生命周期中,需要作出自己的处理信号有SIGHUP、SIGTERM、SIGQUIT、SIGUSR1以及SIGUSR2。下面针对这些信号进行说明。
①、SIGHUP:walsender进程在接收到这个信号的时候,其调用WalSndSigHupHandler信号处理函数,来为重新读配置文件设置标记位。
②、SIGTERM:当walsender进程收到SIGTERM信号的时候,说明需要把walsender进程和walreceiver进程之间的连接终止掉,然后在下一次合适的时间调用exit(0),以关闭walsender进程。
③、SIGQUIT:在遇到紧急的情况时,walsender进程将会收到SIGQUIT信号,在收到这个信号的时候,也就是简单的调用abort和exit函数来终止这个连接并且进程也将会退出。
④、SIGUSR1:在操作系统中,SIGUSR1和SIGUSR2一般都是留给用户自定义的信号。在walsender进程中,SIGUSR1信号被用来设置发送WAL记录的标记。
⑤、SIGUSR2:当 postmaster进程被关闭掉了,这时它将会在所有的常规进程关闭以及关闭记录点也被记录下来的时候,给walsender进程发送SIGUSR2信号,walsender在接收到这个信号的时候,将会把所有发送到缓冲区中的WAL记录全部发送出去,然后walsender进程才会退出。
而对SIGINT、SIGALRM以及SIGPIPE信号的处理方式是忽略掉。对SIGCHLD、SIGTTIN、SIGTTOU、SIGCONT以及SIGWINCH信号的处理方式采取系统默认的方式。
三、walsender进程的创建、与walreceiver进程通信和死亡
(1)、walsender进程的创建
当备用服务器中的walreceiver进程请求和主服务器连接,并要求XLOG流复制时,postmaster就负责启动 walsender进程。具体过程见图3-1所示:
图3-1 walsender进程的创建过程
(2)、walsender进程和walreceiver进程通信
walreceiver进程在流复制的过程中,它接收来自主服务器上面的XLOG记录。而这个XLOG记录是由walsender进程发送过去的。其通信过程见图3-2所示:
图3-2 walsender进程和walreceiver进程通信的过程
(3)、walsender进程的结束
walsender进程的结束一共分为两大类:①、walsender进程自己要求终止,或者是通信断开,或者是walsender进程出现了错误等;②这类情况是postmaster进程出现了错误,而导致walsender进程需要关闭。
如果在postmaster进程没有出现任何异常时,这时候walsender进程的关闭只要是分为三大类:①、接收到SIGTERM信号。这时,walsender进程正常关闭。②、接收到SIGQUIT。这种情况下,一般都是遇到了紧急的情况,这时walsender进程将会被不正常的关闭,在关闭的过程中也将会处理相关的操作。比如把缓存在缓冲区中的日志发送完等。③、当walsender进程和walreceiver进程连接断开,这种情况下,并不当作是一次崩溃,而是当作一次正常的终止。walsender进程将会马上退出并且不会再发送任何XLOG记录了。其图示见图3-3所示:
图3-3 walsender进程的终止分类
假如是服务器被异常终止,postmaster进程在所有的常规后台进程被关闭且关闭检验点也被记录之后,就发送一个SIGUSR2信号给walsender进程,要求walsender进程把所有未解决的WAL发送出去,包括关闭检验点记录,最后walsender进程也关闭。