feat(proxy,server): 实现 ProxyBind/Buffering/ProxyURI 和响应头控制
ProxyBind 支持指定本地地址出站连接,Buffering 控制响应缓冲模式, ProxyURI 实现 nginx proxy_pass URI 替换语义, 响应头新增 HideResponse/PassResponse/IgnoreHeaders/Cookie 域路径重写, 健康检查集成 RecordFailure/RecordSuccess 软失败状态。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c0b7e30bf0
commit
2b63666ea5
@ -210,33 +210,21 @@ func (h *HealthChecker) checkTarget(target *loadbalance.Target) {
|
|||||||
// 此方法用于被动健康检查,代理根据请求处理过程中
|
// 此方法用于被动健康检查,代理根据请求处理过程中
|
||||||
// 观察到的失败将目标标记为不健康。
|
// 观察到的失败将目标标记为不健康。
|
||||||
//
|
//
|
||||||
// 在代理错误处理中的使用示例:
|
// 同时调用 RecordFailure 记录软失败状态,配合 MaxFails/FailTimeout
|
||||||
//
|
// 实现失败计数和冷却机制。
|
||||||
// if err := forwardRequest(target, req, resp); err != nil {
|
|
||||||
// healthChecker.MarkUnhealthy(target)
|
|
||||||
// // 尝试其他目标或返回错误
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// 注意:要再次将目标标记为健康,主动健康检查
|
|
||||||
// 必须成功。没有 MarkHealthy 方法 - 健康状态只能通过
|
|
||||||
// 成功的健康检查积极恢复。
|
|
||||||
func (h *HealthChecker) MarkUnhealthy(target *loadbalance.Target) {
|
func (h *HealthChecker) MarkUnhealthy(target *loadbalance.Target) {
|
||||||
target.Healthy.Store(false)
|
target.Healthy.Store(false)
|
||||||
|
target.RecordFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkHealthy 将目标标记为健康。
|
// MarkHealthy 将目标标记为健康。
|
||||||
// 此方法用于故障转移成功后,将之前失败的目标恢复为健康状态。
|
// 此方法用于故障转移成功后,将之前失败的目标恢复为健康状态。
|
||||||
//
|
//
|
||||||
// 在故障转移成功后的使用示例:
|
// 同时调用 RecordSuccess 重置软失败状态(failCount/failedUntil),
|
||||||
//
|
// 但不修改 Healthy 标志——健康检查器对 Healthy 拥有权威。
|
||||||
// if err := retryRequest(target, req, resp); err == nil {
|
|
||||||
// healthChecker.MarkHealthy(target)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// 注意:此方法与主动健康检查独立运作,用于快速恢复
|
|
||||||
// 故障转移场景中已恢复的目标。
|
|
||||||
func (h *HealthChecker) MarkHealthy(target *loadbalance.Target) {
|
func (h *HealthChecker) MarkHealthy(target *loadbalance.Target) {
|
||||||
target.Healthy.Store(true)
|
target.Healthy.Store(true)
|
||||||
|
target.RecordSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRunning 如果健康检查器当前正在运行,则返回 true。
|
// IsRunning 如果健康检查器当前正在运行,则返回 true。
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
|
"net"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -66,6 +67,7 @@ const (
|
|||||||
lbLeastConn = "least_conn" // 最少连接
|
lbLeastConn = "least_conn" // 最少连接
|
||||||
lbIPHash = "ip_hash" // IP 哈希
|
lbIPHash = "ip_hash" // IP 哈希
|
||||||
lbConsistentHash = "consistent_hash" // 一致性哈希
|
lbConsistentHash = "consistent_hash" // 一致性哈希
|
||||||
|
lbRandom = "random" // 随机(Power of Two Choices)
|
||||||
)
|
)
|
||||||
|
|
||||||
// headersPool 复用缓存 headers map,减少分配。
|
// headersPool 复用缓存 headers map,减少分配。
|
||||||
@ -155,8 +157,12 @@ func NewProxy(cfg *config.ProxyConfig, targets []*loadbalance.Target, transportC
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
client := createHostClient(target.URL, cfg.Timeout, transportCfg, cfg.ProxySSL)
|
client := createHostClient(target.URL, cfg.Timeout, transportCfg, cfg.ProxySSL, cfg.ProxyBind, cfg.Buffering)
|
||||||
p.clients[target.URL] = client
|
clientKey := target.URL
|
||||||
|
if cfg.ProxyBind != "" {
|
||||||
|
clientKey = target.URL + "|" + cfg.ProxyBind
|
||||||
|
}
|
||||||
|
p.clients[clientKey] = client
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化代理缓存(如果启用)
|
// 初始化代理缓存(如果启用)
|
||||||
@ -213,6 +219,8 @@ func createBalancerByName(name string, cfg *config.ProxyConfig) (loadbalance.Bal
|
|||||||
virtualNodes = 150
|
virtualNodes = 150
|
||||||
}
|
}
|
||||||
return loadbalance.NewConsistentHash(virtualNodes, cfg.HashKey), nil
|
return loadbalance.NewConsistentHash(virtualNodes, cfg.HashKey), nil
|
||||||
|
case lbRandom:
|
||||||
|
return loadbalance.NewRandom(), nil
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unsupported load balance algorithm: " + name)
|
return nil, errors.New("unsupported load balance algorithm: " + name)
|
||||||
}
|
}
|
||||||
@ -255,7 +263,7 @@ func createBalancer(cfg *config.ProxyConfig) (loadbalance.Balancer, error) {
|
|||||||
//
|
//
|
||||||
// 返回值:
|
// 返回值:
|
||||||
// - *fasthttp.HostClient: 配置完成的 HostClient 实例
|
// - *fasthttp.HostClient: 配置完成的 HostClient 实例
|
||||||
func createHostClient(targetURL string, timeout config.ProxyTimeout, transportCfg *config.TransportConfig, sslCfg *config.ProxySSLConfig) *fasthttp.HostClient {
|
func createHostClient(targetURL string, timeout config.ProxyTimeout, transportCfg *config.TransportConfig, sslCfg *config.ProxySSLConfig, proxyBind string, buffering *config.ProxyBufferingConfig) *fasthttp.HostClient {
|
||||||
// 从目标 URL 解析主机和协议
|
// 从目标 URL 解析主机和协议
|
||||||
// addDefaultPort=true 确保 HostClient.Addr 包含端口(host:port 格式)
|
// addDefaultPort=true 确保 HostClient.Addr 包含端口(host:port 格式)
|
||||||
addr, isTLS := netutil.ParseTargetURL(targetURL, true)
|
addr, isTLS := netutil.ParseTargetURL(targetURL, true)
|
||||||
@ -287,6 +295,27 @@ func createHostClient(targetURL string, timeout config.ProxyTimeout, transportCf
|
|||||||
SecureErrorLogMessage: false,
|
SecureErrorLogMessage: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyBind:使用指定本地地址作为出站连接源
|
||||||
|
if proxyBind != "" {
|
||||||
|
localAddr := proxyBind
|
||||||
|
client.Dial = func(addr string) (net.Conn, error) {
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
LocalAddr: &net.TCPAddr{IP: net.ParseIP(localAddr)},
|
||||||
|
Timeout: client.MaxConnWaitTimeout,
|
||||||
|
}
|
||||||
|
return dialer.Dial("tcp", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffering 控制
|
||||||
|
if buffering != nil && buffering.Mode == "off" {
|
||||||
|
client.StreamResponseBody = true
|
||||||
|
}
|
||||||
|
if buffering != nil && buffering.BufferSize > 0 {
|
||||||
|
client.ReadBufferSize = buffering.BufferSize
|
||||||
|
client.WriteBufferSize = buffering.BufferSize
|
||||||
|
}
|
||||||
|
|
||||||
// 上游 SSL 配置(使用原生 TLSConfig)
|
// 上游 SSL 配置(使用原生 TLSConfig)
|
||||||
if sslCfg != nil && sslCfg.Enabled && isTLS {
|
if sslCfg != nil && sslCfg.Enabled && isTLS {
|
||||||
tlsCfg, err := CreateTLSConfig(sslCfg, extractHostFromURL(targetURL))
|
tlsCfg, err := CreateTLSConfig(sslCfg, extractHostFromURL(targetURL))
|
||||||
@ -534,6 +563,14 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) {
|
|||||||
// SAFETY: lifetime=ephemeral - consumed immediately by SetRequestURIBytes
|
// SAFETY: lifetime=ephemeral - consumed immediately by SetRequestURIBytes
|
||||||
path := ctx.URI().Path()
|
path := ctx.URI().Path()
|
||||||
query := ctx.URI().QueryString()
|
query := ctx.URI().QueryString()
|
||||||
|
|
||||||
|
// ProxyURI 语义:当 target.ProxyURI 设置时,替换请求路径
|
||||||
|
// 这实现了 nginx proxy_pass URI 传递语义:
|
||||||
|
// proxy_pass http://backend/v2/ → 请求路径替换为 /v2/
|
||||||
|
if target.ProxyURI != "" {
|
||||||
|
path = []byte(target.ProxyURI)
|
||||||
|
}
|
||||||
|
|
||||||
targetURI := make([]byte, 0, len(target.URL)+len(path)+len(query)+1)
|
targetURI := make([]byte, 0, len(target.URL)+len(path)+len(query)+1)
|
||||||
targetURI = append(targetURI, target.URL...)
|
targetURI = append(targetURI, target.URL...)
|
||||||
targetURI = append(targetURI, path...)
|
targetURI = append(targetURI, path...)
|
||||||
@ -642,6 +679,9 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) {
|
|||||||
// 请求成功,减少连接计数
|
// 请求成功,减少连接计数
|
||||||
loadbalance.DecrementConnections(target)
|
loadbalance.DecrementConnections(target)
|
||||||
|
|
||||||
|
// 记录成功,重置软失败状态
|
||||||
|
target.RecordSuccess()
|
||||||
|
|
||||||
// 检测 X-Accel-Redirect 头,支持内部重定向
|
// 检测 X-Accel-Redirect 头,支持内部重定向
|
||||||
if redirectPath := ctx.Response.Header.Peek("X-Accel-Redirect"); len(redirectPath) > 0 {
|
if redirectPath := ctx.Response.Header.Peek("X-Accel-Redirect"); len(redirectPath) > 0 {
|
||||||
utils.SetInternalRedirect(ctx, string(redirectPath))
|
utils.SetInternalRedirect(ctx, string(redirectPath))
|
||||||
@ -949,15 +989,13 @@ func (p *Proxy) extractHashKey(ctx *fasthttp.RequestCtx, hashKey string) string
|
|||||||
|
|
||||||
// getClient 返回指定目标 URL 对应的 HostClient 连接池实例。
|
// getClient 返回指定目标 URL 对应的 HostClient 连接池实例。
|
||||||
// 如果目标 URL 不存在于连接池中,返回 nil。
|
// 如果目标 URL 不存在于连接池中,返回 nil。
|
||||||
//
|
|
||||||
// 参数:
|
|
||||||
// - targetURL: 后端目标 URL
|
|
||||||
//
|
|
||||||
// 返回值:
|
|
||||||
// - *fasthttp.HostClient: 对应的连接池实例
|
|
||||||
func (p *Proxy) getClient(targetURL string) *fasthttp.HostClient {
|
func (p *Proxy) getClient(targetURL string) *fasthttp.HostClient {
|
||||||
|
key := targetURL
|
||||||
|
if p.config.ProxyBind != "" {
|
||||||
|
key = targetURL + "|" + p.config.ProxyBind
|
||||||
|
}
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
client := p.clients[targetURL]
|
client := p.clients[key]
|
||||||
p.mu.RUnlock()
|
p.mu.RUnlock()
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
@ -1012,17 +1050,103 @@ func (p *Proxy) modifyRequestHeaders(ctx *fasthttp.RequestCtx, target *loadbalan
|
|||||||
// 参数:
|
// 参数:
|
||||||
// - ctx: FastHTTP 请求上下文
|
// - ctx: FastHTTP 请求上下文
|
||||||
func (p *Proxy) modifyResponseHeaders(ctx *fasthttp.RequestCtx) {
|
func (p *Proxy) modifyResponseHeaders(ctx *fasthttp.RequestCtx) {
|
||||||
|
respHeaders := &ctx.Response.Header
|
||||||
|
|
||||||
|
// 构建 PassResponse 集合(多处使用)
|
||||||
|
passSet := make(map[string]bool, len(p.config.Headers.PassResponse))
|
||||||
|
for _, h := range p.config.Headers.PassResponse {
|
||||||
|
passSet[h] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassResponse 白名单模式:仅传递列出的头部
|
||||||
|
if len(passSet) > 0 {
|
||||||
|
var toDelete []string
|
||||||
|
for key := range respHeaders.All() {
|
||||||
|
if !passSet[string(key)] {
|
||||||
|
toDelete = append(toDelete, string(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, k := range toDelete {
|
||||||
|
respHeaders.Del(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HideResponse:移除指定的响应头(PassResponse 优先,跳过已传递的头部)
|
||||||
|
for _, key := range p.config.Headers.HideResponse {
|
||||||
|
if !passSet[key] {
|
||||||
|
respHeaders.Del(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreHeaders:从请求和响应中移除(PassResponse 优先)
|
||||||
|
for _, key := range p.config.Headers.IgnoreHeaders {
|
||||||
|
ctx.Request.Header.Del(key)
|
||||||
|
if !passSet[key] {
|
||||||
|
respHeaders.Del(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie 域/路径重写
|
||||||
|
if p.config.Headers.CookieDomain != "" || p.config.Headers.CookiePath != "" {
|
||||||
|
p.rewriteCookies(respHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
// 从配置设置自定义响应头(支持变量展开)
|
// 从配置设置自定义响应头(支持变量展开)
|
||||||
if p.config.Headers.SetResponse != nil {
|
if p.config.Headers.SetResponse != nil {
|
||||||
vc := variable.NewContext(ctx)
|
vc := variable.NewContext(ctx)
|
||||||
defer variable.ReleaseContext(vc)
|
defer variable.ReleaseContext(vc)
|
||||||
for key, value := range p.config.Headers.SetResponse {
|
for key, value := range p.config.Headers.SetResponse {
|
||||||
expanded := vc.Expand(value)
|
expanded := vc.Expand(value)
|
||||||
ctx.Response.Header.Set(key, expanded)
|
respHeaders.Set(key, expanded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rewriteCookies 重写响应中 Set-Cookie 头的 domain 和 path。
|
||||||
|
func (p *Proxy) rewriteCookies(respHeaders *fasthttp.ResponseHeader) {
|
||||||
|
cookieDomain := p.config.Headers.CookieDomain
|
||||||
|
cookiePath := p.config.Headers.CookiePath
|
||||||
|
if cookieDomain == "" && cookiePath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies []string
|
||||||
|
for _, value := range respHeaders.Cookies() {
|
||||||
|
cookie := string(value)
|
||||||
|
if cookieDomain != "" {
|
||||||
|
cookie = rewriteCookieAttr(cookie, "Domain", cookieDomain)
|
||||||
|
}
|
||||||
|
if cookiePath != "" {
|
||||||
|
cookie = rewriteCookieAttr(cookie, "Path", cookiePath)
|
||||||
|
}
|
||||||
|
cookies = append(cookies, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cookies) > 0 {
|
||||||
|
respHeaders.Del("Set-Cookie")
|
||||||
|
for _, c := range cookies {
|
||||||
|
respHeaders.Add("Set-Cookie", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewriteCookieAttr 替换 Cookie 字符串中指定属性的值。
|
||||||
|
func rewriteCookieAttr(cookie, attr, newValue string) string {
|
||||||
|
prefix := attr + "="
|
||||||
|
idx := strings.Index(cookie, prefix)
|
||||||
|
if idx == -1 {
|
||||||
|
return cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
start := idx + len(prefix)
|
||||||
|
end := start
|
||||||
|
for end < len(cookie) && cookie[end] != ';' && cookie[end] != ' ' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie[:start] + newValue + cookie[end:]
|
||||||
|
}
|
||||||
|
|
||||||
// isWebSocketRequest 检查请求是否为 WebSocket 升级请求。
|
// isWebSocketRequest 检查请求是否为 WebSocket 升级请求。
|
||||||
//
|
//
|
||||||
// 通过检查 Connection 和 Upgrade 请求头判断:
|
// 通过检查 Connection 和 Upgrade 请求头判断:
|
||||||
@ -1076,8 +1200,12 @@ func (p *Proxy) UpdateTargets(targets []*loadbalance.Target) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
client := createHostClient(target.URL, p.config.Timeout, nil, p.config.ProxySSL)
|
client := createHostClient(target.URL, p.config.Timeout, nil, p.config.ProxySSL, p.config.ProxyBind, p.config.Buffering)
|
||||||
p.clients[target.URL] = client
|
clientKey := target.URL
|
||||||
|
if p.config.ProxyBind != "" {
|
||||||
|
clientKey = target.URL + "|" + p.config.ProxyBind
|
||||||
|
}
|
||||||
|
p.clients[clientKey] = client
|
||||||
}
|
}
|
||||||
|
|
||||||
p.targets = targets
|
p.targets = targets
|
||||||
|
|||||||
@ -999,7 +999,15 @@ func (s *Server) registerProxyRoutesWithLocationEngine(serverCfg *config.ServerC
|
|||||||
// 转换目标
|
// 转换目标
|
||||||
targets := make([]*loadbalance.Target, len(proxyCfg.Targets))
|
targets := make([]*loadbalance.Target, len(proxyCfg.Targets))
|
||||||
for j, t := range proxyCfg.Targets {
|
for j, t := range proxyCfg.Targets {
|
||||||
targets[j] = loadbalance.NewTargetFromConfig(t.URL, t.Weight)
|
failTimeout := t.FailTimeout
|
||||||
|
if t.MaxFails > 0 && failTimeout == 0 {
|
||||||
|
failTimeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
targets[j] = loadbalance.NewTargetFromConfig(
|
||||||
|
t.URL, t.Weight,
|
||||||
|
int64(t.MaxConns), int64(t.MaxFails), failTimeout,
|
||||||
|
t.Backup, t.Down, t.ProxyURI,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 传递 Transport 配置和 Lua 引擎
|
// 传递 Transport 配置和 Lua 引擎
|
||||||
@ -1122,7 +1130,15 @@ func (s *Server) registerProxyRoutes(router *handler.Router, serverCfg *config.S
|
|||||||
// 转换目标
|
// 转换目标
|
||||||
targets := make([]*loadbalance.Target, len(proxyCfg.Targets))
|
targets := make([]*loadbalance.Target, len(proxyCfg.Targets))
|
||||||
for j, t := range proxyCfg.Targets {
|
for j, t := range proxyCfg.Targets {
|
||||||
targets[j] = loadbalance.NewTargetFromConfig(t.URL, t.Weight)
|
failTimeout := t.FailTimeout
|
||||||
|
if t.MaxFails > 0 && failTimeout == 0 {
|
||||||
|
failTimeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
targets[j] = loadbalance.NewTargetFromConfig(
|
||||||
|
t.URL, t.Weight,
|
||||||
|
int64(t.MaxConns), int64(t.MaxFails), failTimeout,
|
||||||
|
t.Backup, t.Down, t.ProxyURI,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 传递 Transport 配置和 Lua 引擎
|
// 传递 Transport 配置和 Lua 引擎
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user