前言

创建表空间之前还得先去系统里 mkdir,这事老运维都经历过。KingbaseES 后来加了个自动创建目录的功能,一条 SQL 搞定。听起来不复杂,但实际用起来有不少门道——尤其当你把数据库搬到国产操作系统上之后。

老问题:建表空间非要先建目录

做 DBA 的大概都踩过这个坑。你想把某个业务的数据单独放到一块磁盘上,写了条 CREATE TABLESPACE,执行下去直接报错。原因就一个字:路径不存在。

然后你就得去操作系统那边手动 mkdir,完了还得确认属主对不对。要是属主不对,再来一遍 chown。整个过程切碎了看就是来回切换环境——数据库里写 SQL,终端里敲命令,再回数据库写 SQL。

偶尔弄一次也就罢了。赶上批量部署或者搭测试集群的时候,你得额外写 Shell 脚本来协调这两层操作。脚本写得越多,维护成本越高,出岔子的概率也跟着上去。有一回我帮客户排查故障,最后发现就是表空间目录的属主配错了——目录在那放着呢,数据库就是写不进去,日志翻了半天才定位到。

自动创建是怎么回事

后来 KingbaseES 加了表空间目录自动创建的功能。原理不复杂:你执行 CREATE TABLESPACE 的时候,指定的路径不存在的话,数据库引擎自己帮你建,中间缺了几层目录也一并递归创建出来。

控制这个行为的参数叫 auto_createtblspcdir,默认是开的:

SHOW auto_createtblspcdir;

如果你想关掉(后面会聊到什么情况下需要关):

ALTER SYSTEM SET auto_createtblspcdir = off;
SELECT pg_reload_conf();

有规矩,不是随便建

自动创建不等于随便建,引擎在动手之前有几条硬约束。

路径必须是绝对路径。相对路径不认,因为数据库得明确知道数据文件落在磁盘的什么位置。./mysp1 这种写法会直接被拒。

不能把表空间建在数据库自己的 data 目录下面。系统文件和业务数据混一块儿,出了问题连恢复都费劲。这条约束其实挺合理的,数据目录是数据库的"自留地",别往里面塞业务数据。

同一条路径只能挂一个表空间,不能两个表空间共用一个目录,不然数据文件互相覆盖就热闹了。

只有超级用户能建表空间。这属于底层存储的操作,权限卡严点没毛病。

还有一条容易被忽略的:不管目录是自动建的还是手动建的,它的 owner 必须是运行数据库的那个操作系统用户。不是的话数据库写不进去。

# 看一眼属主对不对
ls -ld /data/tablespaces/mysp1
# drwx------ 2 dbuser dbgroup 4096 Jan 15 10:30 /data/tablespaces/mysp1

# 不对的话改一下
chown -R dbuser:dbgroup /data/tablespaces/mysp1

实际跑几个场景

光说不练没用,下面几个测试场景是从文档里扒出来的,一个比一个深入。

目录已经存在了

最简单的 case。提前建好了目录,属主也对:

mkdir -p /data/tablespaces/mysp1
chown dbuser:dbgroup /data/tablespaces/mysp1
CREATE TABLESPACE mysp1
LOCATION '/data/tablespaces/mysp1';

-- 确认一下
SELECT spcname, pg_tablespace_location(oid) AS location
FROM pg_tablespace WHERE spcname = 'mysp1';

-- 清理
DROP TABLESPACE mysp1;

没什么意外,和以前一样。自动创建功能不会对已存在的目录产生任何干扰,完全向后兼容。

父目录在,最后一级不在

稍微复杂点。父目录链有一部分已经存在了,但表空间真正要用的那个子目录没建:

mkdir -p /data/tablespaces
# app_data 这个子目录没建
CREATE TABLESPACE mysp1
LOCATION '/data/tablespaces/app_data';

SELECT spcname, pg_tablespace_location(oid) AS location
FROM pg_tablespace WHERE spcname = 'mysp1';

DROP TABLESPACE mysp1;

数据库自动把最后一级 app_data 建出来了。到这里你可能觉得这也没什么了不起的,接着往下看。

七层目录全不存在

这个就比较狠了。整条路径链从上到下没有一层存在的:

CREATE TABLESPACE mysp1
LOCATION '/data/tablespaces/lv1/lv2/lv3/lv4/lv5/lv6/mysp1';

SELECT spcname, pg_tablespace_location(oid) AS location
FROM pg_tablespace WHERE spcname = 'mysp1';

