技术 ·

如何快速提高项目并发率

本文转载自多个来源:

原文标题:某项目从3000并发到10W并发的优化记录打造自己的 gulp 前端自动化任务

原文作者:大萌

最近在做一个某集团一个线上直播的活动,由于时间紧迫,而且项目描述不清。所以开发和部署的时候没有做特别的优化,但是第一次测试的时候问题非常大。主要出在并发上面。这里记录一下优化记录。

背景

因为是临时通知,所以从项目开始到测试仅仅一周时间,所以就快速开发了一个版本,使用 php + mysql,部署的话非常简单的用了一台8核8G的测试服务器,16核16G的正式服务器,lamp 结构。因为给我的员工名单只有10万左右,预计分析的并发量大概有3000-5000,压力测试也没有什么问题。结果活动彩排当天,内部 app 推送、合作厂商 app 内推送、老总发了个微信朋友圈,然后并发在十分钟内过两万,然后就崩了。。。一脸懵逼。

原因

出这么回事当然要写问题报告和整改方案。查看服务器日志发现以下问题。

  • 流量问题

这个问题是最大的,半个小时之内流量达到30T。

  • CPU 问题

间歇性 cpu 达到 100%

  • 数据库问题

间歇性 cpu 达到 100%

分析

  • 流量问题

主要因为前端资源问题,静态资源没有压缩、图片太大、没有使用 CDN、没有延时加载、资源没有合并

  • CPU问题

因为当时在处理别的事情,所以并没有看到当时服务器进程信息,无法确定具体原因。但是分析是因为 apache 多核支持不好、静态资源处理性能不好、而且前端资源大多没有合并、请求数太大,而且没有控制 apache 的连接数和等待时间,导致大量并发堵塞。之后查看 apache 的 log 发下如下错误

[Wed Apr 19 14:01:34.639455 2017] [mpm_prefork:error] [pid 3649] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting

 

结合其他错误日志和访问日志基本上可以定位到问题

数据库问题

查看数据库的 log 发现主要因为是频繁写入导致。频繁写入只要因为程序中记录用户行为记录导致。

解决

  1. 静态资源压缩、合并、迁移到 cdn
  2. 前后分离、保证静态页面加载速度、数据异步加载、localstorage 缓存部分容灾数据。
  3. 后端使用 redis 缓存常用数据、日志记录加入 redis 队列定时或定量统一入库
  4. 多台服务器进行负载均衡、数据库读写分离、主从同步
  5. apache更换为nginx并升级到最新版
  6. centOs 从 6.8升级到7.3,php5.6升级到php7

实施

服务器部署,大概如下图

静态资源使用 gulp 进行文件的压缩,具体可以参见之前写文章:打造自己的 gulp 前端自动化任务

redis 队列使用实例,

$redis->sAdd('jump_list',$_GET['um']);
//队列大于100时写入文件
if($redis->ssize('jump_list')>100){
$list=$redis->smembers('jump_list');
//清空队列
$redis->delete('jump_list');
//入库
$time=time();
$ip=$_SERVER["REMOTE_ADDR"];
foreach ($list as $v) {
$data[]=[
'username'=>$v,
'addtime'=>$time,
'type'=>2,
'ip'=>$ip
];
}
$database->insert('user_log',$data);
}

 

资源压缩

这里我主要使用了 gulp-minify-css,gulp-uglify,gulp-minify-ejs 来压缩 css,js,和esj 模版。
版本控制

一般来说现在有两种方式来做静态资源版本控制,第一种处理静态资源时给静态资源文件按照文件 md5 修改一个唯一的名字,并修改 html 文件中的引用,如下:

{
    "css/a.css": "css/a-d41d8cd98f.css",
    "css/b.css": "css/b-d41d8cd98f.css",
    ...
    "js/a.js": "js/a-273c2cin3f.js",
    "js/b.js": "js/b-273c2cin3f.js"
}

 

这样服务端更新完之后客户端也会强制更新,现在大多数都是使用的这种方式。使用 gulp-rev 配合 gulp-rev-collector 即可达到这种效果,比较正式的项目推荐使用。

由于这种部署方式会产生冗余文件,所以我的博客就没有才用这种方式,而是才用追加版本号的方式来通知客户端刷新,如下:

{
"css/a.css": "css/a.css?v=d41d8cd98f",
"css/b.css": "css/b.css?v=d41d8cd98f",
...
"js/a.js": "js/a.js?v=273c2cin3f",
"js/b.js": "js/b.js?v=273c2cin3f"
}

