我最近接触金仓的项目比较多。所以总结了经常会遇见的坑,希望本最佳实践指南能给大家带来帮助。

一、空串与 NULL:兼容模式最经典的坑

Oracle 里 ‘’ 就是 NULL,PG 里 ‘’ 是长度为 0 的字符串。KES 用参数 ora_input_emptystr_isnull 切换这两种语义。

那么坑在哪?这个参数只影响写入时的转换,不影响已经存进去的数据。我们自己跑一遍就明白了:

-- 参数关闭时插入
set ora_input_emptystr_isnull = off;
create table t1 (id integer, name varchar(9));
insert into t1 values (1, '');      -- 落盘是空串
insert into t1 values (2, null);    -- 落盘是 null

select * from t1 where name is null;   -- 只有 id=2
select * from t1 where name = '';      -- 只有 id=1

-- 现在把参数打开,存量数据不变,但查询语义变了
set ora_input_emptystr_isnull = on;
select * from t1 where name = '';      -- 0 行!'' 在查询里也被转成 null 了
select length(name) from t1 where id = 1;  -- 返回 0,证明空串还在那
select * from t1 where name is not null;   -- id=1 还能查出来

id=1 那行数据变成了"用 = ''查不到、用 is null 也查不到",但是它又确实存在。现场遇到"数据时有时无",先把这四条查询跑一遍,再看两个环境参数:

show database_mode;
show ora_input_emptystr_isnull;

记住两条规则:

1、判空永远用 is null / is not null。NULL 和任何值做 = 或 <> 比较都不成立,这是 SQL 标准行为,任何模式下都一样。

2、 PG 模式下不要开这个参数,KES 会直接给 WARNING:configuration parameter “ora_input_emptystr_isnull” should not set in current database mode。包括JDBC 报 type “q” does not exist 也是它连带出来的。

这个参数要在建库前设置好,最好写进部署基线。上线后再改,语义就分裂了。

二、字符集:三层编码,一层没对齐就出乱码

KES 的编码链路有三层,排查乱码的固定动作就是把三个值摆出来对:

env | grep LANG                # 操作系统层
show server_encoding;          -- 数据库层,initdb 后不可改
show client_encoding;          -- 连接层,可以会话内 set
\l                             -- 看每个库的 encoding 和 collate

这里最隐蔽的一条:initdb 时如果没显式指定编码,KES 跟随 LANG 环境变量;Docker 或精简系统里往往没设 LANG,初始化出来就是 SQL_ASCII。这种库里插中文,一个汉字按三个字节算:

-- SQL_ASCII 库,oracle 模式
set nls_length_semantics = 'char';
create table test_char (col char(4));
insert into test_char values ('一二');
-- ERROR: value too large for column ... (actual:6, maximum:4)
-- 两个汉字 6 字节,nls_length_semantics 在 ASCII 编码下救不了你x

所以初始化必须显式带编码:

initdb -E UTF8 --locale=zh_CN.UTF-8 -D /data/kingbase

库已经建好了想换编码?先要知道一件事:create database 本质是复制模板库。系统自带两个模板,template1 是默认模板,允许连接和修改,DBA 装过的扩展、建过的对象都会留在里面,所以复制它时不允许换编码——里面已有的对象是按旧编码组织的,换了就自相矛盾。template0 永远保持 initdb 刚完成的出厂状态,不可连接不可修改,复制它时指定什么编码都行。所以换编码建库必须显式指定 template0:

create database appdb encoding 'UTF8' lc_collate 'zh_CN.UTF-8'
    lc_ctype 'zh_CN.UTF-8' template template0;
-- 不加 template template0 会报:
-- new encoding (xxx) is incompatible with encoding of the template database

UTF8 库里汉字占几个"字符",Oracle 模式下由 nls_length_semantics 决定:char 按字符算("一二"占 2),byte 按字节算(占 6)。PG 模式下这个参数无效,一律按字符。迁移前和开发确认列长度按什么语义定义的,别等数据导一半报错再去解决,那样会花很多时间。

跨库场景再补一条:oracle_fdw 查 GBK 的 Oracle 库会出乱码,可以在 wrapper 定义里指定 NLS_LANG,然后重建插件:

CREATE FOREIGN DATA WRAPPER oracle_fdw
    HANDLER oracle_fdw_handler
    VALIDATOR oracle_fdw_validator
    options (nls_lang 'AMERICAN_AMERICA.ZHS16GBK');

三、参数管理:alter system 背后的优先级陷阱

KES 有两个参数文件:kingbase.conf 和 kingbase.auto.conf。alter system set 的改动写进后者。启动时先读前者再读后者,同名条目以 auto.conf 为准。

这里要注意的一点:你在 kingbase.conf 里改了参数,重启后没生效,因为几个月前有人 alter system 过同一个参数,auto.conf 里的旧值把你的配置覆盖了。排查很简单,直接看文件:

grep -v '^#' /data/kingbase/kingbase.auto.conf
grep -i shared_buffers /data/kingbase/kingbase.conf
-- 或者问数据库:参数当前值到底从哪来的
select name, setting, sourcefile, sourceline
from pg_settings
where name = 'shared_buffers';

参数改完怎么生效,查 context 字段,不用背表:

select name, context from pg_settings
where name in ('block_size', 'archive_mode', 'archive_command',
               'log_connections', 'bytea_output');

查出来的 context 值对照下表,就知道这个参数怎么改才生效:

context 怎么生效 例子
internal 改不了,initdb 时定死 block_size
kingbase 重启数据库 archive_mode
sighup sys_ctl reload -D /data/kingbase archive_command
backend reload 后,新连上来的会话生效 log_connections
user 会话里直接 set bytea_output

记住一个使用习惯就行:改参数前先查 context,别上来就重启。很多参数 reload 就能生效,生产环境能不重启就不重启。

这里有一个典型案例:有用户把 max_locks_per_transaction 设成了 215927809。这个参数的共享内存开销有公式:

锁表内存 = max_locks_per_transaction × 270 字节 × (max_connections + max_prepared_transactions)
        = 215927809 × 270 × 1000 ≈ 54 TB

数据库直接起不来,报"内存段超过可用内存"。同样报错还有四种可能:物理内存真不够、shared_buffers 远大于 swap、内核参数过小、huge_pages = on 但操作系统大页没配对。所以大家改内存类参数之前先计算好。

四、SQL 层的几个"为什么"

序列跳号不是 bug。 每个会话按序列的 cache 值在私有内存缓存一段序列值,会话退出或 sys_ctl stop -m fast 时缓存直接丢弃。如果你不能接受跳号就把 cache 关掉:

alter sequence seq_order cache 1;   -- 或建序列时指定 cache 0

代价是每次取值都要访问序列对象,高并发写入场景会变成热点。至于事务回滚后序列值不回收,任何数据库都这样,序列不能当连续流水号用。

now() 在同一个事务里永远返回同一个值。 它返回事务开始时间,这是 PG 的设计。自己验证:

begin;
select now(), clock_timestamp();
select pg_sleep(2);
select now(), clock_timestamp();   -- now() 没变,clock_timestamp() 走了 2 秒
commit;

批量任务里用 now() 打时间戳判断先后顺序的代码,都检查一下,要语句级时间就换 clock_timestamp() 或 statement_timestamp()。

关键字撞名。 先确认到底是不是关键字,以及是哪一级:

select word, catcode, catdesc from pg_get_keywords() where word = 'level';

撞了就用双引号,但会引发另一个问题——双引号标识符大小写敏感,“User” 和 “user” 是两个对象,ORM 生成的 SQL 和手写 SQL 大小写不一致就找不到表。另一条路 exclude_reserved_words 则风险更大:

-- 有人这么配过:
set exclude_reserved_words = 'level';
show transaction isolation level;   -- 直接语法错误
-- 排除一个关键字,所有含它的语法全部失效

业务表和系统视图重名。 客户建了张 sys_user,直接查到的是系统视图。根本原因是search_path 里不含 sys_catalog 时,KES 自动把它插到最前面。解决方法是显式配置,把系统目录挪到最后:

# kingbase.conf,改完重启
search_path = '"$USER",PUBLIC,SYS_CATALOG'

copy 导入报文件找不到。 copy 读服务器上的文件,客户端本地文件用 ksql 的 \copy:

\copy t1 from '/home/user/data.csv' with (format csv);

导对象 DDL。 KES 带了类似 Oracle 的 dbms_metadata,梳理存量对象很好用:

