Nếu bạn gọi file_exists($path)$path trỏ đến một Amazon S3 bucket, bạn không hề truy cập vào đĩa cục bộ—bạn đang thực hiện một yêu cầu mạng. Một dòng kiểm tra đơn giản sẽ trở thành một vòng khứ hồi (round-trip) tới S3, có thể làm tăng thêm hàng chục mili giây cho mỗi yêu cầu.

PHP ẩn kho lưu trữ từ xa đằng sau các stream wrappers. Các hàm như fopen(), file_exists(), is_dir(), unlink()file_put_contents() sẽ đi qua các wrapper này, vốn có nhiệm vụ chuyển đổi các lệnh gọi thành các thao tác S3 API. Cách ánh xạ rất đơn giản:

  • file_exists()HeadObject
  • is_dir()ListObjects
  • unlink()DeleteObject
  • file_put_contents()PutObject

Mỗi lần gọi đều chịu độ trễ tương đương với yêu cầu S3 API tương ứng. Khi một script kiểm tra hàng chục tệp, nó tạo ra một mô hình kiểu N+1: mỗi lần kiểm tra lại tốn một bước nhảy mạng (network hop).

Tại sao điều này lại quan trọng vào lúc này

Trong môi trường phát triển thông thường, hệ thống tệp nằm trên cùng một máy, vì vậy việc kiểm tra sẽ trả về kết quả gần như ngay lập tức. Trong môi trường production, nơi các tài nguyên nằm trên S3, cùng một đoạn mã đó sẽ tạo ra một sự sụt giảm hiệu suất đột ngột (performance cliff). AWS SDK lưu bộ nhớ đệm (cache) một số kết quả trong bộ nhớ, khiến các lần kiểm tra lặp lại trong một tiến trình PHP duy nhất có vẻ nhanh. Tuy nhiên, hầu hết các triển khai PHP đều khởi tạo một tiến trình mới cho mỗi yêu cầu web, xóa sạch cache mỗi lần như vậy. Kết quả là: một vòng khứ hồi mạng đầy đủ cho mỗi đường dẫn riêng biệt trong mỗi yêu cầu.

Một trang web WordPress từng mất mười giây để hiển thị trang chủ. Các truy vấn cơ sở dữ liệu diễn ra nhanh chóng, nhưng trang web đã kích hoạt 73 lệnh gọi S3 riêng lẻ chỉ để lắp ghép nội dung. Mỗi lệnh gọi là một yêu cầu "lạnh" (cold request), biến một hàm file_exists() tưởng chừng vô hại thành một sự chậm trễ đáng kể.

Các chiến lược giảm thiểu

  • Duy trì cache qua các yêu cầu – Chuyển bộ nhớ đệm trong tiến trình sang một kho lưu trữ dùng chung như Redis. Khi một yêu cầu phát hiện ra một khóa tồn tại trên S3, các yêu cầu tiếp theo sẽ đọc kết quả đã được cache thay vì liên hệ với S3 một lần nữa.
  • Áp dụng quy trình ưu tiên cục bộ (local-first) – Ghi tệp vào đĩa cục bộ, sau đó đồng bộ chúng lên S3 một cách bất đồng bộ. Luồng yêu cầu chính chỉ tương tác với hệ thống tệp cục bộ; chi phí mạng sẽ được chuyển sang một tác vụ chạy ngầm (background job).
  • Kiểm tra mã nguồn (Audit codebase) – Tìm kiếm một cách có hệ thống các hàm hệ thống tệp có thể được giải quyết thông qua một remote wrapper. Đánh dấu bất kỳ hàm nào xuất hiện bên trong các vòng lặp chặt (tight loops) hoặc các đường dẫn quan trọng của yêu cầu.
  • Kiểm tra dữ liệu APM – Nếu thời gian phản hồi tăng vọt trong khi các chỉ số cơ sở dữ liệu vẫn ổn định, hãy đi sâu vào thời gian thực thi của storage SDK. Sự gia tăng đột ngột về độ trễ của SDK thường chỉ ra các lệnh gọi S3 ẩn.

Bài học then chốt: một hàm trông có vẻ như chỉ mất micro giây để kiểm tra cục bộ có thể ẩn chứa hàng chục mili giây độ trễ mạng. Hãy coi file_exists() trên S3 như một lệnh gọi bên ngoài, lưu cache kết quả của nó và chuyển các tác vụ nặng ra khỏi luồng yêu cầu. Nếu không, độ trễ ẩn sẽ tiếp tục làm chậm trang web của bạn, qua từng yêu cầu mạng vô hình một.