Oct 18, 2011
237 Views

Các cách tối ưu hoá cho một Website có lượng truy cập và dữ liệu lớn.

Written by

Những Website lớn khi có lượng truy cập và lượng dữ liệu lớn thường hay có hiện tượng treo Server, chêt Process PHP, SQL bị locktable, Copying to tmp table…những hiện tượng này đều do lượng dữ liệu khổng lồ, SQL và code chưa được tối ưu hóa, chưa xử lý cache(cache file, cache SQL, memcache, nén file…), hình ảnh nhiều và lớn… Mình đã sưu tầm được một vài phương pháp, các bạn cùng tham khảo:

1. Cache:

Cơ chế của một web application như sau (ở đây ta coi như nói với php): Request -(1)-> [Controller/Action -(2)-> [[Model -(3)-> DB -(4)]-> View -(5)]] -> Response 

Client sẽ gửi request tới server, thường thì chỉ là một url, lên tại bước này thì không tốn thời gian. Tiếp đó server sẽ parse request, gửi tới DB, và lấy dữ liệu từ DB trả về, parse dữ liệu và trả lại cho Browser, việc query tới DB sẽ tốn rất nhiều thời gian, thêm nữa dữ liệu response thường lớn, đây chính là nguyên nhân làm cho ứng dụng của ta chậm đi.

Tạo cache sẽ giúp cho site chạy nhanh và giảm truy cập trực tiếp vào database, với php việc tạo cache chỉ ở mức genarate ra file, thường là html. Đây là giải pháp rất hay nếu làm tốt, có thể tăng tốc độ lướt web lên hàng chục lần. Tuỳ vào mục đích của ứng dụng để cache, cũng như kết hợp các phương pháp cho hiệu quả:

a. Cache toàn bộ page:

Nguyên cả page ứng với url nhất định được lưu vào cache, các truy cập tiếp theo đến cùng url này sẽ include ngay lập tức file cache được tạo ra trước đó, do đó sẽ giảm tối đa về thời gian do server không phải xử lý gì cả (kể cả truy cập DB). Cách này đơn giản nhưng không linh hoạt, và khó thực thi vì dữ liệu trên các trang là dynamic.

b. Cache từng phần của page:

Ta sẽ chỉ cache một phần của page mà tại đó dữ liệu ít bị thay đổi. Điều này đảm bảo client sẽ nhận được dữ liệu trả về là chính xác mà không phải dữ liệu cũ từ file cache.

c. Cache SQL:

Khi cùng câu lệnh SQL được gọi đi gọi lại, thì chỉ lệnh đầu tiên được gửi đến DB server, trong các Framework đều có cache SQL.

Để bắt đầu thì ta phải tạo một thư mục cache để chứa các file cache cho ứng dụng của ta, không lên để các file cache lung tung vì để có thể cache file ta cần chmod cho thư mục phải có quyền ghi và đọc, do đó để dễ chmod và bảo mật ta cần tạo riêng thư mục cache, việc đặt cache key cũng cần chú ý để dễ phân biệt.

Cache file:

$cacheFile = ‘cache/name_file_cache.php’;
if ( (file_exists($cacheFile)) && ((fileatime($cacheFile) + 600) > time()) ){
$content = file_get_contents($cacheFile);
echo $content;
} else{
ob_start();
echo ‘<h1>Hello world to cache</h1>’;
$content = ob_get_contents();
ob_end_clean();
file_put_contents($cacheFile,$content);
echo $content;

SQL cache: 

$file = ‘sql_cache.txt’;
$expire = 86400; // 24 hours
if (file_exists($file) &&
filemtime($file) > (time() – $expire)) {
$records = unserialize(file_get_contents($file));
} else {
$link = 
MySQL_connect(‘localhost’,’username’,’password’)
or die (
MySQL_error());
MySQL_select_db(‘shop’)
or die (
MySQL_error());
/* form SQL query */
$query = “SELECT * FROM categories”;
$result = 
MySQL_query($query)
or die (
MySQL_error());
while ($record = 
MySQL_fetch_array($result) ) {
$records[] = $record;
}
$OUTPUT = serialize($records);
$fp = fopen($file,”w”);
fputs($fp, $OUTPUT);
fclose($fp);
} // end else

// Query results are in $records array
foreach ($records as $id=>$row) {
if ($row[‘category_id’] == $_REQUEST[‘category_id’]) {
// category selected – print bold
print ‘<B>’.$row[‘category_name’].'</B><br>’;
} else {
// other category – print regular
print $row[‘category_name’].'<br>’;
}
} // end foreach

Dữ liệu out put sẽ chứa kiểu và size, có dạng:

a:1:{i:0;a:6:{i:0;s:1:”1″;s:11:”category_id”;s:1:” 1″;i:1;s:9:”Computers”;s:13:”category_name”;s:9 :
“Computers” ;i:2;s:25:”Description for computers”;s:20:”category_description”
;s:25:”Description for computers”;}}

 

Mình xin bắt đầu với 1 database có bảng users với khoảng 1 triệu records

Trong sự nghiệp lập trình web của mình, hẳn không ít coder chúng ta không có những mục tiêu, hoài bão là xây dựng, thực hiện được những dự án lớn, có tầm cỡ quốc gia, quốc tế. Chẳng hạn Vật Giá, Chợ Điện Tử, Zing, Zooz… ở Việt Nam, hay lớn hơn là những YouTube, MySpace, Facebook… trên qui mô toàn cầu.

Khi nói đến những dự án lớn thì một trong những vấn đề được các coder quan tâm hàng đầu đó là hiệu suất của dự án. Một site nhỏ với qui mô vài trăm user, dung lượng database chưa đáng kể thì thời gian truy vấn, tải trang chưa phải là vấn đề bạn cần quan tâm. Nhưng theo thời gian site phát triển với tốc độ chóng mặt, chẳng mấy chốc đã có tới hàng triệu users tham gia, tỉ lệ thuận với nó là bảng users có hàng triệu records, database phình to, dung lượng có thể lên đến hàng gigabyte, hàng chục gigabyte thậm chí hàng trăm gigabyte… Lúc này, ngoài việc triển khai các kế hoạch kinh doanh, khai thác lợi nhuận (có thể để bộ phận kinh doanh lo) thì một vấn đề lớn đặt ra cho các coder chúng ta là làm sao để website với 1 database to như vậy vẫn chạy mượt mà như là database nhỏ!? Ngoài những vấn đề về đầu tư cơ sở hạ tầng khủng với server cấu hình cao, database server riêng rẽ, chuẩn hóa code ra mình mạo muội viết bài viết này để chúng ta cùng chia sẽ những kinh nghiệm, thủ thuật về tối ưu truy vấn MySQL với một database lớn.

* Thủ thuật 1: INSERT
– Ngữ cảnh: chúng ta có 2 bảng users (1 triệu records), messages (empty) với cấu trúc:
users
– user_id
– name
– money
messages
– message_id
– user_id
– subject
– body

– Yêu cầu: một ngày đẹp trời, bạn muốn gửi thông điệp đến tất cả các users có số money ít hơn 1 USD rằng: Tai khoan cua ban sap het! Hay nop them tien vao tai khoan.

– Cách làm thông thường:

$query = MySQL_query(“SELECT * FROM users WHERE money < 1”);
$subject = “Money cua ban sap het!”;
while ( $row = db_fetch_object($query) ) {

$body = $row->name .” than men! So money trong tai khoan cua ban chi con chua den 1 USD – mua duoc 1 kg rau muong luoc. Hay nop them tien vao tai khoan de giao dich khong bi gian doan.”;

MySQL_query(“INSERT INTO messages (user_id, subject, body) VALUES ($row->user_id, ‘$subject’, ‘$body’)”);
}
// Processed in 67.0436019897 sec

=> Cách làm tối ưu: dùng 1 query để giải quyết tình huống này

MySQL_query(”
INSERT INTO messages
(user_id, subject, body)
SELECT
user_id, ‘Money cua ban sap het!’, CONCAT(name, ‘ than men! So money trong tai khoan cua ban chi con chua den 1 USD – mua duoc 1 kg rau muong luoc. Hay nop them tien vao tai khoan de giao dich khong bi gian doan.’)
FROM users
WHERE money < 1
“);
// Processed in: 3.5900 sec

Kết quả: thời gian xử lí giảm xuống gần 20 lần

Thủ thuật trong trường hợp này:
– Kết hợp INSERT và SELECT để thay thế cho while của PHP.
– Dùng CONCAT để lấy name của user.

* Thủ thuật 2: UPDATE
– Ngữ cảnh: có 2 bảng users (1 triệu records), user_scores (2 triệu records)
users
– user_id
– name
– total_scores
– max_scores_can_contain
user_scores
– user_score_id
– user_id
– score_type_id
– scores

– Yêu cầu: một user sẽ được cộng thêm 1 số điểm là scores trong bảng user_scores tương ứng với mỗi score_type_id (ưu tiên theo score_type_id) mà user đang có. Nhưng tổng số scores hiện có và scores của các score_type_id này không được vượt quá con số max_scores_can_contain trong bảng users, nếu vượt quá thì chỉ lấy số scores tương ứng với tổng số scores bằng max_scores_can_contain. Sao yêu cầu loằng ngoằng vậy ta ? Chắc do nó là advanced nên mới thế .

– SQL:

// Query tat ca users, chi update nhung user co scores > 0
$query = 
MySQL_query(“SELECT * FROM user_scores WHERE scores > 0”);
while ( $row = 
MySQL_fetch_object($query) ) {

// Lay object cua user nay
$user = MySQL_fetch_object(MySQL_query(“SELECT * FROM users WHERE user_id = $row->user_id”));

// Chi cong nhung user cos total_scores < max_scores_can_contain
if ( $user->total_scores < $user->max_scores_can_contain ) {

// Bat dau kiem tra bien scores_addition se cong vao
if ( $user->total_scores + $row->scores >= $user->max_scores_can_contain ) {

// Chi cong vao de total scores = max scores can contain
$scores_addition = $user->max_scores_can_contain – $user->total_scores;
} else {

// Cong binh thuong
$scores_addition = $row->scores;
}

// Bat dau cong
MySQL_query(“UPDATE users SET total_scores = total_scores + $scores_addition WHERE user_id = $user->user_id”);
}
}
// Processed in 530.916620016 sec

==>Tối ưu:

MySQL_query(”
UPDATE users AS u
LEFT JOIN user_scores AS us
ON u.user_id = us.user_id
SET u.total_scores = u.total_scores +
(
CASE
WHEN (u.total_scores + us.scores) > u.max_scores_can_contain
THEN (u.max_scores_can_contain – u.total_scores)
ELSE us.scores
END
)
WHERE u.total_scores < u.max_scores_can_contain
AND us.scores > 0
“);
// Processed in 59.2287611961 sec

Kết quả: thời gian xử lí giảm đi gần 10 lần

Thủ thuật:
– Dùng WHEN ELSE chia case và cộng ngay trong câu truy vấn.

– Ngoài ra trong câu SQL không nên INNER JOIN quá nhiều table trong một câu truy vấn, dữ liệu rất khổng lồ, có thể truyền biến trên URL để có thể tối ưu và giảm bớt INNER JOIN trong SQL.

ví dụ thế này bạn nhé, chả hạn bạn có hai table: News và Category, nếu trên link bạn truyền biến:http://domain.com/news&idcat=123 thì trong câu SQL của bạn:

$sql=”SELECT * from news where idcat=$idcat” ;

Nhưng nếu bạn không truyền biến thì câu SQL của bạn sẽ có dạng:

$sql = “SELECT * from news INNER JOIN cat ON news.idcat=cat.idcat” ;

==> Như vậy thì với cách truyền biến bạn đã có thể giải quyết được việc kết nối INNER JOIN đó

Với vấn đề này, bạn có thể áp dụng:

Nếu hình ảnh do chính tay bạn post lên host thì nên post lên 1 host khác hoặc 1 domain phụ khác ( ví dụ như img.domain của bạn ) như thế sẽ cân bằng tải cho website.

Sau đó ta áp dụng đoạn mã để giúp cho hình ảnh được load sau nội dung:

<script type=”text/javascript” src=”http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js”></script>

<script src=”js/jquery.lazyload.js” type=”text/javascript”></script>
<script type=”text/javascript”>
$(document).ready(function()
{
$(function(){$(“img”).lazyload({ threshold : 200 });})
});
</script> 

Đa số các trình duyệt thông dụng hiện nay đều có khả năng giải nén những trang web đã được nén lại, để tiết kiệm băng thông và giúp thời gian tải website nhanh hơn. Mặc dù tính năng này đã được đưa ra một thời gian, nhưng ít ai chú ý hoặc biết đến nó. Để biết thêm về những khái niệm nén trang web và những hỗ trợ đối với nó, mời bạn tham khảo trang web này. Nhưng nói tóm lại, thì đa số các trình duyệt đề hỗ trợ chức năng này, gồm Internet Explorer, FireFox, Netscape, Opera… bạn có thể kiểm tra bằng cách vào zip_test/]trang web này.

