场景

你在生产环境跑着单节点的 Keycloak,业务方已经依赖它完成 SSO 登录。某天凌晨数据库挂了、节点 OOM 重启了,或者 Kubernetes Node 被 drain 了——所有依赖这套 IAM 完成认证的应用全部掉线。

你需要在故障发生前就把集群搭好,并且有一套经过演练的备份恢复流程。

本文不重复 Keycloak Kubernetes 生产部署 的完整 Operator 部署步骤,而是聚焦三个核心问题:

  1. 多节点集群怎么配才能不丢 Session、不断登录?
  2. 数据库备份怎么做到可验证恢复而不仅仅是"dump 了"?
  3. 故障发生时的恢复步骤和回滚路径是什么?

适用与不适用

适用不适用
Keycloak 24+ 基于 Quarkus 发行版旧版 WildFly 发行版(集群配置不同)
生产环境需要 HA 和灾难恢复开发环境单节点即可
Kubernetes(Operator/Helm)或裸机部署刚接触 Keycloak、还未完成单节点部署
PostgreSQL/MySQL 等外部数据库内嵌 H2 数据库(不可用于集群或生产)

集群架构概览

Keycloak 集群的核心依赖只有两个:共享数据库 + 节点发现机制。没有共享存储、没有共享 Session 文件。

  graph TB
    LB[负载均衡器<br/>Nginx/ALB/HAProxy]
    
    subgraph KC1[Keycloak 节点 1]
        KC1_APP[Keycloak 26.x<br/>Quarkus]
        KC1_CACHE[InfiniSpan Cache<br/>Sessions/ClientSessions]
    end
    
    subgraph KC2[Keycloak 节点 2]
        KC2_APP[Keycloak 26.x<br/>Quarkus]
        KC2_CACHE[InfiniSpan Cache<br/>Sessions/ClientSessions]
    end
    
    DB[(PostgreSQL<br/>共享数据库)]
    
    LB --> KC1_APP
    LB --> KC2_APP
    KC1_APP --> DB
    KC2_APP --> DB
    KC1_CACHE <-->|JGroups 分布式缓存| KC2_CACHE

关键理解:Keycloak 26.x 的会话数据分两类存放。用户会话与 client 会话默认落库(persistent-user-sessions 特性默认启用),认证会话(authenticationSessions)、action token、登录失败计数只存在 Infinispan 分布式缓存里。因此节点故障时在线会话可以从其他缓存副本或数据库恢复;但如果所有节点在同一瞬间丢失,进行中的认证流程、action token 和暴力破解计数会一起丢失,进行中的登录需要重来。

关于缓存层的深度调优——cache-ispn.xml 参数详解、传输栈选型对比、owners 参数调优、Istio 服务网格兼容方案、常见缓存故障排错,参见 Keycloak 集群缓存深度调优与排错指南。

需要容忍「整个 Kubernetes 集群 / 机房掉线」而不只是节点级故障时,单集群不够用,见 Keycloak 跨机房多集群高可用与 IAM 双活部署。

节点发现:JGroups 配置

Keycloak 使用 JGroups 做节点间通信和发现。Kubernetes 环境下推荐 DNS_PING;裸机推荐 JDBC_PING 或 TCPPING。

Kubernetes 环境(DNS_PING)

使用 Operator 部署时,只需在 CR 中指定 hostname 使 Pod 有稳定的 DNS 名。JGroups 默认使用 dns.DNS_PING 通过 Headless Service 发现对端。CR 里 spec.hostname 的完整字段(admin / strict / backchannelDynamic)与校验约束见 Keycloak Hostname v2 配置与 v1 选项迁移。

# 使用 Operator CR 的多节点配置要点
apiVersion: k8s.keycloak.org/v2beta1
kind: Keycloak
metadata:
  name: keycloak
spec:
  instances: 2          # 节点数
  hostname:
    hostname: sso.example.com
  additionalOptions:
    - name: cache
      value: ispn        # 使用 InfiniSpan 分布式缓存(默认)
    - name: cache-stack
      value: kubernetes  # JGroups 使用 kubernetes stack

当前 Keycloak CR 的 apiVersion 是 k8s.keycloak.org/v2beta1;v2alpha1 自 26.6 起标记为废弃,虽然仍然被服务,但新配置应使用 v2beta1(两个版本的 schema 目前没有差异,只需改 apiVersion)。

验证节点发现:

# 查看集群节点
kubectl -n keycloak exec deploy/keycloak -- \
  kcadm.sh get serverinfo --fields members -r master 2>/dev/null || \
  kubectl -n keycloak logs deploy/keycloak | grep "ISPN000094\|joined the cluster"

日志中看到 Received new cluster view 和 ISPN000094 表示节点已加入集群。

裸机/Docker Compose 环境(JDBC_PING)

