霜天部落 | 关注LAMP高性能、高并发架构的设计与研究

node.js技术栈学习指南

nodejs

看看谁是“赵家人”

不知啥时候,“赵家”一词在网络兴起,我一向后知后觉,对所谓的“赵家”一词并无多少了解,也没有多少了解的兴趣,但既然“赵家”一词如此红火,咱也不妨凑个热闹,粗浅的谈谈对“赵家”的看法。

☆“赵家”由来

“赵家”一词据说源自《阿Q正传》。阿Q本想和未庄的赵老太爷套近乎,楞说自己是赵老太爷本家,结果被赵老太爷一个大嘴巴抽得差点找不着北,打了阿Q一大耳刮子还嫌不过瘾,赵老太爷还顺带给阿Q扔了句 “你也配姓赵?”自此,擅长精神胜利法的阿Q倒也乐得逍遥,再也不必为自己姓啥纠结了。

看看谁是“赵家人”

☆何谓“赵家”

其实,赵老太爷的形象并不陌生,可谓一个典型的封建乡绅,抑或地方权贵的真实写照,赵老太爷在未庄有身份、有地位、有钱有势,照此推断,“赵家”应该是指封建社会地方上掌乡土权柄的乡绅。出于阶级立场,赵老太爷一度对“革命”极度反感,但是,在后来自身利益需要的时候,也装模作样的“拥护革命”,与当代中国那些一提“阶级斗争”就肾上腺素激增,恨不得以头抢地耳,但也不介意高呼“与其被革命打倒,倒不如起来领导革命”的公知颇有几分相像。

☆“赵家”情结

赵老太爷是封建乡绅势力的一个代表,是封建社会的“精英”,其“人上人”的生活是旧中国几千年封建剥削势力作威作福的一个缩影。新中国成立后,推翻了三座大山,“乡绅”也作为一个历史名词被扫进了垃圾桶。但是近几年来,一些公知就“乡绅”问题大做文章,为乡绅辩护,甚至一些公知认为“乡绅”文化是中国优秀文化传统的重要组成部分,可见,一些公知的“赵老太爷”梦从未断绝。这表明了直到今天,在21世纪的中国,依然有一些人做着鱼肉乡里、欺男霸女的美梦,延续着“赵家”情结。

“赵家”情结的死灰复燃与公知的“精英”思维密不可分。一些公知以“精英”自居,对底层百姓颐指气使,直呼“穷人就该买不起房”,就差没说“穷人就该吃不起饭”了。这种对底层百姓的赤裸裸的阶级仇恨,证明了公知早就已经站到了民众的对立面。

☆谁是“赵家”

没有人会承认自己是“赵家”,就像没有人会承认自己是“利益集团”一样。著名学者司马南曾直言不讳的指出“潘任美就是利益集团”。放大一点来说,那些在改革开放以后,通过权钱交易、官商勾结迅速积累巨额财富的人就是“利益集团”,就是不折不扣的“赵家”。

“赵家人”的一个具体表现就是仇视人民政权。一些活跃于网络上的“赵家人”和“赵家人”的乏走狗动辄高呼“宪政”、“全盘私有化”,试图从根本上颠覆中华人民共和国的政体和国体。无论他们如何美化自己,都改变不了他们“赵家人”的本质。

于右任曾在《和老友们的心里话》中说出了封建社会“乡绅”的实质。他说“我们这些人,被人称为革命元老、革命元勋,很光耀嘛。其实啊,我们老是老,老而不死是为贼嘛。这不是自贬,这是说实话。我们不就是投机革命的贼么?起初跟着季直公(张謇)搞立宪,为了什么?根子上是为了各自家族在地方上的势力,是为了向清廷索要地方的治权;后来跟着先总理(孙中山)闹革命,不过是清廷不肯放权,我们就要推翻它,找一个肯放权上来;再后来,跟着中央倒军阀,又为什么?盖我等之乡土,皆在军阀之手。不倒军阀,则家族不能施为,族人不得掌乡土之权柄也。而后辅助总统(蒋介石)杀共党,这个简单,共党分我等之田,没我等之财,夺我等之地位,不反何待……如此种种,就是我等老贼之毕生所谓,却无一处可配得上元勋二字,不过满堂守财奴罢了。”

既然是“心里话”,我相信于右任先生是真诚的,他短短的三百字道出了“赵家”的实质。联想到今天一些公知急不可耐的“推墙”,为“乡绅”辩护张目,到底谁才有“赵家”情结?到底谁才做着“赵老太爷”的春秋美梦?答案不是一目了然吗?

☆“赵家”一词兴起的背后

经过前面的分析,我们已经可以很清楚的看到到底谁才是真正的“赵家”。按理说,公知应该是“赵家”最坚定的吹鼓手才对啊,为何公知会反复对“赵家”冷嘲热讽?很显然,这就是公知惯用的贼喊捉贼老套路。公知用“赵家”指代共产党,是典型的“为了打鬼,借助钟馗”的春秋手法。由于公知心里的一些话不敢说也说不出来,只好借助“赵家”一词来含沙射影、指桑骂槐,从而达到“曲线打鬼”的目的,但公知自己也知道他们的那点小算盘是见不得人,更是不可能得逞的,所以,很大程度上,公知鼓噪“赵家”,只是为了闹鬼。

☆共产党不是“赵家”

虽然公知急不可耐的想将“赵家”与共产党联系在一起,甚至一些人在网络上用小号装神弄鬼的将“赵”中的“乂”用党旗代替,但共产党却并非传说中的“赵家”。

虽然,我们从不讳言现在的中国是中国共产党领导下的人民共和国,所有共产党员来自人民,扎根人民,服务人民,是领导我们国家社会主义建设的核心力量。人民拥护党的领导是因为中国共产党和人民利益的一致性,“三个代表”重要思想的本质就是“立党为公,执政为民”,中国共产党没有私利,全心全意为人民服务是共产党唯一的宗旨。

☆新中国不需要“赵家”

虽然一些公知极度想恢复“赵家”过去的“无上荣光”,遗憾的是,新中国不需要“赵家”,人民才是新中国的主人,在这个人民当家作主的国度里,没有“赵家人”,更没有“赵老太爷”存在的位置。

鲁迅的《阿Q正传》并没有交代赵老太爷最终的结局,但在《霸王别姬》中的袁世卿袁四爷身上,我们却看到了赵老太爷的影子。袁四爷就是《霸王别姬》中的“赵老太爷”,用段小楼的话说“无论哪朝哪代,袁四爷永远都是爷”,可是出乎段小楼意料之外的是,到了新中国,在旧社会里横行无忌的袁四爷被镇压了。这样的结局,或许鲁迅并未想到会在赵老太爷身上发生,或许他想到了,不过没说。

诚然,就和地主一样,不是每一个地主都十恶不赦的,有些甚至是大善人,但用广东省高级人民法院原院长李学先的话说:“地主里有好人,与地主阶级是好的,有因果关系吗?没有。地主作为一个阶级,代表着旧土地所有制度,是一个禁锢资本流动,抗击大工业化进程的反动的集团,从整体上讲,是必须消灭的。个人的善恶属性,与其所属阶级的进步或落后,没有必然的关系。”因此作为一个阶级,地主必须被打倒,因为穷人要翻身,就必须对地主说不。乡绅也是如此,我们承认,在旧社会无数的乡绅中,也一定有好人的存在,但是乡绅作为一个整体来说,则是欺压百姓的,是反动的,是阻碍社会进步的,一些公知今天老调重弹,意欲借为“乡绅”正名好借尸还魂,无非是想延续“赵老太爷”的梦罢了,不值一哂。

☆如何解读当今的一些“赵家”现象

不可否认,改革开放以后,随着贫富差距的逐渐加大,一些“赵老太爷”复活了,一些所谓的“官二代”、“富二代”在“赵老太爷”们的庇佑之下无恶不作,但是,与旧社会一个根本的区别在于,今天的“赵老太爷”们在作恶时是心惊胆颤的,而不是像鲁迅笔下的赵老太爷那样,恶得理直气壮。这就是区别。

“赵家”现象的客观存在,严重损害了政府公信力,也严重阻碍了和谐党群关系的构建,因此,推进全面从严治党,加强反腐肃贪力度,刻不容缓。只要让所谓的“赵家”彻底退出历史舞台,公知的“借钟馗打鬼”的美梦就注定是竹篮打水一场空。

中国共产党不是“赵家”,人民当家作主的新中国也不需要“赵家”,翻身做主人的中国老百姓更不欢迎“赵家”。即便今天的中国依然存在着各种形式的事实上的不平等,但却与所谓的“赵家”无关,只要不走老路,不走“邪路”,两极分化的难题终究会迎刃而解。相信随着新中国社会主义建设事业的不断发展,各种不平等必将逐渐消弭,那一天,也就是伟大的“中国梦”开花结果的一天。

解决ubuntu18.04启动时的报错:Failed to connect to lvmetad

解决ubuntu18.04启动时的报错:

WARNING: Failed to connect to lvmetad. Falling back to device scanning.

第一种方案:

1. 打开 /etc/lvm/lvm.conf 文件,

2. 找到 use_lvmetad = 1,

3. 修改为 use_lvmetad = 0

4. 重启系统

如果第一种方案无效,那么执行第二种方案:

1. 打开 /etc/initramfs-tools/conf.d/resume 文件

2. 修改 RESUME=none

3. 运行 sudo update-initramfs -u

4. 重启系统

nodejs真的是单线程吗?

问题由来

事件驱动、异步、单线程、非阻塞I/O,这是我们听得最多的关于nodejs的介绍,连nodejs官网都是这么写的:

Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

过去很长时间里,我都愿意接受这些“耳熟能详”的观点,直到最近,遇到过很多性能问题之后,我才开始思考,nodejs的内部机制到底是怎样的,nodejs的性能瓶颈在哪里?

#问题

  • nodejs既然是单线程,如何实现异步I/O?
  • nodejs如何实现非阻塞I/O的?
  • nodejs事件驱动是如何实现的?
  • nodejs全是异步调用和非阻塞I/O,就真的不用管并发数了吗?
  • nodejs如何靠js和操作系统打交道的?


概念

探讨上面问题之前,我们先看下这些概念是什么意思:

  • 事件驱动:
    所谓的事件驱动是对一些操作的抽象,比如 鼠标点击抽象成一个事件,收到请求抽象成一个事件,事件是对异步的一种实现。

  • 同步/异步
    所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
    当一个异步过程调用发出后,调用者不会立刻得到结果。实际处理这个调用的部件是在调用发出后,通过状态、通知来通知调用者,或通过回调函数处理这个调用。

  • 阻塞/非阻塞
    阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

注意: 很多人弄混了 同步/异步和 阻塞/非阻塞 的关系,实际上他们并不是对等的,同步不一定会阻塞,只是方法没有返回不代表线程被挂起了,实际上你也可以去做别的工作。异步也并不代表一定是非阻塞,它可以立即返回函数,但是在获取回调的时候采用了不断轮训的方式挂起了线程。

nodejs内部揭秘

要弄清楚上面的问题,首先要弄清楚nodejs是怎么工作的。

nodejs真的是单线程吗?

这张图就是nodejs的内部构造。最上面一层就是我们常用的nodejs API,都是通过js封装好的,node-bings是指对底层c/c++代码的封装后和js打交道的部分,属于交界区域,这部分大都是原生API源码调用c++的情况,用户是不需要直接使用c++模块的。
然后就是底层首先是V8引擎,这个我们非常熟悉,他就是 js 的解析引擎,它的作用就是“翻译”js给计算机看,然而我们今天关注的重点并不是V8.在这里我们也看出来node是v8的关系,v8是js解释引擎,node是js的runtime,相当于浏览器是js的runtime一样,我们接下来解释的东西大都发生在runtime上面。
libuv,早期是libev和libeio组成,后来被抽象成libuv,它就是node和操作系统打交道的部分,由它来负责文件系统、网络等等底层工作。也是我们今天重点关注对象。剩下那些这次按住不表。

libuv简介

一张图揭示了libuv在node中的作用

nodejs真的是单线程吗?

可以看出,几乎所有和操作系统打交道的部分都离不开 libuv的支持。libuv也是node实现跨操作系统的核心所在。

现在我们可以回答js是如何同底层操作系统打交道的了?
就是通过libuv,一张简化的图如下(以fs为例):

上面提到过异步和非阻塞IO的特点,那么我们看 nodejs既然是单线程,如何实现异步I/O ?
聪明的你可能马上想到了,js执行线程是单线程,把需要做的I/O交给libuv,自己马上返回做别的事情,然后libuv在指定的时刻回调就行了。其实简化的流程就是酱紫的!细化一点,nodejs会先从js代码通过node-bings调用到C/C++代码,然后通过C/C++代码封装一个叫 请求对象 的东西交给libuv,这个请求对象里面无非就是需要执行的功能+回调之类的东西,给libuv执行以及执行完实现回调。

nodejs异步模型

顺便回答了问题 nodejs真的是单线程吗?,只有js执行是单线程,I/O显然是其它线程,比如我们看到libuv起码要一个线程接受nodejs的异步请求并执行,当然远不止这样,我们后面再说。

libuv何时执行回调?

我们上面提到了libuv接过了js传递过来的 I/O请求,那么何时来处理回调呢?
有人说这还不简单,I/O完了我就回调行不行。这是极度不安全的做法,我们知道js执行是单线程的,如果两个回调同时回来,或者js线程正在工作状态,将会出现回调竞争的情况,这在一个单线程的模式下面是不应该出现的问题,所以,libuv有一个事件循环(event loop)的机制,来接受和管理回调函数的执行。