Đối với một số hiếm hoi các trình duyệt không hỗ trợ nén trang web, thì họ chỉ nhận được trang web không bị nén. Sau đây là một số ví dụ về những trang web được nén với Gzip.

Đây là ví dụ về nén tài liệu html, file javascript và file css. Tổng cộng, trang web nhỏ hơn 75% và sẽ tải nhanh hơn 75%.
Bài hướng dẫn này sẽ hướng dẫn các bạn một cách để nén trang web với PHP. Bạn chỉ cần 5 phút để làm việc này.
Bước 1:

Bật tính năng zlib compress trong PHP. Bạn có thể làm bằng cách thực hiện một trong ba cách sau:

1. Thêm dòng sau vào file .htaccess:

php_flag zlib.output_compression On

2. Thêm hoặc thay đổi (nếu có rồi) dòng sau trong file php.ini:

zlib.output_compression = On

3. Thêm dòng sau vào dòng đầu tiên trong file php của bạn, trước bất kì một output nào:

ini_set(‘zlib_output_compression’,’On’);

Bước 2:

Nếu bạn muốn nén những file khác, như file javascript, css….:
Bạn có thể thay đổi đuôi của file thành .php để có thể output được với dạng Gzip. Ví dụ sau đây sẽ minh hoạ về cách output theo dạng Gzip với file core.js:

1. Đổi tên file core.js thành core.js.php
2. Mở file core.js.php, và thêm dòng sau vào đầu trang, sau đó lưu lại:
<?php header(“Content-type: text/javascript”)?>

3. Gọi file này trong trang của bạn nhưng sau:
<script src=”/home/core.js.php” type=”text/javascript”></script>

 

 

 

 

 

Article Tags:
· · · · · · · · · · · · · · ·
Article Categories:
Code/Web
    http://linholiver.com

    https://linholiver.com/diary/about/