目 录CONTENT

文章目录

【GaussDB】把GaussDB开发者版本做成88MB的docker镜像

DarkAthena
2026-11-02 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

【GaussDB】把华为云公开下载的GaussDB开发者版本做成88MB的docker镜像

背景

2026年9月中旬,华为云总算是把GaussDB的安装介质提供了公开下载,不过需要注意的是,这次提供的安装包并非企业版,而是开发者版本:

版本号为:GaussDB_V2.0-10.0.0_trial

内核版本为:Kernel 507.0.T1 build 7b258e66

官方下载页面:https://www.huaweicloud.com/intl/zh-cn/product/gaussdb/download.html

image-kMPY.png

这算是大放送了,不同操作系统加CPU架构,一共提供了12个安装包。

我大概比较了下guc参数和一些内置视图的情况,个人判断这个版本应该和企业版的507.0.1比较接近,但是gaussdb这个二进制文件的大小还是略有偏差,暂不好判断内核功能是否真的完全一致。不过对于开发者来说,有了这个版本,总算能去正经地去给GaussDB做应用软件适配了,不需要自己花钱买云上的GaussDB服务,也不需要先自己装个openGauss搞一遍,然后再到客户环境重新适配一次GaussDB(虽然不过但是大概率由于版本差异,还是得测一遍)。

我之前有做过GaussDB的容器版本,但是只提供了构建脚本,而且由于GaussDB的安装包当时华为并没有公开,所以我也没有把docker镜像放出来。

现在有公开下载的安装包了,所以完全可以写个一键构建脚本。

这种活我就不自己来了,直接给deepseek-flash一台kylinv10服务器,让它自己去干了。

注:本文提供的方式不是官方的安装方式,只是平掉个人之前的一个意难平。如果想正经安装一次,建议还是遵循官方文档的操作 https://doc.hcs.huawei.com/db/zh-cn/gaussdbdev/26.861.0/install-dev/gaussdb-04-0001.html


ME:

https://www.darkathena.top/archives/docker-build-kylinv10sp3-image-includ-gaussdb-runtime
https://www.darkathena.top/archives/build-a-docker-image-for-gaussdb
参考这几篇文章,用GaussDB开发者版本,基于kylinv10sp3做一个最小化的GaussDB开发者版本的docker镜像,不需要om那些组件
你可以使用192.168.1.80 这台机器进行开发构建,最后给我完整的构建脚本,数据库安装包在 https://dbs-download.obs.cn-north-1.myhuaweicloud.com/GaussDB/1789441455696/GaussDB_V2.0-10.0.0_trial_Euler_X86_20260915102549.zip https://dbs-download.obs.cn-north-1.myhuaweicloud.com/GaussDB/1789441436677/GaussDB_V2.0-10.0.0_trial_Euler_ARM_20260915102549.zip ,另外,确认一下用gs_om安装时,默认参数是怎么配置的,和我之前entrypoint.sh脚本里507版本的有什么差异


README

GaussDB 开发者版最小化 Docker 镜像(Kylin V10 SP3,x86_64 / aarch64 自适应)

基于你之前两篇文章的思路重做,产物是单机集中式内核,不含 om / cm / etcd / gns / gs_roach。构建脚本会按构建机架构自动选择安装包与镜像 tag。

项值
数据库版本GaussDB V2.0-10.0.0 trial,Kernel 507.0.T1 build 7b258e66(2026-09-14 编译)
内核包GaussDB-Kernel_507.0.T1_Euler_64bit.bin(发布包里的 Server 组件,x86/ARM 同名,内容架构不同)
基础系统Kylin V10 SP3,glibc 2.28,dnf --installroot 自建(架构跟随构建机)
镜像gaussdb-dev:10.0.0-507.0T1-x86_64,未压缩 399MB,xz 压缩 88MB
系统层105MB(你之前文章里自建的是 191MB,DockerHub 那个是 609MB)
数据目录/opt/gaussdb/data,属主 1000:1000
构建机192.168.1.80(kylinv10sp3-node1,x86_64,docker 27.0.3);ARM 版需在同架构 Kylin 机器上构建

架构自适应(x86_64 / aarch64)

脚本通过 uname -m 自动判断,无需改任何配置:

x86_64aarch64
安装包目录Euler2.10_X86_64Euler2.10_arm_64
Server 组件..._Server_X86_Developer.tar.gz..._Server_ARM_Developer.tar.gz
内核 .bin96MB / x86-6486MB / ARM aarch64
镜像 tag...-x86_64...-aarch64
numa_distribute_mode不设all(entrypoint 内按 uname -m 追加)
工作目录$WORK/x86_64$WORK/aarch64

几个保障点:

  • 内层 tar 与 .bin 文件名两架构完全同名(GaussDB_V2.0-10.0.0_trial_Euler_64bit_Server.tar.gz),所以脚本全程用 find 动态定位,不硬编码路径。
  • 解压后校验 file bin/gaussdb 的架构,拿错包会直接报 内核包架构与目标架构不符。
  • dnf --installroot 不能跨架构构建,脚本检测到 ARCH 与 uname -m 不符会立即退出并提示。
  • 构建末尾自动冒烟自检(镜像内执行 gaussdb --version),架构错配或缺系统库会当场暴露。
  • 可选包 libstdc++ / libatomic / libgcc 按当前架构仓库可用性动态决定装不装(ARM 包的 dependency/lib 比 x86 少一个 libstdc++,补上更稳)。

ARM 机器上的用法完全一样:

./build_gaussdb_dev_image.sh            # 自动识别 aarch64
# 或强制指定(调试用)
ARCH=aarch64 ./build_gaussdb_dev_image.sh

交付物

文件说明
build_gaussdb_dev_image.sh一键构建脚本(entrypoint 内嵌,单文件即可交付,x86/ARM 通用)
机器上 gaussdb-dev-10.0.0-507.0T1-*.tar.xz已导出的镜像包,88MB

