캐싱과 함께 리버스 프록시를 개발하십시오

캐싱과 함께 리버스 프록시를 개발하십시오

리버스 프록시는 현대 웹 인프라에서 중요한 중개 계층으로 작용하며 클라이언트와 서버간에 앉아로드 밸런싱, SSL 종료 및 캐싱과 같은 추가 기능을 제공합니다. 이 기사에서는 GO의 표준 라이브러리를 사용하여 HTTP 응답 캐싱으로 리버스 프록시를 구성 할 것입니다.

기본 구조

첫 번째 단계로, 우리는 핵심 데이터 구조를 선언 할 것입니다. 우리는 필요합니다 :

  1. 응답을 저장하는 캐시
  2. 요청을 전달하기위한 프록시 서버
  3. 캐시의시기와시기를 결정하는 논리

우리의 출발점은 다음과 같습니다.

package main

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"sync"
	"time"
)

// CacheEntry represents a cached HTTP response
type CacheEntry struct {
	Response    []byte
	ContentType string
	StatusCode  int
	Timestamp   time.Time
	Expiry      time.Time
}

// Cache is a simple in-memory cache for HTTP responses
type Cache struct {
	entries map[string]CacheEntry
	mutex   sync.RWMutex
}

// NewCache creates a new cache
func NewCache() *Cache {
	return &Cache{
		entries: make(map[string]CacheEntry),
	}
}

// Get retrieves a cached response
func (c *Cache) Get(key string) (CacheEntry, bool) {
	c.mutex.RLock()
	defer c.mutex.RUnlock()
	
	entry, found := c.entries[key]
	if !found {
		return CacheEntry{}, false
	}
	
	// Check if entry has expired
	if time.Now().After(entry.Expiry) {
		return CacheEntry{}, false
	}
	
	return entry, true
}

// Set adds a response to the cache
func (c *Cache) Set(key string, entry CacheEntry) {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.entries[key] = entry
}

// ReverseProxy represents our reverse proxy with caching capabilities
type ReverseProxy struct {
	target      *url.URL
	proxy       *httputil.ReverseProxy
	cache       *Cache
	cacheTTL    time.Duration
	cacheableStatus map[int]bool
}

우리의 핵심 구조에는 다음이 포함됩니다.

  1. CacheEntry – HTTP 응답 및 메타 데이터를 보유하는 클래스
  2. Cache -스레드-안전하고 Get and Set를위한 메소드가있는 기본 인 메모리 캐시
  3. ReverseProxy – 리버스 프록시를 캐싱 기능과 연결하기위한 기본 구조

캐시 키 생성

요청을 캐시하려면 요청을 고유하게 식별 할 수 있어야합니다. 이제 요청 메소드, URL 및 관련 헤더를 기반으로 캐시 키를 생성하는 함수를 정의하겠습니다.

// generateCacheKey creates a unique key for a request
func generateCacheKey(r *http.Request) string {
	// Start with method and URL
	key := r.Method + r.URL.String()
	
	// Add relevant headers that might affect response content
	// For example, Accept-Encoding, Accept-Language
	if acceptEncoding := r.Header.Get("Accept-Encoding"); acceptEncoding != "" {
		key += "Accept-Encoding:" + acceptEncoding
	}
	
	if acceptLanguage := r.Header.Get("Accept-Language"); acceptLanguage != "" {
		key += "Accept-Language:" + acceptLanguage
	}
	
	// Create MD5 hash of the key
	hasher := md5.New()
	hasher.Write([]byte(key))
	return hex.EncodeToString(hasher.Sum(nil))
}

우리는 요청의 주요 속성의 MD5 해시를 사용하여 각 캐시 가능한 요청에 대해 컴팩트하고 고유 한 식별자를 생성합니다.

프록시 핸들러 구축

이제 리버스 프록시의 HTTP 핸들러를 구현하겠습니다.

// NewReverseProxy creates a new reverse proxy with caching
func NewReverseProxy(targetURL string, cacheTTL time.Duration) (*ReverseProxy, error) {
	url, err := url.Parse(targetURL)
	if err != nil {
		return nil, err
	}
	
	proxy := httputil.NewSingleHostReverseProxy(url)
	
	// Initialize cacheable status codes (200, 301, 302, etc.)
	cacheableStatus := map[int]bool{
		http.StatusOK:                 true,
		http.StatusMovedPermanently:   true,
		http.StatusFound:              true,
		http.StatusNotModified:        true,
	}
	
	return &ReverseProxy{
		target:          url,
		proxy:           proxy,
		cache:           NewCache(),
		cacheTTL:        cacheTTL,
		cacheableStatus: cacheableStatus,
	}, nil
}

