使用 sed 修改博客配置项

伸手党请跳至本文最后一条命令。

sed基本用法

提到sed,我之前用的最多的用法大概就是:

1
sed 's/old-word/new-word/g' file

例如,有一篇博客的文件如下:

1
2
3
4
5
6
7
8
9
---
title: 使用 sed 修改博文配置
date: 2016-05-12 16:47
status: draft
tags: [sed, range, replace, shell, array]
---
博文开始
...

每次上线的时候,都要将上面文件内容的status选项由draft修改为public:

1
sed 's/^status:.*/status: public/' blog.md

使用shell的模板字符串

但是当我们需要修改时间的时候,又要怎么做呢?
这个时候另一个命令行工具就需要登场了:date

例如,需要按照上面文件内容中的时间格式,输出当前的时间,可以这样搞:

1
date +'%Y-%m-%d %H:%M'

把上面的执行结果放入到我们的sed参数中,修改时间的脚本就成了。
模板字符串使用双引号,这一点和 php 非常相似。

1
sed "s/^date:.*/date: $(date +'%Y-%m-%d %H:%M')/" blog.md

到这里,我们要替换内容的功能,基本上都已经实现了。如果我是一个不严肃、懒惰的工程师,下面的内容可能就不会存在了。

查找边界

上面的脚本里存在的问题在于,如果你的博客的内容中出现了^update:.*^date:.*可以匹配到的行,内容就会被错误的修改。例如本篇博客就已经存在这种情况了。

1
2
3
4
5
6
7
8
9
10
11
12
---
title: 使用 sed 修改博文配置
date: 2016-05-12 16:47:01
status: draft
tags: [sed, range, replace, shell, array]
---
博文开始
update: xxxx-xx-xx xx:xx
status: test
...

实际上,这里只需要修改两个---之间的内容。
那么问题就变成了:「如何找出前两个---的行号?」这地方我绕了一个弯路,下面会解释。
查找行号的方法:

1
grep -n '^---\s*$' blog.md

输出的文本虽然对人可读,但是无法直接用它来编程,所以需要处理一下:

1
2
3
4
5
head_range_string=$(
grep -n '^---\s*$' blog.md |\
head -n 2 |\
awk -F ':' '{print $1}'
)

处理之后,变量中就只剩下了对我们有用的字符串了。但是字符串毕竟是一个变量,想从里面获取到那个数字是开始行号,那个数字是结束行号,还是需要再做一次处理:

1
2
3
head_range_array=($head_range_string)
head_start=${head_range_array[0]}
head_end=${head_range_array[1]}

sed的命令中标记边界

把得到的变量放到sed的脚本里,边界的问题就可以解决了:

1
2
sed -i '' "${head_start},${head_end} s/^status:.*/status: public/" blog.md
sed -i '' "${head_start},${head_end} s/^date:.*/date: $(date +'%Y-%m-%d %H:%M')/" blog.md

-i参数的作用是在原文件操作,结果不输出到终端。)

sed批处理

上面的脚本中,调用了两次sed,实际上会有 4 次 IO(两次读文件,两次写文件)。加入我们要修改的配置项更多,调用次数也会更多。
实际上,sed是支持再一次执行中,处理多条命令(command)的。修改后的版本:

1
sed -i '' "${head_start},${head_end} { s/^date:.*/date: $(date +'%Y-%m-%d %H:%M')/; s/^status:.*/status: publish/; }" blog.md

sed自己查找边界

上面说饶了一点弯路,实际上我们其实不需要用grep+head+tail+awk+array的组合来查找边界,sed自己就可以根据正则来匹配:

1
sed -i '' "/^---/,/^---/ { s/^date:.*/date: $(date +'%Y-%m-%d %H:%M')/; s/^status:.*/status: publish/; }" blog.md

所以刚才写了这么多,最后只用这一条命令就够了。

为什么全栈JavaScript经常被黑