如果不用 Kubernetes,推荐 JDBC_PING——节点通过数据库中的 JGROUPSPING 表相互发现,无需额外服务。

# 启动参数(每个节点用相同数据库但不同 --db 配置即可)
bin/kc.sh start \
  --cache=ispn \
  --cache-stack=jdbc-ping \
  --db=postgres \
  --db-url=jdbc:postgresql://postgres:5432/keycloak \
  --db-username=keycloak \
  --db-password=***

JDBC_PING 会自动在数据库中维护节点注册表,节点退出时自动清理。

Session 亲和性与负载均衡

多节点集群中,推荐启用 Session 亲和性(Sticky Session)。Keycloak 官方说明:用户会话在数据库和缓存中都可访问,但生产部署仍应尽量把用户路由到最初创建会话的节点,避免不必要的状态传输,改善 CPU、内存和网络利用率。

例外:启用 stateless 特性的跨机房方案(multi-cluster v2)不在内存中保存会话数据,官方明确此时配置会话亲和性不再有性能收益,可以改为轮询分发。见 Keycloak 跨机房多集群高可用与 IAM 双活部署。

Nginx 配置

upstream keycloak {
    ip_hash;  # 基于客户端 IP 的会话保持
    server keycloak-0.keycloak-headless:8443;
    server keycloak-1.keycloak-headless:8443;
}

server {
    listen 443 ssl;
    server_name sso.example.com;

    location / {
        proxy_pass https://keycloak;
        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;
    }
}

Kubernetes Ingress 配置

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: keycloak
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "KEYCLOAK_SESSION"
    nginx.ingress.kubernetes.io/session-cookie-path: "/"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "3600"
spec:
  rules:
  - host: sso.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: keycloak-service
            port:
              number: 8443

为什么不推荐 ip_hash → 替代方案:ip_hash 在企业 NAT/代理环境下会将大量用户分配到同一节点,负载不均。nginx.ingress.kubernetes.io/affinity: "cookie" 更可靠,但 Keycloak 本身有 Cookie(KEYCLOAK_SESSION、AUTH_SESSION_ID 等),Ingress Cookie 层只是加速路由,不影响 Keycloak 自身 Cookie。

备份策略

Keycloak 需要备份的三类数据:

数据类型存储位置备份方式频率建议
数据库PostgreSQL/MySQLpg_dump / mysqldump每日全量 + 持续 WAL 归档
Realm 导出Keycloak 管理控制台kc.sh export 或 Operator RealmImport每次重大配置变更后
自定义主题/SPI挂载的 Volume/ConfigMap纳入配置管理(Git/Helm)变更时触发
Realm 配置声明(config as code)Git 仓库 + Keycloak realm attributeskeycloak-config-cli 通过 Admin API 收敛,运行状态写回 realm attributes每次配置提交后

配置即代码工具与备份是互补关系,不能互相替代:keycloak-config-cli 只保证「Git 里声明的配置被应用过」,它不保存用户、会话、密钥,也不检测 Keycloak 侧的手工漂移。灾难恢复时仍然需要上面那份 kc.sh export 的 Realm JSON。两者的分工与误删防护见 IAM 配置即代码:keycloak-config-cli 声明式 Realm 管理与误删防护。

数据库备份(含验证)

#!/bin/bash
# 备份并验证
BACKUP_FILE="/backup/keycloak-$(date +%Y%m%d-%H%M).sql.gz"
pg_dump -h $DB_HOST -U keycloak -d keycloak | gzip > "$BACKUP_FILE"

# 验证备份可恢复(在独立实例上)
if [ "${VERIFY:-0}" = "1" ]; then
  zcat "$BACKUP_FILE" | psql -h verify-db -U keycloak -d keycloak_verify > /dev/null 2>&1
  echo "Backup verified: $BACKUP_FILE"
fi

关键提醒:只 dump 不验证 = 没有备份。建议每周至少一次自动恢复到独立验证实例。

Keycloak 配置导出

# 导出整个 Realm(含 client、role、group、identity provider 配置)
bin/kc.sh export --realm myrealm --dir /backup/realms/ --users realm_file

# 输出:/backup/realms/myrealm-realm.json

导出的 JSON 文件可以直接用 Operator 的 KeycloakRealmImport 恢复,也可以手动导入。

恢复流程(重要)

按顺序执行,每步都验证:

# 步骤 1:停止所有 Keycloak 节点写入
kubectl -n keycloak scale deploy/keycloak --replicas=0  # K8s
# 或 systemctl stop keycloak  # 裸机

# 步骤 2:恢复数据库
zcat /backup/keycloak-20260709-0100.sql.gz | psql -h $DB_HOST -U keycloak -d keycloak

# 验证恢复
psql -h $DB_HOST -U keycloak -d keycloak -c "SELECT COUNT(*) FROM REALM;"