DROP TABLESPACE mysp1;

跑完你就发现,七层目录一口气全建出来了。效果等同于 mkdir -p

大小写混合路径

文档里有个容易被忽视的测试用例——路径里故意用了大小写混写 TEst3

CREATE TABLESPACE mysp1
LOCATION '/data/tablespaces/lv1/lv2/TEst3';

CREATE TABLE cc (
    id   INT,
    name VARCHAR(50)
) TABLESPACE mysp1;

INSERT INTO cc VALUES
    (1, 'zhangsan'), (2, 'lisi'), (3, 'wangwu');

SELECT * FROM cc;

为什么专门搞个大写字母混在里面?因为 Linux 文件系统区分大小写,TEst3test3 是两个完全不同的目录。数据库引擎得确保三件事:目录严格按 SQL 里写的大小写来创建、后续读写都能找对这个目录、权限设置不会因为路径里有大小写字母而抽风。

这个测试看起来不起眼。但等你从 Windows 迁到国产 Linux 上的时候,大小写问题能让你抓狂——后面专门聊。

onoff 到底差在哪

把这个参数的两种模式放在一起对比一下就清楚了:

-- on(默认)
-- 路径不存在?帮你建,中间层级也一并补上
-- 建完后属主自动设为数据库进程用户
CREATE TABLESPACE mysp1
LOCATION '/data/tablespaces/app/user_data';
-- 即使 app/ 这层也不存在,照样成功

-- off
-- 路径不存在就报错,属主不对也报错
-- 一切靠你自己提前准备好

off 模式看着像是"退步",但等聊完国产安全操作系统的场景你就明白了,有些时候手动反而比自动靠谱。

搬到国产操作系统上会遇到什么

2026 年了,政务、金融、能源这些行业的国产化替代已经铺开了。统信 UOS、银河麒麟、龙蜥 Anolis、欧拉 openEuler,各有各的文件系统和安全机制。数据库跑上去,光能启动还不够,表空间管理这块有不少细节值得提前了解。

Windows 到国产 Linux 的路径迁移

一个特别普遍的情况:开发在 Windows 上做,生产要部署到国产 Linux。路径这个事儿在这里面埋着两个雷。

第一个雷是路径分隔符。Windows 写 D:\data\tablespaces,Linux 写 /data/tablespaces。迁移脚本里要是还留着 Windows 风格的路径,到 Linux 上就是定时炸弹:

#!/bin/bash
# 迁移之前先扫一遍,把带盘符的路径揪出来
echo "扫描 SQL 文件中的 Windows 路径..."
grep -rn "LOCATION\s*'[A-Za-z]:\\\\" /path/to/sql_scripts/ 2>/dev/null

if [ $? -eq 0 ]; then
    echo ""
    echo "上面这些路径需要手动改成 Linux 格式"
else
    echo "没发现 Windows 路径,可以继续"
fi

第二个雷是大小写敏感。NTFS 不区分大小写,TEst3test3 指向同一个位置。但 ext4、xfs 这些 Linux 文件系统是严格区分的,它们就是两个不同的目录。

-- Windows 上,两条语句指向同一路径,第二条直接冲突
CREATE TABLESPACE sp1 LOCATION '/data/TEst3';
CREATE TABLESPACE sp2 LOCATION '/data/test3';  -- 报错

-- 国产 Linux 上,两条指向不同路径,都能成功
CREATE TABLESPACE sp1 LOCATION '/data/TEst3';
CREATE TABLESPACE sp2 LOCATION '/data/test3';  -- 正常

反过来想,如果你的代码里某个地方写了 /data/TEst3,另一个地方写了 /data/test3,在 Windows 上跑得好好的,到了国产 Linux 就找不着文件了。我见过一个项目光排查这个问题就花了两天——日志里没有任何明显的报错线索,最后发现就是大小写没对上。

应对方式没什么花哨的:

#!/bin/bash
# 扫描迁移清单里带大写字母的路径
echo "以下路径含有大写字母,建议统一小写:"
echo ""

while IFS='|' read -r ts_name ts_path; do
    lower_path=$(echo "$ts_path" | tr '[:upper:]' '[:lower:]')
    if [ "$ts_path" != "$lower_path" ]; then
        echo "  [$ts_name] $ts_path  →  建议改成 $lower_path"
    fi
done < migration_paths.txt

