PostgreSQL 基础学习

一、PostgreSQL 介绍

1、简介

PostgreSQL 是一个功能强大的开源数据库系统。经过长达15年以上的积极开发和不断改进,PostgreSQL已在可靠性、稳定性、数据一致性等获得了业内极高的声誉。目前PostgreSQL可以运行在所有主流操作系统上,包括Linux、Unix和Windows。

PostgreSQL

PostgreSQL 是完全的事务安全性数据库,支持丰富的数据类型(如JSON和JSONB类型、数组类型)和自定义类型。PostgreSQL数据库提供了丰富的接口,可以很方便地扩展它的功能,如可以在GiST框架下实现自己的索引类型,支持使用C语言写自定义函数、触发器,也支持使用流行的编程语言写自定义函数。PL/Perl提供了使用Perl语言写自定义函数的功能,当然还有PL/Python、PL/Java、PL/Tcl等。

作为一种企业级数据库,PostgreSQL以它所具有的各种高级功能而自豪,像多版本并发控制( MVCC )、按时间点恢复(PITR)、表空间、异步复制、嵌套事务、在线热备、复杂查询的规划和优化以及为容错而进行的预写日志等。它支持国际字符集、多字节编码并支持使用当地语言进行排序、大小写处理和格式化等操作。它也在所能管理的大数据量和所允许的大用户量并发访问时间具有完全的高伸缩性。

2、PostgreSQL 的优势

PostgreSQL 数据库是目前功能最强大的开源数据库,它是最接近工业标准SQL92的查询语言,至少实现了SQL:2011标准中要求的179项主要功能中的160项。

优势 说明
稳定可靠 支持事务、WAL、MVCC、PITR、流复制,适合承载核心业务数据
SQL 能力强 支持复杂查询、窗口函数、CTE、多种 Join、子查询、聚合分析
数据类型丰富 支持 JSONB、数组、范围类型、UUID、网络地址、几何类型等
扩展能力强 支持插件扩展,例如 pg_stat_statements、PostGIS、pg_trgm 等
运维观测能力好 内置大量系统视图,如 pg_stat_activitypg_lockspg_stat_database
开源免费 使用类 BSD 协议,适合企业自建和二次集成
生态成熟 支持主流语言和框架,例如 Java、Go、Python、Node.js、PHP 等
  • 稳定可靠:PostgreSQL是唯一能做到数据零丢失的开源数据库。目前有报道称国内外有部分银行使用PostgreSQL数据库。
  • 开源省钱:PostgreSQL数据库是开源的、免费的,而且使用的是类BSD协议,在使用和二次开发上基本没有限制。
  • 支持广泛:PostgreSQL 数据库支持大量的主流开发语言,包括C、C++、Perl、Python、Java、Tcl以及PHP等。
  • PostgreSQL社区活跃:PostgreSQL基本上每3个月推出一个补丁版本,这意味着已知的Bug很快会被修复,有应用场景的需求也会及时得到响应。

3、PostgreSQL 对比 MySQL

Postgresql和Mysql都是开源数据库。

  • 功能强大:支持所有主流的多表连接查询的方式,如“Hash JOIN”“Sort Merge JOIN”等;字段类型还支持数组类型,甚至有一些业务功能都不再需要写代码来实现了,直接使用数据库的功能即可解决问题。
  • 性能优化工具与度量信息丰富:PostgreSQL数据库中有大量的性能视图,可以方便地定位问题(比如可以看到正在执行的SQL,可以通过锁视图看到谁在等待,以及哪条记录被锁定等)。PostgreSQL中设计了专门架构和进程用于收集性能数据,既有物理I/O方面的统计,也有表扫描及索引扫描方面的性能数据。
  • 在线操作功能好:PostgreSQL增加空值列时,本质上只是在系统表上把列定义上,无须对物理结构做更新,这就让PostgreSQL在加列时可以做到瞬间完成。PostgreSQL还支持在线建索引的功能,在创建索引的过程可以不锁更新操作。
  • 从PostgreSQL9.1开始,支持同步复制(synchronous replication)功能,通过Master和Slave之间的复制可以实现零数据丢失的高可用方案。可以方便地写插件来扩展PostgreSQL数据库的功能:支持移动互联网的新功能,如空间索引。如果应用的数据访问很简单,那么后端使用MySQL也是很合适的。但是如果应用复杂,而且不想消耗太多的开发资源,那么PostgreSQL是一个很明智的选择。

PostgreSQL 和 MySQL 都是常见的开源数据库,但两者定位略有不同。

对比项 PostgreSQL MySQL
SQL 能力 更完整,复杂查询能力强 常规 OLTP 场景简单高效
数据类型 类型丰富,支持 JSONB、数组、范围类型等 类型相对简单,JSON 能力也可用
扩展能力 插件体系强,适合复杂业务 插件生态有,但整体扩展性弱一些
事务一致性 一致性和标准兼容性较强 依赖具体存储引擎,InnoDB 为主
运维复杂度 参数、权限、连接、Vacuum 等需要理解 相对容易上手
适用场景 复杂业务、强一致、报表、GIS、JSONB、企业系统 简单业务、高并发读写、互联网常规业务

如果业务只是简单的增删改查,MySQL 使用成本较低;如果业务中存在复杂查询、强一致性、JSONB、报表分析、权限管理、GIS、数据建模等需求,PostgreSQL 更合适。

4、不适用 PostgreSQL

在不安装任何扩展包的情况下,PG需要占用100MB以上的磁盘空间,可以看出它的个头是比较大的,因此在一些存储空间极为有限的小型设备上使用PG是不合适的。因此在一些存储空间极为有限的小型设备上使用PG是不合适的,把PG当成简单的缓存区来用也是不合适的,此时应选用一些更轻量级的数据库。

因为作为一款企业级数据库产品,PG对其安全也是极其重视的,因此,如果你在开发一个把安全管理放到应用层去做的轻量级应用,那么PG完善的安全机制反倒会成为负担,因为它的角色和权限管理非常复杂,会带来不必要的管理复杂度和性能损耗。

鉴于上面的种种,PG数据库一般是会和别的数据库搭配使用,使他们各展所长。一种常见的组合是把Redis当成PG的查询缓存来用,另一种的组合是用PG做主数据库。

5、核心概念

概念 说明
Cluster PostgreSQL 实例级概念,Ubuntu 下通常是 版本/集群名,例如 16/main
Database 数据库,一个 PostgreSQL Cluster 内可以有多个 Database
Role/User PostgreSQL 中用户和角色统一为 Role,带 LOGIN 权限的角色就是用户
Schema 数据库内的命名空间,默认常见为 public
Data Directory 数据目录,存放真实数据库文件
WAL 预写日志,用于事务恢复、复制和备份
postgresql.conf 主配置文件,控制监听地址、端口、日志、内存、WAL 等参数
pg_hba.conf 客户端认证配置文件,控制哪些 IP、用户、数据库可以连接
pg_lsclusters Ubuntu/Debian 下查看 PostgreSQL 集群状态的工具
pg_ctlcluster Ubuntu/Debian 下启动、停止、重启指定 PostgreSQL 集群的工具

二、PostgreSQL 安装部署

PostgreSQL 官方在 Ubuntu 上主要推荐通过系统自带 APT 源或 PostgreSQL 官方 PGDG APT 仓库安装;Ubuntu 默认源包含 PostgreSQL,但版本会随 Ubuntu 版本固定,而 PGDG 仓库可以提供当前受支持的 PostgreSQL 主版本。

PostgreSQL 在 Ubuntu 环境下常见安装方式有三种:

安装方式 适用场景 特点 推荐程度
Ubuntu 默认 APT 源 测试环境、版本无特殊要求 安装简单,但版本受 Ubuntu 系统版本限制 一般
PostgreSQL PGDG APT 源 生产环境、需要指定 PostgreSQL 主版本 可安装指定主版本,适合标准化部署 推荐
脚本安装 多台机器批量部署、标准化环境 自动完成目录、参数、权限、密码、服务配置 推荐,但脚本要维护

1、Ubuntu apt 源安装

PostgreSQL 官方下载页明确区分了 ready-to-use packages / installers 与 source code archive,源码编译应单独归类为“源码安装”。

本次 PostgreSQL 安装采用 阿里云 PostgreSQL PGDG 源,通过安装脚本完成 PostgreSQL 的安装、初始化、目录规划、参数配置和服务启动。

阿里云源:-https://mirrors.aliyun.com/postgresql/repos/apt

默认安装版本为 PostgreSQL 16,默认目录规划如下:

类型 路径
软件目录 /usr/lib/postgresql/16
软件软链接 /usr/local/pgsql
配置目录 /etc/postgresql/16/main
数据目录 /data/postgresql/16/main
日志目录 /data/postgresql/log
备份目录 /data/postgresql/backup
凭据文件 /data/postgresql/backup/.postgresql.auth
服务名称 postgresql@16-main.service
默认端口 5432

1.1 环境准备

安装依赖包:

BASH
apt update
apt install -y ca-certificates curl gnupg lsb-release postgresql-common

1.2 获取apt源

BASH
install -d /usr/share/postgresql-common/pgdg

curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
  --fail https://mirrors.aliyun.com/postgresql/repos/apt/ACCC4CF8.asc

cat > /etc/apt/sources.list.d/pgdg.sources <<EOF
Types: deb
URIs: https://mirrors.aliyun.com/postgresql/repos/apt
Suites: $(lsb_release -cs)-pgdg
Architectures: $(dpkg --print-architecture)
Components: main
Signed-By: /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc
EOF

1.3 安装PostgreSQL

BASH
apt update
apt install -y postgresql-16 postgresql-client-16 postgresql-contrib

检查安装结果:

BASH
systemctl status postgresql
● postgresql.service - PostgreSQL RDBMS
     Loaded: loaded (/lib/systemd/system/postgresql.service; enabled; vendor preset: enabled)
     Active: active (exited) since Fri 2026-05-15 10:20:51 CST; 5min ago
   Main PID: 43730 (code=exited, status=0/SUCCESS)
        CPU: 930us

May 15 10:20:51 instance-wc5p3ngj systemd[1]: Starting PostgreSQL RDBMS...
May 15 10:20:51 instance-wc5p3ngj systemd[1]: Finished PostgreSQL RDBMS.


pg_lsclusters 
Ver Cluster Port Status Owner    Data directory              Log file
16  main    5432 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log

1.4 登陆验证

切换为postgres用户进行登陆:

BASH
su - postgres 

psql -U postgres 

psql (16.14 (Ubuntu 16.14-1.pgdg22.04+1))
Type "help" for help.

postgres-# \l
                                                   List of databases
   Name    |  Owner   | Encoding | Locale Provider | Collate |  Ctype  | ICU Locale | ICU Rules |   Access privileges   
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
 postgres  | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |            |           | 
 template0 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |            |           | =c/postgres          +
           |          |          |                 |         |         |            |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |            |           | =c/postgres          +
           |          |          |                 |         |         |            |           | postgres=CTc/postgres
(3 rows)

postgres-# \du 
                             List of roles
 Role name |                         Attributes                         
-----------+------------------------------------------------------------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS

postgres-# \q
命令 作用
su - postgres 切换到 PostgreSQL 默认系统用户
psql 进入 PostgreSQL 命令行
\l 查看数据库列表
\du 查看数据库角色 / 用户
\q 退出 psql
exit 退出 postgres 系统用户

1.5 备份默认配置文件

BASH
cp -a /etc/postgresql/16/main/postgresql.conf /etc/postgresql/16/main/postgresql.conf.bak.$(date +%F_%H%M%S)

cp -a /etc/postgresql/16/main/pg_hba.conf /etc/postgresql/16/main/pg_hba.conf.bak.$(date +%F_%H%M%S)
命令 作用
备份 postgresql.conf 防止配置改错后无法恢复
备份 pg_hba.conf 防止认证规则改错导致无法登录

1.6 修改基础配置

修改 pg_hba.conf

BASH
PG_CONF="/etc/postgresql/16/main/postgresql.conf"

sed -ri "s|^[#[:space:]]*listen_addresses[[:space:]]*=.*|listen_addresses = '*'|" "$PG_CONF"
sed -ri "s|^[#[:space:]]*port[[:space:]]*=.*|port = 5432|" "$PG_CONF"
sed -ri "s|^[#[:space:]]*password_encryption[[:space:]]*=.*|password_encryption = 'scram-sha-256'|" "$PG_CONF"
sed -ri "s|^[#[:space:]]*timezone[[:space:]]*=.*|timezone = 'Asia/Shanghai'|" "$PG_CONF"
sed -ri "s|^[#[:space:]]*log_timezone[[:space:]]*=.*|log_timezone = 'Asia/Shanghai'|" "$PG_CONF"

