在 0元用 SBC 架設自己的 WordPress 網站 #3 速度優化 中提到使用 Cloudflare 做 cache,並安裝 WP Cloudflare Super Page Cache 外掛,讓他在發布新文章的時候自動做 preload。
但這個套件是透過 WordPress 主機所在位址對 Cloudflare 做 curl,因此 cache 並沒有發佈到全世界,於是我想到利用 AWS lambda 來實現。以下就來看看如何操作!
Cache Preload 原理
它的原理其實很簡單,比如說我今天新增了一個 A.html,Cloudflare 的 server 上肯定沒有這個檔案,所以當第一個使用者做 GET A.html 的時候,就會發生 cache miss,使他必須等比較久才能下載完成。
但之後的使用者再次 GET A.html 時,因為已經有 cache,速度就會大幅提升!
preload 的原理就是我們自己當作「第一個使用者」,先去對 A.html 做 GET,這樣真實使用者就能直接享用 cache 了!
只是 Cloudflare 在世界各地都有 server,因此 cache 也會有地區性。如果第一個使用者在美國,那 A.html 的 cache 只會存在美國的 server,而日本 server 依然沒有這個 cache。因此我們需要在世界各地發出一樣的 GET A.html,這樣就能把 cache 推到全世界大部分的 Cloudflare server!
Preload script 原理
其實 Cloudflare 有 prefetch 的功能,但是要付費!節省的我當然是把目光轉向每個月有免費扣打的 AWS lambda!這邊我們使用 nodejs 為例
- 讀取 sitemap.xml
以 WordPress 為例,只要安裝了 Yoast SEO,他就會幫你產生 sitemap.xml。透過 script 去抓 sitemap.xml 並解析出所有網址。
const https = require('https')
const xml2js = require('xml2js')
const hostname = '<your hostname>'
const domain = `https://${hostname}`
// Yoast SEO 會再依照類型去區分成不同的子 sitemap,因此這邊我們給定要抓的 sitemap 檔名
const sitemapPost = '/post-sitemap.xml'
const sitemapPage = '/page-sitemap.xml'
const sitemapCategory = '/category-sitemap.xml'
const sitemapPaths = [sitemapPost, sitemapPage, sitemapCategory]
// 給定偽裝成 browser 的 header
const headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36' }
// 從 sitemap 中抓 url
async function parseSitemap() {
// 一次灑出所有 sitemap 的 request
let getBodyTasks = sitemapPaths.map(path => getPageBody(path))
let results = await Promise.all(getBodyTasks)
// 對所有 sitemap 並行解析 xml
let parseTasks = results.map(r => xml2js.parseStringPromise(r))
let parseResults = await Promise.all(parseTasks)
console.log('parse sitemap finish')
// 從 xml 中抓出所有 url,因為 https 套件只需要給定不同的 path,所以這邊同時把 url 中的 domain 去除
let paths = []
for (let i in parseResults) {
for (let j in parseResults[i].urlset.url) {
paths.push(parseResults[i].urlset.url[j].loc[0].replace(domain, ''))
}
}
return paths
}
function getPageBody(path) {
return new Promise(resolve => {
let options = {
hostname: hostname,
path: path + '?preview=true',
headers: headers
}
console.log(`start get ${domain}${path}?preview=true`)
https.get(options, (res) => {
let body = ''
res.on('data', data => {
body += data
})
res.on('end', () => {
resolve(body)
})
});
})
}
- 發出 request
偽裝成 browser 對全部網址發出 GET request 。
// 傳入 paths 灑出所有 request,如果怕一次丟太多 request,可以先把原來的 paths 切成每次丟二十個進來跑
async function startPreload(paths) {
console.log('preload start')
await Promise.all(paths.map(p => startRequest(p)))
console.log('finish')
}
function startRequest(path) {
return new Promise(resolve => {
var options = {
hostname: hostname,
path: path,
headers: headers
}
var t = new Date();
https.get(options, (res) => {
let headers = res.headers
res.on('data', data => { })
res.on('end', () => {
let totalTime = (new Date() - t);
console.log(`prefetch finish => cache status: ${headers['cf-cache-status']}, age: ${headers['age']}, response time: ${totalTime}ms, url: https://${hostname}${path}`)
resolve(true)
})
});
})
}
- 部署世界各地
把 lambda 部署到 AWS 各個 AZ,最後透過 API gateway 暴露出接口統一呼叫即可。
為了錢錢著想,記得到 API gateway 的 Resource Policy 設定 ip 限制,避免別人亂呼叫浪費扣打哦!可參考官方部落格 How do I use a resource policy to allow certain IP addresses to access my API Gateway REST API?
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:<AZ>:<your_account_id>:<api_gateway_id>/*/*/*"
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:<AZ>:<your_account_id>:<api_gateway_id>/*/*/*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"xxx.xxx.xxx.xxx/32",
"xxx.xxx.xxx.xxx/32"
]
}
}
}
]
}
趕快試試看吧!
延伸閱讀:
0元用 SBC 架設自己的 WordPress 網站 #3 速度優化
使用 AWS Parameter Store 實現 Config 和 Credentials 外置