Last Updated on 2019-09-09 by OneJar
本文同步刊載於 ALPHA Camp Blog。
「為什麼網頁這麼慢!」
相信每個人都有類似經驗:遇到熱門新聞、限量特價商品、演唱會搶票,當短時間內大量流量湧入,網頁的存取常常因此失敗或異常緩慢,使用者抱怨連連。這不是因為程式功能寫錯,而是因大量運算或大流量導致系統表現不佳所產生的問題,屬於非功能性需求,也就是「效能優化」的範疇。隨著接觸的專案規模越大,除了功能實作,效能也是重要的課題。
效能優化牽涉的範疇非常廣泛,除了基本的程式面演算法效率,前後端在系統面也有很多優化議題。前端常見議題如頁面的渲染效率,後端如大量資料運算、資料庫存取、伺服器分流、分散式運算等。
其中,資料庫頻繁存取是後端很常見的效能瓶頸。對於有些不常變動、但查詢頻率很高的資料,例如熱門商品的名稱,如果每一次都要從資料庫重新 query,對資料庫來說不啻是一大負擔。
在後端效能優化中,快取(Cache)是常用的作法。透過快取來優化效能有多種實作技術,本文將介紹其中一種作法 — — Memory Cache,並透過一個實際專案來示範。
傳送門 (Table of Contents)
用記憶體幫你快取:Memory Cache
要將資料進行永續化儲存(Persistence),利用資料庫系統將資料存到硬碟裡,是一般系統不可或缺的一環。但當有大量查詢流量湧入時,容易對資料庫造成負擔,甚至導致整體系統的效能拖累。
我們都知道電腦對記憶體的操作要比對硬碟操作快得多,因此就有一種快取方式,將某些會被頻繁查詢的資料暫存在記憶體裡,當有查詢需求時,會優先去記憶體裡找,而不用每次都重新 query 資料庫,提升查詢效能。
Memory Cache 的工具有很多,常見的如 memcached、Redis。其中 memcached 是一套開放原始碼的分散式快取記憶體系統,目前被許多網站服務廣泛使用,包含 Facebook、YouTube、Twitter 等等。memcached 使用 Key-Value 的形式來儲存。
接下來會實際進行一個小專案,具體示範如何利用 memcached 提升系統的查詢效率。
示範專案:短網址系統
短網址是十分普遍的應用,功能也非常單純,很適合作為示範。
系統功能分析
一個基本的短網址系統,主要提供兩個功能:
- 讓使用者輸入想縮短的長網址,產生短網址。
- 使用者在瀏覽器輸入短網址,能導到原始網址。
實作要點
- 一個簡單的 hash 演算法,將長網址加上時間戳記,hash 成一個唯一的簡短代碼(如
Qwbrolrv
)。 - 一支 API 負責產生新的短網址,並將 hashCode 和對應的原始網址存入資料庫 (
POST /tinyUrl/
)。 - 一支 API 負責根據 hashCode 去資料庫查詢並回傳原始網址資訊(
GET /tinyUrl/{hashCode}
)。 - 一支 API,根據 hashCode 去資料庫查詢原始網址,並 redirect。
- 一個主頁面,提供使用者輸入長網址,並顯示短網址結果。
實作成果示意
以下是使用 Node.js + Express + MongoDB 所實作的成果示意 — — GMTU(Give Me Tiny URL):
使用者輸入原始網址,按下「Let’s Go!」,得到短網址:
在瀏覽器輸入短網址,會自動導到原始網址:
如何提升短網址搜尋的效能
當我們將短網址放在一篇熱門文章內,吸引到大量訪客頻繁存取這個短網址。如果每一位訪客來存取,系統都要從資料庫重新 query,無論在效率和系統負擔上都不盡理想,尤其每個短網址對應的原始網址是不變的,為了同一個結果而在短時間內對資料庫做頻繁檢索,非常浪費資源。
這時候我們就可以利用快取。
當短網址 A 第一次被訪客 1 存取,從資料庫 query 出原始網址 A 的資訊,這時候我們可以將短網址 A 和原始網址 A 當成一對 Key-Value 存入 memcached 裡。
下一次訪客 2 來存取短網址 A 時,就可以直接從 memcached 取出原始網址 A,而不用重新 query 資料庫。
概念示意圖如下:
使用 Node.js 實作 memcached 快取
要在 Node.js 上使用 memcached,有個幾個動作:
- 安裝 memcached server
- Node.js 安裝 memcached 套件
- 改寫既有程式,在 query 資料庫前先查詢 memcached
安裝 memcached
我們簡單用 localhost 開發機作為 memcached server,在上面安裝 memcached。在安裝 memcached 前,可能需要安裝幾個 dependencies。
以下是 Mac 上用 homebrew 安裝 memcached 的指令:
$ brew install autoconf automake doxygen libtool pkg-config libevent openssl libevent
$ brew install memcached
安裝完畢後需要啟動 memcached 的 service:
$ brew services start memcached
可以用以下指令檢查 memcached service 是否成功啟動:
$ ps -few | grep memcached
確定啟動後,可以用>確定啟動後,可以用 telnet 連上 memcached server 進行操作,測試是否正常。預設的 port 是 11211:
$ telnet 127.0.0.1 11211
Node.js 引用 memcached 套件
Node.js 提供操作 memcached 的套件,使用上很容易。
安裝 memcached 的套件:
$ npm install memcached
在程式中引用套件的語法:
const memcached = require(‘memcached’);
在進行任何讀取或寫入前,需要先連線到 memcached server:
let cache = new memcached("localhost:11211");
memcached 最主要的操作就是 get 和 set。
在 set 時,需要指定 Key 和 Value,以及一個 lifetime
,代表這個快取的存活時間。語法範例如下:
const memcached = require('memcached');
let key = "name";
ley value = "OneJar";
let lifetime = 60; // seconds
let cache = new memcached("localhost:11211");
cache.set(key, value, lifetime, function(err) {
if (err) {
throw err;
}
console.log('Set value ok!');
cache.end(); // close connection
});
在 get 時,指定想查詢的 Key,就能回傳對應的 Value。語法範例如下:
const memcached = require('memcached');
let key = "name";
let cache = new memcached("localhost:11211");
cache.get(key, function(err, value) {
if (err) {
throw err;
}
console.log(value);
cache.end(); // close connection
});
在既有程式增加快取機制
知道怎麼在 Node.js 裡操作 memcached 後,就來將快取機制加入程式裡。
原本查詢短網址的函數,每次都從資料庫查詢:
const getTinyUrl = (hashCode) => {
if (hashCode === null || hashCode.trim() === '') throw Error('Please specify code');
TinyUrl.findOne({ hashCode })
.then(doc => {
if (doc === null) {
throw Error('Not found');
}
return doc;
})
.catch(e => {
throw Error('Getting TinyUrl failed! Error: ' + e);
});
}
加入快取機制後,會先從快取查詢,如果沒有才會從資料庫查詢。從資料庫查詢成功後,也要記得回存快取:
const getTinyUrl = (hashCode) => {
if (hashCode === null || hashCode.trim() === '') throw Error('Please specify code');
// 先從快取 get
cacheUtil.get(hashCode, (ret)=>{
// 從快取裡得到結果,直接回傳
if(ret !== undefined){
return JSON.parse(ret);
}
// 如果快取不存在,則從資料庫查詢
TinyUrl.findOne({ hashCode })
.then(doc => {
if (doc === null) {
throw Error('Not found');
}
// 查詢成功,存入快取,以便下次使用
cacheUtil.set(hashCode, doc, lifetime, () => {
return doc;
}, (e) => {
throw Error('Set memcached failed: ' + e);
});
})
.catch(e => {
throw Error('Getting TinyUrl failed! Error: ' + e);
});
},
(e)=>{
throw Error('Get memcached failed: ' + e);
});
}
真的有提升效能嗎? — — 簡單實驗
前面提到實作要點時,有一支 API 負責根據 hashCode 回傳原始網址資訊(GET /tinyUrl/{hashCode}
)。實驗方式是另外寫一支小程式,利用 axios 套件去呼叫這支 API。
這邊可以利用 JavaScript 異步的特性輕易模擬多個 client 同時 request。
增加快取機制前
每次從資料庫重新 query,每一個 request 處理時間平均需要花 112 ms。
增加快取機制後
從 memcached 取資料,每一個 request 處理時間平均只需要花 58 ms,提升了大約 48% 的效能。
結語
本文簡單介紹了何謂 Memory Cache、memcached、如何安裝、如何在 Node.js 裡引用,並用一個短網址系統的小專案做具體的快取設計示範,在最後的實驗也成功驗證 Memory Cache 對系統效能有明顯的提升。
本文中使用 memcached 作為示範,在使用上非常容易入門。然而 memcached 有個缺點,一旦 memcached service 發生中斷,重啟後 cache 會消失。這方面可以參考另一套 Memory Cache 工具 — — Redis,也是一套 Key-Value 的快取工具,具有永續儲存的特性。
在進行快取時,快取存活時間的設計是一個重點,也就是本文範例中的 lifetime
。由於 Memory Cache 是佔用記憶體,如果把一大堆存取頻率低的資料放在快取裡霸佔記憶體資源,是另一種資源的浪費。在設計時應該根據系統實際需求,評估資料特性來設定適當的存活時間。例如以短網址系統來說,很多短網址經過一段時間後可能沒人存取,這種資料佔用 Memory Cache 太久的意義就不大。
事實上效能優化的議題五花八門,快取只是其中一種方式,在規劃適合的優化方式時需要考量實際的需求場景。隨著開發經驗累積,除了思考如何實作出能動的系統功能,也需要關注如何改善效能,才能開發出撐得住實際市場流量的服務。
註:本文中的完整範例程式碼可參考 GitHub。
References
- [不是工程師] 讓網站速度飛快的秘密,你了解什麼是網頁快取 (Cache) 嗎?
- memcached — a distributed memory object caching system
- Memcached — Wikipedia
- Memcached 教程 | 菜鸟教程
- 如何使用 memcached 做快取 | ihower { blogging }
- mac 安装 memcached — 代码界的小姑娘的博客 — CSDN博客
- mac Memcached 安装及基本命令 — 简单-生活 — CSDN 博客
- Node.js 快取處理 (1) › Leo Yeh’s Blog
- memcached — npm
- nodejs 操作 memcached — adley_app的博客 — CSDN 博客
- 資料庫的好夥伴:Redis | TechBridge 技術共筆部落格