TungDaDev's Blog

amazon S3

Aws s3.webp
Published on
/9 mins read/

Trong hệ sinh thái điện toán đám mây Amazon Web Services (AWS), Amazon S3 (Simple Storage Service) là một trong những dịch vụ nền tảng lâu đời nhất nhưng cũng đóng vai trò huyết mạch của mọi kiến trúc phần mềm: từ Data Lake, Backup/Disaster Recovery, đến lưu trữ media và tài liệu cho hàng triệu người dùng.

Với cam kết độ bền dữ liệu (Durability) lên đến 99.999999999% (11 số 9), S3 được xem là kho lưu trữ gần như không thể bị phá hủy.

Tuy nhiên, khi đưa S3 vào một hệ thống phân tán chịu tải cao (High Throughput Enterprise System), các kỹ sư phần mềm thường đối mặt với những thách thức kiến trúc nghiêm trọng:

  • Backend bị nghẽn mạng và sập bộ nhớ RAM vì để client upload trực tiếp file qua máy chủ ứng dụng thay vì dùng Direct-to-S3 Presigned URLs.
  • File upload dở dang ngốn hàng nghìn USD chi phí rác mỗi tháng do không cấu hình AbortIncompleteMultipartUpload.
  • Gặp lỗi nghẽn hạn ngạch KMS (KMS Throttling Quota) khi mã hóa hàng triệu objects bằng SSE-KMS.
  • Không hiểu mô hình Strong Read-After-Write Consistency và giới hạn 3,500 PUT / 5,500 GET per prefix, dẫn đến thiết kế cấu trúc Key sai lầm gây thắt cổ chai IOPS.

Bài viết này sẽ phân tích toàn diện các quy chuẩn kiến trúc của Amazon S3 từ cơ chế lưu trữ nội tại đến code mẫu Spring Boot 3 chuẩn production.


# bản chất lưu trữ: object vs block vs file storage

Để lựa chọn đúng giải pháp lưu trữ trên AWS, Kiến trúc sư giải pháp cần phân biệt rạch ròi 3 mô hình lưu trữ:

# mô hình strong consistency của S3

Trước tháng 12/2020, S3 hoạt động theo mô hình Eventual Consistency (sau khi ghi hoặc xóa, việc đọc lại ngay có thể nhận dữ liệu cũ).

Hiện nay, AWS đã nâng cấp toàn bộ hệ thống lên Strong Read-After-Write Consistency:

  • Ngay khi một lệnh PUT hoặc DELETE trả về 200 OK, bất kỳ lệnh GET tiếp theo trên toàn cầu đều ngay lập tức đọc được dữ liệu mới nhất mà không có bất kỳ độ trễ nào.

# kiến trúc direct-to-s3 presigned upload

Sai lầm kiến trúc phổ biến nhất của các lập trình viên nghiệp dư là tạo một endpoint: POST /api/v1/files/upload nhận MultipartFile.

# presigned URL hoạt động thế nào?

  • Backend sử dụng cặp khóa IAM bí mật của mình để tính toán chữ ký số (AWS Signature Version 4 - SigV4) đính kèm vào URL.
  • URL này cấp quyền PUT (hoặc GET) tạm thời cho đúng một object key cụ thể với thời hạn sống ngắn (ví dụ: 15 phút).
  • Client dùng URL đó đẩy trực tiếp dữ liệu lên máy chủ S3. Máy chủ Backend của bạn không tốn một byte RAM hay băng thông nào cho quá trình truyền tải file!

# presigned multipart upload cho file lớn

Khi người dùng upload video dung lượng hàng trăm Megabytes hoặc Gigabytes qua mạng di động, mạng có thể bị chập chờn. Nếu upload một khối duy nhất, khi đứt mạng ở 99%, người dùng phải upload lại từ con số 0!

Multipart Upload chia nhỏ file thành nhiều phần (Parts, từ 5MB - 5GB mỗi part) và upload song song:


# triển khai thực chiến: Spring Boot 3 & AWS SDK v2

# cấu hình S3 client bất đồng bộ

package com.company.storage.config;
 
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
 
@Configuration
public class AwsS3Configuration {
 
    @Value("${aws.region:ap-southeast-1}")
    private String awsRegion;
 
    @Bean
    public S3AsyncClient s3AsyncClient() {
        // Tự động nạp Credentials từ IAM Role (EKS IRSA) mà không hardcode AccessKey!
        return S3AsyncClient.builder()
            .region(Region.of(awsRegion))
            .credentialsProvider(DefaultCredentialsProvider.create())
            .build();
    }
 
    @Bean
    public S3Presigner s3Presigner() {
        return S3Presigner.builder()
            .region(Region.of(awsRegion))
            .credentialsProvider(DefaultCredentialsProvider.create())
            .build();
    }
}

# service tạo presigned upload URL an toàn

package com.company.storage.service;
 
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
 
import java.time.Duration;
import java.util.UUID;
 
@Service
@RequiredArgsConstructor
public class S3PresignedUploadService {
 