关键发现(比文章方案多踩的坑)

  1. Euler 内核能在 Kylin 上跑。trial 包只有 Euler_X86,一开始担心 glibc 版本不匹配。实测 strings bin/gaussdb | grep GLIBC_ 最高只到 GLIBC_2.28,正好等于 Kylin V10 SP3 的 glibc,直接在宿主机 gaussdb --version 就跑通了。所以不需要为它准备 Euler 基础镜像。
  2. Kernel 507 的认证文件改名了:pg_hba.conf → gs_hba.conf。文章里 gs_guc set -Z hba ... 的写法在 507 上直接报 unrecognized node type "hba",正确写法是去掉 -Z:
    gs_guc set -D $PGDATA -h "host all all 0.0.0.0/0 md5"
  3. 不需要 util-linux。文章里为了 su/useradd 装了 util-linux,多出约 50MB。本方案用 Docker 原生 USER 1000:1000 降权 + 手写 /etc/passwd,整个 util-linux/pam/shadow-utils 都不用装。
  4. locale-archive 可以直接删,不用 localedef 裁剪。rootfs 里已有独立的 en_US.utf8/zh_CN.utf8/C.utf8 目录形式,glibc 会回退使用,locale -a 正常。省掉 206MB,也省掉构建阶段那次 build-locale-archive。
  5. dnf remove 不要用。Kylin 源里 coreutils 依赖 gmp,删 mpfr 会连带删掉 gmp→coreutils→head/ls 全没。而 dnf install 又会被弱依赖强行带回 python3/tcl/sqlite(rpm -q --whatrequires 显示没有任何包依赖它们)。所以脚本改成「一次装齐 + 手工删文件」,删完直接 rm -rf /var/lib/rpm。
  6. 单层 docker import 比 docker build 更省。多阶段/多层构建里被 rm 掉的文件仍留在底层,docker save 时照样占体积。单层导入后,399MB 压到 88MB(≈内核包本身的 92MB),这就是压缩包比文章 146MB 小 40% 的原因。
  7. date '2026-11-02' 会报错(invalid value "26-11-02" for "MON")。这是 507 内核对方言下日期字面量的解析行为,datestyle 确实是 ISO, MDY,与镜像无关;用 to_date('2026-11-02','YYYY-MM-DD') 或 '2026-11-02'::date 正常。
  8. sh: more: command not found(长输出翻页报错)。第 3 条不装 util-linux 的副作用:gsql 在交互式终端下输出超过一屏时会调 $PAGER,而 PAGER 未设置时的内置默认值是 more,more 属于 util-linux,被裁掉了,于是出现:
    gaussdb=# show behavior_compat_options;
    sh: more: command not found        <-- gsql 用 sh -c 起的 pager
    
    注意这是被 gsql 自己起的 sh 打出来的,不是你在 shell 里敲的命令有问题;非交互(管道/重定向/-c 且非 tty)时 gsql 不会调 pager,所以脚本里从不复现。
    实测 507 的 gsql 二进制里只认 PAGER(grep -a 只捞到 1 处 PAGER、9 处 LESS,没有 PSQL_PAGER/GSQL_PAGER,与它基于老版 psql 代码一致),且它会自行 setenv("LESS","FRX")。
    修法二选一:装 more(+util-linux ~50MB,不划算)或装 less 并 ENV PAGER=less(+190KB)。本镜像用后者,docker import 时带 ENV PAGER=less + ENV LESS=FRX;docker exec 会继承镜像 ENV,docker run -e PAGER=cat 可临时关掉分页。
    顺带补了 terminfo:原裁剪规则只留 a/d/l/s/v/x 六个目录,把 t/ 删了,tmux 用户(TERM=tmux-256color)进来会报 unknown terminal,现已保留 t/(+572KB)。

系统包清单是怎么定出来的

