前言
盡早接觸node有很多好處,首先,由于node相對(duì)于很多web技術(shù)還比較年輕,這可以讓開(kāi)發(fā)者接觸到較多的底層細(xì)節(jié),例如http協(xié)議、進(jìn)程模型、服務(wù)模型等,這些底層原理與其他現(xiàn)有技術(shù)并無(wú)實(shí)質(zhì)性的差別。由于,node的生態(tài)尚不成熟,因此,在開(kāi)發(fā)實(shí)際的產(chǎn)品中,還是需要很多非編碼相關(guān)的工作以保證項(xiàng)目的進(jìn)展和產(chǎn)品的正常運(yùn)行等,這些工作包括工程化、架構(gòu)、容災(zāi)備份、部署、運(yùn)維等。
項(xiàng)目工程化
所謂項(xiàng)目工程化,就是項(xiàng)目的組織能力,具體包括目錄結(jié)構(gòu)、構(gòu)建工具、編碼規(guī)范和代碼審查等。
目錄結(jié)構(gòu)
node下主要存在兩類(lèi)項(xiàng)目,web應(yīng)用和模塊應(yīng)用,我們來(lái)看下邊的一個(gè)例子:

構(gòu)建工具
項(xiàng)目寫(xiě)完代碼,并測(cè)試完畢后還需要進(jìn)行合并靜態(tài)文件、壓縮文件大小、打包應(yīng)用、編譯模塊等工作。如果每次都手工完成這些重復(fù)的操作,那樣的效率就比較低下了,為了節(jié)約資源,此類(lèi)工作交給工具來(lái)完成比較合適,這樣的工具就是構(gòu)建工具。
Makefile
$ ./configure
$ make
$ make install
以靜態(tài)文件合并編譯、應(yīng)用打包、運(yùn)行測(cè)試、清理目錄、掃描代碼的Makefile文件為例:
TESTS = $(shell ls -S `find test -type f -name "*.js" -print`)
TESTTIMEOUT = 5000
MOCHA_OPTS =
REPORTER = spec
install:
@$PYTHON=`which python2.6` NODE_ENV=test npm install
test:
@NODE_ENV=test ./node_modules/mocha/bin/mocha \
--reporter $(REPORTER) \
--timeout $(TIMEOUT) \
$(MOCHA_OPTS) \
$(TESTS)
test-cov:
@$(MAKE) test REPORTER=dot
@$(MAKE) test MOCHA_OPTS='--require blanket' REPORTER=html-cov > coverage.html
@$(MAKE) test MOCHA_OPTS='--require blanket' REPORTER=travis-cov
reinstall: clean
@$(MAKE) install
clean:
@rm -rf ./node_modules
build:
@./bin/combo views .
.PHONY: test test-cov clean install reinstall
上邊這個(gè)makefile將測(cè)試、測(cè)試覆蓋率、項(xiàng)目清理、依賴(lài)安裝等整合進(jìn)了make命令
Grunt
Grunt的核心是基于插件式的項(xiàng)目構(gòu)建管理,核心插件是以grunt-contrib-開(kāi)頭的,我們可以通過(guò)安裝,grunt、 grunt-init、 grunt-cli,來(lái)使用grunt。我們看一下gruntfile.js的樣子:
module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks("grunt-contrib-jshint");
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-replace');
// Project configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
all: {
src: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
options: {
jshintrc: "jshint.json"
}
}
},
clean: ["lib"],
concat: {
htmlhint: {
src: ['src/core.js', 'src/reporter.js', 'src/htmlparser.js', 'src/rules/*.js'],
dest: 'lib/htmlhint.js'
}
},
uglify: {
htmlhint: {
options: {
banner: "/*!\r\n * HTMLHint v< = pkg.version > % % \r\n *
https://github.com/yaniswang/HTMLHint\r\n *\r\n * (c) 2013 Yanis Wang
<yanis.wang@gmail.com>.\r\n * MIT Licensed\r\n * /\n",
beautify: {
ascii_only: true
}
},
files: {
'lib/< = pkg.name >.js': ['< = concat.htmlhint.dest % % % %>']
}
}
},
replace: {
htmlhint: {
files: { 'lib/htmlhint.js': 'lib/htmlhint.js' },
options: {
prefix: '@',
variables: {
'VERSION': '< = pkg.version >' % %
}
}
}
}
});
grunt.registerTask('dev', ['jshint', 'concat']);
grunt.registerTask('default', ['jshint', 'clean', 'concat', 'uglify', 'replace']);
};
Gulp
原書(shū)中并沒(méi)有Gulp的介紹,我在此處進(jìn)行補(bǔ)充,首先也是需要用npm安裝gulp,然后,寫(xiě)gulpfile.js文件,文件樣子如下:
var gulp = require('gulp'),
compass = require('gulp-compass'),
mincss=require('gulp-minify-css');
gulp.task('css', function() {
// 編譯css
console.log("hello gulp-css");
//壓縮css
gulp.src('./sass/*.scss')
.pipe(compass({
config_file: './config.rb',
css: './public/css2',
sass: 'sass'
}))
//css壓縮
.pipe(mincss())
.pipe(gulp.dest('./public/css2'));
});
gulp.task('watch',function(){
gulp.watch('./sass/*.scss',['css'])
});
gulp.task('default', ['watch'],function() {
// 將你的默認(rèn)的任務(wù)代碼放在這
console.log("hello gulp");
gulp.src('./sass/*.scss')
.pipe(compass({
config_file: './config.rb',
css: './public/css2',
sass: 'sass'
}))
//css壓縮
.pipe(mincss())
.pipe(gulp.dest('./public/css2'));
gulp.src('./sass/billmanager/*.scss')
.pipe(compass({
config_file: './config_billmanager.rb',
css: './public/css2',
sass: 'sass'
}))
//css壓縮
.pipe(mincss())
.pipe(gulp.dest('./public/css2/billmanager'));
gulp.src('./sass/common/*.scss')
.pipe(compass({
config_file: './config_common.rb',
css: './public/css2',
sass: 'sass'
}))
//css壓縮
.pipe(mincss())
.pipe(gulp.dest('./public/css2/common'));
gulp.src('./sass/trading/*.scss')
.pipe(compass({
config_file: './config_trading.rb',
css: './public/css2',
sass: 'sass'
}))
//css壓縮
.pipe(mincss())
.pipe(gulp.dest('./public/css2/trading'));
gulp.src('./sass/wx/*.scss')
.pipe(compass({
config_file: './config_wx.rb',
css: './public/css2',
sass: 'sass'
}))
//css壓縮
.pipe(mincss())
.pipe(gulp.dest('./public/css2/wx'));
});
//gulp.src(); 該方法用于返回我們匹配的文件
通過(guò)構(gòu)建一個(gè)個(gè)的task實(shí)現(xiàn)css的壓縮、圖片壓縮、靜態(tài)文件合并等工作。
編碼規(guī)范
一般情況下,編碼規(guī)范既需要文檔式的約定,也需要在提交代碼時(shí)通過(guò)代碼強(qiáng)制檢查工具進(jìn)行檢測(cè)。這個(gè)工具可以是JSLint或者JSHint。團(tuán)隊(duì)可以約定編碼規(guī)范的詳細(xì)規(guī)則,生成一份規(guī)范文件,并寫(xiě)入.jshintrc文件幫助檢測(cè)編碼規(guī)范。
代碼審查
可以使用gitlab在企業(yè)內(nèi)部搭建代碼托管平臺(tái),這類(lèi)平臺(tái)可以實(shí)現(xiàn)代碼托管、bug跟蹤、代碼審查等功能。(類(lèi)似于github)

