Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ปลดล็อก Rails Apps ให้เร็วขึ้นด้วย Russian Doll Caching

วันนี้เราจะเจาะลึก Russian Doll Caching เป็นกลยุทธ์เพื่อปรับปรุง caching ให้ดียิ่งขึ้นเกินกว่า fragment caching ที่ built-in ของ Rails

Fragment Caching

เมื่อใช้ fragment caching built-in ของ Rails ส่วนของ rendered views จะถูกเก็บเป็น view fragments และนำกลับมาใช้ใหม่หากถูกขออีกครั้ง Cached fragments นี้จะถูกใช้ซ้ำจนกว่าจะ stale หมายถึงเก่าแก่เพราะข้อมูลที่แสดงเปลี่ยนไปตั้งแต่สร้าง fragment หากอยากอ่านเพิ่ม เราอธิบาย fragment caching ลึกขึ้นในโพสต์ก่อนหน้า

👋 หากชอบอ่านเกี่ยวกับประสิทธิภาพกว้างกว่าแค่ caching ดู checklist การติดตามประสิทธิภาพ Ruby ของเราได้เลย

แม้จะให้ speed boost ไปได้ ไม่ว่าจะ views ที่ซับซ้อนหรือ views ที่มี rendered partials มาก เรายังปรับปรุงได้อีกด้วยการเพิ่มความเข้มข้นด้วยแนวทางชื่อ Russian doll caching

เมื่อใช้ caching approach นี้ view fragments จะถูกวางซ้อนกัน คล้ายกับตุ๊กตา "Matryoshka" ที่กลยุทธ์นี้命名โดยการแยก cached fragments เป็นชิ้นเล็กๆ outer cache จึง render เร็วขึ้นเมื่อ nested fragment เพียงหนึ่งตัวเปลี่ยนแปลง

ตัวอย่าง Russian Doll Caching

เป็นตัวอย่าง ใช้ร้านค้าขายสินค้า แต่ละสินค้ามี variants หลายตัว เช่น ขายหลายสีของสินค้าเดียวกัน ในหน้า index เราจะแสดงสินค้าที่ขายได้ทั้งหมด พร้อม variants ทั้งหมด

ใน product index เรา wrap แต่ละ product partial ใน cache block ใช้ product object สร้าง cache key ใช้ invalidate cached fragment ประกอบด้วย id ของ object, updated_at date, และ digest ของ template tree จึง stale อัตโนมัติหาก object เปลี่ยน หรือ template เนื้อหาเปลี่ยน

# app/views/products/index.html.erb
<h1>Products</h1>
<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

Tip: เราเขียน block ทั้งหมดเพื่อความชัดเจน แต่คุณ render แต่ละ product ใน cache block ได้โดย <%= render partial: 'products/product', collection: @products, cached: true %> แทน

ใน products partial เรา render row สำหรับแต่ละ variant ของสินค้า

# app/views/products/_product.html.erb
<article>
  <h1><%= product.title %></h1>
  <ul>
    <% product.variants.each do |variant| %>
      <%= render variant %>
    <% end %>
  </ul>
</article>

Cache Invalidation

แม้ cache keys ใน fragment caching ของ Rails ทำให้ cache invalidation ง่ายขึ้น คุณก็ไม่เคยหลุดพ้นจากการกังวล cache validation (หนึ่งในสองปัญหายากของ computer science)

ในตัวอย่างนี้ เรา cache products partial ซึ่งมีรายการ variants ของสินค้า เนื่องจาก cache key ไม่รวมข้อมูล variants variants ใหม่ที่เพิ่มเข้ามาจะไม่แสดง จนกว่าสินค้าจะเปลี่ยนแปลงด้วย

วิธีแก้คือ ให้สินค้า เปลี่ยนแปลง เมื่ออะไรสักอย่างใน variants เปลี่ยน ทำได้โดยอัปเดต updated_at attribute ของสินค้าเมื่อเกิดเหตุการณ์นั้น เนื่องจากเรื่องนี้พบบ่อย มี argument สำหรับ belongs_to (และ relation methods อื่นๆ ของ ActiveModel) ชื่อ :touch จะอัปเดต parent object's updated_at ให้อัตโนมัติ