知乎链接

作为一个后端工程师,在前端TC晋升到P6,也想来凑个热热闹。而且我们组的同学,也全部是非常靠谱的全栈,招聘要求也是按照全栈的标准来,比较严格的。

内容预警,写的东西比较尖锐或者刻薄,但我认为是比较客观的,所以敢冒着得罪人的风险把这些写下来。

  • 先来回答“为什么全栈会被黑?”
    很简单,因为大多数的全栈都很菜。菜到什么程度这里也不用表述了,估计在大家的认知里,这些都可以脑补。有的连JS都没摸熟,做了一个前后端的WEB项目,写了前后端的代码,就在简历上自称全栈了。
    这些“菜”的工程师直接拉低了全栈工程师的口碑,以至于每次我在别人面前提到我们团队的工作职责时,我都要特别强调好几遍“是真全栈,不要怀疑。”
    但是我要强调的是,“大多数”不代表“全部”,和我一起工作的同事,都是按照非常严格的要求筛选出来的,他们是“真正的全栈工程师”。
  • 那么问题就变成了“为什么大多数全栈都很菜?”
    这个问题也很简单,就两个原因:
    1. 没机会学习全栈的所有技能。那我们再问为什么没机会?
      1. 即使是打着“招聘全栈工程师”的旗号的一些公司,他们把人招过来之后也是把全栈当前端用。
      2. 即便公司想用,但是他们的技术负责人也不一定不会用这种技能的人,于是给他们分配的任务还只是前端的,接触不到其他的全栈技能。
    2. 不知道该怎么学习全栈技能。那我们再问为什么不知道该怎么学?
      1. 上面也说了,leader不会用人,不会带人,而leader又是团队的榜样,leader都弄不明白的事情,其他人弄明白了,其他人不成leader了吗?这种现象不太可能发生。
      2. 自身动力不足,或者学习能力不够。总之自身因素也是有的。
  • 那么,上边提到的对全栈的要求是什么呢?
    我的答案要比列举的技能树要简单的多:熟悉Linux、熟悉HTTP、熟悉浏览器提供的各种API,并能活用以上三种技能,完了。
  • 为什么会有这种要求?
    • Linux,学习的再多也不为过。原因:
    1. 在一个全栈的团队里面,负责和专业运维沟通的人,必定也是全栈。
    2. 在一个全栈的团队里面,负责后端架构、性能优化、高可用、负载均衡设计的人,必定也是全栈。而如果熟悉Linux,对这部分工作内容会有非常大的帮助。有些需要自己解决的问题,可能用Linux中的一些既有功能就可以简单暴力的解决,虽然很暴力但是很有效。
    3. 以上都是讲对后端的作用,其实前端也是离不开Linux的,虽然大部分的前端不用Linux。举一个例子,大家都在用fs.watch()的API,或者nodemon、webpack或者其他构建工具的文件监视工具,但是有几个前端知道用shell怎么实现这种文件监视的功能?那我要反问一句,连文件监视功能是怎么实现的都不知道,怎么敢自称“工程师”?或者“前端工程师”?
    • HTTP很重要,原因大家都知道的,我就不多说了。但是我要强调的是,学HTTP,对于学习的人来讲,价值更大的是,学习学习协议的方法。
      就个人的经历而言,除HTTP协议之外,先后学习过engine.io、kafka、redis、amqp、mqtt的协议,其中kafka、redis中大部分的协议内容部是 request-response 的(kafka全部都是,redis中pub-sub不是,答主是kafka-node的作者之一,同时也实现过redis文本协议的parser)。所以如果学习过HTTP,对于学习其他的协议是很有帮助的。
    • 浏览器提供的各种API,这个技能不光是全栈的,而是所有前端应该去学习的,原因我也不赘述。这里说的API,不光是HTML、CSS,存储、网络、传感器、绘图等都应该是前端的技能范围。面试的时候我会想起来什么问什么。但我不会根据这个来判断被面试者的能力。因为下面一点更重要。
    • “活用以上三种技能”。我再装个大13,前后端分离的项目,大家会怎么定义后端接口的地址?不许修改HOST,不许安装浏览器插件。就这样一个简单的问题,就可能涉及到Linux环境变量或localStorage的问题。这才是全栈最核心的技能,既要善学,也要善用
  • 为什么不需要太了解编程语言?
    语言就是工具,而对于能活学善用的人,你需要告诉他工具怎么用吗?如果不用的话,何必要问呢?我面试过一个应届生,ES6中的generator的API,他根本不了解,但是我告诉他API之后,他就能很快的写出类似co的功能。
  • 为什么不需要太了解数据库?
    看到评论中有朋友说全栈技能应该包括数据库、神经网络等,我觉得这不是必选项。有的项目用mongodb有的用rethinkdb,数据库在这些项目里就是一个框,在外面看来就是能存能查,保证可扩展和高可用就行,框内是什么大部分人应该都不清楚。有个数据库用blockchain的原理,可以实现百万的并发写入,你会去看blockchain是什么吗?那还装这个13干什么。
    至于机器学习,额,大家都懂。对于有兴趣的人,会就会了,不值得炫耀,多在上面花点儿时间罢了。但是多数时候用不上。
  • 最后,我要得罪一下另一类人群了:写Java的人就没有菜的吗?
    有,当然有,比写JS的菜的更多。甚至是我列出来的三种基本技能,一种都不能达标的大有人在

