หากคุณเรียกใช้ file_exists($path) และ $path ชี้ไปยัง Amazon S3 bucket คุณไม่ได้กำลังเรียกใช้งานดิสก์ในเครื่อง แต่คุณกำลังทำการร้องขอผ่านเครือข่าย (network request) การตรวจสอบเพียงบรรทัดเดียวนี้จะกลายเป็นการรับส่งข้อมูลแบบไป-กลับ (round-trip) ไปยัง S3 ซึ่งอาจเพิ่มเวลาในทุกๆ คำร้องขอไปอีกหลายสิบมิลลิวินาที
PHP ซ่อนที่เก็บข้อมูลระยะไกลไว้เบื้องหลัง stream wrappers ฟังก์ชันต่างๆ เช่น fopen(), file_exists(), is_dir(), unlink() และ file_put_contents() จะทำงานผ่าน wrapper เหล่านี้ ซึ่งจะแปลงคำสั่งเหล่านั้นให้เป็นการทำงานผ่าน S3 API โดยมีการจับคู่ที่ตรงไปตรงมาดังนี้:
file_exists()→ HeadObjectis_dir()→ ListObjectsunlink()→ DeleteObjectfile_put_contents()→ PutObject
การเรียกใช้แต่ละครั้งจะมีค่าความหน่วง (latency) เท่ากับการร้องขอ S3 API ที่เกี่ยวข้อง เมื่อสคริปต์ต้องตรวจสอบไฟล์จำนวนมาก มันจะสร้างรูปแบบที่คล้ายกับ N+1: คือมีการกระโดดผ่านเครือข่าย (network hop) หนึ่งครั้งสำหรับการตรวจสอบในแต่ละครั้ง
ทำไมเรื่องนี้ถึงสำคัญในตอนนี้
ในสภาพแวดล้อมการพัฒนาทั่วไป ระบบไฟล์จะอยู่บนเครื่องเดียวกัน ดังนั้นการตรวจสอบจึงส่งผลลัพธ์กลับมาแทบจะทันที แต่ในสภาพแวดล้อมการใช้งานจริง (production) ที่ไฟล์ต่างๆ อยู่บน S3 โค้ดชุดเดียวกันนี้จะทำให้ประสิทธิภาพลดฮวบลงอย่างรวดเร็ว (performance cliff) แม้ว่า AWS SDK จะมีการเก็บแคชบางส่วนไว้ในหน่วยความจำ ทำให้การตรวจสอบซ้ำๆ ภายในกระบวนการ (process) ของ PHP เดียวกันดูเหมือนจะรวดเร็ว แต่ในการติดตั้ง PHP ส่วนใหญ่ จะมีการสร้าง process ใหม่สำหรับทุกๆ web request ซึ่งจะล้างแคชทิ้งทุกครั้ง ผลลัพธ์ที่ได้คือ ต้องมีการรับส่งข้อมูลผ่านเครือข่ายแบบไป-กลับเต็มรูปแบบสำหรับทุกๆ path ที่แตกต่างกันในทุกๆ คำร้องขอ
เว็บไซต์ WordPress เคยใช้เวลาถึงสิบวินาทีในการแสดงผลหน้าแรก แม้ว่าการคิวรีฐานข้อมูลจะรวดเร็ว แต่หน้าเว็บกลับต้องเรียกใช้งาน S3 ถึง 73 ครั้งเพียงเพื่อรวบรวมเนื้อหา การเรียกใช้แต่ละครั้งเป็นการร้องขอแบบ cold request ซึ่งเปลี่ยน file_exists() ที่ดูเหมือนไม่มีพิษมีภัยให้กลายเป็นความล่าช้าที่สังเกตเห็นได้ชัด
กลยุทธ์การบรรเทาปัญหา
- เก็บแคชไว้ข้ามคำร้องขอ (Persist cache across requests) – ย้ายแคชจากใน process ไปยังที่เก็บข้อมูลส่วนกลาง เช่น Redis เมื่อคำร้องขอหนึ่งพบว่ามี key อยู่บน S3 แล้ว คำร้องขอต่อๆ ไปจะอ่านผลลัพธ์จากแคชแทนที่จะติดต่อ S3 อีกครั้ง
- ใช้เวิร์กโฟลว์แบบเน้นไฟล์ในเครื่องเป็นหลัก (Adopt a local-first workflow) – เขียนไฟล์ลงในดิสก์ในเครื่องก่อน แล้วจึงซิงค์ไปยัง S3 แบบ asynchronous โดยเส้นทางการทำงานหลัก (main request path) จะสัมผัสเพียงแค่ระบบไฟล์ในเครื่องเท่านั้น ส่วนภาระด้านเครือข่ายจะถูกย้ายไปเป็นงานเบื้องหลัง (background job) แทน
- ตรวจสอบโค้ด (Audit the codebase) – ค้นหาฟังก์ชันระบบไฟล์ที่อาจถูกแปลงเป็น remote wrapper อย่างเป็นระบบ และทำเครื่องหมายฟังก์ชันที่ปรากฏอยู่ภายในลูป (tight loops) หรือเส้นทางการทำงานที่สำคัญต่อคำร้องขอ (request-critical paths)
- ตรวจสอบข้อมูล APM (Inspect APM data) – หากเวลาในการตอบสนองพุ่งสูงขึ้นในขณะที่ตัวชี้วัดฐานข้อมูลยังคงที่ ให้เจาะลึกไปที่เวลาการทำงานของ storage SDK การเพิ่มขึ้นของความหน่วงใน SDK อย่างกะทันหันมักบ่งชี้ถึงการเรียกใช้งาน S3 ที่ซ่อนอยู่
บทสรุปสำคัญคือ: ฟังก์ชันที่ดูเหมือนเป็นการตรวจสอบในเครื่องที่ใช้เวลาเพียงไมโครวินาที อาจซ่อนความหน่วงของเครือข่ายไว้ถึงหลายสิบมิลลิวินาที จงปฏิบัติกับ file_exists() บน S3 เสมือนเป็นการเรียกใช้งานจากภายนอก (external call) โดยการเก็บแคชผลลัพธ์และย้ายงานหนักๆ ออกจากเส้นทางการทำงานของคำร้องขอ มิฉะนั้น ความหน่วงที่ซ่อนอยู่จะทำให้เว็บไซต์ของคุณช้าลงเรื่อยๆ ผ่านการร้องขอเครือข่ายที่มองไม่เห็นทีละครั้ง
