Skip to content

高可用部署 ​

本文介绍 Kuboard V4(High Availability,HA)生产部署方案:多个 Kuboard 实例组成服务集群,前端由负载均衡器(Load Balancer)分发流量,数据库与 Redis 分别提供高可用与跨实例状态共享。

适用读者

已经完成单机部署(参考 快速开始)并准备将 Kuboard 作为生产系统长期运行的团队。

下载编排文件 ​

本页讲到的编排文件可直接下载使用:

文件用途
docker-compose-ha-opengauss.yaml本页主方案:Redis Cluster(3 主 3 从)+ OpenGauss + Nginx 双副本
docker-compose-mysql-redis.yaml单副本生产对照方案:Redis Standalone + MySQL

配套文件 ​

主方案还需在主 yaml 的同一目录放一个 opengauss-init.sh(OpenGauss 容器的初始化脚本),否则 OpenGauss 无法初始化。把它保存为 opengauss-init.sh:

sh
#!/usr/bin/env bash
set -Eeuo pipefail

export DEBIAN_FRONTEND=noninteractive
unset LD_LIBRARY_PATH

apt-get update -qq
apt-get install -y --no-install-recommends libxml2 ca-certificates >/dev/null

cat > /usr/local/bin/gsql <<'EOF'
#!/usr/bin/env bash
export LD_LIBRARY_PATH="/usr/local/opengauss/lib:${LD_LIBRARY_PATH:-}"
exec /usr/local/opengauss/bin/gsql "$@"
EOF
chmod +x /usr/local/bin/gsql

同一目录再建一个 .env,至少要有镜像仓库与版本两个变量:

sh
KUBOARD_IMAGE_REPO=swr.cn-east-2.myhuaweicloud.com/kuboard/kuboard
KUBOARD_VERSION=v4

准备好后,在该目录执行启动命令,浏览器访问 http://<部署主机>:9001:

sh
docker compose -f docker-compose-ha-opengauss.yaml up -d

部署拓扑 ​

Kuboard V4 是无状态(stateless)的 Spring Boot 服务,可水平扩展为多个副本;真正的跨实例状态存放在数据库与 Redis 中。推荐拓扑如下:

Kuboard V4 高可用部署架构

图示中的负载均衡器、高可用数据库与 Redis 分布式缓存属于外部基础设施,由用户自行提供与运维;Kuboard 只运行多副本应用实例:

  • 多副本 Kuboard 实例:2 个及以上 kuboard-server 容器(官方编排中的 kuboard-1 / kuboard-2);
  • 前置负载均衡:Nginx 或 HAProxy,将用户请求分发到各副本(官方编排使用 Nginx upstream + 反向代理);
  • 高可用数据库:MySQL 主从复制、OpenGauss 主备或 PostgreSQL + Patroni,Kuboard 通过 DB_URL 指向数据库高可用连接地址;
  • Redis 分布式缓存:提供跨实例共享缓存与事件分发,支持三种访问模式:
    • Redis Standalone(单实例,性能好,无故障转移);
    • Redis Sentinel(哨兵模式,自动故障转移,官方场景 3 描述);
    • Redis Cluster(集群模式,官方 HA 编排实际使用:6 节点 3 主 3 从)。

端口模型 ​

  • 浏览器访问:nginx 暴露宿主 9001 → 容器内 nginx 80 → 上游 kuboard-1 / kuboard-2 副本的 80
  • 副本容器内部监听 80(浏览器入口),Spring Boot 后端端口 9090 不对外暴露
  • 管理端点 9091 默认不暴露,接入监控时再决定
  • 依赖组件端口(数据库 5432 / 3306、Redis 7001–7006)由编排内部互通,不需要暴露到宿主
  • 修改端口:只改 nginx 的 9001 映射即可,副本容器保持 80

官方 docker-compose-ha-opengauss.yaml 的组件清单如下:

组件镜像 / 版本角色
kuboard-1 / kuboard-2${KUBOARD_IMAGE_REPO}:${KUBOARD_VERSION}Kuboard 应用副本(2 个)
nginxnginx:1.25-alpine负载均衡,对外暴露 9001:80
dbenmotech/opengauss:5.0.1数据库(示例为单节点 OpenGauss)
redis-node-1 ~ redis-node-6redis:7.2-alpineRedis Cluster 6 节点(3 主 3 从)
redis-cluster-creatorredis:7.2-alpine一次性初始化 Redis Cluster
yaml
services:
  kuboard-1:
    image: ${KUBOARD_IMAGE_REPO}:${KUBOARD_VERSION}
    environment:
      - DB_DRIVER=org.opengauss.Driver
      - DB_URL=jdbc:opengauss://db:5432/kuboard?serverTimezone=Asia/Shanghai
      - DB_USERNAME=kuboard
      - DB_PASSWORD=Kuboard@123
      - KUBOARD_CACHE_PROVIDER=redis
      - KUBOARD_CACHE_REDIS_MODE=cluster
      - KUBOARD_CACHE_REDIS_NODES=redis-node-1:7001,redis-node-2:7002,redis-node-3:7003,redis-node-4:7004,redis-node-5:7005,redis-node-6:7006
      - KUBOARD_CACHE_REDIS_PASSWORD=Kuboard123
    volumes:
      - ./docker-compose-ha-postgres-data/kuboard-1-logs:/app/logs
    depends_on:
      db:
        condition: service_healthy
      redis-cluster-creator:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 60s
    networks:
      kuboard_v4_ha_opengauss:

注意

  • 编排中的 kuboard-1 / kuboard-2 容器内部监听 80 端口(镜像内置 nginx 前置),Spring Boot 端口 9090 不对外暴露;
  • KUBOARD_IMAGE_REPO / KUBOARD_VERSION 需要由部署者通过环境变量提供;
  • 两个副本的 KUBOARD_CACHE_* 配置必须完全一致。

为什么多实例必须使用 KUBOARD_CACHE_PROVIDER=redis ​

Kuboard 的缓存分为两类:走 Spring CacheManager 的共享缓存与进程内 Caffeine 本地缓存。缓存 provider 通过 kuboard.application.cache.provider 切换(环境变量 KUBOARD_CACHE_PROVIDER,默认 caffeine),代码依据 @ConditionalOnProperty 自动装配 CaffeineConfig 或 RedisConfig。

在单实例(caffeine)模式下,以下跨实例状态全部存放在各自 JVM 内,多副本部署时彼此不可见:

状态 / 缓存caffeine(单机,默认)redis(分布式,多副本必需)代码依据
系统配置缓存(SystemConfig、KuboardUserAuthority、KuboardMenu、ClusterNameCache 等)Caffeine 本地,ConfigCache 默认 TTL 5 分钟RedisCacheManager 共享,默认 TTL 10 分钟CaffeineConfig.initCacheManager() / RedisConfig.redisCacheManager()
MCP 会话信息 / 客户端信息(clientInfo)Caffeine 本地,TTL 30 分钟Redis key {kuboard:mcp}:mcp-client-info:*,TTL 24 小时CaffeineMcpSessionInfoService / RedisMcpSessionInfoService
MCP 资源订阅(resources/subscribe)推送仅本实例 SSE 订阅者收到Redis Pub/Sub fanout,channel {kuboard:mcp}:mcp-events:resource-changeMcpResourceChangeRedisPubSub / McpResourceSubscriptionManager
通用 SSE 事件分发仅本实例Redis Pub/Sub,channel channel:kuboard-sse-eventMemorySsePublisher / RedisSsePublisher
OIDC 登录 state(login-start 与回调可能落在不同实例)Caffeine 本地(跨实例回调必失败)Redis key kuboard:oidc:state:{state},TTL 10 分钟,GETDEL 原子消费CaffeineOidcStateStore / RedisOidcStateStore
MCP 每用户限流(rate limit)本机内存计数Redis INCR + EXPIRE 原子窗口RateLimitFilter
变更审批 plan / approvalToken(agent-mutation-approval)数据库为单一事实源(SSOT),不依赖缓存 provider,天然支持多实例同左McpChangePlanRegistry
K8sCapabilityCache(集群能力探测缓存)恒为进程内 Caffeine(versionCache TTL 1h / max 500,entryCache TTL 1h / max 2000),不共享同左(不共享)K8sCapabilityCache
ClusterClientBuilder恒为进程内 Caffeine同左(不共享)CacheUtil.initSimpleCacheManager()

