sys_dump 只导一个库里面的东西,角色、表空间这些全局对象不在里头。这句话听着抽象,真出事是这样的——单库备份文件好好的,校验也过了,换台机器一还原,ksql 当场撂挑子:
ERROR: role "bak_app_owner" does not exist
备份没坏,数据也没丢,问题出在备份的边界上。表的 owner 是某个角色、某张表的查询权限授给了另一个角色,这些「引用」都老老实实备进了单库文件里;可被引用的角色本身,不归 sys_dump 管。新机器上没这俩角色,还原走到 ALTER TABLE ... OWNER TO 那一句就崩了。
这篇就把这条边界讲清楚:单库备份里到底有没有角色、角色该由谁来备、恢复时谁先谁后。用一个 owner 角色加一个只读角色搭个最小现场。照例,建角色、建库都是 DBA 的活,这篇用 system 连。
先看清现场:库里的东西引用了外面的角色
实验现场是这样搭的:两个角色 bak_app_owner(库和表的 owner)和 bak_readonly_user(只读用户),一个库 global_demo_db 归 bak_app_owner 所有,库里 app schema 下有张表也归它,再把这张表的查询权限授给只读用户。先把这个现场查出来:
ksql -h 127.0.0.1 -p 54321 -U system -d app_db -c "\du bak_*"
ksql -h 127.0.0.1 -p 54321 -U system -d global_demo_db -c "
select d.datname, catalog.get_userbyid(d.datdba) as db_owner
from database d where d.datname = 'global_demo_db';"
ksql -h 127.0.0.1 -p 54321 -U system -d global_demo_db -c "
select schemaname, tablename, tableowner from tables
where schemaname = 'app' order by tablename;"
ksql -h 127.0.0.1 -p 54321 -U system -d global_demo_db -c "
select
has_schema_privilege('bak_readonly_user', 'app', 'USAGE') as can_use_schema,
has_table_privilege('bak_readonly_user', 'app.t_global_customer', 'SELECT') as can_select_table;"
两个角色都在,库 owner 是 bak_app_owner,表 app.t_global_customer 的 owner 也是它,只读用户的 schema usage 和表 select 权限都是 t。
把这个现场记在心里:库里的对象(schema、表)挂在外部角色名下,权限也授给了外部角色。角色是数据库实例级别的东西,不属于哪一个 database——这正是它会被单库备份漏在外面的根本原因。
单库备份里:引用了角色,但不创建角色
用 sys_dump 把这个库备出来,纯 SQL 格式方便 grep:
sys_dump -h 127.0.0.1 -p 54321 -U system -d global_demo_db -f "$BACKUP_DIR/global_demo_db.sql"
grep -nE "OWNER TO|GRANT" "$BACKUP_DIR/global_demo_db.sql" | grep -E "bak_app_owner|bak_readonly_user"
if grep -n "CREATE ROLE" "$BACKUP_DIR/global_demo_db.sql"; then
echo "unexpected: database dump contains CREATE ROLE";
else
echo "no CREATE ROLE in database dump";
fi
grep 一抓,备份里这几行都在:
ALTER SCHEMA app OWNER TO bak_app_owner;
ALTER TABLE app.t_global_customer OWNER TO bak_app_owner;
GRANT IF EXISTS USAGE ON SCHEMA app TO bak_readonly_user;
GRANT IF EXISTS SELECT ON TABLE app.t_global_customer TO bak_readonly_user;
schema 和表的归属、给只读用户的授权,全备进去了。但再查有没有创建角色的语句,结果是 no CREATE ROLE in database dump——一句都没有。
这就是单库备份的真实边界:它知道「这张表属于 bak_app_owner、这个权限给了 bak_readonly_user」,并把这些引用原样写进备份;但它默认这俩角色在还原的目标库里已经存在,自己不负责把它们造出来。在原实例上还原没事,角色都在;换个干净的实例,这些 OWNER TO 和 GRANT 就成了悬空的引用,还原直接报角色不存在。
全局对象由 sys_dumpall 负责
角色这类全局对象,是另一个工具 sys_dumpall 的活。先看一眼它是干嘛的:
sys_dumpall --version
sys_dumpall --help
帮助第一句写得很直白:sys_dumpall extracts a Kingbase database cluster into an SQL script file——它面向的是整个实例(cluster),不是某一个库。这里只用它的一个能力:-g / --globals-only,只导全局对象、不碰具体库的数据。顺带留意 --no-role-passwords 这个选项,后面说安全的时候用得上。
把全局对象导出来,只过滤本次实验的两个角色看:
sys_dumpall -h 127.0.0.1 -p 54321 -U system -g -f "$BACKUP_DIR/global_objects.sql"
grep -nE "CREATE USER|ALTER USER|CREATE ROLE|ALTER ROLE|GRANT" "$BACKUP_DIR/global_objects.sql" \
| grep -E "bak_app_owner|bak_readonly_user|global_demo_db" \
| sed -E "s/PASSWORD '[^']+'/PASSWORD '[已脱敏]'/g" \
> "$BACKUP_DIR/global_objects.filtered.txt"
cat "$BACKUP_DIR/global_objects.filtered.txt"
过滤出来是这几行:
CREATE USER bak_app_owner;
ALTER USER bak_app_owner WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS;
CREATE USER bak_readonly_user;
ALTER USER bak_readonly_user WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS;
这里有个容易卡壳的点:明明建的是 role,导出来怎么是 CREATE USER?因为这俩角色都带了 LOGIN,而在 KES里,USER 就是「能登录的 ROLE」,CREATE USER 等价于 CREATE ROLE ... LOGIN。同一样东西的两种写法,看到 CREATE USER 别以为角色没导出来——它就是角色定义,后面那条 ALTER USER ... WITH NOSUPERUSER ... 还把权限属性补全了。
再说一句安全。-g 导的是整个实例的全局对象,里面会有 system、app_user 这些真实账号,正常情况下还跟着各自的密码 hash。所以别图省事 cat 整个 global_objects.sql、更别整文件贴出去。上面那条命令做了两件事:grep 只留本次实验的角色,sed 把 PASSWORD '...' 替换成 [已脱敏]。这次实验角色压根没设密码,输出本就干净;真实环境里要导出全局对象给别人看,要么照这样过滤脱敏,要么直接上 sys_dumpall --no-role-passwords,让它根本不导密码。
两份文件,一个引用、一个创建
把两份备份摆一起看分工,最直接:
echo "database dump role references:"
grep -nE "OWNER TO|GRANT" "$BACKUP_DIR/global_demo_db.sql" | grep -E "bak_app_owner|bak_readonly_user"
echo "global dump role definitions:"
grep -nE "CREATE USER|ALTER USER|CREATE ROLE|ALTER ROLE" "$BACKUP_DIR/global_objects.filtered.txt" \
| grep -E "bak_app_owner|bak_readonly_user"
上半截是单库备份,清一色 OWNER TO / GRANT——引用角色;下半截是全局备份,清一色 CREATE USER / ALTER USER——创建角色。一个用,一个造,刚好补上对方缺的那块。
所以这俩工具不是谁替代谁的关系。sys_dump 管一个库里的对象和数据,sys_dumpall -g 管角色、权限这些跨库的全局对象。备份策略里要是只留了单库文件,迁到新环境这步就缺了角色,前面那个 role does not exist 就是这么来的。
恢复顺序:先全局,再单库
把因果理顺,恢复一个带角色依赖的库,正确顺序是:先在目标实例把全局对象灌进去(角色建好),再还原单库备份。角色先到位,单库里那些 OWNER TO / GRANT 才有落脚的地方。
需要说明的是,下面这个验证是在当前实例做的,不是模拟一台全新机器——当前实例里两个角色本来就在,所以这一步省去了「先灌全局对象」,直接还原单库就行,专门用来确认「角色在的时候,owner 和权限能不能完整恢复」。新建一个恢复库再还原:
ksql -h 127.0.0.1 -p 54321 -U system -d app_db -c "create database global_restore_db owner bak_app_owner;"
ksql -h 127.0.0.1 -p 54321 -U system -d global_restore_db -v ON_ERROR_STOP=1 -f "$BACKUP_DIR/global_demo_db.sql"
还原过程一路 CREATE SCHEMA、ALTER SCHEMA、CREATE TABLE、COPY 2、ALTER TABLE、GRANT,没卡住——因为 bak_app_owner、bak_readonly_user 都在,那些 OWNER TO 和 GRANT 都找得到对应的角色。再查恢复库里 owner 和权限对不对:
ksql -h 127.0.0.1 -p 54321 -U system -d global_restore_db -c "
select schemaname, tablename, tableowner from tables
where schemaname = 'app' order by tablename;"
ksql -h 127.0.0.1 -p 54321 -U system -d global_restore_db -c "
select
has_schema_privilege('bak_readonly_user', 'app', 'USAGE') as can_use_schema,
has_table_privilege('bak_readonly_user', 'app.t_global_customer', 'SELECT') as can_select_table;"
表 owner 还是 bak_app_owner,只读用户的 schema usage 和 select 权限照旧是 t。owner 和授权跟着单库备份原样回来了——前提是角色这个地基在。换成一台空机器,这一步之前必须先把 global_objects.sql 里的角色灌进去,否则就是开头那个报错。
收个尾
这篇其实就讲了一条边界:sys_dump 备的是库内对象,连同对角色的引用;角色本身(连同它的属性、密码、库级授权)要靠 sys_dumpall -g 单独备。生产里这两份得一起留,缺了全局那份,跨机器恢复就会在角色上绊倒。
当然也有另一种活法:要是迁到新环境本来就打算重新规划权限、不想带原来的 owner 和 grant,那 sys_dump 加 --no-owner -x 把归属和授权都剥掉,还原后按新环境重新授权,这时候就不依赖全局对象了。但这是「主动放弃」原权限的选择,不是默认;只要你想原样保住 owner 和权限,全局对象就得先到位。
下一篇换个角度,不再整库备,只挑一个 schema 备出来——很多时候要搬走的,其实只是一个库里的某一块。