路径一律全小写。别搞驼峰,别搞大小写混写,从一开始就避免这个问题。迁移文档里把每个表空间的完整路径写清楚,前后对照着检查一遍。

各家文件系统的差异

国产操作系统底层的文件系统大部分还是 ext4 和 xfs,但各家内核多少有些定制:

系统 常用文件系统 值得注意的
统信 UOS ext4、xfs 标准文件系统,基本没兼容问题
银河麒麟 ext4、xfs 部分版本带了国密加密文件系统
龙蜥 Anolis ext4、xfs ANCK 内核对文件系统做了定制优化
欧拉 openEuler ext4、xfs 针对大容量存储场景有专项优化

ext4 和 xfs 都是用了十几年的东西了,mkdir 系统调用的行为早就标准化了。表空间目录自动创建跑在这些文件系统上没什么兼容性问题。

加密文件系统和表空间结合

等保 2.0 的合规压力摆在那里,数据文件加密基本上是标配了。表空间给了一个很自然的切入点——把表空间目录放在加密文件系统上面。数据库自己完全不用改代码,操作系统内核在 I/O 路径上自动加解密,落盘的数据天然是密文。

拿 ext4 的 fscrypt 举个真实的例子:

# 先让文件系统支持加密
sudo tune2fs -O encrypt /dev/sdb1

# 建一个专门放表空间的目录
sudo mkdir -p /secure_tbs/user_data

# 给这个目录开启加密
sudo fscrypt encrypt /secure_tbs/user_data
# 跟着提示走,设置保护策略

# 属主改成数据库用户
sudo chown -R dbuser:dbgroup /secure_tbs/user_data

然后在数据库那边该怎么用就怎么用,它根本不知道底层是加密的:

CREATE TABLESPACE secure_space
LOCATION '/secure_tbs/user_data';

CREATE TABLE user_private (
    id       INT PRIMARY KEY,
    name     VARCHAR(50),
    id_card  VARCHAR(18),
    phone    VARCHAR(11)
) TABLESPACE secure_space;

INSERT INTO user_private VALUES
    (1, 'zhangsan', '110101199001011234', '13800138000');

SELECT * FROM user_private;

有个好处是,自动创建的子目录会继承父目录的加密策略。你在 /secure_tbs 上设了加密,那 auto_createtblspcdir 在里面自动建的子目录都会带着加密属性。不用额外操作。

安全增强版系统:该关就关

国产安全操作系统(麒麟的安全增强版本是比较典型的)一般会开启强制访问控制。在这种环境下自动创建的目录有可能拿不到正确的安全标签,出了问题还不好查。

碰到这种情况,老老实实手动建目录、手动配安全上下文:

mkdir -p /secure_tbs/finance_data

# 给数据库用户设 ACL
setfacl -R -m u:dbuser:rwx /secure_tbs/finance_data

# 看一下当前的安全上下文
ls -Z /secure_tbs/finance_data

# 按需调整 SELinux 标签
chcon -R -t database_db_t /secure_tbs/finance_data

chown -R dbuser:dbgroup /secure_tbs/finance_data
-- 安全要求高的环境,先关掉自动创建
ALTER SYSTEM SET auto_createtblspcdir = off;
SELECT pg_reload_conf();

-- 手动建完目录再来建表空间
CREATE TABLESPACE finance_data
LOCATION '/secure_tbs/finance_data';

说白了就是一句话:安全策略越严,越别图省事。

容器化场景

国产化云平台上用 K8s 跑数据库的也多了。表空间目录挂的是外部存储卷,自动创建功能在这里倒是挺管用——新挂的 PV 是空的,什么都不用提前准备。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kingbase-cluster
spec:
  replicas: 3
  template:
    spec:
      initContainers:
        - name: init-perms
          image: busybox
          command: ['sh', '-c', 'chown -R 1000:1000 /tablespaces']
          volumeMounts:
            - name: ts-data
              mountPath: /tablespaces/app_data
            - name: ts-index
              mountPath: /tablespaces/app_index
      containers:
        - name: kingbase
          image: kingbase:latest
          volumeMounts:
            - name: data
              mountPath: /kingbase/data
            - name: ts-data
              mountPath: /tablespaces/app_data
            - name: ts-index
              mountPath: /tablespaces/app_index
  volumeClaimTemplates:
    - metadata:
        name: ts-data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 100Gi
    - metadata:
        name: ts-index
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 50Gi

数据库起来之后:

CREATE TABLESPACE app_data LOCATION '/tablespaces/app_data';
CREATE TABLESPACE app_index LOCATION '/tablespaces/app_index';

-- 数据和索引分开存,减少 I/O 争抢
CREATE TABLE orders (
    id         BIGSERIAL,
    order_no   VARCHAR(32),
    amount     DECIMAL(12,2),
    status     SMALLINT DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) TABLESPACE app_data;

CREATE INDEX idx_orders_no ON orders(order_no)
    TABLESPACE app_index;

有两个注意点。一个是刚挂载的 PV 是空的,auto_createtblspcdir = on 在这时候特别管用,省了 initContainer 里额外 mkdir 的逻辑。另一个是不少 CSI 驱动创建的 PV 默认属主是 root,不改的话数据库写不进去。所以上面那个 initContainer 里的 chown 不能省。

参数怎么选,路径怎么起名

不同环境下的参数建议

环境 auto_createtblspcdir 理由
物理机 / 虚拟机 on 省事,没什么副作用
安全增强型国产 OS off 手动建目录才能控制安全标签
K8s on 简化初始化流程
多租户云平台 off 不让用户随便在文件系统上建东西
开发测试 on 省时间

路径命名说两句

不管开不开自动创建,路径起名最好统一规范。全小写、有层次、别搞特殊字符。来看个对比:

-- 推荐的写法
CREATE TABLESPACE ts_order_data
LOCATION '/data/tablespaces/order/data';

CREATE TABLESPACE ts_order_index
LOCATION '/data/tablespaces/order/index';

-- 别这么写,大小写混着来,跨平台迟早出事
CREATE TABLESPACE ts_order
LOCATION '/data/TableSpaces/Order/Data';

-- 也别这么写,太浅了,容易跟别的应用撞
CREATE TABLESPACE ts1 LOCATION '/ts1';

记住几条就行:全小写、固定一个根目录(比如 /data/tablespaces/)、按业务名称分层、只用字母数字和下划线。

巡检脚本

日常运维不能光靠肉眼看,写个脚本定期扫一遍:

#!/bin/bash
# 表空间目录巡检
DB_USER="system"
DB_HOST="localhost"
DB_PORT="54321"
REPORT="/var/log/ts_check_$(date +%Y%m%d).log"

echo "表空间目录巡检 - $(date '+%Y-%m-%d %H:%M:%S')" > "$REPORT"
echo "================================================" >> "$REPORT"

TABLESPACES=$(ksql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" \
    -t -A -c "
        SELECT spcname || '|' || pg_tablespace_location(oid)
        FROM pg_tablespace
        WHERE spcname NOT IN ('pg_default', 'pg_global')
    ")

[ -z "$TABLESPACES" ] && { echo "没有自定义表空间" >> "$REPORT"; exit 0; }

WARN=0
while IFS='|' read -r name location; do
    echo "[$name] 路径: $location" >> "$REPORT"
    if [ -d "$location" ]; then
        owner=$(stat -c '%U' "$location" 2>/dev/null || stat -f '%Su' "$location")
        perms=$(stat -c '%A' "$location" 2>/dev/null || stat -f '%Sp' "$location")
        echo "  属主: $owner  权限: $perms" >> "$REPORT"
        [ -w "$location" ] || { echo "  ⚠ 目录不可写!" >> "$REPORT"; WARN=$((WARN+1)); }
    else
        echo "  ⚠ 目录不存在!" >> "$REPORT"
        WARN=$((WARN+1))
    fi
done <<< "$TABLESPACES"

echo "================================================" >> "$REPORT"
[ "$WARN" -gt 0 ] && echo "有 $WARN 个异常" >> "$REPORT" || echo "全部正常" >> "$REPORT"

cat "$REPORT"

定期跑一下,目录不存在、属主不对、没有写入权限这些问题都能及时发现。

收个尾

表空间目录自动创建说穿了就是数据库帮你干了原来要自己干的活。auto_createtblspcdir 这个参数设计得挺克制——默认开着省事,关掉也不影响什么。

但也别什么场景都开着。安全要求高的国产 OS 环境、多租户平台,关掉自动创建反而是更稳妥的选择。国产化迁移最容易栽跟头的地方往往不是什么大问题,而是路径大小写、分隔符、属主权限这类小细节。每一个单拿出来都不难,但堆在一起、混着操作系统的差异,排查起来就费劲了。提前把底层行为搞明白,把命名规范定好,比出了问题再翻日志强得多。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