From f12ffd180f794c63ed5df226539f8233f0c0d137 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 9 Jun 2026 15:59:36 +0800 Subject: [PATCH] chore: release v0.4.0 - Update CHANGELOG.md for v0.4.0 - Update Makefile FALLBACK_VERSION to 0.4.0 - Fix lint warnings (godoc comments, goconst) - Clean up code formatting --- CHANGELOG.md | 26 +++++++++++++++++++ Makefile | 2 +- internal/config/proxy_config.go | 2 +- internal/converter/nginx/converter.go | 25 +++++++++--------- .../integration/proxy_integration_test.go | 12 --------- internal/loadbalance/ewma.go | 6 +++++ internal/loadbalance/least_time.go | 6 +++-- internal/loadbalance/sticky_config.go | 2 ++ 8 files changed, 53 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3112b9c..4b6e75a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.4.0] - 2026-06-09 + +### Added + +#### 负载均衡 + +- **Least Time 负载均衡器**:基于 EWMA(指数加权移动平均)统计的最少响应时间算法,自动选择响应最快上游 +- **Session Sticky 负载均衡器**:基于 Cookie 的会话粘性,一致性哈希分片,支持 Cookie 过期、域名、路径、Secure/HttpOnly/SameSite 属性 +- 对应 YAML 配置支持:`least_time`、`sticky` 策略及参数 + +#### 平台与构建 + +- FreeBSD 部署示例 + +### Fixed + +- Least Time 响应时间记录修正 +- Sticky Cookie 格式、分片键、过期检查修复 +- Sticky 双重 Stop 防护和重启支持 +- 配置验证:`least_time` 的 `default_time` 不允许负值 + +### Tests + +- Least Time 和 Sticky 负载均衡器集成测试 +- Least Time 和 Sticky 基准测试 + ## [0.3.0] - 2026-06-05 ### Added diff --git a/Makefile b/Makefile index b4548b9..f71de74 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Makefile - Lolly Build Commands APP_NAME := lolly -FALLBACK_VERSION := 0.3.0 +FALLBACK_VERSION := 0.4.0 VERSION := $(shell git describe --tags --always --dirty 2>/dev/null | sed 's/^v//' || echo "$(FALLBACK_VERSION)") GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") diff --git a/internal/config/proxy_config.go b/internal/config/proxy_config.go index 294541e..0e6792a 100644 --- a/internal/config/proxy_config.go +++ b/internal/config/proxy_config.go @@ -19,7 +19,7 @@ import ( // 注意事项: // - Path 使用前缀匹配,较长路径优先匹配 // - 至少配置一个 Target 才能正常工作 -// - 负载均衡算法支持:round_robin、weighted_round_robin、least_conn、ip_hash、consistent_hash、random、least_time、sticky +// - 负载均衡算法支持:round_robin、weighted_round_robin、least_conn、ip_hash、consistent_hash、random、least_time、sticky // - 一致性哈希需要配置 HashKey // // 使用示例: diff --git a/internal/converter/nginx/converter.go b/internal/converter/nginx/converter.go index 160341e..1cac34d 100644 --- a/internal/converter/nginx/converter.go +++ b/internal/converter/nginx/converter.go @@ -11,10 +11,11 @@ import ( ) const ( - gzipType = "gzip" - offValue = "off" - redirectType = "redirect" - staticType = "static" + gzipType = "gzip" + offValue = "off" + redirectType = "redirect" + staticType = "static" + returnDirective = "return" ) // Warning represents a conversion warning for unsupported or partially supported directives. @@ -70,7 +71,7 @@ var unsupportedDirectives = map[string]string{ "split_clients": "the 'split_clients' directive is not supported", "geo": "the 'geo' directive is not supported; use access.geoip config instead", "range": "the 'range' directive is not supported", - "return": "the 'return' directive is not supported for non-redirect status codes; only 301/302 are supported", + returnDirective: "the 'return' directive is not supported for non-redirect status codes; only 301/302 are supported", } // Convert converts a parsed nginx configuration to a lolly configuration. @@ -273,7 +274,7 @@ func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result parseAccessLog(bd, result) case "error_log": parseErrorLog(bd, result) - case "return": + case returnDirective: parseServerReturn(bd, &baseServer, result) case "rewrite": parseRewrite(bd, &baseServer) @@ -457,7 +458,7 @@ func parseServerReturn(d *Directive, server *config.ServerConfig, result *Conver }) default: result.Warnings = append(result.Warnings, Warning{ - Directive: "return", + Directive: returnDirective, Line: d.Line, File: d.File, Message: fmt.Sprintf("return %d is not a redirect; only 301/302 are supported at server level", code), @@ -554,7 +555,7 @@ func classifyLocation(d *Directive, serverRoot string, result *ConvertResult) lo hasRootOrAlias = true case "try_files": hasTryFiles = true - case "return": + case returnDirective: if len(d.Block[i].Args) > 0 { code, err := strconv.Atoi(d.Block[i].Args[0]) if err == nil && (code == 301 || code == 302) { @@ -876,7 +877,7 @@ func convertRedirectDirectives(directives []Directive, locPath string, server *c for i := range directives { d := &directives[i] - if d.Name != "return" { + if d.Name != returnDirective { continue } @@ -900,7 +901,7 @@ func convertRedirectDirectives(directives []Directive, locPath string, server *c Flag: "permanent", }) result.Warnings = append(result.Warnings, Warning{ - Directive: "return", + Directive: returnDirective, Line: d.Line, File: d.File, Message: "return 301 converted to rewrite rule with permanent flag", @@ -912,14 +913,14 @@ func convertRedirectDirectives(directives []Directive, locPath string, server *c Flag: "redirect", }) result.Warnings = append(result.Warnings, Warning{ - Directive: "return", + Directive: returnDirective, Line: d.Line, File: d.File, Message: "return 302 converted to rewrite rule with redirect flag", }) default: result.Warnings = append(result.Warnings, Warning{ - Directive: "return", + Directive: returnDirective, Line: d.Line, File: d.File, Message: fmt.Sprintf("return %d in location is not a redirect; only 301/302 are supported", code), diff --git a/internal/integration/proxy_integration_test.go b/internal/integration/proxy_integration_test.go index 3229ba0..05d1e72 100644 --- a/internal/integration/proxy_integration_test.go +++ b/internal/integration/proxy_integration_test.go @@ -71,7 +71,6 @@ func TestProxyRequestHeaders(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证代理配置已设置 assert.NotNil(t, cfg.Headers.SetRequest) assert.Equal(t, "custom-value", cfg.Headers.SetRequest["X-Custom-Header"]) @@ -101,7 +100,6 @@ func TestProxyResponseHeaders(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证响应头配置 assert.Equal(t, "lolly", cfg.Headers.SetResponse["X-Server"]) assert.Contains(t, cfg.Headers.Remove, "X-Powered-By") @@ -125,7 +123,6 @@ func TestProxyTimeout(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证超时配置 assert.Equal(t, 1*time.Second, cfg.Timeout.Connect) assert.Equal(t, 50*time.Millisecond, cfg.Timeout.Read) @@ -150,7 +147,6 @@ func TestProxyLoadBalanceRoundRobin(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证负载均衡器类型 assert.Equal(t, "round_robin", cfg.LoadBalance) assert.Len(t, targets, 2) @@ -174,7 +170,6 @@ func TestProxyWeightedRoundRobin(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证权重配置 assert.Equal(t, 3, targets[0].Weight) assert.Equal(t, 1, targets[1].Weight) @@ -198,7 +193,6 @@ func TestProxyLeastConn(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - assert.Equal(t, "least_conn", cfg.LoadBalance) } @@ -220,7 +214,6 @@ func TestProxyIPHash(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - assert.Equal(t, "ip_hash", cfg.LoadBalance) } @@ -240,7 +233,6 @@ func TestProxyConsistentHash(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - assert.Equal(t, "consistent_hash", cfg.LoadBalance) assert.Equal(t, "uri", cfg.HashKey) assert.Equal(t, 150, cfg.VirtualNodes) @@ -268,7 +260,6 @@ func TestProxyErrorHandling(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证 MaxFails 配置 (int64 类型) assert.Equal(t, int64(3), targets[0].MaxFails) assert.Equal(t, 10*time.Second, targets[0].FailTimeout) @@ -300,7 +291,6 @@ func TestProxyCacheConfig(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证缓存配置 assert.True(t, cfg.Cache.Enabled) assert.Equal(t, 60*time.Second, cfg.Cache.MaxAge) @@ -330,7 +320,6 @@ func TestProxyNextUpstream(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证故障转移配置 assert.Equal(t, 3, cfg.NextUpstream.Tries) assert.Contains(t, cfg.NextUpstream.HTTPCodes, 502) @@ -360,7 +349,6 @@ func TestProxyHealthCheck(t *testing.T) { _, err := proxy.NewProxy(cfg, targets, nil, nil) require.NoError(t, err) - // 验证健康检查配置 assert.Equal(t, 10*time.Second, cfg.HealthCheck.Interval) assert.Equal(t, "/health", cfg.HealthCheck.Path) diff --git a/internal/loadbalance/ewma.go b/internal/loadbalance/ewma.go index 383c7d1..b94c519 100644 --- a/internal/loadbalance/ewma.go +++ b/internal/loadbalance/ewma.go @@ -14,10 +14,12 @@ type EWMAStats struct { const defaultAlphaScale = 300 // alpha = 0.3 +// NewEWMAStats 创建新的 EWMA 统计器。 func NewEWMAStats() *EWMAStats { return &EWMAStats{} } +// Record 记录一次响应时间样本。 func (e *EWMAStats) Record(headerTime, lastByteTime time.Duration) { e.recordAtomic(&e.headerTime, headerTime) e.recordAtomic(&e.lastByteTime, lastByteTime) @@ -41,18 +43,22 @@ func (e *EWMAStats) recordAtomic(ptr *atomic.Int64, newValue time.Duration) { } } +// HeaderTime 返回首字节时间的 EWMA 值。 func (e *EWMAStats) HeaderTime() time.Duration { return time.Duration(e.headerTime.Load()) } +// LastByteTime 返回完整响应时间的 EWMA 值。 func (e *EWMAStats) LastByteTime() time.Duration { return time.Duration(e.lastByteTime.Load()) } +// SampleCount 返回已记录的样本数量。 func (e *EWMAStats) SampleCount() int64 { return e.sampleCount.Load() } +// Reset 重置所有统计数据。 func (e *EWMAStats) Reset() { e.headerTime.Store(0) e.lastByteTime.Store(0) diff --git a/internal/loadbalance/least_time.go b/internal/loadbalance/least_time.go index 8b43a7f..a68d93d 100644 --- a/internal/loadbalance/least_time.go +++ b/internal/loadbalance/least_time.go @@ -97,5 +97,7 @@ func (l *LeastTime) GetMetric() string { return l.metric } -var _ Balancer = (*LeastTime)(nil) -var _ ResponseTimeRecorder = (*LeastTime)(nil) +var ( + _ Balancer = (*LeastTime)(nil) + _ ResponseTimeRecorder = (*LeastTime)(nil) +) diff --git a/internal/loadbalance/sticky_config.go b/internal/loadbalance/sticky_config.go index 39e8280..6888772 100644 --- a/internal/loadbalance/sticky_config.go +++ b/internal/loadbalance/sticky_config.go @@ -2,6 +2,7 @@ package loadbalance import "time" +// StickyConfig 配置 Sticky 负载均衡的 Cookie 参数。 type StickyConfig struct { Enabled bool `yaml:"enabled"` Name string `yaml:"name"` @@ -13,6 +14,7 @@ type StickyConfig struct { SameSite string `yaml:"same_site"` } +// DefaultStickyConfig 返回 Sticky 负载均衡的默认配置。 func DefaultStickyConfig() StickyConfig { return StickyConfig{ Name: "lolly_route",