canvast / blog Goto Github PK
View Code? Open in Web Editor NEWStay hungry, stay foolish!
License: MIT License
Stay hungry, stay foolish!
License: MIT License
目前项目组内没有一套版本管理规范来对node package
进行规范,每个人对版本号的理解差异导致package
版本号混乱,本文档将给出一套规范来解决该问题,并且在次规范的基础上给出一套node package
发包最佳实践。
如果您对node package
的语义化版本号已经非常了解,可直接跳到最佳实践部分开始阅读
文档适用范围:包括所有发布到npm上的node和前端package
major.minor.patch[-prerelease]
node package
版本号由四部分组成:major.minor.patch[-prerelease],比如:1.0.2-beta.1
,其中prerelease
可选。
alpha、beta、rc
通常我们会看到三种类型的prerelease
,分别是:alpha、beta、RC,如:
1.1.0-alpha.1
1.1.0-beta.1
1.1.0-rc.1
每种类型的prerelease
都有其特殊的含义,请不要乱用。
Release Candidate
顾名思义就是正式发布的候选版本。和Beta版最大的差别在于Beta阶段会一直加入新的功能,但是到了RC版本,几乎就不会加入新的功能了,而主要着重于除错! RC版本是最终发放给用户的最接近正式版的版本,发行后改正bug就是正式版了,就是正式版之前的最后一个测试版思考一个问题:npm install ,会安装哪个版本的package
? 最新版本?
其实node package
也有tag的功能,跟git的tag有点类似,目的就是给某个版本的package
打标签。通过npm dist-tag ls指令可以查看某个package
的所有tag,以vue为例:
> npm dist-tag ls vue
beta: 2.6.0-beta.3
csp: 1.0.28-csp
latest: 2.6.10
tag包括内置类型和自定义类型,其中latest就是内置tag,csp和beta为自定义tag。默认情况下latest指向最新版本的package
,当然我们可以手动修改latest指向的版本,这个我们后面讲。每次npm publish
发包时都会将latest指向当前发布的package
版本。至于beta和csp具体指向哪个版本的package
,完全由我们自己决定。那么tag具体有什么作用?
npm install
时除了指定某个version的package
,也可以指定安装某个tag的package
。以vue为例:
npm install vue@latest
就会安装2.6.10版本。现在回答刚才的问题:“npm install ,会安装哪个版本的package
?”答案是latest指向的version。因此,我们在版本迭代时始终让latest指向最新的稳定版本。
一般pkg在发新版之前都会发布一些公测版让用户先尝鲜,比如0.0.4-beta.0
,一方面是让用户体验新功能,另一方面尽早发现bug修复上线。而在此期间更新版本是相对频繁的,我们不可能每发布一个内测版本都通知内测人员修改版本号,我们可以使用自定义标签解决此类问题。beta tag始终指向最新的带有prerelease的版本。那么用户通过npm install pkg@beta
就可以安装最新的内测版。
除了在npm publish时通过--tag参数的方式指定tag,我们还可以通过npm dist-tag add指令增加或者移动tag。
// 方式一
npm publish --tag beta
// 方式二
npm dist-tag add [email protected] beta
有一点需要特别注意:npm publish 时会自动将latest指向最新的版本包括带有prerelease的版本。为了不改变latest总是指向最新稳定版本的属性,请在publish beta版本时使用 --tag beta参数。
参考了目前流行框架(Vue、React、Taro)的版本管理方案,得出以下最佳实践。
为了规范发包流程,我们做如下约定:
latest
和 beta
两个标签latest
tag永远指向最新的稳定版本beta
tag永远指向最新的公测版本--tag beta
参数npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=] | from-git]
npm 提供了自动升级版本号的工具:npm version
,该工具会自动修改package.json内的版本号并且会自动 git commit, 因此使用该工具时请保持git status是clear的。
假设我们当前版本号为0.0.1
,我们需要升patch号:
npm version patch
那么版本号就会变成0.0.2
npm version major
和 npm version minor
同理,具体使用方法参考官方文档。
其中npm version prerelease
比较特殊,需要扩展说明下。
npm version prerelease
假设当前版本号为0.0.1
,执行 npm version prerelease
后,版本号将变为0.0.2-0
,再执行npm version prerelease
,版本号将变为0.0.2-1
,以此类推。
但是如何升级成类似0.0.2-beta.1
的形式?可以尝试使用 --preid
选项,但前提是您本地的npm版本需要大于6.4.0
// npm 6.4.0 以后可以使用 --preid 选项
npm version prerelease --preid=beta
0.0.1
将变为0.0.2-beta.0
,您也可以选择手动升级:
npm version prerelease 0.0.2-beta.0
这不是明智的选择,我们依然推荐您将npm升级到6.4.0以上的版本,升级方式:
npm i -g npm@latest
我们目前有个package
名称是ossa,第一个版本之前有两个beta版本,那么项目初始化时确保package.json里版本号为1.0.0-beta.0,publish指令:npm publish --tag beta
。
git tag:
git tag 1.0.0-beta.0
接下来会通过npm version prerelease --preid=beta
进行beta版本升级,升级后版本号将变为1.0.0-beta.1,publish指令:npm publish --tag beta
。
git tag:
git tag 1.0.0-beta.1
两个beta版本后需要发布稳定版本1.0.0,请使用指令npm version patch
,版本号将变为1.0.0,publish指令:npm publish
。
git tag:
git tag 1.0.0
接着会发布一个patch版本,请使用指令npm version patch
,版本号将变为1.0.1,publish指令:npm publish
。
git tag:
git tag 1.0.1
接着会发布一个minor版本,请使用指令npm version minor
,版本号将变为1.1.0,publish指令:npm publish
。
git tag:
git tag 1.1.0
发布1.2.0之前会发布一个1.2.0的beta版,此时请不要使用npm version prerelease --preid=beta
,因为这会导致版本号变为1.1.1-beta.0
,请使用指令npm version 1.2.0-beta.0
直接指定,publish指令:npm publish --tag beta
。
git tag:
git tag 1.2.0-beta.0
接下来所有1.2.0的beta版都可以通过npm version prerelease --preid=beta
指令自动升级,比如升级到1.2.0-beta.1,publish指令:npm publish --tag beta
。
git tag:
git tag 1.2.0-beta.1
接着发布1.2.0稳定版,请使用指令npm version minor
,版本号将变为1.2.0,publish指令:npm publish
。
git tag:
git tag 1.2.0
接着发布2.0.0稳定版,请使用指令npm version major
,版本号将变为2.0.0,publish指令:npm publish
。
git tag:
git tag 2.0.0
如果发包时出现tag指向错误的情况,比如:当前包版本为1.0.0
发beta包时没有加--tag beta
参数,tag指向将变为:
此时,可使用npm dist-tag add
指令修改tag指向:
npm dist-tag add [email protected] latest
npm dist-tag add [email protected] beta
修改后tag指向:
上面的例子已包括了常见的发包情形,后面的以此类推,请在发包时严格遵守。
最近在用Beidou同构框架搭建一个SSR同构服务,本地开发时毫无问题,但部署到测试环境和线上环境后,服务会不定期进程会收到exit
事件而异常退出,严重影响到服务的稳定性。
Beidou是由阿里开发的基于EggJS的同构框架,框架本身自带CLI工具拥有进程管理能力,启动方式为beidou start
。但是公司内的发布平台对NodeJS的进程管理进行了规范:
./src/index.js
作为启动脚本为了遵循规范,增加了./src/index.js
并且通过child_process.exec
接口执行beidou start
来启动服务。核心代码如下:
const { exec } = require('child_process')
let command = 'npx beidou start --port=8080 --title=*** --env=test'
exec(command, (error, stdout, stderr) => {
xxxx
});
推荐使用通过ps axjf
指令进行查看,该指令可以将父子进程以树状的形式展示,非常直观,同构服务的状态
PM2 v5.1.0: God Daemon (/home/webedit/.pm2)
\_ node /*/src/index.js
\_ node /usr/local/bin/npx beidou start --port=8080 --title=* --env=test
\_ node --no-deprecation /*/node_modules/egg-scripts/lib/start-cluster
\_ /home/node/bin/node --no-deprecation /*/node_modules/egg-cluster/lib/agent_worker.js
\_ /home/node/bin/node --no-deprecation /*/node_modules/egg-cluster/lib/app_worker.js
\_ /home/node/bin/node --no-deprecation /*/node_modules/egg-cluster/lib/app_worker.js
\_ /home/node/bin/node --no-deprecation /*/node_modules/egg-cluster/lib/app_worker.js
\_ /home/node/bin/node --no-deprecation /*/node_modules/egg-cluster/lib/app_worker.js
这里隐藏了一些项目的信息,但足矣说明进程情况:
./src/index.js
./src/index.js
脚本中用子进程来执行beidou start
egg-scripts/lib/start-cluster
脚本agent_worker
进程和app_worker
进程,app_worker进程数量由CPU数量决定,对这块陌生的同学可查看官方文档真正提供服务的是app_worker进程。如果服务异常,那么可以断定app_worker进程都退出了。要搞清楚app_worker退出的原因,首先要先了解EggJS启动方式,egg-scripts/lib/start-cluster
的源码很简单:
const options = JSON.parse(process.argv[2]);
require(options.framework).startCluster(options);
其实就是以EggJS cluster模式启动服务,的核心代码都在egg-cluster
package中。app_worker进程启动的关键代码在egg-cluster/lib/master.js
中:
forkAppWorkers() {
this.appStartTime = Date.now();
this.isAllAppWorkerStarted = false;
this.startSuccessCount = 0;
const args = [ JSON.stringify(this.options) ];
this.log('[master] start appWorker with args %j', args);
cfork({
exec: this.getAppWorkerFile(),
args,
silent: false,
count: this.options.workers,
// don't refork in local env
refork: this.isProduction,
windowsHide: process.platform === 'win32',
});
...
}
cfork的作用就是启动指定数量的子进程用来执行app_worker的代码。了解了启动方式后就很简单了,只要监听process的exit事件和终止信号就能知道进程何时因为何种原因退出了。
通过日志分析发现,是由于mater进程收到 SIGTERM 信号后杀掉了所有的app_worker进程。
5/18/2021, 7:08:11 PM [start-cmd] Kill child 21539 with undefined
5/18/2021, 7:08:11 PM[master] receive signal SIGTERM, closing
5/18/2021, 7:08:11 PM [master] app_worker#1:undefined exit
5/18/2021, 7:08:11 PM [master] app_worker#4:undefined exit
5/18/2021, 7:08:16 PM [master] app_worker#3:undefined exit
5/18/2021, 7:08:16 PM [master] app_worker#2:undefined exit
但谁发了SIGTERM信号?什么原因发送了SIGTERM信号?系统?还是PM2?难道要看PM2的源码?这些问题困扰了我很久。还真去了解了PM2的原理并研读了部分代码,但不是本文的重点,不展开。
根据进程的树状信息,顺腾摸瓜,当服务异常时,PM2进程却正常,初步推断是PM2内部发送的终端信号,比如内存不足等。
但是通过运维平台,并没有发现机器有内存不足的情况。所以我在./src/index.js
中监听了exit事件和终止信号,当服务退出时,确实没有收到终止信号,思路好像又断了。
无奈只能求助谷歌,文章中提到可以用Audit工具排查哪个进程杀了指定进程。
audit工具是Linux系统中负责审计的进程,可以用来记录Linux系统的一些操作,比如系统调用,文件修改,执行的程序,系统登入登出和记录所有系统中所有的事件,我们可以通过配置aidutd规则来对Linux服务器中发生的一些用户行为和用户操作进行监控。
在SA同学的协助下,最终查到是由于一个node进程杀掉了beidou进程。
对audit工具不熟悉的同学看到这些日志可能一脸懵逼,大概的意思就是:一个node程序的pid为29904
,kill进程的信号由pid为11779
的node进程中发出,而这里29904
就是app_worker,而11779
就是
./src/index.js
所在的进程,好像罪魁祸首是./src/index.js
?index.js脚本中通篇没有发送信号相关的代码,最有可能就是child_process.exec
接口。
通过分析NodeJS的child_process的源码可以发现,exec接口在启动子进程后会通过'data'事件监听子进程的输出,并且设置了输出的上限,一旦超过上限就kill调子进程,而默认的信号就是SIGTERM。上证据:
function exec(command, options, callback) {
const opts = normalizeExecArgs(command, options, callback);
return module.exports.execFile(opts.file,
opts.options,
opts.callback);
}
const MAX_BUFFER = 1024 * 1024;
function execFile(file /* , args, options, callback */) {
...
options = {
encoding: 'utf8',
timeout: 0,
maxBuffer: MAX_BUFFER,
killSignal: 'SIGTERM',
cwd: null,
env: null,
shell: false,
...options
};
...
const child = spawn(file, args, {
cwd: options.cwd,
env: options.env,
gid: options.gid,
shell: options.shell,
signal: options.signal,
uid: options.uid,
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments
});
...
function kill() {
if (child.stdout)
child.stdout.destroy();
if (child.stderr)
child.stderr.destroy();
killed = true;
try {
child.kill(options.killSignal);
} catch (e) {
ex = e;
exithandler();
}
}
...
if (child.stdout) {
if (encoding)
child.stdout.setEncoding(encoding);
child.stdout.on('data', function onChildStdout(chunk) {
const encoding = child.stdout.readableEncoding;
const length = encoding ?
Buffer.byteLength(chunk, encoding) :
chunk.length;
const slice = encoding ? StringPrototypeSlice :
(buf, ...args) => buf.slice(...args);
stdoutLen += length;
if (stdoutLen > options.maxBuffer) {
const truncatedLen = options.maxBuffer - (stdoutLen - length);
ArrayPrototypePush(_stdout, slice(chunk, 0, truncatedLen));
ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stdout');
kill();
} else {
ArrayPrototypePush(_stdout, chunk);
}
});
}
return child;
}
而我们的服务一直在跑,日志一直在输出,所以服务退出是早晚的事儿。官方文档其实已经写得很清楚,只怪自己没仔细看文档。
Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024.
最后提一嘴,SIGTERM信号一般不会由系统发出,如果您遇到SIGTERM的情况,请先从自己应用着手排查。
原因找到了,解决也就很简单了,使用不关心子进程stdout和stderr的接口即可,比如:child_process.spawn
。
let childProcess = spawn(`npx beidou start --port=${PORT} --title=* --env=${EGG_SERVER_ENV}`, [], {
shell: true,
stdio: 'inherit'
})
整个排查过程虽然一波三折,但也有很多收获:
ps axjf
可以以树状的形式查看进程关系EggJS
和pm2
的cluster模式有了完整的认识,至少可以自己写一个cluster模式了感谢您的观看!
由于最近有个个人项目需要后端服务的支持,希望在国内提供稳定的服务,决定尝试购买阿里云的ECS
云服务,而个人域名是通过name.com
购买的。按理说只需要在添加一条A记录指向到阿里云机器的IP即可,但现实就是这么残酷,第一个晚上服务正常,隔天就被封了,提示我域名需要备案。备案是工信部的要求,跟阿里云并没有太大的关系,这一点需要搞清楚。
本文不会手把手教你如何备案,这类教程网上一大堆,而此文的重点是记录本次备案过程中遇到的问题,避免大家再次踩坑浪费时间。
不是所有顶级域名类型都可以在工信部备案的哦,我之前在name.com
上购买的域名是canvast.me
,它的顶级域名类型是.me
,正好不在工信部支持的备案列表内,所以只能在阿里云上再购买一个支持备案的域名,我选择了.site
的域名类型。点击此处查询哪些域名类型支持备案。
通过阿里云备案的第一步就要进行产品验证,这个根据自己的需求决定,比如我的就是ECS
。
然而,在此之前我又给自己埋下了一个巨坑^_^。为了贪便宜,我用我弟的学生身份购买了一年的云服务,导致产品验证时没有服务实例可选,不得已以自己的名义再次购买了阿里云服务。如果您是学生或者24岁一下,那么可以考虑参与阿里云的云翼计划,只需要9.5元/月,太实惠了有没有?
为了走完备案过程,我只购买了一个月的ECS
服务,产品验证时就提示实例ID累计时长不足3个月
难道服务要先运行3个月后才能备案?这不合理啊。其实这里的不足指的是你购买的云服务服务时长,只需要再次购买2个月即可,阿里云真会赚钱...
备案成功后你会收到阿里云的补偿短信:
【阿里云】尊敬的用户:阿里云已为您的云服务器ECS 从2019-10-25 00:00:00免费续费到2019-11-05 00:00:00 。原因:备多久送多久。
本人从2019.6.25提交备案,2019.7.5工信部审核通过,刚好10个工作日,这一点阿里云还是很厚道的。
阿里云控制台 --> 云解析DNS --> 解析设置 --> 添加记录 --> 选择A记录 --> 记录值填云服务实例的公网IP即可
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.