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

说句掏心窝子的话,KingbaseLAC官方文档里那行"支持Kubernetes环境下发实例授权、VCPU授权",我第一次看到的时候差点没把咖啡喷屏幕上。

支持是支持的,但你仔细扒一下它的绑定逻辑,就会发现这件事远没有文档写的那么轻描淡写。

先搞清楚LAC到底拿什么来标识一个客户端

传统裸机环境下,LAC的绑定逻辑非常朴素——服务端绑MAC地址(授权文件LF需要提供服务端所在服务器的MAC),客户端通过 local_ip 上报自己的身份。你在安装客户端一键脚本的时候能看到这么一行:

local_ip=$(ip addr show $(ip route | grep default | awk '{print $5}') | grep -oP 'inet \K[\d.]+')

说白了就是取默认网卡上的第一个IPv4地址,写进 lac_agent.conflocal_ip 字段里。之后每次心跳,lac_agent就把这个IP带给服务端,服务端拿它当"你是谁"的唯一凭据。

裸机上这玩意稳得一批。网卡不变、IP不变、MAC不变,装一次管到机器报废。

但容器环境里呢?

Pod的IP是临时的,这不是秘密

K8s里Pod的IP(Pod IP)是CNI插件动态分配的,每次重建都可能变。哪怕你用了StatefulSet,Pod名字不变,IP照样会变——除非你额外搞了个Headless Service+固定IP的方案,但那已经不是常规操作了。

更要命的是Pod漂移(Pod Drift)。节点故障、资源回收、滚动更新、HPA扩缩容……任何一个场景都可能让Pod在另一个节点上重新拉起来,这时候新Pod拿到的是新节点的网段IP。

你把 lac_agent.conf 里的 local_ip 写死成Pod IP?那Pod一重建,授权就对不上了。服务端那边看到的是一张全新面孔,原来的授权还挂在老IP上,新Pod申请授权又占一个坑,几个轮回下来授权池就被幽灵记录撑爆了。

# 错误示范:把Pod IP写死
apiVersion: v1
kind: ConfigMap
metadata:
  name: lac-agent-config
data:
  lac_agent.conf: |
    lac_host = 10.20.30.40
    lac_port = 11234
    lac_type = Ent
    local_ip = 10.244.2.15    # 这个IP下次Pod重建就变了
    lac_interval = 5
    use_vcpu_limit = 0

我见过有人这么干,还纳闷为什么隔三岔五就有实例掉授权。

那用hostname呢?也不行

有聪明的同学想了——K8s里StatefulSet的Pod hostname是稳定的啊,kes-db-0kes-db-1这些,不会变。把 local_ip 换成hostname不就行了?

不好意思,LAC客户端那边的 local_ip 字段只接受IP地址,不接受域名或hostname。你去看 lac_agent.conf 的配置说明,写得很清楚——local_ip 的取值范围就是"IP地址"。

就算你硬塞个hostname进去,lac_agent解析出来的还是那个随时可能变的Pod IP,因为 /etc/hosts 或者DNS解析出来的就是CNI分配的那个临时地址。

MAC地址在容器里是个笑话

再说说MAC地址的事。LAC服务端的授权文件(LF)是绑MAC地址的——申请授权文件的时候就得提供LAC服务端所在服务器的MAC地址。这个在裸机上没什么问题,但服务端如果也跑在容器里呢?

容器里的MAC地址是veth pair的虚拟MAC,不是你宿主机的物理MAC。而且不同容器运行时(containerd、CRI-O)、不同网络插件(Calico、Cilium、Flannel)给出的虚拟MAC规则还不一样。你在Pod里执行 ip link show 看到的 eth0 的MAC地址,跟宿主机物理网卡的MAC完全是两回事。

# 在Pod里看到的MAC
$ ip link show eth0
3: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
    link/ether 02:42:0a:f4:02:0f brd ff:ff:ff:ff:ff:ff

# 在宿主机上看到的
$ ip link show ens192
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
    link/ether 00:50:56:a1:3b:2c brd ff:ff:ff:ff:ff:ff

完全不是一回事。如果你把LAC服务端也部署在K8s里,授权文件绑的到底是哪个MAC?虚拟网卡还是物理网卡?这个问题官方文档没有明确说,社区里也很少讨论,反正我第一次踩坑的时候是懵了。

更深层的问题:授权的"身份锚点"在容器里不存在

其实说了这么多,核心矛盾就一个:LAC的整个授权体系是建立在"物理机器身份不变"这个假设之上的。MAC不变、IP不变、机器不变,授权绑上去就不用管了。

但K8s的设计哲学恰恰相反——Pod是cattle不是pets。它不关心你是谁,只关心你需要几个副本。Pod可以随时被杀死、随时被重建、随时被调度到另一台机器上。在这种范式下,你拿IP和MAC当身份标识,就跟在沙滩上盖房子一样。

