多页打印视图 点击此处打印.

返回常规视图.

教程

可以在Pigsty中探索的任务,以及常用的操作命令

1 - Pigsty CMDB

您可以使用 PostgreSQL 作为 Pigsty 的配置源,替代静态配置文件。

您可以使用 postgres 作为 Pigsty 的配置源,替代静态配置文件。

使用 CMDB 作为 Ansible 的动态 Inventory具有一些优点:元数据以高度结构化的方式以数据表的形式呈现,并通过数据库约束确保一致性。同时CMDB允许您使用第三方的工具来编辑管理Pigsty元数据,便于与外部系统相互集成。

目前 Pigsty 的CMDB仅支持 PostgreSQL 集群,如果您的 pigsty.yml 中包含 Redis与MatrixDB,则会报错,建议使用单独的 pigsty.yml 配置文件管理Redis与Greenplum集群。

加载配置

Pigsty CMDB的模式会在pg-meta元数据库初始化时自动创建(files/cmdb.sql),位于meta数据库的pigsty 模式中。使用bin/inventory_load可以将静态配置文件加载至CMDB中。

必须在元节点完整执行 infra.yml,安装完毕后,方可使用CMDB

usage: inventory_load [-h] [-p PATH] [-d CMDB_URL]

load config arguments

optional arguments:
  -h, --help            show this help message and exit„
  -p PATH, --path PATH  config path, ${PIGSTY_HOME}/pigsty.yml by default
  -d DATA, --data DATA  postgres cmdb pgurl, ${METADB_URL} by default

默认情况下,不带参数执行该脚本将会把$PIGSTY_HOME/pigsty.yml的名称载入默认CMDB中。

bin/inventory_load
bin/inventory_load -p files/conf/pigsty-demo.yml
bin/inventory_load -p files/conf/pigsty-dcs3.yml -d postgresql://dbuser_meta:DBUser.Meta@10.10.10.10:5432/meta

使用CMDB作为配置源

当原有配置文件加载至CMDB作为初始数据后,即可配置Ansible使用CMDB作为配置源:

bin/inventory_cmdb

您可以切换回静态配置文件:

bin/inventory_conf

修改配置源实质上是编辑Pigsty目录下的 ansible.cfg 实现的。

---
inventory = pigsty.yml
+++
inventory = inventory.sh

2 - PG 标准操作流程

本文给出了Pigsty中PGSQL数据库相关的常用运维操作命令(SOP)

大多数集群管理操作都需要使用到元节点上的管理用户,并在Pigsty根目录执行相应Ansible Playbook。以下示例如无特殊说明,均以沙箱环境,三节点集群 pg-test作为演示对象。

操作命令速查表

集群实例管理

在元节点上使用管理用户执行以下命令管理PostgreSQL集群与实例:

bin/createpg   pg-test       # 初始化PGSQL集群 pg-test
bin/createpg   10.10.10.13   # 初始化PGSQL实例 10.10.10.13
bin/reloadha   pg-test       # 调整PGSQL集群 pg-test 的负载均衡配置
bin/reloadhba  pg-test       # 调整PGSQL集群 pg-test 的认证白名单配置
bin/createuser pg-test -e pg_user=test     # 在PG集群pg-test创建用户test
bin/createdb   pg-test -e pg_database=test # 在PG集群pg-test创建数据库test

底层的相应的Ansible剧本为:

# NODES集群创建/集群扩容
./nodes.yml -l pg-test       # 集群初始化
./nodes.yml -l 10.10.10.13   # 实例初始化

# PGSQL集群创建/集群扩容
./pgsql.yml -l pg-test       # 集群初始化
./pgsql.yml -l 10.10.10.13   # 实例初始化

# PGSQL集群销毁/实例销毁
./pgsql-remove.yml -l pg-test      # 集群销毁
./pgsql-remove.yml -l 10.10.10.13  # 实例销毁

# NODES集群销毁/实例销毁
./nodes-remove.yml -l pg-test      # 集群销毁
./nodes-remove.yml -l 10.10.10.13  # 实例销毁

# PGSQL业务数据库/用户创建
./pgsql-createuser.yml -l pg-test -e pg_user=test
./pgsql-createdb.yml   -l pg-test -e pg_database=test

# 成员身份调整
./pgsql.yml -l pg-test -t pg_hba   # 调整IP白名单
./pgsql.yml -l pg-test -t haproxy_config,haproxy_reload

# 服务注册信息调整
./pgsql.yml -l pg-test -t register_prometheus
./pgsql.yml -l pg-test -t register_grafana

Patroni数据库管理

Pigsty默认使用Patroni管理PostgreSQL实例数据库。这意味着您需要使用patronictl命令来管理Postgres集群,包括:集群配置变更,重启,Failover,Switchover,重做特定实例,切换自动/手动高可用模式等。

用户可以使用patronictl在元节点上的管理用户,或任意数据库节点的dbsu执行。快捷命令pg已经在所有托管的机器上创建,用户可以使用它对所有目标Postgres集群发起管理。

常用的管理命令如下所示,更多命令请参考pg --help

pg list        [cluster]             # 打印集群信息
pg edit-config [cluster]             # 编辑某个集群的配置文件 

pg reload      [cluster] [instance]  # 重载某个集群或实例的配置
pg restart     [cluster] [instance]  # 重启某个集群或实例 
pg reinit      [cluster] [instance]  # 重置某个集群中的实例(重新制作从库)

pg pause       [cluster]             # 进入维护模式(不会触发自动故障切换)
pg resume      [cluster]             # 退出维护模式

pg failover    [cluster]             # 手工触发某集群的Failover
pg switchover  [cluster]             # 手工触发某集群的Switchover

服务组件管理

在Pigsty的部署中,所有组件均由systemd管理;PostgreSQL除外,PostgreSQL由Patroni管理。

例外的例外:当 patroni_moderemove 时例外,Pigsty将直接使用systemd管理Postgres

systemctl stop patroni            # 关闭 Patroni & Postgres
systemctl stop pgbouncer          # 关闭 Pgbouncer 
systemctl stop pg_exporter        # 关闭 PG Exporter
systemctl stop pgbouncer_exporter # 关闭 Pgbouncer Exporter
systemctl stop node_exporter      # 关闭 Node Exporter
systemctl stop haproxy            # 关闭 Haproxy
systemctl stop vip-manager        # 关闭 Vip-Manager
systemctl stop consul             # 关闭 Consul
systemctl stop postgres           # 关闭 Postgres (patroni_mode = remove )

以下组件可以通过 systemctl reload 重新加载配置

systemctl reload patroni             # 重载配置: Patroni
systemctl reload postgres            # 重载配置: Postgres (patroni_mode = remove)
systemctl reload pgbouncer           # 重载配置: Pgbouncer 
systemctl reload pg_exporter         # 重载配置: PG Exporter
systemctl reload pgbouncer_exporter  # 重载配置: Pgbouncer Exporter
systemctl reload haproxy             # 重载配置: Haproxy
systemctl reload vip-manager         # 重载配置: vip-manager
systemctl reload consul              # 重载配置: Consul

在元节点上,还可以通过 systemctl reload 重新加载基础设施组件的配置:

systemctl reload nginx          # 重载配置: Nginx (更新Haproxy管理界面索引,以及外部访问域名)
systemctl reload prometheus     # 重载配置: Prometheus (更新预计算指标计算逻辑与告警规则)
systemctl reload alertmanager   # 重载配置: Alertmanager
systemctl reload grafana-server # 重载配置: Grafana

当Patroni管理Postgres时,请不要使用 pg_ctl 直接操作数据库集簇 (/pg/data)。

您可以通过pg pause <cluster>进入维护模式后再对数据库进行手工管理。

常用命令集锦

./infra.yml -t environ             # 重新在元节点上配置环境变量与访问凭证
./infra.yml -t repo_upstream       # 重新在元节点上添加上游repo
./infra.yml -t repo_download       # 重新在元节点上下载软件包
./infra.yml -t nginx_home          # 重新生成Nginx首页内容
./infra.yml -t nginx_config,nginx_restart # 重新生成Nginx配置文件并重启应用
./infra.yml -t prometheus_config   # 重置Prometheus配置
./infra.yml -t grafana_provision   # 重置Grafana监控面板
./pgsql.yml -l pg-test -t=pgsql       # 完成数据库部署:数据库、监控、服务
./pgsql.yml -l pg-test -t=postgres    # 完成数据库部署
./pgsql.yml -l pg-test -t=service     # 完成负载均衡的部署,(Haproxy & VIP)
./pgsql.yml -l pg-test -t=pg-exporter # 完成监控部署
./pgsql.yml -l pg-test -t=pg-register # 将服务注册至基础设施
./pgsql.yml -l pg-test -t=register_prometheus # 将监控对象注册至Prometheus
./pgsql.yml -l pg-test -t=register_grafana    # 将监控目标数据源注册至Grafana

Case 1:集群创建扩容

集群创建/扩容使用剧本 pgsql.yml创建集群使用集群名作为执行对象,创建新实例/集群扩容则以集群中的单个实例作为执行对象。在使用 pgsql.yml 部署PGSQL数据库前,目标节点应当已经被 nodes.yml 剧本初始化。您可以使用 bin/createpg一次性完成两者。

集群初始化

./nodes.yml -l pg-test      # 初始化 pg-test 包含的机器节点
./pgsql.yml -l pg-test      # 初始化 pg-test 数据库集群

上述两剧本可简化为:

bin/createpg pg-test

集群扩容

假设现在有测试集群pg-test,包含两实例10.10.10.1110.10.10.12,现额外扩容一台10.10.10.13

修改配置

首先需要修改配置清单(pigsty.yml或CMDB)中的相应配置。

请一定注意集群中各实例的pg_seq 必须唯一,否则会出现身份重叠与重复。

pg-test:
  hosts:
    10.10.10.11: { pg_seq: 1, pg_role: primary }
    10.10.10.12: { pg_seq: 2, pg_role: replica }
    10.10.10.13: { pg_seq: 3, pg_role: replica, pg_offline_query: true } # 新实例
  vars: { pg_cluster: pg-test }

执行变更

然后,执行以下命令,完成集群成员的初始化

./nodes.yml -l 10.10.10.13      # 初始化 pg-test 机器节点 10.10.10.13
./pgsql.yml -l 10.10.10.13      # 初始化 pg-test 的实例 pg-test-3

# 上述两命令可简化为:
bin/createpg 10.10.10.13

调整角色

集群扩容会导致集群成员变化,请参考 Case 8:集群角色调整 将流量分发至新实例。

常见问题

常见问题1:PGSQL数据库已经存在,执行中止

Pigsty使用安全保险机制来避免误删运行中的PGSQL数据库,请使用 pgsql-remove 剧本先完成数据库实例下线,再复用该节点。如需进行紧急覆盖式安装,可使用以下参数在安装过程中强制抹除运行中实例(危险!!!)

例如:./pgsql.yml -l pg-test -e pg_clean=clean 将强制对 pg-test集群进行覆盖式安装。

常见问题2:Consul已经存在,执行中止

Pigsty使用安全保险机制来避免误删运行中的Consul实例,请使用 nodes-remove 剧本先完成节点下线,确保Consul已经移除,再复用该节点。如需进行紧急覆盖式安装,可使用以下参数在安装过程中强制抹除运行中实例(危险!!!)

常见问题3:数据库太大,扩容从库执行超时

当扩容操作卡在 Wait for postgres replica online 这一步并中止时,通常是因为已有数据库实例太大,超过了Ansible的超时等待时间。

如果报错中止,该实例仍然会继续在后台拉起从库实例,您可以使用 pg list pg-test 命令列出集群当前状态,当新从库的状态为running时,可以使用以下命令,从中止的地方继续执行Ansible Playbook:

./pgsql.yml -l 10.10.10.13 --start-at-task 'Wait for postgres replica online'

如果拉起新从库因某些意外而中止,请参考常见问题2。

常见问题4:集群处于维护模式 ,从库没有自动拉起

解决方案1,使用pg resume pg-test 将集群配置为自动切换模式,再执行从库创建操作。

解决方案2,使用pg reinit pg-test pg-test-3,手动完成实例初始化。该命令也可以用于重做集群中的现有实例

常见问题5:集群从库带有`clonefrom`标签,但因数据损坏不宜使用或拉取失败

找到问题机器,切换至postgres用户,修改 patroni 配置文件并重载生效

sudo su postgres
sed -ie 's/clonefrom: true/clonefrom: false/' /pg/bin/patroni.yml
sudo systemctl reload patroni
pg list -W # 查阅集群状态,确认故障实例没有clonefrom标签
常见问题6:如何使用现有用户创建固定的管理员用户

系统默认使用 dba 作为管理员用户,该用户应当可以从管理机通过ssh免密码登陆远程数据库节点,并免密码执行sudo命令。

如果分配的机器默认没有该用户,但您有其他的管理用户(例如vagrant)可以ssh登陆远程节点并执行sudo,则可以执行以下命令,使用其他的用户登陆远程机器并自动创建标准的管理用户:

./nodes.yml -t node_admin -l pg-test -e ansible_user=vagrant -k -K
SSH password:
BECOME password[defaults to SSH password]:

如果指定-k|--ask-pass -K|--ask-become-pass 参数,则在执行前应当输入该管理用户的SSH登陆密码与sudo密码。

执行完毕后,即可从元节点上的管理用户(默认为dba) 登陆目标数据库机器,并执行其他剧本。

偶见问题7:集群从库带有clonefrom标签,但因数据损坏不宜使用或拉取失败

