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
This commit is contained in:
parent
503daf65d3
commit
f12ffd180f
26
CHANGELOG.md
26
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
|
||||
|
||||
2
Makefile
2
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")
|
||||
|
||||
@ -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
|
||||
//
|
||||
// 使用示例:
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user