// ServeHTTP handles HTTP requests
func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Only cache GET and HEAD requests
	if r.Method != "GET" && r.Method != "HEAD" {
		p.proxy.ServeHTTP(w, r)
		return
	}
	
	// Generate cache key
	key := generateCacheKey(r)
	
	// Check if response is in cache
	if entry, found := p.cache.Get(key); found {
		// Serve from cache
		log.Printf("Cache hit: %s %s", r.Method, r.URL.Path)
		
		// Set response headers
		w.Header().Set("Content-Type", entry.ContentType)
		w.Header().Set("X-Cache", "HIT")
		w.Header().Set("X-Cache-Age", time.Since(entry.Timestamp).String())
		
		// Set status code and write response
		w.WriteHeader(entry.StatusCode)
		w.Write(entry.Response)
		return
	}
	
	log.Printf("Cache miss: %s %s", r.Method, r.URL.Path)
	
	// Create a custom response writer to capture the response
	responseBuffer := &bytes.Buffer{}
	responseWriter := &ResponseCapturer{
		ResponseWriter: w,
		Buffer:         responseBuffer,
	}
	
	// Serve the request with our capturing writer
	p.proxy.ServeHTTP(responseWriter, r)
	
	// If status is cacheable, store in cache
	if p.cacheableStatus[responseWriter.StatusCode] {
		p.cache.Set(key, CacheEntry{
			Response:    responseBuffer.Bytes(),
			ContentType: responseWriter.Header().Get("Content-Type"),
			StatusCode:  responseWriter.StatusCode,
			Timestamp:   time.Now(),
			Expiry:      time.Now().Add(p.cacheTTL),
		})
		
		// Set cache header
		w.Header().Set("X-Cache", "MISS")
	}
}

// ResponseCapturer captures response data for caching
type ResponseCapturer struct {
	http.ResponseWriter
	Buffer     *bytes.Buffer
	StatusCode int
}

// WriteHeader captures status code before writing it
func (r *ResponseCapturer) WriteHeader(statusCode int) {
	r.StatusCode = statusCode
	r.ResponseWriter.WriteHeader(statusCode)
}

// Write captures response data before writing it
func (r *ResponseCapturer) Write(b []byte) (int, error) {
	// Write to both the original writer and our buffer
	r.Buffer.Write(b)
	return r.ResponseWriter.Write(b)
}

다음은 다음과 같습니다.

  1. 특정 대상 URL을 지시하는 새로운 리버스 프록시를 만듭니다.
  2. 그만큼 ServeHTTP 메소드는 들어오는 요청을 처리합니다.
    • 비 게이트/헤드 요청의 경우 단순히 전달됩니다.
    • Get/Head 요청의 경우 먼저 캐시를 확인합니다.
    • 응답이 캐시되면 캐시에서 직접 제공됩니다.
    • 그렇지 않은 경우 요청을 전달하고 응답을 캡처합니다.
    • 응답이 캐시 가능하면 캐시에 저장됩니다.
  3. 그만큼 ResponseCapturer 관습입니다 http.ResponseWriter 응답을 통과하면서 응답을 기록합니다.

모든 것을 함께 모으십시오

마지막으로, 구현합시다 main 프록시를 시작하는 기능 :