grep -q "^log_min_duration_statement" "$PG_CONF" \
  && sed -ri "s|^[#[:space:]]*log_min_duration_statement[[:space:]]*=.*|log_min_duration_statement = 1000|" "$PG_CONF" \
  || echo "log_min_duration_statement = 1000" >> "$PG_CONF"

grep -q "^log_lock_waits" "$PG_CONF" \
  && sed -ri "s|^[#[:space:]]*log_lock_waits[[:space:]]*=.*|log_lock_waits = on|" "$PG_CONF" \
  || echo "log_lock_waits = on" >> "$PG_CONF"

grep -q "^deadlock_timeout" "$PG_CONF" \
  && sed -ri "s|^[#[:space:]]*deadlock_timeout[[:space:]]*=.*|deadlock_timeout = '1s'|" "$PG_CONF" \
  || echo "deadlock_timeout = '1s'" >> "$PG_CONF"

修改配置说明:

配置 推荐值 作用
listen_addresses '*' 监听所有网卡,允许远程连接
port 5432 PostgreSQL 默认端口
password_encryption 'scram-sha-256' 设置新密码使用 SCRAM 加密方式
timezone 'Asia/Shanghai' 数据库时区
log_timezone 'Asia/Shanghai' 日志时间使用中国时区
log_min_duration_statement 1000 记录执行超过 1000ms 的 SQL
log_lock_waits on 记录锁等待事件
deadlock_timeout '1s' 锁等待超过 1 秒后触发锁等待日志

最终修改的配置:

BASH
listen_addresses = '*'
port = 5432
password_encryption = 'scram-sha-256'
timezone = 'Asia/Shanghai'
log_timezone = 'Asia/Shanghai'
log_min_duration_statement = 1000
log_lock_waits = on
deadlock_timeout = '1s'

验证配置:

BASH
root@instance-wc5p3ngj:/data# grep -E 'listen_addresses|port|password_encryption|timezone|log_timezone|log_min_duration_statement|log_lock_waits|deadlock_timeout' /etc/postgresql/16/main/postgresql.conf 

listen_addresses = '*'
port = 5432
password_encryption = 'scram-sha-256'
log_timezone = 'Asia/Shanghai'
timezone = 'Asia/Shanghai'
log_min_duration_statement = 1000
log_lock_waits = on
deadlock_timeout = '1s'

重启服务生效配置:

BASH
# 重启PostgreSQL 16服务
sudo systemctl restart postgresql@16-main

# 验证服务是否正常运行
sudo systemctl status postgresql@16-main

动态验证服务生效:

BASH
root@instance-wc5p3ngj:/data# sudo -u postgres psql
psql (16.14 (Ubuntu 16.14-1.pgdg22.04+1))
Type "help" for help.

postgres=# SHOW listen_addresses; 
 listen_addresses 
------------------
 *
(1 row)

postgres=# SHOW port; 
 port 
------
 5432
(1 row)

postgres=# SHOW password_encryption;
 password_encryption 
---------------------
 scram-sha-256
(1 row)

postgres=# SHOW timezone;
   TimeZone    
---------------
 Asia/Shanghai
(1 row)

postgres=# SHOW log_min_duration_statement;
 log_min_duration_statement 
----------------------------
 1s
(1 row)

postgres=# SHOW log_lock_waits;
 log_lock_waits 
----------------
 on
(1 row)

postgres=# SHOW deadlock_timeout; 
 deadlock_timeout 
------------------
 1s
(1 row)

postgres=# 

1.7 设置 postgres 密码

使用postgres用户进行登陆:

BASH
su - postgres
psql

执行SQL语句:

BASH
ALTER USER postgres WITH PASSWORD '你的强密码';
\q

退出数据库后退出postgres用户,使用root用户测试登陆。

修改了 postgres 管理员密码,直接在服务器上敲 psql 是无法验证密码的(PostgreSQL 默认本地连接免密),必须用密码认证模式连接,才能验证新密码是否生效。

BAH
psql -U postgres -h localhost -d postgres 
Password for user postgres: 
psql (16.14 (Ubuntu 16.14-1.pgdg22.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

postgres=# 

2、脚本安装

2.1 脚本内容

BASH
#!/usr/bin/env bash
# PostgreSQL apt 源安装脚本
# Ubuntu/Debian
# 默认所有数据、日志、备份和凭据均放在 /data/postgresql 下
# 使用方式:sudo bash postgresql_pgdg_install.sh install
# 卸载方式:sudo bash postgresql_pgdg_install.sh uninstall
# 查看状态:sudo bash postgresql_pgdg_install.sh status
# 重置密码:sudo bash postgresql_pgdg_install.sh reset-password

set -Eeuo pipefail
umask 027
export DEBIAN_FRONTEND=noninteractive

# PostgreSQL 主版本;PGDG APT 包按主版本安装,例如 postgresql-16
POSTGRES_VERSION="${POSTGRES_VERSION:-16}"
# 官方仓库默认会安装16.13

# PostgreSQL 集群名;Ubuntu/Debian 多版本多实例体系中常用 main
CLUSTER_NAME="${CLUSTER_NAME:-main}"

# PostgreSQL 运行用户和组;PGDG/Ubuntu 包默认使用 postgres
POSTGRES_USER="${POSTGRES_USER:-postgres}"
POSTGRES_GROUP="${POSTGRES_GROUP:-postgres}"

# 所有持久化目录默认都在 /data/postgresql 下
PG_BASE_DIR="${PG_BASE_DIR:-/data/postgresql}"
DATA_DIR="${DATA_DIR:-}"
LOG_DIR="${LOG_DIR:-}"
BACKUP_DIR="${BACKUP_DIR:-}"
AUTH_FILE="${AUTH_FILE:-}"

# PostgreSQL 软件真实安装目录由官方 APT 包固定为 /usr/lib/postgresql/<主版本>
# 这里提供软链接,便于统一运维路径
INSTALL_PREFIX="${INSTALL_PREFIX:-}"
INSTALL_LINK="${INSTALL_LINK:-/usr/local/pgsql}"

# 配置目录;pg_createcluster 默认生成到 /etc/postgresql/<版本>/<集群名>
CONFIG_BASE_DIR="${CONFIG_BASE_DIR:-/etc/postgresql}"
CONFIG_DIR="${CONFIG_DIR:-}"
CONF_FILE="${CONF_FILE:-}"
HBA_FILE="${HBA_FILE:-}"
IDENT_FILE="${IDENT_FILE:-}"

# 运行时目录仍使用 /run/postgresql,这是 PostgreSQL-common/systemd 约定目录,不属于持久化数据
RUN_DIR="${RUN_DIR:-/run/postgresql}"

# PGDG 仓库。默认使用阿里云国内镜像;如需官方源,设为 https://apt.postgresql.org/pub/repos/apt
PGDG_REPO_URL="${PGDG_REPO_URL:-https://mirrors.aliyun.com/postgresql/repos/apt}"
PGDG_KEY="${PGDG_KEY:-/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc}"
PGDG_SOURCE="${PGDG_SOURCE:-/etc/apt/sources.list.d/pgdg.sources}"
DISABLE_AUTO_MAIN_CLUSTER="${DISABLE_AUTO_MAIN_CLUSTER:-true}"

# 监听配置
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
LISTEN_ADDRESSES="${LISTEN_ADDRESSES:-0.0.0.0}"
ALLOW_REMOTE_CIDR="${ALLOW_REMOTE_CIDR:-0.0.0.0/0}"
PASSWORD_ENCRYPTION="${PASSWORD_ENCRYPTION:-scram-sha-256}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-passwd}"

# 是否保留关闭防火墙逻辑;如由安全组/iptables/nftables 管控,可设为 false
CLOSE_FIREWALL="${CLOSE_FIREWALL:-true}"

# 基础生产配置
MAX_CONNECTIONS="${MAX_CONNECTIONS:-1000}"
SHARED_BUFFERS="${SHARED_BUFFERS:-auto}"
SHARED_BUFFERS_PERCENT="${SHARED_BUFFERS_PERCENT:-25}"
EFFECTIVE_CACHE_SIZE="${EFFECTIVE_CACHE_SIZE:-auto}"
EFFECTIVE_CACHE_SIZE_PERCENT="${EFFECTIVE_CACHE_SIZE_PERCENT:-75}"
MAINTENANCE_WORK_MEM="${MAINTENANCE_WORK_MEM:-auto}"
WORK_MEM="${WORK_MEM:-auto}"
HUGE_PAGES="${HUGE_PAGES:-try}"
TIMEZONE="${TIMEZONE:-Asia/Shanghai}"
DEFAULT_TEXT_SEARCH_CONFIG="${DEFAULT_TEXT_SEARCH_CONFIG:-pg_catalog.simple}"

# WAL/checkpoint/复制相关配置
WAL_LEVEL="${WAL_LEVEL:-replica}"
WAL_COMPRESSION="${WAL_COMPRESSION:-on}"
MAX_WAL_SIZE="${MAX_WAL_SIZE:-8GB}"
MIN_WAL_SIZE="${MIN_WAL_SIZE:-1GB}"
CHECKPOINT_COMPLETION_TARGET="${CHECKPOINT_COMPLETION_TARGET:-0.9}"
MAX_WAL_SENDERS="${MAX_WAL_SENDERS:-10}"
MAX_REPLICATION_SLOTS="${MAX_REPLICATION_SLOTS:-10}"

# IO/查询优化配置;默认偏 SSD/NVMe。机械盘建议调高 RANDOM_PAGE_COST 并降低 EFFECTIVE_IO_CONCURRENCY
RANDOM_PAGE_COST="${RANDOM_PAGE_COST:-1.1}"
EFFECTIVE_IO_CONCURRENCY="${EFFECTIVE_IO_CONCURRENCY:-200}"
DEFAULT_STATISTICS_TARGET="${DEFAULT_STATISTICS_TARGET:-100}"
JIT="${JIT:-off}"

# 日志配置
LOG_MIN_DURATION_STATEMENT="${LOG_MIN_DURATION_STATEMENT:-1000}"
LOG_LINE_PREFIX="${LOG_LINE_PREFIX:-%m [%p] %u@%d %r }"
LOG_FILENAME="${LOG_FILENAME:-}"
LOG_ROTATION_AGE="${LOG_ROTATION_AGE:-1d}"
LOG_ROTATION_SIZE="${LOG_ROTATION_SIZE:-100MB}"
LOG_TRUNCATE_ON_ROTATION="${LOG_TRUNCATE_ON_ROTATION:-on}"
LOG_LOCK_WAITS="${LOG_LOCK_WAITS:-on}"
DEADLOCK_TIMEOUT="${DEADLOCK_TIMEOUT:-1s}"

# autovacuum 配置
AUTOVACUUM="${AUTOVACUUM:-on}"
AUTOVACUUM_MAX_WORKERS="${AUTOVACUUM_MAX_WORKERS:-5}"
AUTOVACUUM_NAPTIME="${AUTOVACUUM_NAPTIME:-30s}"
AUTOVACUUM_VACUUM_COST_LIMIT="${AUTOVACUUM_VACUUM_COST_LIMIT:-2000}"
AUTOVACUUM_VACUUM_SCALE_FACTOR="${AUTOVACUUM_VACUUM_SCALE_FACTOR:-0.05}"
AUTOVACUUM_ANALYZE_SCALE_FACTOR="${AUTOVACUUM_ANALYZE_SCALE_FACTOR:-0.02}"

# systemd、limits、sysctl
SERVICE_NAME="${SERVICE_NAME:-}"
LIMIT_NOFILE="${LIMIT_NOFILE:-65535}"
MANAGE_LIMITS="${MANAGE_LIMITS:-true}"
MANAGE_SYSCTL="${MANAGE_SYSCTL:-true}"
SWAPPINESS="${SWAPPINESS:-1}"
SOMAXCONN="${SOMAXCONN:-1024}"
FS_FILE_MAX="${FS_FILE_MAX:-1000000}"
VM_OVERCOMMIT_MEMORY="${VM_OVERCOMMIT_MEMORY:-0}"

# 是否把 PostgreSQL 常用命令软链接到系统 PATH 中
LINK_BINARIES="${LINK_BINARIES:-true}"
BIN_LINK_DIR="${BIN_LINK_DIR:-/usr/local/bin}"
WRITE_PROFILE="${WRITE_PROFILE:-true}"

# 是否创建业务库和业务用户
CREATE_APP_USER="${CREATE_APP_USER:-false}"
APP_DB_NAME="${APP_DB_NAME:-appdb}"
APP_DB_USER="${APP_DB_USER:-appuser}"
APP_DB_PASSWORD="${APP_DB_PASSWORD:-}"
APP_HOST_CIDR="${APP_HOST_CIDR:-0.0.0.0/0}"
APP_PRIVILEGES="${APP_PRIVILEGES:-ALL PRIVILEGES}"

# 卸载行为
PURGE_PACKAGES="${PURGE_PACKAGES:-true}"
REMOVE_POSTGRES_USER="${REMOVE_POSTGRES_USER:-false}"
REMOVE_LOG_FILES="${REMOVE_LOG_FILES:-true}"

OS_ID=""
OS_VERSION_CODENAME=""
PG_MAJOR=""
NEW_CLUSTER_CREATED="false"

log() {
  echo "[$(date '+%F %T')] [INFO] $*"
}

warn() {
  echo "[$(date '+%F %T')] [WARN] $*" >&2
}

err() {
  echo "[$(date '+%F %T')] [ERROR] $*" >&2
}

die() {
  err "$*"
  exit 1
}

usage() {
  cat <<EOF_USAGE
用法:
  sudo bash $0 install
  sudo bash $0 uninstall
  sudo bash $0 status
  sudo bash $0 reset-password

不传参数时默认执行 install。

默认目录:
  数据目录:/data/postgresql/<主版本>/<集群名>
  日志目录:/data/postgresql/log
  备份目录:/data/postgresql/backup
  凭据文件:/data/postgresql/backup/.postgresql.auth

常用环境变量示例:
  POSTGRES_VERSION=17 POSTGRES_PASSWORD='StrongPassword' sudo -E bash $0 install
  POSTGRES_VERSION=17 DATA_DIR=/data/postgresql/17/main LOG_DIR=/data/postgresql/log sudo -E bash $0 install
  ALLOW_REMOTE_CIDR='10.0.0.0/8' CLOSE_FIREWALL=false sudo -E bash $0 install
  SHARED_BUFFERS=8GB EFFECTIVE_CACHE_SIZE=24GB MAX_CONNECTIONS=500 sudo -E bash $0 install
  CREATE_APP_USER=true APP_DB_NAME=appdb APP_DB_USER=appuser APP_DB_PASSWORD='AppStrongPassword' sudo -E bash $0 install
  PGDG_REPO_URL='https://apt.postgresql.org/pub/repos/apt' sudo -E bash $0 install
EOF_USAGE
}

is_true() {
  [[ "${1,,}" == "true" || "${1,,}" == "yes" || "${1}" == "1" ]]
}

command_exists() {
  command -v "$1" >/dev/null 2>&1
}

require_root() {
  [[ "${EUID}" -eq 0 ]] || die "请使用 root 用户或 sudo 执行。"
}

confirm_yes() {
  local prompt="$1"
  local answer=""
  read -r -p "${prompt} 输入 yes 继续:" answer || return 1
  [[ "${answer}" == "yes" ]]
}

sql_escape() {
  printf "%s" "$1" | sed "s/'/''/g"
}

conf_quote() {
  local escaped
  escaped="$(printf "%s" "$1" | sed "s/'/''/g")"
  printf "'%s'" "${escaped}"
}

generate_password() {
  LC_ALL=C tr -dc 'A-Za-z0-9_@#%+=-' </dev/urandom | head -c 24
  echo
}

backup_file() {
  local file="$1"
  [[ -f "${file}" ]] || return 0
  install -d -m 0750 -o root -g root "${BACKUP_DIR}"
  cp -a "${file}" "${BACKUP_DIR}/$(basename "${file}").$(date '+%Y%m%d%H%M%S').bak"
}

detect_os() {
  [[ -r /etc/os-release ]] || die "无法识别系统:/etc/os-release 不存在。"
  . /etc/os-release
  OS_ID="${ID,,}"
  OS_VERSION_CODENAME="${VERSION_CODENAME:-}"

  case "${OS_ID}" in
    ubuntu|debian)
      ;;
    *)
      die "当前脚本仅支持 Ubuntu/Debian,当前系统:${OS_ID}。"
      ;;
  esac

  [[ -n "${OS_VERSION_CODENAME}" ]] || die "无法读取 VERSION_CODENAME。"
  log "系统识别完成:${PRETTY_NAME:-${OS_ID}},codename=${OS_VERSION_CODENAME}。"
}

