Skip to content

feat: support LoadBalancer service backed by vpc nat gateway nft shar… - #7047

Draft
zbb88888 wants to merge 2 commits into
kubeovn:masterfrom
zbb88888:support-vpc-lb-based-nft-dnat
Draft

feat: support LoadBalancer service backed by vpc nat gateway nft shar…#7047
zbb88888 wants to merge 2 commits into
kubeovn:masterfrom
zbb88888:support-vpc-lb-based-nft-dnat

Conversation

@zbb88888

@zbb88888 zbb88888 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Pull Request

What type of this PR

  • Features
  • Tests

Description

Make a vpc-nat-gw act as an in-VPC, kube-proxy-like load balancer for
type=LoadBalancer Services, so a Service whose backends live in a custom VPC
(unreachable by the host kube-proxy) gets a working EXTERNAL-IP.

Enabled by --enable-nftable-lb-svc (requires --enable-lb). A Service is handled when it is
type=LoadBalancer and carries ovn.kubernetes.io/eip; the gateway is derived from the EIP's
natGwDp. The controller watches the Service and its EndpointSlices and programs one share
type IptablesDnatRule per (servicePort, ready backend). The nftables map-based share DNAT
(numgen random, added in #6858) then balances new connections and pins them per-connection
via conntrack.

Design — one EIP, two forwarding planes

The same EIP is reachable through two planes, each targeting the backend NIC reachable in its
own domain. The recommended backend is dual-NIC and listens on 0.0.0.0:

Client Path Plane Backend NIC
External BGP/macvlan ARP → gateway pod (bypasses host stack) vpc-nat-gw nft DNAT gateway-VPC NIC
Custom-VPC internal route to gateway (hairpin SNAT) vpc-nat-gw nft DNAT gateway-VPC NIC
Default-VPC internal / host host L3 → kube-ipvs0 local route kube-proxy primary/default-VPC NIC

The two planes are mutually exclusive per packet (L2-to-pod-MAC vs host L3 routing;
arp_ignore=1 makes only the gateway pod answer ARP for the EIP), so they coexist without
conflict and kube-proxy is left untouched.

Backend IP selection. Each ready endpoint is resolved to the NIC that sits in the
gateway's VPC (via the pod's kube-ovn providers/subnets), not the k8s endpoint (primary) IP the
gateway cannot route to:

  • single-NIC backend already in the gateway VPC → matches on its only NIC (unchanged behavior);
  • backend with kube-ovn NICs but none in the gateway VPC → skipped with a one-shot Warning
    event, to avoid a black-hole share DNAT;
  • several NICs in the gateway VPC → lowest IPv4 chosen deterministically (dual-NIC covers ~99%).

Resolution reuses the EndpointSlice informer for readiness and is memoized per pod.

Ownership & lifecycle. The cluster-scoped IptablesDnatRule cannot use an OwnerReference to
a namespaced Service, so generated rules are labeled with the owning Service and reconciled on
Service/EndpointSlice changes; Spec drift (affinity, timeout, EIP) triggers delete+recreate
because share rules are immutable once Ready.

Conflict handling. A share identity (eip:port:proto) is a single nft map programmable by
only one owner. The deterministic winner is derived from the Service objects referencing the
same EIP (stable intent, race-free regardless of reconcile order), looked up via a service
informer index keyed by the eip annotation; a manually-created share rule always wins. Losers
back off with an NftableLbSvcConflict event and requeue. The validating webhook rejects
cross-owner same-identity share rules and enforces consistent session-affinity across an
identity, skipping terminating rules so a recreate is not blocked.

ClientIP session affinity. Adds SessionAffinity/SessionAffinityTimeoutSeconds to
IptablesDnatRule (share only) and implements the kube-proxy nftables affinity pattern in
nat-gateway.sh (per-backend timeout sets + source-IP goto, layered on the same numgen random dispatch). The stateless (none) path is unchanged.

