❯ LB 使用 EIP1 的情况下,如果再配置 一对一 External IP 和 internal IP DNAT 是冲突的。 最多也就是 LB 使用 EIP1 的情况下,最多只有 SNAT 还可以再复用 EIP1。 LB 和 一对一 DNAT 本质上对于一个 EIP,肯定是 二选一的。 也就是说,LB + FIP 是伪需求。LB + SNAT 是正常需求。LB 可以共享 EIP,同时也能满足端口访问的需求,可以理解是 FIP 的替代。 分析这个说法的正确性
● 逐条分析
- "LB + 一对一 DNAT 对同一 EIP 是冲突的"
大体正确,但需精确化。
冲突发生在入方向决策逻辑:
同一个包: dst = EIP:port
LB 的决策: → 从 N 个 backend 中选一个(一对多)
FIP 的决策: → 固定发给 Pod A(一对一)
↑ 对同一个包,不可能同时执行两种决策
但严格来说,冲突的粒度是端口级,不是 IP 级:
┌──────────────────────────────────┬─────────────────────────────┐
│ 场景 │ 是否冲突 │
├──────────────────────────────────┼─────────────────────────────┤
│ EIP:80 做 LB,EIP:80 做 1:1 DNAT │ 冲突 — 同一个包两种处理逻辑 │
├──────────────────────────────────┼─────────────────────────────┤
│ EIP:80 做 LB,EIP:22 做 1:1 DNAT │ 理论上不冲突 — 按端口分流 │
└──────────────────────────────────┴─────────────────────────────┘
但"EIP:80 走 LB,EIP:22 走特定 Pod"这种按端口拆分的模型,已经不是传统 FIP 的语义了。FIP 的定义是全端口 1:1 映射,在这个定义下,与 LB 确实互斥。
结论:在 FIP 的标准语义下,正确。
- "LB 使用 EIP1 的情况下,最多只有 SNAT 还可以再复用 EIP1"
正确。
原因:SNAT 是出方向操作,和 LB 的入方向在数据面上天然隔离:
入方向: Client → EIP:80 → katran LB 处理(VIP:service_port 匹配)
出方向: Pod → External → SNAT 改写 src 为 EIP(不经过 katran)
SNAT 回包: External → EIP:ephemeral_port → katran 查不到 → XDP_PASS → conntrack 还原
SNAT 回包的 dst_port 是临时端口,不会命中 katran 的 VIP:service_port 表项,数据面零冲突。
- "LB + FIP 是伪需求"
对同一个 EIP 而言正确,但表述可以更精确。
准确的说法是:
- 同一 EIP 上 LB + FIP = 伪需求 ✅ — 入方向逻辑互斥
- 系统同时支持 LB 模式和 FIP 模式(不同 EIP)≠ 伪需求 — 有些 EIP 做 LB,有些做 FIP,这是合理的
所以"伪需求"针对的是"同一 EIP 同时做 LB 和 FIP",不是"系统不需要 FIP 功能"。
- "LB + SNAT 是正常需求"
完全正确。 这是非常普遍的场景:
- Pod 通过 LB VIP 对外提供服务(入方向)
- Pod 访问外部 API / DNS / 第三方服务时,需要以 VIP 为源 IP(出方向)
- 外部服务对 VIP 做了 IP 白名单
- "LB 可以共享 EIP,可以理解是 FIP 的替代"
大部分场景正确,但不是完全替代。
对比:
┌─────────────────────┬─────────────┬───────────────────────┐
│ 能力 │ FIP │ LB + SNAT │
├─────────────────────┼─────────────┼───────────────────────┤
│ 出方向有公网 IP │ ✅ 1:1 SNAT │ ✅ N:1 SNAT 复用 VIP │
├─────────────────────┼─────────────┼───────────────────────┤
│ 入方向特定端口可达 │ ✅ 全端口 │ ✅ LB 配置的端口 │
├─────────────────────┼─────────────┼───────────────────────┤
│ 入方向任意端口可达 │ ✅ │ ❌ 只有 LB 注册的端口 │
├─────────────────────┼─────────────┼───────────────────────┤
│ 多 Pod 共享一个 EIP │ ❌ 独占 │ ✅ │
└─────────────────────┴─────────────┴───────────────────────┘
LB + SNAT 覆盖不了的场景:Pod 需要被外部以任意端口主动访问(游戏服务器、P2P、SIP 等)。这些场景仍然需要 FIP。
但在云原生主流场景下(Web 服务、微服务、gRPC),Pod 只暴露固定端口,LB + SNAT 确实足以替代 FIP,而且更节省公网 IP。
总结
┌─────────────────────────┬─────────────┬────────────────────────────────────────────────────────────┐
│ 原文观点 │ 判定 │ 备注 │
├─────────────────────────┼─────────────┼────────────────────────────────────────────────────────────┤
│ LB + 1:1 DNAT 冲突 │ ✅ 正确 │ 严格说是端口级冲突,FIP 语义下等价于 IP 级 │
├─────────────────────────┼─────────────┼────────────────────────────────────────────────────────────┤
│ SNAT 可以复用 LB 的 EIP │ ✅ 正确 │ 入方向与出方向天然隔离 │
├─────────────────────────┼─────────────┼────────────────────────────────────────────────────────────┤
│ LB + FIP 是伪需求 │ ⚠️ 需限定 │ 对同一 EIP 而言正确;系统仍可能需要 FIP 功能(用不同 EIP) │
├─────────────────────────┼─────────────┼────────────────────────────────────────────────────────────┤
│ LB + SNAT 是正常需求 │ ✅ 正确 │ 业界通用模式 │
├─────────────────────────┼─────────────┼────────────────────────────────────────────────────────────┤
│ LB 是 FIP 的替代 │ ⚠️ 部分正确 │ 覆盖主流场景,但全端口可达的场景仍需 FIP │
└─────────────────────────┴─────────────┴────────────────────────────────────────────────────────────┘
❯ 是这样的。 LB 使用 EIP1 的情况下,如果再配置 一对一 External IP 和 internal IP DNAT 是冲突的。最多也就是 LB 使用 EIP1 的情况下,最多只有 SNAT 还可以再复用 EIP1。LB 和 一对一 DNAT 本质上对于一个 EIP,肯定是 二选一的。 也就是说,LB + FIP 是伪需求。梳理以上问题,再仔细评估下,输出最终方案。
● 分析确认
你的判断完全正确。对于同一个 EIP 的入方向:
LB: EIP:port → 选一个 backend(一对多)
1:1 DNAT: EIP → 固定一个 Pod (一对一)
↑ 决策逻辑互斥,无法共存
因此一个 EIP 只有两种互斥模式:
┌──────────┬─────────────────┬───────────────────────┬───────────────┐
│ 模式 │ 入方向 │ 出方向 │ Conntrack │
├──────────┼─────────────────┼───────────────────────┼───────────────┤
│ LB 模式 │ katran 负载均衡 │ 可选:SNAT 复用此 VIP │ SNAT 部分需要 │
├──────────┼─────────────────┼───────────────────────┼───────────────┤
│ FIP 模式 │ 1:1 DNAT │ 1:1 SNAT │ 不需要 │
└──────────┴─────────────────┴───────────────────────┴───────────────┘
LB + FIP 确实是伪需求,排除。
最终方案
统一架构
NIC
│
▼
XDP (katran,不修改)
├─ 匹配 LB VIP:port → 封装 → XDP_TX → backend
└─ 不匹配 → XDP_PASS
│
▼
TC ingress (新增,统一入口)
├─ fip_to_pod 命中 → 1:1 DNAT(无状态) ← FIP 模式
├─ snat_ct 命中 → reverse-DNAT(查 conntrack) ← LB+SNAT 回包
└─ 都不命中 → TC_ACT_OK(正常上送内核)
TC egress (新增,统一出口)
├─ pod_to_fip 命中 → 1:1 SNAT(无状态) ← FIP 模式
├─ snat_binding 命中 → N:1 SNAT(写 conntrack) ← LB+SNAT
└─ 都不命中 → TC_ACT_OK(正常发出)
一共只需新增 2 个 TC BPF 程序,各自内含两条查表路径(FIP 路径 + LB SNAT 路径),互斥不冲突。
BPF Maps(共 5 个)
// ========== FIP 模式(无状态,两个 map 互为反向) ==========
// 入方向:FIP → Pod IP
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct ip_addr); // FIP
__type(value, struct ip_addr); // Pod IP
__uint(max_entries, MAX_FIPS);
} fip_to_pod SEC(".maps");
// 出方向:Pod IP → FIP
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct ip_addr); // Pod IP
__type(value, struct ip_addr); // FIP
__uint(max_entries, MAX_FIPS);
} pod_to_fip SEC(".maps");
// ========== LB SNAT 模式(有状态) ==========
// Pod 子网 → 复用哪个 VIP
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__type(key, struct v4_lpm_key); // Pod 子网
__type(value, __be32); // VIP
__uint(max_entries, 1024);
__uint(map_flags, BPF_F_NO_PREALLOC);
} snat_binding SEC(".maps");
// Conntrack(LRU 自动淘汰,免 GC)
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, struct ct_key); // (ext_ip, vip, ext_port, snat_port, proto)
__type(value, struct ct_val); // (pod_ip, orig_port)
__uint(max_entries, 524288);
} snat_ct SEC(".maps");
// 端口游标(原子递增)
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, __u32);
__type(value, __u32);
__uint(max_entries, 1);
} port_alloc SEC(".maps");
TC Ingress 程序(统一入方向)
SEC("tc")
int nat_ingress(struct __sk_buff *skb) {
struct iphdr *iph = parse_ip(skb);
if (!iph) return TC_ACT_OK;
// --- 路径 1: FIP DNAT(无状态)---
struct ip_addr dst_key = { .addr = iph->daddr };
struct ip_addr *pod = bpf_map_lookup_elem(&fip_to_pod, &dst_key);
if (pod) {
rewrite_dst(skb, iph, pod->addr); // 改 dst + 增量 csum
return TC_ACT_OK;
}
// --- 路径 2: LB SNAT reverse-DNAT(查 conntrack)---
struct ct_key ct = build_ct_key_from_reply(iph, skb);
struct ct_val *val = bpf_map_lookup_elem(&snat_ct, &ct);
if (val) {
val->timestamp = bpf_ktime_get_ns();
rewrite_dst_and_port(skb, iph, val->orig_src, val->orig_port);
return TC_ACT_OK;
}
return TC_ACT_OK; // 非 NAT 流量,正常上送
}
TC Egress 程序(统一出方向)
SEC("tc")
int nat_egress(struct __sk_buff *skb) {
struct iphdr *iph = parse_ip(skb);
if (!iph) return TC_ACT_OK;
// --- 路径 1: FIP SNAT(无状态)---
struct ip_addr src_key = { .addr = iph->saddr };
struct ip_addr *fip = bpf_map_lookup_elem(&pod_to_fip, &src_key);
if (fip) {
rewrite_src(skb, iph, fip->addr); // 改 src + 增量 csum
return TC_ACT_OK;
}
// --- 路径 2: LB SNAT(写 conntrack)---
struct v4_lpm_key lpm = { .prefixlen = 32, .addr = iph->saddr };
__be32 *vip = bpf_map_lookup_elem(&snat_binding, &lpm);
if (vip) {
__u16 new_port = alloc_port();
write_conntrack(skb, iph, *vip, new_port);
rewrite_src_and_port(skb, iph, *vip, new_port);
return TC_ACT_OK;
}
return TC_ACT_OK; // 非 NAT 流量,正常发出
}
控制面操作
# LB 模式:EIP1 做负载均衡,后端 Pod 出方向复用 EIP1
katran.addVip(eip="1.1.1.1", port=80, proto=TCP, backends=[...])
katran.addVip(eip="1.1.1.1", port=443, proto=TCP, backends=[...])
nat.addSnatBinding(pod_cidr="10.0.0.0/16", vip="1.1.1.1")
# FIP 模式:EIP2 一对一绑定到某个 Pod
nat.addFip(eip="2.2.2.2", pod_ip="10.0.1.5")
# 互斥校验(控制面必须拒绝)
katran.addVip(eip="2.2.2.2", ...) → ERROR: EIP 已用于 FIP
nat.addFip(eip="1.1.1.1", ...) → ERROR: EIP 已用于 LB
最终全景
每个 EIP 二选一:
┌─────────────────────────────────────────────────────┐
│ EIP = LB 模式 │
│ │
│ 入: XDP(katran) → VIP:port 匹配 → 封装转发 │
│ 出: TC egress → snat_binding 匹配 → SNAT(有状态) │
│ 回: XDP(miss) → TC ingress → snat_ct 匹配 → revDNAT │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ EIP = FIP 模式 │
│ │
│ 入: XDP(miss) → TC ingress → fip_to_pod → DNAT │
│ 出: TC egress → pod_to_fip → SNAT │
│ 全程无状态,无 conntrack │
└─────────────────────────────────────────────────────┘
组件总计:
katran XDP — 已有,不修改
TC ingress — 新增 1 个程序(~120 行 C)
TC egress — 新增 1 个程序(~120 行 C)
BPF maps — 5 个(2 无状态 + 3 有状态)
控制面 — EIP 模式互斥校验 + map CRUD
结论:katran 不支持 1:1 DNAT/SNAT(Floating IP)
katran 是一个纯粹的 L4 负载均衡器(DSR 模式),不具备为 Pod 配置独立 Floating External IP 的能力。以下是从代码层面的详细分析:
- katran 的核心数据面逻辑
从 katran/lib/bpf/balancer.bpf.c 的 process_packet() 函数可以看到,整个包处理流程是:
入包 → 解析L3/L4 → VIP查表(vip_map) → 一致性哈希/LRU选real → IPIP/GUE封装 → XDP_TX
关键事实:
- 没有任何 IP 地址重写操作。katran 从不修改原始包的 src/dst IP
- 封装方式只有两种:IPIP 隧道 或 GUE 隧道(在原始包外面加一层 outer header)
- 返回路径使用 DSR(Direct Server Return),即后端服务器直接回包给客户端,不经过 katran
- 几个容易混淆的特性分析
┌─────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────┐
│ 特性 │ 实际作用 │ 是否为 1:1 NAT │
├─────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────┤
│ LPM_SRC_LOOKUP │ 根据源 IP 前缀选择目标 real server(而非一致性哈希),用于基于来源的路由策略 │ 否。只是改变选 real 的方式,不改写 IP │
├─────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────┤
│ INLINE_DECAP │ 对 IPIP/GUE 封装的入包做解封装,让本机能接收其他 katran 转发过来的包 │ 否。只是剥掉外层隧道头 │
├─────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────┤
│ encap_v4/encap_v6 │ 给原始包套一个新的 IP 头(IPIP 隧道),outer src 使用 172.16.0.0/10 前缀(不是原始 src) │ 否。是隧道封装,不是 SNAT │
├─────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────┤
│ LOCAL_DELIVERY_OPTIMIZATION │ 当 VIP 和 real 在同一台机器上时,直接 XDP_PASS 交给内核协议栈 │ 否 │
├─────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────┤
│ decap_dst map │ 存放需要跨 PoP 解封装的目标地址白名单 │ 否 │
└─────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────┘
- 为什么 katran 架构上无法做 Floating IP
从架构根因来看:
-
DSR 模式限制:katran 只处理入方向流量(client → VIP),回包(server → client)不经过 katran。要实现 SNAT,必须双向都经过 NAT 节点,这与 DSR 模式矛盾。
-
XDP 层的限制:katran 运行在 XDP 层,在 XDP_TX 时直接将包从网卡发回,没有 conntrack 状态跟踪。1:1 NAT 需要 conntrack 来维护双向映射。
-
无 IP 重写代码:翻遍整个 BPF 数据面代码(balancer.bpf.c、pckt_encap.h),没有任何 iph->saddr = ... 或 iph->daddr = ... 这样的 IP 重写操作。所有的"地址变更"都是通过加/减隧道头实现的。
-
如果要实现 Floating IP,需要什么
要为 Pod 实现 FIP(类似公有云 EIP),需要:
- 双向 NAT 节点:出方向 SNAT(Pod IP → FIP),入方向 DNAT(FIP → Pod IP)
- Conntrack:跟踪连接状态以正确做反向映射
- 典型实现:Linux 内核 netfilter/nftables、Cilium 的 BPF NAT、OVS、或 kube-proxy 的 iptables 规则
katran 的定位是 Facebook 数据中心的 L4 LB 前端(类似 LVS-TUN 模式),不是通用 NAT 网关。
约束条件清单
┌────────────────────────────────────────┬───────────────────────────────────────┬───────────────────┐
│ 约束 │ 原因 │ 执行层 │
├────────────────────────────────────────┼───────────────────────────────────────┼───────────────────┤
│ 同一 EIP 不能同时有 LB 和 DNAT │ 入方向决策冲突 │ 控制面拒绝 │
├────────────────────────────────────────┼───────────────────────────────────────┼───────────────────┤
│ DNAT 的 EIP 只能有一条对应的 SNAT 规则 │ 独占模式不做端口重映射,多 Pod 会冲突 │ 控制面校验 │
├────────────────────────────────────────┼───────────────────────────────────────┼───────────────────┤
│ SNAT 端口从 32768-65535 分配 │ 避免与 LB 服务端口(通常 <32768)冲突 │ 数据面 port_alloc │
├────────────────────────────────────────┼───────────────────────────────────────┼───────────────────┤
│ LB VIP 不用 port=0 通配 │ 否则 katran 会吃掉 SNAT 回包 │ 控制面校验 │
└────────────────────────────────────────┴───────────────────────────────────────┴───────────────────┘
katran 扩展:DNAT + SNAT 最终实现方案
一、领域模型
三个独立原语,各自独立创建/删除:
┌─────────┬──────────────────┬──────┬────────────┬───────────┐
│ 原语 │ 语义 │ 方向 │ 状态 │ 实现位置 │
├─────────┼──────────────────┼──────┼────────────┼───────────┤
│ LB │ EIP:port → N Pod │ 入 │ 已有 │ XDP(katran)│
│ DNAT │ EIP → 1 Pod │ 入 │ 无状态 │ TC ingress │
│ SNAT │ Pod → EIP │ 出+回 │ 视模式而定 │ TC egress │
└─────────┴──────────────────┴──────┴────────────┴───────────┘
二、EIP 组合规则
LB + SNAT ✅ 服务对外 + Pod 出网复用 VIP
DNAT + SNAT ✅ 等效 FIP(同一 Pod、同一 EIP)
LB only ✅
SNAT only ✅
DNAT only ⚠️ 可创建,回包 src 非 EIP,对端可能不通
LB + DNAT ❌ 入方向冲突,控制面拒绝
三、数据面架构
NIC
│
▼
XDP (katran,不修改)
├─ VIP:port 命中 → 封装 → XDP_TX ← LB
└─ 未命中 → XDP_PASS
│
▼
TC ingress (新增)
├─ ① snat_ct 命中 → 还原 IP+端口 ← 共享 SNAT 回包
├─ ② dnat_map 命中 → 改写 dst IP ← DNAT 入方向 / 独占 SNAT 回包
└─ ③ 都不命中 → 放行
TC egress (新增)
├─ snat_map 命中:
│ ├─ dnat_map 有同 Pod 条目 → 独占模式:仅改 src IP,不动端口,无 ct
│ └─ 否则 → 共享模式:改 src IP + 重映射端口 + 写 ct
└─ 未命中 → 放行
四、BPF Maps(4 个)
// DNAT 域:EIP → Pod IP(入方向,无状态)
struct bpf_map dnat_map { // BPF_MAP_TYPE_HASH
key: struct ip_addr // EIP
value: struct ip_addr // Pod IP
};
// SNAT 域:Pod IP → EIP(出方向配置)
struct bpf_map snat_map { // BPF_MAP_TYPE_HASH
key: struct ip_addr // Pod IP
value: struct ip_addr // EIP
};
// SNAT 域:conntrack(共享模式专用,LRU 自动淘汰)
struct bpf_map snat_ct { // BPF_MAP_TYPE_LRU_HASH
key: {ext_ip, eip, ext_port, mapped_port, proto} // 五元组
value: {pod_ip, orig_port} // 原始信息
};
// SNAT 域:端口游标(per-CPU 避免锁竞争)
struct bpf_map port_alloc { // BPF_MAP_TYPE_PERCPU_ARRAY
key: __u32 // 固定 0
value: __u32 // 当前游标,范围 32768-65535
};
领域解耦:dnat_map 和 snat_map 互不引用。SNAT 的两种模式由 TC egress 运行时查两个 map 动态决定,无耦合标记位。
五、SNAT 两种模式的判定
// TC egress 核心逻辑
eip = snat_map[pod_ip] // 查 SNAT 域
dnat_pod = dnat_map[eip] // 查 DNAT 域
exclusive = dnat_pod && dnat_pod == pod_ip
if exclusive → 独占模式(DNAT+SNAT 场景)
else → 共享模式(LB+SNAT / 纯 SNAT 场景)
┌──────┬───────────┬───────────────┬──────────────┬──────────┐
│ 模式 │ 改 src IP │ 改 src port │ 写 conntrack │ 回包路径 │
├──────┼───────────┼───────────────┼──────────────┼──────────┤
│ 独占 │ ✅ │ ❌ 保留原端口 │ ❌ │ dnat_map │
├──────┼───────────┼───────────────┼──────────────┼──────────┤
│ 共享 │ ✅ │ ✅ 重映射 │ ✅ │ snat_ct │
└──────┴───────────┴───────────────┴──────────────┴──────────┘
为什么独占模式必须保留端口:DNAT 入方向连接的回包,src port 是服务端口(如 22),重映射会导致 Client 五元组不匹配,TCP RST。
六、全场景包路径
LB + SNAT(共享模式)
入站 LB: Client → EIP:443 → katran 命中 → 封装 → Backend
Pod 出站: Pod:50000 → Ext:443 → egress 共享 SNAT → EIP:32800 + ct
出站回包: Ext:443 → EIP:32800 → katran 未命中 → PASS → ingress ct 命中 → Pod:50000
DNAT + SNAT(独占模式 = FIP)
入站 DNAT: Client → EIP:22 → katran 未命中 → PASS → ingress dnat_map → Pod:22
DNAT 回包: Pod:22 → Client → egress 独占 SNAT → EIP:22(端口不变)
Pod 出站: Pod:50000 → Ext:443 → egress 独占 SNAT → EIP:50000(端口不变)
出站回包: Ext:443 → EIP:50000 → ingress dnat_map → Pod:50000
纯 SNAT(共享模式)
Pod 出站: Pod:50000 → Ext:443 → egress 共享 SNAT → EIP:32800 + ct
出站回包: Ext:443 → EIP:32800 → ingress ct 命中 → Pod:50000
七、控制面 API
addLbVip(eip, port, proto, backends[]) → 写 katran vip_map
delLbVip(eip, port, proto) → 删 katran vip_map
addDnat(eip, pod_ip) → 校验 EIP 无 LB → 写 dnat_map
delDnat(eip) → 删 dnat_map
addSnat(pod_ip, eip) → 若 EIP 有 DNAT 则校验同 Pod → 写 snat_map
delSnat(pod_ip) → 删 snat_map
八、约束条件
- 同一 EIP: LB 与 DNAT 互斥 ← 控制面拒绝
- DNAT 的 EIP: 最多一条 SNAT 规则且必须是同一 Pod ← 控制面校验
- 共享 SNAT 端口范围: 32768-65535 ← 避免与 LB 服务端口冲突
- LB VIP 不用 port=0 通配 ← 否则 katran 吃掉 SNAT 回包
九、工程量
新增代码:
TC ingress BPF ~100 行 C
TC egress BPF ~120 行 C
控制面 CRUD ~200 行 C++/Go(map 操作 + 互斥校验)
新增 BPF Maps: 4 个
修改 katran: 0 行