某一天用户反馈打开的页面白屏幕,怎么定位到产生错误的原因呢?日常某次发布怎么确定发布会没有引入bug呢?此时捕获到代码运行的bug并上报是多么的重要。
既然捕获错误并上报是日常开发中不可缺少的一环,那怎么捕获到错误呢?万能的**try…catch**
try{
throw new Error()
} catch(e) {
// handle error
}
看上去错误捕获是多么的简单,然而下面的场景下就不能捕获到了
try {
setTimeout(() => {
throw new Error('error')
})
} catch (e) {
// handle error
}
你会发现上面的例子中的错误不能正常捕获,看来错误捕获并不是这样简单**try…catch**就能搞定,当然你也可以为异步函数包裹一层**try…catch**来处理。
浏览器中,**window.onerror**来捕获你的错误
window.onerror = function (msg, url, row, col, error) {
console.log('error');
console.log({
msg, url, row, col, error
})
};
捕获到错误后就可以将错误上报,上报方式很简单,你可以通过创建简单的**img**,通过**src**指定上报的地址,当然为了避免上报发送过多的请求,可以对上报进行合并,合并上报。可以定时将数据进行上报到服务端。
但但你去看错误上报的信息的时候,你会发现一些这样的错误**Script error**
因为浏览器的同源策略,对于不同域名的错误,都抛出了**Script error**,怎么解决这个问题呢?特别是现在基本上js资源都会放在cdn上面。
解决方案
1:所有的资源都放在同一个域名下。但是这样也会存在问题是不能利用cdn的优势。
2:增加跨域资源支持,在cdn 上增加支持主域的跨域请求支持,在script 标签加**crossorigin**属性
在使用Promise过程中,如果你没有catch,那么可以这样来捕获错误
window.addEventListener("unhandledrejection", function(err, promise) {
// handle error here, for example log
});
如何在NodeJs中捕获错误
NodeJs中的错误捕获很重要,因为处理不当可能导致服务雪崩而不可用。当然了不仅仅知道如何捕获错误,更应该知道如何避免某些错误。
- 当你写一个函数的时候,你也许曾经思考过当函数执行的时候出现错误的时候,我是应该直接抛出throw,还是使用callback或者event emitter还是其它方式分发错误呢?
- 我是否应该检查参数是否是正确的类型,是不是null
- 如果参数不符合的时候,你怎么办呢?抛出错误还是通过callback等方式分发错误呢?
- 如果保存足够的错误来复原错误现场呢?
- 如果去捕获一些异常错误呢?try…catch还是domain
操作错误VS编码错误
1. 操作错误
操作错误往往发生在运行时,并非由于代码bug导致,可能是由于你的系统内存用完了或者是由于文件句柄用完了,也可能是没有网络了等等
2.编码错误
编码错误那就比较容易理解了,可能是undefined却当作函数调用,或者返回了不正确的数据类型,或者内存泄露等等
处理操作错误
- 你可以记录一下错误,然后什么都不做
- 你也可以重试,比如因为链接数据库失败了,但是重试需要限制次数
- 你也可以将错误告诉前端,稍后再试
- 也许你也可以直接处理,比如某个路径不存在,则创建该路径
处理编码错误
错误编码是不好处理的,因为是由于编码错误导致的。好的办法其实重启该进程,因为
- 你不确定某个编码错误导致的错误会不会影响其它请求,比如建立数据库链接错误由于编码错误导致不能成功,那么其它错误将导致其它的请求也不可用
- 或许在错误抛出之前进行IO操作,导致IO句柄无法关闭,这将长期占有内存,可能导致最后内存耗尽整个服务不可用。
- 上面提到的两点其实都没有解决问题根本,应该在上线前做好测试,并在上线后做好监控,一旦发生类似的错误,就应该监控报警,关注并解决问题
如何分发错误
- 在同步函数中,直接throw出错误
- 对于一些异步函数,可以将错误通过callback抛出
- async/await可以直接使用try..catch捕获错误
- EventEmitter抛出error事件
NodeJs的运维
一个NodeJs运用,仅仅从码层面是很难保证稳定运行的,还要从运维层面去保障。
多进程来管理你的应用
单进程的nodejs一旦挂了,整个服务也就不可用了,所以我萌需要多个进程来保障服务的可用,某个进程只负责处理其它进程的启动,关闭,重启。保障某个进程挂掉后能够立即重启。
可以参考TSW中多进程的设计。master负责对worker的管理,worker和master保持这心跳监测,一旦失去,就立即重启之。
domain
process.on('uncaughtException', function(err) {
console.error('Error caught in uncaughtException event:', err);
});
process.on('unhandleRejection', function(err) {
// TODO
})
上面捕获nodejs中异常的时候,可以说是很暴力。但是此时捕获到异常的时候,你已经失去了此时的上下文,这里的上下文可以说是某个请求。假如某个web服务发生了一些异常的时候,还是希望能够返回一些兜底的内容,提升用户使用体验。比如服务端渲染或者同构,即使失败了,也可以返回个静态的html,走降级方案,但是此时的上下文已经丢失了。没有办法了。
function domainMiddleware(options) {
return async function (ctx, next) {
const request = ctx.request;
const d = process.domain || domain.create();
d.request = request;
let errHandler = (err) => {
ctx.set('Content-Type', 'text/html; charset=UTF-8');
ctx.body = options.staticHtml;
};
d.on('error', errHandler);
d.add(ctx.request);
d.add(ctx.response);
try {
await next();
} catch(e) {
errHandler(e)
}
}
上面是一个简单的koa2的domain的中间件,利用domain监听error事件,每个请求的Request, Response对象在发生错误的时候,均会触发error 事件,当发生错误的时候,能够在有上下文的基础上,可以走降级方案。
如何避免内存泄露
内存泄漏很常见,特别是前端去写后端程序,闭包运用不当,循环引用等都会导致内存泄漏。
不要阻塞Event Loop的执行,特别是大循环或者IO同步操作
for ( var i = 0; i < 10000000; i++ ) { var user = {}; user.name = 'outmem'; user.pass = '123456'; user.email = 'outmem[@outmem](/user/outmem).com'; }
上面的很长的循环会导致内存泄漏,因为它是一个同步执行的代码,将在进程中执行,V8在循环结束的时候,是没办法回收循环产生的内存的,这会导致内存一直增长。还有可能原因是,这个很长的执行,阻塞了node进入下一个Event loop, 导致队列中堆积了太多等待处理已经准备好的回调,进一步加剧内存的占用。那怎么解决呢?
可以利用setTimeout将操作放在下一个loop中执行,减少长循环,同步IO对进程的阻.阻塞下一个loop 的执行,也会导致应用的性能下降
- 模块的私有变量和方法都会常驻在内存中
var leakArray = [];
exports.leak = function () {
leakArray.push("leak" + Math.random());
};
在node中require一个模块的时候,最后都是形成一个单例,也就是只要调用该函数一下,函数内存就会增长,闭包不会被回收,第二是leak方法是一个私有方法,这个方法也会一直存在内存。加入每个请求都会调用一下这个方法,那么内存一会就炸了。
这样的场景其实很常见
// main.js
function Main() {
this.greeting = 'hello world';
}
module.exports = Main;
var a = require('./main.js')();
var b = require('./main.js')();
a.greeting = 'hello a';
console.log(a.greeting); // hello a
console.log(b.greeting); // hello a
require得到是一个单例,在一个服务端中每一个请求执行的时候,操作的都是一个单例,这样每一次执行产生的变量或者属性都会一直挂在这个对象上,无法回收,占用大量内存。
其实上面可以按照下面的调用方式来调用,每次都产生一个实例,用完回收。
var a = new require('./main.js');
// TODO
有的时候很难避免一些可能产生内存泄漏的问题,可以利用vm每次调用都在一个沙箱环境下调用,用完回收调。
- 最后就是避免循环引用了,这样也会导致无法回收