Advantages

  • Data plane isolated from the host — kube-proxy needs no change and no black-hole; the
    same EIP serves external, custom-VPC and default-VPC clients (k8s LoadBalancer semantics).
  • Reuses the k8s EndpointSlice informer for backend readiness/rollout — no custom health
    checking.
  • Deterministic, race-free conflict resolution from stable Service intent + an O(matched)
    informer index (no full-store scans, no mutation of user Service objects).
  • Backward-compatible wire formatnat-gateway.sh accepts both the legacy 4-field and the
    new 6-field map-add form and keeps identity chain naming unchanged for rolling upgrades.

Implementation

  • CRD (pkg/apis/kubeovn/v1/iptables-dnat-rule.go): SessionAffinity (enum ""/ClientIP)
    and SessionAffinityTimeoutSeconds (0–86400, default 10800).
  • Controller (pkg/controller/nftable_lb_svc.go, new): reconcile, backend NIC resolver,
    conflict resolution, ingress-IP publish/clear; indexers.go service-by-eip index;
    controller.go/config.go/service.go/endpoint_slice.go wiring and enqueue hooks.
  • Data plane (pkg/controller/nft_dnat.go, vpc_nat_gw_nat.go,
    dist/images/vpcnatgateway/nat-gateway.sh): thread affinity into the share DNAT map-add/cleanup.
  • Webhook (pkg/webhook/vpc_nat_gateway.go): affinity + cross-owner identity validation.
  • Shared util (pkg/util/vpc_nat_gateway.go): single decoder NftableLbSvcOwnerKey used by
    both controller and webhook.
  • Manifests: ENABLE_NFTABLE_LB_SVC wired into install.sh and both charts; enabled for the
    iptables-vpc-nat-gw e2e job.

Scope: aligns with kube-proxy's Cluster traffic policy only (all Ready endpoints; no
ExternalTrafficPolicy/InternalTrafficPolicy=Local, topology, or terminating-endpoint
fallback); IPv4 and tcp/udp only.

Tests

  • Unit: rule/affinity generation, backend NIC selection, conflict resolution, service-by-eip
    index.
  • e2e: lifecycle, ingress IP, dual-NIC backend selection, service/manual conflict, ClientIP
    affinity stickiness, and data-path distribution.
flowchart LR
    ExtC["External client"]
    CvpcC["Custom-VPC pod"]
    DvpcC["Default-VPC pod / host"]

    EIP(["EIP<br/>(Service EXTERNAL-IP)"])

    subgraph GW["Plane A · vpc-nat-gw (nft share DNAT)"]
        GWpod["gateway pod net1 = EIP<br/>numgen random + ClientIP affinity"]
    end

    subgraph KP["Plane B · kube-proxy (untouched)"]
        IPVS["kube-ipvs0 (EIP local route)<br/>ipvs load balance"]
    end

    subgraph POD["Dual-NIC backend pod · listens 0.0.0.0"]
        NIC2["gateway-VPC NIC"]
        NIC1["primary / default-VPC NIC"]
    end

    ExtC -- "BGP/macvlan · ARP→gw pod MAC<br/>(bypasses host stack)" --> EIP
    CvpcC -- "route to gateway<br/>(hairpin SNAT)" --> EIP
    EIP --> GWpod
    GWpod -- "DNAT → gateway-VPC NIC IP" --> NIC2

    DvpcC -- "host L3 → local table" --> IPVS
    IPVS -- "DNAT → primary endpoint IP" --> NIC1

    classDef plane fill:#eef,stroke:#88a;
    classDef pod fill:#efe,stroke:#8a8;
    class GW,KP plane;
    class POD pod;
Loading

要点(图与设计一致):

  • 同一个 EIP,两条面按逐包互斥分流:走 L2-to-pod-MAC 的进网关面(Plane A),走 host L3 的进 kube-proxy 面(Plane B),arp_ignore=1 保证只有网关 pod 应答 EIP 的 ARP。
  • Plane A(外部 + 自定义 VPC 内网)→ 网关 nft share DNAT → 后端网关 VPC 网卡
  • Plane B(默认 VPC 内网 / host)→ kube-ipvs0 本地路由 → 后端主网卡(kube-proxy 零改动)。
  • 后端双网卡、监听 0.0.0.0,两面各命中同域可达网卡,无黑洞。

Pull Request

What type of this PR

Examples of user facing changes:

  • Features

Which issue(s) this PR fixes

