淺談高併發-前端優化

ChenXiaoSong12321發表於2018-09-27

前言

最近接到個任務,業務場景是需要處理高併發。

原諒我第一時間想到的居然是前段時間阮一峰的部落格系統遭到了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點的大學食堂,大概都是這個原理吧。這些現實問題怎麼解決的呢,高併發是不是也可以借鑑一下呢?

  1. 洪澇災害:修固堤岸(增強伺服器效能)
  2. 早晚高峰:多選擇其他路線(分流,和分配伺服器線路),不是一定需要就避開早晚高峰(減少客戶端請求)
  3. 中午12點的大學食堂:學校多開幾個食堂(靜態資源與後端api分到不同伺服器)

回到高併發的問題上,我認為解決方案主要有這些:

  1. 靜態資源合併壓縮
  2. 減少或合併HTTP請求(需權衡,不能為了減少而減少)
  3. 使用CDN,分散伺服器壓力
  4. 利用快取過濾請求

後來發現如果要把優化做到很好,雅虎35條軍規中很多條對解決高併發也都是有效的。

回到業務

回到業務上,本次業務是助力免單。設計圖沒有幾張,擔心涉及商業資訊就不放圖了,因為要求是多頁面,我將業務分成三個頁面:

  1. 首頁,檢視活動資訊頁
  2. 檢視自己活動程式頁,包括活動結束,開始活動,活動進行中和助力失敗幾個狀態
  3. 幫助他人助力頁,包括幫他助力和自己也要助力兩個狀態

解決方案

利用快取存放資料

簡單分析了一下,需要的資料有:

{
	// 這個活動的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()
})
複製程式碼

最後總結一下,採取了的措施有:

  1. 利用快取,精簡請求
  2. 合併壓縮
  3. 靜態資源上傳cdn
  4. 避免高頻重新整理頁面獲取資料
  5. 設定響應頭cache-control和last-modified

最主要的措施大概也只有這幾個,做到的優化很少,差的也還很遠,任重而道遠,繼續努力吧。

參考:

相關文章