物理复制(流复制 streaming replication )作为 postgresql 高可用架构的核心技术,其安全性直接关系到数据库集群的可靠性;本文选择物理复制中备库向主库请求建立流复制连接的认证过程,即 walreceiver 进程连接主库时的认证机制,并结合源码解析其实现原理
01 数据库物理复制
如上图所示,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 应用 wal:
startup
进程读取本地 wal 文件,按顺序将变更应用到备库的数据文件中,完成数据同步
02 连接主库认证
当备库以恢复模式(recovery mode)启动时(例如存在 standby.signal
或 recovery.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 | 主库的监听端口(默认 5432 ) | port=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 | 固定值 true 或 database ,用于声明连接为复制流(通常设置为 true ) | replication=true |
connect_timeout | 连接主库的超时时间(单位:秒) | connect_timeout=10 |
keepalives | 是否启用 tcp 保活机制(默认 on ) | keepalives=on |
keepalives_idle | tcp 保活包的空闲时间(单位:秒) | keepalives_idle=60 |
keepalives_interval | tcp 保活包的重试间隔(单位:秒) | keepalives_interval=5 |
keepalives_count | tcp 保活包的最大重试次数 | keepalives_count=3 |
sslmode | ssl 连接模式 | 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 库通过一系列环境变量为连接参数提供默认值,在代码中没有显式指定对应参数时,这些变量会在调用 pqconnectdb
、pqsetdblogin
或 pqsetdb
时生效;这些环境变量同样可以适用于 walreceiver 进程向主库申请建立连接的认证过程
以下是 libpq 支持的常用环境变量,更多的环境变量适用说明可以参考官方文档
https://www.postgresql.org/docs/current/libpq-envars.html
环境变量 | 作用 | 示例值 |
---|---|---|
pghost | 数据库服务器主机名或 ip | localhost |
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 |
pgsslmode | ssl 模式(disable /require 等) | require |
pgrequiressl | 强制 ssl 连接(优先用 pgsslmode ) | 1 |
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
发送启动walreceiver
的请求 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
函数则是pqconnectstartparams
和pqconnectstart
最终进行连接建立时调用的函数,该函数轮询异步连接状态,推动连接过程直至完成或失败
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
到此这篇关于postgresql 流复制认证机制的文章就介绍到这了,更多相关postgresql 流复制认证机制内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论