Fixes #(issue-number)

@coveralls

coveralls commented Jul 20, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31781692423

Coverage decreased (-0.03%) to 32.447%

Details

  • Coverage decreased (-0.03%) from the base build.
  • Patch coverage: 380 uncovered changes across 8 files (154 of 534 lines covered, 28.84%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
pkg/controller/nftable_lb_svc.go 421 132 31.35%
pkg/webhook/vpc_nat_gateway.go 54 5 9.26%
pkg/controller/nft_dnat.go 12 0 0.0%
pkg/controller/controller.go 8 0 0.0%
pkg/controller/vpc_nat_gw_nat.go 8 0 0.0%
pkg/util/vpc_nat_gateway.go 7 0 0.0%
pkg/controller/config.go 5 0 0.0%
pkg/controller/indexers.go 11 9 81.82%
Total (10 files) 534 154 28.84%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 65107
Covered Lines: 21125
Line Coverage: 32.45%
Coverage Strength: 0.38 hits per line

💛 - Coveralls

@zbb88888

Copy link
Copy Markdown
Collaborator Author

核心结论:数据面负载均衡的核心逻辑与 kube-proxy 的 nftables 后端基本一致,但覆盖范围(Service 语义面)明显更窄。

一致的部分(核心 LB 机制)

维度 kube-proxy (nftables) 本实现 是否一致
后端选择原语 numgen random mod N(每新连接随机,conntrack 固定)
非一致性哈希 是随机、非 jhash/一致性哈希
ClientIP 亲和 每端点 dynamic,timeout set;端点链 update @set { ip saddr };服务链 ip saddr @set goto <ep> + numgen random vmap 兜底 逐条对应,结构相同
亲和记录粒度 按 (源IP, ServicePort),不看源端口/具体 VIP 按身份 (eip:port:proto) 的 set,按源 IP ✅ 基本一致
默认亲和超时 10800s 10800s(默认常量)
两层派发 基链 vmap → per-service 链 → per-endpoint 链 基链 vmap(service-ips) → per-identity 链 → per-endpoint 链
事务原子性 每次 sync 一个 nft 事务,add/flush 幂等 + 陈旧对象清理 per-identity nft -f 事务 + 陈旧 ep/aff 清理 ✅ 思路一致

亲和的 nft 规则我用 mock 逐条对照过 kube-proxy 的 writeServiceToEndpointJumps,形态一致。

有差异 / 未覆盖的部分(范围更窄)

  1. Service 面:本实现只处理 LoadBalancer 的外部 EIP:port(靠 ovn.kubernetes.io/eip 注解绑定)。ClusterIP 走的是既有的 OVN LB 路径,NodePort / externalIPs / loadBalancerSourceRanges / healthCheckNodePort 都不处理。kube-proxy 覆盖全部这些。
  2. 协议 / IP 族:仅 TCP/UDP、仅 IPv4(share DNAT 的限制,SCTP 与 IPv6 会被跳过)。kube-proxy 支持 SCTP 与双栈。
  3. 流量策略未实现 ExternalTrafficPolicy=LocalInternalTrafficPolicy=Local、拓扑感知路由、terminating-endpoint 回退。本实现始终等价于 Cluster 策略、只用 Ready 端点。
  4. conntrack 清理粒度:kube-proxy 会为被删除的端点清理 conntrack(尤其 UDP,避免黑洞)。本实现只在整个身份删除conntrack -D单个后端摘除时只重建 map、不 flush 该后端的 conntrack(已在 cleanupShareDnatInPod 注释为有意行为,符合"从 LB 摘除一个后端"的语义,但与 kube-proxy 对 UDP 的处理有别)。
  5. 架构位置:kube-proxy 在每个节点上编程 nft;本实现集中在 vpc-nat-gw pod 上(VPC nat gw 模型固有),不是每节点分布式。
  6. 源地址/伪装:kube-proxy 有一整套 masquerade(SNAT)规则处理 hairpin/off-cluster 等;本实现仅做 DNAT,SNAT 由 vpc-nat-gw 既有 SNAT/EIP 机制负责,不在本特性内对齐。

一句话总结

  • "怎么选后端、怎么做会话亲和" —— 与 kube-proxy nftables 后端基本一致(同样的 numgen random + per-endpoint affinity set 模式)。
  • "能处理哪些 Service 语义" —— 是 kube-proxy 的一个子集:只做 LoadBalancer 外部 EIP、IPv4、TCP/UDP、Cluster 流量策略。

@zbb88888
zbb88888 force-pushed the support-vpc-lb-based-nft-dnat branch from 0fc79ed to 795b409 Compare July 20, 2026 08:46
@zbb88888
zbb88888 force-pushed the support-vpc-lb-based-nft-dnat branch 4 times, most recently from a7ea49c to 20cf8fe Compare July 30, 2026 07:24
@zbb88888
zbb88888 force-pushed the support-vpc-lb-based-nft-dnat branch 2 times, most recently from fe70a24 to f8b14b4 Compare August 7, 2026 05:46
…e DNAT

Make a vpc-nat-gw act as an in-VPC, kube-proxy-like load balancer for
LoadBalancer type Services, so a Service whose backends live in a custom
VPC (unreachable by the host kube-proxy) gets a working EXTERNAL-IP.

Enabled by --enable-nftable-lb-svc (requires --enable-lb). A Service is
handled when it is type=LoadBalancer and carries ovn.kubernetes.io/eip;
the gateway is derived from the EIP's natGwDp. The controller watches the
Service and its EndpointSlices and programs one share type
IptablesDnatRule per (servicePort, ready backend); the nftables map-based
share DNAT (numgen random) then balances new connections and pins them
per-connection via conntrack.

Design
------
One EIP, two forwarding planes, each targeting the backend NIC reachable
in its own domain (the recommended backend is dual-NIC and listens on
0.0.0.0):

  * external and custom-VPC-internal traffic reaches the EIP through the
    NAT gateway (BGP/macvlan ARP resolves the EIP to the gateway pod, so
    it never enters the host stack) and is DNAT'd to the backend's
    gateway-VPC NIC;
  * default-VPC-internal / host traffic reaches the EIP (published as the
    Service ingress IP) through kube-proxy's own ipvs0 local route and is
    balanced to the backend's primary/default-VPC endpoint IP.

The two planes are mutually exclusive per packet (L2-to-pod-MAC vs host
L3 routing), so they coexist without conflict and kube-proxy is left
untouched.

Backend IP selection: each ready endpoint is resolved to the NIC that
sits in the gateway's VPC (via the pod's kube-ovn providers/subnets), not
the k8s endpoint (primary) IP the gateway cannot route to. A single-NIC
backend already in the gateway VPC matches on its only NIC (unchanged
behavior); a backend with kube-ovn NICs but none in the gateway VPC is
skipped with a one-shot Warning event to avoid a black-hole share DNAT;
when several NICs share the gateway VPC the lowest IPv4 is chosen
deterministically (dual-NIC covers ~99% of cases). Resolution reuses the
EndpointSlice informer for readiness and is memoized per pod.