那么代碼審查需要審查的點(diǎn)有:功能是否正確完成、編碼風(fēng)格是否符合規(guī)范、單元測(cè)試是否有同步添加等。流程如下:

部署流程
代碼完成開(kāi)發(fā)、審查、合并之后,才會(huì)進(jìn)入部署流程。
部署環(huán)境
一個(gè)項(xiàng)目的開(kāi)發(fā)到正式發(fā)布會(huì)存在幾種環(huán)境,首先是開(kāi)發(fā)環(huán)境,然后是測(cè)試環(huán)境,也叫stage環(huán)境。接著是預(yù)發(fā)布環(huán)境,也稱(chēng)為pre-release環(huán)境,最后是生產(chǎn)環(huán)境,也叫product環(huán)境。部署流程如下:

部署操作
部署,其實(shí)就是要啟動(dòng)一個(gè)長(zhǎng)時(shí)間執(zhí)行的服務(wù)進(jìn)程,因此,需要使用nohup和&命令,以不掛斷進(jìn)程的方式執(zhí)行:nohup node app.js &。同時(shí)還要考慮項(xiàng)目停止和項(xiàng)目重啟。因此需要寫(xiě)一個(gè)bash腳本來(lái)簡(jiǎn)化操作。bash腳本的內(nèi)容通過(guò)與web應(yīng)用約定好的方式來(lái)實(shí)現(xiàn),這里所說(shuō)的約定,其實(shí)就是要解決進(jìn)程ID不容易查找的問(wèn)題。如果沒(méi)有約定,我們只能手工的ps查找了
$ ps aux | grep node
jacksontian 3618 0.0 0.0 2432768 592 s002 R+ 3:00PM 0:00.00 grep node
jacksontian 3614 0.0 0.4 3054400 32612 s000 S+ 2:59PM 0:00.69 /usr/local/bin/node/Users/jacksontian/git/h5/app.js
然后,還是手工的kill掉進(jìn)程。
這里的約定就是主進(jìn)程啟動(dòng)時(shí)將進(jìn)程ID寫(xiě)入一個(gè)pid文件中,這個(gè)文件可以存放在一個(gè)約定的路徑下,可以在node的進(jìn)程文件,如app.js中添加個(gè)小功能來(lái)寫(xiě)pid。
var fs = require('fs');
var path = require('path');
var pidfile = path.join(__dirname, 'run/app.pid');
fs.writeFileSync(pidfile, process.pid);
腳本停止或者重啟時(shí),可以kill掉進(jìn)程,然后發(fā)送SIGTERM信號(hào)給node,那么進(jìn)程在收到信號(hào)后,將會(huì)刪除app.pid文件,同時(shí)退出進(jìn)程:
process.on('SIGTERM', function () {
if (fs.existsSync(pidfile)) {
fs.unlinkSync(pidfile);
}
process.exit(0);
});
我們來(lái)看一下這個(gè)bash怎么寫(xiě):
#!/bin/sh
DIR = `pwd`
NODE = `which node`
# get action
ACTION = $1
# help
usage() {
echo "Usage: ./appctl.sh {start|stop|restart}"
exit 1;
}
get_pid() {
if [-f./ run / app.pid]; then
echo`cat ./run/app.pid`
fi
}
# start app
start() {
pid = `get_pid`
if [! -z $pid]; then
echo 'server is already running'
else
$NODE $DIR / app.js 2 >& 1 &
echo 'server is running'
fi
}
# stop app
stop() {
pid = `get_pid`
if [-z $pid]; then
echo 'server not running'
else
echo "server is stopping ..."
kill - 15 $pid
echo "server stopped !"
fi
}
restart() {
stop
sleep 0.5
echo =====
start
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
*)
usage
;;
esac
然后執(zhí)行這些命令即可:
./appctl.sh start
./appctl.sh stop
./appctl.sh restart
另外,我還提供了一種比較輕量的機(jī)制,為那些沒(méi)有在node中實(shí)現(xiàn)pid寫(xiě)入的程序使用,這樣就可以在不修改代碼的基礎(chǔ)上,完成啟動(dòng)、停止、重啟服務(wù)的bash腳本了。
結(jié)構(gòu)如下:

bash代碼如下:
//env.sh
export NODE_HOME=/home/userp/luluda
export NODE_PATH=$NODE_HOME/node_modules
export NODE_PID=$NODE_HOME/bin/node.pid
export MAIN_JS="./bin/www"
//start
#!/bin/sh
cd /home/userp/luluda/bin
. ./env.sh
#run
cd $NODE_HOME/bin
echo $NODE_PID
if [ -e $NODE_PID ]
then
echo "服務(wù)已啟動(dòng),線(xiàn)程,請(qǐng)先執(zhí)行 shutdown_node.sh 結(jié)束服務(wù)后,再啟動(dòng)!"
exit -1
fi
#run
cd $NODE_HOME
echo "Start Node.js ... ...."
nohup node $MAIN_JS \
1>>$NODE_HOME/log/node.log 2>&1 &
if [ ! -z "$NODE_PID" ]
then
echo $! > $NODE_PID
fi
echo "Node PID: "`cat $NODE_PID`
//stop
#!/bin/sh
cd /home/userp/luluda/bin
. ./env.sh
if [ -e $NODE_PID ]
then
echo "Stop Node ... ..."
echo "Killing: `cat $NODE_PID`"
kill -9 `cat $NODE_PID`
rm -r $NODE_PID
else
echo "服務(wù)尚未啟動(dòng)!"
fi
//restart
#!/bin/sh
#根據(jù)實(shí)際路徑進(jìn)行修改
/home/userp/meet_server/bin/shutdown_node.sh
sleep 10
#根據(jù)實(shí)際路徑進(jìn)行修改
/home/userp/meet_server/bin/start_node.sh
性能
提升web應(yīng)用性能的方法有好多,例如動(dòng)靜分離、多進(jìn)程架構(gòu)、分布式,但是這些都是需要進(jìn)行拆分的,因此,先說(shuō)一下拆分原則:
1.做專(zhuān)一的事
2.讓擅長(zhǎng)的工具做擅長(zhǎng)的事情
3.將模型簡(jiǎn)化
4.將風(fēng)險(xiǎn)分離
動(dòng)靜分離
node可以通過(guò)中間件的方式實(shí)現(xiàn)動(dòng)靜分離,但是,還是那個(gè)原則,讓擅長(zhǎng)的工具做擅長(zhǎng)的事情。因此,將圖片、腳本、樣式表和多媒體等靜態(tài)文件都引導(dǎo)到專(zhuān)業(yè)的靜態(tài)文件服務(wù)器上,讓node只處理動(dòng)態(tài)請(qǐng)求即可。這個(gè)過(guò)程可以使用nginx或者利用CDN來(lái)處理。

