LAC容器化授权困境(下篇):K8s环境下的授权锚定实战

文章目录



兼容 是对前人努力的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点


上篇把问题讲清楚了------Pod IP会变、MAC是虚拟的、资源弹性跟授权刚性有冲突。这篇聊怎么在实际项目里把这些坑填上。

先声明,我试过的方案有好几个,有些能用有些不能用,有些能凑合用但得加一堆补丁。没有一个方案是完美的,但有至少一条路能让你在生产环境里睡着觉。

方案:Init Container + 动态注册------我的最终方案

折腾了一圈之后,我最后用的是一个"曲线救国"的方案------让lac_agent启动前先搞清楚自己该叫什么名字,而不是依赖自动获取的IP。

思路是这样的:用StatefulSet的序号(ordinal index)作为客户端的唯一标识,启动时通过一个Init Container把自己的"逻辑身份"注册到LAC服务端的一个映射表里,然后把这个逻辑身份对应的某个固定IP(比如Headless Service解析到的IP)写进 lac_agent.conf

yaml 复制代码
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kes-db
spec:
  serviceName: kes-db-headless
  replicas: 5
  template:
    spec:
      initContainers:
      - name: lac-register
        image: busybox:1.36
        command:
        - sh
        - -c
        - |
          # 获取Pod的序号,作为稳定标识
          ORDINAL=${HOSTNAME##*-}
          # 生成逻辑IP(用10.99.x.x段作为标识段,不参与实际网络通信)
          LOGIC_IP="10.99.1.${ORDINAL}"
          
          # 写lac_agent.conf
          cat > /lac-config/lac_agent.conf << EOF
          lac_host = lac-server.lac-system.svc.cluster.local
          lac_port = 11234
          lac_type = Ent
          local_ip = ${LOGIC_IP}
          lac_interval = 10
          use_vcpu_limit = 0
          enable_auto_refresh = 1
          db_paths = /data/kingbase/data
          EOF
          
          echo "Registered kes-db-${ORDINAL} with logic_ip=${LOGIC_IP}"
        volumeMounts:
        - name: lac-config
          mountPath: /lac-config
      containers:
      - name: kes
        image: kingbase/kes:v9r2c14
        volumeMounts:
        - name: lac-config
          mountPath: /opt/Kingbase/ES/V9/Server/share
        - name: data
          mountPath: /data/kingbase
      volumes:
      - name: lac-config
        emptyDir: {}
      - name: data
        persistentVolumeClaim:
          claimName: data-kes-db-0   # 每个Pod用自己的PVC

这里的关键点是 local_ip = 10.99.1.${ORDINAL}。这个IP不是Pod的真实网络IP,而是一个逻辑标识。LAC服务端心跳来的时候,看到的就是 10.99.1.010.99.1.1 这种格式的地址,它们跟Pod的实际网络位置无关,只跟StatefulSet的序号绑定。

Pod重建了?没关系,序号没变,local_ip 还是同一个值。Pod漂移到另一个节点了?也没关系,逻辑IP跟节点无关。

但这个方案有个前提------LAC服务端得认这个逻辑IP。也就是说,服务端那边的路由表里不需要真的能路由到 10.99.1.x,因为lac_agent和服务端之间的通信用的是K8s的Service DNS(lac-server.lac-system.svc.cluster.local),跟 local_ip 无关。local_ip 只是lac_agent上报给服务端的"我叫什么",服务端不会尝试回访这个IP。

我反复验证过这一点------lac_agent.conf 里的 local_ip 确实只是个身份标识字段,不是通信地址。通信地址是 lac_hostlac_port

实例授权 vs VCPU授权------容器场景下的选型建议

回到授权类型选择的问题。经过几个项目的实践,我的建议是:

如果你的K8s集群规模不大(30个KES Pod以内),而且实例数量比较固定,选实例授权。

理由很简单:实例授权的逻辑最清晰,一个Pod对应一个授权,跟裸机时代的思维一致。

PVC挂载与授权文件的持久化

不管选哪种方案,PVC的配置都有讲究。KES的数据目录必须挂PVC,这没商量。但 lac_agent.conflicense.dat 放在哪?

我的做法是分开挂------数据用PVC持久化,配置文件用ConfigMap+emptyDir:

yaml 复制代码
# ConfigMap存放lac_agent.conf
apiVersion: v1
kind: ConfigMap
metadata:
  name: kes-lac-config
data:
  lac_agent.conf: |
    lac_host = lac-server.lac-system.svc.cluster.local
    lac_port = 11234
    lac_type = Ent
    lac_interval = 10
    use_vcpu_limit = 0
    enable_auto_refresh = 1
    log_level = INFO
---
# Secret存放license.dat(如果有的话)
apiVersion: v1
kind: Secret
metadata:
  name: kes-license
type: Opaque
data:
  license.dat: <base64-encoded-license-content>
---
# StatefulSet里的挂载
spec:
  template:
    spec:
      containers:
      - name: kes
        volumeMounts:
        - name: data
          mountPath: /data/kingbase
        - name: lac-config
          mountPath: /opt/Kingbase/ES/V9/Server/share/lac_agent.conf
          subPath: lac_agent.conf
        - name: license
          mountPath: /data/kingbase/license.dat
          subPath: license.dat
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: data-{{.PodName}}
      - name: lac-config
        configMap:
          name: kes-lac-config
      - name: license
        secret:
          secretName: kes-license

这样 lac_agent.conf 通过ConfigMap管理,改配置只需要 kubectl edit configmap 然后重启Pod。license.dat 放在PVC的数据目录下,这样即使Pod重建,文件也在。

但要注意------如果你用的是上面方案二的逻辑IP方案,ConfigMap就不能写死 local_ip 了,得用Init Container动态生成。所以实际上ConfigMap里只放通用配置,local_ip 由Init Container追加:

bash 复制代码
# Init Container里的逻辑
cat /config-map/lac_agent.conf > /runtime-config/lac_agent.conf
echo "local_ip = 10.99.1.${ORDINAL}" >> /runtime-config/lac_agent.conf

LAC服务端的部署位置

这个也很重要------LAC服务端放哪?

放在K8s集群内?放在集群外的裸机上?放在另一个K8s集群里?

我的建议是放在K8s集群外,最好是一台稳定的裸机或虚拟机。原因有三:

第一,LAC服务端是基础设施级别的服务,它的稳定性不应该依赖业务集群。如果K8s集群出了问题(比如etcd挂了、控制面故障),LAC也跟着挂了,那所有KES客户端都拿不到授权、心跳也续不上,整个就死锁了。

第二,LAC服务端的授权文件绑MAC地址。如果服务端也跑在K8s里,你用的是虚拟MAC,前面说过了,这东西不稳定。

第三,运维职责分离。LAC是授权系统,归安全或运维团队管;K8s是业务平台,归开发或平台团队管。两个系统放在同一个集群里,权限边界就模糊了。

bash 复制代码
# LAC服务端部署在独立虚拟机上
# /opt/KingbaseLAC/bin/lac_server.conf
port = 11234
max_workers = 16          # 大规模客户端场景调大
receive_timeout = 15      # 容器网络延迟可能更大,给宽一点
heart_offline_times = 5   # 配合lac_interval=10,判定窗口50分钟

K8s集群内的Pod通过NodePort或LoadBalancer Service访问集群外的LAC服务端:

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: lac-server-external
spec:
  type: ExternalName
  externalName: lac-server.internal.company.com  # LAC服务端的可达地址
---
# 或者直接用IP
# lac_agent.conf里写
lac_host = 192.168.1.100   # LAC服务端物理机IP
lac_port = 11234

生产环境跑了一年半的监控配置

最后说说监控。LAC在容器环境里跑,你不能只靠肉眼看WEB管理界面。必须把监控接进去。

我的做法是写个sidecar容器,定期调lac_agent的status命令,把结果暴露成Prometheus metrics:

bash 复制代码
#!/bin/bash
# lac-metrics-exporter.sh
while true; do
  STATUS=$(lac_agent status 2>&1)
  
  # 解析状态
  if echo "$STATUS" | grep -q "running"; then
    LAC_RUNNING=1
  else
    LAC_RUNNING=0
  fi
  
  # 检查license有效性
  VALID_DAYS=$(ksql -U system -d test -c "select get_license_validdays();" -t 2>/dev/null | head -1)
  
  # 输出Prometheus格式
  cat > /metrics/lac_metrics.prom << EOF
# HELP lac_agent_running Whether lac_agent is running (1=running, 0=stopped)
# TYPE lac_agent_running gauge
lac_agent_running ${LAC_RUNNING}
# HELP lac_license_valid_days Remaining valid days of license
# TYPE lac_license_valid_days gauge
lac_license_valid_days ${VALID_DAYS:-0}
EOF
  
  sleep 60
done
yaml 复制代码
# Sidecar容器配置
- name: lac-metrics
  image: busybox:1.36
  command: ["/bin/sh", "/scripts/lac-metrics-exporter.sh"]
  ports:
  - containerPort: 9100
    name: metrics
  volumeMounts:
  - name: metrics
    mountPath: /metrics
  - name: scripts
    mountPath: /scripts

然后在Grafana里配几个关键告警:

  • lac_agent_running == 0:lac_agent挂了,立刻告警
  • lac_license_valid_days < 30:授权快到期了,提前一个月告警
  • lac_license_valid_days < 7:授权即将到期,紧急告警
  • 心跳中断持续时间 > 15分钟:可能掉授权了

这套东西搞完之后,至少我不用再半夜被叫起来看授权有没有掉了。虽然不能说100%无忧,但至少能睡个囫囵觉。

容器环境下心跳参数的特殊考量

容器网络跟裸机网络有个本质区别------延迟和抖动更大。裸机上两台服务器在同一个VLAN里,ping一下0.2ms。容器里跨节点通信,经过veth pair、iptables、overlay网络(如果是Flannel之类的隧道模式),一个来回可能要2~5ms,网络抖动的时候能到50ms以上。

这对LAC的心跳机制有什么影响呢?首先 receive_timeout 不能卡太死。默认5秒在裸机上绰绰有余,但容器里如果赶上节点负载高或者overlay网络拥堵,5秒可能不够lac_agent把一个心跳包发完整。特别是当多个Pod同时向服务端发心跳的时候,服务端的 max_workers 如果不够大,处理队列会积压,某些请求就会因为超时而失败。

我在一个20个KES Pod的项目里调过这个参数。开始用的默认值 max_workers=8,结果每隔几个小时就会有1~2个Pod的心跳超时。后来把 max_workers 调到16,同时把 receive_timeout 从5调到10,问题就消失了。

ini 复制代码
# 容器环境推荐配置(LAC服务端 lac_server.conf)
max_workers = 16          # 至少是Pod数量的50%~80%
receive_timeout = 10      # 容器网络延迟补偿
heart_offline_times = 5   # 配合lac_interval=10,50分钟判定窗口

客户端那边的 lac_interval 也要考虑容器的资源限制。如果Pod的CPU request只有500m,lac_agent本身要占一点CPU,KES主进程也要占CPU,在资源紧张的时候lac_agent可能被cgroup限流导致心跳延迟。这时候 lac_interval=5 就太紧了,因为5分钟一次心跳,留给延迟恢复的窗口太小。调到10~15分钟,给lac_agent更多的喘息空间。

ini 复制代码
# 容器环境推荐配置(客户端 lac_agent.conf)
lac_interval = 10         # 别太激进,容器里CPU资源可能紧张
enable_auto_refresh = 1   # 这个永远是1,不要动

幽灵授权的清理

用了一年多容器化LAC之后,我发现一个很烦人的问题------幽灵授权会慢慢累积。

什么是幽灵授权?就是一个Pod被删了、重建了、IP变了,老IP上的授权没有被及时回收,一直挂在服务端的已发放列表里。虽然心跳超时后服务端会把状态标成offline,但offline的授权并不会自动从池子里删掉。它在WEB管理界面上一直显示着,只是标了个灰色的"离线"。

时间一长,服务端可能积累了上百条offline记录,而激活文件里的授权总数是有限的。这些幽灵记录虽然不直接占license配额(offline的授权理论上可以被重新分配),但它们会干扰你的判断------你在WEB界面上看,100个客户端里只有30个在线,你以为授权池有70个富余,但其实那70个offline记录对应的Pod可能随时重新上线(比如之前只是网络抖动,Pod进程其实还在)。

bash 复制代码
# 通过lac_manager命令行清理offline的授权记录
# 先查看所有客户端状态
./lac_manager list-clients

# 手动回收某个offline客户端的授权
./lac_manager revoke --client-ip 10.244.2.15

# 批量清理(写个脚本)
for ip in $(./lac_manager list-clients --status offline | awk '{print $1}'); do
  echo "Revoking $ip ..."
  ./lac_manager revoke --client-ip $ip
done

我后来写了个定时清理脚本,每周跑一次,把所有offline超过7天的记录清掉。这个脚本放在LAC服务端的crontab里:

bash 复制代码
# /opt/KingbaseLAC/scripts/cleanup-ghost.sh
#!/bin/bash
LAC_BIN=/opt/KingbaseLAC/bin
LOG=/opt/KingbaseLAC/log/cleanup.log

echo "[$(date)] Starting ghost license cleanup..." >> $LOG

# 获取offline超过7天的客户端IP列表
OFFLINE_CLIENTS=$($LAC_BIN/lac_manager list-clients --status offline --days-since-heartbeat 7 | awk 'NR>2 {print $1}')

for ip in $OFFLINE_CLIENTS; do
  $LAC_BIN/lac_manager revoke --client-ip $ip 2>>$LOG
  echo "[$(date)] Revoked $ip" >> $LOG
done

echo "[$(date)] Cleanup done. Processed $(echo "$OFFLINE_CLIENTS" | wc -l) clients." >> $LOG
cron 复制代码
# 每周日凌晨3点清理
0 3 * * 0 /opt/KingbaseLAC/scripts/cleanup-ghost.sh

这个不算是多高级的方案,但确实帮我避免了授权池被幽灵记录占满的问题。如果LAC能加一个"自动回收offline超过N天的授权"的配置项就好了,但目前没有。

容器化部署的完整清单

搞了这么久,我把容器化环境下部署LAC的checklist整理一下,给后面的人省点时间:

复制代码
□ LAC服务端部署在K8s集群外的物理机或虚拟机上
□ LAC服务端授权文件绑定的是物理机MAC,不是虚拟MAC
□ lac_server.conf中max_workers≥Pod数量×60%
□ lac_server.conf中receive_timeout≥10
□ lac_server.conf中heart_offline_times≥3(根据网络质量调整)
□ 客户端lac_agent.conf中local_ip使用逻辑标识而非Pod IP
□ 客户端lac_agent.conf中enable_auto_refresh=1
□ lac_agent通过Init Container动态注入local_ip
□ license.dat通过Secret或PVC持久化
□ lac_agent.conf通过ConfigMap管理,支持热更新
□ 配置Prometheus监控lac_agent运行状态
□ 配置授权到期提前30天告警
□ 配置offline授权定期清理(每周)
□ 预留授权池余量≥实际需求的30%(应对Pod重建期间的双重占用)
□ HPA的最大副本数×单实例授权数 < 激活文件总授权数
□ 确认CNI插件类型,评估MAC地址变化对服务端的影响

这份清单不完美,但能覆盖大部分坑了。

上面这些脚本和配置,说难听点都是在打补丁。我其实一直想写个更完善的LAC容器化运维工具------自动处理Pod漂移的授权迁移、自动清理幽灵记录、自动适配CNI插件的MAC变化......但一直没抽出时间。前两天看到金仓社区在办"2026智能运维工具开发大赛",感觉这个方向还挺对路的,正好把我这一年多踩的坑沉淀成工具代码交上去。不管参不参赛,这个方向确实值得有人去做

传送门:

回过头来想想

容器化环境下搞授权管理,本质上是在两种完全不同的设计哲学之间找平衡。K8s说"一切都是临时的",LAC说"一切都是固定的"。你不可能让其中一个完全迁就另一个,只能在中间找到一条勉强能走的路。

我的方案二(Init Container+逻辑IP)不是最优解,但它是在"不改LAC底层代码"的前提下能做到的最稳的方案了。如果LAC团队未来能支持用自定义标识(比如hostname或UUID)替代IP作为客户端身份,那容器化授权的问题就能从根本上解决。

但在那之前,我们只能在夹缝中求生存。