PostgreSQL 流复制认证机制

在这里插入图片描述

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

01 数据库物理复制


在这里插入图片描述

图片来源:https://box.kancloud.cn/2015-11-15_5647dfc16eb07.jpg

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

  • walsender(主库):推送 WAL 数据到备库

  • walreceiver(备库):接收并存储 WAL 数据

  • startup(备库):应用 WAL 数据到数据库文件

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

  • 主库生成 WAL:主库执行事务时,将变更写入 WAL 缓冲区,最终持久化到 WAL 文件

  • walsender 发送 WALwalsender 进程从 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
sslmodeSSL 连接模式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数据库用户名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 来启动,该过程执行顺序如下:

  1. 触发条件:当备库负责 WAL 恢复的 startup 进程发现本地 WAL 日志不完整需要从主库流式传输时,会通过信号通知 postmaster 启动 walreceiver 进程

  2. 信号传递startup 调用 SendPostmasterSignal(PMSIGNAL_START_WALRECEIVER),向 postmaster 发送启动 walreceiver 的请求

  3. postmaster 响应postmaster 收到信号后,在其主循环中调用 LaunchMissingBackgroundProcesses(),发现需要启动 walreceiver,随即创建子进程

  4. 进程启动: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 建立连接,认证密码获取方式有:

  1. 通过配置参数:在根据 primary_conninfo 初始化好的 WalRcvData 中包含 password 信息

  2. 通过环境变量:在调用 conninfo_add_defaults 获取默认值时,会使用 getenv 函数遍历PQconninfoOptions 数组中的所有环境变量并获取对应的值,其中就包括 PGPASSWORD 用于给 pgpass 赋值

  3. 通过密码文件:在调用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_FAILED = 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王清欢Randy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值