Appearance
阿里云ACK K8S部署
本文介绍在阿里云容器服务 Kubernetes 版(ACK)上部署织信 2.41 的完整流程,覆盖集群接入、镜像准备、编排文件说明、部署顺序、存储选型和常见问题。内容基于实际客户交付环境整理,既适用于 ACK 托管版集群,也适用于客户自建的专有版集群(控制面由阿里云托管、节点自持)。
阅读说明
文中 <...> 为占位符,需替换为实际环境值。示例里的 IP、域名、仓库账号和密钥仅作格式演示,不可直接照抄。
一、部署形态
织信的数据面(PostgreSQL、Redis、对象存储)建议使用云产品,集群内只保留无状态应用和必须就近部署的中间件。这样数据不依赖容器本地磁盘,Pod 可以随时重建,备份与扩容沿用云产品能力。
| 组件 | 建议位置 | 说明 |
|---|---|---|
| account(入口与调度) | 集群内 Deployment | 1 副本,端口 9881 |
| biz(业务执行) | 集群内 Deployment | 通常 2 副本(biz-0、biz-1),端口 8881 |
| nginx(反向代理) | 集群内 Deployment | 1 副本,80 与 19881,NodePort 30080 对外 |
| RabbitMQ | 集群内 StatefulSet | 1 副本,5672 与 15672 |
| Elasticsearch | 集群内 StatefulSet | 1 副本,9200 与 9300 |
| PostgreSQL | 集群外(RDS PostgreSQL 13) | 需 ltree 等 contrib 扩展 |
| Redis | 集群外(云数据库 Redis) | 注意 IP 白名单 |
| 对象存储 | 集群外(OSS 或客户 S3 兼容服务) | S3 协议,注意 path-style |
两种常见形态:
- 混合部署(推荐,也是本文的默认形态):PG、Redis、OSS 用云产品,RabbitMQ 与 ES 放在集群内。适合客户已有云数据库、只想把应用容器化的场景。
- 全云产品:RabbitMQ 用云消息队列、ES 用阿里云 ES,集群内只有 account、biz、nginx。此时编排文件里对应中间件的 Service 改为 ExternalName 或直接填外网/内网地址,
B-middleware.yaml可以整段删掉。
1.1 组件与端口
| 组件 | 镜像 tag | 资源类型 | 副本 | 端口 |
|---|---|---|---|---|
| account | informat-account-2.41 | Deployment | 1 | 9881 |
| biz | informat-biz-2.41 | Deployment | 2(biz-0 / biz-1) | 8881 |
| nginx | informat-nginx-1.0 | Deployment | 1 | 80、19881、NodePort 30080 |
| rabbitmq | informat-rabbitmq-1.0 | StatefulSet | 1 | 5672、15672 |
| elasticsearch | informat-elasticsearch-7.15.2 | StatefulSet | 1 | 9200、9300 |
镜像 tag 不统一
应用镜像是 2.41,但 nginx 与 rabbitmq 是 1.0,Elasticsearch 用上游版本号 7.15.2。凭「所有组件都是 2.41」的直觉去写 YAML,会直接得到 ImagePullBackOff。
1.2 资源与容量估算
单套 2 副本的最小资源配置参考:
| 组件 | requests | limits | 说明 |
|---|---|---|---|
| account | 250m / 1Gi | 4Gi | JAVA_XMX=2g |
| biz(每副本) | 250m / 1Gi | 8Gi | JAVA_XMX=4g,内置 DinD |
| nginx | 100m / 64Mi | 256Mi | |
| rabbitmq | 100m / 256Mi | 2Gi | |
| elasticsearch | 250m / 1Gi | 2Gi | ES_JAVA_OPTS=-Xms512m -Xmx512m |
limits 合计约 24Gi,因此单节点建议 8 核 32Gi 起步;若节点只有 16Gi,务必先调低 JAVA_XMX 与 ES 堆大小,否则 Pod 会被 OOMKilled 或长期 Pending。
二、前置准备
2.1 集群与 kubectl 版本
| 项 | 要求 |
|---|---|
| 集群版本 | 1.20 及以上;本文实测环境为专有版 v1.20.11-aliyun.1 |
| kubectl | 与集群小版本相差不超过 1,实测用 v1.20.11 |
| 容器运行时 | containerd 或 Docker 均可,不影响编排文件 |
| 节点架构 | 镜像与节点架构必须一致(x86 集群不能用 arm64 镜像包) |
bash
kubectl version --short
kubectl get nodes -o wide
kubectl get storageclass
kubectl auth can-i create pods -n <namespace>kubectl 不要用太新的版本:1.23 起会出现大量弃用警告,1.26 之后与 1.20 集群的 API 协商可能直接失败。
2.2 操作机与 kubeconfig
客户的 ACK 集群常常不暴露公网 API Server,kubectl 只能在同 VPC 的一台 ECS 上执行。推荐直接把数据库服务器或一台跳板机当作操作机。
| 项 | 说明 |
|---|---|
| 操作机 | 同 VPC 内 ECS,能访问 API Server 内网地址与 kubeconfig |
| kubeconfig | ACK 控制台「集群信息 → 连接信息」导出,server 为内网地址 |
| 权限 | 具备目标命名空间的 create / get / logs / exec 权限 |
离线方式把 config 与 kubectl 一起带上去:
bash
# 1) 本机上传离线包
scp k8s-client.tar.gz root@<操作机IP>:/root/
# 2) 操作机上解包
cd /root
tar -xzf k8s-client.tar.gz
mkdir -p /root/.kube
mv config /root/.kube/config && chmod 600 /root/.kube/config
chmod +x kubectl && mv kubectl /usr/local/bin/kubectl
# 3) 验证
kubectl version --short
kubectl get nodes -o wide若操作机与集群不在同一 VPC,需要先打通网络(云企业网、对等连接或 VPN),否则 kubeconfig 里的内网 server 地址不可达。
2.3 镜像仓库凭据
集群拉取私有镜像前,必须先建 imagePullSecrets,否则所有 Pod 都会停在 ImagePullBackOff。这一步要在 apply 任何 YAML 之前做。
bash
kubectl -n <namespace> create secret docker-registry registry-cred \
--docker-server=<registry域名> \
--docker-username='<仓库账号>' \
--docker-password='<仓库密码>'编排文件里每个 Pod 模板都引用它:
yaml
spec:
imagePullSecrets:
- name: registry-cred使用 ACR 时也可以安装 aliyun-acr-credential-helper 组件实现免密拉取,但客户自建 Harbor 或第三方 registry 仍需手工建 Secret。
2.4 网络与白名单
容器化部署失败,一大半原因在网络策略而不是 YAML 本身。上线前逐项确认:
| 方向 | 目标 | 说明 |
|---|---|---|
| Pod → PG | <PG地址>:5432 | 云数据库需把节点网段或 Pod 网段加入白名单 |
| Pod → Redis | <Redis地址>:6379 | 必须放行节点/Pod 网段,见 8.1 |
| Pod → 对象存储 | <OSS Endpoint>:80/443 | OSS 走内网 Endpoint 可免流量费 |
| 外部 → 入口 | <节点IP>:30080 | 安全组与主机防火墙都要放行,见 8.4 |
三、配置 ConfigMap 与 Secret
3.1 ConfigMap
地址类配置集中放在一个 ConfigMap 里,所有组件用 envFrom 注入,避免逐个 Deployment 改环境变量。
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: informat-env
namespace: <namespace>
data:
PG_HOST: <PG地址>
PG_PORT: '5432'
PG_USER: postgres
REDIS_HOST: <Redis地址>
REDIS_PORT: '6379'
RABBITMQ_HOST: informat-rabbitmq
RABBITMQ_PORT: '5672'
SEAWEEDFS_HOST: <对象存储Endpoint>
S3_PORT: '80'
ES_HOST: informat-elasticsearch
ES_PORT: '9200'
INTRANET_URL: http://informat-nginx:19881/
PUBLIC_URL: http://<节点IP>:30080| 变量 | 说明 |
|---|---|
PUBLIC_URL | 浏览器真实访问地址,不能填 Service 域名,否则分享链接、附件预览全部错 |
INTRANET_URL | 集群内入口,供应用内部回调使用,指向 nginx 的 19881 |
SEAWEEDFS_HOST | 兼容 S3 的对象存储 Endpoint,OSS 用内网地址 |
3.2 Secret
密码与密钥统一放 Secret,用 base64 编码:
bash
echo -n '<PG密码>' | base64yaml
apiVersion: v1
kind: Secret
metadata:
name: informat-secret
namespace: <namespace>
type: Opaque
data:
PG_PASSWORD: <base64>
REDIS_PASSWORD: <base64>
RABBITMQ_USER: <base64>
RABBITMQ_PASSWORD: <base64>
S3_ACCESS_KEY: <base64>
S3_SECRET_KEY: <base64>不要把真实凭据提交到仓库
交付包里的 Secret 值应替换为现场生成的随机密码。数据库、Redis 的密码必须与云产品实例上的实际密码一致;修改 Secret 不会同步云产品的认证信息。
3.3 命名空间与 Pod 安全
biz 内置 InformatClaw 的 Docker-in-Docker,需要特权容器。在开启了 Pod Security Admission 的集群上,必须给命名空间打特权标签,否则 Pod 会被直接拒绝创建(报 violates PodSecurity "baseline")。
yaml
apiVersion: v1
kind: Namespace
metadata:
name: <namespace>
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privilegedbiz 本身:
yaml
securityContext:
privileged: true只给 biz 特权,account 与 nginx 不要开。biz 不挂载宿主机 Docker socket,Docker data-root 用独立的 emptyDir。
3.4 nginx 与多 biz 实例
客户要求两个 biz 实例时,nginx 用 /web0/、/web1/ 分别转发到两个 Service,并按客户端 IP 做一致性哈希,保证同一客户端会话落在同一实例:
nginx
upstream backend_account {
hash $proxy_add_x_forwarded_for;
server informat-account:9881;
}
upstream backend_biz_s0 {
hash $proxy_add_x_forwarded_for;
server informat-biz-0:8881;
}
upstream backend_biz_s1 {
hash $proxy_add_x_forwarded_for;
server informat-biz-1:8881;
}
location ^~ /web0/ { proxy_pass http://backend_biz_s0/web/; }
location ^~ /web1/ { proxy_pass http://backend_biz_s1/web/; }实例数必须与 upstream 对齐
biz 扩到 3 个实例时,nginx 配置里也要加 /web2/ 与对应 upstream,否则新实例永远接不到流量。反过来,删实例前先删 upstream 段并 reload nginx。
这份配置通过 ConfigMap 以 subPath 挂进 nginx 的 /etc/nginx/conf.d/,Pod 重建即生效,不需要重新打镜像。
四、部署
交付包共 20 个资源,分成 5 个文件加 1 个合并版:
| 文件 | 资源数 | 内容 |
|---|---|---|
A-base.yaml | 3 | Namespace + ConfigMap + Secret |
B-middleware.yaml | 4 | RabbitMQ、ES 的 Service 与 StatefulSet |
D-db-init.yaml | 2 | account / biz 建库 Job |
E-apps.yaml | 10 | account、biz-0、biz-1、nginx 的 Service 与 Deployment |
F-config-update.yaml | 1 | 配置写入 Job(实际改手工 psql,见步骤 5) |
ALL-IN-ONE.yaml | 17~20 | 合并版,可一次 apply |
严格按「配置 → 地基 → 中间件 → 建库 → 写配置 → 应用」的顺序推进,每一步确认 Pod 状态后再执行下一步。
步骤 1:创建镜像拉取凭证
bash
kubectl -n <namespace> create secret docker-registry registry-cred \
--docker-server=<registry域名> \
--docker-username='<仓库账号>' \
--docker-password='<仓库密码>'
kubectl -n <namespace> get secret registry-cred步骤 2:地基 A-base
bash
kubectl apply -f A-base.yaml
kubectl -n <namespace> get configmap,secret此时先按实际环境改好 ConfigMap 与 Secret 的值,后面所有组件都从这里取配置。
步骤 3:中间件 B-middleware
bash
kubectl apply -f B-middleware.yaml
kubectl -n <namespace> get pod -wStatefulSet 首启会初始化数据目录,RabbitMQ 与 ES 都可能需要 1~3 分钟。建议编排里配 startupProbe,把 failureThreshold 放大到 120,避免启动慢被反复重启:
yaml
startupProbe:
tcpSocket:
port: 9200
periodSeconds: 5
failureThreshold: 120步骤 4:数据库初始化
建库有两种做法,外置 PG 优先用第二种。
方式一:K8s Job(集群内 Pod 能直连 PG 时)
bash
kubectl apply -f D-db-init.yaml
kubectl -n <namespace> logs job/informat-account-db-init --tail=200方式二:在 PG 服务器上直接跑安装脚本(推荐)
外置 PG 常常存在网络策略、账号权限、客户端版本等障碍,Job 容易卡在建库阶段。直接在数据库服务器上执行安装包里的 install.sh 更可控:
bash
cd /root/db
bash install.sh脚本走 psql 驱动,会创建两个库:
db_informat2_account_prddb_informat2_biz_prd_0
执行完确认:
bash
psql -h <PG地址> -p 5432 -U postgres -l | grep informat2检查 contrib 扩展
PG 建库阶段报 could not open extension control file "ltree.control",说明缺少 contrib 包(ltree、pg_trgm 等都在里面)。先装扩展再重跑,否则建库会半途中断,需要 drop 库重来。
步骤 5:写入对象存储配置(启动应用之前)
这一步必须在启动 account/biz 之前完成。 配置项写在 t_config 表里,应用启动即读,缺配置会导致 biz 反复重启。
sql
-- 对象存储
UPDATE t_config SET value='<对象存储Endpoint>' WHERE id='fileservice.s3.endPoint';
UPDATE t_config SET value='<accessKey>' WHERE id='fileservice.s3.accessKey';
UPDATE t_config SET value='<secretKey>' WHERE id='fileservice.s3.secretKey';
UPDATE t_config SET value='true' WHERE id='fileservice.s3.enablePathStyleAccess';
UPDATE t_config SET value='<bucketName>' WHERE id='fileservice.s3.bucketName';
UPDATE t_config SET value='<region>' WHERE id='fileservice.s3.region';在数据库服务器上执行:
bash
psql -h <PG地址> -p 5432 -U postgres -d db_informat2_account_prd -c "UPDATE t_config SET value='<值>' WHERE id='fileservice.s3.endPoint';"不要用 Job 改配置
用 K8s Job 把 SQL 塞进 YAML 里执行,会遇到引号转义、t_config 表存在性依赖、Job 重跑不可幂等等问题。改配置直接 psql 更快也更可控;若确实需要 Job,只让它执行 UPDATE,不要建表。
步骤 6:应用 E-apps
bash
kubectl apply -f E-apps.yaml
kubectl -n <namespace> get pod -w预期结果:account 1/1、biz-0 1/1、biz-1 1/1、nginx 1/1。首次启动 account 约 1~2 分钟(含 flyway 校验),biz 启动更慢,需要等健康检查通过后才 Ready。
五、验证
bash
# 1) 全量状态
kubectl -n <namespace> get pod,svc -o wide
kubectl -n <namespace> get events --sort-by=.metadata.creationTimestamp | tail -20
# 2) 应用健康
kubectl -n <namespace> exec -it deploy/informat-account -- \
curl -s http://127.0.0.1:9881/actuator/health
kubectl -n <namespace> exec -it deploy/informat-biz-0 -- \
curl -s http://127.0.0.1:8881/actuator/health
# 3) 集群内入口
kubectl -n <namespace> exec -it deploy/informat-nginx -- \
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/
# 4) 节点入口(在能访问节点 IP 的机器上)
curl -s -o /dev/null -w 'HTTP=%{http_code}\n' http://<节点IP>:30080/
# 5) 中间件连通性
kubectl -n <namespace> exec -it deploy/informat-account -- bash -c \
'echo > /dev/tcp/informat-elasticsearch/9200 && echo es-ok'验收要点:
| 检查项 | 期望 |
|---|---|
account /actuator/health | status: UP,PG / Redis / RabbitMQ 均 UP |
| biz Pod | 1/1 Ready,日志无 ERROR、无反复重启 |
| nginx 19881 | 集群内返回 200 |
| NodePort 30080 | 外部返回 200,或按 8.4 处理防火墙 |
| ES | curl _cluster/health 为 green 或 yellow |
| 附件上传 | 后台传一个文件后能在对象存储里看到对象 |
六、存储方案选择
这是 ACK 部署最容易卡住的地方。四种方案的实际差异:
| 方案 | 适用场景 | 局限 |
|---|---|---|
emptyDir | 应用运行目录(/data/informat-next)、DinD 的 /data/docker | Pod 重建即清空,只能放可重建的缓存 |
hostPath + nodeSelector | 集群内中间件(RabbitMQ / ES)数据 | 数据绑节点,节点故障即丢;多副本无法共享 |
| PVC(云盘 CSI) | 生产推荐的单副本持久化 | 云盘是 ReadWriteOnce,只能挂一个 Pod;需要 csi-plugin 与 ESSD 可用 |
| PVC(NAS) | 需要多 Pod 共享目录(附件、OnlyOffice 缓存) | 性能低于云盘,小文件多时明显 |
云盘 PVC 一直 Pending
客户集群不给盘时,PVC 会永远停在 Pending,describe pvc 只报 provision 失败。此时有两条路:
- 换 NAS StorageClass(大多数 ACK 集群都有
alicloud-nas)。 - 彻底不用 PVC —— 织信的业务数据全在外置 PG 和 OSS 上,容器本地只是运行时缓存,改成
emptyDir不影响持久性(中间件除外)。
关键结论:应用层可以零 PVC,中间件不行。 RabbitMQ 换 emptyDir 会在 Pod 重建后丢消息,ES 会丢索引,两者必须落 hostPath 或 PVC,并用 nodeSelector 固定节点。
七、镜像迁移到正式仓库
测试仓库 → 正式仓库,tag 保持不变,只换仓库路径。
bash
# 拉(测试账号)
docker login --username=<测试账号> <registry域名>
docker pull <registry域名>/<测试项目>/informat:informat-account-2.41
# ... 其余 4 个镜像同理
# 推(正式账号)
docker login --username=<正式账号> <registry域名>
docker tag <registry域名>/<测试项目>/informat:informat-account-2.41 \
<registry域名>/<正式项目>/informat:informat-account-2.41
docker push <registry域名>/<正式项目>/informat:informat-account-2.41两个实测踩过的坑:
- 同一 registry 连续登录两次,后一次的凭据会覆盖前一次。 拉用测试账号、推用正式账号,中间必须重新登录,不要在一个 shell 里交替执行。
- 同一个 IMAGE ID 被两个 tag 引用时,
docker rmi <ID>会报must be forced。 按仓库:tag逐个删,或用docker system prune -a -f。
迁移完成后,正式环境的 registry-cred 要用正式账号重建,编排文件里的镜像地址也要同步改。
八、常见问题
| 现象 | 根因 | 处理 |
|---|---|---|
| PVC 一直 Pending | 集群云盘 CSI 不给盘 | 改 NAS,或应用层改 emptyDir(见第六节) |
ImagePullBackOff | tag 用错(把 nginx/rabbitmq 当成 2.41) | 对齐 1.1 节的 tag 表 |
| StatefulSet 改镜像后 Pod 不变 | StatefulSet 改 image 不触发滚动更新 | 手动 delete pod 强制重建 |
建库报 ltree.control 不存在 | 缺 postgresql13-contrib | 装 contrib 后 drop 库重跑 |
| account / biz 长期 0/1,日志停在 banner | Redis IP 白名单未放行 | 放行节点/Pod 网段(见 8.1) |
| Pod 内 30080 通,外部超时 | 防火墙按端口放行,只开了 6443 | 放行 30080 或改 Ingress |
| Pod 创建被拒,报 PodSecurity | 命名空间未允许特权 | 打 privileged 标签(见 3.3) |
8.1 Redis 白名单(最隐蔽)
现象:account 与 biz 长期 0/1,日志打完 banner 就不动了;在 Pod 里用 /dev/tcp、nc 测端口都「通」。
根因不在网络层。从线程栈里能看到真实原因:
text
io.lettuce.core.RedisCommandExecutionException: ERR illegal address: 10.167.x.x:xxxxxTCP 握手成功,但 Redis 在应用层拒绝了来源地址——云 Redis 开了 IP 白名单,没放行节点或 Pod 网段。
处理:把集群节点网段(或 Pod 网段)加入 Redis 白名单,account 立刻变 1/1。
排查方法
/dev/tcp、nc、telnet 只能证明 TCP 可达,不能证明应用层协议被放行。连不上时不要停留在端口层,直接看应用日志的具体异常,必要时 jstack 打线程栈定位。
8.2 StatefulSet 改镜像后不重建
kubectl edit statefulset 改 image,Pod 仍旧是旧的、一直 ImagePullBackOff。StatefulSet 不像 Deployment 那样在镜像变更时自动滚动(尤其是只改 image、未改其他模板字段时)。
bash
kubectl -n <namespace> delete pod informat-rabbitmq-0
kubectl -n <namespace> get pod -w删 Pod 不会动 hostPath/PVC 数据,是安全的。只有确认数据已备份时才考虑删 StatefulSet 本体。
8.3 建库缺 ltree 扩展
text
could not open extension control file "/usr/pgsql-13/share/extension/ltree.control"bash
# 装 contrib(跳过 libxslt 依赖,只影响 xml2 扩展,ltree 不受影响)
rpm -ivh --nodeps --force postgresql13-contrib-*.rpm
# drop 掉半成品库后重跑
psql -h <PG地址> -U postgres -c 'DROP DATABASE db_informat2_account_prd;'
cd /root/db && bash install.sh8.4 NodePort 30080 外部不通
Pod 内 curl http://<任一节点IP>:30080 返回 200,但从办公网或跳板机访问超时。
原因:6443 端口通是为了 kubectl,不代表 30080 被放行。安全组和主机防火墙都要单独开。
bash
# 操作机上验证端口
nc -zv <节点IP> 30080三种处理:放行 30080(最简单)、改用 Ingress + 域名(推荐生产)、或临时用端口转发排障:
bash
kubectl -n <namespace> port-forward --address=0.0.0.0 service/informat-nginx 8080:80九、上线检查清单
- [ ] 命名空间已打
privileged标签,biz 能以特权容器启动 - [ ]
registry-cred已创建,5 个镜像 tag 与 1.1 节一致 - [ ] ConfigMap 中
PUBLIC_URL是可访问的真实地址,INTRANET_URL指向 19881 - [ ] Secret 中密码与云产品实例一致,未提交到代码仓库
- [ ] PG 已安装 contrib(ltree、pg_trgm),两个业务库创建成功
- [ ] Redis 白名单已放行节点/Pod 网段
- [ ] 对象存储 Endpoint、AK/SK、bucket、region 已写入
t_config,且 path-style 正确 - [ ] RabbitMQ 与 ES 的数据落在 hostPath 或 PVC,并固定了节点
- [ ] 30080(或 Ingress 域名)从外部可访问,附件上传下载正常
- [ ] 备份方案覆盖 PG 逻辑备份、对象存储与中间件数据