表中的 key 名未含部署前缀

上表列出的是 key / channel 的主体部分。设置了 KUBOARD_CACHE_REDIS_KEY_PREFIX 后,实际的 key 会带上该前缀(如 prod:{kuboard:mcp}:mcp-client-info:xxx),Pub/Sub channel 同理。

由此可以得出两个重要结论:

  1. 配置变更的一致性依赖共享缓存:SystemConfig 等缓存走 Spring CacheManager,caffeine 模式下各副本各持一份(最长 5 分钟不一致),redis 模式下所有副本共享同一份 Redis 缓存,任一实例更新配置并 evict 后,其他副本立即可读到新值。
  2. 有状态会话类功能(MCP SSE 订阅推送、OIDC 登录回调、全局限流)在 caffeine 模式下只对本实例生效,多副本 + 负载均衡时会出现"请求落到实例 A 而状态在实例 B"的故障;redis 模式下通过 Pub/Sub 与共享 key 解决。

关于 MCP confirm token

较早版本的文档提到的 confirmToken 机制在现版本源码中已废弃(KuboardMcpResult / McpAuditAppender 仅保留兼容签名,McpSubscriptionScheduledCleanup 不再调用 confirmToken 清理),高危操作审批由 agent-mutation-approval(变更审批 plan) 替代。plan 与 approvalToken 全部落库,审批/消费在 SQL 层 CAS 原子完成,因此该机制在多实例下不需要 Redis 也能保证一致性。

Redis 环境变量清单 ​

缓存相关配置通过 kuboard.application.cache.* 提供:

环境变量默认值说明
KUBOARD_CACHE_PROVIDERcaffeine缓存提供者:caffeine(单机)/ redis(分布式)
KUBOARD_CACHE_REDIS_MODEstandaloneRedis 访问模式:standalone / sentinel / cluster
KUBOARD_CACHE_REDIS_NODESlocalhost:6379Redis 节点列表,逗号分隔(cluster / sentinel 模式为多个节点)
KUBOARD_CACHE_REDIS_SENTINEL_MASTERmasterSentinel 主节点名称(仅 sentinel 模式生效)
KUBOARD_CACHE_REDIS_USERNAME空Redis 6+ ACL 用户名(三模式通用,留空表示使用 default 用户)
KUBOARD_CACHE_REDIS_PASSWORD空Redis 密码(三模式通用)
KUBOARD_CACHE_REDIS_DATABASE0Redis 逻辑库编号(仅 standalone / sentinel 模式生效)
KUBOARD_CACHE_REDIS_KEY_PREFIX空Redis key 的部署级前缀(同一 Redis 上跑多套 Kuboard 时必设,如 prod / staging),见下文「多套 Kuboard 共用同一个 Redis」

关于 Redis 6+ ACL 用户名

Redis 6 及以上支持多用户 ACL(AUTH <username> <password>)。如果目标 Redis 禁用了 default 用户、只允许以自定义用户登录,请同时设置用户名:

sh
export KUBOARD_CACHE_REDIS_USERNAME=kuboard
export KUBOARD_CACHE_REDIS_PASSWORD=Kuboard123
  • 用户名留空(默认)时只发送 AUTH <password>,等价于 default 用户,行为与旧版本一致;
  • 用户名作用于数据节点(主 / 从),三种模式通用;哨兵不在其列 —— Kuboard 不对 Sentinel 做 AUTH(不设置哨兵用户名 / 密码),因此哨兵自身不能开启 requirepass;
  • 该用户需要在 Kuboard 用到的 key 前缀(kuboard:*、{kuboard:mcp}:* 等)与 Pub/Sub channel 上具备读写 / 订阅权限;若设置了 KUBOARD_CACHE_REDIS_KEY_PREFIX,这些前缀要相应带上部署前缀。

