最近接到个任务,业务场景是需要处理高并发。
原谅我第一时间想到的居然是前段时间阮一峰的博客系统遭到了DDoS攻击,因为在我的理解中,它们的原理是想通的,都是服务器在一定时间内无法处理所有的并行任务,导致部分请求异常,甚至会像阮一峰的博客一样崩溃。
之前不太有接触过高并发的机会,所以并没有什么实际经验,倒是之前做的项目中有秒杀功能的实现做过一定的处理,当时的处理就是多利用缓存进行优化和减少一些没必要的后端请求,但是因为是创业公司,所以并没有多少过多的流量,即便是秒杀,所以也没有进行更进一步的优化了,业务需求不需要,自己也没有过多去思考这个问题了。
其实刚开始我还是有些想法,利用HTTP头部,强缓存(cache-control)、协商缓存(last-modified和Etag)、开启HTTP2,尤其是HTTP2应该能将性能提升不少吧,但是这些方案大多都需要后端支持,那么前端能做什么呢,倒是还真没好好思考和总结一下。
架构搭建之前首先要把需求理解透彻,所以去谷歌搜索了一波,首先看几个名词:
再看几张图:
正常访问:
高并发:
客户端精简与拦截:
那么怎么浅显的解释下高并发呢?把服务器比作水箱,水箱与外界连接换水有三根水管,正常情况下都能正常进行换水,但是突然一段时间大量的水需要流通,水管的压力就承受不了了。再简单点:洪涝灾害、早晚高峰、中午12点的大学食堂,大概都是这个原理吧。这些现实问题怎么解决的呢,高并发是不是也可以借鉴一下呢?
回到高并发的问题上,我认为解决方案主要有这些:
后来发现如果要把优化做到很好,雅虎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,具体实现思路是在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)
}
},
对于所有的数据和接口设置响应头,利用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()
})
最后总结一下,采取了的措施有:
最主要的措施大概也只有这几个,做到的优化很少,差的也还很远,任重而道远,继续努力吧。
参考: