Nodejs通过Thrift操作hbase卡住原因分析及与javascript的垃圾回收机制的关系

在最近使用Nodejs通过Thrift操作hbase的时候写了个脚本,不断发送http请求,从而取得hbase下所需的数据,但是在run的过程中for循环并没有执行完全,在执行一部分后会卡住,就再也进不到hbase下取数据,出现socket hang up的错误,查了很多资料也没解决。当时认为是hbase的并发数问题,其并发数的限制导致了资源负载的极限,后来不断测试找到原因所在,其实与hbase处理并发的能力无关,真正的原因是jsvascript的垃圾回收机制使得资源使用达到瓶颈,下面是代码处理前与处理后的对比:

 1 var thrift = require('thrift'),
 2     HBase = require('./gen-nodejs/Hbase.js'),
 3     HBaseTypes = require('./gen-nodejs/Hbase_types.js'),
 4     connection = thrift.createConnection('localhost', 9090, {
 5         transport: thrift.TFramedTransport,
 6         protocol: thrift.TBinaryProtocol
 7     });
 8 
 9 var client = thrift.createClient(HBase,connection);
10 
11 /*router.get('/', function(req, res)
12 {
13 
14   res.render('index', { title: 'Welcome' });
15 
16 });*/
17 
18 router.get('/search', function(req, res)
19 {
20 
21     var dateTimeArray = {};
22     var valueArray = {};
23     var searchPlateBegin = null;
24     var searchPlateEnd = null;
25     var searchDetailsBegin = null;
26     var searchDetailsEnd = null;
27     var convertReverseArray = new Array();
28 }
 1 /*router.get('/', function(req, res)
 2 {
 3 
 4   res.render('index', { title: 'Welcome' });
 5 
 6 });*/
 7 
 8 router.get('/search', function(req, res)
 9 {
10 
11     var dateTimeArray = {};
12     var valueArray = {};
13     var searchPlateBegin = null;
14     var searchPlateEnd = null;
15     var searchDetailsBegin = null;
16     var searchDetailsEnd = null;
17     var convertReverseArray = new Array();
18 
19      var thrift = require('thrift'),
20     HBase = require('./gen-nodejs/Hbase.js'),
21     HBaseTypes = require('./gen-nodejs/Hbase_types.js'),
22     connection = thrift.createConnection('localhost', 9090, {
23         transport: thrift.TFramedTransport,
24         protocol: thrift.TBinaryProtocol
25     });
26 
27 var client = thrift.createClient(HBase,connection);
28 }

两段代码的唯一区别就是thrift连接hbase的几行代码是否放在了路由search下,在运行的脚本之中不断地请求该路由这就是原因所在,由于不断请求,每请求一次都会进行一次thrift连接hbase,那段代码看似很简单,其实背后的运行很复杂,包括hbase中库的添加,结构组织,内存的分配等等,这样循环下去就创建了非常多的thrift对象,由于JavaScript垃圾回收机制的延时性不可能都进行回收,这样方法对象的不断增多就会造成thrift从hbase中请求数据的阻塞,就像我们看到的那样,卡在了那里。

下面来说一下javascript的回收机制:

现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。

1.标记清除:

这是JavaScript最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”。至于怎么标记有很多种方式,比如特殊位的反转、维护一个列表等,这些并不重要,重要的是使用什么策略,原则上讲不能够释放进入环境的变量所占的内存,它们随时可能会被调用的到。

垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了,因为环境中的变量已经无法访问到这些变量了,然后垃圾回收器相会这些带有标记的变量机器所占空间。

2.引用计数:

另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

比如对象A有一个属性指向对象B,而对象B也有有一个属性指向对象A,这样相互引用

function test(){
            var a={};
            var b={};
            a.prop=b;
            b.prop=a;
        }

这样a和b的引用次数都是2,即使在test()执行完成后,两个对象都已经离开环境,在标记清除的策略下是没有问题的,离开环境的就被清除,但是在引用计数策略下不行,因为这两个对象的引用次数仍然是2,不会变成0,所以其占用空间不会被清理,如果这个函数被多次调用,这样就会不断地有空间不会被回收,造成内存泄露。

减少JavaScript中的垃圾回收:

1.数组Array优化:

将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。

2.函数function优化:

方法一般都是在初始化的时候创建,并且此后很少在运行时进行动态内存分配,这就使得导致内存垃圾产生的方法,找起来就不是那么容易了。但是从另一角度来说,这更便于我们寻找了,因为只要是动态创建方法的地方,就有可能产生内存垃圾。

setTimeout(
    (function(self) {                    
      return function () {
              self.tick();
    };
})(this), 16)

每过16毫秒调用一次this.tick(),嗯,乍一看似乎没什么问题,但是仔细一琢磨,每一次调用都返回了一个新的方法对象,这就导致了大量的方法对象垃圾!

可以将作为返回值的方法保存起来,例如:

this.tickFunc = (
    function(self) {
      return function() {
                self.tick();
      };
    }
)(this);

// in the tick() function
setTimeout(this.tickFunc, 16);

相比于每次都新建一个方法对象,这种方式在每一帧当中重用了相同的方法对象。这种方式的优势是显而易见的,而这种思想也可以应用在任何以方法为返回值或者在运行时创建方法的情况当中。