ในทาง Programming Caching หมายถึง Store Value (หรือ Values) ไว้เพื่อ Quick Retrieval ในอนาคต โดยทั่วไปคุณทำกับ Values ที่ Slow to Compute เนื่องจากเหตุผลบางอย่าง เช่น ต้อง Hit External API เพื่อ Retrieve หรือ Involve Number-crunching มากเพื่อ Generate
Cached Values บ่อยครั้ง Stored บน Separate Server เช่น Memcached หรือ Redis สามารถ Store บน Disk หรือ RAM ใน Code เราบ่อยครั้ง 'Cache' Data ภายใน Variables เพื่อหลีกเลี่ยง Calling Expensive Functions หลายครั้ง
data = some_calculation() a(data) b(data) Trade-off สำหรับ Speed ที่ได้คือคุณใช้ Old Data ถ้า Cached Data กลาย 'Stale' (เก่า/ไม่สด) และไม่ Accurate แล้วล่ะ? คุณต้อง Clear Cache เพื่อ 'Invalidate' มัน
The Argument Against Caching (ข้อโต้แย้งต่อ Caching)
Like the old saying goes, มี 2 Hard Problems ใน Computer Science: 1. Naming Things 2. Cache Invalidation 3. Off-by-one Errors
ทำไม Cache Invalidation ยาก? Cached Value โดย Nature ของมัน 'Hide' Real Value ทุกครั้งที่ 'Real' Value เปลี่ยน คุณ (ใช่ คุณ Programmer) ต้องจำว่า 'Invalidate' Cache เพื่อให้มัน Updated
สมมติคุณ Add 'Word Count' Widget ให้ Text Editor ต้อง Update Word Count ขณะ User Types Simplest Approach คือ Re-count Words ทุก Keystroke แต่ Slow เกินไป มี Approach อื่น:
- Count Words เมื่อ Load File
- Save Word-count ไป Variable (หรือ 'Cache It')
- Display Variable Contents ไป Screen
Implementation นี้เร็วกว่ามาก แต่ Cached 'Word Count' ไม่เปลี่ยนขณะ Type เพื่อให้เปลี่ยน เราต้อง 'Invalidate' Cache ทุกครั้งที่คาดว่า Word Count จะเปลี่ยน
ตอน Keystrokes มา Detect Words (Spaces) Increment Word Counter และ Decrement ตอน Delete Words ง่าย จบ Next Ticket
...แต่นี่: จำได้ไหม Update Word Count ตอน User Cut Text ไป Clipboard? ตอน Paste Text? ตอน Spell-checker Split Typo เป็นสองคำ?
Problem ไม่ใช่ Update Value ซึ่ง Trivial มาก Problem คือคุณต้องจำ Update ใน Every Single Place Miss แค่หนึ่ง Update ก็ Cause Cache Invalidation Problems หมายถึงคุณ Display Stale Value ให้ User
ด้วยนี้ คุณเห็นว่า Adding Caching brings in Technical Complexity และ Potential Sources of Bugs ปกติ Problems นี้ Solve ได้ แต่ควร Keep in Mind ก่อน Jump ไป Caching เป็น Solution
Speed Without Caching
ถ้าเรา Take Caching Off The Table การเร่ง App คือ Identify และ Fix Performance Bottlenecks - Systems ที่ Slower Than They Could Be เรา Group เป็น 3 Categories:
- Database Queries (Too Many หรือ Too Slow)
- View Rendering
- Application Code (เช่น Heavy Calculations)
ตอน Work on Performance มี 2 Techniques ต้องรู้เพื่อ Progress: Profiling และ Benchmarking
Profiling
Profiling คือวิธีรู้ Where Problems ใน App: หน้านี้ช้าถึง Render Template ช้า? หรือ Hit Database 1 ล้านครั้ง?
สำหรับ Ruby on Rails แนะนำ rack-mini-profiler ซึ่ง Add Nice Little Widget ขอบ App ให้ Overview ดีของสิ่งที่ Render Page ใช้ เช่น Database Queries กี่ครั้ง ใช้เวลาเท่าไหร่ Partials Rendered กี่ตัว
สำหรับ Production (Pro-tip: rack-mini-profiler ทำงานดีใน Production แค่ Ensure ให้ Appear สำหรับ Certain Users เช่น Admins หรือ Developers) มี Online Services รวม Skylight, New Relic, Scout Monitor Page Performance
Target <= 100ms ที่ Cited ทั่วไป ดีสำหรับ Page Rendering เพราะ Less Than นี้ User Detect ยากใน Real-world Internet Usage Target ของคุณ Vary ขึ้นอยู่กับ Factors หลายอย่าง ตอนหนึ่ง Working on Legacy App Terrible Performance ผมทำ Target <= 1 Second ซึ่งไม่ Great แต่มากกว่าตอนเริ่มมาก
Benchmarking
เมื่อ Figure Out Where Problem แล้ว เราใช้ Benchmarks ดู Effect ของ Optimization มีหรือไม่ ผมชอบ benchmark-ips Gem ให้ Human-readable Way ดู Differences โค้ดทำ
ตัวอย่าง Trivial เปรียบเทียบ String Concatenation vs Interpolation:
require 'benchmark/ips' @a = "abc" @b = "def" Benchmark.ips do |x| x.report("Concatenation") { @a + @b } x.report("Interpolation") { "#{@a}#{@b}" } x.compare! end และ Results:
Warming up -------------------------------------- Concatenation 316.022k i/100ms Interpolation 282.422k i/100ms Calculating ------------------------------------- Concatenation 10.353M (± 7.4%) i/s - 51.512M in 5.016567s Interpolation 6.615M (± 6.8%) i/s - 33.043M in 5.023636s Comparison: Concatenation: 10112435.3 i/s Interpolation: 6721867.3 i/s - 1.50x slower ได้ Human-readable Result สวย Interpolation ช้ากว่า Concatenation 1.5 เท่า (สำหรับ Small Strings) ดังนั้นแนะนำ Copy Method ที่พยายาม Improve ให้ชื่อใหม่ แล้ว Run Quick Comparisons ดูว่าจริงๆ Improve Performance ไปไหม
Fixing Performance Issues
จุดนี้เรารู้ Parts ที่ Slow มี Benchmarks วัด Improvement ตอนมันเกิด ตอนนี้แค่ทำ Actual Work ของ Optimizing Techniques ที่เลือก Depend On Where Issues: Database, Views, หรือ Application
Database Performance
สำหรับ Database-related Performance Issues มีสิ่งต้องดู แรก Avoid Dreaded 'N+1 Queries' Situations เช่นนี้มักเกิด Rendering Collection ใน View เช่น User มี 10 Blog Posts อยาก Display User และ Posts ทั้งหมด Naive First-cut อาจเป็น:
# Controller def show @user = User.find(params[:id]) end # View Name: <%= @user.name %> Posts: <%= @user.posts each do |post| %> <div>Title: <%= post.title %></div> <% end %> Approach ข้างบน Get User (1 Query) แล้ว Fire Off Query สำหรับแต่ละ Post (N=10 Queries) รวม 11 Total (N+1) โชคดี Rails มี Simple Solution เพิ่ม .includes(:post) ใน ActiveRecord Query ตัวอย่างข้างบน แค่เปลี่ยน Controller Code เป็น:
def show @user = User.includes(:post).find(params[:id]) end ตอนนี้ Fetch User และ All Posts ใน One Database Query
อีกอย่าง Look For Where Push Calculations Into Database ซึ่ง Usually Faster Than Same Operation ใน App Common Form คือ Aggregations เช่น:
total = Model.all.map(&:somefield).sum นี่ Grab All Records จาก Database แต่ Actual Summing เกิดใน Ruby เรา Speed Up ให้ Database คำนวณให้:
total = Model.sum(:somefield) อาจต้องซับซ้อนกว่า เช่น Multiply Two Columns:
total = Model.sum('columna * columnb') Common Databases Support Basic Arithmetic และ Common Aggregations เช่น Sum และ Average ดังนั้น Watch Out map(...).sum Calls ใน Codebase
View Performance
แม้ผมบอกว่า Template-related Performance Woes Lend Themselves More to Caching As Solution ยังมี Low-hanging Fruit ที่อาจ Rule Out ก่อน
General Page-load Times Check ว่าใช้ Minified Sources สำหรับ Javascript หรือ CSS Libraries (บน Production Servers อย่างน้อย)
Watch Out สำหรับ Large Numbers of Partials Included ถ้า _widget.html.erb Template ใช้ 1ms Process แตคุณมี 100 Widgets บน Page นั่น 100ms หายแล้ว One Solution Reconsider UI 100 Widgets On Screen At Once ปกติไม่ Great UX อาจ Look At Pagination หรือ UI/UX Overhaul ที่ Drastic กว่า
Application Code Performance
ถ้า Performance Issue ใน Application Code เอง (Data Manipulation) ไม่ใช่ View หรือ Database Layers มี Options สองอย่าง หนึ่งคือ See If At Least Some Work Could Push Into Database ผ่าน Queries (เหมือนด้านบน) หรือ Database Views กับ Scenic Gem)
อีก Option Move 'Heavy Lifting' Into Background Job แม้อาจต้อง UI Changes Handle Fact ว่า Value Computed Asynchronously
I Still Need Caching; Now What?
ผ่านทั้งนี้ อาจ Decide ว่าใช่ Caching คือ Solution ที่ต้องการ So What Should You Do? Stay Tuned เพราะนี่ Article แรกในซีรีส์ครอบคลุม Different Forms of Caching ใน Ruby on Rails