การใช้ 2-Layer Cache เพื่อลดภาระการทำงานของ Database

วันนี้ได้ลองตอบโพสต์นึงของ กระทู้เกี่ยวกับอะไรสักอย่างนี่แหละที่น่าจะต้องใช้ cache เลยอยากเล่าวิธีการใช้งาน cache ในระดับ low level ที่ใช้ๆกันทั่วไป แต่ไม่ใช่ Hibernate/EF cache นะครับ (แฟนๆ Java ที่ใช้ Hibernate คงรู้จัก Second Layer Cache กันเป็นอย่างดี)

เริ่มต้นจากรูป classic กันก่อน ถ้าเราอยากจะทำเว็บอะไรสักอย่างให้มัน scale ได้แบบสบายๆ หน้าตามันน่าจะคล้ายๆแบบนี้เนอะ คือ มันน่าจะมี Web Cluster โง่ๆอันนึงที่เรียกไปที่ API Layer โดยจะจัดการ session ของผู้ใช้ยังไงก็แล้วแต่ แต่ API ที่อยู่ข้างหลังน่าจะเป็น Idempontent / Immutable / stateless เพื่ออะไรน่ะเหรอ ก็เพื่อให้เราสามารถ scale มันได้อย่างสบายใจยังไงล่ะ เพราะเมื่อเรา scale ตัว API layer ออกไปด้วย Kubernetes หรือ Docker Swarm หรืออะไรก็ตาม ถ้า WebServer ส่ง request มาที่ API server คนละตัวแล้วได้ response คนละแบบคงตลกน่าดู

1_LTGRdrgh-s29OsatPz7prw

ส่วนในส่วนของ Database นั้น ถ้าเป็นระบบที่อ่านมากกว่าเขียน ก็ทำแบบในภาพก็ได้คือเอา Async Tier มาขวางซะ เพราะการ write มันเป็น bottle neck อยู่ละ เราก็ให้ Async Tier นี้มาจัดการใครอยากใช้ Kafka ก็ลองไปอ่าน blog ก่อนหน้านี้เกี่ยวกับ Kafka นะครับ

มาลองใช้ Kafka กับ NodeJS กันเถอะ — ”Good morning Kafka !!!”

เชื่อว่า Kafka หนึ่งในเทคโนโลยีที่ฮอตและร้อนแรงที่สุดในโลก developer เรา เรามักจะได้ยินชื่อ Kafka กันบ่อยๆ…
mahasak.com
พอแต่ละ write request ได้ข้อมูลมาแล้ว ในเมื่อข้อมูลส่วนที่เหลือมัน Read เราก็จัดการขยาย Capacity กันไปเลยด้วย Master-Slave Replication แบบบ้านๆนี่แหละ ถ้าใครยังไม่เคยลองทำ ลองดูใน blog ก่อนหน้านี้อีกอันนะครับ

[PostgreSQL] Master-slave replication + load balance ด้วย pgpool2

วันนี้ต้องสรุปเรื่องการเตรียม Database Server โดยใช้ PostgreSQL ให้กับน้องๆในทีมก่อนที่จะต้องไปทำอย่างอื่น…
mahasak.com
ทีนี่ก็มาถึงประเด็นที่อยากจะเน้นกัน เพราะในสมัยนี้เว็บแอพทั้งหลายชอบใช้ AJAX กันเหลือกันไม่ว่าจะเป็น dropbox เล็กน้อยไปจน update หน้าจอทั้งจอด้วย AJAX ที่ return HTML มาทั้งก้อนเหมือนหลงมาจากปี 2001 ก็ใช้ Ajax กันทั้งนั้น นั่นหมายถึง 10000 user จะไม่ได้มี 10000 request นะครับ มันจะมีมากกว่านั้นแน่ๆ ถ้า ajax request มันไปแตะ static content เราก็จับมันไปวางที่ CDN สิ

แต่ถ้ามันเป็น database request ล่ะ มันจะถล่ม database เราจนหนำใจแน่ๆ วันนี้จะมาเสนอแนวคิดในการใช้ 2-Level cache กันนะครับ สำหรับสาวก Java ที่ใช้ Hibernate คงคุ้นเคยกับ Second Level Cache กันบ้างเนอะ แต่วันนี้จะลองยกตัวอย่างใน nodejs ว่าเรามีอะไรที่พอจะเอามาทำได้บ้าง

1_e3WMRplyxey4Slco_pkWGg

2-Level Cache concept
สมมติว่าผมอยากจะใช้ nodejs + postgresql ในการทำงานกับ 2-level cache อย่างแรกเลยผมต้องมี connection ก่อนเนอะ ว่าแล้วก็หยิบเอา node-postgres มาก่อนเลย