normalize_version_and_paths() {
  if [[ "${POSTGRES_VERSION}" =~ ^[0-9]+\.[0-9]+$ ]]; then
    warn "PGDG APT 包按主版本安装,已将 POSTGRES_VERSION=${POSTGRES_VERSION} 规范化为 ${POSTGRES_VERSION%%.*}。"
    POSTGRES_VERSION="${POSTGRES_VERSION%%.*}"
  fi

  [[ "${POSTGRES_VERSION}" =~ ^[0-9]+$ ]] || die "POSTGRES_VERSION 必须是主版本号,例如 16、17、18。"
  PG_MAJOR="${POSTGRES_VERSION}"

  INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/lib/postgresql/${PG_MAJOR}}"
  CONFIG_DIR="${CONFIG_DIR:-${CONFIG_BASE_DIR}/${PG_MAJOR}/${CLUSTER_NAME}}"
  CONF_FILE="${CONF_FILE:-${CONFIG_DIR}/postgresql.conf}"
  HBA_FILE="${HBA_FILE:-${CONFIG_DIR}/pg_hba.conf}"
  IDENT_FILE="${IDENT_FILE:-${CONFIG_DIR}/pg_ident.conf}"

  DATA_DIR="${DATA_DIR:-${PG_BASE_DIR}/${PG_MAJOR}/${CLUSTER_NAME}}"
  LOG_DIR="${LOG_DIR:-${PG_BASE_DIR}/log}"
  BACKUP_DIR="${BACKUP_DIR:-${PG_BASE_DIR}/backup}"
  AUTH_FILE="${AUTH_FILE:-${BACKUP_DIR}/.postgresql.auth}"

  LOG_FILENAME="${LOG_FILENAME:-postgresql-${PG_MAJOR}-${CLUSTER_NAME}-%Y-%m-%d.log}"
  SERVICE_NAME="${SERVICE_NAME:-postgresql@${PG_MAJOR}-${CLUSTER_NAME}.service}"
  PGDG_REPO_URL="${PGDG_REPO_URL%/}"
}

validate_inputs() {
  [[ "${CLUSTER_NAME}" =~ ^[A-Za-z0-9_]+$ ]] || die "CLUSTER_NAME 只能包含字母、数字、下划线。"
  [[ "${POSTGRES_PORT}" =~ ^[0-9]+$ ]] || die "POSTGRES_PORT 必须是数字。"
  (( POSTGRES_PORT >= 1 && POSTGRES_PORT <= 65535 )) || die "POSTGRES_PORT 必须在 1 到 65535 之间。"
  [[ "${MAX_CONNECTIONS}" =~ ^[0-9]+$ ]] || die "MAX_CONNECTIONS 必须是数字。"
  [[ "${SHARED_BUFFERS_PERCENT}" =~ ^[0-9]+$ ]] || die "SHARED_BUFFERS_PERCENT 必须是数字。"
  [[ "${EFFECTIVE_CACHE_SIZE_PERCENT}" =~ ^[0-9]+$ ]] || die "EFFECTIVE_CACHE_SIZE_PERCENT 必须是数字。"

  case "${PASSWORD_ENCRYPTION}" in
    scram-sha-256|md5) ;;
    *) die "PASSWORD_ENCRYPTION 只能是 scram-sha-256 或 md5。" ;;
  esac

  case "${HUGE_PAGES}" in
    on|off|try) ;;
    *) die "HUGE_PAGES 只能是 on、off 或 try。" ;;
  esac

  if is_true "${CREATE_APP_USER}"; then
    [[ "${APP_DB_NAME}" =~ ^[A-Za-z0-9_]+$ ]] || die "APP_DB_NAME 只能包含字母、数字、下划线。"
    [[ "${APP_DB_USER}" =~ ^[A-Za-z0-9_]+$ ]] || die "APP_DB_USER 只能包含字母、数字、下划线。"
  fi
}

install_base_dependencies() {
  log "安装基础依赖。"
  apt-get update -y
  apt-get install -y ca-certificates curl gnupg lsb-release procps sysstat acl postgresql-common
}

disable_auto_cluster_creation() {
  is_true "${DISABLE_AUTO_MAIN_CLUSTER}" || return 0

  local file="/etc/postgresql-common/createcluster.conf"
  [[ -f "${file}" ]] || return 0

  backup_file "${file}"

  if grep -Eq '^[#[:space:]]*create_main_cluster[[:space:]]*=' "${file}"; then
    sed -ri 's/^[#[:space:]]*create_main_cluster[[:space:]]*=.*/create_main_cluster = false/' "${file}"
  else
    echo 'create_main_cluster = false' >> "${file}"
  fi

  log "已设置 ${file}: create_main_cluster = false,避免安装包自动创建默认数据目录。"
}