有人可能会说,那我用Node IP而不是Pod IP不就行了?确实,Node IP是稳定的,但你想想——K8s里Node也会增减啊。节点缩容了、节点换了、节点维护了,Node IP照样会变。而且一个Node上跑多个KES实例的场景太常见了,你用Node IP当标识,同一个节点上的多个实例怎么区分?

# 用hostNetwork的变通方案
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kes-db
spec:
  template:
    spec:
      hostNetwork: true    # 直接暴露宿主机网络
      dnsPolicy: ClusterFirstWithHostNet

hostNetwork: true 倒是能让Pod拿到宿主机IP,但这带来一堆新问题——端口冲突(多个实例不能共享同一端口)、安全风险(Pod直接暴露在宿主机网络上)、网络策略失效(NetworkPolicy对hostNetwork的Pod不生效)。你解决了授权绑定的问题,又制造了三个新问题。

那PVC呢?数据是持久的,授权文件能跟着走吗?

K8s里有持久化存储——PVC。KES的数据目录挂个PVC,Pod重建了数据不丢。那授权文件 license.dat 能不能也放进PVC里?

技术上当然可以。你把 license.dat 放到PVC挂载的目录下,Pod重建后文件还在,lac_agent启动时能读到,从文件内容来看授权没过期、没缺失,就不会向服务端重新申请。

但这里有个隐蔽的坑——lac_agent校验的不只是文件内容,它还要向服务端发心跳。心跳里带着 local_ip,服务端拿这个IP去匹配授权池里的记录。PVC保住了文件,但保不住IP。Pod重建后IP变了,服务端一看:这IP我没见过啊,你手里那个 license.dat 是发给别人(老IP)的,我不认。

# lac_agent心跳日志里的典型报错
[WARNING] license file exists but server rejected heartbeat: 
  client_ip=10.244.3.22 not match registered_ip=10.244.2.15

所以PVC能解决"文件丢失"的问题,但解决不了"身份漂移"的问题。这两个问题看似相关,实际上是独立的。

实例授权 vs VCPU授权——容器里该选哪个

官方文档里提到LAC支持两种授权类型在K8s环境下发:实例授权和VCPU授权。对应 lac_agent.conf 里的 use_vcpu_limit 参数——0是实例授权,1是VCPU授权。

这两个在容器场景下的表现完全不同,值得好好掰扯一下。

实例授权,顾名思义是按实例数量计量的。你有70个KES实例就需要70个license。这种模式在裸机上很直观——一台机器一个实例一个授权。但在K8s里,一个StatefulSet可能 replicas=5,这5个Pod是同一个逻辑集群的5个节点,你算5个实例还是1个实例?

从LAC的角度看,它不管你的Pod属于哪个StatefulSet,它只看有多少个 local_ip 在心跳。5个Pod就是5个不同的IP,就是5个实例授权。如果你的激活文件里只买了3个实例授权,对不起,第4个Pod申请的时候就会被拒绝。

# lac_agent.conf 实例授权模式
use_vcpu_limit = 0
lac_type = Ent

VCPU授权,是按CPU核心数计量的。比如你买了128核的VCPU授权,那所有客户端加起来最多用128个vCPU。这种模式在容器里看起来更合理——Pod资源请求是明确的(requests.cpu),你可以精确控制每个KES Pod用多少核。

但问题是,VCPU授权怎么计算Pod的vCPU数?是读Pod的 resources.requests.cpu?还是读 resources.limits.cpu?还是直接读cgroup里的实际配额?

官方文档里没有写清楚这个细节。我猜测lac_agent大概率是读容器运行时暴露的CPU配额信息,但这个信息在Pod重建后可能跟之前不完全一致(比如HPA改了request值),导致同一个Pod前后两次上报的VCPU数不同。服务端那边就懵了——你上次说要16核,这次说要8核,中间的差值是回收还是保留?

# VPU授权下的Pod资源配置
resources:
  requests:
    cpu: "8"
    memory: 32Gi
  limits:
    cpu: "16"
    memory: 64Gi

网络插件还会来捣乱

不同CNI插件对容器内MAC地址的处理方式完全不同,这个很多人可能没注意过。

Calico用的是veth pair模式,每个Pod的eth0对应宿主机上的一个veth设备,MAC地址是随机生成的。Flannel也是veth pair,但MAC地址的生成规则不一样。Cilium更离谱,它用eBPF做网络,有些模式下Pod里根本看不到传统的eth0——它的网络栈是在内核层面用BPF程序实现的。

# Calico环境下的Pod MAC
$ ip link show eth0
link/ether ea:58:ab:cd:12:34

# Flannel环境下
$ ip link show eth0  
link/ether 02:11:ab:cd:56:78

