人大金仓数据库的容器化部署,很多人第一反应是"不就是 docker run 一条命令的事"。真上手之后才发现,镜像从哪来、默认端口为什么不是 5432、初始化时那个 system 用户到底怎么设密码、挂载宿主机目录之后为什么起不来,每一处都能卡住半天。这篇内容就是我把人大金仓 KingbaseES 塞进 Docker 的完整过程整理,从镜像获取、单机跑通、参数调优一路写到生产环境真正要考虑的持久化、备份、监控和踩坑排查。适合两类人看:一类是刚接触国产数据库容器化、想快速把环境搭起来做验证的开发,另一类是准备把测试环境的东西往生产搬、但又拿不准哪些雷必须提前排掉的运维。我不会只给命令,每条命令背后为什么这么写、不这么写会出什么问题,都会掰开讲清楚。
1. 先把这笔账算清楚:数据库进容器的收益与隐性成本
动手之前先别急着拉镜像。数据库和普通无状态应用不一样,它是有状态的、对 IO 和内存敏感的、对时间精度和文件权限苛刻的。容器这套东西天生更适合无状态服务,把数据库放进去,本质上是在用一部分确定性换便利性。这笔账算不明白,后面一定会返工。
1.1 容器化真正带来的三个好处
第一个好处是环境一致性。我见过太多"我本地跑得好好的"这种扯皮,甲方现场是某个国产 Linux 发行版,开发机是另一个版本,glibc、locale、文件系统大小写敏感性全都不一样。数据库这种对系统底层依赖很深的软件,装完跑不起来的情况一点不稀奇。打成镜像之后,底层操作系统的那层差异被抹平了,同一份镜像在开发机、测试机、客户现场跑出来的行为是一致的,这一点在国产化适配场景里价值极大。
第二个好处是交付速度。传统方式装一套人大金仓,要先查依赖、改内核参数、建用户、切用户、跑安装脚本、配环境变量、初始化实例、改配置文件,一套流程走下来熟手也要一两个小时,新人半天起步。容器化之后,一次镜像导入加一条启动命令,几分钟出结果,还能复制粘贴。做 POC 演示、做多版本对比测试的时候,这个效率差是碾压性的。
第三个好处是隔离与清理。数据库装宿主机上,卸载是个麻烦事,服务残留、用户残留、目录残留,装第二遍经常互相干扰。容器删掉就没了,宿主机干干净净。做临时验证、做性能基准对比,这点特别舒服。
1.2 数据库跑在容器里要付出的代价
代价也是实打实的。最直接的是数据持久化,容器本身的文件系统是临时的,容器一删数据全没。必须把数据目录挂出来,这就引入了挂载点的权限、性能、备份一整套问题。我后面会专门拿一节讲这个。
然后是内存和共享内存。人大金仓基于 PostgreSQL 内核,大量依赖 System V 共享内存。Docker 默认给容器的 /dev/shm 只有 64MB,稍微跑点并发查询就会撞上共享内存不足的报错。这个坑几乎是必经之路,不知道的人会以为是数据库参数配错了,其实调数据库参数根本没用,得调容器的 shm-size。
再就是性能损耗。容器的网络栈、存储驱动都会带来额外开销,尤其存储这块,如果用 overlay2 做数据落地,随机写性能会明显打折。生产环境数据库容器的数据目录必须走 volume 或者直接 bind mount 到宿主机的高性能磁盘,不能让它落在容器的可写层上。
最后是运维习惯的改变。以前数据库出问题,登机器看日志、看进程、看系统参数,路径都是固定的。容器化之后日志要用 docker logs 或者挂出来,进程在容器内看,宿主机上 ps 看不到数据库进程,排查思路要重新建立。
1.3 判断标准:你的场景到底该不该上
我把常见场景分成三类。开发测试、POC 验证、CI 流水线里的临时数据库,容器化收益远大于成本,闭着眼睛上就行。中小规模的生产系统,日活不高、数据量在几百 GB 以内、有靠谱的备份机制,容器化是可以接受的,但要严格按后面讲的持久化和调优要点来做。核心交易系统、数据量上 TB、对 IO 延迟极度敏感的场景,我个人的建议还是物理机或者虚拟机直接装,容器带来的那点便利,不足以抵消它在 IO 路径上引入的不确定性。
还有一点要说清楚:容器化不等于编排化。很多人一提容器就想到 Kubernetes,觉得数据库也得跑在 K8s 里才叫现代化。有状态服务上 K8s,存储、调度、故障恢复全是麻烦事,运维复杂度是单机 Docker 的好几倍。除非你的团队已经有成熟的有状态服务运维能力,否则单机 Docker 加一套靠谱的备份恢复流程,反而是更稳的选择。
2. 镜像从哪来:三条获取路径与各自的坑
第一个卡住人的地方就是镜像。人大金仓不像 MySQL 那样在公共镜像仓库里随手就能 pull 到官方维护的版本,镜像的获取渠道相对有限,而很多生产环境又是完全断网的内网,这就逼着你必须搞清楚镜像怎么搬进去。
2.1 官方镜像包与 docker load 导入
最稳妥的路子是从官方渠道拿到打包好的镜像文件,通常是.tar或者.tar.gz格式的离线包。拿到之后在目标机器上执行导入。
# 先看看包里的镜像信息,避免导入一堆用不上的层 tar -tf kingbase_v8r6_image.tar | head -20 # 导入镜像 docker load -i kingbase_v8r6_image.tar # 确认导入结果,记下 REPOSITORY 和 TAG docker images | grep -i kingbase注意docker load之后镜像名可能长得很随意,比如kingbase加一串版本号,甚至直接是<none>:<none>。遇到后者说明打包的时候没打 tag,需要手动补一个:
docker tag <镜像ID> kingbase:v8r6这一步看着简单,但有个细节容易忽略:docker load会保留镜像里的所有历史层,如果这个包是经过多次修改重新打包的,体积可能特别大。导入前先看看 tar 包大小,动辄几个 G 的话,先确认下是不是把无关的中间层也打进去了。
提示:导入镜像的机器磁盘空间要留足,Docker 的默认存储路径在
/var/lib/docker,很多云主机这块盘不大,几个镜像下去就满了。导入前用df -h /var/lib/docker看一眼。
2.2 内网离线搬运的完整操作
内网环境没法直接 pull,标准套路是"外网机器拉取、save 成文件、传到内网、load 进去"。这个流程本身不难,难在中间几个容易断的环节。
# 外网机器上,确认镜像名和 tag docker images | grep kingbase # 保存为压缩格式,能省不少传输时间 docker save kingbase:v8r6 | gzip > kingbase_v8r6.tar.gz # 内网机器上解压导入 gunzip -c kingbase_v8r6.tar.gz | docker load传输环节建议用分片加校验的方式,大文件走 U 盘或者网闸的时候经常传一半断了。可以拆成 500MB 一片,传完再合并,最后用sha256sum比对两端的哈希值,确认文件没损坏。听起来啰嗦,但一个 3G 的镜像传坏了重来一次,时间成本比做校验高多了。
还有一个坑是镜像架构。外网机器如果是 ARM 架构的笔记本,导出的镜像是 arm64 的,拿到 x86 服务器上 load 进去能成功,但docker run会直接报exec format error。导出前用docker inspect <镜像名> --format '{{.Architecture}}'确认一下架构,跟你目标机器对得上再打包。
2.3 自建镜像的 Dockerfile 写法与踩雷点
有些情况下你会想基于一个干净的基础镜像自己装。比如官方镜像太大、比如需要在镜像里预置一些初始化脚本、比如要精简掉不需要的组件。这条路可以走,但有几个雷区。
FROM centos:7 # 金仓不接受 root 直接跑,必须建普通用户 RUN groupadd -g 1000 kingbase && \ useradd -g kingbase -u 1000 -m -s /bin/bash kingbase # 安装包和 license 一起放进来 COPY KingbaseES_V8R6_*.run /tmp/ COPY license.dat /opt/Kingbase/license.dat RUN chmod +x /tmp/KingbaseES_V8R6_*.run && \ chown -R kingbase:kingbase /opt/Kingbase && \ chmod 755 /opt/Kingbase/license.dat USER kingbase WORKDIR /opt/Kingbase/ES/V8 RUN /tmp/KingbaseES_V8R6_*.run -i silent ENV PATH=/opt/Kingbase/ES/V8/Server/bin:$PATH ENV LD_LIBRARY_PATH=/opt/Kingbase/ES/V8/Server/lib:$LD_LIBRARY_PATH EXPOSE 54321 CMD ["/opt/Kingbase/ES/V8/Server/bin/sys_ctl", "-D", "/opt/Kingbase/ES/V8/data", "start", "-w"]第一个雷是 license。人大金仓的授权文件是必须的,没有 license 实例起不来或者只能跑很小规模。把 license 打进镜像最省事,但生产环境更推荐用挂载的方式注入,这样换 license 不用重新构建镜像。
第二个雷是用户 ID。容器里的 kingbase 用户如果 UID 是 1000,而宿主机上挂载目录的属主是别的 UID,就会出现数据目录不可写。构建时把 UID 固定下来,宿主机建目录的时候用同样的 UID,能省掉一堆麻烦。
第三个雷是缺依赖库。金仓依赖一些系统库,比如libaio、readline、libnsl,基础镜像里不一定有。构建完先docker run --rm -it 镜像名 bash进去手工跑一遍初始化,把缺的库补齐,否则等到生产环境启动失败再回头查,成本翻倍。
3. 单机跑通:从 docker run 到 ksql 连上的完整链路
镜像有了,接下来就是把容器跑起来并真正连上。这一段是整个流程里最容易出问题的部分,因为涉及端口、用户、权限、初始化四个环节的配合。
3.1 宿主机预检与目录规划
启动前先在宿主机上做几项检查,能省掉后面 80% 的故障。
# 内核共享内存参数,容器内会用 sysctl kernel.shmmax kernel.shmall # 如果是 0 或者偏小,临时调大 sysctl -w kernel.shmmax=4294967296 sysctl -w kernel.shmall=1048576 # 确认端口没被占用,金仓默认 54321,不是 5432 ss -lntp | grep 54321 # 查看磁盘空间和挂载点 df -h /data目录规划上,我习惯按这样的结构组织宿主机目录:
| 宿主机路径 | 容器内路径 | 用途 |
|---|---|---|
| /data/kingbase/data | /opt/Kingbase/ES/V8/data | 数据目录,最关键 |
| /data/kingbase/license | /opt/Kingbase/license | 授权文件 |
| /data/kingbase/backup | /backup | 备份输出目录 |
| /data/kingbase/log | /opt/Kingbase/ES/V8/data/log | 日志单独挂出来 |
日志单独挂载这个习惯强烈建议养成。数据库容器出问题时,docker logs看到的东西非常有限,真正有用的信息在数据库自己的日志文件里。单独挂出来,宿主机上tail -f就能实时看,比进容器翻文件方便得多。
建目录的时候把属主改对:
mkdir -p /data/kingbase/{data,license,backup,log} chown -R 1000:1000 /data/kingbase chmod 700 /data/kingbase/data数据目录权限 700 是有意为之,金仓的 initdb 会检查数据目录权限,太松会拒绝初始化。
3.2 启动命令逐个参数拆解
docker run -d \ --name kingbase \ --restart always \ --privileged=false \ -p 54321:54321 \ -e TZ=Asia/Shanghai \ -e DB_USER=system \ -e DB_PASSWORD='Kingbase@2024' \ -e DB_MODE=oracle \ -v /data/kingbase/data:/opt/Kingbase/ES/V8/data \ -v /data/kingbase/license/license.dat:/opt/Kingbase/license.dat:ro \ -v /data/kingbase/backup:/backup \ -v /data/kingbase/log:/opt/Kingbase/ES/V8/data/log \ --shm-size=2g \ --ulimit nofile=65536:65536 \ --ulimit nproc=16384:16384 \ --memory=8g \ --cpus=4 \ kingbase:v8r6一段一段说。-p 54321:54321这个端口映射一定要写对,金仓默认端口是 54321 而不是 PostgreSQL 的 5432,很多从 PG 转过来的人会习惯性写 5432,然后一直连不上还找不到原因。
DB_MODE=oracle这个环境变量控制兼容模式。人大金仓支持 Oracle 兼容和 PostgreSQL 兼容两种模式,选哪个取决于你的应用是从哪个数据库迁过来的。这个设置在建库之后改起来很麻烦,属于必须在初始化阶段定下来的东西。如果镜像不支持用环境变量控制,就得进容器手工改kingbase.conf里的database_mode参数再重启。
--shm-size=2g是重点。前面说过默认 64MB 不够用,2G 是我在大多数中小规模场景下的经验值。这个值不是越大越好,它占用的是宿主机内存,跟--memory一起算总账。一般是内存的 1/4 到 1/2,具体看你的shared_buffers和并发连接数。
--ulimit nofile=65536:65536也很关键。数据库会打开大量文件句柄,容器默认的 1024 根本不够,并发一上来就报 "too many open files"。这个值要小于宿主机/etc/security/limits.conf里配的上限,否则容器启动会失败。
--restart always让容器随 Docker 服务自启,宿主机重启后数据库能自己回来。但注意这只是容器起来,数据库实例本身能不能起来还得看初始化是否成功、license 是否有效,别以为配了这个就万事大吉。
3.3 首次初始化与 system 用户
容器第一次启动时如果数据目录是空的,镜像里的启动脚本通常会自动跑 initdb。这个过程需要几十秒到几分钟,取决于机器性能。用docker logs -f kingbase盯着看,出现类似 "database system is ready to accept connections" 的字样才算完成。
人大金仓 V8 的默认超级用户是system,不是 postgres。默认数据库一般是test。这两个名字跟 PG 不一样,是很多人第一次连不上的直接原因。
如果镜像是纯安装包、不带自动初始化逻辑,就得手工来:
# 进容器 docker exec -it kingbase bash # 切到 kingbase 用户 su - kingbase # 初始化实例,指定编码和兼容模式 initdb -D /opt/Kingbase/ES/V8/data -U system -E UTF8 --locale=C # 改配置,允许远程连接 echo "listen_addresses = '*'" >> /opt/Kingbase/ES/V8/data/kingbase.conf echo "port = 54321" >> /opt/Kingbase/ES/V8/data/kingbase.conf # 启动 sys_ctl -D /opt/Kingbase/ES/V8/data -l /opt/Kingbase/ES/V8/data/log/startup.log startlisten_addresses这个参数特别容易忘。默认值是 localhost,容器内部连得上,宿主机通过端口映射进来的连接会被拒绝。表现出来就是"容器明明在跑,端口也映射了,就是连不上"。
--locale=C是我踩过坑之后的习惯。默认 locale 如果跟数据里的字符集不匹配,中文排序会出各种诡异结果。C locale 下排序是字节序,虽然不符合中文习惯,但至少是确定性的、不会因为环境变化而变。真要中文排序,就显式指定--locale=zh_CN.UTF-8,前提是镜像里装了这个 locale。
3.4 连通性验证与报错对照表
初始化完成后,从宿主机用 ksql 连一下:
ksql -h 127.0.0.1 -p 54321 -U system -d test如果宿主机没装客户端,进容器里连:
docker exec -it kingbase ksql -h 127.0.0.1 -p 54321 -U system -d test连上之后跑几个命令确认状态:
-- 看版本 select version(); -- 看当前库 select current_database(), current_user; -- 看数据目录 show data_directory; -- 看监听配置 show listen_addresses; show port;常见报错我整理成一张表,基本覆盖了单机部署阶段 90% 的问题:
| 报错信息 | 大概率原因 | 处理方式 |
|---|---|---|
| Connection refused | 端口没映射或实例没起 | 查docker ps端口、查容器内ss -lntp |
| FATAL: no sys_hba.conf entry | 客户端 IP 没在访问控制里 | 改sys_hba.conf加 host 条目后 reload |
| password authentication failed | 用户或密码不对 | 确认是 system 用户,密码看启动时的环境变量 |
| could not connect to server | listen_addresses 是 localhost | 改成*后重启实例 |
| FATAL: the database system is starting up | 实例还在恢复 | 等几十秒,看日志进度 |
| Permission denied | 挂载目录属主不对 | chown成容器内用户 UID |
sys_hba.conf这块要单独说一句。金仓沿用了 PG 的这套访问控制,默认只允许本地连接。要让外部连进来,得加一行:
host all all 0.0.0.0/0 scram-sha-256改完不用重启,sys_ctl reload或者select sys_reload_conf();就能生效。生产环境千万别用0.0.0.0/0加trust,那等于把数据库裸奔在网络上。
4. 生产落地的四块硬骨头
单机能跑起来只是及格线。真正往生产放,有四件事绕不过去:数据怎么持久化、内存怎么分配、字符集和时区怎么定、备份恢复怎么做。这四块任何一块出问题,都是事故级别的。
4.1 数据持久化:卷、挂载与权限
Docker 里持久化数据有两条路,bind mount 和 named volume。数据库场景我一律推荐 bind mount,原因很实际:备份、迁移、故障排查的时候,你直接能在宿主机上看到数据文件在哪,用 rsync 打包、用 du 看空间、用 strace 看 IO,都方便。named volume 藏在/var/lib/docker/volumes下面,路径又长又随机,运维的时候别扭。
bind mount 的坑集中在权限上。容器内的 kingbase 用户 UID 假设是 1000,宿主机上/data/kingbase/data的属主如果是 root,容器启动时写不进去,表现为启动失败或者数据目录初始化失败,日志里一堆 "Permission denied"。
处理办法有三种,我按推荐度排:
第一种,宿主机建目录时就把属主设成容器内的 UID:
chown -R 1000:1000 /data/kingbase/data这种方法的前提是你知道镜像里用户的 UID,用docker run --rm 镜像名 id就能查。
第二种,用 user namespace 映射,让容器内的 root 映射到宿主机的普通用户。这种配置比较绕,团队里没几个人搞得清楚,出问题难排查,不推荐。
第三种,用--user参数指定容器运行用户。这个在数据库场景下反而容易出问题,因为镜像里的启动脚本可能依赖特定用户的环境变量。
注意:生产环境的数据目录千万不要放在 NFS 上。数据库对文件锁、fsync 的行为有依赖,NFS 在这些方面表现不可靠,轻则性能暴跌,重则数据损坏。本地 SSD 或者高性能块存储才是正路。
还有一点,Docker 的存储驱动选择也会影响性能。overlay2 是默认的,但对于数据库这种随机写密集的负载,把数据目录 bind mount 到宿主机原生文件系统上,能绕开 overlay 的一层开销。这个差异在小数据量下不明显,数据量上到几十 G、并发上来之后就很可观了。
4.2 内存与共享内存:最容易翻车的地方
内存这块要分成三层来看:宿主机物理内存、容器内存限额、数据库自身的内存参数。三层要协调,任何一层配得不合理都会出问题。
宿主机层面,数据库容器建议独占机器,别跟其他吃内存的服务混部。容器内存限额用--memory指定,一般给宿主机内存的 70% 到 80%,留一部分给系统和文件缓存。
容器层面,--shm-size是重点。这个值要大于数据库的shared_buffers加上各种共享内存段的需求。经验公式是shared_buffers的 1.5 到 2 倍,最低不要低于 1G。
数据库层面,shared_buffers一般设成容器内存的 25%,work_mem根据并发数反推。比如容器给了 8G 内存,shared_buffers给 2G,剩下的留给连接、排序、临时表。
-- 查看当前的共享内存相关配置 show shared_buffers; show work_mem; show max_connections; -- 计算理论上限:max_connections * work_mem 不能超过可用内存这里有个隐藏的坑:work_mem不是全局的,是每个排序操作、每个哈希连接单独分配的。一个复杂查询里可能有十几个这样的节点,实际占用是work_mem乘以节点数再乘以并发数。设得太大会 OOM,设得太小会大量落盘,性能暴跌。我一般的做法是先给个保守值比如 8MB,上线后看慢查询日志里有没有 "external merge" 的字样,有的话再针对性调整。
共享内存不足的报错长这样:
ERROR: could not resize shared memory segment to 12345678 bytes: No space left on device看到 "No space left on device" 别急着去看磁盘,df -h显示空间充足的话,问题就在/dev/shm。容器里执行df -h /dev/shm一看便知。这个报错我第一次遇到的时候查了两个小时,方向完全跑偏了。
4.3 时区、字符集与排序规则
这三样东西有个共同特点:必须在初始化阶段定死,后期改起来代价极大。字符集改不了,只能导出重建;时区虽然能改,但已经写入的历史数据不会跟着变。
时区这块,容器默认是 UTC,如果不设,now()返回的时间比北京时间晚 8 小时。应用侧看到时间不对,第一反应往往是应用代码有 bug,查半天才发现是数据库时区的问题。
# 启动时指定 -e TZ=Asia/Shanghai # 或者容器起来后改数据库参数 ALTER SYSTEM SET timezone = 'Asia/Shanghai'; SELECT sys_reload_conf();两处都要设。TZ环境变量影响的是容器内操作系统的时区,数据库的timezone参数影响的是数据库内部的时间函数。只设一个的话,日志时间戳和 SQL 返回的时间可能对不上,排查问题的时候特别迷惑。
字符集在 initdb 的时候就定了,-E UTF8是现在的标准选择。除非你有非常特殊的需求,否则一律 UTF8,没有讨论余地。
排序规则是更微妙的东西。lc_collate和lc_ctype决定了字符串比较和排序的行为。C locale 下按字节序排,中文排序结果虽然看起来"不对"但至少稳定;zh_CN.UTF-8下按拼音排,符合中文习惯,但依赖镜像里装了对应的 locale 包,没装的话 initdb 直接失败。
我的建议是:如果应用层不依赖数据库排序(比如排序都在应用里做),用 C locale 最省事最稳;如果确实需要数据库层面按拼音排序,那就明确指定zh_CN.UTF-8,并且在镜像构建阶段就把 locale 包装好、把 locale 生成好。
# 检查镜像里有哪些 locale 可用 docker exec kingbase locale -a这条命令的输出如果没有zh_CN.utf8,那initdb --locale=zh_CN.UTF-8一定失败。
4.4 备份、恢复与版本升级路径
容器化之后备份这件事反而更简单了,因为工具都在容器里,路径也固定。
逻辑备份用sys_dump:
docker exec kingbase sys_dump \ -h 127.0.0.1 -p 54321 -U system \ -F c -b -v \ -f /backup/test_$(date +%Y%m%d).dmp \ test-F c是自定义格式,压缩比高、支持选择性恢复,比纯 SQL 文本格式好用。备份文件落在/backup,这个目录已经挂到宿主机了,容器删了备份还在。
物理备份用sys_basebackup,适合大库全量备份和搭建备库:
docker exec kingbase sys_basebackup \ -h 127.0.0.1 -p 54321 -U system \ -D /backup/basebackup -F t -z -P -X stream恢复的话,逻辑备份用sys_restore:
docker exec kingbase sys_restore \ -h 127.0.0.1 -p 54321 -U system \ -d test -v /backup/test_20240101.dmp升级这块要说清楚。数据库容器的升级有两条路:容器内原地升级数据库版本,或者换新镜像重建容器。前者用sys_upgrade之类的工具,风险高、耗时长、回滚难。后者更符合容器化的思路,但要保证数据目录兼容。
我的做法是先在新镜像上起一个空实例,用逻辑备份恢复数据,验证通过之后再切换。这个过程可以在不影响旧容器的前提下完成,风险可控。直接覆盖数据目录做升级,除非你对版本兼容性有十足把握,否则不要碰。
5. 编排与可观测性:把单容器变成可运维服务
单条docker run命令能跑通,但这种东西没法写进文档交给别人维护。参数太长记不住、改一个参数要重敲一遍、容器异常退出没人知道。要变成真正可运维的服务,得补上编排、健康检查、监控这几块。
5.1 docker-compose 模板与启动顺序
编排我一般用 docker compose,原因很简单:配置文件可版本管理,参数一目了然,团队里谁都能看懂。
version: "3.8" services: kingbase: image: kingbase:v8r6 container_name: kingbase restart: always ports: - "54321:54321" environment: TZ: Asia/Shanghai DB_MODE: oracle DB_PASSWORD: ${KB_PASSWORD} volumes: - /data/kingbase/data:/opt/Kingbase/ES/V8/data - /data/kingbase/license/license.dat:/opt/Kingbase/license.dat:ro - /data/kingbase/backup:/backup - /data/kingbase/log:/opt/Kingbase/ES/V8/data/log shm_size: 2gb ulimits: nofile: soft: 65536 hard: 65536 deploy: resources: limits: memory: 8G cpus: "4.0" healthcheck: test: ["CMD-SHELL", "ksql -h 127.0.0.1 -p 54321 -U system -d test -tAc 'select 1' >/dev/null 2>&1 || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 120s logging: driver: json-file options: max-size: "100m" max-file: "5"几个点值得单独拎出来。密码用环境变量${KB_PASSWORD}从.env文件读,别硬编码在 compose 文件里,这文件是要进版本库的。start_period给 120 秒是有意的,数据库启动慢,给短了健康检查会在启动过程中就把容器判死。logging那段限制日志大小,不加的话docker logs的日志文件会一直涨,几个月下来能把磁盘撑爆,这个问题特别隐蔽,等到发现的时候磁盘已经满了。
如果数据库前面还有应用容器,可以用depends_on控制顺序,但要知道它只保证容器启动顺序,不保证数据库就绪。真正的做法是在应用侧做重试,配合健康检查状态判断。
5.2 健康检查脚本:别只看端口通不通
很多人写健康检查就是nc -z localhost 54321,端口通就算健康。这个判断太粗了,实例在恢复中、在只读模式、在拒绝连接的状态下,端口照样是通的。
真正有意义的健康检查要能执行一条 SQL。用ksql加select 1是最简单有效的办法:
#!/bin/bash # healthcheck.sh export PGPASSWORD="${DB_PASSWORD}" ksql -h 127.0.0.1 -p 54321 -U system -d test \ -tAc "select 1" 2>/dev/null | grep -q '^1$'要检查得更细一点,可以看看实例是不是可写:
#!/bin/bash # 检查是否可写,只读模式下返回非 0 export PGPASSWORD="${DB_PASSWORD}" ksql -h 127.0.0.1 -p 54321 -U system -d test -tAc \ "select sys_is_in_recovery()" 2>/dev/null | grep -q 'f's -is_in_recovery返回 false 表示当前是主库可写状态,返回 true 表示在恢复中是只读的。备库场景下这个判断反着写。
密码这块要提一句,ksql读环境变量PGPASSWORD,人大金仓兼容这个变量名。有的版本也认KINGBASE_PASSWORD,不确定的话两个都设上,反正不影响。
健康检查脚本放进容器的方式有两种,构建时 copy 进去,或者挂载进去。挂载更灵活,改脚本不用重新构建镜像。
5.3 日志采集与关键监控指标
日志我前面说了要单独挂出来。挂出来之后,采集就简单了,宿主机上用 filebeat 或者 fluentd 直接读文件就行,不用去折腾 Docker 的日志驱动。
数据库日志的配置项:
-- 开启连接日志和慢查询日志 ALTER SYSTEM SET log_connections = on; ALTER SYSTEM SET log_disconnections = on; ALTER SYSTEM SET log_min_duration_statement = '1s'; ALTER SYSTEM SET log_checkpoints = on; ALTER SYSTEM SET log_line_prefix = '%m [%p] %u@%d '; SELECT sys_reload_conf();log_min_duration_statement = '1s'表示超过 1 秒的语句都记下来,这是排查性能问题的第一手资料。log_line_prefix里带上时间、进程号、用户、数据库名,日志可读性会好很多。
监控指标这块,容器环境要额外关注几个:
| 指标 | 采集方式 | 关注点 |
|---|---|---|
| 容器内存使用率 | docker stats | 接近限额说明要调参或扩容 |
| 容器 CPU 使用率 | docker stats | 持续打满要查慢 SQL |
| 共享内存使用 | 容器内 ipcs -m | 接近 shm-size 要提前扩 |
| 连接数 | select count(*) from sys_stat_activity | 接近 max_connections 有风险 |
| 数据目录大小 | du -sh | 增长趋势判断容量规划 |
| 备份成功率 | 备份脚本退出码 | 每个备份都要验证可恢复 |
连接数这个指标特别容易被忽略。容器部署之后,应用的连接池配置经常跟数据库的max_connections对不上,应用配了 200 个连接,数据库只允许 100,高峰期就大量报连接失败。这类问题在容器化之前也一样存在,但容器化之后排查路径变长,更容易被误判成容器的问题。
6. 五个真实故障的排查链路复盘
下面这几个问题都是我在实际部署过程中真真切切遇到过的,把排查过程完整写出来,比直接给答案有用。
6.1 容器起来几秒就退出
docker ps看不到容器,docker ps -a显示状态是 Exited (1)。
第一步看日志:docker logs kingbase。如果日志是空的,说明镜像的入口脚本在执行之前就挂了。
第二步看退出码。退出码 127 是命令找不到,常见于自建镜像时 CMD 路径写错;退出码 1 是通用失败,得看日志;退出码 137 是被 SIGKILL,通常是 OOM,用docker inspect kingbase --format '{{.State.OOMKilled}}'确认。
我遇到的一次是日志里反复出现 "data directory exists but is not empty"。原因是之前挂载的目录里已经有半初始化的数据,新容器启动时发现目录不空,拒绝初始化。处理办法是确认数据没用之后清空目录重来,或者把initdb的跳过检查参数打开。但后者要小心,数据目录状态不确定的情况下强行启动,可能起一个损坏的实例。
还有一次是 license 文件权限问题。license.dat挂进去的时候是:ro只读,但容器内用户对它的权限是 644 只有 root 可写,金仓启动时要去校验 license 里的某些信息,读不了直接退出。日志里有一行很不起眼的 permission denied,不注意就漏了。
6.2 宿主机能连、外部连不上
容器里ksql能连,宿主机ksql -h 127.0.0.1也能连,但从另一台机器连不上。
排查顺序是这样的:先在宿主机上用ss -lntp | grep 54321确认端口确实在监听,且监听地址是0.0.0.0而不是127.0.0.1。如果这里显示的是127.0.0.1:54321,说明 Docker 的端口映射只绑了本地回环,-p 127.0.0.1:54321:54321这种写法就会这样,改成-p 54321:54321就好。
端口监听正常但还是连不上,那就查防火墙。宿主机上的iptables或者firewalld可能把 54321 拦了。注意 Docker 会自己往 iptables 里插规则,有时候跟系统原有的防火墙规则冲突,表现就是端口映射看起来配了,实际流量进不来。
再往后就是数据库自己的sys_hba.conf和listen_addresses。这两个都在数据库层面,前面网络层面通了才能轮到它们。
我记得有一次排查了很久,最后发现是云厂商的安全组没放行 54321。这种基础设施层面的东西,排查的时候要放在最前面确认,别一头扎进容器里查。
6.3 中文写入变问号
应用写进去的中文,查出来全是???或者乱码。
第一步确认数据库编码:
show server_encoding; show client_encoding;server_encoding是 UTF8 就没问题,如果是 SQL_ASCII 或者 LATIN1,那就是初始化的时候没指定编码,只能重建库。
服务端编码没问题,那问题在客户端。client_encoding要跟服务端一致,应用连接串里如果没有显式指定,可能用了默认值。JDBC 的连接串加?charSet=UTF-8,其他语言也有对应的参数。
还有一层容易忽略:容器内的 locale。locale命令输出如果是POSIX,某些工具在转换字符的时候会出问题。这个在纯 UTF8 数据流下一般不体现,但涉及到initdb时的--locale参数就有影响。
我遇到过一次特别隐蔽的,数据本身没问题,是终端的问题。docker exec -it进去之后终端编码不对,ksql查出来中文显示乱码,实际数据是好的。这种时候把查询结果重定向到文件再cat,就能判断到底是数据坏了还是显示问题。
6.4 并发一上来就报共享内存错误
压测的时候,几十个并发上去,日志里开始刷:
ERROR: could not resize shared memory segment to ... : No space left on device前面提过这个报错,这里说完整排查链路。
第一步排除磁盘:df -h看所有挂载点,确认没有满的。第二步看共享内存:df -h /dev/shm,容器里这个值默认是 64M。第三步对比数据库的shared_buffers:show shared_buffers,如果显示 1GB 而/dev/shm只有 64M,问题就明确了。
解决方案是启动容器时加--shm-size,或者 compose 里写shm_size。改这个参数需要重建容器,数据在挂载卷里所以不会丢。
调整的时候有个取舍:shm-size给多大合适。它和shared_buffers不是一比一的关系,因为还有各种临时共享内存段、并行查询的工作内存。我一般给shared_buffers的 2 倍,同时不超过容器内存限额的 50%。给得太大会挤占文件缓存的空间,反而拖慢查询。
6.5 挂载宿主机目录后权限报错
容器启动失败,日志里是could not open directory "data": Permission denied或者initdb: could not change permissions of directory。
根因就一个:宿主机目录的属主和容器内进程的 UID 对不上。排查方法:
# 看容器内用户的 UID docker run --rm kingbase:v8r6 id # 输出类似 uid=1000(kingbase) gid=1000(kingbase) # 看宿主机目录的属主 ls -lnd /data/kingbase/data # 输出类似 drwx------ 2 root root 4096 ...两个 UID 一比就清楚了。chown -R 1000:1000 /data/kingbase解决。
还有一种情况是 SELinux 或者 AppArmor 拦的。CentOS 系默认开 SELinux,挂载目录如果没有正确的安全上下文标签,容器读写会被拒。临时验证可以setenforce 0,长期方案是给目录打上svirt_sandbox_file_t标签:
chcon -Rt svirt_sandbox_file_t /data/kingbase这个坑的迷惑之处在于,权限用ls -l看着完全正常,文件对所有人可读写,但就是进不去。因为 SELinux 是在权限位之外又加了一层,ls看不出来。判断方法是getenforce,如果是 Enforcing 且关了之后就正常,那就是它。
7. 上线前的参数与安全收口
环境跑通、数据能读能写、备份能做完,还不算完。上线前有些默认配置必须改,有些安全边界必须划清楚。
7.1 必须改的几个默认配置
max_connections默认一般是 100,这个值对生产来说经常不够用。但也不是越大越好,每个连接都要占内存,max_connections乘以每连接的基础开销,很容易就把内存吃满。我的做法是根据应用连接池的总和来定,留 20% 的余量给运维和监控连接。
shared_buffers默认值在容器环境下通常偏小。容器内存 8G 的话,shared_buffers给 2G 是合理起点。这个参数改了要重启实例,所以上线前就得定好。
work_mem前面说过,先保守后调整。maintenance_work_mem可以给大一点,影响的是建索引、vacuum 这类操作的效率,不常触发,给 512M 到 1G 都行。
effective_cache_size这个参数不占实际内存,只是给优化器一个提示,告诉它操作系统层面大概有多少缓存可用。设成容器内存的 50% 到 75%,优化器会选择更合理的执行计划。
ALTER SYSTEM SET max_connections = 300; ALTER SYSTEM SET shared_buffers = '2GB'; ALTER SYSTEM SET work_mem = '8MB'; ALTER SYSTEM SET maintenance_work_mem = '512MB'; ALTER SYSTEM SET effective_cache_size = '6GB'; SELECT sys_reload_conf();注意shared_buffers这类需要重启的参数,sys_reload_conf()是不生效的,得sys_ctl restart。改完用show命令确认一下。
还有password_encryption,默认可能是 md5,建议改成scram-sha-256,安全性更好。改完之后新建的用户和改过的密码才会用新算法,老密码需要重新设一遍。
7.2 容器环境的资源限制与稳定性
资源限制这事,配得松了没意义,配得紧了数据库直接被杀。
--memory这个限额要考虑到数据库的内存使用不是线性的。shared_buffers是启动时就分配的,但排序、哈希表、连接上下文都是运行时动态分配的。所以限额要留足余量,一般设成shared_buffers的 2.5 到 3 倍。给 8G 限额,shared_buffers2G,是比较舒适的比例。
--cpus设置要注意,数据库是能利用多核的,但也不是核越多越快。4 到 8 核对中小规模场景足够,给太多核反而会因为上下文切换带来额外开销。
--restart always配上,但要意识到一个风险:如果数据库因为数据损坏起不来,always策略会让它无限重启,日志刷屏,磁盘被日志撑爆。所以我把日志大小限制和重启次数限制一起配上:
restart: on-failure:5失败 5 次就不再重启,留出人工介入的窗口。这个比always更适合数据库这种需要人看的服务。
最后是容器本身的稳定性。Docker 服务本身也会挂,systemctl status docker要纳入监控。另外定期docker system prune清理无用的镜像和停止的容器,这些垃圾积累起来能占几十 G 磁盘。但清理命令要慎用,prune不加参数会删掉所有未被使用的镜像,包括你精心保存的离线镜像包,加--filter限定范围更安全。
我在实际使用中的体会是,容器化部署数据库这件事,技术难度其实不高,难的是把那些"看不见的默认值"一个个揪出来改成适合生产的配置。上面列的这些参数,每一个背后都有对应的故障场景,配的时候知道为什么,出问题的时候才知道去哪查。真正上线之前,建议在测试环境用真实的并发压一遍,把docker stats、慢查询日志、数据库日志同时盯着,压测跑通了,参数也就调得差不多了。