
### 摘要

Kube-OVN 的固定 IP 分配机制主要通过以下三个核心决策与设计实现：
1. 持久化层引入名为 IP 的 CRD 资源，并借助 ipsLister 机制对其进行高效的缓存读取与状态同步。
2. 在删除 Pod 阶段（Controller 协调），通过判断 Pod 是否归属于未被删除的 StatefulSet 或 VirtualMachine，选择保留其对应的 IP CRD 资源与内存 IPAM 状态，而不是像普通 Pod 一样将其释放。
3. 在 Pod 重建阶段，由于 StatefulSet 的 Pod 名字具备唯一性，Controller 会基于一致的网口名称（nicName）向 IPAM 申请 IP。IPAM 内存状态检测到该 nicName 已有 IP 映射，从而实现对已有 IP 的复用。

有关 Kube-OVN 的背景和更详细说明，可以参考「[Kube-OVN 官方文档](https://kube-ovn.github.io/)」。

---

### 背景与挑战

在 Kubernetes 的默认设计中，Pod 的网络配置具备动态性与瞬时性。Pod 重启或重建通常意味着其旧有的 IP 会被回收，并由 CNI 插件从 IP 池中随机分发一个新的 IP。

然而，在诸如传统数据库集群、主从架构状态服务，以及虚拟化桥接等场景中，服务通常依赖于稳定的 IP 地址。这就要求容器网络方案能够提供「固定 IP」的能力。Kube-OVN 针对这一痛点，通过结合 Kubernetes 自定义资源、控制器调谐逻辑与内置的 IPAM（IP 地址管理）引擎，实现了两类固定 IP 分配能力：
1. 静态 IP 分配：用户在 Pod 的注解（Annotations）中显式指定一个 IP。
2. 状态保持的固定 IP（StatefulSet / VM）：当 Pod 重构或重启时，无须指定注解，系统会自动保持原有的 IP 不变。

---

### 核心设计与辅助资源

Kube-OVN 通过一套底层的自定义资源（CRD）和内存缓存读取器来实现网络配置的持久化与状态同步。

#### 1. IP 资源（CRD）

Kube-OVN 在设计上并没有只依靠 OVN 或内存状态来管理 IP，而是定义了专有的 CRD 资源，定义文件在 [pkg/apis/kubeovn/v1/ip.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/apis/kubeovn/v1/ip.go#L32)。

其中，IP struct 代表了一个虚拟网卡或者说 IP 分配的实例。其关键的属性如下：
- PodName: 该 IPCR 绑定的 Pod 名称。
- Namespace: 对应的命名空间。
- Subnet: 分配该 IP 的主子网。
- NodeName: 该 Pod 被调度并运行的节点。
- V4IPAddress / V6IPAddress: 已分配的 IPv4 和 IPv6 地址。
- MacAddress: 已分配的 MAC 地址。
- PodType: 记录 Pod 的所属工作负载类型（如 StatefulSet，VirtualMachine，或 Deployment）。

#### 2. ipsLister 的作用

在 Kube-OVN 控制器（kube-ovn-controller）中，ipsLister 是只读的客户端缓存组件（类型为 kubeovnlisters.IPLister）。
- 它的作用是从 client-go 控制器维护的本地 Informer 缓存中，以极高的效率检索和获取集群中已存在的 IP 资源，避免频繁地向 Kubernetes API Server 发送网络请求。
- 在逻辑上，它是 IP 这一 CRD 资源在内存中的反射镜像窗口。
- 在控制器初始化时，它用于批量列出（List）所有 IP 实例来同步内存 IPAM 状态；在调谐与清理流程中，它用于单例查询（Get）特定 Pod 网络端口的 IP 信息。

---

### 固定 IP 工作流与机制

Kube-OVN 固定 IP 的生命周期涵盖了：准入控制、Controller 地址分配与持久化、Pod 删除时的保留决策、以及 CNI 配置。

#### 流程 1：静态 IP 准入与校验

当用户显式配置了静态 IP 注解「ovn.kubernetes.io/ip_address」时，Kube-OVN 的 Mutating/Validating Webhook 会在 Pod 创建请求被处理前进行拦截。

图 1：Webhook 静态 IP 校验与准入流程

```mermaid
graph TD
    A[用户创建/更新 Pod] --> B[Webhook 拦截请求]
    B --> C{是否配置静态 IP/IPPool 注解?}
    C -- 是 --> D[验证 IP CIDR 格式合法性]
    D --> E[验证 IP 协议家族唯一性]
    E --> F[从 cache 中检索已分配 IP 列表]
    F --> G{是否存在 IP 地址冲突?}
    G -- 是 --> H[拒绝请求: 返回 Denied 并阻断 Pod 创建]
    G -- 否 --> I[放行请求: 允许 Pod 创建继续]
    C -- 否 --> I
```

图 1 描述：此图展示了 Webhook 在 API 准入阶段的处理逻辑。通过解析 Pod 注解中的 IP 请求，如果存在冲突或非法的多网卡同协议 IP 地址（例如在一个注解里重复写两个 IPv4），则直接阻断 Pod 调度。

文字实现机制：
在 [pkg/webhook/static_ip.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/webhook/static_ip.go#L104) 中：
- ValidatingHook 的 PodCreateHook 方法被调用。
- validateIPConflict 函数调用 checkIPAddressFamilyUniqueness 检测指定 IP 是否存在同一家族的多地址异常（例如填入了两个 IPv4 地址）。
- 调用 checkIPConflict 比对现存的 IP CRD 列表（利用 cache 列表检索），防止已被占用的静态 IP 被二次分配。

#### 流程 2：Controller 的 IP 分配与保存机制

Controller 在调谐 Pod 时，会分配网络。入口函数在 [pkg/controller/pod.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/controller/pod.go#L622) 的 reconcileAllocateSubnets。

对于 StatefulSet 或 VM，Pod 随时可能因为升级、重启而被重建。如果重建时 IP 被释放，新建的 Pod 就会拿到新的随机 IP。因此其生命周期的保存是固定 IP 的核心逻辑。

图 2：Pod 生命周期协调与地址保存逻辑

```mermaid
graph TD
    A[Pod 被删除] --> B{是否为 StatefulSet/VM?}
    B -- 是 --> C{Owner 资源是否也被删除?}
    B -- 否 --> D[普通清理: 删除 IP CRD 且释放 IPAM 地址]
    C -- 否 --> E[保留模式: keepIPCR 为 true, 跳过 IP CRD 与 IPAM 清理]
    C -- 是 --> D
```

图 2 描述：此图展示了 Pod 被销毁时，kube-ovn-controller 在 handleDeletePod 中的协调决策。如果是正常的 RollingUpdate 或手动删除 Pod 触发的重建，只要 Owner 资源（StatefulSet/VM）依然存在，IP CRD 状态将保持不变，IP 资源不会被释放回 IP 池。

文字实现机制：
- 调谐分配：reconcileAllocateSubnets 调用 acquireAddress。在 acquireAddress 内部，若识别到 Pod 带有静态 IP 注解，会分流至 acquireStaticAddressHelper。通过 acquireStaticAddress 进一步调用内存 IPAM（即 [pkg/ipam/ipam.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/ipam/ipam.go#L68) 中的 GetStaticAddress）预留特定 IP，并调用 createOrUpdateIPCR 方法创建 IP CRD 实例。同时在 Pod 的注解上打上代表分配完成的「ovn.kubernetes.io/allocated: "true"」以及 IP、MAC、子网等参数。
- 销毁保护：在 [pkg/controller/pod.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/controller/pod.go#L1100) 的 handleDeletePod 逻辑中，识别 Pod 归属。如果只是 Pod 本身被删除，而 StatefulSet 资源没有被删，Controller 会判定 keepIPCR = true。若 keepIPCR 为 true，程序在 [L1324-L1328](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/controller/pod.go#L1324-L1328) 将执行 skip clean ip CR 逻辑，跳过对该 Pod IP CRD 资源的删除，也不从内存 IPAM 释放对应的 IP（不调用 ReleaseAddressByNic）。

#### 流程 3：Controller 启动重建与内存 IPAM 初始化

一旦 Controller 重启，或者新主节点选举发生，Controller 需要保证内存 IPAM 数据的正确性。这由 [pkg/controller/init.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/controller/init.go#L438) 处的 Init 流程处理。

图 3：启动时从持久化存储恢复内存 IPAM 状态

```mermaid
graph TD
    A[Controller 启动初始化] --> B[第一阶段: 扫描集群 IP CRD]
    B --> C{是否为 StatefulSet 或 VM 类型的 IPCR?}
    C -- 是 --> D[调用 GetStaticAddress 注册并锁定至内存 IPAM]
    C -- 否 --> E[跳过，留待存活 Pod 扫描阶段处理]
    D --> F[第二阶段: 扫描存活 Pod 列表]
    E --> F
    F --> G{Pod 是否运行且已分配网络?}
    G -- 是 --> H[加载 Pod 注解中的 IP/MAC 至内存 IPAM]
    H --> I[调用 createOrUpdateIPCR 确保同步]
    G -- 否 --> J[跳过]
    I --> K[初始化完成]
    J --> K
```

图 3 描述：此图展示了 Controller 重启时加载已分配 IP 的过程。系统采用两阶段扫描，首先把被保留在集群中的 StatefulSet/VM IP CRD 信息同步至内存，随后扫描存活 Pod 补充加载普通 Pod 网络信息，从而防范了重启导致内存状态丢失引起 IP 被二次分配的风险。

文字实现机制：
- 首先，Init 调用 c.ipsLister.List(labels.Everything()) 扫描所有的 IP 资源。
- 检查 ip.Spec.PodType。对于工作负载类型为 StatefulSet 或 VM 的 IP CRD，无论关联的 Pod 此时是否存在，均将其对应的 IP、MAC 通过 c.ipam.GetStaticAddress 加载回内存 IPAM 中，将其锁定。
- 其次，它扫描存活的 Pod 列表，补充加载运行中的 Pod 占用的 IP 资源。

#### 流程 4：Pod 重建时的 IP 复用

当相同的 StatefulSet Pod 重建时，其名称不变（例如 sts-0）。Controller 会重新调谐该 Pod。

图 4：重建 Pod 时的 IPAM 匹配与复用

```mermaid
graph TD
    A[Pod 重建并被 Controller 调谐] --> B[调用 reconcileAllocateSubnets]
    B --> C[向 IPAM 请求随机分配: 传入 nicName]
    C --> D{IPAM 内存状态中是否存在该 nicName 映射?}
    D -- 是 --> E[直接复用: 返回已绑定的原有 IP 和 MAC 地址]
    D -- 否 --> F[从 IP 池中分配新地址并新建 IP CRD]
    E --> G[Controller 更新 Pod annotations 标记 allocated 为 true]
    F --> G
```

图 4 描述：此图展示了 Pod 重建时，IPAM 内部的复用路径。虽然 Pod 上没有指定静态 IP 的注解，但是 IPAM 通过比对与上一次相同的虚拟网口名称（nicName，即 podName.namespace.provider），在内存数据库中直接命中了之前未释放的缓存，从而达成 IP 地址的固定。

文字实现机制：
- Controller 重新调谐该 Pod 并调用 reconcileAllocateSubnets。由于没有静态 IP 注解，它会进入随机分配分支，调用 c.ipam.GetRandomAddress(key, portName, ...). 这里的 portName 保持一致。
- 深入 [pkg/ipam/subnet.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/ipam/subnet.go#L243) 的 getV4RandomAddress 方法中，IPAM 会首先检查缓存，如果 s.V4NicToIP[nicName] != nil，且 MAC 匹配，直接返回原有的 IP 和 MAC 记录，不再进行新的地址划分。

#### 流程 5：CNI 插件与节点 Daemon 的协作执行

在 Controller 将 IP 配置完成并将「allocated: "true"」写入 Pod 之后，运行在 Pod 所在节点上的 `kube-ovn-daemon` 才会开始工作。

图 5：CNI 插件与节点 Daemon 配置网络接口

```mermaid
graph TD
    A[Kubelet 触发 CNI ADD 请求] --> B[CNI Plugin 向本地 Daemon 发起 RESTful 请求]
    B --> C[Daemon 读取缓存中的 Pod 实例]
    C --> D{Allocated 注解是否为 true?}
    D -- 否 --> E[未分配完成: 循环等待并重试]
    E --> D
    D -- 是 --> F[解析 Pod 注解中的 IP/MAC/子网/网卡类型等]
    F --> G[在宿主机创建网络接口并打入容器 Namespace]
    G --> H[设置容器内 IP 路由并启动网口]
    H --> I[返回成功响应给 Kubelet]
```

图 5 描述：此图展示了从 kubelet 触发 CNI 请求到最终网卡在容器内建立配置的完整链路。CNI plugin 与节点上的 daemon 会循环轮询 Pod 的注解，同步等待 controller 在控制面完成 IP 分配并标记为 true 之后，再拉取地址数据配置容器网卡，确保网络状态的一致。

文字实现机制：
在 [pkg/daemon/handler.go](https://github.com/kubeovn/kube-ovn/blob/8bd070c1c6d4df79495223a6e3ae84b0b25e2e02/pkg/daemon/handler.go#L62) 的 handleAdd 方法中：
- 进程进入循环，等待并拉取 Pod annotations。
- 观察到 AllocatedAnnotationTemplate（"ovn.kubernetes.io/allocated"）的值被更新为 "true" 后，说明 Controller 分配动作已完成。
- 读取 Pod 注解中的 IP 路由等信息。在节点上进行网卡创建（如 veth pair）并将接口打入容器 Namespace，将已固定的 IP 地址和路由刷入容器内，完成整个配置。

---

### 设计总结

Kube-OVN 通过一套组合机制完美实现了 Pod IP 地址的固定：
- 数据面：使用独立的 IP 资源（CRD）作为分布式状态的物理存储，并利用 ipsLister 实现轻量高效的只读缓存查找。
- 控制面调谐：通过 Owner 关系智能决定在 Pod 删除时是否保持 IP 配置。
- IPAM 引擎：通过把唯一的端口标识（nicName）映射到 IP 地址，实现了“名称即地址”的复用链路。

这种基于 Controller 声明式管理与 IPAM 状态缓存的架构，保证了固定 IP 在云原生弹性的应用生命周期中表现稳定。

