Skip to content

Commit d26369f

Browse files
committed
enhance: add FORWARD rules for IPVS FullNAT mode, improve docs and tests, bump version to 0.3.3
1 parent a62ffad commit d26369f

10 files changed

Lines changed: 438 additions & 31 deletions

File tree

cmd/ezlb/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
var (
1717
BuildTime string
1818
BuildCommit string
19-
Version = "0.3.2"
19+
Version = "0.3.3"
2020
configPath string
2121
showVersion bool
2222
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# FullNAT 模式下报文流转与排查
2+
3+
## 问题现象
4+
现在客户端请求 ezlb(IPVS)服务端口失败,tcpdump 抓包发现如下:
5+
1. 客户端 tcp sync ---> ezlb 服务器,正常
6+
2. ezlb 服务器对fullNAT后 tcp sync ---> 后端服务器,正常
7+
3. 后端服务器收到 tcp sync ,返回 sync ack 正常
8+
4. ezlb 服务器 收到 sync ack 后没有反应了,好像丢弃了
9+
10+
## 根因分析
11+
IPVS 的工作方式比较特殊:IPVS 工作在 INPUT 链上,去程的包被 IPVS 直接在 INPUT 链上截获并做 NAT 转发,不走 FORWARD 链。但回程的 SYN-ACK 到达 ezlb 后,conntrack 做完 un-SNAT,此时 dst 变成了客户端 IP,这个包需要被转发出去 → 走 FORWARD 链 → 被 DROP!
12+
13+
### 去程
14+
客户端 → ezlb(INPUT 链,IPVS 在 INPUT 链上做 DNAT)→ IPVS DNAT → POSTROUTING(SNAT)→ 后端
15+
16+
### 返程
17+
后端 → ezlb(PREROUTING,conntrack un-SNAT)→ FORWARD 链 → POSTROUTING(IPVS un-DNAT)→ 客户端
18+
19+
## 解决方案
20+
1. iptables -P FORWARD ACCEPT
21+
2. 新版本ezlb 代码中添加 EZLB-FORWARD iptables 链放行ipvs FullNAT 模式下的返程流量。

docs/deploy/ezlb.service

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ After=network.target
55

66
[Service]
77
Type=simple
8-
ExecStartPre=/sbin/sysctl -w net.ipv4.ip_forward=1
9-
ExecStartPre=/sbin/sysctl -w net.ipv4.vs.conntrack=1
108
ExecStartPre=/sbin/modprobe ip_vs
119
ExecStartPre=/sbin/modprobe ip_vs_rr
1210
ExecStartPre=/sbin/modprobe ip_vs_wrr
1311
ExecStartPre=/sbin/modprobe ip_vs_lc
12+
ExecStartPre=/sbin/sysctl -w net.ipv4.ip_forward=1
13+
ExecStartPre=/sbin/sysctl -w net.ipv4.vs.conntrack=1
14+
ExecStartPre=/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0
15+
ExecStartPre=/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0
1416
ExecStart=/usr/local/bin/ezlb start -c /etc/ezlb/ezlb.yaml
1517
ExecReload=/bin/kill -HUP $MAINPID
1618
Restart=on-failure

docs/load/bench.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
# ezlb_bench.sh 使用 wrk 对 ezlb 转发的 (VIP:PORT) 做多档位压测,并采集 CPU 与连接状态
3+
# 使用方式:
4+
# 1. 在客户端压力机上安装 wrk,并确保能免密 SSH 访问负载均衡器节点(用于抓取 CPU、软中断、socket 状态)。
5+
# 2. 将 VIP_HOST/VIP_PORT 修改为 ezlb 对外监听的地址端口;如需压测 TCP 其他协议,可换用 wrk --script 或自定义 TCP 脚本。
6+
# 3. CONCURRENCY_LEVELS 可根据目标并发量增减;DURATION 设置为每档位压测时间(建议 ≥60s)。
7+
# 4. 运行脚本:chmod +x ezlb_bench.sh && ./ezlb_bench.sh。
8+
# 5. 结果会生成在 logs/ 目录:wrk_*.log 记录吞吐、延迟;lb_stats_*.log 记录压测期间负载均衡器CPU与连接概况,方便对照分析。
9+
#
10+
# 如果需要 UDP 或更底层 TCP 测试,可将 run_wrk 换成 iperf3 -u/netperf 等命令,其他结构保持不变。
11+
12+
VIP_HOST="10.0.0.1"
13+
VIP_PORT="80"
14+
DURATION="60s" # 每档位压测时长
15+
THREADS=4 # wrk 工作线程数
16+
CONCURRENCY_LEVELS=("512" "1024" "4096" "8192")
17+
18+
# 被测负载均衡器节点,用于采集状态
19+
LB_HOST="10.0.0.10"
20+
21+
run_wrk() {
22+
local c=$1
23+
echo "=== 并发 $c ==="
24+
wrk \
25+
-t "${THREADS}" \
26+
-c "${c}" \
27+
-d "${DURATION}" \
28+
--latency \
29+
"http://${VIP_HOST}:${VIP_PORT}/healthz" \
30+
| tee "logs/wrk_${c}.log"
31+
}
32+
33+
collect_lb_stats() {
34+
ssh "${LB_HOST}" "date '+%F %T'; \
35+
mpstat 1 5 | tail -n +4; \
36+
echo '--- softirqs ---'; \
37+
cat /proc/softirqs | head -n 20; \
38+
echo '--- ss summary ---'; \
39+
ss -s" \
40+
> "logs/lb_stats_$(date +%s).log"
41+
}
42+
43+
mkdir -p logs
44+
45+
for concurrency in "${CONCURRENCY_LEVELS[@]}"; do
46+
collect_lb_stats &
47+
RUN_ID=$!
48+
run_wrk "${concurrency}"
49+
wait "${RUN_ID}"
50+
echo ""
51+
done

pkg/lvs/reconciler.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,21 @@ func (r *Reconciler) Cleanup() error {
172172
return nil
173173
}
174174

175-
// reconcileSNAT builds the desired SNAT rules from configs with full_nat enabled
176-
// and delegates to the SNAT manager for declarative reconciliation.
175+
// reconcileSNAT builds the desired SNAT and FORWARD rules from configs with
176+
// full_nat enabled and delegates to the SNAT manager for declarative reconciliation.
177+
// FORWARD rules are needed because IPVS NAT mode requires packets to traverse
178+
// the FORWARD chain, which may have a DROP policy (e.g. Docker environments).
177179
func (r *Reconciler) reconcileSNAT(configs []config.ServiceConfig) error {
178-
var desiredRules []snat.SNATRule
180+
var desiredSNATRules []snat.SNATRule
181+
var desiredForwardRules []snat.ForwardRule
179182

180183
for _, svcCfg := range configs {
181184
if !svcCfg.FullNAT {
182185
continue
183186
}
184187

185188
for _, backendCfg := range svcCfg.Backends {
186-
// Only create SNAT rules for healthy backends
189+
// Only create rules for healthy backends
187190
if svcCfg.HealthCheck.IsEnabled() && !r.healthMgr.IsHealthy(backendCfg.Address) {
188191
continue
189192
}
@@ -202,16 +205,30 @@ func (r *Reconciler) reconcileSNAT(configs []config.ServiceConfig) error {
202205
protocol = "tcp"
203206
}
204207

205-
desiredRules = append(desiredRules, snat.SNATRule{
208+
desiredSNATRules = append(desiredSNATRules, snat.SNATRule{
206209
BackendIP: backendHost,
207210
BackendPort: uint16(backendPort),
208211
Protocol: protocol,
209212
SnatIP: svcCfg.SnatIP,
210213
})
214+
215+
desiredForwardRules = append(desiredForwardRules, snat.ForwardRule{
216+
BackendIP: backendHost,
217+
BackendPort: uint16(backendPort),
218+
Protocol: protocol,
219+
})
211220
}
212221
}
213222

214-
return r.snatMgr.Reconcile(desiredRules)
223+
if err := r.snatMgr.Reconcile(desiredSNATRules); err != nil {
224+
return fmt.Errorf("snat rules: %w", err)
225+
}
226+
227+
if err := r.snatMgr.ReconcileForward(desiredForwardRules); err != nil {
228+
return fmt.Errorf("forward rules: %w", err)
229+
}
230+
231+
return nil
215232
}
216233

217234
// buildDesiredState converts config services into the desired IPVS state,

pkg/lvs/reconciler_snat_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ func TestReconcile_FullNATGeneratesSNATRules(t *testing.T) {
5050
if len(managed) != 2 {
5151
t.Fatalf("expected 2 SNAT rules, got %d", len(managed))
5252
}
53+
54+
// Verify FORWARD rules were created via fake manager
55+
managedForward := fakeSnatMgr.GetManagedForward()
56+
if len(managedForward) != 2 {
57+
t.Fatalf("expected 2 FORWARD rules, got %d", len(managedForward))
58+
}
59+
forwardKey1 := "192.168.1.1:53/udp"
60+
if _, exists := managedForward[forwardKey1]; !exists {
61+
t.Errorf("expected FORWARD rule %q to exist", forwardKey1)
62+
}
63+
forwardKey2 := "192.168.1.2:53/udp"
64+
if _, exists := managedForward[forwardKey2]; !exists {
65+
t.Errorf("expected FORWARD rule %q to exist", forwardKey2)
66+
}
5367
}
5468

5569
func TestReconcile_FullNATDisabledSkipsSNAT(t *testing.T) {
@@ -82,4 +96,10 @@ func TestReconcile_FullNATDisabledSkipsSNAT(t *testing.T) {
8296
if len(managed) != 0 {
8397
t.Fatalf("expected 0 SNAT rules when full_nat is disabled, got %d", len(managed))
8498
}
99+
100+
// Verify no FORWARD rules were created
101+
managedForward := fakeSnatMgr.GetManagedForward()
102+
if len(managedForward) != 0 {
103+
t.Fatalf("expected 0 FORWARD rules when full_nat is disabled, got %d", len(managedForward))
104+
}
85105
}

pkg/snat/manager_fake.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ import (
88
"go.uber.org/zap"
99
)
1010

11-
// FakeManager provides an in-memory SNAT rule manager for non-Linux systems.
11+
// FakeManager provides an in-memory SNAT and FORWARD rule manager for non-Linux systems.
1212
// It simulates iptables behavior for development and testing on macOS.
1313
type FakeManager struct {
14-
managed map[string]SNATRule
15-
mu sync.Mutex
16-
logger *zap.Logger
14+
managed map[string]SNATRule
15+
managedForward map[string]ForwardRule
16+
mu sync.Mutex
17+
logger *zap.Logger
1718
}
1819

1920
// NewManager creates a fake in-memory SNAT Manager for non-Linux systems.
2021
func NewManager(logger *zap.Logger) (Manager, error) {
2122
return &FakeManager{
22-
managed: make(map[string]SNATRule),
23-
logger: logger,
23+
managed: make(map[string]SNATRule),
24+
managedForward: make(map[string]ForwardRule),
25+
logger: logger,
2426
}, nil
2527
}
2628

@@ -55,17 +57,48 @@ func (m *FakeManager) Reconcile(desired []SNATRule) error {
5557
return nil
5658
}
5759

58-
// Cleanup removes all managed SNAT rules from memory.
60+
// ReconcileForward compares desired FORWARD rules with the currently managed set in memory.
61+
func (m *FakeManager) ReconcileForward(desired []ForwardRule) error {
62+
m.mu.Lock()
63+
defer m.mu.Unlock()
64+
65+
desiredMap := make(map[string]ForwardRule, len(desired))
66+
for _, rule := range desired {
67+
desiredMap[rule.Key()] = rule
68+
}
69+
70+
// Remove stale rules
71+
for key := range m.managedForward {
72+
if _, exists := desiredMap[key]; !exists {
73+
delete(m.managedForward, key)
74+
m.logger.Debug("fake: deleted FORWARD rule", zap.String("key", key))
75+
}
76+
}
77+
78+
// Add missing rules
79+
for key, rule := range desiredMap {
80+
if _, exists := m.managedForward[key]; exists {
81+
continue
82+
}
83+
m.managedForward[key] = rule
84+
m.logger.Debug("fake: added FORWARD rule", zap.String("key", key))
85+
}
86+
87+
return nil
88+
}
89+
90+
// Cleanup removes all managed SNAT and FORWARD rules from memory.
5991
func (m *FakeManager) Cleanup() error {
6092
m.mu.Lock()
6193
defer m.mu.Unlock()
6294

6395
m.managed = make(map[string]SNATRule)
64-
m.logger.Debug("fake: cleaned up all SNAT rules")
96+
m.managedForward = make(map[string]ForwardRule)
97+
m.logger.Debug("fake: cleaned up all SNAT and FORWARD rules")
6598
return nil
6699
}
67100

68-
// GetManaged returns a copy of the currently managed rules (for testing).
101+
// GetManaged returns a copy of the currently managed SNAT rules (for testing).
69102
func (m *FakeManager) GetManaged() map[string]SNATRule {
70103
m.mu.Lock()
71104
defer m.mu.Unlock()
@@ -76,3 +109,15 @@ func (m *FakeManager) GetManaged() map[string]SNATRule {
76109
}
77110
return result
78111
}
112+
113+
// GetManagedForward returns a copy of the currently managed FORWARD rules (for testing).
114+
func (m *FakeManager) GetManagedForward() map[string]ForwardRule {
115+
m.mu.Lock()
116+
defer m.mu.Unlock()
117+
118+
result := make(map[string]ForwardRule, len(m.managedForward))
119+
for k, v := range m.managedForward {
120+
result[k] = v
121+
}
122+
return result
123+
}

0 commit comments

Comments
 (0)