找到问题机器,切换至postgres用户,修改 patroni 配置文件并重载生效

sudo su postgres
sed -ie 's/clonefrom: true/clonefrom: false/' /pg/bin/patroni.yml
sudo systemctl reload patroni
pg list -W # 查阅集群状态,确认故障实例没有clonefrom标签

Case 2:集群下线缩容

集群销毁/缩容使用专用剧本pgsql-remove ,针对集群使用时,将下线移除整个集群。针对集群中的单个实例使用时,将从集群中移除该实例。

注意,直接移除集群主库将导致集群Failover,故同时移除包含主库在内的多个实例时,建议先移除所有从库,再移除主库。

注意,pgsql-remove 剧本不受 安全保险 参数影响,会直接移除数据库实例,谨慎使用!

集群销毁

# 销毁 pg-test 集群:先销毁所有非主库实例,最后销毁主库实例
./pgsql-remove.yml -l pg-test

# 销毁集群时,一并移除数据目录与软件包
./pgsql-remove.yml -l pg-test -e rm_pgdata=true -e rm_pgpkgs=true

# 移除 pg-test 包含的节点,可选
./nodes-remove.yml -l pg-test 

集群缩容

./pgsql-remove.yml -l 10.10.10.13  # 实例销毁(缩容):销毁 pg-test 集群中的 10.10.10.13 节点 
./nodes-remove.yml -l 10.10.10.13  # 从Pigsty中移除 10.10.10.13 节点(可选)

调整角色

注意:集群缩容会导致集群成员变化,缩容时,该实例健康检查为假,原本由该实例承载的流量将立刻转由其他成员承载。但您仍需参考参考 Case 8:集群角色调整 中的说明,将该下线实例从集群配置中彻底移除。

下线Offline实例

请注意在默认配置中,如果下线了所有 pg_role = offlinepg_offline_query](/zh/docs/pgsql/config#pg_offline_query) = true 的实例,而集群中仅剩下 primary 实例。那么离线读取流量将没有实例可以承载


Case 3:集群配置变更重启

集群配置修改

修改PostgreSQL集群配置需要通过 pg edit-config <cluster> 进行,此外,还有一些特殊的控制参数需要通过Patroni进行配置与修改,例如:同步复制选项synchronous_mode,必须修改Patroni的配置项(.synchronous_mode),而非(postgresql.parameters.synchronous_mode等参数),类似的参数包括: 控制同步提交节点数量的synchronous_node_count,以及 standby_cluster.recovery_min_apply_delay

配置保存后,无需重启的配置可以通过确认生效。

请注意,pg edit-config修改的参数为集群参数,单个实例范畴的配置参数(例如Patroni的Clonefrom标签等配置)需要直接修改Patroni配置文件(/pg/bin/patroni.yml)并systemctl reload patroni生效。

请注意在Pigsty中,HBA规则由剧本自动创建并维护,请不要使用Patroni来管理HBA规则。

集群重启

需要重启的配置则需要安排数据库重启。重启集群可以使用以下命令进行:

pg restart [cluster] [instance]  # 重启某个集群或实例 

带有需重启生效的实例,在pg list <cluster>中会显示 pending restart 记号。


Case 4:集群业务用户创建

可以通过 pgsql-createuser.yml 在已有的数据库中创建新的业务用户

业务用户通常指生产环境中由软件程序所使用的用户,需要通过连接池访问数据库的用户必须通过这种方式管理。其它用户可以使用Pigsty创建与管理,亦可由用户自行维护管理。

# 在 pg-test 集群创建名为 test 的用户
./pgsql-createuser.yml -l pg-test -e pg_user=test

以上命令可以简写为:

bin/createuser pg-test test  # 在 pg-test 集群创建名为 test 的用户

如果数据库配置有OWNER,请先创建对应OWNER用户后再创建相应数据库。因此,如果需要同时创建业务用户与业务数据库,通常应当先创建业务用户。


Case 5:集群业务数据库创建

可以通过 pgsql-createdb.yml在已有的数据库集群中创建新的业务数据库

业务数据库指代由用户创建并使用的数据库对象。如果您希望通过连接池访问该数据库,则必须使用Pigsty提供的剧本进行创建,以维持连接池中的配置与PostgreSQL保持一致。

# 在 pg-test 集群创建名为 test 的数据库
./pgsql-createdb.yml   -l pg-test -e pg_database=test

以上命令可以简写为:

bin/createdb   pg-test test  # 在 pg-test 集群创建名为 test 的数据库

如果数据库配置有OWNER,请先创建对应OWNER用户后再创建相应数据库。

将新数据库注册为Grafana数据源

执行以下命令,会将pg-test集群中所有实例上所有的业务数据库作为 PostgreSQL 数据源注册入Grafana,供PGCAT应用使用。

./pgsql.yml -t register_grafana -l pg-test

Case 6:集群HBA规则调整

用户可以通过 pgsql.ymlpg_hba 子任务,调整现有的数据库集群/实例的HBA配置。

当集群发生Failover,Switchover,以及HBA规则调整时,应当重新执行此任务,将集群的IP黑白名单规则调整至期待的行为。

HBA配置由 pg_hba_rulespg_hba_rules_extra 合并生成,两者都是由规则配置对象组成的数组。样例如下:

- title: allow internal infra service direct access
  role: common
  rules:
    - host putong-confluence     dbuser_confluence     10.0.0.0/8  md5
    - host putong-jira           dbuser_jira           10.0.0.0/8  md5
    - host putong-newjira        dbuser_newjira        10.0.0.0/8  md5
    - host putong-gitlab         dbuser_gitlab         10.0.0.0/8  md5

执行以下命令,将重新生成HBA规则,并应用生效。

./pgsql.yml -t pg_hba -l pg-test

以上命令可以简写为:

bin/reloadhba pg-test

Pigsty强烈建议使用配置文件自动管理HBA规则,除非您清楚的知道自己在做什么。


Case 7:集群流量控制

Pigsty中PostgreSQL的集群流量默认由HAProxy控制,用户可以直接通过HAProxy提供的WebUI控制集群流量。

使用HAProxy Admin UI控制流量

Pigsty的HAProxy默认在9101端口(haproxy_exporter_port)提供了管理UI,该管理UI默认可以通过Pigsty的默认域名,后缀以实例名(pg_cluster-pg_seq)访问。管理界面带有可选的认证选项,由参数(haproxy_auth_enabled)启用。管理界面认证默认不启用,启用时则需要使用由 haproxy_admin_usernamehaproxy_admin_password的用户名与密码登陆。

使用浏览器访问 http://pigsty/<ins>(该域名因配置而变化,亦可从PGSQL Cluster Dashboard中点击前往),即可访问对应实例上的负载均衡器管理界面。样例界面

您可以在这里对每集群众一个服务,以及每一个后端服务器的流量进行控制。例如要将相应的Server排干,则可以选中该Server,设置 MAINT 状态并应用。如果您同时使用了多个HAProxy进行负载均衡,则需要依次在每一个负载均衡器上执行此动作。

修改集群配置

当集群发生成员变更时,您应当在合适的时候调整集群中所有成员的负载均衡配置,以如实反映集群架构变化,例如当发生主从切换后。

此外通过配置 pg_weight 参数,您可以显式地控制集群中各实例承担的负载比例,该变更需要重新生成集群中HAProxy的配置文件,并reload重载生效。例如,此配置将2号实例在所有服务中的相对权重从默认的100降为0

10.10.10.11: { pg_seq: 1, pg_role: primary}
10.10.10.12: { pg_seq: 2, pg_role: replica, pg_weight: 0 }
10.10.10.13: { pg_seq: 3, pg_role: replica,  }

使用以下命令调整集群配置并生效。

# 重新生成 pg-test 的HAProxy配置(但没有应用)
./pgsql.yml -l pg-test -t haproxy_config 

# 重新加载 pg-test 的HAProxy配置并启用生效
./pgsql.yml -l pg-test -t haproxy_config,haproxy_reload -e haproxy_reload=true 

配置与生效命令可以合并简写为:

bin/reloadha pg-test # 调整pg-test集群所有HAPROXY并重载配置,通常不会影响现有流量。

Case 8:集群角色调整

这里介绍Pigsty默认使用的HAProxy接入方式,如果您使用L4 VIP或其它方式接入,则可能与此不同。

当集群发生任何形式的角色变更,即配置清单中集群与实例的 pg_role 参数无法真实反映服务器状态时,便需要进行此项调整。

例如,集群缩容后,集群负载均衡会根据健康检查立刻重新分配流量,但不会移除下线实例的配置项

集群扩容后,已有实例的负载均衡器配置不会变化。即,您可以通过新实例上的HAProxy访问已有集群的所有成员,但旧实例上的HAProxy配置不变,因此不会将流量分发至新实例上。

1. 修改配置文件 pg_role

当集群发生了主从切换时,应当按照当前实际情况,调整集群成员的pg_role。例如,当pg-test发生了Failover或Switchover,导致pg-test-3实例变为新的集群领导者,则应当修改 pg-test-3 的角色为 primary,并将原主库 pg_role 配置为 replica

同时,您应当确保集群中至少存在一个实例能用于提供Offline服务,故为pg-test-1配置实例参数:pg_offline_query: true。 通常,非常不建议为集群配置一个以上的Offline实例,慢查询与长事务可能会导致在线只读流量受到影响。

10.10.10.11: { pg_seq: 1, pg_role: replica, pg_offline_query: true }
10.10.10.12: { pg_seq: 2, pg_role: replica }
10.10.10.13: { pg_seq: 3, pg_role: primary }

2. 调整集群实例HBA

当集群角色发生变化时,适用于不同角色的HBA规则也应当重新调整。

使用 Case 6:集群HBA规则调整 中介绍的方法,调整集群HBA规则

3. 调整集群负载均衡配置

HAProxy会根据集群中Patroni返回的健康检查结果来动态分发请求流量,因此节点故障并不会影响外部请求。但用户应当在合适的时间(比如早上睡醒后),调整集群负载均衡配置。例如:将故障彻底从集群配置中剔除,而不是以健康检查DOWN的状态继续僵死在集群中。

使用 Case 7:集群流量控制 中介绍的方法,调整集群负载均衡配置。

4.整合操作

您可以在修改配置后,使用以下命令,完成集群角色的调整。

./pgsql.yml -l pg-test -t pg_hba,haproxy_config,haproxy_reload

或使用等价的简写脚本

bin/reloadhba pg-test # 调整集群HBA配置
bin/reloadha  pg-test # 调整集群HAProxy配置

Case 9:监控对象调整

Pigsty默认使用静态文件服务发现的方式管理 Prometheus 监控对象,默认位置:/etc/prometheus/targets

使用 Consul 服务发现是可选项,在此模式下,通常无需手工管理监控对象。使用静态文件服务发现时,在执行实例上线下线时,所有的监控对象都会一并自动处理:注册或注销。但仍然有一些特殊的场景无法覆盖周全(例如修改集群名称)。

手动添加Prometheus监控对象

# 将 pg-test 集群所有成员注册为 prometheus 监控对象
./pgsql.yml -t register_prometheus -l pg-test

PostgreSQL的服务发现对象定义默认存储于所有元节点的 /etc/prometheus/targets/pgsql 目录中:每一个实例对应一个yml文件,包含目标的标签,与Exporter暴露的端口。

# pg-meta-1 [primary] @ 172.21.0.11
- labels: { cls: pg-meta, ins: pg-meta-1 }
  targets: [172.21.0.11:9630, 172.21.0.11:9100, 172.21.0.11:9631, 172.21.0.11:9101]

手工移除Prometheus监控对象

# 移除监控对象文件
rm -rf /etc/prometheus/targets/pgsql/pg-test-*.yml

手工添加Grafana数据源

# 将 pg-test 集群中的每一个数据库对象,注册为 grafana 的数据源
./pgsql.yml -t register_grafana -l pg-test

手工移除Grafana数据源

在Grafana中点击数据源管理,手工删除即可。


Case 10:集群主从切换

例如,想要在三节点演示集群 pg-test 上执行Failover,则可以执行以下命令:

pg failover <cluster>

然后按照向导提示,执行Failover即可,集群Failover后,应当参考 Case 8:集群角色调整 中的说明,修正集群角色。

执行Failover的操作记录
[08-05 17:00:30] postgres@pg-meta-1:~
$ pg list pg-test
+ Cluster: pg-test (6988888117682961035) -----+----+-----------+-----------------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Pending restart | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+
| pg-test-1 | 172.21.0.3  | Leader  | running |  1 |           |                 | clonefrom: true |
| pg-test-2 | 172.21.0.4  | Replica | running |  1 |         0 | *               | clonefrom: true |
| pg-test-3 | 172.21.0.16 | Replica | running |  1 |         0 | *               | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+

[08-05 17:00:34] postgres@pg-meta-1:~
$ pg failover pg-test
Candidate ['pg-test-2', 'pg-test-3'] []: pg-test-3
Current cluster topology
+ Cluster: pg-test (6988888117682961035) -----+----+-----------+-----------------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Pending restart | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+
| pg-test-1 | 172.21.0.3  | Leader  | running |  1 |           |                 | clonefrom: true |
| pg-test-2 | 172.21.0.4  | Replica | running |  1 |         0 | *               | clonefrom: true |
| pg-test-3 | 172.21.0.16 | Replica | running |  1 |         0 | *               | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+
Are you sure you want to failover cluster pg-test, demoting current master pg-test-1? [y/N]: y
2021-08-05 17:00:46.04144 Successfully failed over to "pg-test-3"
+ Cluster: pg-test (6988888117682961035) -----+----+-----------+-----------------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Pending restart | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+
| pg-test-1 | 172.21.0.3  | Replica | stopped |    |   unknown |                 | clonefrom: true |
| pg-test-2 | 172.21.0.4  | Replica | running |  1 |         0 | *               | clonefrom: true |
| pg-test-3 | 172.21.0.16 | Leader  | running |  1 |           | *               | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+

[08-05 17:00:46] postgres@pg-meta-1:~
$ pg list pg-test
+ Cluster: pg-test (6988888117682961035) -----+----+-----------+-----------------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Pending restart | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+
| pg-test-1 | 172.21.0.3  | Replica | running |  2 |         0 | *               | clonefrom: true |
| pg-test-2 | 172.21.0.4  | Replica | running |  2 |         0 | *               | clonefrom: true |
| pg-test-3 | 172.21.0.16 | Leader  | running |  2 |           | *               | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+-----------------+

Case 11:重置组件

俗话说,重启可以解决90%的问题,而重装可以解决剩下的10%。

面对疑难杂症,重置问题组件是一种简单有效的止损手段。使用Pigsty的初始化剧本 infra.ymlpgsql.yml 可以重置基础设施与数据库集群,但通常我们只需要使用特定的子任务来重置特定组件即可。

基础设施重置

常用的基础设施重新配置命令包括:

./infra.yml -t repo_upstream       # 重新在元节点上添加上游repo
./infra.yml -t repo_download       # 重新在元节点上下载软件包
./infra.yml -t nginx_home          # 重新生成Nginx首页内容
./infra.yml -t prometheus_config   # 重置Prometheus配置
./infra.yml -t grafana_provision   # 重置Grafana监控面板

您也可以强行重新安装这些组件

./infra.yml -t nginx      # 重新配置Nginx
./infra.yml -t prometheus # 重新配置Prometheus
./infra.yml -t grafana    # 重新配置Grafana
./infra-jupyter.yml       # 重置Jupyterlab
./infra-pgweb.yml         # 重置PGWeb

此外,您可以使用以下命令重置数据库节点上的具体组件

# 较为常用,安全的重置命令,重装监控与重新注册不会影响服务
./pgsql.yml -l pg-test -t=monitor  # 重新部署监控
./pgsql.yml -l pg-test -t=register # 重新将服务注册至基础设施(Nginx, Prometheus, Grafana, CMDB...)
./nodes.yml -l pg-test -t=consul -e dcs_clean=clean # 在维护模式下重置DCS Agent

# 略有风险的重置操作
./pgsql.yml -l pg-test -t=service    # 重新部署负载均衡,可能导致服务闪断
./pgsql.yml -l pg-test -t=pgbouncer  # 重新部署连接池,可能导致服务闪断

# 非常危险的重置任务
./pgsql.yml -l pg-test -t=postgres # 重置数据库(包括Patroni,Postgres,Pgbouncer)
./pgsql.yml -l pg-test -t=pgsql    # 重新完成完整的数据库部署:数据库、监控、服务
./nodes.yml -l pg-test -t=consul   # 当高可用自动切换模式启用时,直接重置DCS服务器

# 极度危险的重置任务
./nodes.yml -l pg-test -t=consul -e rm_dcs_servers=true  # 强制抹除DCS服务器,可能导致所有DB集群不可写入

例如,如果集群的连接池出现问题,一种兜底的止损方式便是重启或重装Pgbouncer连接池。

./pgsql.yml -l pg-test -t=pgbouncer # 重装连接池(所有用户与DB会重新生成),手工修改的配置会丢失

Case 12:替换集群DCS服务器

DCS(Consul/Etcd)本身是非常可靠的服务,一旦出现问题,其影响也是非常显著的。

按照Patroni的工作逻辑,一旦集群主库发现DCS服务器不可达,会立即遵循Fencing逻辑,将自身降级为普通从库,无法写入。

维护模式

除非当前集群处于“维护模式”(使用pg pause <cluster>进入,使用pg resume <cluster>退出)

# 使目标集群进入维护模式
pg pause pg-test

# 将目标集群恢复为自动故障切换模式(可选)
pg resume pg-test

重置数据库节点的DCS服务

当DCS故障不可用,需要迁移至新的DCS(Consul)集群时,可以采用以下操作。

首先创建新DCS集群,然后编辑配置清单 dcs_servers 填入新DCS Servers的地址。

# 强制重置目标集群上的Consul Agent(因为HA处于维护模式,不会影响新数据库集群)
./nodes.yml -l pg-test -t consul -e dcs_clean=clean

当Patroni完成重启后(维护模式中,Patroni重启不会导致Postgres关停),会将集群元数据KV写入新的Consul集群中,所以必须确保原主库上的Patroni服务首先完成重启。

# 重要!首先重启目标集群主库的Patroni,然后重启其余从库的Patroni
ansible pg-test-1 -b -a 'sudo systemctl reload patroni'
ansible pg-test-2,pg-test-3 -b -a 'sudo systemctl restart patroni'

3 - TPC-H 性能测试

TPC-H 通常用于测试系统的OLAP性能。

Build TPC-H Generator

git clone git@github.com:electrum/tpch-dbgen.git
cd tpch-dbgen/
make

Generate TPC-H Data

SCALE_FACTOR=1
export DSS_QUERY="${PWD}/queries"
export OUT_QUERY="/tmp/queries"
mkdir -p ${OUT_QUERY}

./dbgen -vf -s ${SCALE_FACTOR}



for i in {1..22}
do
    
    diff $i.sql ../queries/$i.sql  
done

for i in {1..22} do ./qgen -s 100 -d ${i} > ${OUT_QUERY}/${i}.sql
done

Prepare Database

psql postgres -c 'DROP DATABASE tpch;';
psql postgres  -c 'CREATE DATABASE tpch';
psql postgres  -c 'TRUNCATE TABLE REGION,PART CASCADE;';
# psql tpc -f dss.ddl ; psql tpc -c '\d+' ; ls -alh *.tbl
CREATE TABLE REGION
(
    R_REGIONKEY INTEGER  NOT NULL PRIMARY KEY,
    R_NAME      CHAR(25) NOT NULL,
    R_COMMENT   VARCHAR(152)
);

CREATE TABLE NATION
(
    N_NATIONKEY INTEGER  NOT NULL PRIMARY KEY,
    N_NAME      CHAR(25) NOT NULL,
    N_REGIONKEY INTEGER  NOT NULL,
    N_COMMENT   VARCHAR(152),
    FOREIGN KEY (N_REGIONKEY) REFERENCES REGION (R_REGIONKEY)
);

CREATE TABLE PART
(
    P_PARTKEY     INTEGER        NOT NULL PRIMARY KEY,
    P_NAME        VARCHAR(55)    NOT NULL,
    P_MFGR        CHAR(25)       NOT NULL,
    P_BRAND       CHAR(10)       NOT NULL,
    P_TYPE        VARCHAR(25)    NOT NULL,
    P_SIZE        INTEGER        NOT NULL,
    P_CONTAINER   CHAR(10)       NOT NULL,
    P_RETAILPRICE DECIMAL(15, 2) NOT NULL,
    P_COMMENT     VARCHAR(23)    NOT NULL
);

CREATE TABLE SUPPLIER
(
    S_SUPPKEY   INTEGER        NOT NULL PRIMARY KEY,
    S_NAME      CHAR(25)       NOT NULL,
    S_ADDRESS   VARCHAR(40)    NOT NULL,
    S_NATIONKEY INTEGER        NOT NULL,
    S_PHONE     CHAR(15)       NOT NULL,
    S_ACCTBAL   DECIMAL(15, 2) NOT NULL,
    S_COMMENT   VARCHAR(101)   NOT NULL,
    FOREIGN KEY (S_NATIONKEY) REFERENCES NATION (N_NATIONKEY)
);

CREATE TABLE PARTSUPP
(
    PS_PARTKEY    INTEGER        NOT NULL,
    PS_SUPPKEY    INTEGER        NOT NULL,
    PS_AVAILQTY   INTEGER        NOT NULL,
    PS_SUPPLYCOST DECIMAL(15, 2) NOT NULL,
    PS_COMMENT    VARCHAR(199)   NOT NULL,
    PRIMARY KEY (PS_PARTKEY, PS_SUPPKEY),
    FOREIGN KEY (PS_SUPPKEY) REFERENCES SUPPLIER (S_SUPPKEY),
    FOREIGN KEY (PS_PARTKEY) REFERENCES PART (P_PARTKEY)
);

CREATE TABLE CUSTOMER
(
    C_CUSTKEY    INTEGER        NOT NULL PRIMARY KEY,
    C_NAME       VARCHAR(25)    NOT NULL,
    C_ADDRESS    VARCHAR(40)    NOT NULL,
    C_NATIONKEY  INTEGER        NOT NULL,
    C_PHONE      CHAR(15)       NOT NULL,
    C_ACCTBAL    DECIMAL(15, 2) NOT NULL,
    C_MKTSEGMENT CHAR(10)       NOT NULL,
    C_COMMENT    VARCHAR(117)   NOT NULL,
    FOREIGN KEY (C_NATIONKEY) REFERENCES NATION (N_NATIONKEY)
);

CREATE TABLE ORDERS
(
    O_ORDERKEY      INTEGER        NOT NULL PRIMARY KEY,
    O_CUSTKEY       INTEGER        NOT NULL,
    O_ORDERSTATUS   CHAR(1)        NOT NULL,
    O_TOTALPRICE    DECIMAL(15, 2) NOT NULL,
    O_ORDERDATE     DATE           NOT NULL,
    O_ORDERPRIORITY CHAR(15)       NOT NULL,
    O_CLERK         CHAR(15)       NOT NULL,
    O_SHIPPRIORITY  INTEGER        NOT NULL,
    O_COMMENT       VARCHAR(79)    NOT NULL,
    FOREIGN KEY (O_CUSTKEY) REFERENCES CUSTOMER (C_CUSTKEY)
);

CREATE TABLE LINEITEM
(
    L_ORDERKEY      INTEGER        NOT NULL,
    L_PARTKEY       INTEGER        NOT NULL,
    L_SUPPKEY       INTEGER        NOT NULL,
    L_LINENUMBER    INTEGER        NOT NULL,
    L_QUANTITY      DECIMAL(15, 2) NOT NULL,
    L_EXTENDEDPRICE DECIMAL(15, 2) NOT NULL,
    L_DISCOUNT      DECIMAL(15, 2) NOT NULL,
    L_TAX           DECIMAL(15, 2) NOT NULL,
    L_RETURNFLAG    CHAR(1)        NOT NULL,
    L_LINESTATUS    CHAR(1)        NOT NULL,
    L_SHIPDATE      DATE           NOT NULL,
    L_COMMITDATE    DATE           NOT NULL,
    L_RECEIPTDATE   DATE           NOT NULL,
    L_SHIPINSTRUCT  CHAR(25)       NOT NULL,
    L_SHIPMODE      CHAR(10)       NOT NULL,
    L_COMMENT       VARCHAR(44)    NOT NULL,
    PRIMARY KEY (L_ORDERKEY, L_LINENUMBER),
    FOREIGN KEY (L_ORDERKEY) REFERENCES ORDERS (O_ORDERKEY),
    FOREIGN KEY (L_PARTKEY, L_SUPPKEY) REFERENCES PARTSUPP (PS_PARTKEY, PS_SUPPKEY)
);

CREATE INDEX ON ORDERS (O_ORDERDATE);
CREATE INDEX ON LINEITEM (L_SHIPDATE);
CREATE INDEX ON LINEITEM (L_PARTKEY, L_SUPPKEY);
-- ALTER TABLE lineitem ADD FOREIGN KEY(l_partkey, l_suppkey) REFERENCES partsupp(ps_partkey, ps_suppkey);
sed 's/|$//' region.tbl    | psql tpch -c "COPY region    FROM STDIN DELIMITER '|';"
sed 's/|$//' nation.tbl    | psql tpch -c "COPY nation    FROM STDIN DELIMITER '|';"       
sed 's/|$//' supplier.tbl  | psql tpch -c "COPY supplier  FROM STDIN DELIMITER '|';"
sed 's/|$//' part.tbl      | psql tpch -c "COPY part      FROM STDIN DELIMITER '|';"
sed 's/|$//' customer.tbl  | psql tpch -c "COPY customer  FROM STDIN DELIMITER '|';"       
sed 's/|$//' partsupp.tbl  | psql tpch -c "COPY partsupp  FROM STDIN DELIMITER '|';"
sed 's/|$//' orders.tbl    | psql tpch -c "COPY orders    FROM STDIN DELIMITER '|';"
sed 's/|$//' lineitem.tbl  | psql tpch -c "COPY lineitem  FROM STDIN DELIMITER '|';"

Prepare Queries

git clone https://github.com/2ndQuadrant/pg-tpch.git
cd pg-tpch/queries
psql tpc

\i q01.analyze.sql
\i q02.analyze.sql
\i q03.analyze.sql
\i q04.analyze.sql
\i q05.analyze.sql
\i q06.analyze.sql
\i q07.analyze.sql
\i q08.analyze.sql
\i q09.analyze.sql
\i q10.analyze.sql
\i q11.analyze.sql
\i q12.analyze.sql
\i q13.analyze.sql
\i q14.analyze.sql
\i q15.analyze.sql
\i q16.analyze.sql
\i q17.analyze.sql
\i q18.analyze.sql
\i q19.analyze.sql
\i q20.analyze.sql
\i q21.analyze.sql
\i q22.analyze.sql
\i q01.explain.sql
\i q02.explain.sql
\i q03.explain.sql
\i q04.explain.sql
\i q05.explain.sql
\i q06.explain.sql
\i q07.explain.sql
\i q08.explain.sql
\i q09.explain.sql
\i q10.explain.sql
\i q11.explain.sql
\i q12.explain.sql
\i q13.explain.sql
\i q14.explain.sql
\i q15.explain.sql
\i q16.explain.sql
\i q17.explain.sql
\i q18.explain.sql
\i q19.explain.sql
\i q20.explain.sql
\i q21.explain.sql
\i q22.explain.sql
-- enhance with index
ALTER TABLE region ADD PRIMARY KEY (r_regionkey);
ALTER TABLE nation ADD PRIMARY KEY (n_nationkey);
ALTER TABLE customer ADD PRIMARY KEY (c_custkey);
ALTER TABLE supplier ADD PRIMARY KEY (s_suppkey);
ALTER TABLE part ADD PRIMARY KEY (p_partkey);
ALTER TABLE orders ADD PRIMARY KEY (o_orderkey);
ALTER TABLE partsupp ADD PRIMARY KEY (ps_partkey, ps_suppkey);
ALTER TABLE lineitem ADD PRIMARY KEY (l_orderkey, l_linenumber);

ALTER TABLE nation ADD FOREIGN KEY(n_regionkey) REFERENCES region(r_regionkey);
ALTER TABLE customer ADD FOREIGN KEY(c_nationkey) REFERENCES nation(n_nationkey);
ALTER TABLE supplier ADD FOREIGN KEY(s_nationkey) REFERENCES nation(n_nationkey);
ALTER TABLE orders ADD FOREIGN KEY(o_custkey) REFERENCES customer(c_custkey);
ALTER TABLE customer ADD FOREIGN KEY(c_suppkey) REFERENCES supplier(s_suppkey);
ALTER TABLE partsupp ADD FOREIGN KEY(ps_partkey) REFERENCES part(p_partkey);
ALTER TABLE lineitem ADD FOREIGN KEY(l_orderkey) REFERENCES orders(o_orderkey);
ALTER TABLE lineitem ADD FOREIGN KEY(l_partkey, l_suppkey) REFERENCES partsupp(ps_partkey, ps_suppkey);

TPC-H

Apple M1 Max (10C 64G 8TB)

s = 10 , s = 100

查询 10仓耗时 100仓耗时
1 3.9 94
2 0.9 21
3 1.4 94
4 5.0 247
5 1.3 123
6 1.8 79
7 1.2 109
8 0.9 288
9 4.6 1346
10 1.7 209
11 0.7 32
12 1.1 115
13 11.8 214
14 0.7 111
15 3.7 189
16 1.5 140
17 7.5 605
18 37.4 844
19 0.3 36
20 1.7 653
21 2.2 215
22 1.0 68

4 - CA自签名

CA用于签发证书

Pigsty默认会在初始化时,在管理节点生成一个自签名的CA。

CA密钥放置于元节点的 /etc/pigsty/ca/ 目录中。

当您需要使用 SSL,数字签名,高级安全特性时,可以使用此 CA。

如果您已经有了自己的 CA 公私钥,将其放置于 files/ 目录,并将 ca_method 修改为 copy,则Pigsty会使用用户提供的CA证书与私钥。

FHS规约

/etc/pki              # CA与证书目录
/etc/pki/ca           # CA证书目录

/etc/pki/ca/cert           # CA证书目录

常见操作

查看证书的内容:

openssl x509 -text -in /etc/pigsty/ca/ca.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            0b:e1:0b:39:c9:67:56:e5:b2:3d:6d:db:6a:b4:74:c5:50:0e:00:4e
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: O=Pigsty CA, CN=pigsty-ca
        Validity
            Not Before: Jul 13 07:52:21 2022 GMT
            Not After : Jun 19 07:52:21 2122 GMT
        Subject: O=Pigsty CA, CN=pigsty-ca
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
                Modulus:
                    ........................................
                    73:09:b5
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:pigsty-ca
            X509v3 Key Usage:
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:1
            X509v3 Subject Key Identifier:
                ........................................
    Signature Algorithm: sha256WithRSAEncryption
         ........................................
-----BEGIN CERTIFICATE-----
........................................
-----END CERTIFICATE-----

```

5 - PG 备份与恢复

备份是DBA的安身立命之本,也是数据库管理中最为关键的工作之一。

故障大体可以分为两类:硬件故障/资源不足(坏盘/宕机),软件缺陷/人为错误(删库/删表)。基于主从复制的物理复制用于应对前者,延迟从库与冷备份通常用于应对后者。

Pigsty提供了完善的备份支持,无需配置即可使用开箱即用的主从物理复制,绝大多数物理故障均可自愈。同时,还提供了延迟备库与冷备份支持,用于应对软件故障与人为误操作。

物理复制

在Pigsty中,可以通过为集群中的数据库实例指定角色( pg_role ),即可以创建物理复制备份,用于从机器与硬件故障中恢复。例如以下配置声明了一个一主两从的高可用数据库集群。

pg-test:
  hosts:
    10.10.10.11: { pg_seq: 1, pg_role: primary } # 主库
    10.10.10.12: { pg_seq: 2, pg_role: replica } # 热备(承载在线只读流量)
    10.10.10.13: { pg_seq: 3, pg_role: offline } # 温备(不承载在线流量)
  vars:
    pg_cluster: pg-test

热备

replica = Hot Standby,承载只读流量,与主库保持实时同步,但可能存在微量复制延迟。

与主库保持一致,当主库出现故障时会接管主库的工作,同时也会用于承接线上只读流量。其中,采用同步复制与主库保持实时一致的热备又可以称为同步备份。正常情况下,物理复制的复制延迟视网络条件与负载水平,可能在1ms-100ms/几十KB ~ 几MB的范围。请参考主从集群

温备

offline = Warm Standby,温备,不承担在线流量。备用,或仅用于离线/分析查询。

温备(Warm Standby):与热备类似,但不承载线上流量。请参考离线从库部署

同步备库

standby = Sync Standby,与主库保持严格实时同步。

使用同步提交的从库,又称做同步备库,详情请参考同步从库部署

延迟从库

延迟从库相比是一种快速应对软件故障/人为错误的措施。延迟从库采用标准的主从流复制机制从主库实时接收变更,但会延迟一段特定时间(例如1小时,一天)后再执行应用。因此在状态上,是原始主库的历史状态副本。当出现诸如误删数据这类问题时,实时主从同步会立即将此类变更同步至所有物理副本,但延迟从库则提供了一个抢救时间窗口:您可以立即从延迟从库中查询出数据并回补原主库。

高可用与主从复制可以解决机器硬件故障带来的问题,但无法解决软件Bug与人为操作导致的故障,例如:误删库删表。误删数据通常需要用到冷备份,但另一种更优雅高效快速的方式是事先准备一个延迟从库。

您可以使用 备份集群 的功能创建延时从库,例如,现在您希望为pg-test 集群指定一个延时从库:pg-testdelay,该集群是pg-test1小时前的状态。因此如果出现了误删数据,您可以立即从延时从库中获取并回灌入原始集群中。

# pg-test是原始数据库
pg-test:
  hosts:
    10.10.10.11: { pg_seq: 1, pg_role: primary }
  vars:
    pg_cluster: pg-test
    pg_version: 14

# pg-testdelay 将作为 pg-test 库的延时从库
pg-testdelay:
  hosts:
    10.10.10.12: { pg_seq: 1, pg_role: primary , pg_upstream: 10.10.10.11 } # 实际角色为 Standby Leader
  vars:
    pg_cluster: pg-testdelay
    pg_version: 14          

创建完毕后,在元节点使用 pg edit-config pg-testdelay编辑延时集群的Patroni配置文件,修改 standby_cluster.recovery_min_apply_delay 为你期待的值,例如1h,应用即可。

 standby_cluster:
   create_replica_methods:
   - basebackup
   host: 10.10.10.11
   port: 5432
+  recovery_min_apply_delay: 1h

冷备份

冷备份是最后的兜底机制,您可能几年都用不上一次,但真用上的时候,可以救命。

冷备(Code Backup):冷备数据库以数据目录静态文件的形式存在,是数据库目录的二进制备份。便于制作,管理简单,便于放到其他AZ实现容灾。误删库误删表,或整集群/整机房出现灾难性故障时,数据备份(冷备)是最后的兜底。

Pigsty提供了一个制作冷备份的脚本 pg-backup,在数据库节点上以dbsu身份执行,即可创建当前实例的全量物理备份,并放置于 /pg/backup 目录中(默认位于 {{ pg_fs_bkup }}/backup)。 用户可以通过参数来指定备份的数据库URL,备份目录,文件名,加密方式,已有备份的保留策略等。

$ pg-backup # 不带任何参数执行备份脚本
[2021-08-05 17:41:35][INFO] ================================================================
[2021-08-05 17:41:35][INFO] [INIT] pg-backup begin, checking parameters
[2021-08-05 17:41:35][DEBUG] [INIT] #====== BINARY
[2021-08-05 17:41:35][DEBUG] [INIT] pg_basebackup     :   /usr/pgsql/bin/pg_basebackup
[2021-08-05 17:41:35][DEBUG] [INIT] openssl           :   /bin/openssl
[2021-08-05 17:41:35][DEBUG] [INIT] #====== PARAMETER
[2021-08-05 17:41:35][DEBUG] [INIT] filename  (-f)    :   backup_pg-meta_20210805.tar.lz4
[2021-08-05 17:41:35][DEBUG] [INIT] src       (-s)    :   postgres:///
[2021-08-05 17:41:35][DEBUG] [INIT] dst       (-d)    :   /pg/backup
[2021-08-05 17:41:35][DEBUG] [INIT] tag       (-t)    :   pg-meta
[2021-08-05 17:41:35][DEBUG] [INIT] key       (-k)    :   pg-meta
[2021-08-05 17:41:35][DEBUG] [INIT] encrypt   (-e)    :   false
[2021-08-05 17:41:35][DEBUG] [INIT] upload    (-u)    :   false
[2021-08-05 17:41:35][DEBUG] [INIT] remove    (-r)    :   -mmin +1200
[2021-08-05 17:41:35][INFO] [LOCK] acquire lock @ /tmp/backup.lock
[2021-08-05 17:41:35][INFO] [LOCK] lock acquired success on /tmp/backup.lock, pid=25438
[2021-08-05 17:41:35][INFO] [BKUP] backup begin, from postgres:/// to /pg/backup/backup_pg-meta_20210805.tar.lz4
[2021-08-05 17:41:35][INFO] [BKUP] backup in normal mode
pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/6B000028 on timeline 1
pg_basebackup: write-ahead log end point: 0/6B000138
pg_basebackup: syncing data to disk ...
pg_basebackup: base backup completed
[2021-08-05 17:41:45][INFO] [BKUP] backup complete!
[2021-08-05 17:41:45][INFO] [RMBK] remove local obsolete backup: 1200
[2021-08-05 17:41:45][INFO] [BKUP] find obsolete backups: find /pg/backup/ -maxdepth 1 -type f -mmin +1200 -name 'backup*.lz4'
[2021-08-05 17:41:45][WARN] [BKUP] remove obsolete backups:
[2021-08-05 17:41:45][INFO] [RMBK] remove old backup complete
[2021-08-05 17:41:45][INFO] [LOCK] release lock @ /tmp/backup.lock
[2021-08-05 17:41:45][INFO] [DONE] backup procdure complete!
[2021-08-05 17:41:45][INFO] ================================================================

该脚本将使用 pg_basebackup 从指定的PGURL(默认为本地数据库实例)发起备份,使用tar归档与lz4压缩,并加以可选的openssl RC4流加密。

备份文件默认放置于/pg/backup/目录下,默认文件名由前缀,集群名,日期组成,形如:backup_pg-meta_20210805.tar.lz4

默认的备份清理策略是当最新备份完成时,会清理掉1200分钟(20小时前)的旧备份文件。

您需要根据自己的业务情况,使用该脚本制作备份并放置在合适的地方,例如专用的对象存储集群/NFS/或者本地备份盘。如果您希望将数据库回溯至任意时刻,而非仅仅回滚至数据库备份时刻,则还需要对集群WAL日志进行归档。因为此功能需要具体功能具体分析,因此Pigsty只提供工具与机制,不提供具体策略与实现。您可以使用配置于节点上的Crontab与本地目录作为最基本的冷备份实现。

node_crontab: # 每日凌晨1点执行一次全量备份
  - '00 01 * * * postgres /pg/bin/pg-backup 2>>/pg/log/backup.log'

从冷备份中恢复

需要使用该备份时,您需要将PG集群设置为维护模式(pg pause <cluster>),停止数据集群主库并清空数据集簇目录,然后冷备份文件解压至/pg/data中,相关命令如下所示。

# 找到最新的备份文件并打印信息
backup_dir="/pg/backup"
data_dir=/pg/data
backup_latest=$(ls -t ${backup_dir} | head -n1)
echo "backup ${backup_latest} will be used"

# 暂停Patroni,关停数据库,移除数据目录(危险)
pg pause pg-meta
pg_ctl -D /pg/data stop
rm -rf /pg/data/*     # 清空数据目录(危险)

# 解压备份至数据库目录
echo "unlz4 -d -c ${backup_dir}/${backup_latest} | tar -xC ${data_dir}"
unlz4 -d -c ${backup_dir}/${backup_latest} | tar -xC ${data_dir}    # 解压至数据库目录
# 可选:如果加密时设置了密码,则需要先解密再解压
openssl enc -rc4 -d -k ${PASSWORD} -in ${backup_latest} | unlz4 -d -c | tar -xC ${data_dir}

# 重新拉起数据库
systemctl restart patroni

# 重做集群的其他从库
pg reinit <cluster> # 依次重置集群的其他实例成员

您也可以使用冷备份创建新的集群进行PITR,或使用 pg_probackup 与 pg_backrest 等工具进行外部备份管理。

6 - PG 高可用演练

您可以通过高可用场景演练,来加强对集群高可用能力的信心。

您可以通过高可用场景演练,来加强对集群高可用能力的信心。

以下列出了24种典型的高可用故障场景,分为主库故障,从库故障,DCS故障三类,每类8个具体场景。

所有演练均假设高可用自动切换模式已启用,其中,Patroni应当正确处理主库故障与从库故障。

编号 案例名称 自动模式 手动切换
A 主库故障
1A 主库节点宕机 自动Failover 人工切换
2A 主库Postgres进程关停(pg_ctl or kill -9 自动Failover 人工重启
3A 主库Patroni进程正常关停(systemctl stop patroni 自动Failover 人工重启
4A 主库Patroni进程异常关停(kill -9 需要确认 无影响
5A 主库负载打满,假死(watchdog) 需要确认 无影响
6A 主库DCS Agent不可用(systemctl stop consul 集群主库降级 无影响
7A 主库网络抖动 超时自动Failover 需观察
8A 误删主库数据目录 自动Failover 手工切换
B 从库故障(1/n , n>1)
1B 从库节点宕机 无影响 无影响
2B 从库Postgres进程关停(pg_ctl or kill -9 无影响 无影响
3B 从库Postgres进程手工关停 (pg_ctl 无影响 无影响
4B 从库Patroni进程异常Kill(kill -9 无影响 无影响
5B 从库DCS Agent不可用(systemctl stop consul 无影响 无影响
6B 从库负载打满,假死 Depends Depends
7B 从库网络抖动 无影响 无影响
8B 误提升一个从库(pg_ctl promte 自动恢复 脑裂
C DCS故障
1C DCS Server完全不可用(多数节点不可用) 所有集群主库降级 无影响
2C DCS通主库,不通从库(1主1从) 无影响 无影响
3C DCS通主库,不通从库(1主n从,n>1) 无影响 无影响
4C DCS通从库,不通主库(1主1从) 无影响 无影响
5C DCS通从库,不通主库(1主n从,n>1) 自动Failover 无影响
6C DCS网络抖动:同时中断,
主库从库同时恢复,或主库先恢复
无影响 无影响
7C DCS网络抖动:同时中断,
从库先恢复,主库后恢复(1主1从)
无影响* 无影响
8C DCS网络抖动:同时中断,
从库先恢复,主库后恢复(1主n从,n>1)
超过TTL自动Failover 无影响

演练环境说明

以下以本地Pigsty 四节点沙箱作为演练对象。

准备负载

在演练中,您可以使用pgbench生成虚拟负载,观察负载流量在各种故障下的状态。

make test-ri     # 在 pg-test集群初始化 pgbench 表
make test-rw     # 生成 pgbench 写入流量
make test-ro     # 生成 pgbench 只读流量

如果您希望仿真其他样式的流量,可以直接调整负载生成的命令并执行。

# 4条连接,总计64读写TPS
while true; do pgbench -nv -P1 -c4 --rate=64 -T10 postgres://test:test@pg-test:5433/test; done

# 8条连接,总计512只读TPS
while true; do pgbench -nv -P1 -c8 --select-only --rate=512 -T10 postgres://test:test@pg-test:5434/test; done

观察状态

PGSQL Cluster 面板提供了关于pg-test集群的重要监控信息,您可以查阅最近5-15分钟的指标,并设置为每5秒自动刷新。

Pigsty的监控指标采集周期默认为10秒,而Patroni主从切换的典型耗时通常在几秒到十几秒之间。您可以使用patronictl来获取亚秒级别的观测精度:

pg list pg-test          # 查看 pg-test 集群状态(在单独的窗口中)
pg list pg-test -w 0.1   # 查看 pg-test 集群状态,每0.1s刷新一次

您可以开启四个Terminal窗口,分别用于:

  • 在元节点上执行管理命令(用来触发模拟故障的命令)
  • 发起并观察读写请求负载(pgbench
  • 发起并观察只读请求负载(pgbench --select-only
  • 实时查阅集群主从状态(pg list

主库故障演练

1A-主库节点宕机

操作说明

ssh 10.10.10.3 sudo reboot    # 直接将 pg-test-1 主节点重启(VIP指向实际主节点)

操作结果

Patroni可以正常处理主库宕机,执行自动Failover。

当集群处于维护模式时,则需要人工介入处理(人工执行pg failover <cluster>

patronictl list 结果
# 正常情况:pg-test-3 是当前集群主库,时间线为3(此集群已经经历过两次Failover)
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  3 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  3 |         0 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Leader  | running |  3 |           | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# ssh 10.10.10.13 sudo reboot 将 pg-test-3 主实例所在节点重启,pg-test-3 实例的Patroni从集群中消失
# 下线超过TTL后,pg-test-1实例抢到Leader Key,成为新的集群领导者。
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | running |  3 |           | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  3 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# pg-test-1实例完成Promote,成为集群的新领导者,时间线变为4
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | running |  4 |           | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  3 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# pg-test-2 实例将自己的上游修改为新领导者 pg-test-1 ,时间线由3变为4,进入新时代,看齐新核心。
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | running |  4 |           | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  4 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# pg-test-3 完成重启,Postgres处于停止状态,Patroni重新加入集群中
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | running |  4 |           | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  4 |         1 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | stopped |    |   unknown | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# pg-test-3 上的Postgres以从库身份被拉起,从新主库 pg-test-1 同步数据。
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | running |  4 |           | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  4 |         1 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | running |  3 |        10 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# pg-test-3 追赶上新领导者,时间线进入4,与新领导保持同步。
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | running |  4 |           | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  4 |         1 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | running |  4 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

2A-主库Postgres进程关停

操作说明

采用两种不同的方式关停主库 Postgres 实例:常规的 pg_ctl 与暴力的 kill -9

# 关停主库上的Postgres主进程
ssh 10.10.10.3 'sudo -iu postgres /usr/pgsql/bin/pg_ctl -D /pg/data stop'

# 查询主库PID并强行Kill
ssh 10.10.10.3 'sudo kill -9 $(sudo cat /pg/data/postmaster.pid | head -n1)'

操作结果

关停 Postgres 后,Patroni 会尝试重新拉起 Postgres 进程。如果成功,则集群恢复正常。

如果无法正常拉起 PostgreSQL 进程,则集群会自动进行Failover。

patronictl list 结果
# 主库实例被强制Kill后,状态显示为crashed,而后立刻被重新拉起,恢复为Running
+ Cluster: pg-test (7037005266924312648) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | crashed |    |           | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  7 |         0 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | running |  7 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# 如果持续Kill主库,导致主库拉起失败(状态变为start failed),那么就会触发Failover
+ Cluster: pg-test (7037005266924312648) ----------+----+-----------+-----------------+
| Member    | Host        | Role    | State        | TL | Lag in MB | Tags            |
+-----------+-------------+---------+--------------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running      | 11 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Leader  | running      | 12 |           | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | start failed |    |   unknown | clonefrom: true |
+-----------+-------------+---------+--------------+----+-----------+-----------------+

3A-主库Patroni进程正常关停

操作说明

# 关停主库上的Postgres主进程
ssh 10.10.10.3 'sudo systemctl stop patroni'

操作结果

通过常规方式关停主库Patroni,会导致Patroni所管理PostgreSQL实例一并关闭,并立即触发集群Failover。

在维护模式下通过正常方式关停Patroni,关闭Patroni不会影响所托管的PostgreSQL实例,这可以用于重启Patroni以重载配置(例如更换使用的DCS)。

patronictl list 结果
# 主库Patroni (pg-test-3) 关停后
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  2 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  2 |         0 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Leader  | running |  2 |           | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# pg-test-3 进入 stopped 状态
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  2 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Replica | running |  2 |         0 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | stopped |    |   unknown | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# 新主库 pg-test-2 当选,时间线从2进入3
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  2 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Leader  | running |  3 |           | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | stopped |    |   unknown | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# 另一个健康从库 pg-test-1 重新追随新主库 pg-test-2 进入时间线3,老主库 pg-test-3 在一段时间后,从集群中消失
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  3 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Leader  | running |  3 |           | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# 使用 systemctl start patroni 重新拉起老主库 pg-test-3,该实例自动进入复制模式,追随新领导者。
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  3 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Leader  | running |  3 |           | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | running |  3 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

4A-主库Patroni进程异常关停

这种情况需要特别关注!

如果使用Kill -9 强行杀死主库Patroni,则主库Patroni有大概率无法关停所管理的PostgreSQL主库实例。这会导致原主库PostgreSQL 实例在Patroni死亡后继续存活,而剩余的集群从库则会进行领导选举选出新的主库来,从而导致脑裂

操作说明

# 关停主库上的Patroni主进程
ssh 10.10.10.3 "ps aux | grep /usr/bin/patroni | grep -v grep | awk '{print $2}'"
ssh 10.10.10.3 'sudo kill -9 723'

操作结果

该操作可能导致集群脑裂:因为Patroni暴死,无暇杀死自己管理的PostgreSQL进程。而其他集群成员则会在TTL超时后进行新一轮选举,选出新的主库。

如果您采用标准的基于负载均衡健康检查的服务接入机制,不会有问题,因为原主库 Patroni已死,健康检查为假。即使该主库存活,负载均衡器也不会将流量分发至此实例。但如果您通过其他方式继续写入该主库,则可能会出现脑裂

Patroni使用Watchdog机制对这种情况进行兜底,您需要视情况使用(参数 patroni_watchdog_mode )。启用watchdog时,如果原主库因为各种原因(Patroni暴死,机器负载假死,虚拟机调度,PG关机太慢)等原因,无法在Failover中及时关停PG主库以避免脑裂,则会使用Linux内核模块softdog强制关机以免脑裂。

patronictl list 结果
# 使用Kill -9 强杀 主库Patroni (pg-test-2) 
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  3 |         0 | clonefrom: true |
| pg-test-2 | 10.10.10.12 | Leader  | running |  3 |           | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | running |  3 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+
# 因为Patroni暴死,PostgreSQL进程通常仍然会存活并且为主库状态
# 因为Patroni暴死,原主库的健康检查会立刻失败,导致主库流量没有实例承载,集群不可写入。

# 因为Patroni暴死,无暇释放 DCS中的Leader Key,因此上面的状态会保持TTL的时间。
# 直到 DCS 中的 Leader Lease 因为超时被释放(约15s),集群才意识到主库已死,发起Failover
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Replica | running |  3 |         0 | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | running |  3 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# 集群触发Failover,pg-test-1 成为新的集群领导者,开始承载只读流量,集群写入服务恢复
+ Cluster: pg-test (7037370797549923387) -----+----+-----------+-----------------+
| Member    | Host        | Role    | State   | TL | Lag in MB | Tags            |
+-----------+-------------+---------+---------+----+-----------+-----------------+
| pg-test-1 | 10.10.10.11 | Leader  | running |  4 |           | clonefrom: true |
| pg-test-3 | 10.10.10.13 | Replica | running |  3 |         0 | clonefrom: true |
+-----------+-------------+---------+---------+----+-----------+-----------------+

# 此时必须注意!原集群主库仍然存活并允许写入!!!
# 如果您采用标准的基于负载均衡健康检查的流量分发机制则不会有问题,因为Patroni已死,健康检查为假。
# 该主库存活,但负载均衡器不会将流量分发至此实例。但如果您通过其他方式直接写入该主库,则会出现脑裂!
$ psql -AXtwh 10.10.10.12 -d postgres -c 'select pg_is_in_recovery();'
t

从这种情况中恢复

当Patroni暴死时,您应当首先手工关闭由其管理的,仍在运行的原PostgreSQL主库实例。然后再重新启动Patroni,并由Patroni拉起PostgreSQL实例,如下所示:

/usr/pgsql/bin/pg_ctl -D /pg/data stop
systemctl restart patroni

如若不然,则可能出现Patroni无法正常启动的错误:

2021-12-03 14:16:18 +0800 INFO:  stderr=2021-12-03 14:16:18.752 HKT [7852] FATAL:  lock file "postmaster.pid" already exists
2021-12-03 14:16:18.752 HKT [7852] HINT:  Is another postmaster (PID 887) running in data directory "/pg/data"?

参数 patroni_watchdog_mode 的说明:

  • 如果模式为 required,但/dev/watchdog不可用,不会影响Patroni启动,只会影响当前实例的领导候选人资格。
  • 如果模式为 required,但/dev/watchdog不可用,那么该实例无法作为合格的主库候选人,即无法参与Failover,即使手工强制指定也不行:会出现Switchover failed, details: 412, switchover is not possible: no good candidates have been found 的错误。若想解决此问题,修改/pg/bin/patroni.yml文件的patroni_watchdog选项为automatic|off即可。
  • 如果模式为automatic,则没有限制,无论/dev/watchdog可不可用,该实例都可以正常参选主库选举。
  • /dev/watchdog 可用需要两个条件,加载softdog内核模块,/dev/watchdog的属主为postgres(dbsu)

5A-主库DCS Agent不可用

在这种情况下,主库上的Patroni会因为无法连接至DCS服务,将自身降级为普通从库,但如果从库Patroni仍然意识到主库存活(例如,流复制仍然正常进行),并不会触发Failover!

在这种情况下,Pigsty的接入机制会因为原主库健康检查为假,而导致整个集群进入无主状态,无法写入,需要特别关注

在维护模式下,不会有变化发生。

6A-主库负载打满,假死

TBD

7A-主库网络抖动

8A-误删主库数据目录


从库故障演练

1B-从库节点宕机

操作说明

ssh 10.10.10.3 sudo reboot    # 直接将 pg-test-1 主节点重启(VIP指向实际主节点)

操作结果

从库宕机会导致该节点上的 HAPorxy PatroniPostgres 等服务不可用。通常业务侧会察觉到极少量的瞬时报错(与故障实例的连接会中断),而后集群中的其他负载均衡器会将此故障节点从后端列表中摘除。

请注意,如果集群为一主一从结构,且唯一一台从库宕机,那么离线查询服务可能会受到影响(没有可用承载实例)。

节点重启完成后,Patroni服务会自动拉起,实例会自动重新加入集群中。

2B-从库Postgres进程关停

操作说明

采用两种不同的方式关停从库 Postgres 实例:常规的 pg_ctl 与暴力的 kill -9

# 关停从库上的Postgres主进程
ssh 10.10.10.3 'sudo -iu postgres /usr/pgsql/bin/pg_ctl -D /pg/data stop'

# 查询从库PID并强行Kill
ssh 10.10.10.3 'sudo kill -9 $(sudo cat /pg/data/postmaster.pid | head -n1)'

操作结果

关停 Postgres 后,Patroni 会尝试重新拉起 Postgres 进程。如果成功,则集群恢复正常。如果

从库 宕机会导致该实例健康检查为Down,集群的负载均衡器不会将流量再分发至该实例,应用只读请求会有少量瞬时报错。

3B-从库Postgres进程手工关停

4B-从库Patroni进程异常Kill

5B-从库DCS Agent不可用

6B-从库负载打满,假死

7B-从库网络抖动

8B-误提升一个从库


DCS故障演练

1C-DCS Server完全不可用

DCS完全不可用是一个极其严重的故障,默认情况下将导致所有数据库集群不可写入。 如果您使用L2 VIP接入,则默认绑定于主库节点的L2 VIP亦不可用,这意味着整集群可能都无法读写!您应当尽全力避免此种故障!

好在DCS本身便是为了解决此问题而生:本身采用分布式架构,并有可靠的容灾机制,能容忍各种常见的硬件故障。例如,3节点的DCS集群允许一台服务器出现故障,而5节点的DCS集群则最多允许两个服务器节点同时出现故障。

有一些方式可以缓解此问题。

关停 Consul 后,所有 启用高可用自动切换模式的数据库集群主库会触发降级逻辑(因为主库的Patroni意识不到其他集群成员的存在,须假定其他从库已经构成一个法定多数的分区并进行选举,因而要将自身降级为从库避免脑裂)

操作说明

关停元节点上的DCS Server,如果有3台,至少应当关停2台,如果有5台,至少应当关停3台。

systemctl stop consul

解决方案

  1. 在维护模式下,用户失去了自动Failover的能力,但DCS故障不会导致主库不可写入。(仍可以手工快速切换)
  2. 使用更多的DCS实例确保DCS的可用性(DCS本身便是为了解决此问题而生)
  3. 为Patroni配置足够长的超时重试时间,并为DCS故障设置最高的响应优先级

2C-DCS通主库,不通从库(1主1从)

3C-DCS通主库,不通从库(1主n从,n>1)

4C-DCS通从库,不通主库(1主1从)

5C-DCS通从库,不通主库(1主n从,n>1)

6C-DCS网络抖动:同时中断,主库从库同时恢复,或主库先恢复

7C-DCS网络抖动:同时中断,从库先恢复,主库后恢复(1主1从)

8C-DCS网络抖动:同时中断,从库先恢复,主库后恢复(1主n从,n>1)

7 - PG 慢查询优化

使用Pigsty监控系统,分析,定位,优化慢查询的一个案例

下面以Pigsty自带的沙箱环境为例,介绍一个使用Pigsty监控系统处理慢查询的过程。

慢查询:模拟

因为没有实际的业务系统,这里我们以一种简单快捷的方式模拟系统中的慢查询。即pgbench自带的tpc-c

在主库上执行以下命令

ALTER TABLE pgbench_accounts DROP CONSTRAINT pgbench_accounts_pkey ;

该命令会移除 pgbench_accounts 表上的主键,导致相关查询变慢,系统瞬间雪崩过载。

图1:单个从库实例的QPS从500下降至7,Query RT下降至300ms

图2:系统负载达到200%,触发机器负载过大,与查询响应时间过长的报警规则。

慢查询:定位

首先,使用PG Cluster面板定位慢查询所在的具体实例,这里以 pg-test-2为例

然后,使用PG Query面板定位具体的慢查询:编号为 -6041100154778468427

图3:从查询总览中发现异常慢查询

该查询表现出:

  • 响应时间显著上升: 17us 升至 280ms
  • QPS 显著下降: 从500下降到 7
  • 花费在该查询上的时间占比显著增加

可以确定,就是这个查询变慢了!

接下来,利用PG Stat Statements面板或PG Query Detail,根据查询ID定位慢查询的具体语句。

图4:定位的查询是SELECT abalance FROM pgbench_accounts WHERE aid = $1

慢查询:猜想

接下来,我们需要推断慢查询产生的原因。

SELECT abalance FROM pgbench_accounts WHERE aid = $1

该查询以 aid 作为过滤条件查询 pgbench_accounts 表,如此简单的查询变慢,大概率是这张表上的索引出了问题。

用屁股想都知道是索引少了,因为就是我们自己删掉的嘛!

分析查询后提出猜想: 该查询变慢是pgbench_accounts表上aid列缺少索引

下一步,查阅 PG Table Detail 面板,检查 pgbench_accounts 表上的访问,来验证我们的猜想

图5: pgbench_accounts 表上的访问情况

通过观察,我们发现表上的索引扫描归零,与此同时顺序扫描却有相应增长。这印证了我们的猜想!

慢查询:解决

确定了问题根源后,我们将着手解决。

尝试在 pgbench_accounts 表上为 aid 列添加索引,看看能否解决这个问题。

加上索引后,神奇的事情发生了。

图6:可以看到,查询的响应时间与QPS已经恢复正常。

图7:系统的负载也恢复正常

慢查询:样例

通过这篇教程,您已经掌握了慢查询优化的一般方法论。

图8:一个慢查询优化的实际例子,将系统的饱和度从40%降到了4%

8 - PG 数据库迁移

本文将基于Pigsty沙箱环境,用实例演示基于PostgreSQL逻辑复制的在线不停机数据库迁移。

本文基于Pigsty沙箱中的实例,介绍基于逻辑复制进行主从切换与数据库迁移的原理,细节与注意事项。

逻辑复制相关基础知识可参考 Postgres逻辑复制详解 一文。

0 逻辑复制迁移

逻辑复制通常可用于跨大版本跨操作系统在线升级PostgreSQL,例如从PG 10到PG 13,从Windows到Linux。

0.1 逻辑迁移的优点

相比原地pg_upgrade升级与pg_dump升级,逻辑复制的好处有:

  • 在线:迁移可以在线进行,不需要或者只需要极小的停机窗口。
  • 灵活:目标库的结构可以与源库不同,例如普通表改为分区表,加列等。可以跨越大版本使用。
  • 安全:相比物理复制,目标库是可写的,因此在最终切换前,可以随意进行测试并重建。
  • 快速:停机窗口很短,可以控制在秒级到分钟级。

0.2 逻辑迁移的局限性

逻辑复制的局限性主要在于设置相对繁琐,初始时刻拷贝数据较物理复制更慢,对于单实例多DB的情况需要迁移多次。大对象序列号需要在迁移时手动同步。

  • 不能复制DDL变更

  • 不能复制序列号(Sequence)

  • 如果逻辑从库上某张被外键引用的表被Truncate,但因为引用该表的表不在订阅集中(所以无法在不truncate该表的情况下继续,但在订阅集之外的表上执行truncate违反语义),那么就会出现冲突。

  • 大对象无法复制。

  • 只支持普通表的复制,包括分区表。不支持视图,物化视图,外部表。

总体来说都,属于可以解决或可以容忍的问题。

0.3 逻辑迁移的基本流程

整体上讲,基于逻辑复制的迁移遵循以下步骤:

其中准备工作与存量迁移部分耗时较长,但不需要停机,不会对生产业务产生影响。

切换时刻需要短暂的停机窗口,采用自动化的脚本可以将停机时间控制在秒级到分钟级

下面将基于Pigsty沙箱介绍这些步骤涉及到的具体细节

1 准备工作

1.1 准备源宿集群

在进行迁移之前,首先要确定迁移的源端集群与目标集群配置正确。

Pigsty标准沙箱由四个节点与两套数据库集群构成。

两套数据库集群pg-metapg-test将分别作为逻辑复制的源端(SRC)宿端(DST)

本例将pg-meta-1作为发布者,pg-test-1作为订阅者,将pgbench相关表从pg-meta迁移至pg-test

1.1.1 用户

迁移通常需要在原宿两端拥有两个用户,分别用于管理复制

CREATE USER dbuser_admin SUPERUSER;              -- 超级用户用于创建发布与订阅
CREATE USER replicator REPLICATION BYPASSRLS;    -- 复制用户用于订阅变更

1.1.2 HBA规则

同时,还需要配置相应的HBA规则,允许复制用户在原宿集群间相互访问

此外,迁移通常会从中控机发起,应当允许管理用户从中控机访问原/宿集群

因为创建订阅需要超级用户权限,建议为管理用户(永久或临时)配置SUPERUSER权限。

1.1.3 配置项

必选的配置项是wal_level,您必须在源端将wal_level配置为logical,方能启用逻辑复制。

其他一些关于复制的相关参数也需要合理配置,但除了wal_level外的参数默认值都不会影响逻辑复制正常工作,均为可选

推荐在源端与宿端使用相同的配置项,下面是在64核机器上,一些相关配置的参考值:

wal_level: logical                      # MANDATORY!	
max_worker_processes: 64                # default 8 -> 64, set to CPU CORE 64
max_parallel_workers: 32                # default 8 -> 32, limit by max_worker_processes
max_parallel_maintenance_workers: 16    # default 2 -> 16, limit by parallel worker
max_parallel_workers_per_gather: 0      # default 2 -> 0,  disable parallel query on OLTP instance
# max_parallel_workers_per_gather: 16   # default 2 -> 16, enable parallel query on OLAP instance

max_wal_senders: 24                     # 10 -> 24
max_replication_slots: 16               # 10 -> 16 
max_logical_replication_workers: 8      # 4 -> 8, 6 sync worker + 1~2 apply worker
max_sync_workers_per_subscription: 6    # 2 -> 6, 6 sync worker

对于数据库来说,通常还需要关注数据库的 编码(encoding)与 本地化 (locale)配置项是否正确,通常建议统一使用C.UTF8

1.1.4 连接信息

为了执行管理命令,您需要通过连接串访问原/宿集群的主库。

建议不要在连接串中使用明文密码,密码可以通过~/.pgpass~/.pg_service,环境变量等方式管理,下面使用时将不会列出密码。

PGSRC='postgres://dbuser_admin@10.10.10.10/meta'        # 源端发布者 (SU)
PGDST='postgres://dbuser_admin@10.10.10.11/test'        # 宿端订阅者 (SU)

建议在中控机/元节点上执行迁移命令,并在操作过程中保持上面两个变量生效。

1.2 确定迁移对象

相比于物理复制,逻辑复制允许用户对复制的内容与过程施加更为精细的控制。您可以选择数据库内容的一个子集进行复制。不过在这个例子中,我们将进行整库复制

在本例中,我们采用pgbench提供的用例作为迁移标的。因此可以在源端集群使用pgbench初始化相关表项。

pgbench -is64 ${PGSRC}

此外,考虑到测试的覆盖范围,我们还将创建一张额外的测试数据表(用于测试Sequence的迁移)

psql ${PGSRC} -qAXtw <<-EOF
DROP TABLE IF EXISTS pgbench_extras;
CREATE TABLE IF NOT EXISTS pgbench_extras
  (id BIGSERIAL PRIMARY KEY,v  TIMESTAMP NOT NULL UNIQUE);
EOF

要注意,只有 基本表 (包括分区表)可以参与逻辑复制,其他类型的对象,包括 视图,物化视图,外部表,索引,序列号都无法加入到逻辑复制中。使用以下查询,可以列出当前数据库中可以加入逻辑复制的表的完全限定名。

SELECT quote_ident(nspname) || '.' || quote_ident(relname) AS name
FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE relkind = 'r' AND nspname NOT IN ('pg_catalog', 'information_schema', 'monitor', 'repack', 'pg_toast')

在准备阶段,您需要筛选出希望进行复制的表。在存量迁移中将这些表的结构定义同步至宿集群中,并建立在这些表上的逻辑复制。

1.3 修复复制标识

并不是所有的表都可以直接纳入逻辑复制中并正常工作。在进行迁移前,您需要对所有待迁移的表进行检查,确认它们都已经正确配置了复制标识

复制身份模式\表上的约束 主键(p) 非空唯一索引(u) 两者皆无(n)
default 有效 x x
index x 有效 x
full 低效 低效 低效
nothing x x x
  • 如果表上有主键,则会默认使用 REPLICA IDENTITY default,这是最好的,不用进行任何修改。

  • 如果表上没有主键,有条件的话请创建一个,没有条件的话,一个建立在非空列集上的唯一索引也可以起到同样的作用。在这种情况下需要显式的为表配置REPLICA IDENTITY USING <tbl_unique_key_idx_name>

  • 如果表上既没有主键,也没有唯一索引,那么您可以为表配置REPLICA IDENTITY FULL,将完整的一行作为复制标识。

    使用FULL身份标识的性能非常差,发布侧和订阅侧的删改操作都会导致顺序扫表,建议只将其作为保底手段使用。

    另一种选择是为表配置REPLICA IDENTITY NOTHING,这样任何在发布端对此表进行UPDATE|DELETE操作都会直接报错中止。

使用以下查询,可以列出所有表的完全限定名,复制标识配置,以及表上是否有主键或唯一索引,

SELECT quote_ident(nspname) || '.' || quote_ident(relname) AS name, con.ri AS keys,
       CASE relreplident WHEN 'd' THEN 'default' WHEN 'n' THEN 'nothing' WHEN 'f' THEN 'full' WHEN 'i' THEN 'index' END AS identity
FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid, LATERAL (SELECT array_agg(contype) AS ri FROM pg_constraint WHERE conrelid = c.oid) con
WHERE relkind = 'r' AND nspname NOT IN ('pg_catalog', 'information_schema', 'monitor', 'repack', 'pg_toast')
ORDER BY 2,3;

以1.2的测试场景为例:

          name           | keys  | identity
-------------------------+-------+----------
 public.spatial_ref_sys  | {c,p} | default
 public.pgbench_accounts | {p}   | default
 public.pgbench_branches | {p}   | default
 public.pgbench_tellers  | {p}   | default
 public.pgbench_extras   | {p,u} | default
 public.pgbench_history  | NULL  | default

如果表上只有唯一索引,例如您需要检查该唯一索引是否满足要求:所有列都为非空,not deferrablenot partial,如果满足,则可以使用以下命令将表的复制身份修改为index模式。

-- 一个例子:即使pgbench_extras上有主键,但也可以使用唯一索引作为身份标识
ALTER TABLE pgbench_extras REPLICA IDENTITY USING INDEX pgbench_extras_v_key;

如果表上没有主键,也没有唯一约束。如上面的pgbench_history表,那就需要通过以下命令将其复制身份设置为FULL|NOTHING

ALTER TABLE pgbench_history REPLICA IDENTITY FULL;

完成修复后,所有表都应当具有合适的复制身份

          name           | keys  | identity
-------------------------+-------+----------
 public.spatial_ref_sys  | {c,p} | default
 public.pgbench_accounts | {p}   | default
 public.pgbench_branches | {p}   | default
 public.pgbench_tellers  | {p}   | default
 public.pgbench_extras   | {p,u} | index
 public.pgbench_history  | NULL  | full

2 存量迁移

2.1 同步数据库模式

2.1.1 转储

使用以下命令转储所有对象定义,并复制到宿端应用。

pg_dump ${PGSRC} --schema-only -n public | psql ${PGDST}

可以通过pg_dump-n-t参数进行灵活控制,只转储所需的对象。例如,如果只需要public模式下pgbench的相关表,则可以通过以下命令转储:

pg_dump ${PGSRC} --schema-only -n public -t 'pgbench_*' | psql ${PGDST}

2.1.2 校验

同步完成后,通常需要进行模式校验。

  • 所有目标表及其索引、序列号是否已经建立
  • 函数、类型、模式、用户、权限是否均符合预期?

数据库模式需要根据用户自己的需求进行同步与校验,没有什么通用的方式。

2.2 在源端创建发布

源端集群主库作为发布者,需要创建发布,将所需的表加入到发布集中。

2.2.1 创建发布的方式

创建发布的语法如下所示:

CREATE PUBLICATION name
    [ FOR TABLE [ ONLY ] table_name [ * ] [, ...]
      | FOR ALL TABLES ]
    [ WITH ( publication_parameter [= value] [, ... ] ) ]

针对所有表创建发布(需要超级用户权限):

CREATE PUBLICATION "pg_meta_pub" FOR ALL TABLES;

注意无论是发布还是订阅,名称都建议遵循PostgreSQL对象标识符命名规则([a-z][0-9a-z_]+),特别是不要在名称中使用-。以免不必要的麻烦,例如创建订阅同名的复制槽因命名不规范而失败。

如果需要控制订阅的事件类型(不常见),可以通过参数publish指定,默认为insert, update, delete, truncate

如果源端上有分区表,有一个参数可以用于控制其复制行为。把分区表当成一张表(使用分区根表的复制标识),还是当成多张子表(使用子表上的复制标识)来处理。启用这个选项可以把分区表在逻辑上看成一张表(分区根表),而不是一系列的分区子表,所以订阅端只需要存在一张分区根表的同名表即可正常复制,这是13版本引入的新选项。该选项默认为false,也就是说逻辑复制分区表时,源端的每一个分区都必须在订阅端存在。

额外的参数可以通过以下的形式传入:

CREATE PUBLICATION "pg_meta_pub" FOR ALL TABLES 
WITH(publish = 'insert', publish_via_partition_root = true);

2.2.2 发布的内容

如果不希望发布所有的表,则可以在发布中具体指定所需的表名称。

例如在这个例子中spatial_ref_sys是一张postgis扩展使用的常量表,并不需要迁移,我们可以将其排除。利用以下SQL,可以直接在数据库中拼接出创建发布的SQL命令:

SELECT E'CREATE PUBLICATION pg_meta_pub FOR TABLE\n' ||
       string_agg(quote_ident(nspname) || '.' || quote_ident(relname), E',\n') || ';' AS sql
FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE relkind = 'r' AND nspname NOT IN ('pg_catalog', 'information_schema', 'monitor', 'repack', 'pg_toast')
AND relname ~ 'pgbench'; -- 只复制表名形如 pgbench* 的表
\gexec    -- 在psql中执行上面命令生成的SQL语句

在这个例子中,实际生成并执行的命令如下:

psql ${PGSRC} -Xtw <<-EOF
  CREATE PUBLICATION pg_meta_pub FOR TABLE
    public.pgbench_accounts,
    public.pgbench_branches,
    public.pgbench_tellers,
    public.pgbench_history,
    public.pgbench_extras;
EOF

2.2.3 确认发布状态

建立完发布后,可以从 pg_publication 视图看到所创建的发布。

$ psql ${PGSRC} -Xxwc 'table pg_publication;'
-[ RECORD 1 ]+------------
oid          | 24679
pubname      | pg_meta_pub
pubowner     | 10
puballtables | f
pubinsert    | t
pubupdate    | t
pubdelete    | t
pubtruncate  | t
pubviaroot   | f

可以从pg_publication_tables确认纳入到发布中的表有哪些。

$ psql ${PGSRC} -Xwc 'table pg_publication_tables;'
   pubname   | schemaname |    tablename
-------------+------------+------------------
 pg_meta_pub | public     | pgbench_history
 pg_meta_pub | public     | pgbench_tellers
 pg_meta_pub | public     | pgbench_accounts
 pg_meta_pub | public     | pgbench_branches
 pg_meta_pub | public     | pgbench_extras

确认无误后,发布端的工作完成。接下来要在宿端集群主库上创建订阅,订阅源端集群主库上的这个发布

2.3 在宿端创建订阅

宿端集群主库作为订阅者,需要创建订阅,从发布者上订阅所需的变更。

2.3.1 创建订阅

创建订阅需要SUPERUSER权限,创建订阅的语法如下所示:

CREATE SUBSCRIPTION subscription_name
    CONNECTION 'conninfo'
    PUBLICATION publication_name [, ...]
    [ WITH ( subscription_parameter [= value] [, ... ] ) ]

创建订阅必须使用CONNECTION子句指定发布者的连接信息,通过PUBLICATION子句指定发布名称。这里使用replicator用户连接发布者,该用户的密码已经写入宿端实例下~/.pgpass,因此这里可以在连接串中省去。

创建订阅还有一些其他的参数,通常只有手动管理复制槽时才需要修改这些参数:

  • copy_data,默认为true,当复制开始时,是否要复制全量数据。
  • create_slot,默认为true,该订阅是否会在发布实例上创建复制槽。
  • enabled,默认为true,是否立即开始订阅。
  • connect,默认为true,是否连接至订阅实例,如果不连接,上面几个选项都会被重置为false

这里,创建订阅的实际命令为:

psql ${PGDST} -Xtw <<-EOF
    CREATE SUBSCRIPTION "pg_test_sub" 
      CONNECTION 'host=10.10.10.10 user=replicator dbname=meta' 
      PUBLICATION "pg_meta_pub";
EOF

2.3.2 订阅状态确认

成功创建订阅后,可以从 pg_subscription 视图看到所创建的发布。

$ psql ${PGDST} -Xxwc 'TABLE pg_subscription;'
-[ RECORD 1 ]---+---------------------------------------------
oid             | 20759
subdbid         | 19351
subname         | pg_test_sub
subowner        | 16390
subenabled      | t
subconninfo     | host=10.10.10.10 user=replicator dbname=meta
subslotname     | pg_test_sub
subsynccommit   | off
subpublications | {pg_meta_pub}

可以从pg_subscription_rel中确认哪些表被纳入到订阅的范围,及其复制状态。

$ psql ${PGDST} -Xwc 'table pg_subscription_rel;'
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   20759 |   20742 | r          | 0/B0BC1FB8
   20759 |   20734 | r          | 0/B0BC20B0
   20759 |   20737 | r          | 0/B0BC20B0
   20759 |   20745 | r          | 0/B0BC20B0
   20759 |   20731 | r          | 0/B0BC20B0

2.4 等待逻辑复制同步

创建订阅后,首先必须监控 发布端与订阅端两侧的数据库日志,确保没有错误产生

2.4.1 逻辑复制状态机

如果一切正常,逻辑复制会自动开始,针对每张订阅中的表执行复制状态机逻辑,如下图所示。

当所有的表都完成复制,进入r(ready)状态时,逻辑复制的存量同步阶段便完成了,发布端与订阅端整体进入同步状态。

stateDiagram-v2 [*] --> init : 表被加入到订阅集中 init --> data : 开始同步表的初始快照 data --> sync : 存量数据同步完成 sync --> ready : 同步期间的增量变更应用完毕,进入就绪状态

当创建或刷新订阅时,表会被加入到 订阅集 中,每一张订阅集中的表都会在pg_subscription_rel视图中有一条对应纪录,展示这张表当前的复制状态。刚加入订阅集的表初始状态为i,即initialize初始状态

如果订阅的copy_data选项为真(默认情况),且工作进程池中有空闲的Worker,PostgreSQL会为这张表分配一个同步工作进程,同步这张表上的存量数据,此时表的状态进入d,即拷贝数据中。对表做数据同步类似于对数据库集群进行basebackup,Sync Worker会在发布端创建临时的复制槽,获取表上的快照并通过COPY完成基础数据同步。

当表上的基础数据拷贝完成后,表会进入sync模式,即数据同步,同步进程会追赶同步过程中发生的增量变更。当追赶完成时,同步进程会将这张表标记为r(ready)状态,转交逻辑复制主Apply进程管理变更,表示这张表已经处于正常复制中。

2.4.2 同步进度跟踪

数据同步(d)阶段可能需要花费一些时间,取决于网卡,网络,磁盘,表的大小与分布,逻辑复制的同步worker数量等因素。

作为参考,1TB的数据库,20张表,包含有250GB的大表,双万兆网卡,在6个数据同步worker的负责下大约需要6~8小时完成复制。

在数据同步过程中,每个表同步任务都会源端库上创建临时的复制槽。请确保逻辑复制初始同步期间不要给源端主库施加过大的不必要写入压力,以免WAL撑爆磁盘。

发布侧的 pg_stat_replicationpg_replication_slots,订阅端的pg_stat_subscriptionpg_subscription_rel提供了逻辑复制状态的相关信息,需要关注。

psql ${PGDST} -Xxw <<-'EOF'
    SELECT subname, json_object_agg(srsubstate, cnt) FROM
    pg_subscription s JOIN
      (SELECT srsubid, srsubstate, count(*) AS cnt FROM pg_subscription_rel 
       GROUP BY srsubid, srsubstate) sr
    ON s.oid = sr.srsubid GROUP BY subname;
EOF

可以使用以下SQL确认订阅中表的状态,如果所有表的状态都显示为r,则表示逻辑复制已经成功建立,订阅端可以用于切换。

   subname   | json_object_agg
-------------+-----------------
 pg_test_sub | { "r" : 5 }

当然,最好的方式始终是通过监控系统来跟踪复制状态。

3 切换时刻

3.1 准备工作

一个良好的工程实践是,在搞事情之前,在源端宿端都执行几次存盘操作,避免后续操作因被内存刷盘拖慢。

也可以执行分析命令更新统计信息,便于后续快速对比校验数据完整性。

psql ${PGSRC} -Xxwc 'CHECKPOINT;ANALYZE;CHECKPOINT;'
psql ${PGSRC} -Xxwc 'CHECKPOINT;ANALYZE;CHECKPOINT;'

在此之后的操作,都处于服务不可用状态,因此尽可能快地进行。通常情况下在分钟级内完成较为合适。

3.2 停止源端写入流量

3.2.1 选择合适的停止方式

暂停源端写入有多种方式,请根据实际业务场景选择与组合:

  • 告知业务方停止流量
  • 停止解析源端主库域名
  • 停止或暂停负载均衡器(Haproxy | VIP)的流量转发
  • 停止或暂停连接池Pgbouncer
  • 停止或暂停Postgres实例
  • 修改数据库主库的参数,设置默认事务模式为只读。
  • 修改数据库主库的HBA规则,拒绝业务访问。

通常建议使用修改HBA,修改连接池,修改负载均衡器的方式停止主库的写入流量。

请注意,无论使用何种方式,建议保持PostgreSQL存活,并且管理用户复制用户仍然可以连接到源端主库。

3.2.2 确认源端写入流量停止

当源端主库停止接受写入后,首先执行确认逻辑,通过观察pg_stat_replication,确认逻辑订阅者已经与发布者保持同步。

psql ${PGSRC} -Xxw <<-EOF
    SELECT application_name AS name,
           pg_current_wal_lsn() AS lsn,
           pg_current_wal_lsn() - replay_lsn AS lag 
    FROM pg_stat_replication;
EOF

-[ RECORD 1 ]-----
name | pg_test_sub
lsn  | 0/B0C24918
lag  | 0

重复执行上述命令,如果lsn字段保持不变,lag始终为0,就说明主库的写入流量已经正确停止,且逻辑从库上已经没有复制延迟,可以用于切换。

3.2.3 建立反向逻辑复制(可选)

如果要求迁移失败后业务可以随时回滚,可以在停止源端写入流量后,设置反向的逻辑复制,将后续订阅端(新主库)的变更反向同步至原来的发布端(旧主库)。不过此过程需要重新同步数据,耗时太久。通常情况下,只有在数据非常重要,且数据量不大或停机窗口足够长的情况下才适用于此方法。

首先停止宿端现有的逻辑订阅。必须停止现有逻辑复制才能继续后面的步骤,否则会形成循环复制

停止源端写入流量后,继续维持逻辑复制没有意义,因此可以停止宿端的订阅。但建议保留该订阅,只是禁用它,以备迁移失败回滚。

psql ${PGDST} -qAXtwc 'ALTER SUBSCRIPTION pg_test_sub DISABLE;'

然后依照上述流程重新建立 反向的逻辑复制,这里只给出命令:

# 在宿端创建发布:pg_test_pub
psql ${PGDST} -Xtw <<-EOF
  CREATE PUBLICATION pg_test_pub FOR TABLE
    public.pgbench_accounts,
    public.pgbench_branches,
    public.pgbench_tellers,
    public.pgbench_history,
    public.pgbench_extras;
  TABLE pg_publication;
EOF

# 在源端创建订阅
psql ${PGSRC} -Xtw <<-EOF
    CREATE SUBSCRIPTION "pg_meta_sub" 
      CONNECTION 'host=10.10.10.11 user=replicator dbname=test' 
      PUBLICATION "pg_test_pub";
    TABLE pg_subscription;
EOF

# 清空源端所有相关表(危险),等待/或者不等待同步完成
psql ${PGSRC} -Xtw <<-EOF
  TRUNCATE TABLE
    public.pgbench_accounts,
    public.pgbench_branches,
    public.pgbench_tellers,
    public.pgbench_history,
    public.pgbench_extras;
  TABLE pg_publication;
EOF

3.3 同步序列号与其他对象

逻辑复制不复制序列号(Sequence),因此基于逻辑复制做Failover时,必须在切换前手工同步序列号的值。

3.3.1 从源端同步序列号值

如果您的序列号都是从表上的SERIAL列定义自动创建的,而且宿端库也单纯只从源端订阅,那么同步序列号比较简单。从订阅端找出所有需要同步的序列号:

PGSRC='postgres://dbuser_admin@10.10.10.10/meta'        # 源端发布者 (SU)
PGDST='postgres://dbuser_admin@10.10.10.11/test'        # 宿端订阅者 (SU)

-- 查询订阅端,生成的用于同步SEQUENCE的shell命令
psql ${PGDST} -qAXtw <<-'EOF'
    SELECT 'pg_dump ${PGSRC} -a ' ||
    string_agg('-t ' || quote_ident(schemaname) || '.' || quote_ident(sequencename), ' ') ||
    ' | grep setval | psql -qAXtw ${PGDST}'
    FROM pg_sequences;
EOF

在本例中,只有pgbench_extras.id上有一个对应的SEQUENCE pgbench_extras_id_seq。这里生成的同步语句为

pg_dump ${PGSRC} -a -t public.pgbench_extras_id_seq | grep setval | psql -qAXtw ${PGDST}

比较复杂的情况,需要您手工生成这条命令,通过-t依次指定需要转储的序列号。

3.3.2 基于业务数据设置序列号值

另一种管理序列号的方式是直接根据表中的数据设置序列号的值,而无需从源端同步

例如,表pgbench_extras.id的最大值为100,那么将订阅端端pgbench_extras_id_seq直接设置为一个足够大的值,例如100+10000 = 10100,就可以保证迁移后使用该序列号分配的新id不会与已有数据冲突。

采用这种方式,可以直接在故障切换前进行序列号的设置,减少迁移切换所需的停机时间。但这样可能会导致业务数据序列号分配出现空洞,对于一些边界条件与特殊的序列号使用场景需要特别小心。例如:序列号从未被使用过,序列号的增长步长为负数,采用函数发号器调用Sequence等。

直接设置序列号的命令如下所示:

psql ${PGDST} -qAXtw <<-'EOF'
  SELECT pg_catalog.setval('public.pgbench_extras_id_seq', (SELECT max(id) + 1000 FROM pgbench_extras));
EOF

3.3.3 其他对象的同步

某些无法被逻辑复制处理的对象,也需要在这里一并进行同步。

例如:刷新物化视图,手工迁移大对象等。但这些功能很少有人会用到,所以在此不详细展开。

3.4 校验数据一致性

如果逻辑复制工作正常,通常不用校验数据,您可以在第二步中间执行多次对比校验以增强对逻辑复制的信心。

在停机窗口期间,建议只进行简单基本的数据校验,例如,比较表中的行数,主键的最大最小值是否一致。

以下函数用于执行这一校验

function compare_relation(){
	local relname=$1
	local identity=${2-'id'}
	psql ${3-${PGSRC}} -AXtwc "SELECT count(*) AS cnt, max($identity) AS max, min($identity) AS min FROM ${relname};"
	psql ${4-${PGDST}} -AXtwc "SELECT count(*) AS cnt, max($identity) AS max, min($identity) AS min FROM ${relname};"
}
compare_relation pgbench_accounts aid
compare_relation pgbench_branches bid
compare_relation pgbench_history  tid
compare_relation pgbench_tellers  tid
function compare_relation() {
    local src_url=${1}
    local dst_url=${2}
    local relname=${3}
    res1=$(psql "${src_url}" -AXtwc "SELECT count(*) AS cnt FROM ${relname};")
    res2=$(psql "${dst_url}" -AXtwc "SELECT count(*) AS cnt FROM ${relname};")
    if [[ "${res1}" == "${res2}" ]]; then
        echo -e "[ok] ${relname}\t\t\t${res1}\t${res2}"
    else
        echo -e "[xx] ${relname}\t\t\t${res1}\t${res2}"
    fi
}

function compare_all() {
    local src_url=${1}
    local dst_url=${2}
    tables=$(psql ${src_url} -AXtwc "SELECT quote_ident(nspname) || '.' || quote_ident(relname) AS name FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE relkind = 'r' AND nspname NOT IN ('pg_catalog', 'information_schema', 'monitor', 'repack', 'pg_toast')")
    for tbl in $tables; do
        result=$(compare_relation "${src_url}" "${dst_url}" ${tbl})
        echo ${result}
    done
}

compare_all ${PGSRC} ${PGDST}

同时,也可以过一遍3.3中同步的序列号,确认其配置是否相同。

psql ${PGSRC} -qwXtc "SELECT schemaname || '.' || sequencename AS name, last_value AS v FROM pg_sequences;"
psql ${PGDST} -qwXtc "SELECT schemaname || '.' || sequencename AS name, last_value AS v FROM pg_sequences;"

其他在3.3.3中手工同步的对象请按需自行校验。如果需要进行其他业务侧的校验,也在这里进行。但停机窗口时间宝贵,花费在这里的时间越长,服务不可用时间也越久。

校验完成后,就可以进行最终的流量切换了。

3.5 流量切换与善后

完成数据校验后就可以进行流量切换。

流量切换的方式取决于您所使用的访问方式,通常与3.2中停流量的方式对偶。例如:

  • 修改应用端连接串,并应用生效
  • 将源端主库域名解析至新主库
  • 将负载均衡器(Haproxy | VIP)的流量转发至新主库
  • 将原主库上Pgbouncer连接池的流量转发至新主库

通过监控系统或其他方式,确认写入流量已经正确应用订阅端的新主库后,基于逻辑复制的迁移就完成了。

不要忘记一些善后清理工作停用并删除订阅端的订阅删除发布端的发布

同时,应当继续确保原主库拒绝新的写入,以免有未清理干净的流量因为配置失误错漏仍然向旧主库访问。

# 删除订阅侧的 订阅
psql ${PGDST} -qAXtw <<-'EOF'
    ALTER SUBSCRIPTION pg_test_sub DISABLE;
    DROP SUBSCRIPTION pg_test_sub;
EOF

# 删除发布侧的 发布
psql ${PGSRC} -qAXtw <<-'EOF'
    DROP PUBLICATION pg_meta_sub;
EOF

至此,基于逻辑复制的完整迁移结束。

内置剧本

Pigsty内置了一个 数据库在线迁移的辅助脚本:pgsql-migration.yml ,提供了一个开箱即用的基于逻辑复制的不停机数据库迁移方案。

填入源集群与宿集群相关信息,该剧本即会自动创建出迁移中所需的脚本,在数据库迁移时只需要依次执行即可,包括:

activate                          # 激活迁移上下文,注册环境变量
check-replica-identity            # 准备阶段:检查源集群所有表是否都具有复制身份(主键,或非空唯一候选键)
check-replica-identity-solution   # 准备阶段:针对没有合理复制身份表,生成修复SQL语句
check-special-object              # 准备阶段:检查物化视图,复合类型等特殊对象
compare                           # 比较:对源宿集群中的表进行快速比较(行数计算)
copy-schema                       # 存量迁移:将源集群中的模式复制到宿集群中(可以幂等执行)
create-pub                        # 存量迁移:在源集群中创建发布
create-sub                        # 存量迁移:在宿集群中创建订阅,建立源宿集群之间的逻辑复制
progress                          # 存量迁移:打印逻辑复制的进度
copy-seq                          # 存量/增量迁移:将源集群中的序列号复制到宿集群中(可以幂等执行,在切换时需要再次执行)
next-seq                          # 切换时刻:将宿集群的所有序列号紧急步进1000,以避免主键冲突。
remove-sub                        # 移除宿集群中的逻辑订阅

准备工作

准备源宿集群

现在假设我们希望迁移沙箱中的pg-meta集群(包含Pigsty元数据库与pgbench测试表)至pg-test集群。

pg-meta-1	10.10.10.10  --> pg-test-1	10.10.10.11 (10.10.10.12,10.10.10.13)

首先,新创建好空的目标集群pg-test,然后编辑pgsql-migration.yml 中的变量清单部分,填入相关信息(原宿集群主库的连接信息)

#--------------------------------------------------------------#
#                   MIGRATION CONTEXT                          #
#--------------------------------------------------------------#

# src cluster (the old cluster)
src_cls: pg-meta                       # src cluster name
src_db: meta                           # src database name
src_ip: 10.10.10.10                    # ip address of src cluster primary
src_list: [ ]                          # ip address list of src cluster members (non-primary)

#--------------------------------------------------------------#
# dst cluster (the new cluster)
dst_cls: pg-test                       # dst cluster name
dst_db: test                           # dst database name
dst_ip: 10.10.10.11                    # dst cluster leader ip addressh
dst_list: [ 10.10.10.12, 10.10.10.13 ] # dst cluster members (non-primary)

# dst cluster access information
dst_dns: pg-test                       # dst cluster dns records
dst_vip: 10.10.10.3                    # dst cluster vip records

#--------------------------------------------------------------#
# credential (assume .pgpass viable)
pg_admin_username: dbuser_dba          # superuser @ both side
pg_replicatoin_username: replicator    # repl user @ src to be used
migration_context_dir: ~/migration     # this dir will be created
#--------------------------------------------------------------#

执行pgsql-migration.yml,该脚本默认会在元节点上创建 ~/migration/pg-meta.meta 目录,包含有迁移使用的资源与脚本。

迁移模板

公告

准备工作

存量迁移

切换时刻

9 - TimescaleDB 快速上手

快速使用TimescaleDB的基本功能

https://docs.timescale.com/getting-started/latest/create-hypertable/#hypertables-and-chunks

CREATE TABLE stocks_real_time
(
    time       TIMESTAMPTZ      NOT NULL,
    symbol     TEXT             NOT NULL,
    price      DOUBLE PRECISION NULL,
    day_volume INT              NULL
);

SELECT create_hypertable('stocks_real_time', 'time');
CREATE INDEX ix_symbol_time ON stocks_real_time (symbol, time DESC);
TABLE timescaledb_information.hypertables;
TABLE timescaledb_information.chunks;
SELECT add_retention_policy('conditions', INTERVAL '24 hours');
SELECT remove_retention_policy('conditions');

SELECT j.hypertable_name,
       j.job_id,
       config,
       schedule_interval,
       job_status,
       last_run_status,
       last_run_started_at,
       js.next_start,
       total_runs,
       total_successes,
       total_failures
FROM timescaledb_information.jobs j
         JOIN timescaledb_information.job_stats js
              ON j.job_id = js.job_id
WHERE j.proc_name = 'policy_retention';