Files
geo/deploy/k3s
root 78fb9c42e2 docs(k3s): note CONCURRENTLY index verification and SMZDM cover preflight
Document how to confirm the new dedup/lookup indexes finished as valid
(no INVALID leftovers from interrupted CREATE INDEX CONCURRENTLY), and how
to find/abort SMZDM publish tasks queued without a cover before rolling
out the smzdm cover-required check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:43:50 +08:00
..

geo-rankly k3s deployment

这套清单用于把当前 Docker Compose 拓扑搬到单节点或小集群 k3s 中。默认包含:

  • infra: postgres, monitoring-postgres, pgbouncer, monitoring-pgbouncer, rabbitmq, redis, qdrant, minio
  • jobs: migrate, minio-init
  • apps: tenant-api, browser-fetch, worker-generate, kol-assist-worker, scheduler, ops-api, frontend, ops-web
  • ingress: geo-rankly.local -> frontend, ops.geo-rankly.local -> ops-web

对象存储部署跟随 deploy/config.yaml / deploy/config.yml 里的 object_storage.provider,并允许 deploy/config.local.yaml / deploy/config.local.yml 覆盖:

  • provider: minioprovider: mino:部署 MinIO 和 minio-init
  • provider: aliyun / aliyun_oss / aliyun-oss / oss:使用 deploy/k3s-aliyun-oss overlay,不部署 MinIO 和 minio-init
  • provider: r2 / s3 / custom_s3:复用 S3 兼容客户端,不部署自建 MinIOCloudflare R2 建议配置 region: autoforce_path_style: true

1. 准备镜像

在线环境建议推到镜像仓库后修改 kustomization.yaml 里的 images

kubectl kustomize deploy/k3s

离线或内网 k3s 可以先用现有打包脚本生成镜像包,再导入到 k3s containerd

bash deploy/scripts/package.sh latest linux/amd64
tar xzf geo-rankly-deploy-*-amd64.tar.gz
cd geo-rankly-deploy-*-amd64
sudo k3s ctr images import images.tar

如果你的服务器是 ARM,把第二个参数换成 linux/arm64

2. 修改密钥

上线前先改 deploy/k3s/secrets.yaml

  • JWT_SECRET
  • OPS_JWT_SECRET
  • OPS_DEFAULT_ADMIN_PASSWORD
  • SCHEDULER_INTERNAL_METRICS_TOKEN
  • BROWSER_FETCH_TOKEN
  • POSTGRES_PASSWORD / RABBITMQ_DEFAULT_PASS / MINIO_ROOT_*
  • config.local.yaml 里的 auth.password_cipher.private_key_pem
  • ops-config.local.yaml 里的 auth.password_cipher.private_key_pem
  • config/config.yaml 里的 LLM / SiliconFlow API key
  • config.local.yaml 里对应的数据库、RabbitMQ、MinIO、JWT 配置

注意:应用运行时通过 pgbouncer / monitoring-pgbouncer 访问数据库;migrate 任务仍然直连 postgres / monitoring-postgres。数据库密码统一来自 POSTGRES_PASSWORD secret,应用配置和 PgBouncer 都会跟随这个 secret。

登录密码加密使用 RSA-OAEP-SHA256。多 Pod 部署时,同一类服务的所有副本必须读取同一把私钥和同一个 key_id,否则某个 Pod 下发的公钥对应密文可能被另一个 Pod 解不开。建议租户端和 ops 端分别生成不同私钥:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out login-password-rsa.pem
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ops-login-password-rsa.pem

然后把完整 PEM 分别粘贴到 deploy/k3s/secrets.yamlconfig.local.yamlops-config.local.yaml

3. 部署

kubectl apply -k deploy/k3s
kubectl -n geo-rankly get pods -w

如果使用阿里云 OSS

kubectl apply -k deploy/k3s-aliyun-oss
kubectl -n geo-rankly get pods -w

离线包里的 bash deploy.sh k3s 会自动读取根目录 config.yaml/config.ymlconfig.local.yaml/config.local.yml,并选择 k3sk3s-aliyun-oss

查看一次性任务:

kubectl -n geo-rankly logs job/migrate
kubectl -n geo-rankly logs job/minio-init

部署包含 CREATE INDEX CONCURRENTLY 的迁移前,先在 staging 用与生产完全一致的 migrate 镜像 tag 跑一次完整迁移,确认 golang-migrate 版本和驱动行为一致;发布清单里不要长期沿用 geo-rankly/migrate:latest,应在发版时把 deploy/k3s/kustomization.yamlimages[].newTag 固定到本次构建 tag。

迁移完成后,建议检查并发创建的索引是否有效,避免 CREATE INDEX CONCURRENTLY 被中断后留下 INVALID 索引:

kubectl -n geo-rankly exec statefulset/postgres -- sh -lc \
  'PGPASSWORD="$POSTGRES_PASSWORD" psql -U geo -d geo -c "SELECT indexrelid::regclass AS index_name, indisvalid FROM pg_index WHERE indexrelid::regclass::text LIKE '\''uq_desktop_tasks_publish_dedup%'\'' OR indexrelid::regclass::text LIKE '\''idx_publish_records_article_account_active%'\'' OR indexrelid::regclass::text LIKE '\''idx_desktop_tasks_publish_record_payload%'\'';"'

所有结果的 indisvalid 都应为 t。若出现 f,先 DROP INDEX CONCURRENTLY IF EXISTS <index_name>,再重新执行迁移。

升级包含 SMZDM(什么值得买)封面必填逻辑时,建议先确认没有已排队或执行中的 SMZDM 发布任务缺少封面,否则这些旧任务会在新版桌面端回写 publish_cover_required 失败:

kubectl -n geo-rankly exec statefulset/postgres -- sh -lc \
  'PGPASSWORD="$POSTGRES_PASSWORD" psql -U geo -d geo -c "SELECT dt.desktop_id, dt.status, dt.created_at FROM desktop_tasks dt JOIN publish_records pr ON pr.id = CASE WHEN (dt.payload->>'\''publish_record_id'\'') ~ '\''^[0-9]+$'\'' THEN (dt.payload->>'\''publish_record_id'\'')::bigint ELSE NULL END JOIN publish_batches pb ON pb.id = pr.publish_batch_id WHERE dt.kind = '\''publish'\'' AND dt.platform_id = '\''smzdm'\'' AND dt.status IN ('\''queued'\'', '\''in_progress'\'') AND dt.payload ? '\''publish_record_id'\'' AND COALESCE(NULLIF(pb.cover_asset_url, '\'''\''), NULLIF(dt.payload#>> '\''{content_ref,cover_asset_url}'\'', '\'''\'')) IS NULL ORDER BY dt.created_at DESC;"'

确认这些旧任务可废弃后,可先中止仍在排队的任务,避免升级后集中失败;in_progress 任务需等待租约结束或由客户端回写:

kubectl -n geo-rankly exec statefulset/postgres -- sh -lc \
  'PGPASSWORD="$POSTGRES_PASSWORD" psql -U geo -d geo -c "WITH stale AS (SELECT dt.desktop_id FROM desktop_tasks dt JOIN publish_records pr ON pr.id = CASE WHEN (dt.payload->>'\''publish_record_id'\'') ~ '\''^[0-9]+$'\'' THEN (dt.payload->>'\''publish_record_id'\'')::bigint ELSE NULL END JOIN publish_batches pb ON pb.id = pr.publish_batch_id WHERE dt.kind = '\''publish'\'' AND dt.platform_id = '\''smzdm'\'' AND dt.status = '\''queued'\'' AND dt.payload ? '\''publish_record_id'\'' AND COALESCE(NULLIF(pb.cover_asset_url, '\'''\''), NULLIF(dt.payload#>> '\''{content_ref,cover_asset_url}'\'', '\'''\'')) IS NULL) UPDATE desktop_tasks dt SET status = '\''aborted'\'', error = jsonb_build_object('\''reason'\'', '\''smzdm_cover_required_upgrade_abort'\'', '\''source'\'', '\''ops_upgrade'\''), active_attempt_id = NULL, lease_token_hash = NULL, lease_expires_at = NULL, updated_at = NOW() FROM stale WHERE dt.desktop_id = stale.desktop_id RETURNING dt.desktop_id;"'

如果是升级后需要重新跑迁移:

kubectl -n geo-rankly delete job migrate --ignore-not-found
kubectl apply -k deploy/k3s

3.1 可选安装 Kite dashboard

Kite dashboard 用于管理 k3s/Kubernetes 资源,默认安装到 kube-system,并通过 NodePort 暴露:

kubectl apply -f deploy/k3s/addons/kite.yaml

访问:

  • Kite dashboard: http://<node-ip>:30082
  • NAS 示例:http://192.168.100.19:30082

注意:Kite 官方清单绑定了 cluster-admin 权限,只建议在可信内网使用,不要直接暴露到公网。

4. 访问

k3s 默认同时支持两种访问方式。

4.1 直接用节点 IP + 端口

frontendops-web 已固定为 NodePort

  • tenant admin: http://<node-ip>:30080
  • ops admin: http://<node-ip>:30081

这两个 NodePort Service 使用 externalTrafficPolicy: Local,用于保留客户端源 IP,后端日志和登录审计会读取规范化后的 X-Forwarded-For / X-Real-IP

如果通过 k3s 内置 Traefik Ingress 对外提供服务,也需要把 Traefik Service 调整为保留源地址:

kubectl -n kube-system patch svc traefik -p '{"spec":{"externalTrafficPolicy":"Local"}}'

临时用 frpc/frps 做 TCP 隧道时,用户源 IP 通常不会穿透到 k3s;这类链路需要 proxy protocol 或 HTTP 反代写入可信 X-Forwarded-For,否则日志只能记录隧道出口/NAS 本地转发地址。

例如 NAS IP 是 192.168.100.19

  • tenant admin: http://192.168.100.19:30080
  • ops admin: http://192.168.100.19:30081

4.2 用域名 Ingress

k3s 默认 Traefik 会接管 Ingress。先把域名指到 k3s 节点 IP:

<node-ip> geo-rankly.local
<node-ip> ops.geo-rankly.local

然后访问:

  • tenant admin: http://geo-rankly.local
  • ops admin: http://ops.geo-rankly.local

临时调试时也可用端口转发:

kubectl -n geo-rankly port-forward svc/frontend 8088:80
kubectl -n geo-rankly port-forward svc/ops-web 8089:80
kubectl -n geo-rankly port-forward svc/rabbitmq 15672:15672
kubectl -n geo-rankly port-forward svc/minio 9001:9001

5. 常用排查

kubectl -n geo-rankly get all
kubectl -n geo-rankly describe pod <pod-name>
kubectl -n geo-rankly logs deploy/tenant-api -f
kubectl -n geo-rankly logs deploy/browser-fetch -f
kubectl -n geo-rankly logs deploy/scheduler -f
kubectl -n geo-rankly logs deploy/worker-generate -f
kubectl -n geo-rankly logs deploy/ops-api -f

如果应用 Pod 先于 migrate 启动失败,通常会自动重启;等 migrate 完成后再看 kubectl -n geo-rankly get pods 即可。