更多"C/C++、PostgreSQL、编译原理、计算机原理、TCP/IP、数据结构&算法、Linux编程”等技术文章更新于公众号: 君子黎


1. SysLogger系统日志收集器

     在 【PostgreSQL教程】· PostgreSQL配置管理日志(一) 一文中,详细介绍了如何在PostgreSQL中开启日志收集器,以及配置log文件存储目录和大小,同时还介绍了许多与log文件相关联的配置参数。此外还说明了log文件在PostgreSQL10.0之前与之后的一些细微差异化变动。本节内容主要用于分析SysLogger日志收集器的内部原理,在学习 了本文之后,将对Logger的工作方式有着更加清晰的认识。

1.1 SysLogger启动入口

     PostgreSQL是一个客户端/服务器模式(C/S)架构,整个服务的初始化代码入口是main.c(/src/backend/main)文件中的main函数。在main函数中会根据启动参数选项来进行判断,并走不同的分支。然后进行postmaster守护进程初始化操作,这一初始化过程主要在postmaster.c文件中实现(位于/src/backend/postmaster/目录)。守护进程postmaster负责整个系统的启动和关闭,它监听并接受来自客户端的连接请求,并未其每一个请求分配一个postgres服务。之后该客户端连接上面的所有请求操作都直接与postgres进程进行交互,而不再经由postmaster守护进程参与

     SysLogger日志收集器的初始化入口是SysLogger_Start函数(/src/backend/postmaster/syslogger.c)。在初始化日志收集器前,会对GNC全局变量参数Logging_collector进行判断(对应postgresql.conf配置文件中的logging_collector,更多细节阅读 【PostgreSQL教程】· PostgreSQL配置管理日志(一) ),若该参数值为true,则表示开启日志收集器进程logger,反之则退出,不开启logger进程。

1.1.1 SysLogger日志收集器进程名

     SysLogger日志收集器的守护进程名是logger,在初始化SysLogger进程的时候,会对全局变量 MyBackendType (BackendType MyBackendType;)进程初始化为:B_LOGGER 的操作(MyBackendType = B_LOGGER;),后面在创建守护进程时候,根据GetBackendTypeDesc()函数获取对应的守护进程名。在PostgreSQL 13.2版本***支持以下几种类型的后台守护进程,如下所示枚举值(更多关于枚举类型的知识请阅读 C/C++ 枚举类型)。

typedef enum BackendType
{
    B_INVALID = 0,
    B_AUTOVAC_LAUNCHER,
    B_AUTOVAC_WORKER,
    B_BACKEND,
    B_BG_WORKER,
    B_BG_WRITER,
    B_CHECKPOINTER,
    B_STARTUP,
    B_WAL_RECEIVER,
    B_WAL_SENDER,
    B_WAL_WRITER,
    B_ARCHIVER,
    B_STATS_COLLECTOR,
    B_LOGGER,
} BackendType;

     以下是各守护进程(枚举值)对应的进程名字:

在这里插入图片描述

1.1.2 PostgreSQL默认开启的守护进程

     并非所有的守护进程都会默认开启,有些是需要在postgresql.conf配置文件中进手动配置启动,比如日志收集器,就需要置参数logging_collector为on。 默认情况下,PostgreSQL仅开启了 checkpointer、background write、walwriter、autovacuum launcher、stats collector、logical replication launcher 这几个后台守护进程,如下图所示:

在这里插入图片描述

1.1.3 SysLogger日志收集器启动流程

1.1.3.1 SysLogger系统日志整体初始化流程图

     SysLogger日志收集器的整体初始化过程如下流程图所示:

在这里插入图片描述
     在初始化日志收集器时候,先根据postgres.conf配置文件中的参数(log_directory)来创建对应的log日志目录,默认log日志目录权限为文件拥有者具有读、写和执行权限。如下:

#ifndef S_IRWXU
#define S_IRWXU (S_IRUSR | S_IWUSR | S_IXUSR)
#endif

