From 99389f26bb8fb1c6323636dd10325a30792ca346 Mon Sep 17 00:00:00 2001 From: user123456 Date: Wed, 11 Jun 2025 14:49:29 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90Manifest=E7=BC=93=E5=AD=98?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E9=95=9C=E5=83=8F=E6=8B=89=E5=8F=96?= =?UTF-8?q?=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.toml | 11 ++-- src/docker.go | 66 +++++++++++++++++++++--- src/token_cache.go | 123 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 167 insertions(+), 33 deletions(-) diff --git a/src/config.toml b/src/config.toml index dae8a22..53e3e56 100644 --- a/src/config.toml +++ b/src/config.toml @@ -82,9 +82,14 @@ enabled = true # authType = "basic" # enabled = false -# docker的匿名Token缓存配置,显著提升性能 +# 缓存配置:Token和Manifest统一管理,显著提升性能 [tokenCache] -# 是否启用token缓存 +# 是否启用缓存(同时控制Token和Manifest缓存) enabled = true -# 默认缓存时间(自动从响应中提取实际TTL) +# 默认缓存时间(适用于mutable标签,如latest等) defaultTTL = "20m" +# 说明: +# - immutable digest缓存24小时 +# - latest/main等热门标签缓存5分钟 +# - 普通tag使用defaultTTL +# - Token自动从响应中提取TTL diff --git a/src/docker.go b/src/docker.go index 003784f..9c4192a 100644 --- a/src/docker.go +++ b/src/docker.go @@ -181,6 +181,17 @@ func parseRegistryPath(path string) (imageName, apiType, reference string) { // handleManifestRequest 处理manifest请求 func handleManifestRequest(c *gin.Context, imageRef, reference string) { + // Manifest缓存逻辑(仅对GET请求缓存) + if isCacheEnabled() && c.Request.Method == http.MethodGet { + cacheKey := buildManifestCacheKey(imageRef, reference) + + // 优先从缓存获取 + if cachedItem := globalCache.Get(cacheKey); cachedItem != nil { + writeCachedResponse(c, cachedItem) + return + } + } + var ref name.Reference var err error @@ -223,10 +234,24 @@ func handleManifestRequest(c *gin.Context, imageRef, reference string) { return } + // 设置响应头 + headers := map[string]string{ + "Docker-Content-Digest": desc.Digest.String(), + "Content-Length": fmt.Sprintf("%d", len(desc.Manifest)), + } + + // 缓存响应 + if isCacheEnabled() { + cacheKey := buildManifestCacheKey(imageRef, reference) + ttl := getManifestTTL(reference) + globalCache.Set(cacheKey, desc.Manifest, string(desc.MediaType), headers, ttl) + } + // 设置响应头 c.Header("Content-Type", string(desc.MediaType)) - c.Header("Docker-Content-Digest", desc.Digest.String()) - c.Header("Content-Length", fmt.Sprintf("%d", len(desc.Manifest))) + for key, value := range headers { + c.Header(key, value) + } // 返回manifest内容 c.Data(http.StatusOK, string(desc.MediaType), desc.Manifest) @@ -318,10 +343,10 @@ func ProxyDockerAuthGin(c *gin.Context) { // proxyDockerAuthWithCache 带缓存的认证代理 func proxyDockerAuthWithCache(c *gin.Context) { // 1. 构建缓存key(基于完整的查询参数) - cacheKey := buildCacheKey(c.Request.URL.RawQuery) + cacheKey := buildTokenCacheKey(c.Request.URL.RawQuery) // 2. 尝试从缓存获取token - if cachedToken := globalTokenCache.Get(cacheKey); cachedToken != "" { + if cachedToken := globalCache.GetToken(cacheKey); cachedToken != "" { writeTokenResponse(c, cachedToken) return } @@ -339,7 +364,7 @@ func proxyDockerAuthWithCache(c *gin.Context) { // 5. 如果认证成功,缓存响应 if recorder.statusCode == 200 && len(recorder.body) > 0 { ttl := extractTTLFromResponse(recorder.body) - globalTokenCache.Set(cacheKey, string(recorder.body), ttl) + globalCache.SetToken(cacheKey, string(recorder.body), ttl) } // 6. 写入实际响应(如果还没写入) @@ -501,6 +526,17 @@ func handleMultiRegistryRequest(c *gin.Context, registryDomain, remainingPath st // handleUpstreamManifestRequest 处理上游Registry的manifest请求 func handleUpstreamManifestRequest(c *gin.Context, imageRef, reference string, mapping RegistryMapping) { + // Manifest缓存逻辑(仅对GET请求缓存) + if isCacheEnabled() && c.Request.Method == http.MethodGet { + cacheKey := buildManifestCacheKey(imageRef, reference) + + // 优先从缓存获取 + if cachedItem := globalCache.Get(cacheKey); cachedItem != nil { + writeCachedResponse(c, cachedItem) + return + } + } + var ref name.Reference var err error @@ -541,9 +577,25 @@ func handleUpstreamManifestRequest(c *gin.Context, imageRef, reference string, m return } + // 设置响应头 + headers := map[string]string{ + "Docker-Content-Digest": desc.Digest.String(), + "Content-Length": fmt.Sprintf("%d", len(desc.Manifest)), + } + + // 缓存响应 + if isCacheEnabled() { + cacheKey := buildManifestCacheKey(imageRef, reference) + ttl := getManifestTTL(reference) + globalCache.Set(cacheKey, desc.Manifest, string(desc.MediaType), headers, ttl) + } + + // 设置响应头 c.Header("Content-Type", string(desc.MediaType)) - c.Header("Docker-Content-Digest", desc.Digest.String()) - c.Header("Content-Length", fmt.Sprintf("%d", len(desc.Manifest))) + for key, value := range headers { + c.Header(key, value) + } + c.Data(http.StatusOK, string(desc.MediaType), desc.Manifest) } } diff --git a/src/token_cache.go b/src/token_cache.go index 6041163..a30fb58 100644 --- a/src/token_cache.go +++ b/src/token_cache.go @@ -4,49 +4,105 @@ import ( "crypto/md5" "encoding/json" "fmt" + "strings" "sync" "time" "github.com/gin-gonic/gin" ) -// CachedToken 缓存的Token信息 -type CachedToken struct { - Token string - ExpiresAt time.Time +// CachedItem 通用缓存项,支持Token和Manifest +type CachedItem struct { + Data []byte // 缓存数据(token字符串或manifest字节) + ContentType string // 内容类型 + Headers map[string]string // 额外的响应头 + ExpiresAt time.Time // 过期时间 } -// SimpleTokenCache 极简Token缓存 -type SimpleTokenCache struct { +// UniversalCache 通用缓存,支持Token和Manifest +type UniversalCache struct { cache sync.Map // 线程安全的并发映射 } -var globalTokenCache = &SimpleTokenCache{} +var globalCache = &UniversalCache{} -// Get 获取缓存的token,如果不存在或过期返回空字符串 -func (c *SimpleTokenCache) Get(key string) string { +// Get 获取缓存项,如果不存在或过期返回nil +func (c *UniversalCache) Get(key string) *CachedItem { if v, ok := c.cache.Load(key); ok { - if cached := v.(*CachedToken); time.Now().Before(cached.ExpiresAt) { - return cached.Token + if cached := v.(*CachedItem); time.Now().Before(cached.ExpiresAt) { + return cached } - // 自动清理过期token,保持内存整洁 + // 自动清理过期项,保持内存整洁 c.cache.Delete(key) } + return nil +} + +// Set 设置缓存项 +func (c *UniversalCache) Set(key string, data []byte, contentType string, headers map[string]string, ttl time.Duration) { + c.cache.Store(key, &CachedItem{ + Data: data, + ContentType: contentType, + Headers: headers, + ExpiresAt: time.Now().Add(ttl), + }) +} + +// GetToken 获取缓存的token(向后兼容) +func (c *UniversalCache) GetToken(key string) string { + if item := c.Get(key); item != nil { + return string(item.Data) + } return "" } -// Set 设置token缓存,自动计算过期时间 -func (c *SimpleTokenCache) Set(key, token string, ttl time.Duration) { - c.cache.Store(key, &CachedToken{ - Token: token, - ExpiresAt: time.Now().Add(ttl), - }) +// SetToken 设置token缓存(向后兼容) +func (c *UniversalCache) SetToken(key, token string, ttl time.Duration) { + c.Set(key, []byte(token), "application/json", nil, ttl) } // buildCacheKey 构建稳定的缓存key -func buildCacheKey(query string) string { +func buildCacheKey(prefix, query string) string { // 使用MD5确保key的一致性和简洁性 - return fmt.Sprintf("token:%x", md5.Sum([]byte(query))) + return fmt.Sprintf("%s:%x", prefix, md5.Sum([]byte(query))) +} + +// buildTokenCacheKey 构建token缓存key(向后兼容) +func buildTokenCacheKey(query string) string { + return buildCacheKey("token", query) +} + +// buildManifestCacheKey 构建manifest缓存key +func buildManifestCacheKey(imageRef, reference string) string { + key := fmt.Sprintf("%s:%s", imageRef, reference) + return buildCacheKey("manifest", key) +} + +// getManifestTTL 根据引用类型智能确定TTL +func getManifestTTL(reference string) time.Duration { + cfg := GetConfig() + defaultTTL := 30 * time.Minute + if cfg.TokenCache.DefaultTTL != "" { + if parsed, err := time.ParseDuration(cfg.TokenCache.DefaultTTL); err == nil { + defaultTTL = parsed + } + } + + // 智能TTL策略 + if strings.HasPrefix(reference, "sha256:") { + // immutable digest: 长期缓存 + return 24 * time.Hour + } + + // mutable tag的智能判断 + if reference == "latest" || reference == "main" || reference == "master" || + reference == "dev" || reference == "develop" { + // 热门可变标签: 短期缓存 + return 10 * time.Minute + } + + // 普通tag: 中等缓存时间 + return defaultTTL } // extractTTLFromResponse 从响应中智能提取TTL @@ -69,15 +125,36 @@ func extractTTLFromResponse(responseBody []byte) time.Duration { return defaultTTL } -// writeTokenResponse 写入token响应 +// writeTokenResponse 写入token响应(向后兼容) func writeTokenResponse(c *gin.Context, cachedBody string) { // 直接返回缓存的完整响应体,保持格式一致性 c.Header("Content-Type", "application/json") c.String(200, cachedBody) } -// isTokenCacheEnabled 检查token缓存是否启用 -func isTokenCacheEnabled() bool { +// writeCachedResponse 写入缓存响应 +func writeCachedResponse(c *gin.Context, item *CachedItem) { + // 设置内容类型 + if item.ContentType != "" { + c.Header("Content-Type", item.ContentType) + } + + // 设置额外的响应头 + for key, value := range item.Headers { + c.Header(key, value) + } + + // 返回数据 + c.Data(200, item.ContentType, item.Data) +} + +// isCacheEnabled 检查缓存是否启用 +func isCacheEnabled() bool { cfg := GetConfig() return cfg.TokenCache.Enabled +} + +// isTokenCacheEnabled 检查token缓存是否启用(向后兼容) +func isTokenCacheEnabled() bool { + return isCacheEnabled() } \ No newline at end of file