setup_pgdg_repo() {
  log "配置 PostgreSQL PGDG APT 仓库:${PGDG_REPO_URL}"

  install -d -m 0755 "$(dirname "${PGDG_KEY}")"

  if curl -fsSL -o "${PGDG_KEY}" "${PGDG_REPO_URL}/ACCC4CF8.asc"; then
    log "已从 ${PGDG_REPO_URL} 下载 PostgreSQL 仓库签名密钥。"
  else
    warn "从 ${PGDG_REPO_URL}/ACCC4CF8.asc 下载密钥失败,尝试官方源。"
    curl -fsSL -o "${PGDG_KEY}" https://www.postgresql.org/media/keys/ACCC4CF8.asc
  fi
  chmod 0644 "${PGDG_KEY}"

  cat > "${PGDG_SOURCE}" <<EOF_PGDG
Types: deb
URIs: ${PGDG_REPO_URL}
Suites: ${OS_VERSION_CODENAME}-pgdg
Architectures: $(dpkg --print-architecture)
Components: main ${PG_MAJOR}
Signed-By: ${PGDG_KEY}
EOF_PGDG

  rm -f /var/lib/apt/lists/*apt.postgresql.org* 2>/dev/null || true
  rm -f /var/lib/apt/lists/*mirrors.aliyun.com*postgresql* 2>/dev/null || true
  rm -f /var/lib/apt/lists/*mirrors.cloud.aliyuncs.com*postgresql* 2>/dev/null || true

  apt-get update -y

  if ! apt-cache show "postgresql-${PG_MAJOR}" >/dev/null 2>&1; then
    echo
    warn "当前可见的 PostgreSQL 主版本包如下:"
    apt-cache search '^postgresql-[0-9]+$' || true
    echo
    die "未找到 postgresql-${PG_MAJOR} 包。请检查 PGDG_REPO_URL=${PGDG_REPO_URL} 是否同步完整,或更换 POSTGRES_VERSION。"
  fi

  log "已找到 postgresql-${PG_MAJOR}$(apt-cache policy "postgresql-${PG_MAJOR}" | awk '/Candidate:/ {print $2; exit}')"
}

install_postgresql_packages() {
  local packages=()

  packages+=("postgresql-${PG_MAJOR}")
  packages+=("postgresql-client-${PG_MAJOR}")

  if apt-cache show "postgresql-contrib-${PG_MAJOR}" >/dev/null 2>&1; then
    packages+=("postgresql-contrib-${PG_MAJOR}")
  elif apt-cache show "postgresql-contrib" >/dev/null 2>&1; then
    packages+=("postgresql-contrib")
  else
    warn "未找到 postgresql-contrib 包,跳过 contrib 扩展包安装。"
  fi

  log "安装 PostgreSQL ${PG_MAJOR} 官方二进制包:${packages[*]}。"
  apt-get install -y "${packages[@]}"

  [[ -x "/usr/lib/postgresql/${PG_MAJOR}/bin/postgres" ]] || die "PostgreSQL 二进制文件不存在:/usr/lib/postgresql/${PG_MAJOR}/bin/postgres。"

  ln -sfn "/usr/lib/postgresql/${PG_MAJOR}" "${INSTALL_LINK}"

  if [[ -d "${INSTALL_LINK}/lib" ]]; then
    echo "${INSTALL_LINK}/lib" > "/etc/ld.so.conf.d/postgresql-${PG_MAJOR}.conf"
    ldconfig || warn "ldconfig 执行失败,请手动检查。"
  fi
}

cluster_exists() {
  pg_lsclusters --no-header 2>/dev/null | awk -v ver="${PG_MAJOR}" -v name="${CLUSTER_NAME}" '
    $1 == ver && $2 == name { found=1 }
    END { exit found ? 0 : 1 }
  '
}

cluster_status() {
  pg_lsclusters --no-header 2>/dev/null | awk -v ver="${PG_MAJOR}" -v name="${CLUSTER_NAME}" '
    $1 == ver && $2 == name { print $4; exit }
  '
}

cluster_data_dir() {
  pg_lsclusters --no-header 2>/dev/null | awk -v ver="${PG_MAJOR}" -v name="${CLUSTER_NAME}" '
    $1 == ver && $2 == name { print $6; exit }
  '
}

cluster_port() {
  pg_lsclusters --no-header 2>/dev/null | awk -v ver="${PG_MAJOR}" -v name="${CLUSTER_NAME}" '
    $1 == ver && $2 == name { print $3; exit }
  '
}

check_port_conflict() {
  local conflict
  conflict="$(pg_lsclusters --no-header 2>/dev/null | awk -v ver="${PG_MAJOR}" -v name="${CLUSTER_NAME}" -v port="${POSTGRES_PORT}" '
    $3 == port && !($1 == ver && $2 == name) { print $1 "/" $2 " 使用端口 " $3 }
  ' || true)"

  [[ -z "${conflict}" ]] || die "端口 ${POSTGRES_PORT} 已被其他 PostgreSQL 集群占用:${conflict}。"
}

prepare_dirs() {
  log "创建 PostgreSQL 目录。"
  install -d -m 0750 -o "${POSTGRES_USER}" -g "${POSTGRES_GROUP}" "${DATA_DIR}"
  install -d -m 0750 -o "${POSTGRES_USER}" -g "${POSTGRES_GROUP}" "${LOG_DIR}"
  install -d -m 0755 -o "${POSTGRES_USER}" -g "${POSTGRES_GROUP}" "${RUN_DIR}" || true
  install -d -m 0750 -o root -g root "${BACKUP_DIR}"
}

ensure_data_dir_safe() {
  if [[ -f "${DATA_DIR}/PG_VERSION" ]]; then
    return 0
  fi

  if find "${DATA_DIR}" -mindepth 1 -maxdepth 1 ! -name lost+found | grep -q .; then
    die "数据目录 ${DATA_DIR} 非空但未检测到 PostgreSQL 初始化标记 PG_VERSION。为避免误删数据,请清空目录或更换 DATA_DIR。"
  fi
}

create_or_validate_cluster() {
  check_port_conflict
  prepare_dirs
  ensure_data_dir_safe

  if cluster_exists; then
    local existing_dir real_existing real_expected existing_port
    existing_dir="$(cluster_data_dir)"
    existing_port="$(cluster_port)"
    real_existing="$(readlink -f "${existing_dir}")"
    real_expected="$(readlink -f "${DATA_DIR}")"

    [[ "${real_existing}" == "${real_expected}" ]] || die "已存在集群 ${PG_MAJOR}/${CLUSTER_NAME},但数据目录为 ${existing_dir},不是当前配置的 ${DATA_DIR}。"
    [[ "${existing_port}" == "${POSTGRES_PORT}" ]] || warn "已存在集群端口为 ${existing_port},当前配置 POSTGRES_PORT=${POSTGRES_PORT};稍后会写入配置并重启。"
    log "PostgreSQL 集群已存在:${PG_MAJOR}/${CLUSTER_NAME}。"
    NEW_CLUSTER_CREATED="false"
  else
    log "创建 PostgreSQL 集群:${PG_MAJOR}/${CLUSTER_NAME},数据目录:${DATA_DIR}。"
    pg_createcluster "${PG_MAJOR}" "${CLUSTER_NAME}" \
      --datadir="${DATA_DIR}" \
      --port="${POSTGRES_PORT}" \
      --start
    NEW_CLUSTER_CREATED="true"
  fi

  [[ -f "${CONF_FILE}" ]] || die "配置文件不存在:${CONF_FILE}。"
  [[ -f "${HBA_FILE}" ]] || die "认证文件不存在:${HBA_FILE}。"
}

calculate_percent_mb() {
  local percent="$1"
  local min_mb="$2"
  local max_mb="$3"
  local mem_kb mem_mb result

  mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo)"
  mem_mb=$(( mem_kb / 1024 ))
  result=$(( mem_mb * percent / 100 ))

  (( result >= min_mb )) || result="${min_mb}"
  if [[ "${max_mb}" -gt 0 && "${result}" -gt "${max_mb}" ]]; then
    result="${max_mb}"
  fi

  echo "${result}MB"
}

calculate_work_mem() {
  if [[ "${WORK_MEM}" != "auto" ]]; then
    echo "${WORK_MEM}"
    return 0
  fi

  local mem_kb mem_mb result
  mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo)"
  mem_mb=$(( mem_kb / 1024 ))
  result=$(( mem_mb / MAX_CONNECTIONS / 4 ))
  (( result >= 4 )) || result=4
  (( result <= 64 )) || result=64
  echo "${result}MB"
}

postgresql_conf_set() {
  local key="$1"
  local value="$2"
  local file="${CONF_FILE}"

  if grep -Eq "^[#[:space:]]*${key}[[:space:]]*=" "${file}"; then
    sed -ri "s|^[#[:space:]]*${key}[[:space:]]*=.*|${key} = ${value}|" "${file}"
  else
    echo "${key} = ${value}" >> "${file}"
  fi
}

write_postgresql_conf() {
  backup_file "${CONF_FILE}"

  local effective_shared_buffers effective_cache effective_maintenance_mem effective_work_mem

  if [[ "${SHARED_BUFFERS}" == "auto" ]]; then
    effective_shared_buffers="$(calculate_percent_mb "${SHARED_BUFFERS_PERCENT}" 128 0)"
  else
    effective_shared_buffers="${SHARED_BUFFERS}"
  fi

  if [[ "${EFFECTIVE_CACHE_SIZE}" == "auto" ]]; then
    effective_cache="$(calculate_percent_mb "${EFFECTIVE_CACHE_SIZE_PERCENT}" 256 0)"
  else
    effective_cache="${EFFECTIVE_CACHE_SIZE}"
  fi

  if [[ "${MAINTENANCE_WORK_MEM}" == "auto" ]]; then
    effective_maintenance_mem="$(calculate_percent_mb 5 64 2048)"
  else
    effective_maintenance_mem="${MAINTENANCE_WORK_MEM}"
  fi

  effective_work_mem="$(calculate_work_mem)"

  log "写入 PostgreSQL 生产配置:${CONF_FILE}。"

  postgresql_conf_set "data_directory" "$(conf_quote "${DATA_DIR}")"
  postgresql_conf_set "hba_file" "$(conf_quote "${HBA_FILE}")"
  postgresql_conf_set "ident_file" "$(conf_quote "${IDENT_FILE}")"
  postgresql_conf_set "external_pid_file" "$(conf_quote "${RUN_DIR}/${PG_MAJOR}-${CLUSTER_NAME}.pid")"
  postgresql_conf_set "listen_addresses" "$(conf_quote "${LISTEN_ADDRESSES}")"
  postgresql_conf_set "port" "${POSTGRES_PORT}"
  postgresql_conf_set "unix_socket_directories" "$(conf_quote "${RUN_DIR}")"
  postgresql_conf_set "password_encryption" "$(conf_quote "${PASSWORD_ENCRYPTION}")"
  postgresql_conf_set "max_connections" "${MAX_CONNECTIONS}"
  postgresql_conf_set "shared_buffers" "${effective_shared_buffers}"
  postgresql_conf_set "effective_cache_size" "${effective_cache}"
  postgresql_conf_set "maintenance_work_mem" "${effective_maintenance_mem}"
  postgresql_conf_set "work_mem" "${effective_work_mem}"
  postgresql_conf_set "huge_pages" "${HUGE_PAGES}"
  postgresql_conf_set "dynamic_shared_memory_type" "posix"
  postgresql_conf_set "timezone" "$(conf_quote "${TIMEZONE}")"
  postgresql_conf_set "log_timezone" "$(conf_quote "${TIMEZONE}")"
  postgresql_conf_set "default_text_search_config" "$(conf_quote "${DEFAULT_TEXT_SEARCH_CONFIG}")"

  postgresql_conf_set "wal_level" "${WAL_LEVEL}"
  postgresql_conf_set "wal_compression" "${WAL_COMPRESSION}"
  postgresql_conf_set "max_wal_size" "${MAX_WAL_SIZE}"
  postgresql_conf_set "min_wal_size" "${MIN_WAL_SIZE}"
  postgresql_conf_set "checkpoint_completion_target" "${CHECKPOINT_COMPLETION_TARGET}"
  postgresql_conf_set "max_wal_senders" "${MAX_WAL_SENDERS}"
  postgresql_conf_set "max_replication_slots" "${MAX_REPLICATION_SLOTS}"

  postgresql_conf_set "random_page_cost" "${RANDOM_PAGE_COST}"
  postgresql_conf_set "effective_io_concurrency" "${EFFECTIVE_IO_CONCURRENCY}"
  postgresql_conf_set "default_statistics_target" "${DEFAULT_STATISTICS_TARGET}"
  postgresql_conf_set "jit" "${JIT}"

  postgresql_conf_set "logging_collector" "on"
  postgresql_conf_set "log_directory" "$(conf_quote "${LOG_DIR}")"
  postgresql_conf_set "log_filename" "$(conf_quote "${LOG_FILENAME}")"
  postgresql_conf_set "log_rotation_age" "${LOG_ROTATION_AGE}"
  postgresql_conf_set "log_rotation_size" "${LOG_ROTATION_SIZE}"
  postgresql_conf_set "log_truncate_on_rotation" "${LOG_TRUNCATE_ON_ROTATION}"
  postgresql_conf_set "log_min_duration_statement" "${LOG_MIN_DURATION_STATEMENT}"
  postgresql_conf_set "log_line_prefix" "$(conf_quote "${LOG_LINE_PREFIX}")"
  postgresql_conf_set "log_lock_waits" "${LOG_LOCK_WAITS}"
  postgresql_conf_set "deadlock_timeout" "${DEADLOCK_TIMEOUT}"
  postgresql_conf_set "log_checkpoints" "on"
  postgresql_conf_set "log_autovacuum_min_duration" "1000"

  postgresql_conf_set "autovacuum" "${AUTOVACUUM}"
  postgresql_conf_set "autovacuum_max_workers" "${AUTOVACUUM_MAX_WORKERS}"
  postgresql_conf_set "autovacuum_naptime" "${AUTOVACUUM_NAPTIME}"
  postgresql_conf_set "autovacuum_vacuum_cost_limit" "${AUTOVACUUM_VACUUM_COST_LIMIT}"
  postgresql_conf_set "autovacuum_vacuum_scale_factor" "${AUTOVACUUM_VACUUM_SCALE_FACTOR}"
  postgresql_conf_set "autovacuum_analyze_scale_factor" "${AUTOVACUUM_ANALYZE_SCALE_FACTOR}"

  chown "${POSTGRES_USER}:${POSTGRES_GROUP}" "${CONF_FILE}"
  chmod 0640 "${CONF_FILE}"
}

write_pg_hba() {
  backup_file "${HBA_FILE}"

  log "写入 PostgreSQL 认证配置:${HBA_FILE}。"

  cat > "${HBA_FILE}" <<EOF_HBA
# 由 postgresql_pgdg_install.sh 生成
# local postgres 使用 peer,便于本机 root/runuser 运维
# 业务访问默认使用 ${PASSWORD_ENCRYPTION}

local   all             postgres                                peer
local   all             all                                     ${PASSWORD_ENCRYPTION}
host    all             all             127.0.0.1/32            ${PASSWORD_ENCRYPTION}
host    all             all             ::1/128                 ${PASSWORD_ENCRYPTION}
host    all             all             ${ALLOW_REMOTE_CIDR}    ${PASSWORD_ENCRYPTION}
EOF_HBA

  chown "${POSTGRES_USER}:${POSTGRES_GROUP}" "${HBA_FILE}"
  chmod 0640 "${HBA_FILE}"
}

configure_limits() {
  is_true "${MANAGE_LIMITS}" || return 0
  log "配置 PostgreSQL 文件句柄限制。"

  cat > "/etc/security/limits.d/99-${POSTGRES_USER}.conf" <<EOF_LIMITS
${POSTGRES_USER} soft nofile ${LIMIT_NOFILE}
${POSTGRES_USER} hard nofile ${LIMIT_NOFILE}
${POSTGRES_USER} soft nproc ${LIMIT_NOFILE}
${POSTGRES_USER} hard nproc ${LIMIT_NOFILE}
EOF_LIMITS
}

configure_sysctl() {
  is_true "${MANAGE_SYSCTL}" || return 0
  log "写入 PostgreSQL sysctl 参数。"

  cat > /etc/sysctl.d/99-postgresql.conf <<EOF_SYSCTL
vm.swappiness = ${SWAPPINESS}
net.core.somaxconn = ${SOMAXCONN}
fs.file-max = ${FS_FILE_MAX}
vm.overcommit_memory = ${VM_OVERCOMMIT_MEMORY}
EOF_SYSCTL

  sysctl -p /etc/sysctl.d/99-postgresql.conf >/dev/null || warn "加载 sysctl 参数失败,请手动检查 /etc/sysctl.d/99-postgresql.conf。"
}

write_systemd_override() {
  local override_dir="/etc/systemd/system/${SERVICE_NAME}.d"
  install -d -m 0755 "${override_dir}"

  log "写入 systemd override:${override_dir}/override.conf。"

  cat > "${override_dir}/override.conf" <<EOF_SYSTEMD
[Service]
LimitNOFILE=${LIMIT_NOFILE}
OOMScoreAdjust=-500
EOF_SYSTEMD

  systemctl daemon-reload
}

link_binaries() {
  is_true "${LINK_BINARIES}" || return 0
  log "创建 PostgreSQL 常用命令软链接到 ${BIN_LINK_DIR}。"

  install -d -m 0755 "${BIN_LINK_DIR}"
  local bin
  for bin in psql pg_dump pg_restore pg_basebackup pg_isready pg_ctl initdb postgres createdb createuser dropdb dropuser vacuumdb reindexdb clusterdb; do
    if [[ -x "${INSTALL_LINK}/bin/${bin}" ]]; then
      ln -sfn "${INSTALL_LINK}/bin/${bin}" "${BIN_LINK_DIR}/${bin}"
    fi
  done
}

write_profile() {
  is_true "${WRITE_PROFILE}" || return 0
  log "写入 PostgreSQL PATH 配置。"

  cat > /etc/profile.d/postgresql.sh <<EOF_PROFILE
# 由 postgresql_pgdg_install.sh 生成
export PGHOME=${INSTALL_LINK}
export PGDATA=${DATA_DIR}
export PGPORT=${POSTGRES_PORT}
export PATH=${INSTALL_LINK}/bin:\$PATH
EOF_PROFILE

  chmod 0644 /etc/profile.d/postgresql.sh
}

close_firewall() {
  is_true "${CLOSE_FIREWALL}" || return 0

  if command_exists ufw && ufw status 2>/dev/null | grep -q "active"; then
    log "关闭 ufw 防火墙。"
    ufw disable >/dev/null || warn "ufw disable 执行失败。"
  fi

  if command_exists firewall-cmd && systemctl is-active --quiet firewalld; then
    log "关闭 firewalld 防火墙。"
    systemctl stop firewalld || true
    systemctl disable firewalld >/dev/null 2>&1 || true
  fi
}

start_or_restart_cluster() {
  log "启动/重启 PostgreSQL 集群:${PG_MAJOR}/${CLUSTER_NAME}。"

  install -d -m 2775 -o "${POSTGRES_USER}" -g "${POSTGRES_GROUP}" "${RUN_DIR}"

  if [[ "$(cluster_status || true)" == "online" ]]; then
    pg_ctlcluster "${PG_MAJOR}" "${CLUSTER_NAME}" restart
  else
    pg_ctlcluster "${PG_MAJOR}" "${CLUSTER_NAME}" start
  fi

  systemctl enable postgresql >/dev/null 2>&1 || true

  local i
  for i in $(seq 1 60); do
    if pg_isready -h 127.0.0.1 -p "${POSTGRES_PORT}" -U postgres >/dev/null 2>&1; then
      log "PostgreSQL 健康检查通过。"
      return 0
    fi
    sleep 1
  done

  echo
  warn "PostgreSQL 健康检查失败,输出最近日志:"
  pg_lsclusters || true
  journalctl -u "postgresql@${PG_MAJOR}-${CLUSTER_NAME}.service" --no-pager -n 120 >&2 || true
  find "${LOG_DIR}" -maxdepth 1 -type f -name "postgresql-${PG_MAJOR}-${CLUSTER_NAME}-*.log" -print -exec tail -n 120 {} \; 2>/dev/null || true
  die "PostgreSQL 健康检查失败。"
}

set_postgres_password() {
  if [[ -z "${POSTGRES_PASSWORD}" ]]; then
    POSTGRES_PASSWORD="$(generate_password)"
    log "已自动生成 postgres 密码。"
  fi

  local escaped
  escaped="$(sql_escape "${POSTGRES_PASSWORD}")"

  log "设置 postgres 用户密码。"
  runuser -u "${POSTGRES_USER}" -- "${INSTALL_LINK}/bin/psql" -X -v ON_ERROR_STOP=1 --dbname=postgres <<EOF_SQL
ALTER USER postgres WITH PASSWORD '${escaped}';
EOF_SQL

  install -d -m 0750 -o root -g root "$(dirname "${AUTH_FILE}")"
  cat > "${AUTH_FILE}" <<EOF_AUTH
PGHOST=127.0.0.1
PGPORT=${POSTGRES_PORT}
PGUSER=postgres
PGPASSWORD=${POSTGRES_PASSWORD}
PGDATABASE=postgres
PGDATA=${DATA_DIR}
PGSERVICE=${SERVICE_NAME}
EOF_AUTH
  chmod 0600 "${AUTH_FILE}"
}

run_postgres_sql() {
  PGPASSWORD="${POSTGRES_PASSWORD}" "${INSTALL_LINK}/bin/psql" \
    -h 127.0.0.1 \
    -p "${POSTGRES_PORT}" \
    -U postgres \
    -d postgres \
    -X \
    -v ON_ERROR_STOP=1
}

create_app_database() {
  is_true "${CREATE_APP_USER}" || return 0

  if [[ -z "${APP_DB_PASSWORD}" ]]; then
    APP_DB_PASSWORD="$(generate_password)"
    log "已自动生成业务用户密码。"
  fi

  local escaped_app_password
  escaped_app_password="$(sql_escape "${APP_DB_PASSWORD}")"

  log "创建业务库和业务用户:${APP_DB_NAME} / ${APP_DB_USER}。"

  run_postgres_sql <<EOF_SQL
DO \$\$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${APP_DB_USER}') THEN
    CREATE ROLE ${APP_DB_USER} LOGIN PASSWORD '${escaped_app_password}';
  ELSE
    ALTER ROLE ${APP_DB_USER} WITH LOGIN PASSWORD '${escaped_app_password}';
  END IF;
END
\$\$;

SELECT 'CREATE DATABASE ${APP_DB_NAME} OWNER ${APP_DB_USER}'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = '${APP_DB_NAME}')\gexec

GRANT ${APP_PRIVILEGES} ON DATABASE ${APP_DB_NAME} TO ${APP_DB_USER};
EOF_SQL

  if ! grep -q '^APP_DB_NAME=' "${AUTH_FILE}" 2>/dev/null; then
    cat >> "${AUTH_FILE}" <<EOF_APP_AUTH
APP_DB_NAME=${APP_DB_NAME}
APP_DB_USER=${APP_DB_USER}
APP_DB_PASSWORD=${APP_DB_PASSWORD}
APP_HOST_CIDR=${APP_HOST_CIDR}
EOF_APP_AUTH
    chmod 0600 "${AUTH_FILE}"
  fi

  if ! grep -Eq "^host[[:space:]]+${APP_DB_NAME}[[:space:]]+${APP_DB_USER}[[:space:]]+${APP_HOST_CIDR}[[:space:]]+" "${HBA_FILE}"; then
    cat >> "${HBA_FILE}" <<EOF_APP_HBA
host    ${APP_DB_NAME}    ${APP_DB_USER}    ${APP_HOST_CIDR}    ${PASSWORD_ENCRYPTION}
EOF_APP_HBA
    pg_ctlcluster "${PG_MAJOR}" "${CLUSTER_NAME}" reload
  fi
}

print_result() {
  local effective_shared effective_cache effective_work_mem
  effective_shared="$(awk -F= '/^[[:space:]]*shared_buffers[[:space:]]*=/{gsub(/[[:space:]]/,"",$2); print $2}' "${CONF_FILE}" | tail -n1)"
  effective_cache="$(awk -F= '/^[[:space:]]*effective_cache_size[[:space:]]*=/{gsub(/[[:space:]]/,"",$2); print $2}' "${CONF_FILE}" | tail -n1)"
  effective_work_mem="$(awk -F= '/^[[:space:]]*work_mem[[:space:]]*=/{gsub(/[[:space:]]/,"",$2); print $2}' "${CONF_FILE}" | tail -n1)"

  cat <<EOF_RESULT

PostgreSQL 安装完成。

版本:${PG_MAJOR}
服务:${SERVICE_NAME}
软件目录:/usr/lib/postgresql/${PG_MAJOR}
软链接:${INSTALL_LINK}
命令目录:${INSTALL_LINK}/bin
配置目录:${CONFIG_DIR}
配置文件:${CONF_FILE}
认证文件:${HBA_FILE}
数据目录:${DATA_DIR}
日志目录:${LOG_DIR}
备份目录:${BACKUP_DIR}
运行目录:${RUN_DIR}
端口:${POSTGRES_PORT}
监听:${LISTEN_ADDRESSES}
允许访问:${ALLOW_REMOTE_CIDR}
shared_buffers:${effective_shared}
effective_cache_size:${effective_cache}
work_mem:${effective_work_mem}
凭据文件:${AUTH_FILE}

常用命令:
  systemctl status ${SERVICE_NAME}
  systemctl restart ${SERVICE_NAME}
  journalctl -u ${SERVICE_NAME} -f
  psql -h 127.0.0.1 -p ${POSTGRES_PORT} -U postgres -d postgres
  pg_lsclusters
EOF_RESULT

  if is_true "${CREATE_APP_USER}"; then
    cat <<EOF_APP_RESULT
业务库:
  数据库:${APP_DB_NAME}
  用户:${APP_DB_USER}
  密码:已写入 ${AUTH_FILE}
EOF_APP_RESULT
  fi
}

install_postgresql() {
  require_root
  normalize_version_and_paths
  validate_inputs
  detect_os
  install_base_dependencies
  disable_auto_cluster_creation
  setup_pgdg_repo
  install_postgresql_packages
  configure_limits
  configure_sysctl
  link_binaries
  write_profile
  close_firewall
  create_or_validate_cluster
  write_postgresql_conf
  write_pg_hba
  write_systemd_override
  start_or_restart_cluster
  set_postgres_password
  start_or_restart_cluster
  create_app_database
  print_result
}

reset_password() {
  require_root
  normalize_version_and_paths
  validate_inputs

  command_exists pg_lsclusters || die "未找到 pg_lsclusters,请先安装 PostgreSQL。"
  cluster_exists || die "未找到 PostgreSQL 集群 ${PG_MAJOR}/${CLUSTER_NAME}。"

  if [[ "$(cluster_status || true)" != "online" ]]; then
    pg_ctlcluster "${PG_MAJOR}" "${CLUSTER_NAME}" start
  fi

  set_postgres_password
  log "postgres 密码重置完成,凭据已写入 ${AUTH_FILE}。"
}

status_postgresql() {
  require_root
  normalize_version_and_paths
  detect_os

  echo
  pg_lsclusters || true
  echo

  if command_exists systemctl; then
    systemctl --no-pager --full status "${SERVICE_NAME}" || true
  fi
}

uninstall_postgresql() {
  require_root
  normalize_version_and_paths
  detect_os

  echo
  warn "即将卸载 PostgreSQL ${PG_MAJOR}/${CLUSTER_NAME}。"
  warn "服务:${SERVICE_NAME}"
  warn "配置目录:${CONFIG_DIR}"
  warn "数据目录:${DATA_DIR}"
  warn "日志目录:${LOG_DIR}"
  warn "备份目录:${BACKUP_DIR}"
  echo

  if ! confirm_yes "第一次确认:是否真的卸载 PostgreSQL?"; then
    log "用户取消卸载。"
    exit 0
  fi

  if command_exists pg_lsclusters && cluster_exists; then
    log "删除 PostgreSQL 集群:${PG_MAJOR}/${CLUSTER_NAME}。"
    pg_dropcluster --stop "${PG_MAJOR}" "${CLUSTER_NAME}"
  else
    warn "未检测到集群 ${PG_MAJOR}/${CLUSTER_NAME},跳过 pg_dropcluster。"
    systemctl stop "${SERVICE_NAME}" 2>/dev/null || true
    systemctl disable "${SERVICE_NAME}" 2>/dev/null || true
  fi

  log "清理 systemd override。"
  rm -rf "/etc/systemd/system/${SERVICE_NAME}.d"
  systemctl daemon-reload || true
  systemctl reset-failed || true

  echo
  warn "第二次确认将删除 PostgreSQL 相关文件。"
  warn "将删除:${INSTALL_LINK}${CONFIG_DIR}${DATA_DIR}${AUTH_FILE}、profile、limits、sysctl、ldconfig、命令软链接。"
  if is_true "${REMOVE_LOG_FILES}"; then
    warn "还将删除匹配 ${LOG_DIR}/postgresql-${PG_MAJOR}-${CLUSTER_NAME}-*.log 的日志文件。"
  fi
  echo

  if ! confirm_yes "第二次确认:是否删除配置、数据和脚本生成的文件?"; then
    log "用户选择保留文件。卸载已完成:集群已停止并移除,文件未删除。"
    exit 0
  fi

  log "清理 PostgreSQL 命令软链接。"
  local bin
  for bin in psql pg_dump pg_restore pg_basebackup pg_isready pg_ctl initdb postgres createdb createuser dropdb dropuser vacuumdb reindexdb clusterdb; do
    if [[ -L "${BIN_LINK_DIR}/${bin}" ]] && readlink "${BIN_LINK_DIR}/${bin}" | grep -q "${INSTALL_LINK}/bin/"; then
      rm -f "${BIN_LINK_DIR}/${bin}"
    fi
  done

  log "清理 PostgreSQL 文件。"
  rm -f "${INSTALL_LINK}"
  rm -rf "${CONFIG_DIR}"
  rm -rf "${DATA_DIR}"
  rm -f "${AUTH_FILE}"
  rm -f /etc/profile.d/postgresql.sh
  rm -f "/etc/security/limits.d/99-${POSTGRES_USER}.conf"
  rm -f /etc/sysctl.d/99-postgresql.conf
  rm -f "/etc/ld.so.conf.d/postgresql-${PG_MAJOR}.conf"
  ldconfig || true

  if is_true "${REMOVE_LOG_FILES}"; then
    find "${LOG_DIR}" -maxdepth 1 -type f -name "postgresql-${PG_MAJOR}-${CLUSTER_NAME}-*.log" -delete 2>/dev/null || true
  fi

  if is_true "${PURGE_PACKAGES}"; then
    log "卸载 PostgreSQL ${PG_MAJOR} 软件包。"
    apt-get purge -y "postgresql-${PG_MAJOR}" "postgresql-client-${PG_MAJOR}" || true
    apt-get autoremove -y || true
  fi

  if is_true "${REMOVE_POSTGRES_USER}"; then
    warn "删除 postgres 用户可能影响其他 PostgreSQL 版本或集群。"
    userdel "${POSTGRES_USER}" 2>/dev/null || true
    groupdel "${POSTGRES_GROUP}" 2>/dev/null || true
  fi

  log "PostgreSQL 卸载完成。"
}

main() {
  local action="${1:-install}"

  case "${action}" in
    install)
      install_postgresql
      ;;
    uninstall|remove|purge)
      uninstall_postgresql
      ;;
    status)
      status_postgresql
      ;;
    reset-password|resetpwd)
      reset_password
      ;;
    -h|--help|help)
      usage
      ;;
    *)
      usage
      die "不支持的参数:${action}。"
      ;;
  esac
}

main "$@"

2.2 手动配置脚本参数

脚本中的参数都集中在开头部分,正常情况下只需要关注少量核心参数,不需要修改全部配置。

参数 默认值 作用
POSTGRES_VERSION 16 PostgreSQL 主版本
CLUSTER_NAME main PostgreSQL 集群名称
PG_BASE_DIR /data/postgresql PostgreSQL 数据、日志、备份的根目录
POSTGRES_PORT 5432 PostgreSQL 监听端口
LISTEN_ADDRESSES 0.0.0.0 PostgreSQL 监听地址
ALLOW_REMOTE_CIDR 0.0.0.0/0 允许访问 PostgreSQL 的远程网段
POSTGRES_PASSWORD passwd postgres 用户密码
PASSWORD_ENCRYPTION scram-sha-256 密码认证方式
PGDG_REPO_URL https://mirrors.aliyun.com/postgresql/repos/apt PostgreSQL APT 源地址
CREATE_APP_USER false 是否创建业务库和业务用户
APP_DB_NAME appdb 业务数据库名称
APP_DB_USER appuser 业务数据库用户
APP_DB_PASSWORD 业务数据库用户密码

如果只做基础安装,一般只需要修改以下几个参数:

BASH
POSTGRES_VERSION="${POSTGRES_VERSION:-16}"
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
LISTEN_ADDRESSES="${LISTEN_ADDRESSES:-0.0.0.0}"
ALLOW_REMOTE_CIDR="${ALLOW_REMOTE_CIDR:-0.0.0.0/0}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-passwd}"

2.3 脚本执行流程

脚本执行安装时,整体流程如下:

步骤 说明
1 检查是否使用 root 或 sudo 执行
2 识别当前系统版本,只支持 Ubuntu / Debian
3 安装基础依赖包
4 配置 PostgreSQL PGDG APT 源
5 安装 PostgreSQL 指定版本的软件包
6 创建 PostgreSQL 数据目录、日志目录、备份目录
7 创建或检查 PostgreSQL 集群
8 写入 postgresql.conf 主配置
9 写入 pg_hba.conf 访问控制配置
10 配置 systemd、limits、sysctl 等基础系统参数
11 启动或重启 PostgreSQL 服务
12 设置 postgres 用户密码
13 根据参数决定是否创建业务库和业务用户
14 输出安装结果和常用命令

脚本默认执行的是安装流程:

BASH
sudo bash postgresql_pgdg_install.sh install

不传参数时,也会默认执行安装:

BASH
sudo bash postgresql_pgdg_install.sh

查看状态:

BASH
sudo bash postgresql_pgdg_install.sh status

重置 postgres 用户密码:

BASH
POSTGRES_PASSWORD='NewStrongPassword' sudo -E bash postgresql_pgdg_install.sh reset-password

卸载 PostgreSQL:

BASH
sudo bash postgresql_pgdg_install.sh uninstall

注意:卸载操作会涉及停止集群、删除配置、删除数据目录等动作,生产环境不要随意执行。

2.4 执行脚本测试

BASH
 ./psql_install.sh install 
[2026-05-15 10:56:25] [INFO] 系统识别完成:Ubuntu 22.04.5 LTS,codename=jammy。
[2026-05-15 10:56:25] [INFO] 安装基础依赖。
Hit:1 http://mirrors.tencentyun.com/ubuntu jammy InRelease
Hit:2 http://mirrors.tencentyun.com/ubuntu jammy-updates InRelease
Hit:3 http://mirrors.tencentyun.com/ubuntu jammy-security InRelease

...... 自动安装过程省略......

[2026-05-15 10:57:07] [INFO] 写入 PostgreSQL 生产配置:/etc/postgresql/16/main/postgresql.conf。
[2026-05-15 10:57:07] [INFO] 写入 PostgreSQL 认证配置:/etc/postgresql/16/main/pg_hba.conf。
[2026-05-15 10:57:07] [INFO] 写入 systemd override:/etc/systemd/system/postgresql@16-main.service.d/override.conf。
[2026-05-15 10:57:07] [INFO] 启动/重启 PostgreSQL 集群:16/main。
[2026-05-15 10:57:11] [INFO] PostgreSQL 健康检查通过。
[2026-05-15 10:57:11] [INFO] 设置 postgres 用户密码。
ALTER ROLE
[2026-05-15 10:57:11] [INFO] 启动/重启 PostgreSQL 集群:16/main。
[2026-05-15 10:57:14] [INFO] PostgreSQL 健康检查通过。

PostgreSQL 安装完成。

版本:16
服务:postgresql@16-main.service
软件目录:/usr/lib/postgresql/16
软链接:/usr/local/pgsql
命令目录:/usr/local/pgsql/bin
配置目录:/etc/postgresql/16/main
配置文件:/etc/postgresql/16/main/postgresql.conf
认证文件:/etc/postgresql/16/main/pg_hba.conf
数据目录:/data/postgresql/16/main
日志目录:/data/postgresql/log
备份目录:/data/postgresql/backup
运行目录:/run/postgresql
端口:5432
监听:0.0.0.0
允许访问:0.0.0.0/0
shared_buffers:1904MB
effective_cache_size:5714MB
work_mem:4MB
凭据文件:/data/postgresql/backup/.postgresql.auth

常用命令:
  systemctl status postgresql@16-main.service
  systemctl restart postgresql@16-main.service
  journalctl -u postgresql@16-main.service -f
  psql -h 127.0.0.1 -p 5432 -U postgres -d postgres
  pg_lsclusters

image-20260515105921705

安装完成。

如果无法登陆,检查以下内容:

检查项 命令
服务是否运行 pg_lsclusters
端口是否监听 `ss -lnpt
访问网段是否放行 cat /data/postgresql/backup/.postgresql.authcat /etc/postgresql/16/main/pg_hba.conf
防火墙是否放行 ufw statusnft list ruleset
云安全组是否放行 检查云厂商安全组 5432 端口
密码是否正确 使用 reset-password 重置密码

3、Docker 安装

Docker 安装 PostgreSQL 比较简单,核心就是:

  • 指定镜像版本;
  • 设置初始化用户、密码、数据库;
  • 挂载数据目录,保证容器删除后数据不丢;
  • 映射 5432 端口;

启动后测试连接。

这里以 PostgreSQL 16 为例。


3.1 准备数据目录

BASH
mkdir -p /data/postgresql/docker/data
mkdir -p /data/postgresql/docker/backup

chmod 700 /data/postgresql/docker/data

目录说明:

目录 作用
/data/postgresql/docker/data PostgreSQL 数据目录
/data/postgresql/docker/backup 后续备份目录

3.2 Docker 直接启动

使用 docker run 启动 PostgreSQL:

BASH
docker run -d \
  --name postgres16 \
  --restart always \
  -p 5432:5432 \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD='你的强密码' \
  -e POSTGRES_DB=postgres \
  -e TZ=Asia/Shanghai \
  -v /data/postgresql/docker/data:/var/lib/postgresql/data \
  postgres:16

参数说明:

参数 作用
--name postgres16 容器名称
--restart always 容器异常退出或服务器重启后自动拉起
-p 5432:5432 将宿主机 5432 映射到容器 5432
POSTGRES_USER 初始化数据库用户
POSTGRES_PASSWORD 初始化数据库用户密码
POSTGRES_DB 初始化默认数据库
TZ=Asia/Shanghai 设置容器时区
-v /data/postgresql/docker/data:/var/lib/postgresql/data 持久化数据库数据
postgres:16 使用 PostgreSQL 16 镜像

查看容器状态:

BASH
docker ps | grep postgres16

查看日志:

BASH
docker logs -f postgres16

进入容器:

BASH
docker exec -it postgres16 bash

进入数据库:

BASH
docker exec -it postgres16 psql -U postgres -d postgres

3.3 Docker Compose 启动

如果后续需要长期维护,推荐使用 docker-compose.yml 管理。

创建目录:

BASH
mkdir -p /data/postgresql/docker-compose
cd /data/postgresql/docker-compose

编写 docker-compose.yml

YAML
version: "3.8"

services:
  postgres:
    image: postgres:16
    container_name: postgres16
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: "你的强密码"
      POSTGRES_DB: postgres
      TZ: Asia/Shanghai
    volumes:
      - /data/postgresql/docker/data:/var/lib/postgresql/data
      - /data/postgresql/docker/backup:/backup
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

启动:

BASH
docker compose up -d

如果服务器使用的是老版本 docker-compose,则执行:

BASH
docker-compose up -d

查看服务:

BASH
docker compose ps

查看日志:

BASH
docker compose logs -f postgres

停止服务:

BASH
docker compose down

重启服务:

BASH
docker compose restart postgres

3.4 登录验证

本机连接测试:

BASH
psql -h 127.0.0.1 -p 5432 -U postgres -d postgres

如果宿主机没有安装 psql 客户端,也可以直接进入容器测试:

BASH
docker exec -it postgres16 psql -U postgres -d postgres

进入数据库后执行:

SQL
\l
\du
SELECT version();
\q

3.5 创建业务库和业务用户

进入数据库:

BASH
docker exec -it postgres16 psql -U postgres -d postgres

创建业务用户和数据库:

SQL
CREATE USER appuser WITH PASSWORD '业务用户强密码';

CREATE DATABASE appdb OWNER appuser;

GRANT ALL PRIVILEGES ON DATABASE appdb TO appuser;

\c appdb

GRANT USAGE, CREATE ON SCHEMA public TO appuser;

ALTER SCHEMA public OWNER TO appuser;

\q

业务连接方式:

BASH
psql -h PostgreSQL服务器IP -p 5432 -U appuser -d appdb

3.6 常用 Docker 管理命令

BASH
# 查看容器
docker ps | grep postgres16

# 查看日志
docker logs -f postgres16

# 重启容器
docker restart postgres16

# 停止容器
docker stop postgres16

# 启动容器
docker start postgres16

# 进入容器
docker exec -it postgres16 bash

# 进入数据库
docker exec -it postgres16 psql -U postgres -d postgres

3.7 数据备份和恢复

备份数据库:

BASH
docker exec -t postgres16 pg_dump -U postgres -d appdb > /data/postgresql/docker/backup/appdb.sql

恢复数据库:

BASH
cat /data/postgresql/docker/backup/appdb.sql | docker exec -i postgres16 psql -U postgres -d appdb

备份为自定义格式:

BASH
docker exec -t postgres16 pg_dump -U postgres -d appdb -F c > /data/postgresql/docker/backup/appdb.dump

恢复自定义格式:

BASH
cat /data/postgresql/docker/backup/appdb.dump | docker exec -i postgres16 pg_restore -U postgres -d appdb

3.8 注意事项

  1. 不建议使用 postgres:latest,生产环境应固定版本,例如 postgres:16
  2. 一定要挂载数据目录,否则容器删除后数据会丢失。
  3. 不建议将 5432 直接暴露到公网。
  4. 生产环境建议使用独立业务用户,不要让业务直接使用 postgres 超级用户。
  5. POSTGRES_PASSWORD 只在第一次初始化数据目录时生效,如果数据目录已经存在,修改环境变量不会自动修改数据库密码。
  6. 如果需要修改密码,应进入数据库执行:
SQL
ALTER USER postgres WITH PASSWORD '新密码';

3.9 Docker 安装方式总结

安装方式 适用场景 特点
docker run 临时测试、快速验证 命令简单,但后续维护不方便
docker compose 长期运行、标准化部署 配置清晰,便于维护和迁移

如果只是测试 PostgreSQL,使用 docker run 即可。
如果是长期部署,建议使用 docker compose

三、PostgreSQL 常用命令与 SQL 语句

1、数据库运维命令

1.1 服务管理命令

BASH
# 查看 PostgreSQL 总服务状态
systemctl status postgresql

# 查看 PostgreSQL 16/main 实例状态
systemctl status postgresql@16-main.service

# 启动 PostgreSQL 16/main
systemctl start postgresql@16-main.service

# 停止 PostgreSQL 16/main
systemctl stop postgresql@16-main.service

# 重启 PostgreSQL 16/main
systemctl restart postgresql@16-main.service

# 设置开机自启
systemctl enable postgresql

# 查看 PostgreSQL 服务日志
journalctl -u postgresql@16-main.service -f
命令 作用
systemctl status postgresql 查看 PostgreSQL 总管理服务状态
systemctl status postgresql@16-main.service 查看具体数据库实例状态
systemctl start 启动 PostgreSQL
systemctl stop 停止 PostgreSQL
systemctl restart 重启 PostgreSQL
systemctl enable 设置开机自启
journalctl -u 查看 systemd 日志

1.2 集群管理命令

BASH
# 查看当前 PostgreSQL 集群
pg_lsclusters

# 启动指定集群
pg_ctlcluster 16 main start

# 停止指定集群
pg_ctlcluster 16 main stop

# 重启指定集群
pg_ctlcluster 16 main restart

# 重载配置,不中断数据库连接
pg_ctlcluster 16 main reload
命令 作用
pg_lsclusters 查看当前机器上的 PostgreSQL 集群
pg_ctlcluster 16 main start 启动 PostgreSQL 16/main 集群
pg_ctlcluster 16 main stop 停止 PostgreSQL 16/main 集群
pg_ctlcluster 16 main restart 重启 PostgreSQL 16/main 集群
pg_ctlcluster 16 main reload 重载配置文件,常用于修改 pg_hba.conf 后生效

1.3 登陆数据库

BASH
# 切换到 postgres 系统用户
su - postgres

# 登录 PostgreSQL
psql

# 指定主机、端口、用户、数据库登录
psql -h 127.0.0.1 -p 5432 -U postgres -d postgres

# 退出 psql
\q
命令 作用
su - postgres 切换到 PostgreSQL 默认系统用户
psql 进入 PostgreSQL 命令行
-h 指定数据库地址
-p 指定端口
-U 指定数据库用户
-d 指定数据库名称
\q 退出 PostgreSQL 命令行

1.4 psql 常用内部命令

进入psql库后,可以使用以下命令:

BASH
-- 查看数据库列表
\l

-- 查看用户 / 角色
\du

-- 查看当前连接信息
\conninfo

-- 查看当前数据库下的表
\dt

-- 查看表结构
\d 表名

-- 查看所有 schema
\dn

-- 切换数据库
\c 数据库名

-- 查看 psql 帮助
\?

-- 查看 SQL 帮助
\h

-- 退出
\q
命令 作用
\l 查看数据库列表
\du 查看用户和角色
\conninfo 查看当前连接信息
\dt 查看当前数据库中的表
\d 表名 查看指定表结构
\dn 查看 schema
\c 数据库名 切换数据库
\? 查看 psql 命令帮助
\h 查看 SQL 语法帮助
\q 退出 psql

1.5 数据库和用户管理

BASH
-- 创建数据库
CREATE DATABASE testdb;

-- 删除数据库
DROP DATABASE testdb;

-- 创建用户
CREATE USER testuser WITH PASSWORD 'StrongPassword';

-- 修改用户密码
ALTER USER testuser WITH PASSWORD 'NewStrongPassword';

-- 给用户授权数据库
GRANT ALL PRIVILEGES ON DATABASE testdb TO testuser;

-- 删除用户
DROP USER testuser;
SQL 作用
CREATE DATABASE 创建数据库
DROP DATABASE 删除数据库
CREATE USER 创建数据库用户
ALTER USER ... PASSWORD 修改用户密码
GRANT ALL PRIVILEGES 授权用户访问数据库
DROP USER 删除用户

2、数据库增删改查SQL语句

2.1 创建测试表

SQL
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    age INT,
    email VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
字段 说明
id 自增主键
username 用户名,不能为空
age 年龄
email 邮箱
created_at 创建时间,默认当前时间

2.2 插入数据

SQL
INSERT INTO users (username, age, email)
VALUES ('zhangsan', 25, 'zhangsan@example.com');

INSERT INTO users (username, age, email)
VALUES 
('lisi', 28, 'lisi@example.com'),
('wangwu', 30, 'wangwu@example.com');
SQL 作用
INSERT INTO 插入数据
VALUES 指定插入的值
多行 VALUES 一次插入多条数据

2.3 查询数据

SQL
-- 查询所有数据
SELECT * FROM users;
 id | username | age |        email         |         created_at         
----+----------+-----+----------------------+----------------------------
  1 | zhangsan |  25 | zhangsan@example.com | 2026-05-15 11:10:37.106878
  2 | lisi     |  28 | lisi@example.com     | 2026-05-15 11:10:37.108108
  3 | wangwu   |  30 | wangwu@example.com   | 2026-05-15 11:10:37.108108
(3 rows)

-- 查询指定字段
SELECT id, username, email FROM users;
 id | username |        email         
----+----------+----------------------
  1 | zhangsan | zhangsan@example.com
  2 | lisi     | lisi@example.com
  3 | wangwu   | wangwu@example.com
(3 rows)

-- 条件查询
SELECT * FROM users WHERE age > 25;
 id | username | age |       email        |         created_at         
----+----------+-----+--------------------+----------------------------
  2 | lisi     |  28 | lisi@example.com   | 2026-05-15 11:10:37.108108
  3 | wangwu   |  30 | wangwu@example.com | 2026-05-15 11:10:37.108108
(2 rows)

-- 模糊查询
SELECT * FROM users WHERE username LIKE '%zhang%';
 id | username | age |        email         |         created_at         
----+----------+-----+----------------------+----------------------------
  1 | zhangsan |  25 | zhangsan@example.com | 2026-05-15 11:10:37.106878
(1 row)

-- 排序查询
SELECT * FROM users ORDER BY id DESC;
 id | username | age |        email         |         created_at         
----+----------+-----+----------------------+----------------------------
  3 | wangwu   |  30 | wangwu@example.com   | 2026-05-15 11:10:37.108108
  2 | lisi     |  28 | lisi@example.com     | 2026-05-15 11:10:37.108108
  1 | zhangsan |  25 | zhangsan@example.com | 2026-05-15 11:10:37.106878
(3 rows)

-- 限制返回条数
SELECT * FROM users LIMIT 10;

-- 分页查询
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;
SQL 作用
SELECT * 查询所有字段
WHERE 条件过滤
LIKE 模糊匹配
ORDER BY 排序
LIMIT 限制返回条数
OFFSET 跳过指定条数,常用于分页

2.4 修改数据

SQL
-- 修改单个字段
UPDATE users
SET age = 26
WHERE username = 'zhangsan';

-- 验证 25->26
 SELECT * FROM users WHERE username LIKE '%zhang%'; 
 id | username | age |        email         |         created_at         
----+----------+-----+----------------------+----------------------------
  1 | zhangsan |  26 | zhangsan@example.com | 2026-05-15 11:10:37.106878
(1 row)


-- 修改多个字段
UPDATE users
SET age = 29,
    email = 'new_lisi@example.com'
WHERE username = 'lisi';

-- 验证 多条数据改变
SELECT * FROM users WHERE username LIKE '%lisi%';                      
 id | username | age |        email         |         created_at         
----+----------+-----+----------------------+----------------------------
  2 | lisi     |  29 | new_lisi@example.com | 2026-05-15 11:10:37.108108
(1 row)
SQL 作用
UPDATE 修改数据
SET 设置新的字段值
WHERE 指定修改条件

执行 UPDATE 时一定要带 WHERE 条件,否则会更新整张表。

2.5 删除数据

SQL
-- 删除指定用户
DELETE FROM users
WHERE username = 'wangwu';

-- 验证
SELECT * FROM users;                               
 id | username | age |        email         |         created_at         
----+----------+-----+----------------------+----------------------------
  1 | zhangsan |  26 | zhangsan@example.com | 2026-05-15 11:10:37.106878
  2 | lisi     |  29 | new_lisi@example.com | 2026-05-15 11:10:37.108108
(2 rows)

-- 删除年龄小于 18 的用户
DELETE FROM users
WHERE age < 18;
SQL 作用
DELETE FROM 删除数据
WHERE 指定删除条件

注意:
执行 DELETE 时一定要确认 WHERE 条件,否则会删除整张表数据。

2.6 修改表结构

SQL
-- 增加字段
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- 验证
\d users 
                                        Table "public.users"
   Column   |            Type             | Collation | Nullable |              Default              
------------+-----------------------------+-----------+----------+-----------------------------------
 id         | integer                     |           | not null | nextval('users_id_seq'::regclass)
 username   | character varying(50)       |           | not null | 
 age        | integer                     |           |          | 
 email      | character varying(100)      |           |          | 
 created_at | timestamp without time zone |           |          | CURRENT_TIMESTAMP
 phone      | character varying(20)       |           |          | 

-- 修改字段类型
ALTER TABLE users ALTER COLUMN phone TYPE VARCHAR(50);

-- 删除字段
ALTER TABLE users DROP COLUMN phone;

-- 重命名字段
ALTER TABLE users RENAME COLUMN username TO name;

-- 重命名表
ALTER TABLE users RENAME TO user_info;
SQL 作用
ALTER TABLE ... ADD COLUMN 增加字段
ALTER TABLE ... ALTER COLUMN 修改字段类型
ALTER TABLE ... DROP COLUMN 删除字段
RENAME COLUMN 重命名字段
RENAME TO 重命名表

2.7 创建和删除索引

SQL
-- 创建普通索引
CREATE INDEX idx_users_username ON users(username);

-- 创建唯一索引
CREATE UNIQUE INDEX idx_users_email ON users(email);

-- 删除索引
DROP INDEX idx_users_username;
SQL 作用
CREATE INDEX 创建普通索引
CREATE UNIQUE INDEX 创建唯一索引
DROP INDEX 删除索引

索引可以提高查询速度,但也会增加写入成本。常见适合建索引的字段包括:

SQL
WHERE 条件字段
JOIN 关联字段
ORDER BY 排序字段
高频查询字段

2.8 事务操作

SQL
-- 开启事务
BEGIN;

-- 执行修改
UPDATE users SET age = 31 WHERE username = 'zhangsan';

-- 提交事务
COMMIT;

如果发现操作有问题,可以回滚:

SQL
BEGIN;

DELETE FROM users WHERE username = 'lisi';

ROLLBACK;
SQL 作用
BEGIN 开启事务
COMMIT 提交事务
ROLLBACK 回滚事务

3、数据库内部状态查询SQL语句

3.1 查看当前数据库版本

SQL
SELECT version();

PostgreSQL 16.14 (Ubuntu 16.14-1.pgdg22.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0, 64-bit
(1 row)

作用:查看当前 PostgreSQL 版本信息。


3.2 查看当前数据库和用户

SQL
SELECT current_database();

SELECT current_user;

SELECT inet_client_addr();

SELECT inet_server_addr();
SQL 作用
current_database() 查看当前数据库
current_user 查看当前登录用户
inet_client_addr() 查看客户端 IP
inet_server_addr() 查看服务端 IP

3.3 查看当前连接

SQL
SELECT
    pid,
    usename,
    datname,
    client_addr,
    state,
    backend_start,
    query_start,
    query
FROM pg_stat_activity
ORDER BY query_start DESC;

字段说明:

字段 说明
pid PostgreSQL 后端进程 ID
usename 数据库用户
datname 数据库名称
client_addr 客户端 IP
state 当前连接状态
backend_start 连接创建时间
query_start SQL 开始执行时间
query 当前 SQL

3.4 查看正在执行的 SQL

SQL
SELECT
    pid,
    usename,
    datname,
    client_addr,
    now() - query_start AS running_time,
    query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY running_time DESC;

作用:查看当前正在执行的 SQL,并按执行时间排序。


3.5 查看空闲连接

SQL
SELECT
    pid,
    usename,
    datname,
    client_addr,
    state,
    now() - state_change AS idle_time
FROM pg_stat_activity
WHERE state = 'idle'
ORDER BY idle_time DESC;

作用:查看空闲连接,排查连接池是否释放连接异常。


3.6 查看连接数统计

SQL
SELECT
    datname,
    COUNT(*) AS connection_count
FROM pg_stat_activity
GROUP BY datname
ORDER BY connection_count DESC;

作用:按数据库统计当前连接数。

查看最大连接数配置:

TEXT
SHOW max_connections;

3.7 查看锁等待

SQL
SELECT
    pid,
    usename,
    datname,
    wait_event_type,
    wait_event,
    state,
    query
FROM pg_stat_activity
WHERE wait_event IS NOT NULL;

作用:查看当前是否存在等待事件,例如锁等待、IO 等待。


3.8 查看阻塞关系

SQL
SELECT
    blocked.pid AS blocked_pid,
    blocked.query AS blocked_query,
    blocking.pid AS blocking_pid,
    blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_locks blocked_locks
    ON blocked.pid = blocked_locks.pid
JOIN pg_locks blocking_locks
    ON blocking_locks.locktype = blocked_locks.locktype
   AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
   AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
   AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
   AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
   AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
   AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
   AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
   AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
   AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
   AND blocking_locks.pid != blocked_locks.pid
JOIN pg_stat_activity blocking
    ON blocking.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

作用:查看谁被阻塞、谁在阻塞别人。


3.9 查看数据库大小

SQL
SELECT
    datname,
    pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
ORDER BY pg_database_size(datname) DESC;

作用:查看每个数据库占用空间。


3.10 查看表大小

SQL
SELECT
    schemaname,
    relname AS table_name,
    pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
    pg_size_pretty(pg_relation_size(relid)) AS table_size,
    pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS index_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;

字段说明:

字段 说明
total_size 表总大小,包括索引
table_size 表数据大小
index_size 索引大小

3.11 查看表行数估算

SQL
SELECT
    schemaname,
    relname AS table_name,
    n_live_tup AS estimated_rows
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;

作用:快速查看业务表的大概数据量。

注意:
这里是估算值,不是精确 COUNT(*)。大表不要频繁执行全表 COUNT(*)


3.12 查看数据库命中率

SQL
SELECT
    datname,
    blks_hit,
    blks_read,
    ROUND(
        blks_hit * 100.0 / NULLIF(blks_hit + blks_read, 0),
        2
    ) AS cache_hit_ratio
FROM pg_stat_database
WHERE blks_hit + blks_read > 0
ORDER BY cache_hit_ratio ASC;

作用:查看数据库缓存命中率。

一般来说,缓存命中率长期偏低,可能说明:

  • 内存不足
  • SQL 扫描数据过多
  • 索引设计不合理
  • 业务访问数据范围过大

3.13 查看事务提交和回滚情况

SQL
SELECT
    datname,
    xact_commit,
    xact_rollback,
    ROUND(
        xact_rollback * 100.0 / NULLIF(xact_commit + xact_rollback, 0),
        2
    ) AS rollback_ratio
FROM pg_stat_database
ORDER BY rollback_ratio DESC;

作用:查看事务提交和回滚比例。

如果回滚比例过高,可能说明:

  • 业务异常较多
  • SQL 执行失败较多
  • 事务控制不合理
  • 应用连接数据库存在错误

3.14 查看慢 SQL 配置

SQL
SHOW log_min_duration_statement;

作用:查看当前慢 SQL 记录阈值。

例如:

TEXT
1000

表示记录执行时间超过 1000ms 的 SQL。


3.15 查看常用配置参数

SQL
SHOW listen_addresses;
SHOW port;
SHOW max_connections;
SHOW shared_buffers;
SHOW work_mem;
SHOW timezone;
SHOW log_timezone;

作用:查看当前 PostgreSQL 主要配置是否生效。


3.16 取消正在执行的 SQL

SQL
SELECT pg_cancel_backend(pid);

示例:

SQL
SELECT pg_cancel_backend(12345);

作用:取消指定 PID 正在执行的 SQL。

如果取消无效,可以终止连接:

SQL
SELECT pg_terminate_backend(12345);

区别:

命令 作用
pg_cancel_backend(pid) 只取消当前 SQL,连接还在
pg_terminate_backend(pid) 直接断开该数据库连接

PostgreSQL 运维调优

一、监控部署

PostgreSQL 监控一般通过 postgres_exporter 采集数据库内部指标,再由 Prometheus 抓取,最后在 Nightingale / Grafana 中展示和配置告警。

本次设计不使用Prometheus作为存储,选择使用:

BASH
PostgreSQL

postgres_exporter

 Categraf

victoriametrics

Nightingale

1、创建 PostgreSQL 监控用户

监控用户不建议使用 postgres 超级用户,应该单独创建一个只读监控用户。

进入 PostgreSQL:

BASH
su - postgres
psql

执行 SQL:

SQL
CREATE USER postgres_exporter WITH PASSWORD '<监控用户密码>';

GRANT pg_monitor TO postgres_exporter;

GRANT CONNECT ON DATABASE postgres TO postgres_exporter;

退出:

TEXT
\q

参数说明:

SQL 作用
CREATE USER postgres_exporter 创建 PostgreSQL 监控用户
WITH PASSWORD 设置监控用户密码
GRANT pg_monitor 授予 PostgreSQL 内置监控权限
GRANT CONNECT ON DATABASE postgres 允许监控用户连接 postgres 数据库

其中 pg_monitor 是 PostgreSQL 内置监控角色,适合 exporter 采集数据库状态、连接数、事务、锁、表统计等信息。

2、验证监控用户连接

在 PostgreSQL 服务器本机执行:

BASH
psql -h 127.0.0.1 -p 5432 -U postgres_exporter -d postgres

输入密码后,如果可以正常进入,说明监控用户创建成功。

进入后可以执行:

SQL
SELECT current_user;
SELECT current_database();
\q

正常结果应显示当前用户为:

BASH
postgres_exporter

3、创建 postgres_exporter 配置文件

创建环境变量配置文件:

BASH
cat > /etc/default/postgres_exporter <<'EOF'
DATA_SOURCE_URI=127.0.0.1:5432/postgres?sslmode=disable
DATA_SOURCE_USER=postgres_exporter
DATA_SOURCE_PASS=<监控用户密码>
EOF

修改权限:

BASH
chmod 600 /etc/default/postgres_exporter

配置说明:

配置项 说明
DATA_SOURCE_URI PostgreSQL 连接地址
127.0.0.1:5432 连接本机 PostgreSQL 5432 端口
/postgres 连接的数据库名称
sslmode=disable 本机连接不启用 SSL
DATA_SOURCE_USER exporter 使用的数据库用户
DATA_SOURCE_PASS exporter 使用的数据库密码

注意:
文档中不要直接写真实密码,建议使用 <监控用户密码> 占位。


4、创建 postgres_exporter systemd 服务

假设 postgres_exporter 程序目录为:

BASH
/data/postgres_exporter

二进制文件路径为:

BASH
/data/postgres_exporter/postgres_exporter

创建 systemd 服务文件:

BASH
cat > /etc/systemd/system/postgres_exporter.service <<'EOF'
[Unit]
Description=PostgreSQL Exporter
After=network.target postgresql.service

[Service]
Type=simple
WorkingDirectory=/data/postgres_exporter
EnvironmentFile=/etc/default/postgres_exporter
ExecStart=/data/postgres_exporter/postgres_exporter --web.listen-address=:9187
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

配置说明:

配置项 作用
Description 服务描述
After=network.target postgresql.service 网络和 PostgreSQL 服务启动后再启动 exporter
WorkingDirectory exporter 工作目录
EnvironmentFile 引用 exporter 数据库连接配置
ExecStart exporter 启动命令
--web.listen-address=:9187 exporter 监听 9187 端口
Restart=always 异常退出后自动重启
RestartSec=5 退出 5 秒后重启
WantedBy=multi-user.target 设置为系统多用户模式下启动

5、启动 postgres_exporter

重新加载 systemd:

BASH
systemctl daemon-reload

设置开机自启并立即启动:

BASH
systemctl enable --now postgres_exporter

查看服务状态:

BASH
systemctl status postgres_exporter

查看端口监听:

BASH
ss -lnpt | grep 9187

正常情况下可以看到:

BASH
LISTEN 0 4096 0.0.0.0:9187

6、验证 exporter 指标

本机访问 exporter 指标接口:

BASH
curl http://127.0.0.1:9187/metrics

如果正常,会看到类似指标:

BASH
pg_up 1
pg_database_size_bytes
pg_stat_database_xact_commit
pg_stat_database_xact_rollback
pg_stat_activity_count

7、接入Categraf

编辑 Categraf 的 Prometheus input 配置文件。

常见路径为:

BASH
vim /etc/categraf/conf/input.prometheus/prometheus.toml

添加 PostgreSQL exporter 采集配置:

BASH
# psql
[[instances]]
urls = ["http://127.0.0.1:9187/metrics"]
url_label_key = "instance"
url_label_value = "{{.Host}}"
interval_times = 2
labels = { job = "psql_exporter", service = "psql", cluster = "psql-prod", env = "prod" }

配置说明:

配置项 说明
urls postgres_exporter 指标地址
url_label_key 采集后附加的实例标签名称
url_label_value 实例标签值,{{.Host}} 表示当前主机
interval_times 采集间隔倍数,基于 Categraf 全局采集周期
job 任务名称,方便在 PromQL 中筛选
service 服务类型,这里标记为 psql
cluster PostgreSQL 集群名称,自定义即可
env 环境标签,例如 prodtestdev

如果 Categraf 和 postgres_exporter 不在同一台机器,需要把:

BASH
urls = ["http://127.0.0.1:9187/metrics"]

改成 PostgreSQL 服务器 IP:

BASH
urls = ["http://PostgreSQL服务器IP:9187/metrics"]

8、配置 Categraf 写入 VictoriaMetrics

如果 Categraf 需要直接写入本机 VictoriaMetrics,可以在 Categraf 主配置文件中配置 writers

常见配置文件路径:

BASH
vim /etc/categraf/conf/config.toml

添加或确认如下配置:

BASH
# victoria-metrics
[[writers]]
url = "http://127.0.0.1:8428/api/v1/write"

配置说明:

配置项 说明
[[writers]] Categraf 指标写入目标
url VictoriaMetrics remote write 地址
127.0.0.1:8428 本机 VictoriaMetrics 地址
/api/v1/write VictoriaMetrics remote write 接口

如果 VictoriaMetrics 不在本机,需要改成实际地址:

BASH
[[writers]]
url = "http://VictoriaMetrics服务器IP:8428/api/v1/write"

9、配置 Categraf 心跳上报 Nightingale

如果 Categraf 需要向 Nightingale 上报主机心跳,需要配置 heartbeat

BASH
[heartbeat]
enable = true

# report os version cpu.util mem.util metadata
url = "http://127.0.0.1:17000/v1/n9e/heartbeat"

配置说明:

配置项 说明
enable = true 开启 Categraf 心跳上报
url Nightingale heartbeat 地址
127.0.0.1:17000 Nightingale server 地址
/v1/n9e/heartbeat 夜莺心跳接口

如果 Nightingale 不在本机,需要修改为实际地址:

BASH
[heartbeat]
enable = true
url = "http://Nightingale服务器IP:17000/v1/n9e/heartbeat"

重启 Categraf,修改完成后,重启 Categraf:

BASH
systemctl restart categraf

查看服务状态:

BASH
systemctl status categraf

查看 Categraf 日志:

BASH
journalctl -u categraf -n 100 --no-pager

如果需要实时查看:

BASH
journalctl -u categraf -f

监控大盘:(自定义夜莺仪表盘json)

image-20260515113543003

二、主从分离

评论