最后,还是希望励志成为“全栈工程师”的同学们不要放弃,坚持自己的学习道路。

FEDAY 2016 参会总结

本来只想写一个“正面反应版”。但是有问题不问,有不足不说,会议组织方也很高兴,明年照样收费办个场,内容照样水,组织照样混乱,那这种会就成了骗钱的了。所以写着写着就成“客观评价版”了。

PPT链接

嘉宾

参加这种会议,讲师的名字一定要记住。这次令人印象深刻的嘉宾阵容包括:

  • 周裕波(会议负责人)
    这次会议由很多失误,这些失误与会议负责人是有直接关系的。
  • 高博(翻译官)
    首先必须要给高博点个赞,尤其是在最后一场讲“HTTP/2”的时候,有很多大段翻译,有的时候一段翻译跨越了5页(或更多,没仔细数)PPT。但是其实我认为,如果没有翻译,演讲的效果可能更连贯,更利于“听得懂英语”的同学理解。
  • 江剑锋(讲师,来自微信,分享主题:微信Web APP开发最佳实践)
    这次分享最有价值的分享内容,我认为必须要给这位来自微信的同学。一句话评价分享内容:干货十足,数据价值爆表,演讲风格幽默。
  • Stepan(讲师,来自facebook,分享主题:使用React、Redux和Node.js构建通用应用)
    分享内容没什么含金量。但是作为“会讲中文”的外宾,还是给大家带来了不少段子。我对他印象深刻的理由也就仅限于此了。
  • 黄士旗(讲师,来自facebook,分享主题:React tips)
    我看到这个标题的时候,就觉得含金量不高。但是有一部分内容的思想是非常值得借鉴的,例如HOC,虽然简单,讲出来概念大家都明白,“不就是一种代码组织方式吗?”,但是只有极少数人把这个概念用好。

分享内容精编

取其精华,去其不精华(糟粕?我还是委婉一些吧)。

微信Web APP开发最佳实践

  • Android端,微信用户中,数量排前5的设备全是小米和三星。
    (讲师:有些大厂一定表示不服,但是不服我们也没有什么办法,统计到的数据就是这个样子。)
    其他数据要看PPT吧。
  • 吐槽了很多X5内核的BUG。BUG太多了,这部分回头看PPT就行。
    QA环节有人问为什么吴亦凡的活动,视频可以自动播放,有没有什么技巧可以绕过。讲师回答:其实没有技巧,其实就是白名单(台下笑)。
  • X5 BUG那么多,最后讲师给出了一个终极解决方案:微信将在4月初升级X5,内核由webkit改为blink,很多BUG就都不存在了。目前还在灰度放量中。
    (讲师:如果有人说他有4-5年微信APP开发经验,那他一定是再骗你。因为从4月初,以前的经验都没用了。)