event loop是libuv的核心所在,上面我们提到 js 会把回调和任务交给libuv,libuv何时来调用回调就是 event loop 来控制的。event loop 首先会在内部维持多个事件队列(或者叫做观察者 watcher),比如 时间队列、网络队列等等,使用者可以在watcher中注册回调,当事件发生时事件转入pending状态,再下一次循环的时候按顺序取出来执行,而libuv会执行一个相当于 while true的无限循环,不断的检查各个watcher上面是否有需要处理的pending状态事件,如果有则按顺序去触发队列里面保存的事件,同时由于libuv的事件循环每次只会执行一个回调,从而避免了 竞争的发生。libuv官方的event loop执行图:

nodejs真的是单线程吗?
哪天有时间了详细讲一下这个循环的过程,也很有意思

文件I/O

上面有副图提到了libuv在nodejs中的作用,右半部分 文件I/O ,DNS 和用户的代码对应的是线程池的机制,它的执行过程大概就是:
1 js层面调用如fs.open等指令通过node-bindings转成c/c++代码。
2 把回调函数等封装成一个请求对象,如果线程池有空闲线程,交给一个线程去执行。
3 执行完成在libuv的事件循环中的文件观察中注入一个回调事件,这个事件中会向上转换成js的回调并执行。

在这里我们就看到了线程池的概念,发现nodejs并不是单线程的,而且还有并行事件发生。同时,线程池默认大小是 4 ,也就是说,同时能有4个线程去做文件i/o的工作,剩下的请求会被挂起等待直到线程池有空闲。 nodejs全是异步调用和非阻塞I/O,就真的不用管并发数了吗?得到了回答。

线程池的大小可以通过 UV_THREADPOOL_SIZE 这个环境变量来改变 或者在nodejs代码中通过 process.env.UV_THREADPOOL_SIZE来重新设置。大概的工作流程可参考下面的流程图:

nodejs真的是单线程吗?

还有深入浅出中的图也很有代表性:

网络I/O

libuv的网络I/O采用了纯事件机制,其实现是使用的操作系统底层方法,在不同的操作系统中选择了不同的解决方案,比如linux下面使用的是 epoll,在windows下面使用的是IOCP等。
以linux为例,epoll是linux下面非常高效的一种异步I/O解决方案,nginx便是采用的这种方案,通过epoll(见下图)可以实现事件通知机制,网络内核在接收到任何绑定了的事件之后都会通知绑定者,然后执行相应的代码。

nodejs真的是单线程吗?

所以我们可以理解为, js绑定事件-> libuv绑定事件 -> 网络内核监听事件. 内核事件触发 -> libuv -> js回调的过程。 所以网络I/O 并没有并发数的限制,因为它也没有线程池的概念,在一个线程中飞快的处理各种回调。

网络I/O的执行流程和文件I/O不同的地方就在于它并没有线程池,而是通过事件机制交给了操作系统去做,操作系统响应或者处理了请求就会触发libuv的callback,进而传递到nodejs执行相应的业务代码。

JS中集合对象(Array、Map、Set)及类数组对象的使用与对比

在使用js编程的时候,常常会用到集合对象,集合对象其实是一种泛型,在js中没有明确的规定其内元素的类型,但在强类型语言譬如Java中泛型强制要求指定类型。

ES6引入了iterable类型,Array,Map,Set都属于iterable类型,它们可以使用for…of循环来遍历,都内置forEach方法。

数组

遍历

普通遍历

最简单的一种,也是使用频率最高的一种。

let arr = ['a', 'b', 'c', 'd', 'e']
for (let i = 0; i < arr.length; i++) {
  console.log(i, ' => ', arr[i])
}

优化: 缓存数组长度:

let arr = ['a', 'b', 'c', 'd', 'e']
for (let i = 0, len = arr.length; i < len; i++) {
  console.log(i, ' => ', arr[i])
}

使用临时变量,将长度缓存起来,避免重复获取数组长度,当数组较大时优化效果才会比较明显。

for-in

这个循环很多人爱用,但实际上,经分析测试,在众多的循环遍历方式中它的效率是最低的。

let arr = ['a', 'b', 'c', 'd', 'e']
for (let i in arr) {
  console.log(i, ' => ', arr[i])
}
for-of

这种方式是es6里面用到的,性能要好于forin,但仍然比不上普通for循环。

let arr = ['a', 'b', 'c', 'd', 'e']
let index = 0
for (let item of arr) {
  console.log(index++, ' => ', item)
}
forEach

数组自带的foreach循环,使用频率较高,实际上性能比普通for循环弱。

let arr = ['a', 'b', 'c', 'd', 'e']
arr.forEach((v, k) => {
  console.log(k, ' => ', v)
})

forEach接受第三个参数,指向原数组,没有返回值,对其进行操作会改变原数组对象

let ary = [12, 23, 24, 42, 1]
let res = ary.forEach((item, index, input) => {
   input[index] = item * 10
})
console.log(res) //-->undefined
console.log(ary) //-->会对原来的数组产生改变

如果版本低的浏览器不兼容(IE8-),可以自定义方法实现:

/**
* forEach遍历数组
* @param callback [function] 回调函数;
* @param context [object] 上下文;
*/
Array.prototype.myForEach = function (callback,context) {   context = context || window;   if('forEach' in Array.prototype) {     this.forEach(callback,context)     return   }   // IE6-8下自己编写回调函数执行的逻辑   for(let i = 0,len = this.length; i < len; i++) {     callback && callback.call(context, this[i], i, this)   }
}
let arr = [12, 23, 24, 42, 1]
arr.myForEach((v, k) => {
  console.log(k, ' => ', v)
})
map

map会返回一个全新的数组,同样接受第三个参数,如果对其进行操作会改变原数组。

let ary = [12, 23, 24, 42, 1]
let res = ary.map((item, index, input) => {
   return item * 10
})
console.log(res) //-->[120,230,240,420,10]
console.log(ary) //-->[12,23,24,42,1]

如果版本低的浏览器不兼容(IE8-),可以自定义方法实现:

/**
* map遍历数组
* @param callback [function] 回调函数;
* @param context [object] 上下文;
*/
Array.prototype.myMap = function myMap(callback,context){   context = context || window   if('map' in Array.prototype) {     return this.map(callback, context)   }   //IE6-8下自己编写回调函数执行的逻辑
  let newAry = []   for(var i = 0,len = this.length; i < len; i++) {     if(typeof callback === 'function') {       var val = callback.call(context, this[i], i, this)       newAry[newAry.length] = val     }   }   return newAry
}
arr.myMap((v, k) => {
  console.log(k, ' => ', v)
})

过滤

filter

对数组中的每个元素都执行一次指定的函数(callback),并且创建一个新的数组,该数组元素是所有回调函数执行时返回值为 true 的原数组元素。它只对数组中的非空元素执行指定的函数,没有赋值或者已经删除的元素将被忽略,同时,新创建的数组也不会包含这些元素。

let arr = [12, 5, 8, 130, 44]
let ret = arr.filter((el, index, array) => {
  return el > 10
})
console.log(ret) // [12, 130, 44]
map

map也可以作为过滤器使用,不过返回的是对原数组每项元素进行操作变换后的数组,而不是每项元素返回为true的元素集合。

let strings = ["hello", "Array", "WORLD"]
function makeUpperCase(v) {
  return v.toUpperCase()
}
let uppers = strings.map(makeUpperCase)
console.log(uppers) // ["HELLO", "ARRAY", "WORLD"]
some

对数组中的每个元素都执行一次指定的函数(callback),直到此函数返回 true,如果发现这个元素,some 将返回 true,如果回调函数对每个元素执行后都返回 false ,some 将返回 false。它只对数组中的非空元素执行指定的函数,没有赋值或者已经删除的元素将被忽略%

如何理解stream_set_blocking的阻塞和非阻塞

伪代码如下:

<?php
$main_socket = stream_socket_server("tcp://0.0.0.0:8888", $error_code, $error_msg)  or die('create server failed');

while(1)
{
    set_error_handler(function(){}); 
    $new_socket = stream_socket_accept($main_socket, 5, $remote_address);
    restore_error_handler();

    if(!$new_socket) continue;

    //屏蔽或开启本行代码进行调试
    //stream_set_blocking($new_socket, 0);

    if(pcntl_fork() == 0)
    {
        while(1)
        {
            $pid = posix_getpid();
            $request = fread($new_socket, 8192);
            var_dump($request);
            $msg = trim($request);
            if(!empty($msg))
            {
                $response = "{$pid} | {$remote_address} | {$msg}" . PHP_EOL;
                fwrite($new_socket, $response);
            }
        }
        exit(0);
    }
}

区别就在于: 
1、当 socket处于阻塞模式时,比如:fread系统调用必须等待socket有数据返回,即进程因系统调用阻塞;相反若处于非阻塞模式,内核不管socket数据有没有准备好,都会立即返回给进程。

2、另外进程阻塞和socket阻塞不是一个概念,进程阻塞是因为系统调用所致,socket是否阻塞只是说明socket上事件是不是可以内核即刻处理。