# Cilium环境下(某些模式)
$ ip link show eth0
link/ether 7a:42:ab:cd:90:ab

这意味着什么?意味着你在Calico环境下部署LAC拿到的MAC地址,换到Flannel环境下就对不上了。如果你做过集群迁移(比如从Calico迁到Cilium),LAC服务端的授权文件会直接失效——因为它绑的是Calico给你的那个虚拟MAC。

还有个更阴间的情况。有些CNI插件在Pod重启后会复用同一个veth设备(从而复用MAC),有些则不会。这导致同样的K8s操作(删除Pod再重建),在不同网络环境下对LAC的影响完全不同。你在Calico上测试没问题,不代表换到Cilium上也行。

Pod生命周期事件对lac_agent的影响

再说一个容易被忽视的问题——Pod的优雅终止(Graceful Termination)。

当K8s要删除一个Pod时,先发SIGTERM给容器里的进程,等 terminationGracePeriodSeconds(默认30秒)之后再发SIGKILL。在这30秒内,lac_agent应该做三件事:向服务端发送最后一次心跳(“我要下线了”)、释放当前持有的授权、清理本地license.dat。

但问题是lac_agent的设计里并没有优雅下线的逻辑。你看它的命令列表——startstopstatusstop命令会停进程和crontab,但不会主动通知服务端"我要释放授权了"。所以Pod被删的时候,lac_agent直接被SIGTERM干掉了,授权还挂在服务端那边,直到心跳超时才被回收。

# lac_agent stop的实际行为
$ lac_agent stop
# 停止lac_agent常驻进程
# 停止定时任务(unset crontab)
# 注意:不会主动通知服务端释放授权

如果你设了 lac_interval=10heart_offline_times=5,那一个被删除的Pod要等50分钟之后,它占的授权才会被服务端回收。如果这时候有HPA扩容,新Pod起来要申请授权,池子满了就会被拒绝。

这个问题在裸机环境下影响不大——谁没事天天重启数据库服务器呢。但在K8s里,Pod被调度、被驱逐、被更新是家常便饭,这个50分钟的回收延迟就变成了实实在在的授权浪费。

多副本StatefulSet的授权爆炸

还有一种场景值得单独拿出来说——多副本StatefulSet做读写分离。

典型的KES读写分离集群架构是1主2备或1主3备。在K8s里通常用StatefulSet部署,replicas=3replicas=4。每个Pod都是一个完整的KES实例,都有自己的data目录、自己的进程、自己的lac_agent。

问题来了——这3个Pod在LAC眼里是3个独立客户端,需要3个授权。但你的业务逻辑里它们是一个集群,逻辑上应该只算"一套数据库"。

# 1主2备的StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kes-cluster
spec:
  replicas: 3    # LAC认为这是3个独立客户端
  template:
    spec:
      containers:
      - name: kes
        # 每个Pod都会启动lac_agent
        # 每个Pod都会申请一个独立的实例授权

如果你买了10个实例授权,跑了3个三节点集群,就用掉了9个。看起来没问题对吧?但只要有一个Pod因为节点维护被驱逐重建,重建期间它既占着老授权(服务端还没回收)又要申请新授权(新IP嘛),瞬间就变成11个——超了。

这时候你要么紧急扩容授权池(花钱),要么临时停掉一个集群的备节点(影响高可用),要么在LAC WEB管理界面上手动回收幽灵授权(半夜爬起来操作)。

我有一次就是这么栽的——三个集群共9个Pod,某天节点维护触发了4个Pod同时重建,授权池瞬间被撑爆,第10个Pod拿不到授权,整个集群的备节点全挂了。主节点还在,但运维那边以为全挂了,差点启动容灾切换。

说来也巧,我在社区发帖吐槽这个坑的时候,看到金仓社区刚开了个"同行者计划"——荐商机赢好礼的活动。说白了就是你遇到什么坑、有什么需求,整理一下反馈给社区,审核通过就有奖励。我想了想,LAC在容器环境下的这些痛点,与其自己一个人折腾,不如整理成案例报上去,说不定哪天LAC新版本就把这些问题解决了。活动详情在这:https://bbs.kingbase.com.cn/forumDetail?articleId=1d09d598f414ab764eda4907e8f54758

到目前为止我们搞清楚了什么

容器化环境下LAC授权面临三个根本性冲突:

  1. IP临时性 vs 身份绑定:Pod IP随重建而变,LAC用IP标识客户端身份
  2. MAC虚拟化 vs 物理绑定:容器里的MAC是虚拟的,LAC服务端授权绑的是物理MAC
  3. 资源弹性 vs 授权刚性:Pod的资源可以动态调整,但授权池的数量是刚性的

这三个冲突不解决,你在K8s里跑LAC就是定时炸弹。至于怎么解决——下篇来聊,我踩了不少坑才找到一条能用的路。

更多推荐