DEV: Introduce S3 transfer acceleration for uploads behind hidden setting (#24238)

This commit adds an `enable_s3_transfer_acceleration` site setting,
which is hidden to begin with. We are adding this because in certain
regions, using https://aws.amazon.com/s3/transfer-acceleration/ can
drastically speed up uploads, sometimes as much as 70% in certain
regions depending on the target bucket region. This is important for
us because we have direct S3 multipart uploads enabled everywhere
on our hosting.

To start, we only want this on the uploads bucket, not the backup one.
Also, this will accelerate both uploads **and** downloads, depending
on whether a presigned URL is used for downloading. This is the case
when secure uploads is enabled, not anywhere else at this time. To
enable the S3 acceleration on downloads more generally would be a
more in-depth change, since we currently store S3 Upload record URLs
like this:

```
 url: "//test.s3.dualstack.us-east-2.amazonaws.com/original/2X/6/123456.png"
```

For acceleration, `s3.dualstack` would need to be changed to `s3-accelerate.dualstack`
here.

Note that for this to have any effect, Transfer Acceleration must be enabled
on the S3 bucket used for uploads per https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration-examples.html.
This commit is contained in:
Martin Brennan 2023-11-07 11:50:40 +10:00 committed by GitHub
parent 6bf66ccd1a
commit fe05fdae24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 5 deletions

View File

@ -225,6 +225,7 @@ s3_cdn_url =
s3_endpoint = s3_endpoint =
s3_http_continue_timeout = s3_http_continue_timeout =
s3_install_cors_rule = s3_install_cors_rule =
s3_enable_transfer_acceleration =
# Optionally, specify a separate CDN to be used for static JS assets stored on S3 # Optionally, specify a separate CDN to be used for static JS assets stored on S3
s3_asset_cdn_url = s3_asset_cdn_url =

View File

@ -1462,6 +1462,9 @@ files:
enable_s3_uploads: enable_s3_uploads:
default: false default: false
client: true client: true
enable_s3_transfer_acceleration:
default: false
hidden: true
s3_use_iam_profile: s3_use_iam_profile:
default: false default: false
s3_access_key_id: s3_access_key_id:

View File

@ -25,6 +25,7 @@ module FileStore
S3Helper.new( S3Helper.new(
s3_bucket, s3_bucket,
Rails.configuration.multisite ? multisite_tombstone_prefix : TOMBSTONE_PREFIX, Rails.configuration.multisite ? multisite_tombstone_prefix : TOMBSTONE_PREFIX,
use_accelerate_endpoint: SiteSetting.enable_s3_transfer_acceleration,
) )
end end

View File

@ -47,6 +47,15 @@ class S3Helper
setting_klass = use_db_s3_config ? SiteSetting : GlobalSetting setting_klass = use_db_s3_config ? SiteSetting : GlobalSetting
options = S3Helper.s3_options(setting_klass) options = S3Helper.s3_options(setting_klass)
options[:client] = s3_client if s3_client.present? options[:client] = s3_client if s3_client.present?
use_accelerate_endpoint =
(
if use_db_s3_config
SiteSetting.enable_s3_transfer_acceleration
else
GlobalSetting.s3_enable_transfer_acceleration
end
)
options[:use_accelerate_endpoint] = !for_backup && use_accelerate_endpoint
bucket = bucket =
if for_backup if for_backup
@ -349,7 +358,12 @@ class S3Helper
def presigned_url(key, method:, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, opts: {}) def presigned_url(key, method:, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, opts: {})
Aws::S3::Presigner.new(client: s3_client).presigned_url( Aws::S3::Presigner.new(client: s3_client).presigned_url(
method, method,
{ bucket: s3_bucket_name, key: key, expires_in: expires_in }.merge(opts), {
bucket: s3_bucket_name,
key: key,
expires_in: expires_in,
use_accelerate_endpoint: @s3_options[:use_accelerate_endpoint],
}.merge(opts),
) )
end end
@ -362,7 +376,12 @@ class S3Helper
) )
Aws::S3::Presigner.new(client: s3_client).presigned_request( Aws::S3::Presigner.new(client: s3_client).presigned_request(
method, method,
{ bucket: s3_bucket_name, key: key, expires_in: expires_in }.merge(opts), {
bucket: s3_bucket_name,
key: key,
expires_in: expires_in,
use_accelerate_endpoint: @s3_options[:use_accelerate_endpoint],
}.merge(opts),
) )
end end

