Skip to content

Commit e08527f

Browse files
committed
feat: add HTTP health check support
1 parent 853f3f3 commit e08527f

10 files changed

Lines changed: 433 additions & 15 deletions

File tree

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A lightweight Layer-4 TCP load balancer based on Linux IPVS, using declarative r
99
- **IPVS Kernel-Level Load Balancing**: High-performance Layer-4 forwarding powered by Linux IPVS
1010
- **Declarative Reconcile**: Automatically compares desired state with actual IPVS rules and applies incremental changes
1111
- **Multiple Scheduling Algorithms**: Round Robin (rr), Weighted Round Robin (wrr), Least Connection (lc), Weighted Least Connection (wlc), Destination Hashing (dh), Source Hashing (sh)
12-
- **TCP Health Checks**: Independent health check configuration per service, with option to disable
12+
- **TCP & HTTP Health Checks**: Independent health check configuration per service, supporting TCP connection probes and HTTP GET probes with configurable path and expected status code
1313
- **Hot Config Reload**: File changes automatically trigger reconciliation without restart
1414

1515
## Quick Start
@@ -41,6 +41,7 @@ services:
4141
scheduler: wrr
4242
health_check:
4343
enabled: true
44+
type: tcp # optional: tcp (default), http
4445
interval: 5s
4546
timeout: 3s
4647
fail_count: 3
@@ -50,6 +51,25 @@ services:
5051
weight: 5
5152
- address: 192.168.1.11:8080
5253
weight: 3
54+
55+
- name: api-service
56+
listen: 10.0.0.1:443
57+
protocol: tcp
58+
scheduler: wlc
59+
health_check:
60+
enabled: true
61+
type: http # HTTP health check
62+
interval: 10s
63+
timeout: 5s
64+
fail_count: 5
65+
rise_count: 3
66+
http_path: /healthz # default: /
67+
http_expected_status: 200 # default: 200
68+
backends:
69+
- address: 192.168.2.10:8443
70+
weight: 1
71+
- address: 192.168.2.11:8443
72+
weight: 1
5373
```
5474
5575
### Usage
@@ -86,7 +106,7 @@ ezlb/
86106
├── pkg/
87107
│ ├── config/ # Config management (loading, validation, hot reload)
88108
│ ├── lvs/ # IPVS management (operations, reconcile)
89-
│ ├── healthcheck/ # Health checking (TCP probes)
109+
│ ├── healthcheck/ # Health checking (TCP & HTTP probes)
90110
│ └── server/ # Server orchestration (lifecycle management)
91111
├── tests/e2e/ # End-to-end tests
92112
├── examples/ # Example configurations

README_CN.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- **IPVS 内核级负载均衡**:基于 Linux IPVS 实现高性能四层转发
1010
- **声明式 Reconcile**:自动对比期望状态与实际 IPVS 规则,增量同步变更
1111
- **多种调度算法**:支持轮询 (rr)、加权轮询 (wrr)、最少连接 (lc)、加权最少连接 (wlc)、目标地址哈希 (dh)、源地址哈希 (sh)
12-
- **TCP 健康检查**:每个服务独立配置检查参数,支持禁用
12+
- **TCP & HTTP 健康检查**:每个服务独立配置检查参数,支持 TCP 连接探测和 HTTP GET 探测(可配置路径和期望状态码)
1313
- **配置热加载**:修改配置文件自动触发 Reconcile,无需重启
1414

1515
## 快速开始
@@ -41,6 +41,7 @@ services:
4141
scheduler: wrr
4242
health_check:
4343
enabled: true
44+
type: tcp # 可选: tcp(默认)、http
4445
interval: 5s
4546
timeout: 3s
4647
fail_count: 3
@@ -50,6 +51,25 @@ services:
5051
weight: 5
5152
- address: 192.168.1.11:8080
5253
weight: 3
54+
55+
- name: api-service
56+
listen: 10.0.0.1:443
57+
protocol: tcp
58+
scheduler: wlc
59+
health_check:
60+
enabled: true
61+
type: http # HTTP 健康检查
62+
interval: 10s
63+
timeout: 5s
64+
fail_count: 5
65+
rise_count: 3
66+
http_path: /healthz # 默认: /
67+
http_expected_status: 200 # 默认: 200
68+
backends:
69+
- address: 192.168.2.10:8443
70+
weight: 1
71+
- address: 192.168.2.11:8443
72+
weight: 1
5373
```
5474
5575
### 运行
@@ -86,7 +106,7 @@ ezlb/
86106
├── pkg/
87107
│ ├── config/ # 配置管理(加载、校验、热加载)
88108
│ ├── lvs/ # IPVS 管理(操作封装、Reconcile)
89-
│ ├── healthcheck/ # 健康检查(TCP 探测)
109+
│ ├── healthcheck/ # 健康检查(TCP & HTTP 探测)
90110
│ └── server/ # 服务编排(生命周期管理)
91111
├── tests/e2e/ # 端到端测试
92112
├── examples/ # 示例配置

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.1.5"
19+
Version = "0.1.6"
2020
configPath string
2121
showVersion bool
2222
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
### 增加HTTP方式健康检查 ###
2+
在现有 TCP 健康检查基础上,增加可选的 HTTP 健康检查方式。用户可以在配置文件中通过 type 字段选择 tcp 或 http,HTTP 模式支持配置路径、期望状态码等参数。
3+
4+
5+
## 现状分析
6+
7+
当前项目仅支持 TCP 健康检查:
8+
- `pkg/healthcheck/checker.go` 定义了 `Checker` 接口和 `TCPChecker` 实现
9+
- `pkg/healthcheck/manager.go``UpdateTargets` 中硬编码 `NewTCPChecker`
10+
- `pkg/config/config.go``HealthCheckConfig` 无检查类型字段
11+
12+
## 实施步骤
13+
14+
### 步骤 1:扩展配置结构体 `HealthCheckConfig`
15+
16+
**文件**: `pkg/config/config.go`
17+
18+
`HealthCheckConfig` 中新增以下字段:
19+
- `Type string` - 健康检查类型,可选值 `tcp`(默认)、`http`
20+
- `HTTPPath string` - HTTP 健康检查请求路径,默认 `/`
21+
- `HTTPExpectedStatus int` - 期望的 HTTP 状态码,默认 `200`
22+
23+
新增辅助方法:
24+
- `GetType() string` - 返回检查类型,默认 `tcp`
25+
- `GetHTTPPath() string` - 返回 HTTP 路径,默认 `/`
26+
- `GetHTTPExpectedStatus() int` - 返回期望状态码,默认 `200`
27+
28+
`Validate` 函数中增加校验:
29+
- `type` 字段仅允许 `tcp``http`(或为空,默认 `tcp`
30+
-`type``http` 时,`http_path` 必须以 `/` 开头(如果设置了的话)
31+
- `http_expected_status` 必须在 100-599 范围内(如果设置了的话)
32+
33+
### 步骤 2:实现 `HTTPChecker`
34+
35+
**文件**: `pkg/healthcheck/checker.go`
36+
37+
新增 `HTTPChecker` 结构体,实现 `Checker` 接口:
38+
- 字段:`timeout``path``expectedStatus`
39+
- `Check(address string)` 方法:向 `http://{address}{path}` 发送 GET 请求,检查响应状态码是否匹配期望值
40+
- 使用 `http.Client` 并设置超时
41+
42+
新增构造函数 `NewHTTPChecker(timeout, path, expectedStatus)`
43+
44+
### 步骤 3:修改 Manager 根据配置选择 Checker 类型
45+
46+
**文件**: `pkg/healthcheck/manager.go`
47+
48+
修改 `UpdateTargets` 方法中创建 checker 的逻辑(约第 99 行):
49+
- 当前硬编码:`checker := NewTCPChecker(svcCfg.HealthCheck.GetTimeout())`
50+
- 改为根据 `svcCfg.HealthCheck.GetType()` 选择创建 `TCPChecker``HTTPChecker`
51+
52+
### 步骤 4:补充单元测试
53+
54+
**文件**: `pkg/healthcheck/checker_test.go`
55+
56+
新增 HTTP 健康检查测试用例:
57+
- `TestHTTPChecker_Success` - 启动本地 HTTP server,验证正常返回 200
58+
- `TestHTTPChecker_UnexpectedStatus` - 后端返回非期望状态码,验证报错
59+
- `TestHTTPChecker_ConnectionRefused` - 连接不可达,验证报错
60+
- `TestHTTPChecker_CustomPath` - 验证自定义路径生效
61+
- `TestHTTPChecker_Timeout` - 验证超时场景
62+
63+
**文件**: `pkg/config/config_test.go`
64+
65+
新增配置校验测试用例:
66+
- `TestValidate_HealthCheckTypeHTTP` - type 为 http 时校验通过
67+
- `TestValidate_HealthCheckTypeInvalid` - type 为非法值时校验失败
68+
- `TestGetType_Default` - 未设置 type 时默认返回 tcp
69+
70+
### 步骤 5:更新示例配置和文档
71+
72+
**文件**: `examples/ezlb.yaml`
73+
74+
在某个 service 中增加 HTTP 健康检查的示例配置,展示 `type``http_path``http_expected_status` 的用法。
75+
76+
**文件**: `README.md``README_CN.md`
77+
78+
更新健康检查相关描述,说明支持 TCP 和 HTTP 两种模式。
79+
80+
## 配置示例
81+
82+
```yaml
83+
health_check:
84+
enabled: true
85+
type: http # 可选: tcp (默认), http
86+
interval: 5s
87+
timeout: 3s
88+
fail_count: 3
89+
rise_count: 2
90+
http_path: /healthz # HTTP 模式专用,默认 /
91+
http_expected_status: 200 # HTTP 模式专用,默认 200
92+
```
93+
94+
95+
updateAtTime: 2026/2/11 07:18:24
96+
97+
planId: 5b2638e7-5c0b-4463-b112-60fa0266afba

examples/ezlb.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ services:
2626
scheduler: wlc
2727
health_check:
2828
enabled: true
29+
type: http
2930
interval: 10s
30-
timeout: 5s
31-
fail_count: 5
32-
rise_count: 3
31+
timeout: 2s
32+
fail_count: 3
33+
rise_count: 2
34+
http_path: /healthz
35+
http_expected_status: 200
3336
backends:
3437
- address: 192.168.2.10:8443
3538
weight: 1

pkg/config/config.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ type ServiceConfig struct {
3434

3535
// HealthCheckConfig defines per-service health check parameters.
3636
type HealthCheckConfig struct {
37-
Enabled *bool `yaml:"enabled" mapstructure:"enabled"`
38-
Interval string `yaml:"interval" mapstructure:"interval"`
39-
Timeout string `yaml:"timeout" mapstructure:"timeout"`
40-
FailCount int `yaml:"fail_count" mapstructure:"fail_count"`
41-
RiseCount int `yaml:"rise_count" mapstructure:"rise_count"`
37+
Enabled *bool `yaml:"enabled" mapstructure:"enabled"`
38+
Type string `yaml:"type" mapstructure:"type"`
39+
Interval string `yaml:"interval" mapstructure:"interval"`
40+
Timeout string `yaml:"timeout" mapstructure:"timeout"`
41+
FailCount int `yaml:"fail_count" mapstructure:"fail_count"`
42+
RiseCount int `yaml:"rise_count" mapstructure:"rise_count"`
43+
HTTPPath string `yaml:"http_path" mapstructure:"http_path"`
44+
HTTPExpectedStatus int `yaml:"http_expected_status" mapstructure:"http_expected_status"`
4245
}
4346

4447
// IsEnabled returns whether health check is enabled for this service.
@@ -76,6 +79,33 @@ func (h HealthCheckConfig) GetTimeout() time.Duration {
7679
return duration
7780
}
7881

82+
// GetType returns the health check type.
83+
// Defaults to "tcp" if not set.
84+
func (h HealthCheckConfig) GetType() string {
85+
if h.Type == "" {
86+
return "tcp"
87+
}
88+
return h.Type
89+
}
90+
91+
// GetHTTPPath returns the HTTP health check request path.
92+
// Defaults to "/" if not set.
93+
func (h HealthCheckConfig) GetHTTPPath() string {
94+
if h.HTTPPath == "" {
95+
return "/"
96+
}
97+
return h.HTTPPath
98+
}
99+
100+
// GetHTTPExpectedStatus returns the expected HTTP response status code.
101+
// Defaults to 200 if not set.
102+
func (h HealthCheckConfig) GetHTTPExpectedStatus() int {
103+
if h.HTTPExpectedStatus <= 0 {
104+
return 200
105+
}
106+
return h.HTTPExpectedStatus
107+
}
108+
79109
// GetFailCount returns the consecutive failure threshold.
80110
// Defaults to 3 if not set.
81111
func (h HealthCheckConfig) GetFailCount() int {
@@ -230,6 +260,23 @@ func Validate(cfg *Config) error {
230260
return fmt.Errorf("service %q: invalid health_check.timeout %q: %w", svc.Name, svc.HealthCheck.Timeout, err)
231261
}
232262
}
263+
264+
// Validate health check type
265+
checkType := svc.HealthCheck.GetType()
266+
if checkType != "tcp" && checkType != "http" {
267+
return fmt.Errorf("service %q: unsupported health_check.type %q (supported: tcp, http)", svc.Name, checkType)
268+
}
269+
270+
// Validate HTTP-specific parameters
271+
if checkType == "http" {
272+
if svc.HealthCheck.HTTPPath != "" && svc.HealthCheck.HTTPPath[0] != '/' {
273+
return fmt.Errorf("service %q: health_check.http_path must start with '/'", svc.Name)
274+
}
275+
if svc.HealthCheck.HTTPExpectedStatus != 0 &&
276+
(svc.HealthCheck.HTTPExpectedStatus < 100 || svc.HealthCheck.HTTPExpectedStatus > 599) {
277+
return fmt.Errorf("service %q: health_check.http_expected_status must be between 100 and 599", svc.Name)
278+
}
279+
}
233280
}
234281

235282
// Validate backends

pkg/config/config_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,88 @@ func TestValidate_HealthCheckTimeoutInvalid(t *testing.T) {
175175
}
176176
}
177177

178+
func TestValidate_HealthCheckTypeHTTP(t *testing.T) {
179+
cfg := validConfig()
180+
cfg.Services[0].HealthCheck.Type = "http"
181+
cfg.Services[0].HealthCheck.HTTPPath = "/healthz"
182+
cfg.Services[0].HealthCheck.HTTPExpectedStatus = 200
183+
err := Validate(cfg)
184+
if err != nil {
185+
t.Fatalf("expected valid config with http health check, got: %v", err)
186+
}
187+
}
188+
189+
func TestValidate_HealthCheckTypeInvalid(t *testing.T) {
190+
cfg := validConfig()
191+
cfg.Services[0].HealthCheck.Type = "grpc"
192+
err := Validate(cfg)
193+
if err == nil {
194+
t.Fatal("expected error for unsupported health_check.type, got nil")
195+
}
196+
}
197+
198+
func TestValidate_HealthCheckHTTPPathInvalid(t *testing.T) {
199+
cfg := validConfig()
200+
cfg.Services[0].HealthCheck.Type = "http"
201+
cfg.Services[0].HealthCheck.HTTPPath = "no-leading-slash"
202+
err := Validate(cfg)
203+
if err == nil {
204+
t.Fatal("expected error for http_path without leading slash, got nil")
205+
}
206+
}
207+
208+
func TestValidate_HealthCheckHTTPExpectedStatusInvalid(t *testing.T) {
209+
cfg := validConfig()
210+
cfg.Services[0].HealthCheck.Type = "http"
211+
cfg.Services[0].HealthCheck.HTTPExpectedStatus = 999
212+
err := Validate(cfg)
213+
if err == nil {
214+
t.Fatal("expected error for http_expected_status out of range, got nil")
215+
}
216+
}
217+
218+
func TestGetType_Default(t *testing.T) {
219+
hc := HealthCheckConfig{}
220+
if hc.GetType() != "tcp" {
221+
t.Errorf("expected default type 'tcp', got %q", hc.GetType())
222+
}
223+
}
224+
225+
func TestGetType_HTTP(t *testing.T) {
226+
hc := HealthCheckConfig{Type: "http"}
227+
if hc.GetType() != "http" {
228+
t.Errorf("expected type 'http', got %q", hc.GetType())
229+
}
230+
}
231+
232+
func TestGetHTTPPath_Default(t *testing.T) {
233+
hc := HealthCheckConfig{}
234+
if hc.GetHTTPPath() != "/" {
235+
t.Errorf("expected default http_path '/', got %q", hc.GetHTTPPath())
236+
}
237+
}
238+
239+
func TestGetHTTPPath_Custom(t *testing.T) {
240+
hc := HealthCheckConfig{HTTPPath: "/healthz"}
241+
if hc.GetHTTPPath() != "/healthz" {
242+
t.Errorf("expected http_path '/healthz', got %q", hc.GetHTTPPath())
243+
}
244+
}
245+
246+
func TestGetHTTPExpectedStatus_Default(t *testing.T) {
247+
hc := HealthCheckConfig{}
248+
if hc.GetHTTPExpectedStatus() != 200 {
249+
t.Errorf("expected default http_expected_status 200, got %d", hc.GetHTTPExpectedStatus())
250+
}
251+
}
252+
253+
func TestGetHTTPExpectedStatus_Custom(t *testing.T) {
254+
hc := HealthCheckConfig{HTTPExpectedStatus: 204}
255+
if hc.GetHTTPExpectedStatus() != 204 {
256+
t.Errorf("expected http_expected_status 204, got %d", hc.GetHTTPExpectedStatus())
257+
}
258+
}
259+
178260
func TestValidate_HealthCheckDisabledSkipsIntervalValidation(t *testing.T) {
179261
cfg := validConfig()
180262
cfg.Services[0].HealthCheck.Enabled = boolPtr(false)

0 commit comments

Comments
 (0)