create extension dbms_metadata;
select dbms_metadata.get_ddl('table', 't1');
select dbms_metadata.get_index_ddl('ind_t1', 'public');

忘了数据库密码。 密码不可逆但可以重置,全程不用重启(变更操作,trust 窗口期内本机免密进库,动作要快):

# 1. 编辑 data/sys_hba.conf,把 local 认证临时改成 trust
# 2. reload 生效
sys_ctl reload -D /data/kingbase
# 3. 连进去改密码
ksql -U system -d test -c "alter role system password 'NewPass#2026';"
# 4. 把 sys_hba.conf 改回原认证方式,再 reload
sys_ctl reload -D /data/kingbase

数据库 core 了怎么留证据。 生产上 core 文件默认经常生成不了,两个原因:ulimit 限制和 abrt 拦截。提前配好:

ulimit -c                         # 确认是 unlimited
# CentOS 7 系:修改 /etc/abrt/abrt-action-save-package-data.conf
#   OpenGPGCheck = no
#   ProcessUnpackaged = yes
systemctl restart abrtd           # core 会落在 /var/spool/abrt/ccpp*
# 分析:
gdb --core=/var/spool/abrt/ccpp-xxx/coredump --exec=/opt/Kingbase/ES/V9/Server/bin/kingbase

五、高可用集群:几条不可逆的红线

1、集群的 max_connections 只能调大,不能调小。 调小后备库无法启动,唯一办法是重做备机。上线前把连接数规划好,宁可留富余!

2、 VIP 的子网掩码必须和主备库一致。 比如:VIP 配 /24,主库 /16,备库 /15,主库看到的备库来源 IP 变成了 VIP,备库反复重启加不进集群。数据库日志只会告诉你"备库连不上主库",真正的原因要在网络层看:

ip addr | grep -E 'inet .*(eth|bond|ens)'   # 逐台对掩码,VIP 和主备必须同网段同掩码

其他常见现象,对号入座:

备库查询被杀,报 conflict with recovery。主库 vacuum 回放到备库时和备库长查询冲突,超过 max_standby_streaming_delay 就终止查询,备库跑 sys_dump 报错也是它。要在备库做大查询或备份,把参数调大(sighup 级,reload 生效):

alter system set max_standby_streaming_delay = '600s';
sys_ctl reload -D /data/kingbase

读写分离所有查询都压在主库。大概率是连接用户权限不够,JDBC 检测线程查不到在线备机。用业务同一个用户验证:

select client_addr, state, sync_state from sys_stat_replication;
-- 查不到备机记录 = 权限问题,JDBC 会把所有 SQL 发主库

读语句总发往同一台备机。同一个 Statement 底层绑定同一个备连接,PreparedStatement 反复 execute 也一样,想分散就新建 Statement。失败重发机制也顺带记住了:备机报错的 SQL 自动转发主机重跑,主机只有 IO 错误才重发,RETRYTIMES 默认 10。想现场演练备机故障转移:

-- 把 HOSTLOADRATE 设为 0,跑个长查询,然后杀备机进程
select sys_sleep(20);

集群部署阶段的三个检查项,一次做完:

# root ssh 互信要求 PermitRootLogin yes
grep PermitRootLogin /etc/ssh/sshd_config &amp;&amp; systemctl restart sshd
# open files,es_client 检测的是非交互 shell 的值,写进 /etc/profile 才算数
echo 'ulimit -HSn 102400' >> /etc/profile &amp;&amp; source /etc/profile
ulimit -n

sshd 只在集群启停时用到(远程改 crontab、跑 sys_ctl),平时停掉不影响数据库运行。

六、备份恢复:一致性和性能各有一个关键点

sys_dump 默认导出的不是同一时间点的数据。 它对每张表分别发 copy,各表快照时间不同。表间有关联、又在业务高峰导出,恢复出来可能对不上账。时间点要严格一致,用快照导出:

-- 会话 A:开事务导出快照,导出期间保持事务不提交
begin;
select sys_export_snapshot();   -- 返回快照 ID,例如 00000806-1
# 会话 B:带快照 ID 导出,导的是 A 执行函数那一刻已提交的数据
sys_dump --snapshot=00000806-1 -Fc -f backup.dmp -d appdb -U system -p 54321
# 导完后会话 A 再 commit