这种方式也可以达到刷新缓存的作用,但是据说问题多多,正式项目不推荐使用。

因为静态资源放在七牛,所以我去 github 搜到了一个插件 gulp-cdn-replace 挺适合,并对插件做了一些修改,达到自己想要的效果:替换七牛链接并追加时间戳作为版本号。

把 gulp-cdn-replace/index.js 中 getNewUrl 修改为

function getNewUrl(url,type){
var basename=path.basename(url);
//添加suffix
if(option.suffix&&basename.indexOf(option.suffix)==-1){
var arr=basename.split('.');
basename=arr[0]+option.suffix+'.'+arr[1];
}
var newurl=option.root+'/'+basename;
//添加时间戳
if(basename.indexOf('?')==-1){
newurl+='?v='+new Date().getTime();
}
return newurl;
}

执行任务即可将 html 文件中中对静态资源的引用替换为 cdn 地址,并追加时间戳为版本号。
资源同步

如果每次更新都要手动上传到七牛岂不是太累,所以又去 github 搜了一下,发现一个 gulp-qndn 基本上可以满足要求,但是使用的时候发现一个问题,就是这个插件只做了上传,但是七牛是不会自动覆盖同名文件的,于是小小的改动一下,在上传之前先删除同名文件,并增加一个配置项来控制这个功能。

把 gulp-qn-dn/index.js 中 client.upload 之前增加删除操作

if(options.qn.delete){
client.delete(fileKey, function(err, result) {
if (err) {
log('Error', colors.red(new PluginError(PLUGIN_NAME, err).message));
} else {
log('Delete:', colors.green(options.qn.domain+'/'+fileKey));
}
});
}

这样每次执行 gulp 任务就可以同步到七牛。
实例

完整依赖

"devDependencies": {
"gulp": "^3.9.1",
"gulp-cdn-replace": "^0.2.0",
"gulp-minify-css": "^1.2.4",
"gulp-minify-ejs": "^1.0.3",
"gulp-qndn": "0.0.4",
"gulp-rename": "^1.2.2",
"gulp-uglify": "^1.5.3"
}

任务

var gulp = require('gulp'), //基础库
minifycss = require('gulp-minify-css'), //css压缩
uglify = require('gulp-uglify'), //js压缩 
rename = require('gulp-rename'), //文件重命名
minifyejs = require('gulp-minify-ejs'), //压缩html[esj模版]
upload = require('gulp-qndn').upload, //七牛上传
cdn = require('gulp-cdn-replace'); //替换CDN链接var qnOptions = {
accessKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
secretKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
bucket: 'hersface',
domain: 'http://xxxxxxxxxx.glb.clouddn.com',
delete:false //是否清除同名文件
};
gulp.task('script', function() {
console.log('压缩并上传js...');
return gulp.src('www/static/js/easyou.js')
.pipe(rename({ suffix: '.min' }))
.pipe(uglify())
.pipe(upload({qn: qnOptions}))
.pipe(gulp.dest('www/static/js'));
});
gulp.task('css', function() {
console.log('压缩并上传css...');
return gulp.src(['www/static/css/easyou.css','www/static/css/admin.css'])
.pipe(rename({ suffix: '.min' }))
.pipe(minifycss())
.pipe(upload({qn: qnOptions}))
.pipe(gulp.dest('www/static/css'));
});
gulp.task('html', function() {
console.log('压缩html...');
console.log('更新版本号...');
return gulp.src('view/development/*/*.html')
.pipe(cdn({suffix: '.min',root: 'http://xxxxxxxxxxxx.glb.clouddn.com'}))
.pipe(minifyejs())
.pipe(gulp.dest('view/production'))
});
gulp.task('default', ['script','css','html']); //定义默认任务

 

负载均衡因为时间紧迫,没有自己搭建请求转发服务器,而是使用某云的负载均衡。

文件同步

由于代码部署在多台服务器中,所以测试服务器要和生产服务器文件进行同步,这里使用 scp 进行文件同步。因为用到 shell 的流程就控制所以需要用到 expect 。服务器没有的需要先安装一下。

#!/usr/bin/expect -f
set password test
spawn scp -r /home/wwwroot/default root@10.24.162.xxx:/home/wwwroot
set timeout 300
expect "root@10.24.162.xxx's password:"
set timeout 300
send "$password\r"
set timeout 300
send "exit\r"
expect eof

 

结果

修改之后单台服务器压测并发能达到12000-15000左右。CPU 几乎没有负载。

参与评论