View File

@ -780,6 +780,29 @@ RSpec.describe UploadsController do
) )
end end
context "when enable_s3_transfer_acceleration is true" do
before { SiteSetting.enable_s3_transfer_acceleration = true }
it "uses the s3-accelerate endpoint for presigned URLs" do
post "/uploads/generate-presigned-put.json",
**{
params: {
file_name: "test.png",
file_size: 1024,
type: "card_background",
metadata: {
"sha1-checksum" => "testing",
"blah" => "wontbeincluded",
},
},
}
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["url"]).to include("s3-accelerate")
end
end
describe "rate limiting" do describe "rate limiting" do
before { RateLimiter.enable } before { RateLimiter.enable }
@ -1045,7 +1068,7 @@ RSpec.describe UploadsController do
XML XML
stub_request( stub_request(
:get, :get,
"https://s3-upload-bucket.s3.us-west-1.amazonaws.com/#{external_upload_stub.key}?max-parts=1&uploadId=#{mock_multipart_upload_id}", "https://s3-upload-bucket.#{SiteSetting.enable_s3_transfer_acceleration ? "s3-accelerate" : "s3.us-west-1"}.amazonaws.com/#{external_upload_stub.key}?max-parts=1&uploadId=#{mock_multipart_upload_id}",
).to_return({ status: 200, body: list_multipart_result }) ).to_return({ status: 200, body: list_multipart_result })
end end
@ -1129,6 +1152,24 @@ RSpec.describe UploadsController do
) )
end end
context "when enable_s3_transfer_acceleration is true" do
before { SiteSetting.enable_s3_transfer_acceleration = true }
it "uses the s3-accelerate endpoint for presigned URLs" do
stub_list_multipart_request
post "/uploads/batch-presign-multipart-parts.json",
params: {
unique_identifier: external_upload_stub.unique_identifier,
part_numbers: [2, 3, 4],
}
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["presigned_urls"].keys).to eq(%w[2 3 4])
expect(result["presigned_urls"]["2"]).to include("s3-accelerate")
end
end
describe "rate limiting" do describe "rate limiting" do
before { RateLimiter.enable } before { RateLimiter.enable }
@ -1173,7 +1214,7 @@ RSpec.describe UploadsController do
describe "#complete_multipart" do describe "#complete_multipart" do
let(:upload_base_url) do let(:upload_base_url) do
"https://#{SiteSetting.s3_upload_bucket}.s3.#{SiteSetting.s3_region}.amazonaws.com" "https://#{SiteSetting.s3_upload_bucket}.#{SiteSetting.enable_s3_transfer_acceleration ? "s3-accelerate" : "s3.#{SiteSetting.s3_region}"}.amazonaws.com"
end end
let(:mock_multipart_upload_id) do let(:mock_multipart_upload_id) do
"ibZBv_75gd9r8lH_gqXatLdxMVpAlj6CFTR.OwyF3953YdwbcQnMA2BLGn8Lx12fQNICtMw5KyteFeHw.Sjng--" "ibZBv_75gd9r8lH_gqXatLdxMVpAlj6CFTR.OwyF3953YdwbcQnMA2BLGn8Lx12fQNICtMw5KyteFeHw.Sjng--"
@ -1396,7 +1437,7 @@ RSpec.describe UploadsController do
describe "#abort_multipart" do describe "#abort_multipart" do
let(:upload_base_url) do let(:upload_base_url) do
"https://#{SiteSetting.s3_upload_bucket}.s3.#{SiteSetting.s3_region}.amazonaws.com" "https://#{SiteSetting.s3_upload_bucket}.#{SiteSetting.enable_s3_transfer_acceleration ? "s3-accelerate" : "s3.#{SiteSetting.s3_region}"}.amazonaws.com"
end end
let(:mock_multipart_upload_id) do let(:mock_multipart_upload_id) do
"ibZBv_75gd9r8lH_gqXatLdxMVpAlj6CFTR.OwyF3953YdwbcQnMA2BLGn8Lx12fQNICtMw5KyteFeHw.Sjng--" "ibZBv_75gd9r8lH_gqXatLdxMVpAlj6CFTR.OwyF3953YdwbcQnMA2BLGn8Lx12fQNICtMw5KyteFeHw.Sjng--"