由X5升级引发的一些感想:要做有前途的前端,真的不能把兼容性当成一个研究方向。要不然在别人学习、接受新东西的时候,你只会在原地踏步。

React tips

关键词:HOC(Higher-order Component)

变种:HOF(Higher-order Function)、HOC(Higher-order Class)

用最简洁的代码示范一下,我们以前的组件代码组织方式大概是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
function render() {
var input = createInput()
var checkbox = createCheckbox(input)
var buttonDelete = createButtonDelete(input)
return (
<div>
{checkbox}
{input}
{buttonDelete}
</div>
)
}

而HOC的姿势是这个样子的:

1
2
3
4
5
6
7
8
9
10
function render() {
var input = createInput()
input = makeCheckable(input)
input = makeDeletable(input)
return (
<div>
{input}
</div>
)
}

初看不就是一种“代码组织方式吗?”,但是引起我注意的是,又有几个人会想到用这种组织方式?或者用过?用好?

这种组织方式是高度可扩展的,我这里只是自己写了一个示例,但是用到工程中之后,带来的生产力收益会非常大。

会议组织失误

  • Winter到了会场才写PPT。
  • 投影设备频出故障。讲师演示的时候也很尴尬。而且一直到下午最后一场,还会出问题,不能马上解决。
  • 午餐组织地相当失误。本来没有午餐,但是可能是因为收的门票钱有结余,所以主办方给大家加了午餐。但是太混乱了:
    • 6个人的桌,我们坐了5个人,服务员非要再拉3个不认识的人过来,这就一桌8认了。一个桌2个火锅,搞得我们这边坐在另一个火锅前的同学非常尴尬,靠着别人的锅,但是要到我们这边来夹菜。
    • 服务员要求参会嘉宾把盘子里的吃完了再去拿菜。
    • 纸巾需要收费。很少有见到纸巾收费的饭店。
  • 领发票时场面会乱,无组织无纪律。而且,要发票还需要单独收钱!!!我排了好久的队,结果告诉我没有我们的发票!

会后收获

会议结束之后,与守强、张虓一起吃了晚饭。然后我提出来(也是咨询前辈)几个问题:

技术团队应该花多少资源在基础技术建设上?

说几个现状:

  • 某兄弟团队在基础技术建设上的投入是0。
  • 我们业务开发和基础技术上的投入比大概是8:2。
  • 腾讯在技术上,对外是基本上没有输出的。
  • facebook有1000名工程师在维护react。

对比之后,我个人的结论是:业务开发团队,在基础技术建设上的投入应该小于2/10,尤其是小型团队,更是如此。但是当团队到达一定规模时,这个投入是免不了的,就算不输出(比如腾讯),也要做投入一些资源做基础技术为业务服务。

技术团队的梯度该如何设置?

我了解到的一些部门的情况:

  • 杨自强老师在高工培训的课上讲过的,理想情况下,团队梯度P7:P6:P5:P4=1:2:2:2。依据是P7要独当两面,两个P6一人帮他挡一面,然后每个P6带一个P5和P4。这是理想情况。
  • 我们团队,一共5个RD(不算锋哥),1 P6,2 P5,2 P4。之前我还跟锋哥讨论过,以为和理想情况偏差的已经很大了。
  • 某兄弟团队,一共20个人,有2 P6,10+ P5。
  • 某兄弟团队,一共20个人,其中只有一个P6。