    private final S3Presigner s3Presigner;
 
    @Value("${aws.s3.bucket}")
    private String bucketName;
 
    public PresignedUploadResponse generateSecureUploadUrl(String originalFileName, String contentType, long fileSize) {
        // 1. Validate MIME type và kích thước tối đa (ví dụ max 20MB cho ảnh)
        if (!contentType.startsWith("image/") && !contentType.equals("application/pdf")) {
            throw new IllegalArgumentException("Định dạng file không được phép!");
        }
 
        // 2. Tạo Storage Key an toàn chống trùng lặp: prefixes/YYYY/MM/UUID.ext
        String uniqueKey = String.format("documents/%s/%s", UUID.randomUUID(), originalFileName);
 
        // 3. Khởi tạo PutObjectRequest với Content-Type ràng buộc
        PutObjectRequest objectRequest = PutObjectRequest.builder()
            .bucket(bucketName)
            .key(uniqueKey)
            .contentType(contentType)
            .build();
 
        // 4. Ký Presigned Request với thời hạn sống 10 phút
        PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
            .signatureDuration(Duration.ofMinutes(10))
            .putObjectRequest(objectRequest)
            .build();
 
        PresignedPutObjectRequest presigned = s3Presigner.presignPutObject(presignRequest);
 
        return new PresignedUploadResponse(uniqueKey, presigned.url().toString(), 10 * 60);
    }
}

TIP

Tối ưu hóa Key Prefix cho High-Throughput: S3 tự động chia partition dựa trên Key Prefix. Nếu bạn ghi hàng chục nghìn file/giây, hãy tránh đặt prefix theo định dạng ngày tháng tuần tự như logs/2026-04-12/ mà hãy dùng hash prefix hoặc UUID phía trước (logs/a1b2-2026-04-12/) để phân tán đều tải trọng qua nhiều partition S3.


# bảo mật zero-trust & bẫy hiệu năng SSE-KMS

Khi lưu trữ dữ liệu nhạy cảm trên S3, bảo mật tại chỗ (Encryption at Rest) là yêu cầu bắt buộc:

CAUTION

Hiểm Họa Nghẽn KMS Quota: Nếu bạn lưu trữ hàng triệu objects với SSE-KMS mà quên bật tính năng S3 Bucket Keys, mỗi thao tác GET/PUT object sẽ bắn một request sang AWS KMS. Khi đạt lưu lượng cao, hệ thống sẽ bị chặn bởi lỗi KMS.ThrottlingException! Bật bucket_key_enabled = true sẽ gom hàng nghìn request dùng chung một data key ngắn hạn, hóa giải hoàn toàn điểm nghẽn này.


# tối ưu chi phí: cạm bẫy AbortIncompleteMultipartUpload

Trong quá trình sử dụng Multipart Upload, nếu client đang upload được một nửa thì bị mất mạng hoặc tắt trình duyệt, các Parts đã upload lên S3 sẽ không bao giờ tự biến mất!

  • Các parts này nằm ẩn dưới dạng mảnh vỡ (in-flight parts) mà không hiển thị trên danh sách object thông thường.
  • AWS vẫn tính phí lưu trữ cho toàn bộ các mảnh vỡ này mỗi tháng!
  • Trong các dự án lớn, chi phí "rác" từ các upload dở dang có thể lên tới hàng nghìn USD mỗi tháng mà không ai hay biết.

# chính sách lifecycle bắt buộc

{
  "Rules": [
    {
      "ID": "AutoAbortIncompleteUploadsAfter7Days",
      "Status": "Enabled",
      "Filter": {},
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      }
    }
  ]
}

Mọi bucket S3 trong doanh nghiệp bắt buộc phải có rule tự động hủy các Multipart Upload chưa hoàn thành sau 7 ngày để tránh rò rỉ chi phí ngân sách đám mây.


# tổng kết & checklist thực chiến

Hạng mục kiểm traTiêu chuẩn production bắt buộcHậu quả nếu bỏ qua
Upload PatternLuôn dùng Presigned URLs để Client đẩy thẳng lên S3.Backend nghẽn băng thông, tràn RAM và sập server.
Large FilesDùng Presigned Multipart Upload cho file > 100MB.Người dùng phải upload lại từ đầu khi đứt mạng.
CredentialsDùng IAM Roles for Service Accounts (IRSA) trên EKS.Nguy cơ lộ AccessKey/SecretKey nếu hardcode vào source code.
Cost HygieneBật Lifecycle Rule AbortIncompleteMultipartUpload (7 ngày).Rò rỉ hàng nghìn USD chi phí lưu trữ mảnh vỡ ẩn.
KMS ThrottlingLuôn bật S3 Bucket Keys khi sử dụng SSE-KMS.Sập request do vượt quá hạn ngạch gọi AWS KMS.
Public SafetyBật cờ Block All Public Access ở mức độ Account/Bucket.Lộ lọt toàn bộ dữ liệu nội bộ ra ngoài Internet.

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.

← Previous postrabbitmq pattern