Node.js 子程式與應用場景

DuanPengfei發表於2019-03-01

背景

由於 ons(阿里雲 RocketMQ 包)基於 C艹 封裝而來,不支援單一程式內例項化多個生產者與消費者,為了解決這一問題,使用了 Node.js 子程式。

在使用的過程中碰到的坑

  • 釋出:程式管理關閉主程式後,子程式變為作業系統程式(pid 為 1)

幾種解決方案

  • 將子程式看做獨立執行的程式,記錄 pid,釋出時程式管理關閉主程式同時關閉子程式
  • 主程式監聽關閉事件,主動關閉從屬於自己的子程式

子程式種類

  • spawn:執行命令
  • exec:執行命令(新建 shell)
  • execFile:執行檔案
  • fork:執行檔案

子程式常用事件

  • exit
  • close
  • error
  • message

close 與 exit 是有區別的,close 是在資料流關閉時觸發的事件,exit 是在子程式退出時觸發的事件。因為多個子程式可以共享同一個資料流,所以當某個子程式 exit 時不一定會觸發 close 事件,因為這個時候還存在其他子程式在使用資料流。

子程式資料流

  • stdin
  • stdout
  • stderr

因為是以主程式為出發點,所以子程式的資料流與常規理解的資料流方向相反,stdin:寫入流,stdout、stderr:讀取流。

spawn

spawn(command[, args][, options])

執行一條命令,通過 data 資料流返回各種執行結果。

基礎使用

const { spawn } = require(`child_process`);

const child = spawn(`find`, [ `.`, `-type`, `f` ]);
child.stdout.on(`data`, (data) => {
    console.log(`child stdout:
${data}`);
});

child.stderr.on(`data`, (data) => {
    console.error(`child stderr:
${data}`);
});

child.on(`exit`, (code, signal) => {
    console.log(`child process exit with: code ${code}, signal: ${signal}`);
});
複製程式碼

常用引數

{
    cwd: String,
    env: Object,
    stdio: Array | String,
    detached: Boolean,
    shell: Boolean,
    uid: Number,
    gid: Number
}
複製程式碼

重點說明下 detached 屬性,detached 設定為 true 是為子程式獨立執行做準備。子程式的具體行為與作業系統相關,不同系統表現不同,Windows 系統子程式會擁有自己的控制檯視窗,POSIX 系統子程式會成為新程式組與會話負責人。

這個時候子程式還沒有完全獨立,子程式的執行結果會展示在主程式設定的資料流上,並且主程式退出會影響子程式執行。當 stdio 設定為 ignore 並呼叫 child.unref(); 子程式開始真正獨立執行,主程式可獨立退出。

exec

exec(command[, options][, callback])

執行一條命令,通過回撥引數返回結果,指令未執行完時會快取部分結果到系統記憶體。

const { exec } = require(`child_process`);

exec(`find . -type f | wc -l`, (err, stdout, stderr) => {
    if (err) {
        console.error(`exec error: ${err}`);
        return;
    }

    console.log(`Number of files ${stdout}`);
});
複製程式碼

兩全其美 —— spawn 代替 exec

由於 exec 的結果是一次性返回,在返回前是快取在記憶體中的,所以在執行的 shell 命令輸出過大時,使用 exec 執行命令的方式就無法按期望完成我們的工作,這個時候可以使用 spawn 代替 exec 執行 shell 命令。

const { spawn } = require(`child_process`);

const child = spawn(`find . -type f | wc -l`, {
    stdio: `inherit`,
    shell: true
});

child.stdout.on(`data`, (data) => {
    console.log(`child stdout:
${data}`);
});

child.stderr.on(`data`, (data) => {
    console.error(`child stderr:
${data}`);
});

child.on(`exit`, (code, signal) => {
    console.log(`child process exit with: code ${code}, signal: ${signal}`);
});
複製程式碼

execFile

child_process.execFile(file[, args][, options][, callback])

執行一個檔案

與 exec 功能基本相同,不同之處在於執行給定路徑的一個指令碼檔案,並且是直接建立一個新的程式,而不是建立一個 shell 環境再去執行指令碼,相對更輕量級更高效。但是在 Windows 系統中如 .cmd.bat 等檔案無法直接執行,這是 execFile 就無法工作,可以使用 spawn、exec 代替。

fork

child_process.fork(modulePath[, args][, options])

執行一個 Node.js 檔案

// parent.js

const { fork } = require(`child_process`);

const child = fork(`child.js`);