class Variant < ApplicationRecord
  belongs_to :product, touch: true
end

Nested Fragments

ตอนนี้เราแน่ใจว่า product fragments อัปเดตเมื่อ variants เปลี่ยน แล้วมา cache variants ด้วย เหมือนเดิม เราเพิ่ม cache block รอบแต่ละตัว

<article>
  <h1><%= product.title %></h1>
  <ul>
    <% product.variants.each do |variant| %>
      <% cache(variant) do %>
        <%= render variant %>
      <% end %>
    <% end %>
  </ul>
</article>

Tip: เราเขียน block ทั้งหมดเพื่อความชัดเจน แต่ render แต่ละ variant ใน cache block ได้โดย <%= render partial: 'variants/variant', collection: product.variants, cached: true %> แทน

บน cold cache (clear cache ได้โดยรัน rake tmp:cache:clear) request แรกจะ render แต่ละ product partial

เมื่อ request หน้านี้อีกครั้ง (อย่าลืมเปิด caching ใน development ด้วย rails dev:cache) แต่ละ product partial จะถูก cache เป็น partial และ request ที่สองจะคืน cached fragments

Started GET "/products" for 127.0.0.1 at 2018-03-30 14:51:38 +0200
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.2ms) SELECT "products".* FROM "products"
Variant Load (0.9ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51)
Rendered variants/_variant.html.erb (0.5ms)
Rendered variants/_variant.html.erb (0.1ms)
...
Rendered products/_product.html.erb (44.8ms) [cache miss]
...
Rendered products/_product.html.erb (46.2ms) [cache miss]
Rendered products/index.html.erb within layouts/application (1378.6ms)
Completed 200 OK in 1414ms (Views: 1410.5ms | ActiveRecord: 1.1ms)
Started GET "/products" for 127.0.0.1 at 2018-03-30 14:51:41 +0200
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.3ms) SELECT "products".* FROM "products"
Variant Load (12.7ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51)
Rendered products/index.html.erb within layouts/application (48.1ms)
Completed 200 OK in 76ms (Views: 59.0ms | ActiveRecord: 13.0ms)

Voila: Russian Doll Magic

ความมหัศจรรย์ของ Russian doll caching เห็นได้เมื่อเปลี่ยน variant หนึ่ง เมื่อ request index อีกครั้งหลัง variant เปลี่ยน cached product fragment จะ rerender เพราะ updated_at attribute เปลี่ยน

Product partial รวม variants ทุกตัวของสินค้า Cached fragment ของ variant ที่เปลี่ยนไปเป็น stale ต้อง regenerate แต่ variants อื่นไม่เปลี่ยน จึง reuse cached fragments ใน logs จะเห็นทั้ง variant และ product partials render ครั้งละหนึ่งครั้ง

Started GET "/products" for 127.0.0.1 at 2018-03-30 14:52:04 +0200
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.3ms) SELECT "products".* FROM "products"
Variant Load (1.2ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51)
Rendered variants/_variant.html.erb (0.5ms)
Rendered products/_product.html.erb (13.3ms) [cache miss]
Rendered products/index.html.erb within layouts/application (45.9ms)
Completed 200 OK in 78ms (Views: 73.5ms | ActiveRecord: 1.5ms)

ผลลัพธ์สุดท้าย

ด้วยการ nest cache fragments แบบนี้ view เกือบไม่เคย render ทั้งหมด เว้นแต่ cache ว่างเปล่าสมบูรณ์ แม้ข้อมูลเปลี่ยน ส่วนใหญ่ของ rendered pages ก็ถูก serve ตรงจาก cache

หวังว่าบทความนี้ช่วยให้คุณได้ insights ใหม่เกี่ยวกับประสิทธิภาพแอป นั่นคือเหตุผลเราอยู่ที่นี่ หากชอบบทความเกี่ยว caching และอยากอ่านเพิ่ม อ่านเพิ่มได้จากโพสต์เกี่ยว ActiveRecord's Counter Cache, Cache Stores in Rails, collection caching และ fragment caching ใน Rails ที่กล่าวไว้ก่อนหน้านี้