淺談高併發-前端優化

前言

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

原諒我第一時間想到的竟然是前段時間阮一峯的博客系統遭到了DDoS攻擊,由於在個人理解中,它們的原理是想通的,都是服務器在必定時間內沒法處理全部的並行任務,致使部分請求異常,甚至會像阮一峯的博客同樣崩潰。前端

以前不太有接觸太高併發的機會,因此並無什麼實際經驗,卻是以前作的項目中有秒殺功能的實現作過必定的處理,當時的處理就是多利用緩存進行優化和減小一些不必的後端請求,可是由於是創業公司,因此並無多少過多的流量,即使是秒殺,因此也沒有進行更進一步的優化了,業務需求不須要,本身也沒有過多去思考這個問題了。node

其實剛開始我仍是有些想法,利用HTTP頭部,強緩存(cache-control)、協商緩存(last-modified和Etag)、開啓HTTP2,尤爲是HTTP2應該能將性能提高很多吧,可是這些方案大多都須要後端支持,那麼前端能作什麼呢,卻是還真沒好好思考和總結一下。webpack

理解

架構搭建以前首先要把需求理解透徹,因此去谷歌搜索了一波,首先看幾個名詞:web

  • QPS:每秒鐘請求或者查詢的數量,在互聯網領域,指每秒響應請求數(指HTTP請求)
  • 吞吐量:單位時間內處理的請求數量(一般由QPS與併發數決定)
  • 響應時間:從請求發出到收到響應花費的時間,例如系統處理一個HTTP請求須要100ms,這個100ms就是系統的響應時間
  • PV:綜合瀏覽量(Page View),即頁面瀏覽量或者點擊量,一個訪客在24小時內訪問的頁面數量,同一我的瀏覽你的網站同一頁面,只記做一次PV
  • UV:獨立訪問(UniQue Visitor),即必定時間範圍內相同訪客屢次訪問網站,只計算爲1個獨立訪客
  • 帶寬:計算帶寬大小需關注兩個指標,峯值流量和頁面的平均大小

再看幾張圖:express

正常訪問:npm

高併發:segmentfault

客戶端精簡與攔截:後端

那麼怎麼淺顯的解釋下高併發呢?把服務器比做水箱,水箱與外界鏈接換水有三根水管,正常狀況下都能正常進行換水,可是忽然一段時間大量的水須要流通,水管的壓力就承受不了了。再簡單點:洪澇災害、遲早高峯、中午12點的大學食堂,大概都是這個原理吧。這些現實問題怎麼解決的呢,高併發是否是也能夠借鑑一下呢?api

  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

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

參考:

相關文章
相關標籤/搜索