Nginx后置的Golang对象存储代理实现

Nginx后置的Golang对象存储代理实现 我正在开发一个私人图片托管网站。为了支持扩展性,我切换到了兼容S3的对象存储服务。存储桶是私有的。主Web应用使用Laravel构建。我打算用以下代码代理图片请求。这个Go应用将部署在nginx后面。

如果有人能帮忙查看下面的代码,并告诉我是否有任何提升性能的方法,我将不胜感激。如果您有任何建议,我也非常乐意听取。谢谢!

package goos

import (
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)

// Logger interface
type Logger interface {
	Print(string)
}

// Goos holds the information to connect to an S3 compatible Object Storage Service
type Goos struct {
	KeyID    string
	Secret   string
	Endpoint string
	Region   string
	Bucket   string
	Logger
}

func (g *Goos) session() *session.Session {
	s3Config := &aws.Config{
		Credentials: credentials.NewStaticCredentials(g.KeyID, g.Secret, ""),
		Endpoint:    aws.String(g.Endpoint),
		Region:      aws.String(g.Region),
	}
	sess := session.New(s3Config)

	return sess
}

// Handler will return the handler we need
func (g *Goos) Handler() http.HandlerFunc {
	svc := s3.New(g.session())
	fn := func(w http.ResponseWriter, r *http.Request) {

		cf := r.Header.Get("X-Real-Ip")
		ip := r.Header.Get("X-Forwarded-For")

		if r.URL.String() == "/" {
			g.logMessage(cf, ip, r.URL.String(), "/")
			notFound(w)
			return
		}

		url, err := url.QueryUnescape(r.URL.String())
		if err != nil {
			g.logMessage(cf, ip, r.URL.String(), "404")
			notFound(w)
			return
		}

		input := &s3.GetObjectInput{
			Bucket: aws.String(g.Bucket),
			Key:    aws.String(url),
		}
		result, err := svc.GetObject(input)
		if err != nil {
			g.logMessage(cf, ip, url, "404")
			notFound(w)
			return
		}
		defer result.Body.Close()

		w.Header().Set("Content-Length", strconv.FormatInt(*result.ContentLength, 10))
		w.Header().Set("Last-Modified", result.LastModified.Format("Mon, 02 Jan 2006 15:04:05 MST"))
		w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat))
		w.Header().Set("Cache-Control", "max-age:290304000")
		w.Header().Set("Etag", *result.ETag)

		_, err = io.Copy(w, result.Body)
		if err != nil {
			g.logMessage(cf, ip, url, "500")
			notFound(w)
			return
		}

		g.logMessage(cf, ip, url, "200")
	}

	return http.HandlerFunc(fn)
}

func notFound(w http.ResponseWriter) {
	w.Header().Set("Cache-Control", "max-age:0, private")
	w.WriteHeader(http.StatusNotFound)
	w.Write([]byte("Not Found."))
}

func (g *Goos) logMessage(cf string, ip string, url string, status string) {
	if g.Logger == nil {
		return
	}
	g.Print("[" + cf + "|" + ip + "] " + "[" + url + "] " + "[" + status + "]")
}

更多关于Nginx后置的Golang对象存储代理实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Nginx后置的Golang对象存储代理实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


以下是针对您提供的Go代码的性能优化建议和实现改进。代码本身已经实现了基本功能,但可以通过几个关键点来提升性能和可靠性。

1. 会话和S3客户端复用

当前代码在每次请求时都会创建新的session和S3客户端,这会带来不必要的开销。建议在初始化时创建并复用这些对象:

type Goos struct {
    KeyID    string
    Secret   string
    Endpoint string
    Region   string
    Bucket   string
    Logger
    svc *s3.S3
}

func NewGoos(keyID, secret, endpoint, region, bucket string, logger Logger) *Goos {
    g := &Goos{
        KeyID:    keyID,
        Secret:   secret,
        Endpoint: endpoint,
        Region:   region,
        Bucket:   bucket,
        Logger:   logger,
    }
    
    s3Config := &aws.Config{
        Credentials: credentials.NewStaticCredentials(keyID, secret, ""),
        Endpoint:    aws.String(endpoint),
        Region:      aws.String(region),
    }
    sess := session.Must(session.NewSession(s3Config))
    g.svc = s3.New(sess)
    
    return g
}

