[CI Day 9] Query Caching Modify
เออ ช่วงนี้เห็นหลายคนขอ M ผมเข้ามาเยอะเหลือเกิน ยังไงผมก็จะให้ไว้ในกระทู้นี้ละกัน d n a b o a r d @ g m a i l . c o m ยังไงแวะเวียนมาแลกเปลี่ยนความรู้กันได้เลย
วันนี้เรื่องที่จะมาเล่า คือเรื่องการทำ Caching ของ CI ครับ แน่นอนครับ ทำเวบสมัยนี้ มันไม่เหมือนเมื่อก่อนแล้ว มันมีเทคนิคที่ พัฒนาไปมาก ในการทำเวบ เรื่องที่ผมใส่ใจมากเป็นพิเศษ นอกเหนือจาก Structure แล้วก็คือ Performance นี่ล่ะครับ และสิ่งที่ Developer ทำกันมากที่สุด ในการ Tuning Performance นั่นก็คือ การทำ Caching ครับ ผมยกตัวอย่างเช่น Facebook.com
รู้มั้ยครับ facebook เองเขียนด้วย PHP ส่วน DB ก็ยังใช้ MySQL นี่ล่ะ แต่ทำไม มันถึงรองรับ คนได้มากเหลือเกิน ซึ่งจะว่ากันไปจริงๆ แล้วมันมีหลายเรื่องทั้ง Load Balance, Reverse Proxy แล้วก็ องค์ประกอบของ Hardware แต่เรื่องที่ น่าสนใจคือ FB มีการ Access DB น้อยมาก ซึ่งเราเองรู้กันดีอยู่แล้วว่า MySQL เนี่ย มันไม่ค่อยจะดีนัก ต่อเวบที่มี คนเยอะๆ มันอ่อนแอเหลือเกิน
ดังนั้นแทนที่ FB จะทำการ Access เข้า DB ตรงๆ อยู่ตลอดเวลา ก็เปลี่ยนมาออกแบบให้ Access ป่าน Memcached ซึ่งเป็นการ Cached ใน Fast Memory (**Data จะหายเมื่อมีการ Reboot**) ด้วยวิธีนี้จะทำให้การ Access DB ลดน้อยลง จนเราแปลกใจ และเป็นสาเหตุหลักที่ทำให้เวบมี Down Time ต่ำมากๆ
อ้างอิง: Facebook Scalability
แต่ด้วยข้อจำกัดของ Server ที่เพื่อนๆ ใช้มันทำให้การที่ผมจะไปพูดถึง Cached ระดับ Memory เช่น APC, Memcached นั้น ดูจะแคบไปหน่อย ผมก็เลยขอเลี่ยงไปพูดถึง Cache ในระดับ Flat File แทน ซึ่งน่าจะทำกันได้ในทุกๆ Server อยู่แล้ว
(ผมไม่ขอพูดถึง Caching ทีมีใน CI นะครับ เพราะว่าผมไม่ปลื้มเท่าไหร่ ถ้าสนใจก็อ่านใน manual น่าจะเข้าใจได้ไม่ยาก)
เอาล่ะไม่รู้จะเกริ่นยาวไปถึงไหน มาเริ่มกันเลยดีกว่าครับ....
ในการออกแบบ Structure ของวันนี้เราจะพูดถึงเรื่องหลักๆ 2 เรื่องคือ
1. การออกแบบ Folder Structure (Node)
2. การออกแบบ Cache แบบ GZIP
เรื่องแรกก่อน (วันนี้สงสัยไม่จบง่ายๆ -*-)
เพื่อนๆ รู้มั้ยครับ การจัดเก็บไฟล์ลงใน Dir แต่ละ Dir นั้นก็มีข้อจำกัดเหมือนกัน ไม่ใช่เอะอะๆ ยัดเก็บ Dir เดียว พรวดๆ ถ้าทำแบบนั้นมันจะเกิดปัญหาในท้ายที่สุดคือ Node เต็ม ซึ่งตามที่เคยรู้มามันควรจะเป็นราวๆ 60,000 ต่อ 1 ชั้นราวๆ นี้มั้ง (จริงๆ ยัดลงไปได้อีกเยอะ แต่มันจะช้าลงเรื่อยๆ จนตายไปในที่สุด)
ตัวอย่างการจัดเก็บที่ดีก็คือ Flickr ลองไปสังเกตุ Path ของรูปดูสิครับ จะเห็นได้ว่า มีการแบ่งเป็น Farm และ Sup Path ไว้อย่างดีเลยทีเดียว
ทีนี้ผมจะมาลอง algorithm ง่ายๆก่อน โดยจะใช้แค่ Dir ขวางเพียงชั้นเดียว โดยทำ Hashing ด้วย CRC32
function crc32_hash($key, $max=5000)
{
return (crc32($key) & 0x7fffffff) % $max;
}
เขียนเท่านี้ล่ะ ผมไม่ขออธิบายถึง CRC32 Algorithm นะครับ แต่ว่าเพื่อนๆ ไปหาข้อมูลเพิ่มได้จากพี่ Goo นั่นล่ะ ผมจะขอบอกแค่ว่าจาก Function ด้านบนสิ่งที่เพื่อนๆจะได้กลับมาก็คือ ข้อมูล 1 ชุดที่เป็นตัวเลข ไม่เกิน 5,000 โดยเราเอาอะไรใส่เข้าไปก็ได้ เช่น
echo crc32_hash('Tee++;');
// output 322
ตัวเลขที่ออกมานั่นล่ะครับ คือส่งที่ผมจะเอาไปสร้าง node
/main_dir/322/
เห็นมั้ยครับเท่านี้ผมก็จะได้ Dir มาขวาง 1 ชั้นแล้ว ไม่จำเป็นต้องเอาไปเก็บที่ main_dir อย่างเดียว โดยที่ Folder ในชั้นที่ขวางนี้จะมีไม่เกิน 5,000 เพราะผมตั้งเอาไว้แค่นั้น
นอกจากนี้ Algorithm ของการทำ Hashing Dir ยังมีอีกมากมายครับ ลองหาเอาจากใน Net ดูกันเอา ส่วนตอนี้ผมจะเข้าเรื่องที่ผมจะเขียนก่อนล่ะ
เรื่องที่สอง
มาเขียน lib caching เพื่อไป plug ใน CI กัน
[CI]/application/libraries/Caching.php
<?php if ( ! defined(‘BASEPATH’)) exit(‘No direct script access allowed’);
class Caching {
var $CI;
var $_parent = ‘cache/’;
var $_maxDir = 5000;
var $_path;
function __construct()
{
$this->CI =& get_instance();
$this->CI->load->helper(‘node’);
}
function setParent($parent)
{
if (substr($parent, -1) != "/")
$parent = $parent.‘/’;
$this->_parent = $parent;
}
function setMaxDir($max)
{
$this->_maxDir = $max;
}
function setDir($subDir, $str=null)
{
if (is_null($str) || empty($str)) return;
$hash = ’server_’.crc32_hash($str, $this->_maxDir).‘/’;
$this->_path = APPPATH.$this->_parent.$subDir.‘/’.$hash;
}
#### end if use key as string #####
function store($key, $data, $ttl=86400)
{
if (!$key || !$data) return;
mkdirs(dirname($this->getFileName($key)));
$h = @gzopen($this->getFileName($key), ‘w’);
if (!$h) return;
$data = serialize(array(time() + $ttl, $data));
if (gzwrite($h, $data) === false) return;
gzclose($h);
}
function fetch($key)
{
if (!$key) return;
$filename = $this->getFileName($key);
if (!file_exists($filename) || !is_writeable($filename)) return false;
$h = @gzopen($filename, ‘r’);
if (!$h) return;
while (!gzeof($h))
$data .= gzread($h, 4096);
gzclose($h);
$data = unserialize($data);
if (!$data || time() > $data[0])
{
$this->delete($key);
return false;
}
return $data[1];
}
function delete($key)
{
if (!$key) return;
$filename = $this->getFileName($key);
if (file_exists($filename))
return unlink($filename);
else
return false;
}
function getFileName($key)
{
return $this->_path.md5($key).‘.gz’;
}
}
?>
เอาล่ะ ผมจะมาอธิบายว่าในแต่ละ method ทำหน้าที่อะไรกันบ้าง
1. construct
ใช้สำหรับเรียกค่าต่างๆ ของ CI เข้ามาใช้กันและทำการ load helper node มาใช้ พอดีไอ้ function ชุดแรกผมไปเขียนใน helper ที่ชื่อว่า node น่ะครับ
2. setParnet
เป็นตัวกำหนด Dir หลักที่จะ ทำการใส่ data cache ลงไป
3. setMaxDir
เป็นตัวกำหนดว่าจะ hash dir ย่อยทั้งหมดภายใต้ main dir กี่ folder
4. setDir
เป็นตัว hash dir ย่อยภายใต้ main dir อีกที โดยจะรับค่า string เข้ามาทำการ hash
5. strore
เป็นตัวเก็บข้อมูลเพื่อทำการ Caching โดยจะระบุ key, data, expiration โดยจะ write file เป็น .gz เพื่อบีบขนาดให้เล็กที่สุด ข้อมูลภายในจะถูกเก็บเป็น array โดย
index:0 จะเก็บ current time + expire time
index:1 data ในรูปแบบ data ที่เข้ามาปกติ โดย data จะอยู่ในรูปแบบใดก็ได้ เช่น string, array, object
จากนั้นจะทำการ Write ข้อมูลแบบ PHP Serialize เพื่อแปลง Array เป็น String ชุดเดียว
6. fetch
เป็นตัวดึงข้อมูลกลับออกมาจาก cache โดยระบุ key ค้นหาเข้าไป การดึงข้อมูลกลับ จะทำการเช็คที่ index:0 ก่อนคือ Expiration ถ้าหมดอายุก็จะทำการ delete cache ทิ้งและ return false กลับมา (miss cached) ถ้าไม่ใช่ก็จะคืน data ที่ cached เข้าไปกลับมา
7. delete
เป็นตัว ลบข้อมูลของ cached ตาม key index
8. getFileName
อันนี้จะถูกใช้แค่ระหว่าง method เป็นการ hash ชื่อไฟล์ที่จะ cache ด้วย md5 โดยจะ return กลับไปพร้อมๆ กับ path
เอาล่ะๆ จบแล้วสำหรับขั้นตอนการเขียน ตอนนี้มาดูขั้นตอนการใช้งานจริงๆ กันเลยดีกว่า ส่วนมาก caching นี้ผมจะเอามา cache Query ที่อยู่ใน model เสียเป็นส่วนใหญ่คือ ไม่ให้ดึง data จาก DB ตลอดเวลา แต่ให้ดึงเอาจาก cache ในกรณีที่ data ไม่มีการเปลี่ยนแปลง
สมมุติ ผมมี model::User_model->getUserData($user_id) เพื่อที่จะดึงข้อมูลของสมาชิก แทนที่ผมจะ Query db ตรงๆ ผมก็จะเขียนแบบนี้แทน
$this->caching->setDir(‘user_dir’, $user_id)
if (!$row = $this->caching->fetch($user_id))
{
$sql = "SELECT ……….";
$row = "FETCH YOUR QUERY";
$this->caching->store($user_id, $row, 3600);
}
เป็นอันจบครับสำหรับ lib:caching ที่เขียนเองขึ้นมา ส่วนการจัดการ cache ผมจะ ไกด์คร่าวๆ ว่ามันควรจะเป็นยังไงนะครับ มันมีผังการเดินทางของมันอยู่ ตัวอย่างเช่น
1. ถ้า fetch ข้อมูล จาก key เจอ จะ ใช้ data จาก cache
2. ถ้า fetch ข้อมูล จาก key ไม่เจอ ก็จะใช้ data สดๆ แต่ก็ทำ การ store cache ไปพร้อมๆกัน
3. ถ้ามีการ update data ก็จะทำการลบ cache ตาม key เพื่อให้ข้อมูลมีความสดใหม่ และวนกลับไปยังข้อ 2
สรุป method ใน model ที่ควรจะมีความเกี่ยวเนื่องกับระบบ cache มันก็ควรจะมีพวกเรื่อง
getData, updateData
อืมม์ เรื่องการ manage cache ให้ได้ข้อมูลสดอยู่ตลอดไม่ใช่เรื่องง่ายครับ แต่ผมคิดว่ามันคงไม่ยากเกินไปหรอกครับ และที่สำคัญ ถ้าเพื่อนๆ ทำความเข้าใจกับ lib:caching ที่ผมเขียนขึ้นมาตัวนี้ได้แล้ว จะเอา method ไปแก้ไขเป็น memcached หรือ apc อะไรก็ไม่ลำบากแล้วล่ะครับ
วันนี้เนื้อหาค่อนข้างหนักพอสมควร ผมเองก็ปวดหัวละ ขอพอก่อนละกัน สวัสดีครับบ

8 comments