# 步骤 3:如果需要,导入 Realm 配置
# 方式 A:通过 Operator(K8s)
kubectl -n keycloak apply -f realm-import.yaml
# 方式 B:导入数据库后重启节点自动使用已有 Realm 配置

# 步骤 4:启动集群
kubectl -n keycloak scale deploy/keycloak --replicas=2

# 步骤 5:验证集群就绪
kubectl -n keycloak rollout status deploy/keycloak
kubectl -n keycloak logs deploy/keycloak | grep "ISPN000094"

# 步骤 6:功能验证
curl -s https://sso.example.com/realms/master/.well-known/openid-configuration | jq .issuer
curl -s -X POST https://sso.example.com/realms/myrealm/protocol/openid-connect/token \
  -d 'client_id=test&grant_type=password&username=test&password=test' | jq '.access_token'

恢复后如果打不开管理控制台,通常不是数据库恢复失败,而是恢复点里没有可用的管理员凭据(例如备份取自密码轮换之前,或强化认证凭据在恢复后不匹配)。这种情形的恢复路径是停节点后执行 kc.sh bootstrap-admin,而不是重新恢复数据库,具体见 Keycloak 管理员账号进不去:bootstrap-admin 恢复与 26.x 变量变更。

同一套流程也是升级回滚的唯一路径:Keycloak 升级后数据库 schema 只能前滚,退回旧版本要恢复旧安装再按上面的步骤恢复数据库;升级前必须先判定这次能不能滚动、以及哪些变更会强制停机,见 Keycloak 升级与零停机滚动更新。

常见错误与排错

症状根因排错命令
节点启动后孤岛运行(只有自己)JGroups 发现机制配置错误grep -i "ISPN000094|JGRP" 看日志:无 “Received new cluster view” 说明未发现对端
登录后跳转到另一个节点就掉登录Session 亲和性未配置检查 Ingress/LB 是否配置 Sticky Session
InfiniSpan org.infinispan.remoting.transport.jgroups.SuspectException网络分区或节点间端口不通确认 JGroups 端口(默认 7800)在节点间互通:nc -zv <peer-ip> 7800
恢复数据库后 Realm 不显示缓存未刷新启动时加 --spi-connections-infinispan-quarkus-cache-remote-max-idle=1 清理缓存,或重启后等待缓存同步
备份恢复后用户登录报 invalid_grantSession/Token 状态不一致恢复的是数据库快照,快照时间点之后的会话与 token 状态丢失是正常的——用户重新登录即可;也可用 kc.sh export/import 配合 --users skip 保留数据一致性
集群节点全部重启后缓存的认证会话为空authenticationSessions / actionTokens / loginFailures 只在分布式缓存中,无持久化正常行为:用户会话与 client 会话在数据库中,用户不必重新登录;但进行中的认证流程会中断,需要重新发起登录
Kubernetes DNS_PING 报 java.net.UnknownHostExceptionHeadless Service 未创建或 DNS 未就绪检查 Service: kubectl get svc keycloak-headless;确认 Pod 内 nslookup keycloak-headless 可解析

生产检查清单

  • 数据库使用外部 PostgreSQL(非 H2),配置了主从或定期备份
  • 备份脚本有验证步骤,定期做恢复演练(至少每月一次)
  • 集群至少 2 个节点,分布于不同宿主机/可用区
  • 负载均衡器配置了 Session 亲和性
  • JGroups 节点发现已验证(启动日志中有 cluster view)
  • PodDisruptionBudget 已配置(K8s)
  • 监控已覆盖:节点数、JGroups 状态、数据库连接数、登录延迟(参考 Keycloak Prometheus 监控)
  • 有明确的恢复 Runbook(步骤、负责人、预期时长)
  • 节点间网络延迟 < 5ms(InfiniSpan 分布式缓存对网络延迟敏感)
  • Keycloak 版本、Operator 版本、CRD 版本保持一致

回滚方式

灾难恢复有两种回滚场景:

场景 A:恢复后发现问题,回滚到上一个备份

# 1. 停服
kubectl -n keycloak scale deploy/keycloak --replicas=0

# 2. 恢复到上一个备份
zcat /backup/keycloak-20260708-0100.sql.gz | psql -h $DB_HOST -U keycloak -d keycloak

# 3. 重新启动集群(缓存为空,从数据库重新加载)
kubectl -n keycloak scale deploy/keycloak --replicas=2

场景 B:数据库本身完好,只需重建 Keycloak 节点

# 用户会话与 client 会话在数据库中,认证会话在分布式缓存里;
# 逐个重建节点不会中断已登录用户,只会影响恰好在该节点上进行中的认证流程
kubectl -n keycloak rollout restart deploy/keycloak

重要:不要在生产环境中直接 DELETE Keycloak CR 再重建——Operator 会删除关联的 Secrets/Ingress 等资源。需要重建时,使用 rollout restart 或逐个替换 Pod。

延伸阅读