大对象表备份卡死。 blob/clob 走默认 copy 文本模式要转码,加上默认压缩,CPU 全耗在这上面。两个开关:

sys_dump -d appdb -Fc --copy-binary -Z 0 -f backup.dmp
# --copy-binary 二进制导出免转码(只能出 dmp 格式)
# -Z 0 关压缩,空间换时间
# 个别分支没有 --copy-binary,用 --insert 替代,大对象场景也比 copy 文本快

还原侧两个容易碰到的问题:

还原 RANGE 分区报"指定的下限 (‘Z’) 大于或等于上限 (‘a’)"。源库和目标库排序规则不一致,‘Z’ 和 ‘a’ 谁大谁小两边答案不同。还原前对一下:

show lc_collate;   -- 源、目标各查一次,不一致就用 template0 重建目标库

指定表的语法两边不对称,这个设计确实反直觉:

sys_dump   -d test -Fc -f test.dmp -t s1.t1      # 备份:-t 模式.表名
sys_restore -d test -n s1 -t t1 test.dmp          # 还原:-n 模式 -t 表名
# 还原侧用错写法不报错,静默匹配不到,什么都没还原——比报错更坑

sys_rman 两个环境问题:报 current time may be rewound 是系统时间被回调过,库里存在"未来"对象,物理备份救不了,只能 sys_dump 逻辑重建,生产的 NTP 别乱动;UOS/Debian 上报 libssl_conf.so: cannot open shared object file,补一个环境变量:

echo 'export OPENSSL_CONF=/etc/ssl/' >> /etc/profile
# 归档命令里也可能要带:archive_command = 'export OPENSSL_CONF=/etc/ssl/ &amp;&amp; ...'

七、License:别让授权把生产搞停

国产库特有的一环。日常巡检两条 SQL:

select get_license_validdays();   -- 剩余天数,-2 表示永久授权
select get_license_info();        -- 完整授权信息:版本、MAC 绑定、功能开关

替换 license 不用重启,两种方式(变更操作,先备份旧 license.dat):

# 方式一:sys_ctl
chown kingbase:kingbase /path/new_license.dat
su - kingbase
sys_ctl -D /data/kingbase reload_license -L /path/new_license.dat
# data 路径不确定就 ps -ef | grep kingbase 找 -D 参数
-- 方式二:库内执行,重新登录后生效
select sys_reload_license('/path/new_license.dat');

换了新 license 还提示过期,按顺序排查:产品版本号和授权版本号是否匹配(不同版本 license 不通用);MAC 地址、CPU 核数等绑定项是否一致;到期日怎么算——"浮动基准日期"禁用时是生产日期加有效期,启用时是 license 更换日期加有效期。

两个部署形态:Docker 没有物理 MAC,要申请不绑硬件的专用 license;一个 license 最多绑三个 MAC,主备迁移场景提前把几台机器的 MAC 都报上去。

最后把有效期接进监控,剩 30 天告警:

# crontab,每天 9 点检查
0 9 * * * days=$(ksql -U system -d test -t -A -c 'select get_license_validdays();'); \
  [ "$days" != "-2" ] &amp;&amp; [ "$days" -lt 30 ] &amp;&amp; echo "KES license 剩 $days 天" | mail -s 'license 告警' dba@example.com

license 过期导致数据库拉不起来,属于最没技术含量但最真实的生产事故。

总结

回头看以上内容,真正的高频根因就四个:

  1. 兼容模式的语义分裂。空串、汉字长度、now() 行为,都是 Oracle 语义和 PG 语义打架。应对方法是建库前把 database_mode、ora_input_emptystr_isnull、nls_length_semantics`定死,写进部署基线。
  2. 编码链路不对齐。LANG、server_encoding、client_encoding 三层,initdb 显式指定编码能消除一大半问题。
  3. 参数的隐蔽优先级。auto.conf 覆盖 kingbase.conf,集群 max_connections 不可逆调小,内存参数有乘法公式。改参数前先查 pg_settings 的 context 和 sourcefile。
  4. 备份的一致性假设。sys_dump 默认不是单一时间点,排序规则影响还原。换句话说,没有做过恢复演练的备份很多时候是无效的。
Logo

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

更多推荐