从上面的梯度数据,我个人的结论就不讲了。但是反映出来的一个问题是,我们的业务开发部门,在梯度上的质量是远远不够的,而且可能永远都达不到理想情况。如果一个公司既不在梯度质量上投入资源,又不在基础技术上投入资源,工程的质量就很不乐观了。当然我们公司的投入还是很多的,技术工程部就是专门为后者而设立的。

用正确的姿势在分布式的构建环境中优化 nodejs 项目部署速度

拆解一下标题:

  • 优化nodejs项目部署速度
  • 在分布式的构建环境中
  • 用正确的姿势

三个阶段

Deploy At Time Spend
2015-12-10 6s
2016-03-11 240s
2016-03-28 53s

第一阶段:单机构建、单机上线

这是我们最早的部署流程。线上只有一台机器,我们可以登录这台机器,然后手动的安装一些通用依赖(比如node、npm、nginx),甚至是项目的package.json中的依赖,就已经可以跑起来了。

但是这个部署流程的问题非常严重(已在上图标出):

  • 没有扩展性可言。一旦要加机器,必须登录新机器,手动把依赖装一遍。加一台两台还好,加个5、6台,挨个机器登录装依赖烦死你。尤其是在我们这种有事没事儿给你搞个机房下线、域名乱改的生存环境中,到之后对接运维工作的人只能呵呵啊!
  • 依赖更新不及时。原则上,我们的依赖应该要与社区的stable按本对齐的。可是有几个团队保持这种原则?原因略过。就算是你不想更新,那天社区爆出了一个安全漏洞,你也不得不去更新。到之后怎么办?再挨个机器登录更新依赖?到时候一样只能呵呵啊!

这一阶段,部署一次,大概花费6秒左右。

第二阶段:多机构建、多机上线

后来我把发布的流程改成了这个样子(标黄的内容是做的修改)。抹平了上一个阶段的坑(然后又砸出了几个坑)。

把安装依赖的过程放到构建机来做,这样做:

  • 不需要跑到每个机器上安装依赖。因为安装依赖已经在重启服务的脚本里了。
  • 保证了每次上线的时候,依赖都是和社区的stable版本对齐的。

但是又出现了新的问题:

  • 构建机安装依赖的时间非常长。我们每次点击了“我要部署”的按钮之后,都要等构建机器安装好了最新的依赖(2-5分钟),才能进入发布文件的流程。
  • 多个项目的构建脚本和重启脚本维护起来比较困难。但是这些脚本的内容很多是重复的。比如几乎所有项目的pre-deploy.sh的脚本内容都是一样的,再比如每个脚本基本上都要检查nvm、node的版本并执行npm install。

这一阶段,部署一次,大概花费240秒左右。

第三阶段:优化后

这一阶段是我们要探讨的主要问题是,我们不能接受上线一次需要花费240秒(有时甚至是300秒)。

那我们该怎么做呢?

找出“岁月神偷”

1
2
3
[2016-03-11 15:25:44] => npm install
...
[2016-03-11 15:28:52] => bower install

以上是从部署入职里摘出来的一部分。真的留意的是“…”上下的时间,一共花费了188秒。

npm install 的确很慢,但是每次都慢吗?

其实npm发现当前项目目录中已经有node_modules目录的时候,是会检查(虽然检查的很慢)那些依赖已经被安装过了的。

1
2
3
4
5
6
$ time npm install
...
real 0m11.685s
user 0m11.411s
sys 0m1.110s

这就说明其实构建机并没有保存我们上一次的构建结果,每次部署结束之后,构建结果都会被自动清理掉。

之后我又向维护OPS的同学求证了一下,构建结果其实有保存,是保存在了云存储上。但是并没有给我们开放获取构建结果的途径。

1
2
[2016-03-28 11:44:32] Upload build/mit.blog/mit.blog-2016-03-28-11-43-56.tar.gz ...
[2016-03-28 11:44:37] Upload build/mit.blog/mit.blog-2016-03-28-11-43-56.tar.gz success!

