PM2全称是Process Manager2。谢谢你,真方便。
nodejs程序想要多个process共享一个socket端口侦听,似乎只能用cluster库了,这个库不知道什么时候状态变成Stable了,可以放心用了。
他地原理主要是在SocketServer.listen的内部动作做了手脚,不是直接listen,而是让一个控制进程侦听,把得到的新的connection的fd传过来用,这样就实现了connection级别的均衡分配。
至于http request内容级别的均衡分配,那还是得要nginx之类的东西,nodejs做到这个地步已经是很给力了。
想要把一个程序改成cluster化的,标准的做法,就是原本的代码上下加上一个套子。头部加这个:
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
尾部加这个:
}
实际上不止这么简单,一旦分成住副本,那如何管理就需要花些精力了,需要全体退出,自动重启副本,... ,监测内部信息,log的管理... ...。而PM2就是做这个用的,他甚至可以跨机器发布。
话说,理论上,Nodejs真是可以做一个公用的服务程序,专门为了侦听socket的连接做分配,所有其它的nodejs程序里的socket侦听都内部从这个服务里取一个而已。
而最终我发现PM2就含有这个意思,当然他本身还包含了很多其它功能,例如forever的一些功能,还有热更新模块。
虽然用个hello world例子也行,但是我还是找了个express做的http server的例子:
bin/www (这其实是个nodejs脚本,这个例子里是从这个开始启动的)
app.js
...
普通启动,那就是执行bin/www 或者node bin/www。
如果换成pm2启动,那就可以指定cluster化的副本个数,例如
$ pm2 start --name myweb --instances 4 --node-args=--debug bin/www #4换成0可以表示CPU总数
[PM2] Starting bin/www in cluster_mode (4 instances)
[PM2] Done.
┌──────────┬────┬─────────┬───────┬────────┬─────────┬────────┬─────────────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ memory │ watching │
├──────────┼────┼─────────┼───────┼────────┼─────────┼────────┼─────────────┼──────────┤
│ myweb │ 0 │ cluster │ 53061 │ online │ 0 │ 0s │ 40.484 MB │ disabled │
│ myweb │ 1 │ cluster │ 53062 │ online │ 0 │ 0s │ 40.813 MB │ disabled │
│ myweb │ 2 │ cluster │ 53063 │ online │ 0 │ 0s │ 33.941 MB │ disabled │
│ myweb │ 3 │ cluster │ 53064 │ online │ 0 │ 0s │ 24.742 MB │ disabled │
└──────────┴────┴─────────┴───────┴────────┴─────────┴────────┴─────────────┴──────────┘
Use `pm2 show ` to get more details about an app
看起来不错的样子。
想要停止就用
pm2 stop myweb
这些不是重点,重点在于,我看了系统的进程信息里显示,这几个process的命令行参数都是
$ ps -e | grep -E '53061|53062|53063|53064'
53061 ?? 0:00.88 node /Users/q/tmp/nodejs_express_test/bin/www
53062 ?? 0:00.88 node /Users/q/tmp/nodejs_express_test/bin/www
53063 ?? 0:00.94 node /Users/q/tmp/nodejs_express_test/bin/www
53064 ?? 0:00.93 node /Users/q/tmp/nodejs_express_test/bin/www
53448 ttys001 0:00.00 grep -E 53061|53062|53063|53064
这就奇怪了,他如何控制这个process里我的socket端口侦听的动作呢?如何建立和控制进程的通信通道呢,应该是通过环境变量罗?
NODE_CHANNEL_FD #副本进程和控制进程之间通信通道fd
NODE_UNIQUE_ID #副本进程的序号
奇怪了,没找到任何环境变量,别的process都能看到,就这几个没有!
$ ps -AE | grep -E '53061|53062|53063|53064'
53061 ?? 0:01.53 node /Users/q/tmp/nodejs_express_test/bin/www
53062 ?? 0:01.53 node /Users/q/tmp/nodejs_express_test/bin/www
53063 ?? 0:01.59 node /Users/q/tmp/nodejs_express_test/bin/www
53064 ?? 0:01.58 node /Users/q/tmp/nodejs_express_test/bin/www
53732 ttys001 0:00.00 grep -E 53061|53062|53063|53064 TERM_PROGRAM=Apple_Terminal SHELL=/bin/bash ... ...
头一次碰到这种事儿,我不信邪,于是去调试了这些副本进程,正好练习体会一下node的原始debug功能。
$ pm2 delete myweb #因为接下来要改变pm2启动的参数,所以先得删除以前记下的配置,不然没有效果。
... ...
$ pm2 start --name myweb --instances 4 --node-args=--debug bin/www
... ...
│ myweb │ 0 │ cluster │ 53061 │ online │ 0 │ 0s │ 40.484 MB │ disabled │
│ myweb │ 1 │ cluster │ 53062 │ online │ 0 │ 0s │ 40.813 MB │ disabled │
│ myweb │ 2 │ cluster │ 53063 │ online │ 0 │ 0s │ 33.941 MB │ disabled │
│ myweb │ 3 │ cluster │ 53064 │ online │ 0 │ 0s │ 24.742 MB │ disabled │
... ...
$ lsof -P -p 53061,53062,53063,53064 | grep LISTEN #各个副本的调试服务端口是不一样的,所以得查一下
node 53061 q 17u IPv6 0xcf294233a1ce9d2d 0t0 TCP *:5956 (LISTEN)
node 53062 q 17u IPv6 0xcf294233a1ceb12d 0t0 TCP *:5957 (LISTEN)
node 53063 q 17u IPv6 0xcf2942339024882d 0t0 TCP *:5958 (LISTEN)
node 53064 q 17u IPv6 0xcf294233a1ceac2d 0t0 TCP *:5959 (LISTEN)
$ node debug localhost:5957 #随便连接其中一个端口调试
connecting to localhost:5957 ... ok
debug> pause #必须得先pause以便让目标进程进入可观察状态。
break in timers.js:56
... ...
>56 function listOnTimeout() {
... ...
debug> repl
Press Ctrl + C to leave debug repl
> process.env
{ TERM_PROGRAM: undefined, #各种系统环境变量
ANDROID_HOME: undefined, #各种用户自定义环境变量
SHELL: undefined,
TERM: undefined,
mac_sdk_root: undefined,
CLICOLOR: undefined,
gcc_darwin_version: undefined,
TMPDIR: undefined,
Apple_PubSub_Socket_Render: undefined,
TERM_PROGRAM_VERSION: undefined,
ANDROID_SDK_ROOT: undefined,
OLDPWD: undefined,
TERM_SESSION_ID: undefined,
USER: undefined,
CCACHE_DIR: undefined,
ANDROID_NDK_ROOT: undefined,
build_mac_version: undefined,
SSH_AUTH_SOCK: undefined,
__CF_USER_TEXT_ENCODING: undefined,
LSCOLORS: undefined,
PATH: undefined,
PWD: undefined,
HOMEBREW_GITHUB_API_TOKEN: undefined,
LANG: undefined,
PYTHONSTARTUP: undefined,
NODE_PATH: undefined,
XPC_FLAGS: undefined,
XPC_SERVICE_NAME: undefined,
mac_sdk_version: undefined,
SHLVL: undefined,
GREP_OPTIONS: undefined,
LOGNAME: undefined,
USE_CCACHE: undefined,
DISPLAY: undefined,
NDK_ROOT: undefined,
SECURITYSESSIONID: undefined,
_: undefined,
HOME: undefined,
SILENT: undefined,
node_args: undefined, #这些都是PM2嵌入的环境变量
name: undefined,
instances: undefined,
vizion: undefined,
pmx: undefined,
automation: undefined,
autorestart: undefined,
treekill: undefined,
exec_mode: undefined,
pm_exec_path: undefined,
env: undefined,
pm_cwd: undefined,
exec_interpreter: undefined,
pm_out_log_path: undefined,
pm_err_log_path: undefined,
pm_pid_path: undefined,
NODE_APP_INSTANCE: undefined,
vizion_running: undefined,
status: undefined,
pm_uptime: undefined,
axm_actions: undefined,
axm_monitor: undefined,
axm_options: undefined,
axm_dynamic: undefined,
created_at: undefined,
pm_id: undefined,
restart_time: undefined,
unstable_restarts: undefined,
started_inside: undefined,
command: undefined,
_pm2_version: undefined }
这还了得,都是undefined, 真的不会出问题吗。我又查看了运行中的模块,发现了可疑之处了。
#先Ctrl+C退出repl模式。
debug> scripts
66: ProcessContainer.js #这绝对不是我的文件
68: index.js
69: index.js
70: index.js
71: index.js
72: index.js
75: Nodejs
77: debug.js
78: index.js
79: sample-conf.js
80: conf.js
81: Utility.js
82: safeclonedeep.js
83: async.js
84: semver.js
86: index.js
87: events.js
88: transport.js
89: stringify.js
90: common.js
91: actions.js
93: notify.js
94: configuration.js
95: autocast.js
96: transaction.js
97: proxy.js
98: simple_http.js
99: Probe.js
100: Counter.js
101: Histogram.js
102: EDS.js
103: BinaryHeap.js
105: Meter.js
106: EWMA.js
107: alert.js
108: sample.js
109: network.js
110: monitor.js
111: profiling.js
113: pacemaker.js
117: www
118: app.js
120: express.js
121: index.js
122: application.js
123: index.js
124: Nodejs
125: debug.js
126: index.js
127: index.js
139: index.js
140: index.js
141: index.js
142: index.js
143: route.js
144: array-flatten.js
145: layer.js
146: index.js
148: index.js
149: index.js
150: index.js
151: index.js
152: init.js
153: query.js
155: stringify.js
156: utils.js
157: parse.js
158: view.js
159: utils.js
160: index.js
161: index.js
162: index.js
163: index.js
164: index.js
166: index.js
167: index.js
168: index.js
169: index.js
170: mime.js
171: index.js
172: index.js
181: index.js
182: index.js
183: ipaddr.js
187: request.js
188: index.js
189: index.js
190: charset.js
191: encoding.js
192: language.js
193: mediaType.js
194: index.js
196: index.js
197: index.js
202: response.js
203: index.js
204: index.js
205: index.js
207: index.js
208: index.js
209: index.js
210: index.js
211: index.js
212: index.js
213: index.js
214: index.js
215: index.js
216: parse.js
217: index.js
219: index.js
220: index.js
222: crc1.js
223: define_crc.js
224: crc8.js
225: crc8_1wire.js
226: crc16.js
227: crc16_ccitt.js
228: crc16_modbus.js
229: crc16_xmodem.js
230: crc16_kermit.js
231: crc24.js
232: crc32.js
233: index.js
234: index.js
235: index.js
236: index.js
237: session.js
238: memory.js
239: store.js
240: cookie.js
debug> cont #退出前得让目标继续运行。
debug> quit
至于环境变量消失了的问题,我发现仅仅是debugger现实的问题,他不会自动把子项目的内容显示出来,用JSON.stringify(proess.env)就可以显示出来。但是太长了就变成...了。
所以最终我还是换成node-inspector来从chrome的developer tool里调试观察了。
$ node-inspector --web-host 127.0.0.1 --web-port 8081 --debug-port 5958
Node Inspector v0.12.5
Visit http://127.0.0.1:8081/?ws=127.0.0.1:8081&port=5958 to start debugging.
然后就是打开Chrome,输入上面的URL, 进入Developer tools, 可以看到代码了。完美。
然后按[中断]按钮就可以观察变量了,到旁边的Console里输入要观察的东西。
转了一圈最终确定是 PM2的ProcessContainer.js 里干了这些事儿。
// Rename process
process.title = 'node ' + pm2_env.pm_exec_path;
这句代码太迷惑人了。