func main() {
	// Parse command line flags
	port := flag.Int("port", 8080, "Port to serve on")
	target := flag.String("target", " "Target URL to proxy")
	cacheTTL := flag.Duration("cache-ttl", 5*time.Minute, "Cache TTL (e.g., 5m, 1h)")
	flag.Parse()
	
	// Create the reverse proxy
	proxy, err := NewReverseProxy(*target, *cacheTTL)
	if err != nil {
		log.Fatal(err)
	}
	
	// Start server
	server := http.Server{
		Addr:    fmt.Sprintf(":%d", *port),
		Handler: proxy,
	}
	
	log.Printf("Reverse proxy started at :%d -> %s", *port, *target)
	log.Printf("Cache TTL: %s", *cacheTTL)
	
	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

우리의 주요 기능에서 우리는 다음과 같습니다.

  1. 구문 분석 명령 줄 플래그 포트, 대상 URL 및 캐시 TTL을 구성합니다.
  2. 캐싱으로 새로운 리버스 프록시를 만듭니다.
  3. 프록시를 핸들러로 사용하여 HTTP 서버를 시작하십시오.

캐시 관리 및 향상

우리의 현재 구현은 좋은 출발이지만 생산 준비 프록시에는 더 많은 기능이 필요합니다. 캐시 관리 및 몇 가지 개선 사항을 추가합시다.

// Add to ReverseProxy struct
type ReverseProxy struct {
	// ... existing fields
	maxCacheSize int64
	currentCacheSize int64
}

// Add to NewReverseProxy function
func NewReverseProxy(targetURL string, cacheTTL time.Duration, maxCacheSize int64) (*ReverseProxy, error) {
	// ... existing code
	return &ReverseProxy{
		// ... existing fields
		maxCacheSize: maxCacheSize,
		currentCacheSize: 0,
	}, nil
}

// Modify the Set method in Cache
func (c *Cache) Set(key string, entry CacheEntry, proxy *ReverseProxy) bool {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	
	// Check if we would exceed the max cache size
	newSize := proxy.currentCacheSize + int64(len(entry.Response))
	if proxy.maxCacheSize > 0 && newSize > proxy.maxCacheSize {
		// Simple eviction policy: remove oldest entries
		var keysToRemove []string
		var sizeToFree int64
		
		// Calculate how much we need to free up
		sizeToFree = newSize - proxy.maxCacheSize + 1024*1024 // Free an extra MB for headroom
		
		// Find oldest entries to remove
		var entries []struct {
			key string
			timestamp time.Time
			size int64
		}
		
		for k, v := range c.entries {
			entries = append(entries, struct {
				key string
				timestamp time.Time
				size int64
			}{k, v.Timestamp, int64(len(v.Response))})
		}
		
		// Sort by timestamp (oldest first)
		sort.Slice(entries, func(i, j int) bool {
			return entries[i].timestamp.Before(entries[j].timestamp)
		})
		
		// Remove oldest entries until we have enough space
		var freedSize int64
		for _, entry := range entries {
			if freedSize >= sizeToFree {
				break
			}
			keysToRemove = append(keysToRemove, entry.key)
			freedSize += entry.size
		}
		
		// Remove entries
		for _, k := range keysToRemove {
			oldSize := int64(len(c.entries[k].Response))
			delete(c.entries, k)
			proxy.currentCacheSize -= oldSize
		}
		
		log.Printf("Cache eviction: removed %d entries, freed %d bytes", len(keysToRemove), freedSize)
	}
	
	// Add the new entry
	c.entries[key] = entry
	proxy.currentCacheSize += int64(len(entry.Response))
	
	return true
}

// Add a method to the Cache for cleaning expired entries
func (c *Cache) CleanExpired(proxy *ReverseProxy) {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	
	now := time.Now()
	var freedSize int64
	count := 0
	
	for k, v := range c.entries {
		if now.After(v.Expiry) {
			freedSize += int64(len(v.Response))
			delete(c.entries, k)
			count++
		}
	}
	
	proxy.currentCacheSize -= freedSize
	
	if count > 0 {
		log.Printf("Cache cleanup: removed %d expired entries, freed %d bytes", count, freedSize)
	}
}

// Add a periodic cleanup in main
func main() {
	// ... existing code
	
	// Start a goroutine for periodic cache cleanup
	go func() {
		ticker := time.NewTicker(1 * time.Minute)
		for {
			select {
			case 

이러한 개선 사항은 다음을 추가합니다.

  1. 최대 캐시 크기 제한
  2. 간단한 캐시 퇴거 정책 (먼저 가장 오래된 항목 제거)
  3. 만료 된 캐시 항목의 정기적 인 정리

프록시 테스트

다음 명령으로 프록시를 테스트 할 수 있습니다.

# Start the proxy targeting a public website
go build -o caching-proxy main.go
./caching-proxy -target  -port 8080 -cache-ttl 1m

# Make requests to the proxy
curl -v 

# Make the same request again to see cache headers
curl -v 

이것은 좋은 출발점이지만 프로덕션 준비 프록시에는 다음과 같은 추가 기능이 필요합니다.

  1. HTTP 헤더를 기반으로 한보다 정교한 캐시 제어 (캐시 제어, ETAG 등)
  2. 메모리 사용 모니터링
  3. TLS 지원
  4. 보다 고급 캐시 퇴거 전략
  5. 동일한 동일한 요청에 대한 Coalescing 요청
  6. 지속적인 캐시 저장소

GO의 표준 라이브러리는 이러한 도구를 비교적 간단하게 만드는 강력한 네트워킹 기능을 제공합니다. 그만큼 net/http 특히 패키지는 프록시,로드 밸런서 및 API 게이트웨이와 같은 HTTP 기반 네트워크 응용 프로그램을위한 훌륭한 기반을 제공합니다.

소스 코드

여기에서 소스 코드를 찾을 수 있습니다.

출처 참조

Post Comment

당신은 놓쳤을 수도 있습니다