(丫的,不让我们用还占用了我们5s的时间。5s,5s我都可以上天了!)

总结一下我们在构建这件事情上遇到的问题:

  • 构建机器是分布式的。会从一个集群中随机抽取一台来处理我们的构建请求。
  • 构建机器上的构建结果会被保存,但是我们无法获取。
  • 即使我们使用了缓存的构建结果,npm检查的速度依然很慢。

抄近道

超车哪有不粗暴的?针对上面遇到的问题,我就是要用粗暴的方式搞定。

近道一:自建服务器缓存构建结果

核心思路是把构建结果缓存起来,实现188s(无node_modules) => 11s (有node_modules)的依赖安装过程优化。

近道二:使用更粗暴的方式比对项目依赖是否需要更新

这就更暴力了,直接把现在的package.json和之前有构建结果的package.json进行文本比对,再最理想的情况下,可以直接干掉npm检查本地node_modules的时间,实现11s(使用npm install检查) => 0s(使用diff检查)的依赖安装过程的优化。

(上图中,实现表示理想情况,虚线表示需要重新安装依赖的情况。)

部署速度不是全部的财富

需要注意的是,我们优化之后仍然不会恢复到之前5s上线的速度,因为我们做了一系列的严格环境检查:

  • 对构建环境的node版本做了检查。
  • 对线上环境的node版本做了检查。
  • 线上机器是分布式的环境,会对服务进行轮流重启。每重启一批机器,大概需要花费7秒。一般情况下,不管服务器有几台,都会分两批重启(保持一批持续提供服务),所以一般会花费14秒在重启服务商。
  • OPS把构建结果上传到了云存储。不受我们控制,每次消耗5秒(还不给我们用)。
  • 除上述之外,OPS准备时间花费了14秒。

这样算下来,到现在为止,我们自己在构建上花费的时间真的就只有不到20(53-14-5-14)秒了,未来我们会干掉bower,还会再减7秒。而且建立了非常稳健的环境检查机制,这比单纯追求速度更有价值。

1
2
3
4
5
6
7
8
[2016-03-28 11:44:12] [localhost] local: /bin/bash -l -c "./bin/pre-deploy.sh"
...
[2016-03-28 11:44:13] Downloading http://npm.sankuai.com/dist/node/v5.9.1/node-v5.9.1-linux-x64.tar.xz...
...
[2016-03-28 11:44:13] WARNING: checksums are currently disabled for node.js v4.0 and later
[2016-03-28 11:44:16] Now using node v5.9.1 (npm v3.7.3)
...
[2016-03-28 11:44:21] modules are loaded fram [email protected]10.4.232.99:~/.tmp/mtblog/

对构建环境的node版本做了检查

1
2
3
[2016-03-28 11:44:40] [server1] run: ./bin/post-deploy.sh
...
[2016-03-28 11:44:44] [server1] out: [PM2] Done.

每重启一批机器,大概需要花费9秒。

1
2
[2016-03-11 15:28:52] => bower install
[2016-03-11 15:28:59] bower breakpoint-sass#~2.6.1 ENOTFOUND Request to https://bower.herokuapp.com/packages/breakpoint-sass failed: getaddrinfo ENOTFOUND

未来我们会干掉bower,还会再减7秒。

不要人言亦言,合理处理 nodejs 的内存问题

很多人都说 nodejs 的 GC 特别渣,我也觉得渣。例如:

但是要明确一点:怎么用/用成什么样,和 nodejs、v8、项目都没有关系。大部分的问题来自于开发者自己。用不好不能怨别人。

什么时候出现内存泄露

对比一下下面的两段代码:

