集成Manifest缓存,增加镜像拉取速度
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user