const { Client } = require('pg') 
const client = new Client()  
await client.connect()  
const res = await client.query(
    'SELECT $1::text as message', 
    ['Hello world!']
) 
console.log(res.rows[0].message) // Hello world! 
await client.end()

ที่นี้ถ้าเราอยากเพิ่ม 2-level cache ลงไป ผมก็จะใช้ node-cache สำหรับเป็น internal cache ใน internal process และผมจะใช้ async-redis สำหรับ out-of-process cache ทีนี้เราจะเห็นว่า ใน InProc Cache และ OutProc Cache นั้น เราต้องใช้ร่วมกันระหว่าง in-memory และ couchbase document สิ่งที่เราอาจจะต้องทำเพิ่มเติมคือ คิดว่าเราจะเก็บยังไงในแต่ละ level ของ cache เช่น ใน InProc เราจะเก็บเป็น JavaScript object แต่ใน Redis นั้นเราสามารถเก็บ JS document ตรงๆหรือจะแปลงเป็น String ก่อนเก็บก็ได้ เราจึงสามารถเอากลับมา refill ใน InProc Cache ได้ตรงๆเลย แต่ในตัวอย่างนี้จะแสดงตัวอย่างการเก็บเป็น string เพื่อจะได้เห็นไอเดียคร่าวๆนะครับ

// In-Proc cache
const NodeCache = require( "node-cache" )
const inMemoryCache = new NodeCache()
// Out-Proc cache
const asyncRedis = require("async-redis");
const redis = asyncRedis.createClient();
// Database Client
const { Client } = require('pg') 
const db = new Client()  
await db.connect()
const isObject = (obj) => obj === Object(obj)
const forOutProc = (obj) => JSON.stringify(obj)
const async getCache = (key, sql) => {
    let value = inMemoryCache.get(key);
    // check in-proc first
    if ( value !== undefined && isObject(value){
        return value; // internal cache hit
    }
    //check out-proc second
    value = await redis.get(key);
    
    // If found cache fill local cache & return
    // If not query from database, refill both cache and return
    if ( value === undefined) {
        const res = await db.query(sql)
        // Create cache object from database result
        value = {k1: v1, result: res, ... }
        // Fill In-Proc cache
        inMemoryCache.set( key, value, 10000 );
        // Fill Out-Proc cache
        await redis.set("string key", "string val");
        // if not success, log and send metrics
        return value
    } else {
        success = inMemoryCache.set( key, value, 10000 );
        // if not success, log and send metrics
        return value
    }
}

code ข้างต้นนี้เป็นตัวอย่างไอเดียว่าสิ่งที่ต้องทำมีอะไรบ้างยังต้อง implement เพิ่มอีกหน่อย และนอกเหนือไปจากนั้น ไม่มีวิธีตายตัวที่จะทำให้ match กับทุก Solution นะครับ

(แก้ไข) ในการกำหนด cache key นี่ก็สำคัญนะครับ แต่ว่ายังไม่อยากเล่าเยอะวันนี้ เอาไว้วันหลังนะ

สิ่งหนึ่งที่ต้องทำแน่ๆคือ เก็บข้อมูลการใช้งาน cache และเอาไป improve กระบวนการ cache เช่น cache replacement policy จะใช้แบบไหน LRU-Least Recent used, FIFO, Time-based เพราะทุก cache ที่เราเก็บนั้นจะใช้ทรัพยากรของเครื่องไปเรื่อยๆถ้าเราไม่จัดการให้ดี สำหรับใครที่อยากลองดูเพิ่มเติมเรื่องการ monitor ลองดูอันนี้ได้นะครับ

Monitoring API performance with Grafana & Elasticsearch

วันนี้มีพี่ท่านนึงมาถามเรื่อง TSDB: Time Series Database เพื่อจะใช้ในการทำ monitoring ซึ่งก็มีพูดถึงหลายตัวที่เคยใช้งาน…
mahasak.com
จริงๆแล้วการใช้ Cache เป็นอีกหนึ่งกลยุทธ์ในการลด pressure ของการใช้งานต่อระบบ และต่อชีวิตให้ระบบได้ โดยเฉพาะ Dynamic Data ที่ต้องมีการ interact กับ Database ซึ่งถ้าปล่อยให้ access จาก Database ทั้งหมดบอกได้คำเดียว มีเงินเท่าไหร่ก็ไม่พอจ่ายค่า server ครับ

Happy Coding ครับ

References:
https://node-postgres.com/