int
MakePGDirectory(const char *directoryName)
{
    return mkdir(directoryName, pg_dir_create_mode);
}

     目录创建好之后,再获取当前系统时间,然后将时间按照postgresql.conf配置文件中的log_filename参数的值进行对应的格式化,生成一个新log文件。然后以文件访问模式“+a”的方式打开log文件,若不存在则新建,并且对该log文件的属性进行调整,同时设置该log文件的文件流缓冲区为行缓存(_IOLBF)形式。在log文件创建成功之后,并调用函数fork_process()创建logger子进程(fork_process函数是fork函数的封装,包括返回值都匹配fork.),并在子进程中对内存相关的参数(OOM)进行一些内部设置。之后该函数返回进程的PID。

     根据PID的值进行对应的其他工作处理,若PID为0,则表示为子进程中,在子进程中会初始化一些与子进程状态相关的全局变量、注册父进程状态信号、关闭读管道、关闭postmaster父进程中的监听套接字和与父进程postmaster相关的内存数据等。然后进入SysLoggerMain()函数真正开始logger日志收集器进程的相关处理操作。 而在父进程中,则会先刷新stout、stderr等文件描述符的缓冲区数据,然后在将stdout、stderr文件描述符重定向到管道syslogPipe[1]d 写端,接着关闭管道写端和日志文件描述符句柄syslogFile。 因为postmaster父进程将永远不会向该log文件中写数据。

     之后父进程postmaster中将返回logger日志收集器的子进程PID。 该子进程PID将用于postmaster父进程的ServerLoop()函数中。ServerLoop()函数是守护进程postmaster(父进程)的主要空循环处理函数。该函数为一个死循环函数(for( ; ; )), 该函数内部主要负责对 checkpointer(检查点进程)、background write(后台写进程)、walwriter(预写式日志写进程)、autovacuum launcher(系统自动清理进程)、stats collector(统计数据搜集进程)、logger(系统日志进程)、archiver(预写式日志归档进程)等 辅助守护进程的状态管理维护,若发现其中某个进程PID丢失,则立刻重新创建一个新的对应守护进程。

     比如对于下图中的几个辅助进程logger、background writer、walwriter、autovacuum launcher、stats collector、logical replication launcher,若其中一个被手动人为kill掉,则postmaster守护进程将会检查到对应辅助子进程被kill掉的状态和对应信号(若开启了最高等级(debug5)日志,则会打印出对应信号值)。然后立刻重新fork()一个对应的子进程。备注:我结合代码逻辑亲自测试过,实际情况与逻辑是相吻合、匹配的。

在这里插入图片描述
     此外,ServerLoop()函数还负责监听用户的连接请求,对于用户下发的每个请求,postmaster都会fork一个子进程(postgres)来进行处理,之后的该用户的所有请求操作,包括数据库、表、索引等的增删改查等操作都交由该postgres进程处理响应。因此PostgreSQL是一个多进程的客户端/服务器模型。

if (selres > 0)
        {
            int            i;

            for (i = 0; i < MAXLISTEN; i++)
            {
                if (ListenSocket[i] == PGINVALID_SOCKET)
                    break;
                if (FD_ISSET(ListenSocket[i], &rmask))
                {
                    Port       *port;

                    port = ConnCreate(ListenSocket[i]);
                    if (port)
                    {
                        BackendStartup(port);

                        /*
                         * We no longer need the open socket or port structure
                         * in this process
                         */
                        StreamClose(port->sock);
                        ConnFree(port);
                    }
                }
            }
        }

     在接收到用户的连接请求后,ServerLoop()函数将首先创建一个与该请求对应的本地连接ConnCreate()。之后的fork子进程等工作则交给BackendStartup()函数中去处理。当fork进程成功后,父进程中将会把本次创建的子进程的PID放入到后端活动的进程PID链表中,该工作由dlist_push_head()函数负责完成。

3. 总结

     到这里为止,较为详细地对PostgreSQL 8.0引入的logger系统日志收集器的初始化流程与工作原理做了梳理和总结,通过本文的阅读学习,将提升你对logger辅助进程的理解。同时,这也对PostgreSQL数据库服务工作的日志排查有着辅助性的帮助。下一节将继续对其他辅助进程的工作原理进行分析。