Ownership & lifecycle: the cluster-scoped IptablesDnatRule cannot use an
OwnerReference to a namespaced Service, so generated rules are labeled
with the owning Service and reconciled (create/delete) on Service and
EndpointSlice changes; Spec drift (affinity, timeout, EIP) triggers
delete+recreate because share rules are immutable once Ready.

Conflict handling: a share identity (eip:port:proto) is a single nft map
and can be programmed by only one owner. The deterministic winner is
derived from the Service objects referencing the same EIP (stable intent,
race-free regardless of reconcile order), looked up via a service
informer index keyed by the eip annotation; a manually-created share rule
always wins. Losers back off with an NftableLbSvcConflict event and
requeue to take over once the identity is released. The validating
webhook rejects cross-owner same-identity share rules (and enforces a
consistent session-affinity across an identity), skipping terminating
rules so a recreate is not blocked.

ClientIP session affinity: add SessionAffinity/SessionAffinityTimeoutSeconds
to IptablesDnatRule (share only) and implement the kube-proxy nftables
affinity pattern in nat-gateway.sh (per-backend timeout sets + source-IP
goto layered on the same numgen random dispatch). The stateless (none)
path is unchanged.

Advantages
----------
  * data plane isolated from the host: kube-proxy needs no change and no
    black-hole; the same EIP serves external, custom-VPC and default-VPC
    clients, matching k8s LoadBalancer semantics;
  * reuses the k8s EndpointSlice informer for backend readiness/rollout;
  * deterministic, race-free conflict resolution from stable Service
    intent plus an O(matched) informer index (no full-store scans, no
    mutation of user Service objects);
  * backward-compatible wire format: nat-gateway.sh accepts both the
    legacy 4-field and the new 6-field map-add form and keeps identity
    chain naming unchanged for rolling upgrades.

