写在前面
做数据库运维久了,你会发现一个规律——真正影响日常工作效率的,往往不是那些听起来高大上的新功能,而是一些看似不起眼的"小优化"。比如今天要聊的这个:KingbaseES 的表空间目录自动创建特性。
它解决的问题特别朴素——以前创建表空间必须先手动 mkdir,现在直接执行 SQL 就行。听起来平平无奇,对吧?但如果你顺着这个特性往下挖,会发现它牵扯出的是 KingbaseES 存储管理的一整套体系:表空间架构、目录权限、磁盘规划、GUC 参数机制……一个小功能,串起来的是一片技术纵深。
这篇文章就想带你从这个切入点出发,把相关的运维要点和调优思路都梳理一遍。
一、老问题:为什么创建表空间这么折腾
在早期版本里,创建一个自定义表空间是"两头跑"的活儿:
- 先登录服务器,用操作系统命令一层层把目录建好
- 确认权限和属主都没问题
- 再回到数据库里执行
CREATE TABLESPACE
只要中间有一步出错——目录少建一层、属主给成了 root、权限设成了 755——创建语句就直接报错,前面的功夫全白费。
这个问题的根源其实挺有意思:数据库进程本身没有在文件系统里随意创建目录的权限。这是安全设计的必然结果,但也造成了运维体验的割裂。尤其在以下几个场景里特别难受:
- 批量初始化环境:脚本里要先判断目录是否存在,不存在就
mkdir -p,再执行 SQL,代码又臃肿又容易遗漏 - 自动化部署:DevOps 流水线要在 OS 层和数据库层反复切换操作,增加了编排复杂度
- 测试环境快速搭建:临时建一个表空间,光准备目录就得花好几分钟
- 多层嵌套路径:手敲
/data/ts/business/module1/submodule/sp1这种路径,拼写错误几乎是必然
KingbaseES 的解决思路很直接:既然目录不存在是报错的根源,那就让数据库自己把它建出来。
二、auto_createtblspcdir:这个开关怎么用
参数基本情况
这个特性由一个 GUC 参数 auto_createtblspcdir 控制,先看它的基本属性:
| 属性 | 值 |
|---|---|
| 参数名 | auto_createtblspcdir |
| 类型 | boolean |
| 默认值 | on |
| 生效方式 | reload 即可,无需重启 |
| 影响范围 | 全局 |
默认就是开启的,也就是说你装好数据库,不做任何配置,这个特性就已经在工作了。
开和关的行为差异
开启时(on,默认):
- 目录不存在 → 数据库递归创建整条路径
- 路径中已存在的父目录 → 属主必须是数据库 OS 用户
- 新建目录的属主 → 自动设为数据库 OS 用户
关闭时(off):
- 目录不存在 → 直接报错
- 目录必须已经存在、必须为空、属主必须正确
- 相当于回退到传统模式
大多数场景下,建议保持默认开启。只有在你有非常严格的目录治理规范——比如所有存储路径必须由专门的运维流程创建——才考虑关掉它。
配置方式
推荐用 ALTER SYSTEM 来修改,它会把变更写入 kingbase.auto.conf,方便追溯:
-- 查看当前值
SHOW auto_createtblspcdir;
-- 修改
ALTER SYSTEM SET auto_createtblspcdir = off;
-- 让修改生效(不需要重启)
SELECT pg_reload_conf();
-- 确认
SHOW auto_createtblspcdir;
想看参数的完整元信息,包括它当前值是从哪里来的:
SELECT name, setting, source, context, short_desc
FROM pg_settings
WHERE name = 'auto_createtblspcdir';
三、安全边界:自动不等于任意
自动创建虽然方便,但 KingbaseES 对表空间设置了一套雷打不动的约束,无论参数开关,这些规则都必须遵守。
规则 1:必须是绝对路径
-- 正确
CREATE TABLESPACE mysp1 LOCATION '/data/tablespaces/mysp1';
-- 报错
CREATE TABLESPACE mysp1 LOCATION './mysp1';
-- ERROR: tablespace location must be an absolute path
相对路径对数据库没有意义——它不知道"相对于谁"。
规则 2:路径不能在 data 目录内
-- 假设 $KINGBASE_DATA = /opt/Kingbase/ES/V8/data
CREATE TABLESPACE mysp1 LOCATION '/opt/Kingbase/ES/V8/data/mysp1';
-- ERROR: tablespace location must not be inside the data directory
这条限制是为了避免表空间文件和系统数据文件混在一起,否则备份、迁移、权限管理都会变得混乱不堪。
规则 3:一个路径只能绑一个表空间
CREATE TABLESPACE sp1 LOCATION '/data/ts/sp1';
-- 再来一个会失败
CREATE TABLESPACE sp2 LOCATION '/data/ts/sp1';
-- ERROR: directory is already in use as a tablespace
防止的是命名冲突和资源争用。
规则 4:只有超级用户能创建
\c - normal_user
CREATE TABLESPACE mysp2 LOCATION '/data/tablespaces/mysp2';
-- ERROR: must be superuser to create a tablespace
如果希望普通用户能在表空间里建表,但不能管理表空间本身,可以这样授权:
CREATE ROLE ops_admin LOGIN;
GRANT CREATE ON TABLESPACE ts_data TO ops_admin;
规则 5:属主必须一致(最容易踩的坑)
不管是自动创建还是手动创建,路径中已经存在的那些父目录,属主必须是启动数据库的 OS 用户。
# 检查属主
ls -la /data/tablespaces/
# drwx------ 2 kingbase kingbase 4096 ...
# 如果属主不对,用这个修正
sudo chown -R kingbase:kingbase /data/tablespaces/
sudo chmod 700 /data/tablespaces/
最典型的翻车现场:运维用 sudo mkdir 建了目录,忘了 chown,然后创建表空间死活失败。这也是为什么建议让 auto_createtblspcdir 保持开启——让数据库自己建,就没有属主不一致的问题。
四、四个场景看自动创建怎么工作
场景 1:目录已完整存在
-- /data/ts/test/test1/test2/mysp1 已经提前建好
CREATE TABLESPACE mysp1
LOCATION '/data/ts/test/test1/test2/mysp1';
和传统行为一致,直接使用现有目录。
场景 2:目录只存在一部分
-- 只有 /data/ts/test/test1 存在,后面几层没建
CREATE TABLESPACE mysp1
LOCATION '/data/ts/test/test1/test2/test3/mysp1';
-- 数据库会自动把 test2/test3/mysp1 补齐
场景 3:整条路径都不存在
-- 从头到尾没有一层目录存在
CREATE TABLESPACE mysp1
LOCATION '/data/ts/test1/test2/test3/test4/test5/test6/test7/mysp1';
-- 一条语句全部递归创建
在老版本里,这种深层嵌套路径得先写 mkdir -p,现在一步到位。
场景 4:大小写混合路径 + 实际读写
-- 目录名带大写字母也没问题
CREATE TABLESPACE mysp1 LOCATION '/data/ts/test/test1/test2/TEst3';
-- 建表用用看
CREATE TABLE cc (id INT, name VARCHAR(50)) TABLESPACE mysp1;
INSERT INTO cc VALUES (1, 'xiaozhang'), (2, 'xiaozhao'), (3, 'xiaohong');
SELECT * FROM cc;
自动创建出来的目录和手工建的没有任何区别,后续所有操作完全正常。
五、表空间不只是"换个地方存数据"
很多 DBA 对表空间的理解停留在"给表找个存放位置",但它真正的价值在于——它是数据库存储架构里最值得利用的调优手段之一。
底层映射关系
KingbaseES 内部,表空间通过 OID 和符号链接关联物理目录。$KINGBASE_DATA/pg_tblspc/ 下存放的都是指向实际路径的软链接:
ls -la $KINGBASE_DATA/pg_tblspc/
# 16384 -> /data/tablespaces/mysp1
# 16385 -> /data/tablespaces/mysp2
整体层次结构:
数据库集群(Cluster)
└── 数据库(Database)
├── 模式(Schema) ← 逻辑层,管命名空间
│ ├── 表(Table)
│ └── 索引(Index)
└── 表空间(Tablespace) ← 物理层,管存储位置
└── 数据文件(按 OID 命名)
系统初始化时会自带两个表空间:
| 表空间 | OID | 用途 |
|---|---|---|
| pg_default | 1663 | 默认位置,建表不指定就存这里 |
| pg_global | 1664 | 集群级系统目录表 |
真正值钱的用法:I/O 分离
表空间最核心的调优价值,是让你可以把不同类型的数据分散到不同的物理磁盘上。
策略 1:数据表和索引分开
索引的访问模式和数据表不一样——索引随机读多、热点集中,单独放到更快的存储上,效果立竿见影:
CREATE TABLESPACE ts_data LOCATION '/ssd1/tablespaces/data';
CREATE TABLESPACE ts_index LOCATION '/ssd2/tablespaces/index';
CREATE TABLE orders (
order_id BIGSERIAL,
customer_id INT,
order_date DATE,
amount NUMERIC(12, 2)
) TABLESPACE ts_data;
CREATE INDEX idx_orders_customer
ON orders (customer_id) TABLESPACE ts_index;
CREATE INDEX idx_orders_date
ON orders (order_date) TABLESPACE ts_index;
策略 2:冷热数据分层
在线业务留在 SSD 保响应速度,历史归档扔到 HDD 省成本:
CREATE TABLESPACE ts_hot LOCATION '/ssd/tablespaces/hot';
CREATE TABLESPACE ts_cold LOCATION '/hdd/tablespaces/cold';
-- 热表
CREATE TABLE real_time_metrics (
metric_id BIGSERIAL,
metric_name VARCHAR(100),
value DOUBLE PRECISION,
captured_at TIMESTAMP DEFAULT now()
) TABLESPACE ts_hot;
-- 归档表
CREATE TABLE metrics_archive (
LIKE real_time_metrics INCLUDING DEFAULTS
) TABLESPACE ts_cold;
定期归档脚本:
BEGIN;
INSERT INTO metrics_archive
SELECT * FROM real_time_metrics
WHERE captured_at < now() - INTERVAL '30 days';
DELETE FROM real_time_metrics
WHERE captured_at < now() - INTERVAL '30 days';
COMMIT;
策略 3:临时操作独占一块盘
排序、哈希聚合会生成大量临时文件,不单独处理就会和业务数据抢 I/O:
CREATE TABLESPACE ts_temp LOCATION '/nvme/tablespaces/temp';
-- 会话级指定
SET temp_tablespaces = 'ts_temp';
-- 或者数据库级默认
ALTER DATABASE mydb SET temp_tablespaces = 'ts_temp';
查询表和表空间的对应关系
日常运维常用:
-- 看每个表/索引存在哪
SELECT
c.relname AS object_name,
c.relkind AS object_type,
COALESCE(t.spcname, 'pg_default') AS tablespace
FROM pg_class c
LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE c.relkind IN ('r', 'i')
ORDER BY t.spcname, c.relname;
-- 各表空间占多大
SELECT
spcname,
pg_size_pretty(pg_tablespace_size(spcname)) AS size
FROM pg_tablespace
ORDER BY pg_tablespace_size(spcname) DESC;
六、存储空间运维:规划好了省心,规划不好救火
磁盘布局建议
一个合理的目录结构长这样:
/data ← 独立挂载点
├── kingbase/ ← 软件安装目录
├── db_data/ ← data 目录(独占一块盘)
│ ├── base/
│ ├── pg_wal/ ← WAL 日志,建议放最快的盘
│ └── pg_tblspc/ ← 表空间符号链接
├── ts_oltp/ ← OLTP(SSD)
├── ts_olap/ ← OLAP(HDD 也行)
├── ts_index/ ← 索引(最快的盘)
├── ts_temp/ ← 临时
└── backup/ ← 备份(独立存储)
几个核心原则:
- data 目录独占一块盘,不和业务表空间混放
- WAL 日志优先上最快的盘,它对延迟最敏感
- 不同业务类型用不同表空间,便于容量管理和迁移
权限管理别嫌啰嗦
权限配置看着简单,但生产环境里权限不当引起的故障往往最难排查。原则就几条:
sudo mkdir -p /data/tablespaces
sudo chown kingbase:kingbase /data/tablespaces
sudo chmod 700 /data/tablespaces
| 原则 | 原因 |
|---|---|
| 权限 700 | 只允许数据库 OS 用户访问 |
| 属主一致 | 属主不对直接报错 |
| 目录不混用 | 表空间目录里不放其他应用文件 |
| 定期检查 | 系统升级、人员变动可能改动权限 |
批量检查脚本:
#!/bin/bash
DB_USER="kingbase"
TS_BASE="/data/tablespaces"
for dir in ${TS_BASE}/*/; do
owner=$(stat -c '%U:%G' "$dir")
perm=$(stat -c '%a' "$dir")
[ "$owner" != "${DB_USER}:${DB_USER}" ] && echo "[WARN] 属主异常: $dir -> $owner"
[ "$perm" != "700" ] && echo "[WARN] 权限异常: $dir -> $perm"
done
日常巡检
把这几条 SQL 加到每周巡检清单里:
-- 各表空间大小
SELECT spcname, pg_size_pretty(pg_tablespace_size(spcname))
FROM pg_tablespace
ORDER BY pg_tablespace_size(spcname) DESC;
-- 找出占用大头的表
SELECT
schemaname || '.' || tablename AS full_name,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS size
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
LIMIT 20;
表空间迁移
当某块盘快满了,需要把表空间挪到新盘上:
# 1. 停库
sys_ctl stop -D $KINGBASE_DATA
# 2. 搬目录
mv /old_disk/ts_data /new_disk/ts_data
# 3. 确认新目录属主权限
chown -R kingbase:kingbase /new_disk/ts_data
chmod 700 /new_disk/ts_data
# 4. 更新符号链接(OID 从 pg_tblspc 下看)
rm $KINGBASE_DATA/pg_tblspc/16384
ln -s /new_disk/ts_data $KINGBASE_DATA/pg_tblspc/16384
# 5. 启库
sys_ctl start -D $KINGBASE_DATA
删除前先清空
-- 看里面还有什么
SELECT relkind, count(*)
FROM pg_class
WHERE reltablespace = (SELECT oid FROM pg_tablespace WHERE spcname = 'mysp1')
GROUP BY relkind;
-- 一键迁移所有对象到 pg_default
DO $$
DECLARE
tbl RECORD;
ts_name TEXT := 'mysp1';
BEGIN
FOR tbl IN SELECT schemaname, tablename FROM pg_tables WHERE tablespace = ts_name
LOOP
EXECUTE format('ALTER TABLE %I.%I SET TABLESPACE pg_default',
tbl.schemaname, tbl.tablename);
END LOOP;
END $$;
-- 确认清空后再删
DROP TABLESPACE mysp1;
七、GUC 机制:改个参数不只是改个值
auto_createtblspcdir 是一个 GUC 参数,而 GUC(Grand Unified Configuration)是 KingbaseES 里管理所有配置的统一框架。理解 GUC 体系,对日常调优和故障排查都有直接帮助。
配置分层结构
GUC 不是简单的键值对,它有优先级分层:
配置来源(优先级从高到低)
├── ALTER SYSTEM SET → kingbase.auto.conf
├── 启动命令行参数 → -c param=value
├── kingbase.conf → 主配置文件
├── 环境变量
└── 编译时默认值 → 最低
同一参数在多处配置时,取优先级最高的。你随时可以查到当前值的来源:
SELECT name, setting, source, sourcefile, sourceline
FROM pg_settings
WHERE name = 'auto_createtblspcdir';
-- source 含义:
-- default → 编译时默认
-- configuration file → kingbase.conf
-- database → ALTER SYSTEM
-- command line → 启动参数
上下文级别:改完要不要重启
GUC 参数的生效方式取决于它的上下文级别(context):
| 级别 | 含义 | 重启要求 |
|---|---|---|
| internal | 编译时确定 | 改不了 |
| postmaster | 启动时读取 | 要重启 |
| sighup | 信号触发 reload | pg_reload_conf() |
| backend | 连接建立时读 | 新连接生效 |
| superuser | 超级用户可改 | 即时生效 |
| user | 任何用户可改 | 即时生效 |
auto_createtblspcdir 属于 sighup 级别,改完执行一次 pg_reload_conf() 就生效,不用重启。这在生产环境里非常关键——重启意味着停服,而 reload 完全在线。
常见参数的级别对比:
SELECT name, context, vartype, boot_val
FROM pg_settings
WHERE name IN (
'auto_createtblspcdir', -- sighup
'shared_buffers', -- postmaster (要重启)
'work_mem', -- user (即时生效)
'max_connections', -- postmaster (要重启)
'temp_tablespaces', -- user
'wal_level' -- postmaster
)
ORDER BY context;
pg_reload_conf() 背后做了什么
1. SQL 会话调用 pg_reload_conf()
2. 向 postmaster 主进程发 SIGHUP 信号
3. postmaster 重新读 kingbase.conf + kingbase.auto.conf
4. 更新内存中的参数值
5. 向所有子进程转发 SIGHUP
6. 各子进程更新自己的参数副本
整个过程毫秒级完成,正在执行的查询不受任何影响。这就是 sighup 级别参数调优方便的根本原因。
完整的参数管理流程
-- 1. 查看当前状态
SELECT name, setting, source, context
FROM pg_settings WHERE name = 'auto_createtblspcdir';
-- 2. 修改(推荐方式)
ALTER SYSTEM SET auto_createtblspcdir = off;
-- 3. 触发 reload
SELECT pg_reload_conf();
-- 4. 验证生效
SELECT name, setting, source
FROM pg_settings WHERE name = 'auto_createtblspcdir';
-- source 变成 database,说明是 ALTER SYSTEM 设的
-- 5. 恢复默认
ALTER SYSTEM RESET auto_createtblspcdir;
SELECT pg_reload_conf();
八、写在最后
回到最开始的问题:为什么一个"自动建目录"的小功能值得写这么长?
因为表面上它只帮你省了一步 mkdir,但往深了挖,它串起来的是 KingbaseES 存储管理的一整套知识:表空间的安全约束、I/O 分离的调优思路、磁盘规划的运维规范、GUC 参数的动态加载机制。这些知识点放在一起,才构成一套完整的数据库存储管理能力。
几条最实用的建议,收尾记住就好:
- 参数层面:
auto_createtblspcdir保持默认开启,省事且安全 - 规划层面:表空间的核心价值是 I/O 分离——数据和索引分开、冷热分开、临时操作隔离
- 权限层面:目录 700、属主一致、定期巡检,这三条是底线
- 配置层面:改 GUC 参数用
ALTER SYSTEM,别手动编辑配置文件 - 维护层面:表空间迁移先备份再停库,别在线操作
- 监控层面:空间问题不会突然爆发,只会慢慢积累到你没注意的时候——把巡检做成常态
把这些落到位,数据库的存储管理就算上了轨道。一个小特性,背后是整套运维功底的体现。