不是猜的。把解压后 bin/* 和 lib/*.so* 全部用 LD_LIBRARY_PATH=lib 跑一遍 ldd,聚合所有落到 /usr、/lib 的系统 so,再用 rpm -qf(readlink -f 解开符号链接,避免误判成 -devel 包)反查归属,最终只有 6 个包:

glibc  keyutils-libs  libaio  libuuid  libxcrypt  ncurses-libs

libssl/libcrypto/libcurl/libxml2/libkrb5/libz/libzstd 等全部由内核自带 $GAUSSHOME/lib 提供,不用装。ldd 里出现的 libcom_err/libgssapi_krb5 之类都是被自带库满足的,别被误导。

用法

# 构建(在对应架构的 Kylin 机器上,root)
cd /data/gaussdb-mini/build
./build_gaussdb_dev_image.sh            # 全流程,约 40 秒(不含首次 initdb)
./build_gaussdb_dev_image.sh base       # 只重建最小系统 rootfs
./build_gaussdb_dev_image.sh clean      # 清理中间产物

# 下面示例用 x86_64 的 tag,ARM 机器上把后缀换成 -aarch64
IMG=gaussdb-dev:10.0.0-507.0T1-x86_64

# 启动(首启约 60 秒做 initdb,之后 6 秒内可连)
docker run -d --name gaussdb --memory=4g -p 8000:5432 $IMG

# 连接
docker exec -it gaussdb gsql -d postgres
docker exec -it gaussdb gsql -d postgres -U admin -WGaussdb@123 -h 127.0.0.1 -p 5432

# 持久化(用命名卷,属主会继承,不用手动 chown)
docker run -d --name gaussdb --memory=4g -p 8000:5432 \
  -v gsdata:/opt/gaussdb/data $IMG

# 导出/导入
docker save $IMG | xz -T0 -6 > gaussdb-dev.tar.xz
docker load < gaussdb-dev.tar.xz

可用环境变量

变量默认说明
GS_PASSWORDGaussdb@123initdb 口令 + admin 口令
GS_ADMIN_USERadmin首启创建的系统管理员
GS_PORT5432库监听端口
GS_MAX_PROCESS_MEMORY_MB自动容器内存 ×0.665(对齐 gs_om 公式),限 2048~32768
GS_SHARED_BUFFERS_MB自动容器内存 ÷4,上限 1024(对齐 gs_om 单机模板 1GB),下限 128

内存参数按容器 cgroup 上限自动算:--memory=4g → max_process_memory=2723MB / shared_buffers=1024MB,不必手改。建议至少给 4G。

初始化参数(来源:gs_om 单机模板 + TPOPS 507 分支)

参数不是拍脑袋定的,冲突项按来源取舍:标 gs_om 的取 guc_ci_centralized_list_8c_32g.xml(gs_om 装单机时的默认模板),标 507 的取 TPOPS entrypoint 里 507 分支的值。

参数值来源
max_process_memory容器内存×0.665gs_om(原为公式 PHYSIC_MEMORY*0.665/(N+MAX_MASTER_DATANUM_IN_ONENODE))
shared_buffers1GB(小容器按 25% 缩)gs_om
max_connections1024gs_om
maintenance_work_mem64MBgs_om
max_prepared_transactions800gs_om
effective_cache_size512MBgs_om
timezone / log_timezonePRC / PRCgs_om
wal_buffers256MB507
work_mem64MB507
ssloff507
session_timeout0507
log_min_duration_statement100ms507
thread_pool_attr16,2,(nobind)507
password_encryption_type1(md5)507
a_format_version / a_format_dev_version10c / s6507
behavior_compat_options33 项(实测生效 33/33)507
enable_alarmoff容器内无 snas 告警组件
password_policy0开发者便利(内核默认 1)

几个和原 TPOPS 脚本不同的处理:

  • sql_beta_feature 去掉了 a_style_coerce(507 内核已不识别,设置后被静默丢弃),并去掉了值里的空格。
  • 原脚本的版本判断 [ "$v" == "507.0.0" ] 对 trial 版(507.0.T1)不成立,会导致 507 专属参数整块跳过;本镜像直接固定为 507 参数集,不存在这个问题。
  • 单机无意义的 dcf_run_mode、dcf_rep_append_thread_num、most_available_sync、standby_shared_buffers_fraction 未设置。

裁剪清单

从内核包里删掉的(都是集群/备份/服务发现类,单机跑SQL用不到):

类别文件省
DCF 元数据存储bin/etcd、bin/etcdctl、lib/libetcdapi.so.164M
服务发现 GNSbin/gns_ctl、bin/gns_server、gns_control.sh34M
备份 Roachbin/gs_roach7.6M
DCF 工具bin/dcf_tool2.6M
应急/黑匣子/性能服务gs_rescue、gs_rescue_tui、gs_blackbox、gs_perf_service~4M
前端资源/杂项echarts*.js、dbe_stats_tools.py、krb5 服务端~3M
AI/Python 扩展libxgboost.so、libpyzenith.so、pyzenith.so10M
告警配置bin/alarmItem.conf(trial 包自带解析缺陷,启动必报 Invalid data in AlarmItem file)0

保留了 libdcf.so、libremote.so(gaussdb 会 dlopen,删了有风险)、ecpg、gstrace、gs_dump/gs_restore、全部编码转换插件和 share/gaussdb(系统表定义,不能删)。

GAUSSHOME 从 405M 降到 287M,其中 bin/gaussdb 单文件就 157M,是压不下去的主体。

目录结构

镜像内
/opt/gaussdb            GAUSSHOME(属主 gaussdb)
├── bin/                gaussdb、gs_initdb、gs_ctl、gs_guc、gsql、gs_dump...
├── lib/                自带 so + lib/gaussdb/ 编码/插件
├── share/gaussdb/      系统表定义、SQL 脚本
├── data/               PGDATA(命名卷挂这里)
├── log/{pg_log,pg_audit,asp_data,pg_perf,sql_monitor}/dn_6001
└── entrypoint.sh

注意

  • 首启慢是正常的:initdb + 建 admin + 重启,约 60 秒。日志出现 [entrypoint] 启动 GaussDB 后再等几秒即可连。
  • bind mount 宿主机目录要 chown:镜像以 1000:1000 运行,挂宿主机目录时目录属主是 root 会写不进去,entrypoint 会直接报错并提示 chown -R 1000:1000。用 docker volume 命名卷没这个问题。
  • 启动日志里可能有 1 条 framework_wlmgr_cpuctrl_gscgroup.cpp 相关的 ERROR:容器内没有 cgroup 写权限,WLM 的 cgroup 初始化失败,不影响使用。如需彻底安静,可在脚本 entrypoint 的参数数组里补一条 -c "use_workload_manager = off" 后重建。
  • trial 版有 license 限制(单机、容量/时长),生产别用。
  • 想在容器里装东西(python 等)需 docker exec -u 0,镜像内没有包管理器和 yum 源。
  • ARM 上若端口映射不通(你之前文章里提过 docker 18 在 arm64 上映射不出端口),先用 --net=host 绕过,库直接监听宿主 5432。

一键构建脚本

build_gaussdb_dev_image.sh

#!/bin/bash
###############################################################
# GaussDB 开发者版 最小化 Docker 镜像 一键构建脚本(x86_64 / aarch64 自适应)
#
#   基础系统 : Kylin V10 SP3(架构跟随构建机)—— dnf --installroot 生成
#   数据库   : GaussDB V2.0-10.0.0 trial (Kernel 507.0.T1)
#   组件范围 : 仅单机集中式内核,不含 om / cm / etcd / gns / gs_roach
#
# 构建机要求: Kylin V10 SP3(x86_64 或 aarch64)/ root / docker / 可访问 Kylin yum 源
# 用法:  ./build_gaussdb_dev_image.sh           # 全流程
#        ./build_gaussdb_dev_image.sh base      # 只做最小系统 rootfs
#        ./build_gaussdb_dev_image.sh kernel    # 只下载解出内核 .bin
#        ./build_gaussdb_dev_image.sh image     # 只装内核并打包镜像
#        ./build_gaussdb_dev_image.sh clean     # 清理中间产物
#        ARCH=aarch64 ./build_gaussdb_dev_image.sh   # 强制指定架构(默认取 uname -m)
###############################################################
set -euo pipefail

#========================= 架构检测 =========================
# 用 ARCH 环境变量可强制指定;默认取 uname -m
ARCH_RAW="${ARCH:-$(uname -m)}"
case "$ARCH_RAW" in
  x86_64|amd64)          ARCH="x86_64"  ;;
  aarch64|arm64|armv8*)  ARCH="aarch64" ;;
  *) echo "[ERROR] 不支持的架构: $ARCH_RAW(仅支持 x86_64 / aarch64)" >&2; exit 1 ;;
esac

# 官方 trial 包下载地址(可用 PKG_URL 环境变量覆盖)
PKG_URL_X86="https://dbs-download.obs.cn-north-1.myhuaweicloud.com/GaussDB/1789441455696/GaussDB_V2.0-10.0.0_trial_Euler_X86_20260915102549.zip"
PKG_URL_ARM="https://dbs-download.obs.cn-north-1.myhuaweicloud.com/GaussDB/1789441436677/GaussDB_V2.0-10.0.0_trial_Euler_ARM_20260915102549.zip"

case "$ARCH" in
  aarch64) DEFAULT_PKG_URL="$PKG_URL_ARM" ;;
  *)       DEFAULT_PKG_URL="$PKG_URL_X86" ;;
esac
PKG_URL="${PKG_URL:-$DEFAULT_PKG_URL}"

# 按架构分目录,避免同机先后构建两种架构时互相污染
WORK="${WORK:-/data/gaussdb-mini/build}/$ARCH"
IMAGE_NAME="${IMAGE_NAME:-gaussdb-dev}"
IMAGE_TAG="${IMAGE_TAG:-10.0.0-507.0T1-$ARCH}"
GAUSSHOME="/opt/gaussdb"          # 镜像内数据库安装目录
GS_UID=1000                        # 容器内 gaussdb 用户 uid
GS_GID=1000
RELEASEVER="${RELEASEVER:-10}"     # Kylin V10 -> 10

# 必需系统包(x86_64 依据 gaussdb 全部二进制 ldd 结果反查 rpm 得出,aarch64 同构)
# 注意保留 less:gsql 输出超过一屏时会调用 $PAGER 分页,未装 pager 会报
# "sh: more: command not found"。默认 pager 是 more(属 util-linux,约 50MB,已裁掉),
# 所以这里显式装 less(约 190KB)并在打包时把 PAGER 指到 less。
BASE_PKGS=(
  glibc glibc-common bash coreutils
  libaio libuuid libxcrypt keyutils-libs ncurses-libs
  procps-ng iproute findutils sed grep gawk
  zlib openssl-libs tar gzip less
)

# 可选系统包:不同架构的仓库覆盖情况不同,存在才装,缺失不报错
OPT_PKGS=(
  libstdc++ libatomic libgcc
)

ROOTFS="$WORK/rootfs"
KERNEL_BIN=""                      # 由 prepare_kernel 赋值

#========================= 工具函数 =========================
c_red()  { echo -e "\033[31m$*\033[0m"; }
c_grn()  { echo -e "\033[32m$*\033[0m"; }
c_ylw()  { echo -e "\033[33m$*\033[0m"; }
step()   { c_grn "\n===== $* ====="; }
die()    { c_red "[ERROR] $*"; exit 1; }

need_root() { [ "$(id -u)" = "0" ] || die "请用 root 执行"; }

# 下载 / 解出 GaussDB 内核自解压包(.bin)
prepare_kernel() {
  step "1/5 准备 GaussDB 内核二进制"
  mkdir -p "$WORK"
  cd "$WORK"

  if [ ! -f pkg.zip ]; then
    c_ylw "下载发布包: $PKG_URL"
    curl -fL --retry 3 -o pkg.zip "$PKG_URL" || die "下载失败"
  fi

  if [ ! -d pkg_unzip ]; then
    mkdir -p pkg_unzip
    unzip -q -o pkg.zip -d pkg_unzip || die "解压 zip 失败"
  fi

  # 发布包结构:
  #   <OS 目录>/server/<...>_Server_X86_Developer.tar.gz
  #     -> 内层 <...>_Server.tar.gz -> GaussDB-Kernel_*.bin (7z 自解压)
  if [ ! -d "$WORK/kernel" ]; then
    local srv_tar inner
    srv_tar=$(find pkg_unzip -type f -name "*Server*.tar.gz" | head -1)
    [ -n "$srv_tar" ] || die "发布包内未找到 Server 组件"
    mkdir -p ktmp kernel
    tar -xzf "$srv_tar" -C ktmp
    inner=$(find ktmp -type f -name "*_Server.tar.gz" | head -1)
    if [ -n "$inner" ]; then
      tar -xzf "$inner" -C kernel
    else
      inner=$(find ktmp -type f -name "GaussDB-Kernel*.bin" | head -1)
      [ -n "$inner" ] || die "未找到 GaussDB-Kernel-*.bin"
      cp "$inner" kernel/
    fi
    rm -rf ktmp
  fi

  KERNEL_BIN=$(find "$WORK/kernel" -maxdepth 1 -type f -name "GaussDB-Kernel*.bin" | head -1)
  [ -n "$KERNEL_BIN" ] || die "未找到 GaussDB-Kernel-*.bin"
  c_grn "内核包: $KERNEL_BIN"
  c_grn "版本  : $(head -1 "$WORK/kernel/version.cfg" 2>/dev/null || echo unknown)"
}

# 用 dnf --installroot 生成最小 Kylin V10 SP3 根文件系统
build_rootfs() {
  step "2/5 生成最小 Kylin V10 SP3 rootfs ($ARCH)"

  # dnf --installroot 不能跨架构构建,构建机架构必须与目标一致
  local host_arch
  host_arch=$(uname -m)
  case "$ARCH:$host_arch" in
    x86_64:x86_64|aarch64:aarch64) ;;
    *) die "目标架构 $ARCH 与构建机 $host_arch 不一致;dnf --installroot 无法跨架构构建,请在对应架构的 Kylin 机器上执行" ;;
  esac

  rm -rf "$ROOTFS"
  mkdir -p "$ROOTFS"
  cd "$WORK"

  dnf --installroot="$ROOTFS" install -y \
      --nodocs --nogpgcheck \
      --setopt=install_weak_deps=0 \
      --setopt=tsflags=nodocs \
      --releasever="$RELEASEVER" \
      --disablerepo=trzsz \
      "${BASE_PKGS[@]}" || die "dnf installroot 失败(请确认本机 Kylin yum 源可用)"

  # 可选包:只在当前架构仓库里存在时才装(如 aarch64 缺某些包不影响主流程)
  local avail=() p
  for p in "${OPT_PKGS[@]}"; do
    dnf -q info "$p" >/dev/null 2>&1 && avail+=("$p")
  done
  if [ ${#avail[@]} -gt 0 ]; then
    c_ylw "补充可选包: ${avail[*]}"
    dnf --installroot="$ROOTFS" install -y \
        --nodocs --nogpgcheck \
        --setopt=install_weak_deps=0 \
        --setopt=tsflags=nodocs \
        --releasever="$RELEASEVER" \
        --disablerepo=trzsz \
        "${avail[@]}" || c_ylw "可选包安装失败,已忽略"
  fi

  du -sh "$ROOTFS"
}

# 手工瘦身(不走 dnf remove,避免连带删除 coreutils/glibc 等基础包)
prune_rootfs() {
  step "3/5 裁剪 rootfs"
  local R="$ROOTFS"

  # 文档、帮助、翻译、许可证
  rm -rf "$R"/usr/share/{doc,man,info,locale,i18n,licenses,pki,python-wheels} 2>/dev/null || true

  # locale 归档 206MB —— 已有 en_US.utf8/zh_CN.utf8/C.utf8 独立目录形式,直接删
  rm -f "$R"/usr/lib/locale/locale-archive "$R"/usr/lib/locale/locale-archive.tmpl

  # 被 dnf 弱依赖顺带装入的 python3 / tcl(无其他包依赖它们)
  rm -rf "$R"/usr/lib/python3* "$R"/usr/lib64/python3* "$R"/usr/lib64/libpython3* \
         "$R"/usr/bin/python3* "$R"/usr/bin/pydoc* "$R"/usr/bin/pip3* 2>/dev/null || true
  rm -rf "$R"/usr/share/tcl8* "$R"/usr/lib64/libtcl8* "$R"/usr/lib64/libtk8* \
         "$R"/usr/bin/tclsh* "$R"/usr/bin/wish* 2>/dev/null || true

  # 缓存 / 日志 / rpm 数据库(运行镜像不需要)
  rm -rf "$R"/var/cache/* "$R"/var/log/* "$R"/var/lib/rpm "$R"/var/lib/dnf \
         "$R"/var/lib/yum "$R"/var/lib/systemd 2>/dev/null || true
  rm -f  "$R"/anaconda-post.log

  # 时区只保留 UTC + Asia/Shanghai
  local tz="$R/usr/share/zoneinfo"
  if [ -d "$tz" ]; then
    find "$tz" -mindepth 1 -maxdepth 1 \
      ! -name Asia ! -name UTC ! -name 'posix*' \
      ! -name zone.tab ! -name zone1970.tab ! -name iso3166.tab \
      -exec rm -rf {} + 2>/dev/null || true
    find "$tz/Asia" -mindepth 1 -maxdepth 1 ! -name Shanghai -exec rm -rf {} + 2>/dev/null || true
  fi

  # terminfo 只保留常用终端;t 保留 tmux-* 等条目(libevent 系终端复用器用户的 TERM
  # 多为 tmux-256color,缺失会让 gsql/less 报 unknown terminal)
  local ti="$R/usr/share/terminfo"
  if [ -d "$ti" ]; then
    find "$ti" -mindepth 1 -maxdepth 1 -type d \
      ! -name a ! -name d ! -name l ! -name s ! -name t ! -name v ! -name x -exec rm -rf {} + 2>/dev/null || true
  fi

  du -sh "$ROOTFS"
}

# 释放内核包 -> 装进 rootfs -> 裁掉集群/备份类组件
install_gaussdb() {
  step "4/5 安装 GaussDB 内核到 rootfs"
  # 单独执行 image 阶段时 KERNEL_BIN 尚未赋值,这里兜底查找
  if [ -z "$KERNEL_BIN" ]; then
    KERNEL_BIN=$(find "$WORK/kernel" -maxdepth 1 -type f -name "GaussDB-Kernel*.bin" 2>/dev/null | head -1)
  fi
  [ -n "$KERNEL_BIN" ] || die "未找到内核 bin,请先执行: $0 kernel"
  local X="$WORK/extract"
  rm -rf "$X"; mkdir -p "$X"
  cd "$X"
  cp "$KERNEL_BIN" .
  cp "$(dirname "$KERNEL_BIN")/$(basename "${KERNEL_BIN%.bin}").sha256" . 2>/dev/null || true
  chmod +x ./*.bin
  # 7z 自解压,释放出 GAUSSHOME 结构 (bin/lib/share/include/etc)
  ./*.bin >/dev/null 2>&1 || die "内核包自解压失败"
  [ -f bin/gaussdb ] || die "自解压后未见 bin/gaussdb"

  # 架构自检:防止拿错架构的内核包(x86 包 release 与 ARM 包目录名不同,容易下混)
  if [ "$ARCH" = "aarch64" ]; then
    file bin/gaussdb | grep -q "aarch64" || die "内核包架构与目标架构不符:期望 aarch64"
  else
    file bin/gaussdb | grep -q "x86-64" || die "内核包架构与目标架构不符:期望 x86_64"
  fi
  c_grn "内核架构校验通过: $(file -b bin/gaussdb | cut -d, -f2 | sed 's/^ //')"

  rm -rf "$ROOTFS$GAUSSHOME"
  mkdir -p "$ROOTFS$GAUSSHOME"
  ( cd "$X" && tar cf - bin lib share include etc ) | ( cd "$ROOTFS$GAUSSHOME" && tar xf - )

  # ---- 组件裁剪:去掉集群/备份/服务发现等,仅保留单机内核 ----
  cd "$ROOTFS$GAUSSHOME"
  local drop_bin=(
    etcd etcdctl                        # DCF 元数据存储
    gns_ctl gns_server gns_control.sh   # 服务发现(GNS)
    gs_roach                            # 备份恢复(Roach)
    dcf_tool                            # DCF 运维工具
    gs_rescue gs_rescue_tui             # 应急修复
    gs_blackbox gs_perf_service         # 黑匣子/性能服务
    echarts.min.js echarts.common.min.js
    dbe_stats_tools.py
    kadmin.local kdb5_util krb5kdc      # krb5 服务端
  )
  local f
  for f in "${drop_bin[@]}"; do rm -rf "bin/$f"; done

  # 库裁剪:etcd api / AI 推理 / python 存储过程 shim(gaussdb 主程序不依赖)
  rm -f lib/libetcdapi.so* lib/libxgboost.so lib/libpyzenith.so lib/pyzenith.so

  # trial 包自带 alarmItem.conf 有解析缺陷(启动必报 "Invalid data in AlarmItem file!
  # Read alarm English name failed"),且容器内没有 snas 告警组件、已设 enable_alarm=off,
  # 移除该文件可彻底消除启动日志噪音(实测不影响启动)
  rm -f bin/alarmItem.conf

  du -sh . bin lib
}

# 生成用户、目录、entrypoint,打包成单层镜像
pack_image() {
  step "5/5 配置并生成镜像"
  local R="$ROOTFS"

  # ---- 用户 ----
  grep -q "^gaussdb:" "$R/etc/passwd" || \
    echo "gaussdb:x:${GS_UID}:${GS_GID}:GaussDB:/home/gaussdb:/bin/bash" >> "$R/etc/passwd"
  grep -q "^gaussdb:" "$R/etc/group"  || echo "gaussdb:x:${GS_GID}:" >> "$R/etc/group"

  # ---- 目录 ----
  mkdir -p "$R/home/gaussdb" "$R/tmp" "$R/run" "$R$GAUSSHOME/data"
  mkdir -p "$R$GAUSSHOME"/log/{pg_log,pg_audit,asp_data,pg_perf,sql_monitor}/dn_6001

  # ---- 时区 ----
  ln -sf /usr/share/zoneinfo/Asia/Shanghai "$R/etc/localtime"

  # ---- entrypoint ----
  cat > "$R$GAUSSHOME/entrypoint.sh" <<'ENTRYPOINT_EOF'
#!/bin/bash
# GaussDB 容器入口:首启自动 initdb,之后直接拉起单机内核
set -euo pipefail

export GAUSSHOME="${GAUSSHOME:-/opt/gaussdb}"
export PGDATA="${PGDATA:-$GAUSSHOME/data}"
export GAUSSLOG="${GAUSSLOG:-$GAUSSHOME/log}"
export PGDATABASE="${PGDATABASE:-postgres}"
export PATH="$GAUSSHOME/bin:$PATH"
export LD_LIBRARY_PATH="$GAUSSHOME/lib:${LD_LIBRARY_PATH:-}"

GS_PORT="${GS_PORT:-5432}"
GS_PASSWORD="${GS_PASSWORD:-Gaussdb@123}"
GS_ADMIN_USER="${GS_ADMIN_USER:-admin}"
GS_NODE_NAME="${GS_NODE_NAME:-primary}"
# 可选:手工指定内存参数(MB),不设则按容器内存上限自动换算
GS_MAX_PROCESS_MEMORY_MB="${GS_MAX_PROCESS_MEMORY_MB:-}"
GS_SHARED_BUFFERS_MB="${GS_SHARED_BUFFERS_MB:-}"

log() { echo "[entrypoint] $*"; }

# 容器内存上限(MB):优先 cgroup limit,取不到则用 MemTotal
mem_limit_mb() {
  local lim="" val
  for f in /sys/fs/cgroup/memory.max /sys/fs/cgroup/memory/memory.limit_in_bytes; do
    [ -r "$f" ] || continue
    val=$(cat "$f" 2>/dev/null || true)
    case "$val" in ''|max|*[!0-9]*) continue;; esac
    # cgroup v1 未设限时是 9223372036854771712 这类天文数字
    if [ "$val" -lt 9223372036854771712 ] 2>/dev/null; then lim="$val"; break; fi
  done
  [ -n "$lim" ] || lim=$(( $(awk '/MemTotal/{print $2}' /proc/meminfo) * 1024 ))
  echo $(( lim / 1024 / 1024 ))
}

init_database() {
  local mem_mb max_proc_mb shared_mb
  mem_mb=$(mem_limit_mb)

  # max_process_memory: 对齐 gs_om 单机模板公式 PHYSIC_MEMORY*0.665/(N+MAX_MASTER_DATANUM_IN_ONENODE)
  # 单机分母按 1 计 -> *0.665;下限 2GB(内核要求),上限 32GB
  if [ -n "$GS_MAX_PROCESS_MEMORY_MB" ]; then
    max_proc_mb="$GS_MAX_PROCESS_MEMORY_MB"
  else
    max_proc_mb=$(( mem_mb * 665 / 1000 ))
    [ "$max_proc_mb" -gt 32768 ] && max_proc_mb=32768
    [ "$max_proc_mb" -lt 2048 ]  && max_proc_mb=2048
  fi

  # shared_buffers: 对齐 gs_om 单机模板 1GB,小容器按内存 25% 缩放
  if [ -n "$GS_SHARED_BUFFERS_MB" ]; then
    shared_mb="$GS_SHARED_BUFFERS_MB"
  else
    shared_mb=$(( mem_mb / 4 ))
    [ "$shared_mb" -gt 1024 ] && shared_mb=1024
    [ "$shared_mb" -lt 128 ]  && shared_mb=128
  fi

  log "初始化数据目录 (容器内存 ${mem_mb}MB -> max_process_memory=${max_proc_mb}MB, shared_buffers=${shared_mb}MB)"
  mkdir -p "$PGDATA"
  gs_initdb --pgdata="$PGDATA" \
            --nodename="$GS_NODE_NAME" \
            --pwpasswd="$GS_PASSWORD" \
            --encoding=UTF-8 \
            --locale=C || { log "initdb 失败"; exit 1; }

  log "写入初始化参数"
  local cargs=(
    # ---- 连接 ----
    -c "port = $GS_PORT"
    -c "listen_addresses = '*'"
    -c "local_bind_address = '0.0.0.0'"
    -c "max_connections = 1024"                    # gs_om 单机模板
    -c "max_prepared_transactions = 800"           # gs_om 单机模板
    # ---- 内存 ----
    -c "max_process_memory = ${max_proc_mb}MB"
    -c "shared_buffers = ${shared_mb}MB"
    -c "work_mem = 64MB"                           # 507 分支
    -c "maintenance_work_mem = 64MB"               # gs_om 单机模板
    -c "wal_buffers = 256MB"                       # 507 分支
    -c "effective_cache_size = 512MB"              # gs_om 单机模板
    # ---- WAL / 复制 ----
    -c "wal_level = logical"
    -c "max_wal_senders = 20"
    -c "max_replication_slots = 20"
    -c "enable_slot_log = on"
    -c "wal_keep_segments = 128"
    -c "checkpoint_segments = 1024"
    -c "xloginsert_locks = 16"
    -c "recovery_max_workers = 4"
    -c "recovery_time_target = 0"
    # ---- 线程池 ----
    -c "enable_thread_pool = on"
    -c "thread_pool_attr = '16,2,(nobind)'"        # 507 分支
    # ---- 兼容性(507 分支 TPOPS 参数加兼容性优化) ----
    -c "a_format_version = '10c'"
    -c "a_format_dev_version = 's6'"
    -c "behavior_compat_options = 'aformat_null_test,aformat_regexp_match,allow_procedure_compile_check,bind_procedure_searchpath,compat_cursor,convert_string_digit_to_numeric,correct_to_number,current_sysdate,display_leading_zero,dynamic_sql_compat,enable_bpcharlikebpchar_compare,enable_case_when_alias,enable_crosstype_integer_operator,enable_ora_joinop_in_updatestmt,enable_use_ora_timestamptz,end_month_calculate,forbid_package_function_with_prefix,forbid_skip_tableof_empty_str_elem,forbid_update_multi_same_tables,plsql_rollback_keep_user,plsql_security_definer,plstmt_implicit_savepoint,proc_implicit_for_loop_variable,proc_outparam_override,proc_outparam_transfer_length,rownum_type_compat,show_full_error_lineno,sys_function_without_brackets,tableof_elem_constraints,time_constexpr_compact,truncate_numeric_tail_zero,unbind_divide_bound,varray_compat'"
    -c "disable_keyword_options = 'datetime,regexp,rlike,zerofill,unit'"
    -c "cursor_sharing = off"
    -c "instr_unique_sql_combination_options = 'in_clause,forbid_select_for_update,temp_table'"
    -c "gs_format_behavior_compat_options = 'sqrt_karatsuba'"
    -c "ddl_invalid_mode = 'invalid'"
    -c "enable_force_create_obj = on"
    -c "character_set_client = 'UTF8'"
    -c "sql_beta_feature = 'sel_semi_poisson,sel_expr_instr,rand_cost_opt,param_path_opt,page_est_opt'"
    -c "explain_perf_mode = 'pretty'"
    -c "extra_float_digits = 3"
    # ---- 口令策略 ----
    -c "password_encryption_type = 1"             # 507 分支(md5)
    -c "password_policy = 0"
    -c "failed_login_attempts = 100"
    -c "password_lock_time = 1"
    -c "password_effect_time = 0"
    -c "password_reuse_time = 0"
    # ---- 超时 / 锁 ----
    -c "session_timeout = 0"                       # 507 分支
    -c "lockwait_timeout = 0"
    -c "update_lockwait_timeout = 0"
    -c "idle_in_transaction_timeout = 0"
    # ---- 日志 / 审计 ----
    -c "log_min_duration_statement = 100ms"        # 507 分支
    -c "log_statement = 'ddl'"
    -c "log_error_verbosity = 'verbose'"
    -c "log_line_prefix = '%m %n %u %d %h %p %S %x %a %e'"
    -c "timezone = 'PRC'"                          # gs_om 单机模板
    -c "log_timezone = 'PRC'"                      # gs_om 单机模板
    -c "track_activity_query_size = 40960"
    -c "instr_unique_sql_count = 200000"
    -c "system_view_version = 1"
    -c "verify_log_buffers = 16MB"
    -c "audit_resource_policy = off"
    -c "audit_file_remain_time = 180"
    -c "log_directory = '$GAUSSLOG/pg_log/dn_6001'"
    -c "audit_directory = '$GAUSSLOG/pg_audit/dn_6001'"
    -c "asp_log_directory = '$GAUSSLOG/asp_data/dn_6001'"
    -c "perf_directory = '$GAUSSLOG/pg_perf/dn_6001'"
    -c "query_log_directory = '$GAUSSLOG/sql_monitor/dn_6001'"
    # ---- 其他 ----
    -c "max_files_per_process = 1024"
    -c "max_stack_depth = 4MB"
    -c "max_compile_functions = 100000"
    -c "max_compile_packages = 2000"
    -c "max_concurrent_autonomous_transactions = 1000"
    -c "max_recursive_times = 1500"
    -c "undo_retention_time = 900"
    -c "enable_recyclebin = on"
    -c "enable_mergejoin = off"
    -c "enable_nestloop = off"
    -c "vacuum_cost_delay = 1"
    -c "vacuum_cost_limit = 1000"
    -c "ai_watchdog_oom_other_used_memory_threshold = -1"
    -c "enable_alarm = off"                        # 容器内无 snas 告警组件,关闭避免日志噪音
    -c "ssl = off"                                 # 507 分支
  )
  # ARM(aarch64) 上按 TPOPS 建议开启 NUMA 分布(x86 不设)
  if [ "$(uname -m)" = "aarch64" ]; then
    cargs+=(-c "numa_distribute_mode = 'all'")
  fi
  if ! gs_guc set -D "$PGDATA" "${cargs[@]}" >/tmp/gs_guc.log 2>&1; then
    log "WARN: 批量设置参数失败,改为逐项设置"
    local a
    for a in "${cargs[@]}"; do
      [ "$a" = "-c" ] && continue
      gs_guc set -D "$PGDATA" -c "$a" >/dev/null 2>&1 || log "WARN: 参数设置失败: $a"
    done
  fi

  # 放通外部连接(md5)。注意: Kernel 507 的认证文件是 gs_hba.conf,且 -h 不再需要 -Z hba
  gs_guc set -D "$PGDATA" -h "host all all 0.0.0.0/0 md5" >/dev/null 2>&1 \
    || log "WARN: hba 配置失败"

  log "启动数据库并创建管理员用户 ${GS_ADMIN_USER}"
  gs_ctl start -D "$PGDATA" -Z single_node -w -t 180 >/dev/null 2>&1 || true
  local i
  for i in $(seq 1 90); do
    gsql -d postgres -p "$GS_PORT" -c "select 1" >/dev/null 2>&1 && break
    sleep 1
  done
  gsql -d postgres -p "$GS_PORT" -c \
    "create user ${GS_ADMIN_USER} password '${GS_PASSWORD}' sysadmin monadmin;" >/dev/null 2>&1 \
    || log "WARN: 管理员用户创建失败(可能已存在)"
  gs_ctl stop -D "$PGDATA" -Z single_node -m fast >/dev/null 2>&1 || true
  log "初始化完成"
}

# 目录可写性检查(bind mount 宿主机目录时最常见的坑)
if [ ! -w "$GAUSSHOME" ]; then
  echo "[entrypoint] ERROR: $GAUSSHOME 不可写。" >&2
  echo "[entrypoint] 若用 -v 挂载了宿主机目录,请先 chown -R 1000:1000 <宿主机目录>" >&2
  exit 1
fi
if [ -d "$PGDATA" ] && [ ! -w "$PGDATA" ]; then
  echo "[entrypoint] ERROR: 数据目录 $PGDATA 不可写(通常是 bind mount 宿主机目录属主为 root)。" >&2
  echo "[entrypoint] 解决: chown -R 1000:1000 <宿主机目录>;或改用 docker volume 命名卷。" >&2
  exit 1
fi

mkdir -p "$GAUSSHOME/log"/{pg_log,pg_audit,asp_data,pg_perf,sql_monitor}/dn_6001 2>/dev/null || true

if [ ! -s "$PGDATA/PG_VERSION" ]; then
  init_database
else
  log "检测到已初始化数据目录,直接启动"
fi

log "启动 GaussDB (port=$GS_PORT, data=$PGDATA)"
exec gaussdb -D "$PGDATA"
ENTRYPOINT_EOF
  chmod 755 "$R$GAUSSHOME/entrypoint.sh"

  # ---- 权限(只在最后一层设置,避免镜像层体积翻倍)----
  chown -R "${GS_UID}:${GS_GID}" "$R$GAUSSHOME" "$R/home/gaussdb"
  chmod 1777 "$R/tmp"

  du -sh "$ROOTFS"

  # ---- 打包导入为单层镜像 ----
  c_ylw "打包 rootfs 并导入为 $IMAGE_NAME:$IMAGE_TAG ..."
  tar -C "$ROOTFS" --numeric-owner -cf - . \
    | docker import \
        --change 'ENV LANG=en_US.UTF-8' \
        --change 'ENV LC_ALL=en_US.UTF-8' \
        --change 'ENV TZ=Asia/Shanghai' \
        --change 'ENV PAGER=less' \
        --change 'ENV LESS=FRX' \
        --change "ENV GAUSSHOME=$GAUSSHOME" \
        --change "ENV PGDATA=$GAUSSHOME/data" \
        --change "ENV GAUSSLOG=$GAUSSHOME/log" \
        --change 'ENV PGDATABASE=postgres' \
        --change "ENV LD_LIBRARY_PATH=$GAUSSHOME/lib" \
        --change "ENV PATH=$GAUSSHOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
        --change 'EXPOSE 5432' \
        --change "USER ${GS_UID}:${GS_GID}" \
        --change 'WORKDIR /home/gaussdb' \
        --change "ENTRYPOINT [\"$GAUSSHOME/entrypoint.sh\"]" \
        - "$IMAGE_NAME:$IMAGE_TAG" | tail -1

  # 冒烟自检:确认内核能在该镜像内执行(架构错配或缺系统库会在这里暴露)
  c_ylw "冒烟自检:镜像内执行 gaussdb --version ..."
  if docker run --rm --entrypoint /bin/bash "$IMAGE_NAME:$IMAGE_TAG" -c 'gaussdb --version' >/tmp/gs_smoke.log 2>&1; then
    c_grn "冒烟自检通过: $(tr -d '\n' < /tmp/gs_smoke.log)"
  else
    c_red "冒烟自检失败(内核查不到或缺少系统库),请检查:"
    sed 's/^/    /' /tmp/gs_smoke.log | head -20
  fi

  # 分页器自检:$PAGER 必须能解析到可执行文件,否则 gsql 输出超一屏会报
  # "sh: more: command not found"
  local pager_in_img
  pager_in_img=$(docker run --rm --entrypoint /bin/bash "$IMAGE_NAME:$IMAGE_TAG" \
                   -c 'printf "%s->%s" "${PAGER:-unset}" "$(command -v "${PAGER:-more}" || echo MISSING)"' 2>/dev/null)
  if printf '%s' "$pager_in_img" | grep -q 'MISSING'; then
    c_red "分页器缺失:镜像内 PAGER=$pager_in_img,gsql 长输出会报 command not found"
  else
    c_grn "分页器就绪: PAGER=$pager_in_img"
  fi

  c_grn "\n镜像构建完成:"
  docker images "$IMAGE_NAME:$IMAGE_TAG"
}

clean_all() {
  c_ylw "清理中间产物: $WORK/{rootfs,extract,ktmp,kernel,pkg_unzip,pkg.zip}"
  rm -rf "$WORK/rootfs" "$WORK/extract" "$WORK/ktmp" "$WORK/kernel" "$WORK/pkg_unzip" "$WORK/pkg.zip"
}

usage() {
  sed -n '2,15p' "$0"
  exit 0
}

main() {
  local action="${1:-all}"
  need_root
  command -v docker >/dev/null || die "缺少 docker"
  command -v dnf    >/dev/null || die "缺少 dnf"

  c_grn "架构: $ARCH   工作目录: $WORK"
  c_grn "镜像: $IMAGE_NAME:$IMAGE_TAG"
  c_grn "安装包: $PKG_URL"

  case "$action" in
    all)    prepare_kernel; build_rootfs; prune_rootfs; install_gaussdb; pack_image ;;
    base)   build_rootfs; prune_rootfs ;;
    kernel) prepare_kernel ;;
    image)  install_gaussdb; pack_image ;;
    clean)  clean_all ;;
    -h|--help|help) usage ;;
    *) die "未知参数: $action" ;;
  esac
}

main "$@"

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
博主关闭了所有页面的评论