1
2
3
4
5
6
7
function notLeakMemory() {
var bigData = Array(1024 * 1024 * 16).map(() => 0)
}
for (var i = 0, l = 16; i < l; i++) {
notLeakMemory()
}
1
2
3
4
5
6
7
8
9
function leakMemory() {
var bigData = Array(1024 * 1024 * 16).map(() => 0)
setTimeout(() => {
bigData // leak
}, 3600000) // 1 hour
}
for (var i = 0, l = 16; i < l; i++) {
leakMemory()
}

分别运行一下就会知道,notLeakMemory()跑个一年半载也不会出现进程挂掉的情况,而leakMemory()运行16次之后进程就挂掉了(以下是最后的错误输出)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<--- Last few GCs --->
41914 ms: Scavenge 2179.9 (2217.4) -> 2179.9 (2217.4) MB, 0.3 / 0 ms (+ 9.5 ms in 1 steps since last GC) [allocation failure] [incremental marking delaying mark-sweep].
42584 ms: Mark-sweep 2179.9 (2217.4) -> 2051.8 (2089.4) MB, 670.5 / 0 ms (+ 18.5 ms in 7 steps since start of marking, biggest step 9.5 ms) [last resort gc].
43285 ms: Mark-sweep 2051.8 (2089.4) -> 2051.8 (2089.4) MB, 701.1 / 0 ms [last resort gc].
<--- JS stacktrace --->
==== JS stack trace =========================================
Security context: 0x3bc199de3ac1 <JS Object>
1: /* anonymous */(aka /* anonymous */) [vm.js:39] [pc=0x27aef1c738fe] (this=0x3bc199d04189 <undefined>,code=0x670b6007161 <String[5]: Debug>)
2: ensureDebugIsInitialized(aka ensureDebugIsInitialized) [util.js:194] [pc=0x27aef1c7379b] (this=0x3bc199d04189 <undefined>)
3: inspectPromise(aka inspectPromise) [util.js:200] [pc=0x27aef1c73441] (this=0x3bc199d04189 <undefined>,p=0xe72ae11...
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory

为什么会出现内存泄漏

一句话概括:当存在可被访问的函数引用了函数外的变量之后,该变量不会被自动回收

如何避免/修复内存泄漏

上面的这句话虽然短,但是有几个地方是可以留意并修复避免/修复内存泄漏的。

“可被访问的函数”

当函数不再是“可被访问的”,引用的变量也会被释放。举个例子,把上面出错代码中的延时从3600000调整为0:

1
2
3
4
5
6
7
8
9
10
function leakMemory() {
var bigData = Array(1024 * 1024 * 16).map(() => 0)
setTimeout(() => {
bigData
}, 0) // run now!
}
for (var i = 0, l = 16; i < l; i++) {
leakMemory()
}
// no error any more

匿名函数被setTimeout()调用之后,该函数的引用将被setTimeout()删除,此时匿名函数就不再是“可被访问的函数”了,所以变量占用的空间会被自动回收掉,就不会造成process out of memory的错误了。

除了setTimeout()之外,还有一些场景会自动删除函数引用:

  • 使用clearInterval()/clearTimeout()清除了创建的timer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function leakMemory() {
    var bigData = Array(1024 * 1024 * 16).map(() => 0)
    var timer = setTimeout(() => {
    bigData
    }, 3600000)
    clearTimeout(timer)
    }
    for (var i = 0, l = 16; i < l; i++) {
    leakMemory()
    }
  • 数组的方法(例如.forEach().map()等)也会在运行完函数之后删除回调函数的引用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function leakMemory() {
    var bigData = Array(1024 * 1024 * 16).map(() => 0)
    var array = [1, 2]
    array.map(() => {
    bigData
    })
    }
    for (var i = 0, l = 16; i < l; i++) {
    leakMemory()
    }

“函数外的变量”

这一点比较好理解,假如我们不访问函数外的变量,也不会造成内存泄露。

1
2
3
4
5
6
7
8
9
10
function leakMemory() {
var bigData = Array(1024 * 1024 * 16).map(() => 0)
setTimeout(() => {
var bigData // prevent leak
}, 3600000)
}
for (var i = 0, l = 16; i < l; i++) {
leakMemory()
}
// no error any more

“自动回收”

当变量 GC 不能被自动回收时,我们需要手动将变量释放掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
function leakMemory() {
var bigData = Array(1024 * 1024 * 16).map(() => 0)
setTimeout(() => {
bigData // bigData no longer holds big data
}, 3600000)
setTimeout(() => {
bigData = null // release resource
},0)
}
for (var i = 0, l = 16; i < l; i++) {
leakMemory()
}
// no error any more

灵丹妙药

  • 如果遇到和v8gc相关的参数问题,首先使用node --v8-options | less获得帮助
  • 不要过早优化内存使用。1.7G 内存不够用时,尝试使用--max_old_space_size调整老生区大小。
  • 调大新生区,可以减少 GC 阻塞进程的时间。可以通过--max_new_space_size设置
  • 使用--trace_gc参数查看 GC 的活动情况。
  • 使用 node-inspector 的内存快照功能,可以分析出不正常的内存使用。
  • 当且仅当 node 进程需要给系统中其他进程让出内存时,使用--expose_gc参数,手动调用gc()。我仍然相信绝大多数时候你不需要使用它。
  • 除了上述提到的几个示例外,这里还有一些JavaScript内存泄漏的典型示例

nodejs 中的几个 API 变化

这些是后端该关注的,前端请按 Ctrl+W。

Cluster

  • 文档v0.12
  • 文档v5.1
  • 相关模块:PM2
  • 应用场景:部署后端服务
  • 关注原因:在v0.12中稳定性为 2 - Unstable 而在v5.1中已经变为 2 - Stable。换句话说,我们不必担心将它用在线上环境会产生bug了。

顺便打脸前段时间的一篇微博:《当我们谈论 cluster 时我们在谈论什么(上)》,原文及下篇中显然没有意识到nodejs中已经悄悄更新了cluster的实现。
分发策略:

The cluster module supports two methods of distributing incoming connections.
The first one (and the default one on all platforms except Windows), is the round-robin approach, where the master process listens on a port, accepts new connections and distributes them across the workers in a round-robin fashion, with some built-in smarts to avoid overloading a worker process.
The second approach is where the master process creates the listen socket and sends it to interested workers. The workers then accept incoming connections directly.
The second approach should, in theory, give the best performance. In practice however, distribution tends to be very unbalanced due to operating system scheduler vagaries. Loads have been observed where over 70% of all connections ended up in just two processes, out of a total of eight.

设置分发策略:cluster.schedulingPolicy
最后看一下PM2中关于cluster_mode的源码
可以得出结论:想省事,直接设置 NODE_CLUSTER_SCHED_POLICY 环境变量就够了。

Buffer

  • 文档v0.12
  • 文档v5.1
  • 相关模块:所有涉及二进制处理的模块
  • 应用场景:二进制处理、协议解析、字符编码转换
  • 关注原因:新增3个ES6数组方法,以及迭代器文档

buffer.entries()
buffer.keys()
buffer.values()
ES6 iteration

另外需要注意的是,Buffer的是实现在 v0.8 => v0.10 的升级中也发生过变化,所以在网上搜索 Buffer 相关文档的时候一定要留意nodejs的版本:

Creating a typed array from a Buffer works with the following caveats:

  1. The buffer’s memory is copied, not shared.
  2. The buffer’s memory is interpreted as an array, not a byte array. That is, new Uint32Array(new Buffer([1,2,3,4])) creates a 4-element Uint32Array with elements [1,2,3,4], not a Uint32Array with a single element [0x1020304] or[0x4030201].
    NOTE: Node.js v0.8 simply retained a reference to the buffer in array.buffer instead of cloning it.

While more efficient, it introduces subtle incompatibilities with the typed arrays specification. ArrayBuffer#slice() makes a copy of the slice while Buffer#slice()creates a view.