// Handler will return the handler we need
func (g *Goos) Handler() http.HandlerFunc {
    fn := func(w http.ResponseWriter, r *http.Request) {
        // 使用预初始化的g.svc
        input := &s3.GetObjectInput{
            Bucket: aws.String(g.Bucket),
            Key:    aws.String(r.URL.Path),
        }
        result, err := g.svc.GetObject(input)
        // ... 其余代码保持不变
    }
    
    return http.HandlerFunc(fn)
}

2. 改进URL处理和错误处理

当前代码使用r.URL.String()获取路径,但这会包含查询参数。建议使用r.URL.Path并添加路径清理:

fn := func(w http.ResponseWriter, r *http.Request) {
    cf := r.Header.Get("X-Real-Ip")
    ip := r.Header.Get("X-Forwarded-For")
    
    // 清理路径,移除开头的斜杠和进行URL解码
    path := strings.TrimPrefix(r.URL.Path, "/")
    if path == "" {
        g.logMessage(cf, ip, "/", "404")
        notFound(w)
        return
    }
    
    // URL解码
    decodedPath, err := url.PathUnescape(path)
    if err != nil {
        g.logMessage(cf, ip, path, "400")
        http.Error(w, "Bad Request", http.StatusBadRequest)
        return
    }
    
    input := &s3.GetObjectInput{
        Bucket: aws.String(g.Bucket),
        Key:    aws.String(decodedPath),
    }
    
    result, err := g.svc.GetObject(input)
    if err != nil {
        g.logMessage(cf, ip, decodedPath, "404")
        notFound(w)
        return
    }
    defer result.Body.Close()
    
    // 设置正确的缓存控制头
    w.Header().Set("Content-Length", strconv.FormatInt(*result.ContentLength, 10))
    w.Header().Set("Content-Type", aws.StringValue(result.ContentType))
    w.Header().Set("Last-Modified", result.LastModified.Format(time.RFC1123))
    w.Header().Set("Cache-Control", "public, max-age=290304000, immutable")
    w.Header().Set("ETag", aws.StringValue(result.ETag))
    
    // 处理条件请求(If-None-Match)
    if match := r.Header.Get("If-None-Match"); match != "" {
        if match == aws.StringValue(result.ETag) {
            w.WriteHeader(http.StatusNotModified)
            g.logMessage(cf, ip, decodedPath, "304")
            return
        }
    }
    
    _, err = io.Copy(w, result.Body)
    if err != nil {
        g.logMessage(cf, ip, decodedPath, "500")
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    
    g.logMessage(cf, ip, decodedPath, "200")
}

3. 添加连接池和超时配置

在初始化AWS会话时配置HTTP客户端以优化连接管理:

func NewGoos(keyID, secret, endpoint, region, bucket string, logger Logger) *Goos {
    g := &Goos{
        KeyID:    keyID,
        Secret:   secret,
        Endpoint: endpoint,
        Region:   region,
        Bucket:   bucket,
        Logger:   logger,
    }
    
    httpClient := &http.Client{
        Timeout: 30 * time.Second,
        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 100,
            IdleConnTimeout:     90 * time.Second,
        },
    }
    
    s3Config := &aws.Config{
        Credentials:      credentials.NewStaticCredentials(keyID, secret, ""),
        Endpoint:         aws.String(endpoint),
        Region:           aws.String(region),
        HTTPClient:       httpClient,
        S3ForcePathStyle: aws.Bool(true),
    }
    
    sess := session.Must(session.NewSession(s3Config))
    g.svc = s3.New(sess)
    
    return g
}

4. 改进内容类型处理

确保正确设置Content-Type头:

// 在成功获取对象后
contentType := "application/octet-stream"
if result.ContentType != nil {
    contentType = *result.ContentType
}
w.Header().Set("Content-Type", contentType)

这些优化将显著提升代理的性能和可靠性,特别是在高并发场景下。主要改进包括连接复用、更好的错误处理、条件请求支持和优化的HTTP客户端配置。

回到顶部