child.on(`message`, (msg) => {
    console.log(`Message from child`, msg);
});

child.send({ hello: `world` });
複製程式碼
// child.js

process.on(`message`, (msg) => {
    console.log(`Message from parent:`, msg);
});

let counter = 0;

setInterval(() => {
    process.send({ counter: counter++ });
}, 3000);
複製程式碼

fork 實際是 spawn 的一種特殊形式,固定 spawn Node.js 程式,並且在主子程式間建立了通訊通道,讓主子程式可以使用 process 模組基於事件進行通訊。

子程式使用場景

  • 計算密集型系統
  • 前端構建工具利用多核 CPU 平行計算,提升構建效率
  • 程式管理工具,如:PM2 中部分功能

實踐:Akyuu PM 啟動專案

  1. 程式 ①:用來解析使用者輸入,呼叫啟動命令
    const commander = require(`commander`);
    const path = require(`path`);
    const start = require(`./lib/start`);
    
    commander
        .command(`start <entry>`)
        .description(`start process`)
        .option(`--dev`, `set dev environment`)
        .action(function(entry, options) {
            start({
                entry: path.resolve(__dirname, `../entry/${entry}`),
                isDaemon: !options.dev
            });
        });
    複製程式碼
  2. 程式 ①:
    1. 利用 spawn 啟動指定入口檔案程式 ②, 設定 detached 為 true,呼叫 child.unref(); 使子程式獨立執行
      const { spawn } = require(`child_process`);
      
      const child = spawn(process.execPath, [ path.resolve(__dirname, `cluster`) ], {
          cwd: path.resolve(__dirname, `../../`),
          env: Object.assign({}, process.env, {
              izayoiCoffee: JSON.stringify({
                  configDir: config.akyuuConfigDir,
                  entry: options.entry
              })
          }),
          detached: true,
          stdio: `ignore`
      });
      
      child.on(`exit`, function(code, signal) {
          console.error(`start process `${path.basename(options.entry)}` failed, ` +
              `code: ${code}, signal: ${signal}`);
          process.exit(1);
      });
      
      child.unref();
      複製程式碼
    2. 記錄子程式 pid 到檔案
      child
          .on(`fork`, function(worker) {
              try {
                  fs.writeFileSync(
                      `pid file path`,
                      worker.process.pid,
                      { encoding: `utf8` }
                  );
              } catch(err) {
                  console.error(
                      `[%s] [uncaughtException] [master: %d] 
      %s`,
                      moment().utcOffset(8).format(`YYYY-MM-DD HH:mm:ss.SSS`),
                      process.pid,
                      err.stack
                  );
              }
          })
          .on(`exit`, function(worker, code, signal) {
              try {
                  fs.unlinkSync(`pid file path`);
              } catch(err) {
                  console.error(
                      `[%s] [uncaughtException] [master: %d] 
      %s`,
                      moment().utcOffset(8).format(`YYYY-MM-DD HH:mm:ss.SSS`),
                      process.pid,
                      err.stack
                  );
              }
          });
      複製程式碼
  3. 程式 ②:如果程式 ② 中還需要啟動自己的子程式,在啟動子程式後,監聽自己的退出事件,並主動關閉子程式,防止子程式變為作業系統程式而不受控
    const { fork } = require(`child_process);
    
    const child = fork(`some child process file`);
    
    // 程式停止訊號
    process.on(`SIGHUP`, function() {
        child.kill(`SIGHUP`);
        process.exit(0);
    });
    
    // kill 預設引數訊號
    process.on(`SIGTERM`, function() {
        child.kill(`SIGHUP`);
        process.exit(0);
    });
    
    // Ctrl + c 訊號
    process.on(`SIGINT`, function() {
        child.kill(`SIGHUP`);
        process.exit(0);
    });
    
    // 退出事件
    process.on(`exit`, function() {
        child.kill(`SIGHUP`);
        process.exit(0);
    });
    
    // 未捕獲異常
    process.on(`uncaughtException`, function() {
        child.kill(`SIGHUP`);
        process.exit(0);
    });
    複製程式碼

總結

在使用 Node.js 做開發中,尤其是 API 開發過程中很少涉及到子程式,但是子程式還是比較重要的一個組成部分。Node.js 可以利用子程式做些計算密集型任務,雖然沒有 C艹 等其他語言高效、方便,但是也不失為一種方案,在沒有掌握其他語言時可以用 Node.js 支撐起業務場景。對於子程式的採坑與使用在本文中記錄,以供未來的自己參考。

相關文章