캐싱과 함께 리버스 프록시를 개발하십시오
리버스 프록시는 현대 웹 인프라에서 중요한 중개 계층으로 작용하며 클라이언트와 서버간에 앉아로드 밸런싱, SSL 종료 및 캐싱과 같은 추가 기능을 제공합니다. 이 기사에서는 GO의 표준 라이브러리를 사용하여 HTTP 응답 캐싱으로 리버스 프록시를 구성 할 것입니다.
기본 구조
첫 번째 단계로, 우리는 핵심 데이터 구조를 선언 할 것입니다. 우리는 필요합니다 :
- 응답을 저장하는 캐시
- 요청을 전달하기위한 프록시 서버
- 캐시의시기와시기를 결정하는 논리
우리의 출발점은 다음과 같습니다.
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
}
우리의 핵심 구조에는 다음이 포함됩니다.
CacheEntry
– HTTP 응답 및 메타 데이터를 보유하는 클래스Cache
-스레드-안전하고 Get and Set를위한 메소드가있는 기본 인 메모리 캐시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)
}
다음은 다음과 같습니다.
- 특정 대상 URL을 지시하는 새로운 리버스 프록시를 만듭니다.
- 그만큼
ServeHTTP
메소드는 들어오는 요청을 처리합니다.- 비 게이트/헤드 요청의 경우 단순히 전달됩니다.
- Get/Head 요청의 경우 먼저 캐시를 확인합니다.
- 응답이 캐시되면 캐시에서 직접 제공됩니다.
- 그렇지 않은 경우 요청을 전달하고 응답을 캡처합니다.
- 응답이 캐시 가능하면 캐시에 저장됩니다.
- 그만큼
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)
}
}
우리의 주요 기능에서 우리는 다음과 같습니다.
- 구문 분석 명령 줄 플래그 포트, 대상 URL 및 캐시 TTL을 구성합니다.
- 캐싱으로 새로운 리버스 프록시를 만듭니다.
- 프록시를 핸들러로 사용하여 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
이러한 개선 사항은 다음을 추가합니다.
- 최대 캐시 크기 제한
- 간단한 캐시 퇴거 정책 (먼저 가장 오래된 항목 제거)
- 만료 된 캐시 항목의 정기적 인 정리
프록시 테스트
다음 명령으로 프록시를 테스트 할 수 있습니다.
# 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
이것은 좋은 출발점이지만 프로덕션 준비 프록시에는 다음과 같은 추가 기능이 필요합니다.
- HTTP 헤더를 기반으로 한보다 정교한 캐시 제어 (캐시 제어, ETAG 등)
- 메모리 사용 모니터링
- TLS 지원
- 보다 고급 캐시 퇴거 전략
- 동일한 동일한 요청에 대한 Coalescing 요청
- 지속적인 캐시 저장소
GO의 표준 라이브러리는 이러한 도구를 비교적 간단하게 만드는 강력한 네트워킹 기능을 제공합니다. 그만큼 net/http
특히 패키지는 프록시,로드 밸런서 및 API 게이트웨이와 같은 HTTP 기반 네트워크 응용 프로그램을위한 훌륭한 기반을 제공합니다.
소스 코드
여기에서 소스 코드를 찾을 수 있습니다.
Post Comment