Files
seaweedfs/weed/storage/backend/s3_backend/s3_sessions.go
T
Javier de la TorreandJavier de la Torre 21add33b6e s3 backend: optional HTTP client settings for the cloud tier (#11897)
* s3 backend: optional HTTP client settings for the cloud tier

The S3 tier backend builds its client from a bare aws.Config, so it always
uses Go's default transport: HTTP/2 wherever the endpoint offers it over TLS,
no cap on connections per host, and a 90s idle timeout. Against some
S3-compatible services that is a poor fit. Over HTTP/2 every upload and
range read of a tiered volume shares one connection and its flow-control
window; with a small window, tier uploads and concurrent reads of tiered
volumes become throughput-bound on that one connection, while HTTP/1.1 opens
a connection per concurrent request. Some services also limit parallel
connections per client address, or drop idle connections before 90s.

Add three optional settings to [storage.backend.s3.<id>], all off by
default so existing deployments keep the SDK's default client:
- disable_http2: keep the client on HTTP/1.1 (ForceAttemptHTTP2=false and an
  empty TLSNextProto, the documented opt-out);
- max_connections: MaxConnsPerHost, 0 = no cap;
- idle_connection_timeout_seconds: IdleConnTimeout, 0 = Go's default.

The session cache key includes the options when any is set, so two backends
on the same endpoint with different settings do not share a client.
ToProperties reports the settings; the master.toml scaffold documents them.

Tests: TestS3BackendHTTPOptionsReachTheClient fails without the change (the
settings never reach the client); TestS3BackendDefaultHTTPClientUnchanged,
TestS3BackendSessionsDifferByHTTPOptions, TestParseNonNegative.

* s3 backend: reject an idle timeout too large for time.Duration

An idle_connection_timeout_seconds above math.MaxInt64/1e9 overflowed when
multiplied by time.Second. The negative result made the transport keep its
default timeout and ToProperties export a negative value. Warn and use the
default instead.

* s3 backend: parse the idle timeout as 64-bit seconds

parseIdleConnTimeout went through strconv.Atoi, so on 32-bit platforms
any value above 2147483647 seconds was rejected while the same value was
accepted on 64-bit, and the overflow test failed under GOARCH=386. Parse
with ParseInt(…, 64) and export the effective value with FormatInt so the
accepted range is the same on every platform.

---------

Co-authored-by: Javier de la Torre <jatorre@lakehousebox.com>
2026-10-21 14:44:39 -07:00

107 lines
3.1 KiB
Go

package s3_backend
import (
"crypto/tls"
"fmt"
"net/http"
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3iface"
"github.com/seaweedfs/seaweedfs/weed/util/version"
)
var (
s3Sessions = make(map[string]s3iface.S3API)
sessionsLock sync.Mutex
)
// httpOptions tunes the HTTP transport of the tier backend's S3 client. The
// zero value keeps the SDK's default client.
type httpOptions struct {
// disableHTTP2 keeps the client on HTTP/1.1. Over HTTP/2 every upload and
// range read to the endpoint shares one connection and its flow-control
// window, which some S3-compatible services keep small.
disableHTTP2 bool
// maxConnsPerHost caps the connections to the endpoint (0 = no cap), for
// services that limit parallel connections per client address.
maxConnsPerHost int
// idleConnTimeout closes idle connections sooner than Go's default 90s,
// for services that drop idle connections earlier (0 = default).
idleConnTimeout time.Duration
}
// newTransport returns the transport for the options, or nil for the default.
func (o httpOptions) newTransport() *http.Transport {
if o == (httpOptions{}) {
return nil
}
t := http.DefaultTransport.(*http.Transport).Clone()
if o.disableHTTP2 {
t.ForceAttemptHTTP2 = false
t.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
}
if o.maxConnsPerHost > 0 {
t.MaxConnsPerHost = o.maxConnsPerHost
}
if o.idleConnTimeout > 0 {
t.IdleConnTimeout = o.idleConnTimeout
}
return t
}
func createSession(awsAccessKeyId, awsSecretAccessKey, region, endpoint string, forcePathStyle bool, httpOpts httpOptions) (s3iface.S3API, error) {
sessionsLock.Lock()
defer sessionsLock.Unlock()
cacheKey := fmt.Sprintf("%s|%s", region, endpoint)
if httpOpts != (httpOptions{}) {
cacheKey = fmt.Sprintf("%s|%+v", cacheKey, httpOpts)
}
if t, found := s3Sessions[cacheKey]; found {
return t, nil
}
config := &aws.Config{
Region: aws.String(region),
Endpoint: aws.String(endpoint),
S3ForcePathStyle: aws.Bool(forcePathStyle),
S3DisableContentMD5Validation: aws.Bool(true),
}
if t := httpOpts.newTransport(); t != nil {
config.HTTPClient = &http.Client{Transport: t}
}
if awsAccessKeyId != "" && awsSecretAccessKey != "" {
config.Credentials = credentials.NewStaticCredentials(awsAccessKeyId, awsSecretAccessKey, "")
}
sess, err := session.NewSession(config)
if err != nil {
return nil, fmt.Errorf("create aws session in region %s: %v", region, err)
}
sess.Handlers.Build.PushBack(func(r *request.Request) {
r.HTTPRequest.Header.Set("User-Agent", "SeaweedFS/"+version.VERSION_NUMBER)
})
t := s3.New(sess)
s3Sessions[cacheKey] = t
return t, nil
}
func deleteFromS3(sess s3iface.S3API, sourceBucket string, sourceKey string) (err error) {
_, err = sess.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(sourceBucket),
Key: aws.String(sourceKey),
})
return err
}