各模式的行为(见 RedisConfig.lettuceConnectionFactory()):

模式取节点DATABASESENTINEL_MASTER备注
standalone只取 NODES 第一个生效忽略(有日志告警)单点,无故障转移
sentinel全部 NODES 作为哨兵地址生效生效(指定主节点名)自动故障转移
cluster全部 NODES 作为集群节点忽略(有日志告警)忽略(有日志告警)分片 + 自动故障转移
其它值———打印错误日志并 System.exit(1) 拒绝启动
sh
# Redis Standalone(单机生产,参考 docker-compose-mysql-redis.yaml)
export KUBOARD_CACHE_PROVIDER=redis
export KUBOARD_CACHE_REDIS_MODE=standalone
export KUBOARD_CACHE_REDIS_NODES=redis:6379
export KUBOARD_CACHE_REDIS_PASSWORD=Kuboard123

# Redis Sentinel(1 主 1 从 3 哨兵)
export KUBOARD_CACHE_PROVIDER=redis
export KUBOARD_CACHE_REDIS_MODE=sentinel
export KUBOARD_CACHE_REDIS_NODES=sentinel-1:26379,sentinel-2:26379,sentinel-3:26379
export KUBOARD_CACHE_REDIS_SENTINEL_MASTER=mymaster
export KUBOARD_CACHE_REDIS_PASSWORD=Kuboard123

# Redis Cluster(3 主 3 从,参考 docker-compose-ha-opengauss.yaml)
export KUBOARD_CACHE_PROVIDER=redis
export KUBOARD_CACHE_REDIS_MODE=cluster
export KUBOARD_CACHE_REDIS_NODES=redis-node-1:7001,redis-node-2:7002,redis-node-3:7003,redis-node-4:7004,redis-node-5:7005,redis-node-6:7006
export KUBOARD_CACHE_REDIS_PASSWORD=Kuboard123

TIP

  • 变量名以本文为准:部分早期的示例文件把 Sentinel 主节点变量写作 KUBOARD_CACHE_REDIS_MASTER_NAME,Kuboard 实际识别的是 KUBOARD_CACHE_REDIS_SENTINEL_MASTER。
  • Redis Cluster 模式下所有 key 与 Pub/Sub channel 使用 hash tag {kuboard:mcp} / {kuboard:mcp}:...,保证在分片路由中落到同一 slot,相关 key 与订阅事件路由一致。

多套 Kuboard 共用同一个 Redis ​

同一套部署的多个副本共用一个 Redis 是正常用法(上面所有场景都是如此)。需要隔离的是不同的 Kuboard 部署 —— 例如生产与测试共用一台 Redis、或者同一台机器上跑了两套编排。

不隔离时的表现(两套部署的 key 完全重名):

  • 只有一套能拿到调度 leader 租约,另一套的同步调度 / 健康巡检 / 审计建表全部停摆;
  • 菜单 / 权限 / 系统配置缓存互相覆盖(表现为「权限串台」);
  • 页面事件(SSE)互相投递。

隔离方式(二选一,优先用前缀):

yaml
environment:
  - KUBOARD_CACHE_REDIS_KEY_PREFIX=prod # 只允许 A-Z a-z 0-9 . _ -
  # 或:- KUBOARD_CACHE_REDIS_DATABASE=3 # 仅 standalone / sentinel 模式有效,cluster 模式忽略
  • 同一套部署的所有副本必须使用同一个前缀(否则副本之间互相不可见,leader 租约会分裂成两套);
  • 前缀只允许字母、数字、.、_、-;含冒号或花括号的取值会被忽略并在启动日志中告警;
  • 留空(默认)时 key 与升级前完全一致,升级不清缓存、不重选 leader、不断 SSE 订阅。

切换前缀必须一次全量重启:滚动升级期间新旧前缀并存,最长 120 秒(一个 leader 租约的 TTL)内存在两套调度 leader。请按「停全部副本 → 改前缀 → 一起启动」执行,而不是滚动更新。

