前言
最近接到個任務,業務場景是需要處理高併發。
原諒我第一時間想到的居然是前段時間阮一峰的部落格系統遭到了DDoS攻擊,因為在我的理解中,它們的原理是想通的,都是伺服器在一定時間內無法處理所有的並行任務,導致部分請求異常,甚至會像阮一峰的部落格一樣崩潰。
之前不太有接觸過高併發的機會,所以並沒有什麼實際經驗,倒是之前做的專案中有秒殺功能的實現做過一定的處理,當時的處理就是多利用快取進行優化和減少一些沒必要的後端請求,但是因為是創業公司,所以並沒有多少過多的流量,即便是秒殺,所以也沒有進行更進一步的優化了,業務需求不需要,自己也沒有過多去思考這個問題了。
其實剛開始我還是有些想法,利用HTTP頭部,強快取(cache-control)、協商快取(last-modified和Etag)、開啟HTTP2,尤其是HTTP2應該能將效能提升不少吧,但是這些方案大多都需要後端支援,那麼前端能做什麼呢,倒是還真沒好好思考和總結一下。
理解
架構搭建之前首先要把需求理解透徹,所以去谷歌搜尋了一波,首先看幾個名詞:
- QPS:每秒鐘請求或者查詢的數量,在網際網路領域,指每秒響應請求數(指HTTP請求)
- 吞吐量:單位時間內處理的請求數量(通常由QPS與併發數決定)
- 響應時間:從請求發出到收到響應花費的時間,例如系統處理一個HTTP請求需要100ms,這個100ms就是系統的響應時間
- PV:綜合瀏覽量(Page View),即頁面瀏覽量或者點選量,一個訪客在24小時內訪問的頁面數量,同一個人瀏覽你的網站同一頁面,只記作一次PV
- UV:獨立訪問(UniQue Visitor),即一定時間範圍內相同訪客多次訪問網站,只計算為1個獨立訪客
- 頻寬:計算頻寬大小需關注兩個指標,峰值流量和頁面的平均大小
再看幾張圖:
正常訪問:
高併發:
客戶端精簡與攔截:
那麼怎麼淺顯的解釋下高併發呢?把伺服器比作水箱,水箱與外界連線換水有三根水管,正常情況下都能正常進行換水,但是突然一段時間大量的水需要流通,水管的壓力就承受不了了。再簡單點:洪澇災害、早晚高峰、中午12點的大學食堂,大概都是這個原理吧。這些現實問題怎麼解決的呢,高併發是不是也可以借鑑一下呢?
- 洪澇災害:修固堤岸(增強伺服器效能)
- 早晚高峰:多選擇其他路線(分流,和分配伺服器線路),不是一定需要就避開早晚高峰(減少客戶端請求)
- 中午12點的大學食堂:學校多開幾個食堂(靜態資源與後端api分到不同伺服器)
回到高併發的問題上,我認為解決方案主要有這些:
- 靜態資源合併壓縮
- 減少或合併HTTP請求(需權衡,不能為了減少而減少)
- 使用CDN,分散伺服器壓力
- 利用快取過濾請求
後來發現如果要把優化做到很好,雅虎35條軍規中很多條對解決高併發也都是有效的。
回到業務
回到業務上,本次業務是助力免單。設計圖沒有幾張,擔心涉及商業資訊就不放圖了,因為要求是多頁面,我將業務分成三個頁面:
- 首頁,檢視活動資訊頁
- 檢視自己活動程式頁,包括活動結束,開始活動,活動進行中和助力失敗幾個狀態
- 幫助他人助力頁,包括幫他助力和自己也要助力兩個狀態
解決方案
利用快取存放資料
簡單分析了一下,需要的資料有:
{
// 這個活動的id,防止多個助力活動同時發起,本地儲存混亂的問題
id:'xxxxxxxxxx',
// 結束時間,這個時間一般是固定的,也可以放到本地儲存,不需要多次請求,過了時間可以clear這個
endTime:'xxxxxxxx',
// 需要助力的人數
needFriendsNumber:3,
// 直接購買的價格
directBuyPrice: 9.9,
// 自己的資訊,在幫助別人和發起助力時需要自己的資訊
userInfo:{
id:'xxxxxxxxx',
avatar:'xxxxxxxxx'
},
// 幫助過我的人列表,顯示幫助我的頁面需要用,根據需求看,這個列表人數不會太多,也可以放到本地儲存
helpMeList:[{
id:'xxxxxxxxx',
avatar:'xxxxxxx'
},{
id:'xxxxxxxxx',
avatar:'xxxxxxx'
}
...
],
// 幫助別人的列表,可以放到本地儲存中,在進入給別人助力時不用再發起請求,幫助過別人後加到陣列中
helpOtherList:[{
id:'xxxxxxxxx',
avatar:'xxxxxxx'
},{
id:'xxxxxxxxx',
avatar:'xxxxxxx'
}
...
]
}
複製程式碼
嗯,貌似都可以藉助本地儲存實現減少請求的目的,5M的localStrong應該也夠用。這樣算來除了助力他人和第一次獲取基本資訊還有獲取助力名單,貌似也不需要其他的額外的請求了。精簡請求這個方面目前就是這樣了,因為還沒有完全寫完,所以還有沒考慮到的就要到寫實際業務的時候碰到再處理了。
資源壓縮
壓縮資源的話webpack在build的時候已經做過了。
靜態資源上傳cdn
然後就是靜態資源上傳到七牛cdn,具體實現思路是在npm run build之後,執行額外的upload.js,伺服器部署的時候只需要部署三個html檔案就可以了。 package中:
"build": "node build/build.js && npm run upload",
複製程式碼
const qiniu = require('qiniu')
const fs = require('fs')
const path = require('path')
var rm = require('rimraf')
var config = require('../config')
const cdnConfig = require('../config/app.config').cdn
const {
ak, sk, bucket
} = cdnConfig
const mac = new qiniu.auth.digest.Mac(ak, sk)
const qiniuConfig = new qiniu.conf.Config()
qiniuConfig.zone = qiniu.zone.Zone_z2
const doUpload = (key, file) => {
const options = {
scope: bucket + ':' + key
}
const formUploader = new qiniu.form_up.FormUploader(qiniuConfig)
const putExtra = new qiniu.form_up.PutExtra()
const putPolicy = new qiniu.rs.PutPolicy(options)
const uploadToken = putPolicy.uploadToken(mac)
return new Promise((resolve, reject) => {
formUploader.putFile(uploadToken, key, file, putExtra, (err, body, info) => {
if (err) {
return reject(err)
}
if (info.statusCode === 200) {
resolve(body)
} else {
reject(body)
}
})
})
}
const publicPath = path.join(__dirname, '../dist')
// publicPath/resource/client/...
const uploadAll = (dir, prefix) => {
const files = fs.readdirSync(dir)
files.forEach(file => {
const filePath = path.join(dir, file)
const key = prefix ? `${prefix}/${file}` : file
if (fs.lstatSync(filePath).isDirectory()) {
return uploadAll(filePath, key)
}
doUpload(key, filePath)
.then(resp => {
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
})
console.log(resp)
})
.catch(err => console.error(err))
})
}
uploadAll(publicPath)
複製程式碼
拋開與網站伺服器的Http請求,第一次開啟首頁:
之後:
原理大概是這樣,效果也還是不錯,自己的伺服器只需要執行必要的介面任務就行了,不需要負責靜態資源的傳輸
避免高頻重新整理頁面
做了一個限定,5秒內重新整理頁面只獲取一次列表資料,避免高頻重新整理帶給伺服器的壓力
async init() {
try {
const store = JSON.parse(util.getStore('hopoActiveInfo'))
// 避免高頻重新整理增加伺服器壓力
if (store && (new Date() - new Date(store.getTime)) < 5000) {
this.basicInfo = store
} else {
this.basicInfo = await getActiveInfo()
this.basicInfo.getTime = new Date()
}
util.setStore(this.basicInfo, 'hopoActiveInfo')
this.btn.noPeopleAndStart.detail[0].text = `${
this.basicInfo.directBuyPrice
} 元直接購買`
this.computedStatus()
} catch (error) {
console.log(error)
}
},
複製程式碼
設定響應頭cache-control和last-modified
對於所有的資料和介面設定響應頭,利用express模擬,如果兩次請求間隔小於5秒,直接返回304,不需要伺服器進行處理
app.all('*', function(req, res, next){
res.set('Cache-Control','public,max-age=5')
if ((new Date().getTime() - req.headers['if-modified-since'] )< 5000) {
// 檢查時間戳
res.statusCode = 304
res.end()
}
else {
var time =(new Date()).getTime().toString()
res.set('Last-Modified', time)
}
next()
})
複製程式碼
最後總結一下,採取了的措施有:
- 利用快取,精簡請求
- 合併壓縮
- 靜態資源上傳cdn
- 避免高頻重新整理頁面獲取資料
- 設定響應頭cache-control和last-modified
最主要的措施大概也只有這幾個,做到的優化很少,差的也還很遠,任重而道遠,繼續努力吧。
參考: