开发者

PostgreSQL 流复制认证机制详解

目录
  • 01 数据库物理复制
  • 02 连接主库认证
    • 2.1 根据配置文件获取密码
    • 2.2 通过环境变量注入密码
    • 2.3 通过密码文件获取密码
  • 03 walreceiver 认证源码解析
    • 04 libpq 的连接控制函数
      • 参考资料

        PostgreSQL 流复制认证机制详解

        物理复制(流复制 Streaming Replication )作为 PostgreSQL 高可用架构的核心技术,其安全性直接关系到数据库集群的可靠性;本文选择物理复制中备库向主库请求建立流复制连接的认证过程,即 walreceiver 进程连接主库时的认证机制,并结合源码解析其实现原理

        01 数据库物理复制

        PostgreSQL 流复制认证机制详解

        如上图所示,PostgreSQL 的主备物理复制即流复制(Streaming Replication)机制确保主库(Primary)生成的预写日志(WAL)能实时传输到备库(Standby)并正确应用,从而实现数据的同步,其实现依赖于三个关键进程:

        • walsender(主库):推送 WAL 数据到备库
        • walreceiver(备库):接收并存储 WAL 数据
        • startup(备库):应用 WAL 数据到数据库文件

        在流复制过程中,预写日志(Write Ahead Log)即图中的 XLOG 的生命周期如下:

        • 主库生成 WAL:主库执行事务时,将变更写入 WAL 缓冲区,最终持久化到 WAL 文件
        • walsender 发送 WAL:walsender 进程从 WAL 文件或缓冲区读取数据,通过复制协议发送给备库的 walreceiver
        • walreceiver 接收并存储 WAL:备库的 walreceiver 将接收到的 WAL 数据写入本地 pg_wal 目录,并通知 startup 进程
        • startup 应用 WALstartup 进程读取本地 WAL 文件,按顺序将变更应用到备库的数据文件中,完成数据同步

        02 连接主库认证

        当备库以恢复模式(Recovery Mode)启动时(例如存在 standby.signalrecovery.conf 文件),PostgreSQL 主进程postmaster会直接启动 startup 进程。在 startup 进程初始化过程中对 primary_conninfo 中的参数信息解析后填充到共享内存中的 WalRcvData 数据结构中,然后备库在启动 walreceiver 进程时根据配置尝试连接到主库。连接成功,该备库的 walreceiver 进程,与主库的 walsender 建立复制流

        所以,备库想要和主库建立复制流,需要进行连接认证

        2.1 根据配置文件获取密码

        通过配置文件中的 primary_conninfo 参数 password 明文配置连接密码是最常用的方式,正确配置对应字段之后,walreceiver 进程则根据该信息进行连接认证, primary_conninfo 参数配置样例如下,

        primary_conninfo = 'host=192.168.1.100 port=5432 user=replicator password=yourpassword application_name=standby1 sslmode=require sslcompression=0 keepalives=on connect_timeout=10'

        primary_conninfo 中常见的配置项及其说明如下:

        参数说明示例值
        host主库的 IP 地址或主机名host=192.168.1.100
        port主库的监听端口(默认 5432port=5432
        user主库上具有 REPLICATION 权限的用户名(用于复制的专用用户)user=replicator
        password复制用户的密码password=yourpassword
        dbname主库的数据库名(通常固定为 replication 或主库的某个数据库)dbname=postgres
        application_name备库的标识名称,主库的 pg_stat_replication 视图会显示此名称application_name=standby1
        channel_binding是否启用通道绑定(Channel Binding),增强 SSL/TLS 安全性(可选)channel_binding=prefer
        replication固定值 truedatabase,用于声明连接为复制流(通常设置为 truereplication=true
        connect_timeout连接主库的超时时间(单位:秒)connect_timeout=10
        keepalives是否启用 TCP 保活机制(默认 onkeepalives=on
        keepalives_idleTCP 保活包的空闲时间(单位:秒)keepalives_idle=60
        keepalives_intervalTCP 保活包的重试间隔(单位:秒)keepalives_interval=5
        keepalives_countTCP 保活包的最大重试次数keepalives_count=3
        sslmowww.devze.comdeSSL 连接模式sslmode=require
        sslcompression是否启用 SSL 压缩(默认 0,即禁用)sslcompression=0
        sslkey客户端 SSL 私钥文件路径sslkey=/etc/ssl/client.key
        sslcert客户端 SSL 证书文件路径sslcert=/etc/ssl/client.crt
        sslrootcert根证书文件路径(用于验证主库证书)sslrootcert=/etc/ssl/ca.crt

        如果需要避免在 primary_conninfo 中明文存储密码,可以通过接下来的两种方式进行认证:在备库启动时通过环境变量提供密码或通过.pgpass 密码文件提供密码

        2.2 通过环境变量注入密码

        PostgreSQL 的 libpq 库通过一系列环境变量为连接参数提供默认值,在代码中没有显式指定对应参数时,这些变量会在调用 PQconnectdbPQsetdbLoginPQsetdb 时生效;这些环境变量同样可以适用于 walreceiver 进程向主库申请建立连接的认证过程

        以下是 libpq 支持的常用环境变量,更多的环境变量适用说明可以参考官方文档

        https://www.postgresql.org/docs/current/libpq-envars.html

        环境变量作用示例值
        PGHOST数据库服务器主机名或 IPlocalhost
        PGHOSTADDR数据库服务器的 IP 地址(跳过 DNS)192.168.1.100
        PGPORT数据库端口号5432
        PGDATABASE要连接的数据库名mydb
        PGUSER数据库用python户名postgres
        PGPASSWORD数据库密码yourpassword
        PGPASSFILE密码文件路径~/.pgpass
        PGOPTIONS连接选项(如 -c search_path=...-c statement_timeout=1000
        PGSSLMODESSL 模式(disable/require 等)require
        PGREQUIRESSL强制 SSL 连接(优先用 PGSSLMODE1
        PGURI完整的连接 URI(覆盖其他参数)postgresql://user:pass@host/db

        通过环境变量注入密码,需要确保 walreceiver 进程启动时的环境变量中已经配置了 PGPASSWORD,即在备库启动之前需要先使用如下命令设置 PGPASSWORD 环境变量,当然也可以直接通过编辑 .bashrc 等文件进行配置

        export PGPASSWORD="yourpassword"

        这样就可以在 primary_conninfo 没有配置 password 字段的情况下进行验证,但需要保证该密钥与流复制用户正确匹配才能认证成功

        但在实际使用中 PGPASSWORD明文密码可能被进程监控工具捕获,同样存在安全风险,推荐使用 .pgpass 密码文件

        2.3 通过密码文件获取密码

        PostgreSQL 中通过密码文件 .pgpass 存储数据库密码是一种较为安全的方式,避免在代码、命令行或环境变量中暴露明文密码。当客户端工具连接数据库时,若未通过其他方式指定密码,会自动从 .pgpass 文件中匹配条目获取密码;该密码文件的默认路径是 ~/.pgpass ,文件格式如下

        hostname:port:database:username:password
        字段说明
        hostname主机名或 IP,* 表示匹配任意主机(包括本地套接字)
        port端口号,* 表示匹配任意端口
        database数据库名,* 表示匹配任意数据库
        username用户名,* 表示匹配任意用户
        password明文密码

        需要注意的是,密码文件必须限制访问权限,仅允许文件所有者读写,否则 PostgreSQL 会忽略该文件

        chmod 600 ~/.pgpass

        除了默认的文件路径 ~/.pgpass ,也可以通过环境变量 PGPASSFILE 或者直接设置连接参数 passfile 来指定自定义密码文件路径

        export PGPASSFILE=/path/to/custom_passfile

        walreceiver 进程通过 libpq 进行认证时,如果未显示指定密码,则会尝试在备库的密码文件中查找匹配的密码,但作为流复制用户在 .pgpass 文件中该记录的数据库名称需要配置成 replication

        hostname:port:replication:username:password

        03 walreceiver 认证源码解析

        前文提到 startup 进程在主进程postmaster发现作为备库启动即以恢复模式(Recovery Mode)启动时直接启动;而 walreceiver 进程则是由 startup 进程在进行一系列条件判断后,通知 postmaster 来启动,该过程执行顺序如下:

        • 触发条件:当备库负责 WAL 恢复的 startup 进程发现本地 WAL 日志不完整需要从主库流式编程客栈传输时,会通过信号通知 postmaster 启动 walreceiver 进程
        • 信号传递startup 调用 SendPostmasterSignal(PMSIGNAL_START_WALRECEIVER),向 postmaster 发送启动 walreceijsver 的请求
        • postmaster 响应postmaster 收到信号后,在其主循环中调用 LaunchMissingBackgroundProcesses(),发现需要启动 walreceiver,随即创建子进程

        进程启动:postmaster 通过 fork() 创建子进程,子进程执行 WalReceiverMain(),成为 walreceiver 进程,连接到主库拉取 WAL 数据

        StartupProcessMain()          // 备库启动 startup 进程的主函数
          ->StartupXLOG()             // 负责 WAL 恢复的核心逻辑
            ->InitWalRecovery()       // 初始化 WAL 恢复环境
              ->XLogReaderAllocate()  // 分配 WAL 读取器
                ->XLogPageRead()      // 读取 WAL 页
                  ->WaitForWALToBecomeAvailable()  // 检查 WAL 是否可用
                    ->RequestXLogStreaming()       // 判断需要流复制,触发启动 walreceiver
                      ->SendPostmasterSignal(PMSIGNAL_START_WALRECEIVER)  // 通知 postmaster
                        // (postmaster 进程侧操作)
                        ->process_pm_pmsignal()    // 处理信号 PMSIGNAL_START_WALRECEIVER
                          ->LaunchMissingBackgroundProcesses()  // 检查并启动缺失的后台进程
                            ->StartChildProcess(B_WAL_RECEIVER) // 启动 walreceiver 进程
                              ->postmaster_child_launch()       // 创建子进程
                                ->WalReceiverMain()             // walreceiver 主函数

        walreceiver 进程启动之后,根据 WalRcvData 中已经初始化好的连接信息 conninfo 尝试和主库建立连接,连接过程使用 libpq 和核心函数 PQconnectStartParams 建立连接,认证密码获取方式有:

        • 通过配置参数:在根据 primary_conninfo 初始化好的 WalRcvData 中包含 password 信息
        • 通过环境变量:在调用 conninfo_add_defaults 获取默认值时,会使用 getenv 函数遍历PQconninfoOptions 数组中的所有环境变量并获取对应的值,其中就包括 PGPASSWORD 用于给 pgpass 赋值
        • 通过密码文件:在调用pqConnectOptions2函数时如果发现当前的 conn->pgpass 仍然为空,则根据默认的密码文件 ~/.pgpass 或用户自定义的密码文件路径 PGPASSFILE 并调用passwordFromFile函数获取所有 host 对应的密码
        WalReceiverMain()             // walreceiver 进程主入口
          ->walrcv_connect()          // 触发连接主库的逻辑
            ->libpqrcv_connect()       // 调用 libpq 库的封装接口
              ->libpqsrv_connect_params()  // 增加一些额外的参数选项 options
                ->PQconnectStartParams()   // 初始化非阻塞连接
                  ->conninfo_array_parse()  // 解析连接参数数组
                    ->conninfo_add_defaults()  // 补充默认连接参数(从 service file 或者环境变量中获取默认值)
                  ->pqConnectOptions2()     // 处理认证相关选项(如密码文件)
                    ->passwordFromFile()    // 从 .pgpass 文件读取密码
                  ->pqConnectDBStart()      // 启动异步连接过程
                    ->PQconnectPoll()       // 处理连接状态机(包括认证协商)

        认证过程中使用密码时,优先使用从密码文件中获取的密码conn->connhost[conn->whichhost].password,该逻辑由 PQpass 函数实现

        char *
        PQpass(const PGconn *conn)
        {
            char       *password = NULL;
            if (!conn)
                return NULL;
            if (conn->connhost != NULL)
                password = conn->connhost[conn->whichhost].password;
            if (password == NULL)
                password = conn->pgpass;
            /* Historically we've returned "" not NULL for no password specified */
            if (password == NULL)
                password = "";
            return password;
        }

        04 libpq 的连接控制函数

        在介绍 walreceiver 连接认证时,提到使用PQconnectStartParams 去建立于主库节点的连接,这个函数通过参数数组接收连接信息,这种直接传递键值对可以自动处理特殊字符,是新版本引入的启动异步连接函数

        PQconnectStartParams 函数定义如下,接受两个数组:keywords 包含参数关键字,values 包含参数值,并通过 expand_dbname 指定是否允许扩展参数

        PGconn *PQconnectStartParams(const char *const *keywords, const char *const *values, int expand_dbname)

        PQconnectStart 函数是另一种支持连接字符串的连接控制函数,定义如下,数据库连接信息是用从 conninfo 连接字符串里取得的参数中解析出来的

        PGconn *PQconnectStart(const char *conninfo)

        PQconnectPoll 函数则是PQconnectStartParamsPQconnectStart最终进行连接建立时调用的函数,该函数轮询异步连接状态,推动连接过程直至完成或失败

        PostgresPollingStatusType PQconnectPoll(PGconn *conn)

        PQconnectPoll 函数返回状态 PostgresPollingStatusType 定义如下

        typedef enum
        {
            PGRES_POLLING_FoCIMpAILED = 0,   // 连接成功
            PGRES_POLLING_READING,      // 需等待套接字可读
            PGRES_POLLING_WRITING,      // 需等待套接字可写
            PGRES_POLLING_OK,           // 连接成功
            PGRES_POLLING_ACTIVE        /* unused; keep for backwards compatibility */
        } PostgresPollingStatusType;

        上述三个函数PQconnectStart, PQconnectStartParams, PQconnectPoll 都是用于打开一个与数据库服务器之间的非阻塞的连接,即应用程序在执行这些函数的时候不会因远端的 I/O 而被阻塞

        基于这三个函数,libpq 提供了三种连接控制接口PQconnectdb, PQconnectdbParams, PQsetdbLogin

        PQconnectdb, PQconnectdbParams 分别对应对PQconnectStart, PQconnectStartParams 函数的封装,函数调用参数一致,如下所示

        PGconn *
        PQconnectdbParams(const char *const *keywords,
                          const char *const *values,
                          int expand_dbname)
        PGconn *
        PQconnectdb(const char *conninfo)

        PQsetdbLogin函数则是 libpq 早期的遗留函数,仍保留对旧版本的兼容,接受不太灵活的分立的参数形式:host, port, options, dbname, user, password,其定义如下

        PGconn *
        PQsetdbLogin(const char *pghost, const char *pgport, const char *pgoptions,
                     const char *pgtty, const char *dbName, const char *login,
                     const char *pwd)

        这三种接口区别在于参数传递方式:

        PQconnectdbParams函数建立连接的示例如下,通过关键字和值的数组传递连接参数,这种方式在动态生成参数时更安全,无需转义能避免字符串拼接错误,而且支持参数扩展

        const char *keywords[] = {"host", "port", "dbname", NULL};
        const char *values[] = {"localhost", "5432", "mydb", NULL};
        PGconn *conn = PQconnectdbParams(keywords, values, 0); 

        PQconnectdb函数建立连接的示例如下,通过连接字符串传递连接参数,这种方式在处理密码等字符串时需要手动进行转义,也支持扩展参数

        PGconn *conn = PQconnectdb("host=127.0.0.1 port=5432 dbname=mydb");

        PQsetdbLogin函数建立连接的示例如下,通过固定参数传递有限的连接参数,这种方式缺乏灵活性,新代码不建议使用该接口,该接口仅用于旧版本的兼容

        PGconn *conn = PQsetdbLogin("localhost", "5432", "", "mydb", "postgres", "yourpassword");

        参考资料

        https://www.kancloud.cn/taobaomysql/monthly/81110

        https://zhuanlan.zhihu.com/p/530628881

        PostgreSQL: Documentation: 17: 19.6. Replication

        PostgreSQL: Documentation: 17: 32.15. Environment Variables

        PostgreSQL: Documentation: 17: 32.16. The Password File

        https://www.postgresql.org/docs/current/libpq-connect.html//LIBPQ-PQCONNECTDB 

        到此这篇关于PostgreSQL 流复制认证机制的文章就介绍到这了,更多相关PostgreSQL 流复制认证机制内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新数据库

        数据库排行榜