CDN會(huì)讓靜態(tài)文件與用戶(hù)盡可能靠近,同時(shí)CDN自己也有更加精確和高效的緩存機(jī)制。靜態(tài)文件請(qǐng)求分離后,對(duì)靜態(tài)請(qǐng)求使用不同的域名或多個(gè)域名還能消除掉不必要的cookie傳輸和瀏覽器對(duì)下載線(xiàn)程數(shù)的限制。另外,有些網(wǎng)頁(yè)中,動(dòng)態(tài)內(nèi)容內(nèi)還存在靜態(tài)內(nèi)容,因此,將此部分的靜態(tài)內(nèi)容再次分離還可以提高效率。(這里就要使用buffer了,因此,靜態(tài)內(nèi)容無(wú)需進(jìn)行字符串轉(zhuǎn)化,直接使用buffer二進(jìn)制就可以顯示,也就是說(shuō),直接保留buffer原始內(nèi)容即可。因此,效率又可以進(jìn)一步提升。)
啟用緩存
提升性能差不多只有兩個(gè)途徑,一是提升服務(wù)的速度,二是避免不必要的計(jì)算。避免不必要的計(jì)算使用最多的場(chǎng)景就是緩存的使用。現(xiàn)在的通常做法是使用redis作為緩存。將從數(shù)據(jù)庫(kù)中查詢(xún)出來(lái)的靜態(tài)內(nèi)容或者不變的內(nèi)容,通過(guò)redis進(jìn)行存儲(chǔ),等到下一次同樣的請(qǐng)求到來(lái)時(shí),就會(huì)優(yōu)先檢查緩存是否存在數(shù)據(jù),如果存在就命中緩存中的數(shù)據(jù),如果沒(méi)有就去db中再次請(qǐng)求,然后返回并同步緩存。
接下來(lái)的內(nèi)容是樸靈在書(shū)中,沒(méi)有寫(xiě)的,我在這里簡(jiǎn)單的補(bǔ)充一下:
redis+node的緩存小代碼
例如,我們請(qǐng)求一個(gè)api
const express = require('express')
const superagent = require('superagent')
const PORT = 3000
const app = express()
function getNumberOfRepos(req, res, next) {
const org = req.query.org
superagent.get(`https://api.github.com/orgs/${org}/repos`, (err, response) => {
if (err) throw err
var num = response.body.length
res.send(`Organization "${org}" has ${num} public repositories.`)
})
}
app.get('/repos', getNumberOfRepos)
app.listen(PORT, () => console.log(`app listen on port ${PORT}`))
因?yàn)橐酵饩W(wǎng)訪(fǎng)問(wèn),因此,速度沒(méi)有保障,同時(shí)這個(gè)api獲取的是一個(gè)靜態(tài)的結(jié)果,因此,可以使用緩存。
另外,值得注意的是,redis的流行,也是因?yàn)閞edis的簡(jiǎn)單,使用redis就像使用一般編程語(yǔ)言中的hash map一樣簡(jiǎn)單。
添加數(shù)據(jù)只需要:
client.set(‘some key’, ‘some value’);
然后再設(shè)置個(gè)過(guò)期時(shí)間,防止內(nèi)存過(guò)滿(mǎn),另外設(shè)置lru也是非常必要的。
client.setex(‘some key’, 3600, ‘some value’);
const express = require('express')
const superagent = require('superagent')
const redis_client = require('redis').createClient(6379)
const PORT = 3000
const app = express()
function getNumberOfRepos(req, res) {
const org = req.query.org
superagent.get(`https://api.github.com/orgs/${org}/repos`, (err, response) => {
if (err) throw err
var num = response.body.length
redis_client.setex(org, 10, num)
res.send(`Organization "${org}" has ${num} public repositories.`)
})
}
function cache(req, res, next) {
const org = req.query.org
redis_client.get(org, (err, data) => {
if (err) throw err
if (data != null) {
res.send(`Organization "${org}" has ${data} public repositories.`)
} else {
next()
}
})
}
app.get('/repos', cache, getNumberOfRepos)
app.listen(PORT, () => console.log(`app listen on port ${PORT}`))
redis的lru置換策略
置換緩存時(shí),記得設(shè)置置換策略。
noeviction: 不進(jìn)行置換,表示即使內(nèi)存達(dá)到上限也不進(jìn)行置換,所有能引起內(nèi)存增加的命令都會(huì)返回error
allkeys-lru: 優(yōu)先刪除掉最近最不經(jīng)常使用的key,用以保存新數(shù)據(jù)
volatile-lru: 只從設(shè)置失效(expire set)的key中選擇最近最不經(jīng)常使用的key進(jìn)行刪除,用以保存新數(shù)據(jù)
allkeys-random: 隨機(jī)從all-keys中選擇一些key進(jìn)行刪除,用以保存新數(shù)據(jù)
volatile-random: 只從設(shè)置失效(expire set)的key中,選擇一些key進(jìn)行刪除,用以保存新數(shù)據(jù)
volatile-ttl: 只從設(shè)置失效(expire set)的key中,選出存活時(shí)間(TTL)最短的key進(jìn)行刪除,用以保存新數(shù)據(jù)
redis的數(shù)據(jù)類(lèi)型
此處只說(shuō)Redis的五種數(shù)據(jù)類(lèi)型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
我們做的行情tick就是使用了list這個(gè)結(jié)構(gòu)。
多進(jìn)程架構(gòu)
首先,第九章中就已經(jīng)明白了,使用多進(jìn)程架構(gòu)可以充分利用cpu,同時(shí),因?yàn)閚ode不需要額外的容器就可以使用http服務(wù)(基于http模塊),因此,需要開(kāi)發(fā)者自己處理多進(jìn)程的管理,另外,也可以使用官方提供的cluster模塊,或者pm、forever、pm2這樣的模塊來(lái)進(jìn)行進(jìn)程的管理。詳細(xì)內(nèi)容見(jiàn)第九章。
讀寫(xiě)分離
讀寫(xiě)分離主要是對(duì)于數(shù)據(jù)庫(kù)的操作時(shí)讀寫(xiě)分離的,讀的速度要遠(yuǎn)遠(yuǎn)快于寫(xiě)的速度。(因?yàn)閷?xiě)需要鎖表,來(lái)保護(hù)數(shù)據(jù)一致性),讀寫(xiě)分離需要將數(shù)據(jù)庫(kù)進(jìn)行主從設(shè)計(jì),但是,因?yàn)槲夜緵](méi)有專(zhuān)門(mén)的運(yùn)維人員,因此,我們當(dāng)時(shí)使用的阿里的rds進(jìn)行讀寫(xiě)分離的實(shí)現(xiàn)的。
日志
為了建立健全的排查和跟蹤機(jī)制,需要為系統(tǒng)增加日志,完善的日志最能還原問(wèn)題現(xiàn)場(chǎng),好似偵探斷案的第一手線(xiàn)索。
一般情況下,如果使用nginx作為反向代理,可以通過(guò)nginx來(lái)啟用日志。
我的項(xiàng)目是使用node來(lái)自己記錄日志的。
另外,對(duì)于用戶(hù)的來(lái)訪(fǎng),還可以記錄remote-addr和response-time,來(lái)定位用戶(hù)分布情況、服務(wù)器響應(yīng)時(shí)間、響應(yīng)狀態(tài)和客戶(hù)端類(lèi)型。
日志的等級(jí)
console.log:輸出給process.stdout
console.info:輸出給process.stdout
console.warn:輸出給process.stderr
console.error:輸出給process.stderr
捕獲抓不到的異常
有些異常很是詭異,抓不到,因此,給全局設(shè)置一個(gè)uncaughtException,來(lái)抓取全局異常。
日志內(nèi)容格式化
var format = function (msg) {
var ret = '';
if (!msg) {
return ret;
}
var date = moment();
var time = date.format('YYYY-MM-DD HH:mm:ss.SSS');
if (msg instanceof Error) {
var err = {
name: msg.name,
data: msg.data
};
err.stack = msg.stack;
ret = util.format(' s s: s % % % \nHost: s % \nData: j % \n s % \n\n',
time,
err.name,
err.stack,
os.hostname(),
err.data,
time
);
console.log(ret);
} else {
ret = time + ' ' + util.format.apply(util, arguments) + '\n';
}
return ret;
}
上邊的代碼展示了如何格式化日志,以此精確定位錯(cuò)誤的發(fā)生。
日志與數(shù)據(jù)庫(kù)
日志在線(xiàn)寫(xiě),日志分析通過(guò)一些文件同步到數(shù)據(jù)庫(kù)中。
分割日志
可以按照日期分割,也可以按照日志類(lèi)型分割(_stdout和_stderr)。
監(jiān)控報(bào)警
對(duì)于新上線(xiàn)的應(yīng)用,需要兩個(gè)方面的監(jiān)控,業(yè)務(wù)邏輯的監(jiān)控和硬件型的監(jiān)控。我們來(lái)看看具體怎么做。
監(jiān)控
1.日志監(jiān)控
例如查看具體的業(yè)務(wù)實(shí)現(xiàn),通過(guò)日志時(shí)間分析,來(lái)反映某項(xiàng)業(yè)務(wù)的qps,同時(shí),在日志上也可以查詢(xún)到pv(每日ip訪(fǎng)問(wèn)或者刷新次數(shù))和uv(每日某個(gè)客戶(hù)端訪(fǎng)問(wèn)的次數(shù),不重復(fù)計(jì)算),可以通過(guò)pv和uv很好地知道使用者的習(xí)慣、預(yù)知訪(fǎng)問(wèn)高峰等。
2.響應(yīng)時(shí)間
健康的系統(tǒng)響應(yīng)時(shí)間波動(dòng)較小,持續(xù)均衡。
3.進(jìn)程監(jiān)控
檢查操作系統(tǒng)中運(yùn)行的應(yīng)用(工作)進(jìn)程數(shù),如果低于某個(gè)預(yù)估值,就應(yīng)當(dāng)發(fā)出報(bào)警。
4.磁盤(pán)監(jiān)控
監(jiān)控磁盤(pán)用量,防止因?yàn)榇疟P(pán)空間不足造成的系統(tǒng)問(wèn)題,一旦磁盤(pán)用量超過(guò)警戒值,服務(wù)器的管理者就應(yīng)該清理日志或者清理磁盤(pán)了。
5.內(nèi)存監(jiān)控
檢查是否有內(nèi)存泄漏的情況。如果內(nèi)存只升不降,那么鐵定就是內(nèi)存泄漏了。健康的內(nèi)存應(yīng)該是有升有降的。
如果進(jìn)程中存在內(nèi)存泄漏,又一時(shí)沒(méi)有排查解決,有一種方案可以解決這種情況,這種方案應(yīng)用于多進(jìn)程架構(gòu)的服務(wù)集群,讓每個(gè)工作進(jìn)程指定服務(wù)多少次請(qǐng)求,達(dá)到請(qǐng)求數(shù)之后進(jìn)程就不再服務(wù)新的鏈家,主進(jìn)程啟動(dòng)新的工作進(jìn)程來(lái)服務(wù)客戶(hù),舊的進(jìn)程等所有連接斷開(kāi)后就退出。
6.cpu占用監(jiān)控
cpu使用分為用戶(hù)態(tài)、內(nèi)核態(tài)、IOWait等,如果用戶(hù)態(tài)cpu使用率較高,說(shuō)明服務(wù)器上的應(yīng)用需要大量的cpu開(kāi)銷(xiāo),如果內(nèi)核態(tài)cpu使用率較高,說(shuō)明服務(wù)器花費(fèi)大量時(shí)間進(jìn)行進(jìn)程調(diào)度或者系統(tǒng)調(diào)用,IOWait使用率則反應(yīng)的是cpu等待磁盤(pán)IO操作。
用戶(hù)態(tài)小于70%、內(nèi)核態(tài)小于35%且整體小于70%,cpu處于健康狀態(tài)。
7.cpu load監(jiān)控
cpu load又稱(chēng)為cpu平均負(fù)載,描述操作系統(tǒng)當(dāng)前的繁忙程度,可以簡(jiǎn)單的理解為cpu在單位時(shí)間內(nèi)正在使用和等待使用cpu的平均任務(wù)數(shù)。它有三個(gè)指標(biāo),即1分鐘的平均負(fù)載、5分鐘的平均負(fù)載、15分鐘的平均負(fù)載。cpu load 高說(shuō)明進(jìn)程數(shù)量過(guò)多,這在node中可能體現(xiàn)在用子進(jìn)程模塊反復(fù)啟動(dòng)新的進(jìn)程。
8.IO負(fù)載
IO負(fù)載,主要講的是磁盤(pán)IO,對(duì)于node來(lái)說(shuō),此類(lèi)IO壓力多半來(lái)源于數(shù)據(jù)庫(kù)IO。
9.網(wǎng)絡(luò)監(jiān)控
主要監(jiān)控網(wǎng)絡(luò)流量,這個(gè)值可以查看公司的相關(guān)宣傳是否有效,廣告是否有效,是否增加了訪(fǎng)問(wèn)流量。(監(jiān)控流入流量和流出流量)
10.應(yīng)用狀態(tài)監(jiān)控
這個(gè)監(jiān)控可以通過(guò)增加時(shí)間戳來(lái)實(shí)現(xiàn):
app.use('/status', function (req, res) {
res.writeHead(200);
res.end(new Date());
})
同時(shí),對(duì)于業(yè)務(wù)相關(guān)的內(nèi)容也需要盡可能的打印出來(lái)。
11.DNS監(jiān)控
可以基于第三方的軟件進(jìn)行檢測(cè),如DNSPod等,我們用的阿里云的DNS。
報(bào)警的實(shí)現(xiàn)
有了監(jiān)控,那么就一定應(yīng)該提供報(bào)警系統(tǒng)。一般情況下,報(bào)警系統(tǒng)有:郵件報(bào)警、IM報(bào)警、短信報(bào)警、電話(huà)報(bào)警
郵件報(bào)警
基于node編寫(xiě)郵件報(bào)警系統(tǒng),可以通過(guò)nodemailer模塊來(lái)實(shí)現(xiàn),看個(gè)例子:
var nodemailer = require("nodemailer");
// 建建立一個(gè)SMTP傳輸連接
var smtpTransport = nodemailer.createTransport("SMTP", {
service: "Gmail",
auth: {
user: "gmail.user@gmail.com",
pass: "userpass"
}
});
// 郵件選項(xiàng)
var mailOptions = {
from: "Fred Foo ? <foo@bar.com>", // 發(fā)件人地址
to: "bar@bar.com, baz@bar.com", // 收件人地址
subject: "Hello ? ", // 標(biāo)題
text: "Hello world ? ", // 純文本內(nèi)容
html: "<b>Hello world ? </b>" // HTML內(nèi)容
}
// 發(fā)送郵件
smtpTransport.sendMail(mailOptions, function (err, response) {
if (err) {
console.log(err);
} else {
console.log("Message sent: " + response.message);
}
})
短信或電話(huà)報(bào)警
對(duì)接短信或者電話(huà)平臺(tái)即可。
監(jiān)控系統(tǒng)的穩(wěn)定性
對(duì),需要確保監(jiān)控系統(tǒng)的穩(wěn)定性,否則,監(jiān)控系統(tǒng)頻繁出錯(cuò),反而得不償失了。我們公司使用了阿里云提供的檢測(cè)系統(tǒng),還不錯(cuò),基本滿(mǎn)足需求。
node服務(wù)穩(wěn)定性
這本書(shū),經(jīng)常在闡述應(yīng)用的穩(wěn)定性問(wèn)題,其中第四章從單進(jìn)程角度描述了應(yīng)用的穩(wěn)定性,第九章從多進(jìn)程角度闡述了應(yīng)用的穩(wěn)定性。單獨(dú)一臺(tái)服務(wù)器滿(mǎn)足不了業(yè)務(wù)無(wú)限增長(zhǎng)的需求,這就需要將node按多進(jìn)程的方式部署到多臺(tái)機(jī)器中,這樣如果某臺(tái)機(jī)器出現(xiàn)問(wèn)題,其余機(jī)器為用戶(hù)繼續(xù)提供服務(wù)。另外,大企業(yè)也會(huì)進(jìn)行異地機(jī)房災(zāi)備和搭建就近的服務(wù)器。這就抵消了一部分因?yàn)榈乩砦恢脦?lái)的網(wǎng)絡(luò)延遲的問(wèn)題。為了更好的穩(wěn)定性,典型的水平擴(kuò)展方式就是多進(jìn)程、多機(jī)器、多機(jī)房,這樣的分布式設(shè)計(jì)在現(xiàn)在的互聯(lián)網(wǎng)公司并不少見(jiàn)。
多機(jī)器

看到上邊的示意圖,可以發(fā)現(xiàn),在多機(jī)器的情況下,需要考慮負(fù)載均衡、狀態(tài)共享和數(shù)據(jù)一致性等問(wèn)題。
多機(jī)房
這個(gè)作者在書(shū)中秋風(fēng)掃落葉的一筆帶過(guò)。我也沒(méi)有機(jī)會(huì)玩這樣的部署,因此,不懂。
容災(zāi)備份

異構(gòu)共存
node雖然神奇,但是,任何神奇的node功能,都是由操作系統(tǒng)的底層功能進(jìn)行支持的。因此,node的異構(gòu)共存,也是很簡(jiǎn)單和普遍的一件事。

如果不是標(biāo)準(zhǔn)協(xié)議,而是一個(gè)RESTful服務(wù)接口的話(huà),也完全不存在問(wèn)題,這樣做其實(shí)就是現(xiàn)在比較流行的微服務(wù)架構(gòu)的一個(gè)基礎(chǔ)設(shè)計(jì)了。
總之,使用node不存在推翻已有設(shè)計(jì)的情況,node可以通過(guò)標(biāo)準(zhǔn)協(xié)議(tcp之類(lèi))和各種系統(tǒng)、語(yǔ)言和平相處。