KingbaseES 表空间目录自动创建:一个小特性背后的存储管理全景

4 阅读7分钟

写在前面

做数据库运维久了,你会发现一个规律——真正影响日常工作效率的,往往不是那些听起来高大上的新功能,而是一些看似不起眼的"小优化"。比如今天要聊的这个:KingbaseES 的表空间目录自动创建特性。

它解决的问题特别朴素——以前创建表空间必须先手动 mkdir,现在直接执行 SQL 就行。听起来平平无奇,对吧?但如果你顺着这个特性往下挖,会发现它牵扯出的是 KingbaseES 存储管理的一整套体系:表空间架构、目录权限、磁盘规划、GUC 参数机制……一个小功能,串起来的是一片技术纵深。

这篇文章就想带你从这个切入点出发,把相关的运维要点和调优思路都梳理一遍。


一、老问题:为什么创建表空间这么折腾

在早期版本里,创建一个自定义表空间是"两头跑"的活儿:

  1. 先登录服务器,用操作系统命令一层层把目录建好
  2. 确认权限和属主都没问题
  3. 再回到数据库里执行 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_default1663默认位置,建表不指定就存这里
pg_global1664集群级系统目录表

真正值钱的用法: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信号触发 reloadpg_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,别手动编辑配置文件
  • 维护层面:表空间迁移先备份再停库,别在线操作
  • 监控层面:空间问题不会突然爆发,只会慢慢积累到你没注意的时候——把巡检做成常态

把这些落到位,数据库的存储管理就算上了轨道。一个小特性,背后是整套运维功底的体现。