Scope: aligns with kube-proxy's Cluster traffic policy only (all Ready
endpoints; no ExternalTrafficPolicy/InternalTrafficPolicy=Local,
topology, or terminating-endpoint fallback); IPv4 and tcp/udp only.

Manifests: wire ENABLE_NFTABLE_LB_SVC into install.sh and both charts;
enable it for the iptables-vpc-nat-gw e2e job.

Tests: unit tests for rule/affinity generation, backend NIC selection,
conflict resolution and the service-by-eip index; e2e for lifecycle,
ingress IP, dual-NIC backend selection, service/manual conflict, ClientIP
affinity stickiness and data-path distribution.

Signed-off-by: zbb88888 <jmdxjsjgcxy@gmail.com>
@zbb88888
zbb88888 force-pushed the support-vpc-lb-based-nft-dnat branch from f8b14b4 to 560bb7a Compare August 7, 2026 07:45
@zbb88888

zbb88888 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

完整测试报告已生成并保存到 /root/myskills/ai-tests/kubeovn/e2e-vpc-lb/TEST_REPORT.md。以下是完整报告内容:

────────────────────────────────────────────────────────────────────────────────

kube-ovn nftable-lb-svc E2E 测试报告

│ 测试对象 PR:feat: support LoadBalancer service backed by vpc nat gateway nft share DNAT
│ 分支 support-vpc-lb-based-nft-dnat / HEAD 560bb7a(作者 zbb88888)
│ 测试日期:2026-08-07(三轮完整串行验证,均全绿)

────────────────────────────────────────────────────────────────────────────────

  1. 测试概览
┌──────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 项       │ 值                                                                                                  │
├──────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 结果     │ ✅ 3 轮完整套件全绿(9/9 步骤 rc=0,共 27 个步骤级 PASS)                                           │
├──────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 用例数   │ 6(对应 PR e2e 全部 6 个 ConformanceIt 用例)                                                       │
├──────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 覆盖特性 │ nft share DNAT 生命周期 / 双网卡后端选路 / 服务冲突仲裁 / 手工规则冲突 / ClientIP 粘性 / 数据面分发 │
├──────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 复用资源 │ eg-vpc / eg-subnet / eg-system/eg-nad / eg-ext-subnet(未新建 VPC/NAD/外部子网)                    │
└──────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────┘

运行记录:run 1 ✅ 全绿 → run 2 ❌(环境故障:apiserver↔etcd 连接超时级联重启,非用例问题)→ run 3 ✅ 全绿 → run 4 ✅ 全绿(本报告引用其完整证据)

  1. 环境
  • 单节点 mini(192.168.100.100),K8s v1.34.3,kube-ovn 镜像 pr7047-final,控制器 --enable-lb=true --enable-nftable-lb-svc=true
  • 复用:eg-vpc(自定义 VPC)、eg-subnet(10.88.0.0/24)、eg-system/eg-nad(内部 NAD)、eg-ext-subnet(192.168.100.0/24,EIP 池)
  • 套件临时资源(测试后全部清理):共享网关 nftlb-suite-gw(lanIp 10.88.0.247)+ eg-vpc 静态路由 192.168.100.0/24→10.88.0.247 + 命名空间 nftlb-e2e

数据面架构(实测确认):

  gw pod: eth0=10.88.0.247(eg-subnet), net1=macvlan(外部网络+EIP 副地址)
  nft: map service-ips { EIP.tcp.port : goto dnat-<idhash> }
       chain prerouting: ip daddr . meta l4proto . th dport vmap @service-ips
       chain dnat-<idhash>: dnat to numgen random mod N map { backends }
  iptables: HAIRPIN_SNAT (mark 0x1 + ctstate DNAT + ctorigdst EIP -> SNAT to EIP)  # 回程
  1. 用例 1:生命周期 [nftable-lb-svc](02_test_lifecycle.sh,端口 18084,EIP .89)
┌──────┬────────────────────────────────────────────────────┬───────────────────────────┐
│ 步骤 │ 断言                                               │ 实测                      │
├──────┼────────────────────────────────────────────────────┼───────────────────────────┤
│ 1    │ EIP + 2 后端(10.88.0.16/.17)+ LB svc(eip 注解) │ ✅ ingress=192.168.100.89 │
├──────┼────────────────────────────────────────────────────┼───────────────────────────┤
│ 2    │ 2 条 share 规则;nft mod 2 map 含双后端            │ ✅ rules=2                │
├──────┼────────────────────────────────────────────────────┼───────────────────────────┤
│ 3    │ 删 backend2 → 1 条规则;nft mod 1                  │ ✅ rules=1                │
├──────┼────────────────────────────────────────────────────┼───────────────────────────┤
│ 4    │ 去 eip 注解 → 0 条规则;ingress 清空               │ ✅                        │
├──────┼────────────────────────────────────────────────────┼───────────────────────────┤
│ 5    │ 删 svc → 规则 0;nft identity 移除                 │ ✅ identity 移除          │
└──────┴────────────────────────────────────────────────────┴───────────────────────────┘

生成的规则携带归属标签 ovn.kubernetes.io/nftable-lb-svc-ns/name(cluster-scoped 规则替代 OwnerReference),type: share,status.ready=true。✅ PASS

  1. 用例 2:双网卡后端选路 [nftable-lb-svc-dualnic](03_test_dualnic_backend.sh,端口 18085,EIP .90)
┌──────┬────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────┐
│ 步骤 │ 断言                                                                           │ 实测                                         │
├──────┼────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
│ 1    │ 双网卡后端 eth0=10.16.0.4(默认 VPC)/ net1=10.88.0.19(eg-subnet,经 eg-nad) │ —                                            │
├──────┼────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
│ 2    │ 规则 internalIp == net1 且 ≠ eth0                                              │ ✅ lb-...-8826240d66cf internalIp=10.88.0.19 │
├──────┼────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
│ 3    │ EndpointSlice 仍暴露 eth0(10.16.0.4)                                         │ ✅                                           │
├──────┼────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
│ 4    │ 规则 ready + nft map 以 net1 为后端                                            │ ✅                                           │
└──────┴────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘

核心:控制器按 pod 的 provider/subnet 解析出网关 VPC 侧 NIC,而非 kube-proxy 可见的默认 VPC endpoint。✅ PASS

  1. 用例 3:服务间冲突仲裁 [nftable-lb-svc-conflict](04_test_service_conflict.sh,端口 18086,EIP .91)
┌──────┬────────────────────────────────────────────────────────────────────────────────────┬──────────────────────┐
│ 步骤 │ 断言                                                                               │ 实测                 │
├──────┼────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 2    │ 两 svc 引用同一 EIP:port(...-a-winner < ...-z-loser)                             │ —                    │
├──────┼────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 3    │ 字典序更小的 winner 拥有 identity(1 规则);loser 持续 0 规则(Consistently 15s) │ ✅ winner=1, loser=0 │
├──────┼────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 4    │ loser 收到 NftableLbSvcConflict 事件                                               │ ✅                   │
├──────┼────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 5    │ 删 winner → loser 接管(1 规则)                                                   │ ✅                   │
└──────┴────────────────────────────────────────────────────────────────────────────────────┴──────────────────────┘

✅ PASS

  1. 用例 4:服务 vs 手工规则冲突 [nftable-lb-svc-manual-conflict](05_test_manual_conflict.sh,端口 18087,EIP .92)
┌──────┬────────────────────────────────────────────────────────────────────────┬──────────┐
│ 步骤 │ 断言                                                                   │ 实测     │
├──────┼────────────────────────────────────────────────────────────────────────┼──────────┤
│ 2    │ 先手工建 share 规则占用 identity                                       │ ✅ ready │
├──────┼────────────────────────────────────────────────────────────────────────┼──────────┤
│ 3    │ LB svc 指向同 EIP:port → 服务让位(0 规则)                            │ ✅       │
├──────┼────────────────────────────────────────────────────────────────────────┼──────────┤
│ 4    │ 服务收到 NftableLbSvcConflict 事件(webhook 拒绝跨 owner 同 identity) │ ✅       │
├──────┼────────────────────────────────────────────────────────────────────────┼──────────┤
│ 5    │ 删手工规则 → 服务接管(1 规则)                                        │ ✅       │
└──────┴────────────────────────────────────────────────────────────────────────┴──────────┘

手工规则(owner 为空)永远优先,特性不覆盖手管规则。✅ PASS

  1. 用例 5:ClientIP 会话粘性 [nftable-lb-svc-affinity](06_test_clientip_affinity.sh,端口 18088,EIP .94,timeout 600s)
┌──────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────┐
│ 步骤 │ 断言                                                                                                         │ 实测                  │
├──────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┤
│ 2    │ 每条规则 sessionAffinity=ClientIP + timeoutSeconds=600                                                       │ ✅                    │
├──────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┤
│ 3    │ nft affinity 结构:numgen mod 2 vmap + ip saddr @aff- + ep- 链 dnat to ip:port + aff- 动态 timeout 集合(10m) │ ✅                    │
├──────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┤
│ 4    │ 客户端经 EIP 可达                                                                                            │ ✅                    │
├──────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┤
│ 5    │ 20/20 次请求落在同一后端                                                                                     │ ✅ 20 次全部 backend2 │
└──────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────┘

✅ PASS(粘性 100%)

  1. 用例 6:数据面分发 [nftable-lb-svc-dataplane](07_test_dataplane_distribution.sh,端口 18089,EIP .95)
┌──────┬──────────────────────────────────────────┬───────────────┐
│ 步骤 │ 断言                                     │ 实测          │
├──────┼──────────────────────────────────────────┼───────────────┤
│ 2    │ nft mod 2 map 含双后端(10.88.0.27/.28) │ ✅            │
├──────┼──────────────────────────────────────────┼───────────────┤
│ 3    │ 30 次真实 HTTP 请求经 EIP 分发到两个后端 │ ✅ 15/15 均衡 │
├──────┼──────────────────────────────────────────┼───────────────┤
│ 4    │ ingress == EIP                           │ ✅ .95        │
└──────┴──────────────────────────────────────────┴───────────────┘

数据路径:客户端(eg-subnet) → OVN 静态路由 → gw eth0(打标) → nft DNAT(numgen) → 后端 → 回包经 HAIRPIN_SNAT + conntrack 逆变换回客户端。✅ PASS

  1. 已知边界(与 PR 设计一致)

  2. 仅 IPv4 + tcp/udp,对齐 kube-proxy Cluster 流量策略(无 Local/拓扑)

  3. 双网卡后端端到端数据面不做断言(回包走主网卡默认路由,与 PR dualnic e2e 范围一致;端到端由单网卡用例覆盖)

  4. 宿主机→EIP 依赖 ipvs0 + 后端主网卡可达性,本环境客户端置于自定义 VPC 内

  5. run 2 失败为环境故障(apiserver↔etcd 超时级联 + 无关 etcd 备份脚本 bug),恢复后复跑全绿

  6. 结论

✅ PR 的 6 个 e2e 用例在真实集群全部通过,三轮完整套件复跑全绿(27 个步骤级 PASS),验证了规则自动生成/生命周期、nft numgen 负载均衡(15/15)与后端增减重建(mod 2→1→移除)、双网卡后端 net1
选路、冲突仲裁(服务间/手工规则、事件、接管)、ClientIP 粘性(20/20)、ingress IP 发布等全部关键行为。测试后集群零残留,复用资源与业务(envoy .93)完好。

────────────────────────────────────────────────────────────────────────────────

报告与脚本均维护在 /root/myskills/ai-tests/kubeovn/e2e-vpc-lb/:

  • TEST_REPORT.md(本报告)、README.md、scripts/(00-09 + lib.sh)、yamls/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants