- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 前言
- 部署方式的选择 — Operator vs Helm
- Infinispan 缓存结构 — 会话与认证状态的存储库
- JGroups DNS_PING — Kubernetes 中的节点发现
- Persistent User Sessions — 26 的游戏规则改变者
- DB 选型与连接池
- Sticky Session 是否必要
- Multi-Site (Cross-DC) Active-Active
- 26.6 Zero-Downtime Rolling Patch
- 资源规格与 JVM 调优
- 健康检查与 Startup Probe
- 分故障场景的应对
- 结语
- 参考资料
前言
SSO 服务器是组织内所有服务共同依赖的单一入口。Keycloak 一旦宕机,登录就会停止;登录停止,实际上就等同于全公司级故障。因此 Keycloak 运维的核心课题始终是高可用性(HA)。
所幸 2026 年的 Keycloak 26.x 大幅降低了 HA 运维的难度。persistent user sessions 成为默认值后,“重启就全员登出”的问题消失了;26.6 的 zero-downtime rolling patch 使补丁升级时的无停机部署获得官方支持。本文将完整介绍在 Kubernetes 上构建并运维 Keycloak HA 集群的全过程。
- Operator vs Helm 部署方式的选择
- Infinispan 缓存结构与 JGroups DNS_PING 发现机制
- persistent user sessions 的含义与工作方式
- DB 选型、连接池、sticky session 之争的梳理
- multi-site(cross-DC)Active-Active 架构
- 资源规格、JVM 调优、健康检查
- 分故障场景的应对手册
部署方式的选择 — Operator vs Helm
在 Kubernetes 上部署 Keycloak 的代表性方式是官方 Operator 和社区 Helm chart(主要是 Bitnami 或 codecentric)。
| 项目 | Keycloak Operator(官方) | Helm chart(社区) |
|---|---|---|
| 维护主体 | Keycloak 项目官方 | 社区/厂商 |
| 抽象层级 | 用 Keycloak CR 声明 | 用 values.yaml 做精细控制 |
| 26.6 rolling patch 自动化 | 支持(update strategy) | 需手动配置 |
| realm import | KeycloakRealmImport CR | 初始化脚本 |
| 自定义镜像 | 支持(推荐模式) | 支持 |
| 精细的 Pod 控制 | 有限(可用 podTemplate 补充) | 自由 |
| 推荐对象 | 标准架构、重视运维自动化 | 非标准拓扑、已有 Helm 流水线 |
如果是新建环境,推荐官方 Operator。因为版本升级自动化和 26.6 的无停机补丁策略都已内置于 Operator 中。Operator 的安装方式如下。
kubectl create namespace keycloak
kubectl apply -n keycloak \
-f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
kubectl apply -n keycloak \
-f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
kubectl apply -n keycloak \
-f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/kubernetes.yml
以下是 Keycloak CR 的实战示例。
apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: keycloak
namespace: keycloak
spec:
instances: 3
image: registry.example.com/idp/keycloak-custom:26.6.2
startOptimized: true
db:
vendor: postgres
host: keycloak-db.database.svc.cluster.local
port: 5432
database: keycloak
usernameSecret:
name: keycloak-db-secret
key: username
passwordSecret:
name: keycloak-db-secret
key: password
poolMinSize: 10
poolInitialSize: 10
poolMaxSize: 30
hostname:
hostname: sso.example.com
strict: true
http:
httpEnabled: true
proxy:
headers: xforwarded
additionalOptions:
- name: log-console-output
value: json
- name: event-metrics-user-enabled
value: "true"
resources:
requests:
cpu: "1"
memory: 1500Mi
limits:
memory: 3Gi
update:
strategy: Auto
Infinispan 缓存结构 — 会话与认证状态的存储库
Keycloak 集群的心脏是内嵌的 Infinispan 缓存。节点间的状态共享全都在这里发生。主要缓存分类如下。
| 缓存名称 | 类型 | 用途 | 26+ 默认行为 |
|---|---|---|---|
| realms, users | local | DB 实体的读缓存 | 按节点本地,通过 invalidation 消息同步 |
| authorization | local | 授权策略缓存 | 按节点本地 |
| sessions, clientSessions | distributed | 登录会话 | DB 持久化 + 缓存 |
| offlineSessions | distributed | 离线会话 | DB 持久化 + 缓存 |
| authenticationSessions | distributed | 进行中的认证(登录表单阶段) | 集群分布 |
| loginFailures | distributed | 暴力破解计数器 | 集群分布 |
| work | replicated | 节点间 invalidation 的传播 | 全节点复制 |
| actionTokens | distributed | 邮件链接等一次性令牌 | 集群分布 |
用图来看这套结构如下。
+-----------------+ +-----------------+ +-----------------+
| Keycloak Pod 1 | | Keycloak Pod 2 | | Keycloak Pod 3 |
| | | | | |
| local: realms, | | local: realms, | | local: realms, |
| users | | users | | users |
| | | | | |
| distributed: | | distributed: | | distributed: |
| sessions(o2) <----> sessions(o2) <----> sessions(o2) |
| authSessions | | authSessions | | authSessions |
| | | | | |
| replicated: | | replicated: | | replicated: |
| work <----> work <----> work |
+--------+--------+ +--------+--------+ +--------+--------+
| | |
+----------+----------+----------+----------+
| JGroups (gossip) |
v v
+-------------+ +--------------+
| PostgreSQL | | DNS headless |
| (sessions | | service |
| 持久化) | | (DNS_PING) |
+-------------+ +--------------+
distributed 缓存默认 owners 数为 2,因此一个条目会复制到两个节点上。也就是说,即使一个节点宕机,会话数据依然存活。若同时失去两个节点,缓存上的数据可能丢失,但从 26 开始会话也会持久化到 DB,因此可以恢复。
JGroups DNS_PING — Kubernetes 中的节点发现
Infinispan 的集群成员管理由 JGroups 负责。Kubernetes 中组播被禁用,因此使用基于 DNS 的发现机制(DNS_PING)。工作原理很简单。
- headless Service 将所有 Keycloak Pod 的 IP 以 DNS A 记录暴露出来
- 各节点在启动时查询该 DNS 名称,获取对等节点列表
- JGroups 通过 7800 端口形成集群
使用 Operator 会自动完成配置,若要手动配置则如下。
apiVersion: v1
kind: Service
metadata:
name: keycloak-discovery
namespace: keycloak
spec:
clusterIP: None
publishNotReadyAddresses: true
selector:
app: keycloak
ports:
- name: jgroups
port: 7800
targetPort: 7800
# Keycloak 启动选项(以 StatefulSet/Deployment 的环境变量为准)
KC_CACHE=ispn
KC_CACHE_STACK=kubernetes
JAVA_OPTS_APPEND=-Djgroups.dns.query=keycloak-discovery.keycloak.svc.cluster.local
之所以要开启 publishNotReadyAddresses,是因为 Pod 必须在 readiness 之前的阶段就加入集群,启动过程中的会话再平衡才能正常工作。集群是否形成通过日志确认。
kubectl logs -n keycloak keycloak-0 | grep "ISPN000094"
# ISPN000094: Received new cluster view ... (3) [keycloak-0-..., keycloak-1-..., keycloak-2-...]
从 26.x 开始,JGroups 流量的 TLS 加密默认启用(Operator 部署时),节点间的会话数据不会以明文传输。
Persistent User Sessions — 26 的游戏规则改变者
在 Keycloak 24 之前,在线会话是纯内存的(Infinispan)。整体重启或多节点同时故障时,所有用户都会被登出。从 Keycloak 26 开始,persistent-user-sessions 功能默认启用,带来了以下变化。
- 所有 user session / client session 在创建时刻就写入 DB。
- Infinispan 降级为热数据缓存的角色,真实数据源变成 DB。
- 即使整个集群重启,用户依然保持登录状态。
- 内存使用量大幅下降(无需把全部会话都放在内存中)。
代价是 DB 写入负载增加。每次登录/登出/refresh 都会产生 DB 写入,因此必须把登录高峰场景(早上 9 点上班时段)的 DB IOPS 纳入容量估算。虽然可以关闭(从 features 中排除),但 26 的运维模型是以持久会话为前提设计的,若无特殊理由,建议保持默认值。
DB 选型与连接池
| 项目 | 推荐 | 理由 |
|---|---|---|
| DB 引擎 | PostgreSQL 15+ | 官方性能测试基准,Aurora PostgreSQL 已验证 |
| 隔离级别 | READ COMMITTED | 默认值,无需修改 |
| 连接池大小 | 每节点 max 30 左右 | 过大的连接池只会加重 DB 负担 |
| HA | Patroni / RDS Multi-AZ / Aurora | 避免让 DB 成为 SPOF |
| 连接池估算 | 以峰值并发请求为准 | 考虑登录 TPS x 平均查询数 |
连接池的计算公式并不简单,但按经验法则可以从“每 100 TPS 登录、每节点连接池 10-15”起步,再通过监控(agroal 指标)调整。连接池一旦耗尽,直接导致的不是登录变慢而是登录失败,因此必须对 db-pool 相关指标配置告警。
# Keycloak CR 中的连接池配置部分
db:
poolMinSize: 10
poolInitialSize: 10
poolMaxSize: 30
additionalOptions:
- name: transaction-xa-enabled
value: "false"
Sticky Session 是否必要
先说结论:以 26 为准并非必需,但依然有益。
- authenticationSessions(登录进行状态)是 distributed 缓存,因此请求发到任何节点都能处理。
- 不过如果持续路由到同一节点,直接命中 owner 节点的概率会提高,节点间 RPC 减少,延迟随之改善。
- Keycloak 会把节点信息编码进 AUTH_SESSION_ID cookie,利用它的 LB(例如 ingress-nginx 的 session affinity)自然就具备了 sticky 行为。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak
namespace: keycloak
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "KC_ROUTE"
nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
spec:
ingressClassName: nginx
rules:
- host: sso.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keycloak-service
port:
number: 8080
tls:
- hosts: [sso.example.com]
secretName: sso-tls
调大 proxy-buffer-size 是实务上的必备技巧。Keycloak 的响应头(尤其是包含令牌的重定向)超出默认缓冲区而导致 502 的案例相当常见。
Multi-Site (Cross-DC) Active-Active
26.x 的官方 multi-site 架构支持两站点 Active-Active。核心构成要素如下。
Site A (eu-west-1) Site B (eu-central-1)
+------------------------+ +------------------------+
| Keycloak (3 pods) | | Keycloak (3 pods) |
| | | | | |
| Infinispan (external) | <-----> | Infinispan (external) |
| cross-site replication| RELAY2 | cross-site replication|
+-----------+------------+ +-----------+------------+
| |
+----------------+-----------------+
|
+----------v-----------+
| Aurora Global DB |
| (writer: Site A) |
+----------------------+
^
+----------------+----------------+
| Global LB (Route53 / |
| 基于健康检查的 failover) |
+---------------------------------+
- 会话同步:外部 Infinispan 集群的 cross-site replication(RELAY2)
- DB:Aurora Global Database 这类单 writer 的全局 DB
- 路由:全局 LB 基于健康检查把流量分配到两个站点
- 得益于 persistent user sessions,即使站点间缓存同步失败也能经由 DB 恢复
multi-site 的运维复杂度非常高,因此只在确实存在 RTO/RPO 要求时才引入;在此之前,最好先评估单区域多 AZ 加上稳健的备份/恢复流程是否已经足够。详细内容请参考官方 HA 指南。
26.6 Zero-Downtime Rolling Patch
在 26.6 之前,任何版本升级都因缓存协议可能不兼容,默认采用把整个集群停掉再启动的 recreate 策略。从 26.6 开始,补丁版本之间(例如从 26.6.0 到 26.6.2)的兼容性得到保证,rolling update 获得官方支持。
# Keycloak CR
spec:
update:
strategy: Auto # 兼容性自动判定:可以则 rolling,否则 recreate
Auto 策略的行为如下。
- Operator 用新镜像运行 update-compatibility 检查任务
- 若缓存/配置兼容,则逐个替换 Pod(无停机)
- 若不兼容,则 recreate(整体重启,靠 persistent sessions 维持登录)
也可以手动检查兼容性。
# 在现有版本上生成元数据
bin/kc.sh update-compatibility metadata --file=/tmp/metadata.json
# 在新版本上检查
bin/kc.sh update-compatibility check --file=/tmp/metadata.json
echo $? # 为 0 则可以 rolling
资源规格与 JVM 调优
基于官方规格指南的起点如下。
| 负载指标 | 每 1 vCPU 的处理量(大致) | 备注 |
|---|---|---|
| 密码登录 | 每秒 15 次左右 | 深受哈希成本(argon2)影响 |
| client credentials 授权 | 每秒 120 次左右 | 最轻量的操作 |
| refresh token | 每秒 120 次左右 | 包含 DB 写入 |
| 内存(含非堆) | 每 Pod 1.25-3Gi | realm/客户端数量的影响大于会话数 |
从 26 开始,JVM 内存默认按容器内存的比例计算(默认堆 70%)。若要显式控制:
additionalOptions: []
# 或者通过环境变量
# JAVA_OPTS_KC_HEAP: "-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50"
resources:
requests:
cpu: "1"
memory: 1500Mi
limits:
memory: 3Gi
一般建议不要设置 CPU limit(避免节流带来的延迟尖刺)。内存 limit 为防止 OOMKill,要比堆+元空间+本地内存的总和留出更多余量。
健康检查与 Startup Probe
Keycloak 在管理端口(默认 9000)上提供 health 端点。
# 直接编写 Deployment/StatefulSet 时
livenessProbe:
httpGet:
path: /health/live
port: 9000
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 9000
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /health/started
port: 9000
periodSeconds: 5
failureThreshold: 60 # 最多 5 分钟的启动宽限
- started:判定启动完成。专供 startup probe 使用,考虑到迁移耗时较长的升级之后,failureThreshold 要给得宽裕些。
- ready:包含 DB 是否可连接。要知道 DB 瞬断时 Pod 会一齐变为 not-ready,看起来像是全面故障。
- live:进程本身是否存活。失败会触发重启,因此要保守设置。
此外,PodDisruptionBudget 与 topologySpreadConstraints 是 HA 的基本功。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: keycloak-pdb
namespace: keycloak
spec:
minAvailable: 2
selector:
matchLabels:
app: keycloak
分故障场景的应对
场景 1:单节点宕机
- 症状:几乎没有。distributed 缓存 owners 为 2 保住了会话,LB 会重新分配流量。
- 应对:确认 Pod 自动重建。查看日志确认是否已重新加入 JGroups 集群视图。
场景 2:DB 瞬断(failover 30 秒)
- 症状:全节点 readiness 失败,登录/令牌签发全面失败。已签发令牌的校验受影响较小(签名校验在本地完成)。
- 应对:确认 DB failover 的自动化是第一优先。Keycloak 在 DB 恢复时会自动复原,无需重启 Pod。反而要注意别把 liveness 设得太敏感,以免引发重启风暴。
场景 3:脑裂(网络分区)
- 症状:集群裂成两组,各自形成视图。暴力破解计数器/会话可能出现不一致。
- 应对:26 的默认配置会在分区合并时通过 MERGE 事件恢复。得益于 persistent sessions,会话数据会以 DB 为准收敛。若分区频繁发生,检查 CNI/节点网络才是根本对策。
场景 4:整体重启(灾难恢复)
- 症状:26 之前是全员登出,而在 26+ 会从 DB 恢复会话,登录状态得以保持。
- 应对:为应对从 DB 备份恢复这一最坏场景,另行保存 realm export(配置与数据的双重备份)。
# 定期 realm export(推荐用 CronJob 自动化)
bin/kc.sh export --dir /tmp/export --realm production --users different_files
场景 5:登录高峰(上班时段尖刺)
- 症状:CPU 饱和,密码哈希运算成为瓶颈。
- 应对:用 HPA 做水平扩展;由于哈希成本主导 CPU,横向扩容的效果非常直接。但要重新计算连接池上限,确保 DB 连接总数不超过 DB 的极限。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: keycloak-hpa
namespace: keycloak
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: StatefulSet
name: keycloak
minReplicas: 3
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
结语
Keycloak 26 时代的 HA 运维,已经从“想方设法哄好 Infinispan 的活儿”简化为“以 DB 为中心的普通有状态服务运维”。persistent user sessions 与 zero-downtime rolling patch 就是那个转折点。即便如此,JGroups 发现机制、连接池估算、探针调优这些基本功依然是运维者的分内事。请把本文的 YAML 示例当作起点,并且务必用自己环境的压测来验证这些数值。
下一篇文章将介绍扩展 Keycloak 功能本身的 SPI 开发(自定义 Authenticator、EventListener)。