refactor(loadbalancer): implement sticky sessions and improve algorithm separation

- Refactor load balancer interface to separate server selection (ChooseServer) from request handling
- Add cookie-based sticky session support with configurable max-age and secure cookie handling
- Integrate idlewatcher requests with automatic sticky session assignment
- Improve algorithm implementations:
  * Replace fnv with xxhash3 for better performance in IP hash and server keys
  * Add proper bounds checking and error handling in all algorithms
  * Separate concerns between server selection and request processing
- Add Sticky and StickyMaxAge fields to LoadBalancerConfig
- Create dedicated sticky.go for session management utilities
This commit is contained in:
yusing
2025-11-07 15:24:57 +08:00
parent 910ef639a4
commit d33ff2192a
6 changed files with 166 additions and 37 deletions

View File

@@ -1,11 +1,11 @@
package loadbalancer
import (
"hash/fnv"
"net"
"net/http"
"sync"
"github.com/bytedance/gopkg/util/xxhash3"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
@@ -19,6 +19,9 @@ type ipHash struct {
mu sync.Mutex
}
var _ impl = (*ipHash)(nil)
var _ customServeHTTP = (*ipHash)(nil)
func (lb *LoadBalancer) newIPHash() impl {
impl := &ipHash{LoadBalancer: lb}
if len(lb.Options) == 0 {
@@ -62,31 +65,27 @@ func (impl *ipHash) OnRemoveServer(srv types.LoadBalancerServer) {
}
func (impl *ipHash) ServeHTTP(_ types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request) {
if impl.realIP != nil {
impl.realIP.ModifyRequest(impl.serveHTTP, rw, r)
} else {
impl.serveHTTP(rw, r)
}
}
func (impl *ipHash) serveHTTP(rw http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(rw, "Internal error", http.StatusInternalServerError)
impl.l.Err(err).Msg("invalid remote address " + r.RemoteAddr)
return
}
idx := hashIP(ip) % uint32(len(impl.pool))
srv := impl.pool[idx]
srv := impl.ChooseServer(impl.pool, r)
if srv == nil || srv.Status().Bad() {
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
return
}
if impl.realIP != nil {
impl.realIP.ModifyRequest(srv.ServeHTTP, rw, r)
} else {
srv.ServeHTTP(rw, r)
}
srv.ServeHTTP(rw, r)
}
func hashIP(ip string) uint32 {
h := fnv.New32a()
h.Write([]byte(ip))
return h.Sum32()
func (impl *ipHash) ChooseServer(_ types.LoadBalancerServers, r *http.Request) types.LoadBalancerServer {
if len(impl.pool) == 0 {
return nil
}
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
}
return impl.pool[xxhash3.HashString(ip)%uint64(len(impl.pool))]
}