把前缀当缓存版本用:跨大版本升级且缓存结构变化时,把前缀改成带版本号的值(如 prod-v435),旧缓存自然不可见,无需手工 FLUSHDB;即便不改前缀,读到无法反序列化的旧值时 Kuboard 也会自动清理并回源。

完整的配置与排查说明见 环境变量参考。

缓存失效传播机制 ​

Kuboard 的缓存失效由 K8sCapabilityCacheEvictNotifier(静态通知桥接器)与 K8sCapabilityCacheEvictListener(Spring 组件)协作完成,触发链如下:

更新系统配置(SystemConfigService.updateSystemConfigById)
  ├─ 写数据库(保留当前版本 + 最多 10 条历史,current 标记切换)
  ├─ systemConfig.evict(t)               → 清除 CacheManager 中的共享缓存条目
  ├─ publishEvent(SystemConfigChangedEvent)  → McpToolsetRefresher 重建工具集(本地)
  └─ 若目标类型是 ConfigClusterSync(cacheResources 变更):
       ├─ ConfigClusterSync.onCacheResourcesChanged()
       │    └─ K8sCapabilityCacheEvictNotifier.notifyEvictAll()   // 静态桥接,多 handler
       │         ├─ K8sCapabilityCacheEvictListener → capabilityCache.evictAll()(清 L3 探源缓存)
       │         └─ McpNotificationToolsChangedListener → 向 SSE 订阅者广播 notifications/tools/list_changed
       └─ 发布 EnvironmentChangeEvent(注入类配置)→ 触发线程池动态调整等本地监听

要点:

  • Notifier 是静态桥接器:K8sCapabilityCacheEvictNotifier 维护一个 CopyOnWriteArrayList,Spring 组件在 @PostConstruct 时注册 handler(K8sCapabilityCacheEvictListener 负责清 capability 缓存、McpNotificationToolsChangedListener 负责 MCP 广播),notifyEvictAll() 依次执行全部 handler;
  • 集群 CRUD 也触发失效:ClusterService 在导入 / 更新 / 删除集群后调用 K8sCapabilityCacheEvictListener.notifyEvictCluster(clusterId),清除该集群的 version 与 entry 缓存;
  • cacheResources 变更不重建上下文:代码刻意不为 ConfigClusterSync 执行 contextRefresher.refresh()(全量刷新会断开 MCP SSE 长连接),改为发布 EnvironmentChangeEvent,key 包含 cacheResources 时 K8sCapabilityCacheEvictListener.onEnvironmentChange 执行 evictAllCapabilities()。

跨副本传播的真实行为

以上事件都是 Spring 应用上下文内的本地事件,不会通过 Redis Pub/Sub 转发到其他副本。多副本下的一致性由两层机制保证:

  1. 共享缓存层:SystemConfig 等 CacheManager 缓存存放在 Redis,任一实例 evict 后所有副本的"下一次读取"都会穿透到数据库(立即全局一致);
  2. 本地缓存层:K8sCapabilityCache 恒为进程内 Caffeine,副本 B 的失效事件只在本实例触发;副本 A 上导入集群后,副本 B 的 capability 缓存最长保留到 1 小时 TTL 自然过期后才重新探测。

SystemConfigService 中亦留有 // FIXME 在集群环境下刷新配置 注释,表明跨副本的主动通知刷新尚未在代码层实现,生产环境请接受"本地缓存 TTL 内最终一致"的行为(功能正确性不受影响,仅在变更后存在短暂的旧能力判定窗口)。

数据库高可用建议 ​

Kuboard 对数据库的访问通过 DB_DRIVER / DB_URL / DB_USERNAME / DB_PASSWORD 四个环境变量注入(spring.datasource),本身不感知数据库高可用架构,因此可以直接接入您已有的数据库 HA 方案:

  • MySQL / MariaDB 主从:配置半同步复制 + MHA / Orchestrator 等自动切换,DB_URL 指向虚拟 IP(VIP)或连接串中配置多个候选地址;
  • OpenGauss 主备:docker-compose-ha-opengauss.yaml 中的 db 服务为单节点示例(enmotech/opengauss:5.0.1),生产建议启用 OpenGauss 主备或使用云数据库(RDS)形态;
  • PostgreSQL:Patroni + etcd 组成自动故障转移集群,DB_URL 指向 Patroni VIP;
  • 云数据库:直接使用云厂商的 MySQL / PostgreSQL 高可用实例,由云平台负责主备切换。
