新增共享 PG 的 SQL 级指标与 dev 告警分流

- 数据库主机上按实例运行 prometheus-community/postgres_exporter 0.20.1,
  以实例 OS 用户走本地 socket,经 peer + ident 映射到仅有 pg_monitor 的
  homelab_monitor 角色;无密码、不经 Bao。版本与官方 sha256sums.txt 同源。
- prod/dev 启用 pg_stat_statements;monitoring.yml 仅在未生效时 reload/重启
  (prod 走 Patroni pending restart),重复执行 changed=0。
- 告警以上游 postgres_mixin 为底改写为 PrometheusRule,阈值对齐 Patroni
  failover lag 与 max_slot_wal_keep_size;抓取目标带 env 标签。
- Alertmanager 新增 env="dev" 路由到独立 dev 频道,dev SQL 检查告警补 env。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-01 16:20:15 +00:00
co-authored by Claude Opus 5.5
parent cd9d461950
commit 0cd4a67e12
19 changed files with 551 additions and 5 deletions
@@ -0,0 +1,70 @@
---
- name: 设置现有实例路径
ansible.builtin.set_fact:
pg_profile: "{{ pg_profiles[pg_instance] }}"
pg_config_dir: "{{ pg_config_root }}/{{ pg_instance }}"
pg_socket_dir: "/run/homelab-postgresql-{{ pg_instance }}"
- name: 激活前必须存在运行中的受管服务
ansible.builtin.command:
argv: [systemctl, is-active, "homelab-postgresql-{{ pg_instance }}.service"]
changed_when: false
# 生产:Patroni 只在自身 reload 时把本地配置写成数据目录里的 postgresql.conf/pg_ident.conf,
# 所以文件视图能说明 Patroni 是否已接收新配置;预加载库是否已生效另看 current_setting。
# dev:ident_file 是 postmaster 级参数,视图读取的是“当前生效”的那个文件,
# 所以启动时仍指向旧路径就会读不到映射;两列都为真才算已加载。
- name: 读取监控配置的激活状态
ansible.builtin.command:
argv:
- "{{ pg_bin_dir }}/psql"
- -X
- -At
- -h
- "{{ pg_socket_dir }}"
- -p
- "{{ pg_profile.port | string }}"
- -d
- postgres
- -c
- >-
SELECT
EXISTS (SELECT 1 FROM pg_ident_file_mappings WHERE map_name = 'monitor' AND error IS NULL)
AND EXISTS (SELECT 1 FROM pg_file_settings WHERE name = 'shared_preload_libraries'
AND setting ~ 'pg_stat_statements' AND error IS NULL),
current_setting('shared_preload_libraries') ~ 'pg_stat_statements'
become: true
become_user: "{{ pg_profile.user }}"
changed_when: false
register: pg_monitor_state
- name: 让 Patroni 接收新的本地配置(reload,不重启)
ansible.builtin.command:
argv: [systemctl, kill, --kill-whom=main, --signal=HUP, homelab-postgresql-prod.service]
when: pg_instance == 'prod' and pg_monitor_state.stdout.split('|')[0] != 't'
changed_when: true
- name: 等待 Patroni 写出新配置
ansible.builtin.command:
argv: "{{ pg_monitor_state.cmd }}"
become: true
become_user: "{{ pg_profile.user }}"
when: pg_instance == 'prod' and pg_monitor_state.stdout.split('|')[0] != 't'
changed_when: false
register: pg_monitor_reloaded
retries: 10
delay: 3
until: pg_monitor_reloaded.stdout.split('|')[0] == 't'
# dev 没有 Patroni;预加载库与 ident_file 都是 postmaster 级参数,只能重启。
- name: 重启 dev 以加载监控配置
ansible.builtin.systemd_service:
name: homelab-postgresql-dev.service
state: restarted
when: pg_instance == 'dev' and pg_monitor_state.stdout != 't|t'
- name: 检查 dev 重启后监控配置已生效
ansible.builtin.command:
argv: "{{ pg_monitor_state.cmd }}"
become: true
become_user: "{{ pg_profile.user }}"
when: pg_instance == 'dev'
changed_when: false
register: pg_monitor_dev
retries: 10
delay: 3
until: pg_monitor_dev.rc == 0 and pg_monitor_dev.stdout == 't|t'
@@ -0,0 +1,61 @@
---
- name: 设置现有实例路径
ansible.builtin.set_fact:
pg_profile: "{{ pg_profiles[pg_instance] }}"
pg_config_dir: "{{ pg_config_root }}/{{ pg_instance }}"
pg_parent_dir: "{{ pg_data_root }}/{{ pg_instance }}"
pg_socket_dir: "/run/homelab-postgresql-{{ pg_instance }}"
pg_psql: ["{{ pg_bin_dir }}/psql", -X, -At, -v, ON_ERROR_STOP=1, -h, "/run/homelab-postgresql-{{ pg_instance }}",
-p, "{{ pg_profiles[pg_instance].port | string }}", -d, postgres]
# 角色与扩展只在可写实例上创建,standby 经复制获得。
- name: 检查监控角色与扩展是否符合声明
ansible.builtin.command:
argv: "{{ pg_psql + ['-c', pg_monitor_check] }}"
vars:
pg_monitor_check: >-
SELECT pg_is_in_recovery(),
COALESCE((SELECT rolcanlogin AND NOT rolsuper AND NOT rolcreaterole AND NOT rolcreatedb
AND NOT rolreplication AND NOT rolbypassrls AND rolconnlimit = 3 AND rolpassword IS NULL
AND pg_has_role(oid, 'pg_monitor', 'MEMBER')
FROM pg_authid WHERE rolname = '{{ pg_monitor_user }}'), false)
AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements')
become: true
become_user: "{{ pg_profile.user }}"
changed_when: false
register: pg_monitor_role
- name: 创建或修正监控角色(无密码,仅能经本地 ident 映射登录)
ansible.builtin.command:
argv: "{{ pg_psql }}"
stdin: |
SELECT 'CREATE ROLE {{ pg_monitor_user }}'
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{{ pg_monitor_user }}') \gexec
ALTER ROLE {{ pg_monitor_user }} LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS
CONNECTION LIMIT 3 PASSWORD NULL;
GRANT pg_monitor TO {{ pg_monitor_user }};
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
become: true
become_user: "{{ pg_profile.user }}"
when: pg_monitor_role.stdout == 'f|f'
changed_when: true
- name: 安装实例 exporter 服务
ansible.builtin.template:
src: postgres-exporter.service.j2
dest: "/etc/systemd/system/homelab-postgresql-{{ pg_instance }}-exporter.service"
mode: '0644'
register: pg_exporter_unit
- name: 启用 exporter
ansible.builtin.systemd_service:
name: "homelab-postgresql-{{ pg_instance }}-exporter.service"
daemon_reload: "{{ pg_exporter_unit is changed }}"
enabled: true
state: "{{ 'restarted' if pg_exporter_unit is changed else 'started' }}"
# 同时证明 HBA/ident 已加载、角色可登录、exporter 能查询。
- name: 检查 exporter 已连上数据库
ansible.builtin.uri:
url: "http://{{ pg_address }}:{{ pg_profile.exporter_port }}/metrics"
return_content: true
register: pg_exporter_metrics
changed_when: false
retries: 10
delay: 3
until: pg_exporter_metrics.status == 200 and pg_exporter_metrics.content is search('(?m)^pg_up 1$')