yaml
# docker-compose-ha-opengauss.yaml 中数据库服务的连接配置(单节点示例)
services:
  db:
    image: enmotech/opengauss:5.0.1
    environment:
      GS_PASSWORD: Kuboard@123
      TZ: Asia/Shanghai
    healthcheck:
      test: ["CMD-SHELL", "gsql -U kuboard -W Kuboard@123 -d kuboard -c 'SELECT 1'"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 180s

写入模式对主从友好

Kuboard 的系统配置写入采用"新版本插入 + current 标记"方式(保留 10 条历史),属于低频轻量写入;集群能力探测等读多写少。相比控制面 K8s 的写放大,Kuboard 对主从延迟的容忍度较高,但仍建议主从延迟监控,避免配置刚保存立即在从库读到旧值。

官方 HA 编排示例 ​

Redis Cluster + OpenGauss + Nginx(docker-compose-ha-opengauss.yaml) ​

Redis 集群部分(6 节点 3 主 3 从,密码鉴权 + AOF 持久化):

yaml
services:
  redis-node-1:
    image: redis:7.2-alpine
    configs:
      - source: redis_node_1_conf
        target: /redis/redis.conf
    volumes:
      - ./docker-compose-ha-postgres-data/redis-node-1:/data
    command: redis-server /redis/redis.conf
    ports:
      - 7001:7001
    healthcheck:
      test: ["CMD", "redis-cli", "-p", "7001", "-a", "Kuboard123", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      kuboard_v4_ha_opengauss:

  redis-cluster-creator:
    image: redis:7.2-alpine
    command: /bin/sh -c "sleep 10 && redis-cli -p 7001 -a Kuboard123 --cluster check redis-node-1:7001 >/dev/null 2>&1 && echo 'Redis cluster already exists, skipping creation' && exit 0; redis-cli -p 7001 -a Kuboard123 --cluster create redis-node-1:7001 redis-node-2:7002 redis-node-3:7003 redis-node-4:7004 redis-node-5:7005 redis-node-6:7006 --cluster-replicas 1 --cluster-yes; redis-cli -p 7001 -a Kuboard123 --cluster check redis-node-1:7001 >/dev/null 2>&1"
    depends_on:
      redis-node-1: { condition: service_healthy }
      redis-node-2: { condition: service_healthy }
      redis-node-3: { condition: service_healthy }
      redis-node-4: { condition: service_healthy }
      redis-node-5: { condition: service_healthy }
      redis-node-6: { condition: service_healthy }
    networks:
      kuboard_v4_ha_opengauss:

节点配置(通过 docker config 注入,所有节点相同):

text
# redis_node_X_conf
port 7001
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
requirepass Kuboard123
masterauth Kuboard123

Nginx 负载均衡部分(含 kuboard 反向代理、IP 粘性、全路径 WebSocket 升级与合理超时):

nginx
# http -> upstream WS upgrade 映射
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream kuboard_backend {
  # HA 多副本:ip_hash 把同一客户端粘性到固定后端,
  # 避免 WebSocket / SSE 长连接在两个实例之间被轮询切换。
  # 配合 kuboard-server P2-5a 日志断线重连效果最佳。
  ip_hash;
  server kuboard-1:80;
  server kuboard-2:80;
}

server {
  listen 80;
  server_name localhost;

  gzip on;
  gzip_min_length 1k;
  gzip_buffers 4 16k;
  gzip_comp_level 9;
  gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
  gzip_vary on;

  keepalive_timeout 65;

  # 通用代理头(被各 location 复用)
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  # $http_host 保留端口,便于后端还原用户实际访问的地址;只用 $host 会丢掉端口。
  # proxy_set_header 会覆盖客户端自带的同名头,避免外部伪造。
  proxy_set_header X-Forwarded-Host $http_host;
  proxy_set_header X-Forwarded-Port $server_port;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;
  # K8s watch / 日志流 / 终端 / MCP SSE 都可能持续数小时;24h 超时覆盖绝大多数场景
  proxy_read_timeout 86400;
  proxy_send_timeout 86400;
  client_max_body_size 10m;

  # 前端 SPA
  location / {
    proxy_pass http://kuboard_backend;
  }

  # K8s API 代理(包含 WebSocket watch)
  location /api/k8s.kuboard.cn/ {
    proxy_pass http://kuboard_backend;
  }
  location /k8s-api/ {
    proxy_pass http://kuboard_backend;
  }
  location /k8s-proxy/ {
    proxy_pass http://kuboard_backend;
  }

  # 终端 / 日志 / attach / port-forward(WebSocket)
  location /k8s-ws/ {
    proxy_pass http://kuboard_backend;
  }

  # 容器文件浏览器 HTTP 流式上传(大文件需放开请求体限制并关闭请求体缓冲)
  location /k8s-upload/ {
    proxy_pass http://kuboard_backend;
    client_max_body_size 0;       # 覆盖 server 级的 10m
    proxy_request_buffering off;  # 边收边转,避免整份文件先落临时文件
  }

  # MCP server 端点(Streamable HTTP + SSE 推送)
  location /mcp {
    proxy_pass http://kuboard_backend;
  }
  location /mcp/ {
    proxy_pass http://kuboard_backend;
  }
  location /mcp/message {
    proxy_pass http://kuboard_backend;
  }

  # MCP 资源订阅 SSE 推送
  location /mcp-subscriptions/ {
    proxy_pass http://kuboard_backend;
  }

  # 其余 /api/ 路径
  location /api/ {
    proxy_pass http://kuboard_backend;
  }

  # Swagger / OpenAPI 文档(KUBOARD_SWAGGER_ENABLED=true 时需要)
  # 注意:location 不能带尾斜杠,否则 /v3/api-docs 会被 301 到 /v3/api-docs/,
  # 完整描述与分组地址(/v3/api-docs/{分组名})都会失效。
  location /v3/api-docs {
    proxy_pass http://kuboard_backend;
  }
}
yaml
services:
  nginx:
    image: nginx:1.25-alpine
    configs:
      - source: nginx_conf
        target: /etc/nginx/conf.d/default.conf
    ports:
      - "9001:80"
    depends_on:
      - kuboard-1
      - kuboard-2
    networks:
      kuboard_v4_ha_opengauss:

HA 反向代理的关键配置

  • ip_hash:HA 多副本的必备配置。轮询(默认)会让 WebSocket / SSE 长连接在两个副本之间被随机切换,导致日志流、终端、MCP SSE 推送频繁中断。ip_hash 把同一客户端粘性到固定后端,避免轮询引发的掉线;
  • 全路径 WebSocket 升级:Kuboard 的终端走 /k8s-ws/,K8s watch 走 /k8s-api/ 与 /api/k8s.kuboard.cn/,MCP SSE 走 /mcp 与 /mcp-subscriptions/。原官方编排仅配置 /api/k8s.kuboard.cn/v4/clusters/ 一条 WS 路径,会导致终端与日志功能不可用——必须按上方样例把全部路径补齐;
  • 超时:WebSocket 长连接可达小时级,proxy_read_timeout / proxy_send_timeout 默认 60s 会导致流被截断。建议全局设 86400s(24h)覆盖日常使用;
  • P2-5a 日志断线重连:kuboard-server 在 HA 模式下配合 ip_hash 即可让绝大多数 WebSocket / 日志流在副本滚动重启时不掉线(同一客户端固定到固定后端)。如果副本仍掉线(如 ip_hash 不可用),K8sTerminalLogs 视图已内置指数退避重连(1s → 30s,封顶 8 次)并通过 Redis stream buffer 恢复断线期间产生的日志(5min 窗口)。详见 反向代理 与代码注释。

Redis Standalone + MySQL(docker-compose-mysql-redis.yaml) ​

单副本生产环境的缓存配置作为对照:

yaml
services:
  redis:
    image: redis:7.2-alpine
    command: redis-server --appendonly yes --requirepass Kuboard123
    volumes:
      - ./docker-compose-mysql-redis-data/kuboard-redis:/data
    ports:
      - 6379:6379
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "Kuboard123", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  kuboard:
    image: ${KUBOARD_IMAGE_REPO}:${KUBOARD_VERSION}
    environment:
      - DB_DRIVER=com.mysql.cj.jdbc.Driver
      - DB_URL=jdbc:mysql://db:3306/kuboard?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
      - DB_USERNAME=kuboard
      - DB_PASSWORD=kuboardpwd
      - KUBOARD_CACHE_PROVIDER=redis
      - KUBOARD_CACHE_REDIS_MODE=standalone
      - KUBOARD_CACHE_REDIS_NODES=redis:6379
      - KUBOARD_CACHE_REDIS_PASSWORD=Kuboard123
    volumes:
      - ./docker-compose-mysql-redis-data/kuboard-logs:/app/logs
    ports:
      - "8000:80"

故障切换行为 ​

单个 Kuboard 实例宕机 ​

  • 负载均衡层(nginx upstream)默认轮询分发,实例连接被拒绝(connection refused)时 nginx 自动把请求转发给下一个上游实例,服务不中断;
  • 已建立的 WebSocket / SSE 长连接会断开,客户端重连后被负载均衡分发到存活实例(MCP 客户端通常会重连并重新 initialize);
  • 由于实例无状态,剩余实例可以独立提供服务(所有跨实例状态都在数据库与 Redis 中);
  • 建议在编排中补充 restart: always 让宕机实例自动拉起,并接入监控与告警。

Redis 不可用 ​

Redis 在 kuboard-server 中的角色是"硬依赖 + 有界降级":

阶段行为
启动期fail-fast,不自动回退。RedisConfig.lettuceConnectionFactory 在启动时立即建立连接,Redis 不可达直接抛出 BeanCreationException: ... Unable to connect to Redis;OIDC 启用且使用 RedisOidcStateStore 时还有 OidcStartupValidator 的 PING 探活,同样 fail-fast。这是刻意的设计选择(运维可控,避免静默单机化)
运行期共享缓存读写、MCP SSE 跨实例 Pub/Sub、OIDC state、全局限流等依赖 Redis 的能力受影响;不依赖 Redis 的能力不受影响——变更审批 plan / approvalToken 以数据库为单一事实源,数据库正常即可继续审批与执行
恢复恢复 Redis 服务后实例自动重连(Lettuce 连接池);若 Redis 需要较长时间恢复,可将 KUBOARD_CACHE_PROVIDER 改回 caffeine 重启(注意:SSE 订阅 / OIDC state / 全局限流等跨实例状态会短暂丢失,MCP 客户端可能收到推送中断,需要重新建立订阅)

数据库故障 ​

  • 多副本无法弥补数据库故障:Kuboard 的所有业务数据、系统配置、变更审批 plan 都持久化在数据库中;
  • 数据库主备切换期间,依赖数据库的读写会失败,直到新主库可用(连接池自动恢复);
  • 建议使用高可用数据库方案(见上文)并对数据库做备份与恢复演练(参考 升级与备份)。

恢复建议清单 ​

  • 所有副本的环境变量保持一致,特别是 KUBOARD_CACHE_PROVIDER / KUBOARD_CACHE_REDIS_*;
  • 为 nginx 上游配置 max_fails / fail_timeout,或改用带主动健康检查的负载均衡器;
  • 监控三件事:Redis 连通性与内存、数据库主从状态、Kuboard 实例健康(/actuator/health,管理端口默认 9091);
  • 升级 / 扩缩容时逐副本滚动操作:先重启一个副本确认健康,再继续下一个(MCP 会话与 SSE 连接逐实例重建,避免全部同时中断)。

相关文档 ​