<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Node.js 多進(jìn)程/線程 —— 日志系統(tǒng)架構(gòu)優(yōu)化實(shí)踐

          共 12149字,需瀏覽 25分鐘

           ·

          2021-11-19 13:34

          大廠技術(shù)??高級(jí)前端??Node進(jìn)階

          點(diǎn)擊上方?程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          1. 背景

            在日常的項(xiàng)目中,常常需要在用戶側(cè)記錄一些關(guān)鍵的行為,以日志的形式存儲(chǔ)在用戶本地,對(duì)日志進(jìn)行定期上報(bào)。這樣能夠在用戶反饋問題時(shí),準(zhǔn)確及時(shí)的對(duì)問題進(jìn)行定位。

            為了保證日志信息傳輸?shù)陌踩⒖s小日志文件的體積,在實(shí)際的日志上傳過(guò)程中會(huì)對(duì)日志進(jìn)行加密和壓縮,最后上傳由若干個(gè)加密文件組成的一個(gè)壓縮包。

            為了更清晰的查看用戶的日志信息。需要搭建一個(gè)用戶日志管理系統(tǒng),在管理系統(tǒng)中可以清晰的查看用戶的日志信息。但是用戶上傳的都是經(jīng)過(guò)加密和壓縮過(guò)的文件,所以就需要在用戶上傳日志后,實(shí)時(shí)的對(duì)用戶上傳的日志進(jìn)行解密和解壓縮,還原出用戶的關(guān)鍵操作。如下圖所示,是一個(gè)用戶基本的使用過(guò)程。

            但是解密和解壓縮都是十分耗時(shí)的操作,需要進(jìn)行大量的計(jì)算,在眾多用戶龐大的日志量的情況下無(wú)法立即完成所有的解密操作,所以上傳的日志擁有狀態(tài)。(解密中、解密完成、解密失敗等)

            一個(gè)常見的日志系統(tǒng)架構(gòu)如下:

            其中按照解密狀態(tài)的變化,大體分為三個(gè)階段:

          1. 用戶終端上傳日志到 cos 并通知后臺(tái)日志服務(wù)已經(jīng)上傳了日志,后臺(tái)日志服務(wù)記錄這條日志,將其狀態(tài)設(shè)置為未解密

          2. 日志服務(wù)通知解密服務(wù)對(duì)剛上傳的日志進(jìn)行解密,收到響應(yīng)后將日志的狀態(tài)更改為解密中

          3. 解密服務(wù)進(jìn)行解密,完成后將明文日志上傳并通知日志服務(wù)已完成解密,日志服務(wù)將解密狀態(tài)更改為解密完成。如果過(guò)程中出現(xiàn)錯(cuò)誤,則將日志解密狀態(tài)更改為解密失敗

            但是在實(shí)際的項(xiàng)目使用過(guò)程中,發(fā)現(xiàn)系統(tǒng)中有很多問題,具體表現(xiàn)如下:

          • 有些日志在上傳很久以后,狀態(tài)仍然為解密中。

          • 日志會(huì)大量解密失敗。(只要有一個(gè)步驟出現(xiàn)錯(cuò)誤,狀態(tài)就會(huì)設(shè)置為解密失敗)

          接下來(lái)將以這些問題為線索,對(duì)其背后的技術(shù)實(shí)現(xiàn)進(jìn)行深入探索。

          2. 問題分析

            第一個(gè)問題是有些日志上傳很久之后,狀態(tài)仍然為解密中。根據(jù)表現(xiàn),可以初步確定問題出現(xiàn)在上述的階段 3(日志狀態(tài)已設(shè)置為解密中,但并未進(jìn)行進(jìn)一步的狀態(tài)設(shè)置),因此,可以判斷是解密服務(wù)內(nèi)部出現(xiàn)異常。

            解密服務(wù)使用 Node.js?實(shí)現(xiàn),整體架構(gòu)如下:

            解密服務(wù)?Master 主進(jìn)程負(fù)責(zé)進(jìn)程調(diào)度與負(fù)載均衡,由它開啟多個(gè)工作進(jìn)程(Work Process)處理 cgi 請(qǐng)求,同時(shí)它也開啟一個(gè)解密進(jìn)程專用于解密操作。下面將著重介紹?Node.js 實(shí)現(xiàn)多進(jìn)程和其通信的方法

          2.1 Node.js 實(shí)現(xiàn)多進(jìn)程

          2.1.1 使用多進(jìn)程的好處

            進(jìn)程是資源分配的最小單位,不同進(jìn)程之間是隔離開來(lái),內(nèi)存不共享的,使用多進(jìn)程將相對(duì)復(fù)雜且獨(dú)立的內(nèi)容分隔開來(lái),能降低代碼的復(fù)雜度,每個(gè)進(jìn)程只需要關(guān)注其具體工作內(nèi)容即可,降低了程序之間的耦合。并且子進(jìn)程崩潰不影響主進(jìn)程的穩(wěn)定性,能夠增加系統(tǒng)的魯棒性。  進(jìn)程作為線程的容器,使用多進(jìn)程也充分享受多線程所帶來(lái)的好處。在下文會(huì)有多線程的詳細(xì)介紹。

          2.1.2 使用多進(jìn)程的劣勢(shì)

            進(jìn)程作為資源分配的最小單位,啟動(dòng)一個(gè)進(jìn)程必須分配給它獨(dú)立的內(nèi)存地址空間,需要建立眾多的數(shù)據(jù)表來(lái)維護(hù)它的代碼段、堆棧段和數(shù)據(jù)段,在進(jìn)程切換時(shí)開銷很大,速度較為緩慢。除此之外,進(jìn)程之間的數(shù)據(jù)不共享,進(jìn)程之間的數(shù)據(jù)傳輸會(huì)造成一定的消耗。

            因此,在使用多進(jìn)程時(shí)應(yīng)充分考慮程序的可靠性、運(yùn)行效率等,創(chuàng)建適量的進(jìn)程。

          2.1.2 Node.js 提供的實(shí)現(xiàn)多進(jìn)程的模塊

            Node.js 內(nèi)部通過(guò)兩個(gè)庫(kù)創(chuàng)建子進(jìn)程:child_process 和 cluster,下文先介紹 child_process 模塊。

            child_process 模塊提供了四個(gè)創(chuàng)建子進(jìn)程的函數(shù),分別為:spawn、execFile、exec、fork,可以根據(jù)實(shí)際的需求選用適當(dāng)?shù)姆椒ǎ鱾€(gè)函數(shù)的區(qū)別如下:

            其中 fork 用于開啟 Node.js 應(yīng)用,在 Node.js?中較為常用,其用法如下:

            一個(gè)簡(jiǎn)單的 demo 如下:

          // demo/parent.js
          const ChildProcess = require('child_process');

          console.log(`parent pid: ${process.pid}`)

          const childProcess = ChildProcess.fork('./child.js');
          childProcess.on('message', (msg) => {
          console.log("parent received:", msg);
          })
          // demo/child.js
          console.log(`child pid: ${process.pid}`)

          setInterval(() => {
          process.send(newDate());
          }, 2000)
          $ cd demo && node parent.js // 在demo目錄下執(zhí)行parent.js文件

            結(jié)果:

            在任務(wù)管理器(活動(dòng)監(jiān)視器)中看到,確實(shí)創(chuàng)建了對(duì)應(yīng) pid 的 Node.js?進(jìn)程:

          2.2 Node.js 實(shí)現(xiàn)多進(jìn)程通信

          2.2.1 常見的進(jìn)程通信方式

            試想有以下兩個(gè)獨(dú)立的進(jìn)程,它們通過(guò)執(zhí)行兩個(gè) js 文件創(chuàng)建,那么如何在它們之間傳遞信息呢?

          // Process 1
          console.log("PID 1:", process.pid);

          setInterval(() => { // 保持進(jìn)程不退出
          console.log("PROCESS 1 is alive");
          }, 5000)
          // Process 2
          console.log("PID 2:", process.pid);

          setInterval(() => { // 保持進(jìn)程不退出
          console.log("PROCESS 2 is alive");
          }, 5000)

            接下來(lái)介紹幾種通信方式:

          1. 信號(hào)

            信號(hào)是一種通信機(jī)制,程序運(yùn)行時(shí)會(huì)接受并處理一系列信號(hào),并且可以發(fā)送信號(hào)。

          1.1 發(fā)送信號(hào)

            可以通過(guò) kill 指令向指定進(jìn)程發(fā)送信號(hào),如下例子表示向 pid 為 3000 的進(jìn)程發(fā)送 USR2 信號(hào)(用戶自定義信號(hào))

          // shell指令,可以直接在命令行中輸入
          $ kill -USR2 3000

          1.2 接收信號(hào)

            定義 process 在指定信號(hào)事件時(shí),執(zhí)行處理函數(shù)即可接收并處理信號(hào)。在收到未定義處理函數(shù)的信號(hào)時(shí)進(jìn)程會(huì)直接退出

          // javascript
          process.on('SIGUSR2', () => {
          console.log("接收到了信號(hào)USR2");
          }

          1.3 示例

          // Receiver
          console.log("PID", process.pid);

          setInterval(() => {
          console.log("PROCESS 1 is alive");
          }, 5000)

          process.on('SIGUSR2', () => {
          console.log("收到了USR2信號(hào)");
          })

            假設(shè) Receiver 執(zhí)行之后的 pid 為 58241,則:

          // Sender
          const ChildProcess = require('child_process');

          console.log("PID", process.pid);

          setInterval(() => {
          console.log("PROCESS 2 is alive");
          }, 5000)

          const result = ChildProcess.execSync('kill -USR2 58241');

            在運(yùn)行 Sender 后,Receiver 成功收到信號(hào),實(shí)現(xiàn)了進(jìn)程間的通信。同樣的方式,Receiver 也可以向 Sender 發(fā)送信號(hào)。

          2. 套接字通信

            通過(guò)在接受方和發(fā)送方之間建立 socket 連接實(shí)現(xiàn)全雙工通信,例如在兩者間建立 TCP 連接:

          // Server
          const net = require('net');

          let server = net.createServer((client) => {
          client.on('data', (msg) => {
          console.log("ONCE", String(msg));
          client.write('server send message');
          })
          })

          server.listen(8087);
          // Client
          const net = require('net');

          const client = new net.Socket();

          client.connect('8087', '127.0.0.1');
          client.on('data', data =>console.log(String(data)));
          client.write('client send message');

            創(chuàng)建 server 和 client 進(jìn)程后成功發(fā)送并接收消息,分別輸出以下內(nèi)容:


          3. 共享內(nèi)存

            在兩個(gè)進(jìn)程之間共享部分內(nèi)存段,兩個(gè)進(jìn)程都可以訪問,可用于進(jìn)程之間的通信。Node.js?中暫無(wú)原生的共享內(nèi)存方式,可通過(guò)使用 cpp 擴(kuò)展模塊實(shí)現(xiàn),實(shí)現(xiàn)較為復(fù)雜,在此不再舉例。

          4. 命名管道

            命名管道可以在不相關(guān)的進(jìn)程之間和不同的計(jì)算機(jī)之間使用,建立命名管道時(shí)給他指定一個(gè)名字,任何進(jìn)程都可以使用名字將其打開,根據(jù)給定權(quán)限進(jìn)行通信。

            例如我們創(chuàng)建一個(gè)命名管道,通過(guò)它在 server 和 client 之間傳輸信息,例如 server 向 client 發(fā)送消息:

          // shell
          $ mkfifo /tmp/nfifo
          // Server
          const fs = require('fs');

          fs.writeFile('/tmp/tmpipe', 'info to send', (data, err) =>console.log(data, err));
          // Client

          const fs = require('fs');

          fs.readFile('/tmp/tmpipe', (err, data) => {
          console.log(err, String(data));
          })

          先創(chuàng)建命名管道 /tmp/nfifo 后運(yùn)行 client,與讀取一般的文件不同,讀取一般的文件會(huì)直接返回結(jié)果,而讀取 fifo 則會(huì)等待,在 fifo 有數(shù)據(jù)寫入時(shí)返回結(jié)果,然后開啟 server,server 向 fifo 中寫入信息,client 將收到信息,并打印結(jié)果,如下所示:



          5. 匿名管道

            匿名管道與命名管道類似,但是它是在調(diào)用 pipe 函數(shù)生成匿名管道后返回一個(gè)讀端和一個(gè)寫端,而不具備名字,沒有具名管道靈活,在此不做過(guò)多介紹。

          2.2.2 Node.js 原生的通信方式

            原生的 Node.js 在 windows 中使用命名管道實(shí)現(xiàn),在 * nix 系統(tǒng)采用 unix domain socket(套接字)實(shí)現(xiàn),它們都可以實(shí)現(xiàn)全雙工通信,Node.js 對(duì)這些底層實(shí)現(xiàn)進(jìn)行了封裝,表現(xiàn)在應(yīng)用層上的進(jìn)程間通信,只有簡(jiǎn)單的 message 事件和 send () 方法,例如父子進(jìn)程發(fā)送消息:

          // 主進(jìn)程 process.js
          const fork = require('child_process').fork;

          const worker = fork('./child_process.js');
          worker.send('start');
          worker.on('message', (msg) => {
          console.log(`SERVER RECEIVED: ${msg}`)
          })

          // 子進(jìn)程 child_process.js

          process.on('message', (msg) => {
          console.log("CLIENT RECEIVED", msg)
          process.send('done');
          });

          2.2.3 兄弟進(jìn)程之間通信的實(shí)現(xiàn)

            Node.js?創(chuàng)建進(jìn)程時(shí)便實(shí)現(xiàn)了其進(jìn)程間通信,但這種方式只能夠用于父子進(jìn)程之間的通信,而不能在兄弟進(jìn)程之間通信,若要利用原生的方式實(shí)現(xiàn)兄弟進(jìn)程之間的通信,則需要借助它們公共的父進(jìn)程,發(fā)送消息的子進(jìn)程將消息發(fā)送給父進(jìn)程,然后父進(jìn)程收到消息時(shí)將消息轉(zhuǎn)發(fā)給接收消息的進(jìn)程。但是使用這種方式進(jìn)行進(jìn)程間的通信經(jīng)過(guò)父進(jìn)程的轉(zhuǎn)發(fā)效率低下,所以我們可以根據(jù) Node.js?原生的進(jìn)程間通信方式實(shí)現(xiàn)兄弟進(jìn)程的通信:在 windows 上使用命名管道,在 * nix 上使用 unix 域套接字,該方法與上文套接字通信類似,只是這里不是監(jiān)聽一個(gè)端口,而是使用一個(gè)文件。

          // Server
          const net = require('net');

          let server = net.createServer(() => {
          console.log("Server start");
          })
          server.on('connection', (client) => {
          client.on('data', (msg) => {
          console.log(String(msg));
          client.write('server send message');
          })
          })

          server.listen('/tmp/unix.sock');
          // Client
          const net = require('net');

          const client = new net.Socket();

          client.connect('/tmp/unix.sock');
          client.on('data', data =>console.log(String(data)));
          client.write('client send message');

            啟動(dòng) server 后會(huì)在指定路徑創(chuàng)建文件,用于 ipc 通信。

          2.2.4 本案例中的問題分析

            本項(xiàng)目中通過(guò)一個(gè) requestManager 實(shí)現(xiàn)兄弟進(jìn)程之間的通信,set 方法用于設(shè)定當(dāng)指定序列號(hào)收到消息時(shí)執(zhí)行的回調(diào)函數(shù)。


            在本項(xiàng)目中過(guò)程如下:

            1 和 2 流程:

            3 流程:

            解密數(shù)據(jù)處理片段:

            而本項(xiàng)目的第一個(gè)問題,就出現(xiàn)在這里:程序在返回結(jié)果時(shí),調(diào)用了 res.toString 方法,在出現(xiàn)異常時(shí)調(diào)用 e.toString 方法獲取異常字符串,而實(shí)際中項(xiàng)目拋出的異常可能為空異常 null,null 不具有 toString 方法,所以向客戶端寫入數(shù)據(jù)失敗,導(dǎo)致了解密狀態(tài)的更新沒有觸發(fā)。

            提示:在處理異常時(shí),返回的異常信息一般情況下應(yīng)該能描述具體的異常,而不應(yīng)該返回空值;其次,可以使用 String (e) 代替 e.toString (),并且不應(yīng)該在捕獲到異常時(shí)靜默處理。

          2.3 “粘包” 問題的解決

            在解決完上述的問題后,發(fā)現(xiàn) bug 并沒有完全解決,于是發(fā)現(xiàn)了另一個(gè)問題:接收端每次接受的數(shù)據(jù)并不一定是發(fā)送的單條數(shù)據(jù),而可能是多條數(shù)據(jù)的合體。當(dāng)發(fā)送端只發(fā)送單條 JSON 數(shù)據(jù)時(shí),服務(wù)端 JSON.parse 單條數(shù)據(jù)順利處理消息;然而,當(dāng)接收端同時(shí)接受多條消息時(shí),便會(huì)出現(xiàn)錯(cuò)誤,最終造成進(jìn)程間通信超時(shí):

          Uncaught SyntaxError: Unexpected token { inJSON

          2.3.1 “粘包” 問題的出現(xiàn)原因

            由于 TCP 協(xié)議是面向字節(jié)流的,為了減少網(wǎng)絡(luò)中報(bào)文的數(shù)量,默認(rèn)采取 Nagle 算法進(jìn)行優(yōu)化,當(dāng)向緩沖區(qū)寫入數(shù)據(jù)后不會(huì)立即將緩沖區(qū)的數(shù)據(jù)發(fā)送出去,而可能在寫入多條數(shù)據(jù)后將數(shù)據(jù)一同發(fā)送出去,所以接收端收到的消息可能是多條數(shù)據(jù)的組合體。除此之外,也有可能是發(fā)送端一次發(fā)送一條數(shù)據(jù),但是接收端沒有及時(shí)讀取,導(dǎo)致后續(xù)一次讀取多條消息。

          2.3.1 “粘包” 問題的解決辦法

            “粘包” 問題的根本原因就在于傳輸?shù)臄?shù)據(jù)邊界不明確,因此確定數(shù)據(jù)邊界即可。

            可以通過(guò)在發(fā)送的消息前指定消息的長(zhǎng)度大小,服務(wù)端讀取指定長(zhǎng)度大小的數(shù)據(jù)。

            除此之外,還能夠制定消息的起始和結(jié)束符號(hào),起始符和結(jié)束符中間的內(nèi)容即為一條消息。

          2.4 異常的處理

            在本項(xiàng)目中,解密會(huì)大量失敗,而大量失敗的原因是進(jìn)程間通信失敗,查看具體原因后發(fā)現(xiàn)是解密進(jìn)程已經(jīng)退出,導(dǎo)致大量的失敗。接下來(lái)將探討 Node.js 進(jìn)程退出的原因和其解決辦法。

          2.4.1 Node.js 進(jìn)程退出的原因

          在實(shí)際 Node.js?進(jìn)程使用中,如果異常處理不當(dāng),會(huì)造成進(jìn)程的退出,使服務(wù)不可用。Node.js?退出的原因有以下幾種:

          • Node.js 事件循環(huán)不再需要執(zhí)行任何額外的工作,這是一種最常見的進(jìn)程退出原因,當(dāng)運(yùn)行一個(gè) js 文件時(shí),發(fā)現(xiàn)文件執(zhí)行完成之后,進(jìn)程會(huì)自動(dòng)退出,其原因就是因?yàn)槭录h(huán)不需要執(zhí)行額外的工作。阻止此類進(jìn)程退出可以不斷在事件循環(huán)中添加事件,如使用 setInterval 方法定時(shí)添加任務(wù)。

          • 顯式調(diào)用?process.exit()?方法,該方法可接受一個(gè)參數(shù),表示返回代碼,代碼為 0 表示正常退出,否則為異常。

          • 未捕獲的異常,?未捕獲的異常會(huì)導(dǎo)致進(jìn)程退出并打印錯(cuò)誤信息。使用?process.setUncaughtExceptionCaptureCallback(fn)?可以在有未捕獲異常時(shí)調(diào)用 fn,防止進(jìn)程的退出。

          • 未兌現(xiàn)的承諾,未捕獲的?Promise.reject?在高版本的 Node.js(v15 以后)會(huì)導(dǎo)致進(jìn)程的退出,而在低版本不會(huì)。

          • 未監(jiān)聽的錯(cuò)誤事件new EventEmitter().emit('error')?若沒有監(jiān)聽 error 事件則會(huì)導(dǎo)致進(jìn)程退出,處理方法同未捕獲的異常

          • 未處理的信號(hào),在向進(jìn)程發(fā)送信號(hào)時(shí),若沒有設(shè)置監(jiān)聽函數(shù),則進(jìn)程會(huì)退出。

            $ kill -USR2 <程序中輸出的pid>

          2.4.2 處理異常的方式

          對(duì)于上述造成 Node.js 退出的原因,都有其解決辦法。

          • Node.js 事件循環(huán)不再需要執(zhí)行任何額外的工作,可以在事件循環(huán)中定時(shí)添加任務(wù),例如?setInterval?會(huì)定時(shí)添加任務(wù),阻止進(jìn)程退出。

          • 顯示調(diào)用?process.exit()?方法,在程序中非必要情況下,不要調(diào)用 exit 方法。

          • 未捕獲的異常,使用?try { ... } catch (e) { }?對(duì)異常進(jìn)行捕獲,并且可以設(shè)置?process.setUncaughtExceptionCaptureCallback(fn)?可以在有未捕獲異常時(shí)調(diào)用 fn,防止進(jìn)程的退出,作為兜底策略。

          • 未兌現(xiàn)的承諾,在 promise 后調(diào)用.catch?方法或者設(shè)置?process.on('unhandledRejection', fn),防止進(jìn)程退出,作為兜底策略。

          • 未監(jiān)聽的錯(cuò)誤事件,在觸發(fā) 'error' 事件前,可以通過(guò)?EventEmitter.listenerCount?方法查看其監(jiān)聽器的個(gè)數(shù),如果沒有監(jiān)聽器,則使用其它策略提示錯(cuò)誤。

          • 未處理的信號(hào),對(duì)于信號(hào)量,設(shè)置監(jiān)聽函數(shù)?process.on('信號(hào)量', fn)?監(jiān)聽其信號(hào)量的接受,防止進(jìn)程退出。

          2.4.3 異常對(duì)于 Promise 狀態(tài)的影響

          process.on('uncaughtException', err =>console.log(err));
          let pro = newPromise((resolve, reject) => {
          thrownewError('error');
          });
          setInterval(() =>console.log(pro), 1000);

          這種情況這個(gè) promise 的狀態(tài)如何呢?在 promise 內(nèi)部既沒有調(diào)用 resolve 方法,也沒有調(diào)用 reject 方法。那么 promise 的狀態(tài)為 pending 嗎?

          -- 答案是否定的,在 promise 內(nèi)部拋出異常,會(huì)立即將 promise 的狀態(tài)更改為 reject,而不會(huì)使 promise 的狀態(tài)始終為 pending。

          那么又有另外一個(gè)問題,如果當(dāng)前不捕獲異常的情況下,這里使用那個(gè)事件捕獲異常呢?

          unhandledRejectionuncaughtException?

          答案是都可以,這個(gè)異常會(huì)先由?unhandledRejection?的?handler?處理,如果該事件未定義則由?uncaughtException?的?handler?處理,如果兩個(gè)事件都未定義則會(huì)提示錯(cuò)誤并終止進(jìn)程,具體原因在此處不作過(guò)多討論。

          2.5 Node.js 多線程

            由于需要進(jìn)行大量的解密和解壓縮操作,在本項(xiàng)目中的解密進(jìn)程中,創(chuàng)建了多個(gè)線程,接下來(lái)將對(duì) Node.js?多線程做詳細(xì)的介紹。

          2.5.1 使用多線程的好處

            前文已經(jīng)提到過(guò),進(jìn)程是資源分配的最小單位,使用多進(jìn)程能夠?qū)㈥P(guān)聯(lián)很小的部分隔離開來(lái),使其各自關(guān)注自己的職責(zé)。

            而線程則是 CPU 調(diào)度的最小單位,使用多線程能夠充分利用 CPU 的多核特性,在每一個(gè)核心中執(zhí)行一個(gè)線程,多線程并發(fā)執(zhí)行,提高 CPU 的利用率,適合用于計(jì)算密集型任務(wù)。

          2.5.2 Node.js 提供的實(shí)現(xiàn)多線程的模塊

            在 Node.js?中,內(nèi)置了用于實(shí)現(xiàn)多線程的模塊?worker_threads?,該模塊提供了如下方法 / 變量:

          • isMainThread:當(dāng)線程不運(yùn)行在 Worker 線程中時(shí),為 true。

          • Worker?類:代表獨(dú)立的 javascript 執(zhí)行線程:

          • parentPort:用于父子線程之間的信息傳輸:

            // 子線程 -> 父線程

            // 子線程中
            const { parentPort } = require('worker_threads');
            parentPort.postMessage(`msg`);

            // 父線程中
            const { Worker } = require('worker_threads');
            const worker = new Worker('filepath');
            worker.on('message', (msg) => { console.log(msg) });
            // 父線程 -> 子線程

            // 父線程中
            const { Worker } = require('worker_threads');
            const worker = new Worker('filepath');
            worker.postMessage(`msg`);

            // 子線程中
            const { parentPort } = require('worker_threads');
            parentPort.on('message', (msg) =>console.log(msg));

          2.5.3 單線程、多線程、多進(jìn)程的比較

            接下來(lái),將使用單線程、多線程、多進(jìn)程完成相同的操作。

          // 單線程
          console.time('timer');
          let j;

          for(let i = 0;i<6e9;i++) {
          Math.random();
          }

          console.timeEnd('timer');
          // 多線程

          // 主線程 thread.js
          console.time('timer');

          const { Worker, isMainThread } = require('worker_threads')

          let cnt = 15;
          for(let i = 0;i<15;i++) {
          const worker = new Worker('./worker.js');
          worker.postMessage('start');
          worker.on('message', () => {
          if(--cnt === 0) {
          console.timeEnd('timer');
          process.exit(0);
          }
          })
          }

          // 工作線程 worker.js
          const { parentPort, isMainThread } = require('worker_threads');

          parentPort.on('message', () => {
          for(let i = 0;i<1e9;i++) {
          Math.random();
          }
          parentPort.postMessage('done');
          })
          // 多進(jìn)程

          // 主進(jìn)程 process.js

          console.time('timer');
          const fork = require('child_process').fork;

          let cnt = 15;
          for(let i = 0;i<15;i++) {
          const worker = fork('./child_process.js');
          worker.send('start');
          worker.on('message', () => {
          if(--cnt === 0) {
          console.timeEnd('timer');
          process.exit(0);
          }
          })
          }

          // 子進(jìn)程 child_process.js

          process.on('message', () => {
          for(let i = 0;i<1e9;i++) {
          Math.random();
          }
          process.send('done');
          });

            實(shí)際運(yùn)行結(jié)果如下(測(cè)試機(jī)為 8 核 CPU):

            分別為單個(gè)線程、6 個(gè)線程、6 個(gè)進(jìn)程的運(yùn)行結(jié)果,(在多次實(shí)驗(yàn)后)結(jié)果有以下規(guī)律:

            多線程 <多進(jìn)程 < 單線程 < (多線程 / 多進(jìn)程) * 6

            其原因如下:

            多線程:由于充分利用 CPU,所以執(zhí)行的最快。

            多進(jìn)程:由于每個(gè)進(jìn)程中都有一個(gè)線程,同樣能充分利用 CPU,但是進(jìn)程創(chuàng)建的開銷要比線程大,所以執(zhí)行的略慢于多線程。

            單線程:由于?CPU 利用不充分所以慢于多線程和多進(jìn)程,但是由于多線程 / 多進(jìn)程的創(chuàng)建需要一定的開銷,所以快于單個(gè)線程執(zhí)行時(shí)間 * 線程個(gè)數(shù)。

            結(jié)果與預(yù)期一致。

          2.5.2 本案例中線程池的問題

            在本系統(tǒng)中,實(shí)現(xiàn)了一個(gè)線程池,它能夠在線程持續(xù)空閑的時(shí)候?qū)⒕€程退出,它會(huì)在線程創(chuàng)建時(shí)監(jiān)聽它的退出事件。

          worker.on('exit', () => {
          // 找到該線程對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),然后刪除該線程的數(shù)據(jù)結(jié)構(gòu)
          const position = this.workerQueue.findIndex(({worker}) => {
          return worker.threadId === threadId;
          });
          const exitedThread = this.workerQueue.splice(position, 1);
          // 退出時(shí)狀態(tài)是BUSY說(shuō)明還在處理任務(wù)(非正常退出)
          this.totalWork -= exitedThread.state === THREAD_STATE.BUSY ? 1 : 0;
          });

            當(dāng)線程一段時(shí)間內(nèi)都是空閑時(shí),調(diào)用線程的 terminate 方法,將其退出。然而,這段代碼中的問題是,線程在調(diào)用 terminate 函數(shù)退出后,其 threadId 自動(dòng)重置為 - 1,所以這段代碼并不會(huì)在線程池中將其移除,而由于 splice (-1, 1) 會(huì)將線程池中的最后一個(gè)線程移出。這樣,當(dāng)線程池分配任務(wù)時(shí),會(huì)分配給已經(jīng)退出的線程,而已經(jīng)退出的線程不具備處理任務(wù)的能力,因此造成進(jìn)程間通信超時(shí)。

          2.6 內(nèi)存泄漏問題的處理

            在實(shí)際的應(yīng)用中一個(gè)服務(wù)端項(xiàng)目往往都會(huì)持續(xù)運(yùn)行很長(zhǎng)時(shí)間,Node.js?會(huì)自動(dòng)對(duì)沒有引用的變量所占用的內(nèi)存進(jìn)行回收,但是還有很多內(nèi)存泄漏的問題,系統(tǒng)并不能夠自動(dòng)對(duì)其進(jìn)行處理,例如使用對(duì)象作為緩存,在對(duì)象上不斷添加數(shù)據(jù),而不對(duì)無(wú)用的緩存做清除,則會(huì)導(dǎo)致這個(gè)對(duì)象占用的內(nèi)存越來(lái)越大,直到達(dá)到內(nèi)存分配的最大限度后進(jìn)程自動(dòng)退出。下文將介紹如何分析內(nèi)存泄漏問題。

          2.6.1 內(nèi)存快照分析

            分析內(nèi)存泄漏問題最基本的方式是通過(guò)內(nèi)存快照,在 Node.js?中可以通過(guò) heapdump 庫(kù)獲取內(nèi)存快照,內(nèi)存快照可以用于查看內(nèi)存的具體占用情況。

          const heapdump = require('heapdump');

          const A = { // 4587
          b: { // 4585
          c: { // 4583
          d: newArray(1e7),
          }
          }
          }

          heapdump.writeSnapshot();

            例如在 Chrome 調(diào)試工具中查看內(nèi)存快照:

            在 Summary 快照總覽中可以看到內(nèi)存分配的詳細(xì)信息。

            第一列是 Constructor 構(gòu)造函數(shù),表示該內(nèi)存的對(duì)象由該構(gòu)造函數(shù)創(chuàng)建,()包裹的部分為內(nèi)置的底層構(gòu)造函數(shù),后方的 x1407 表示有 1407 個(gè)實(shí)例通過(guò)該構(gòu)造函數(shù)創(chuàng)建,下方 Object @4583 表示該 Object 實(shí)例的唯一內(nèi)存標(biāo)識(shí)為 4583,下方 d::Array 表示其內(nèi)部的鍵為 d 的值為一個(gè) Array 類型的數(shù)據(jù);

            第二列為 Distance,距離頂層 GCroot 的距離,例如直接在全局作用域中的變量。

            第三列為 Shallow Size,表示其自身真實(shí)占用的內(nèi)存大小。

            第四列為 Retained Size,表示與其關(guān)聯(lián)的內(nèi)存大小,此處和此處可釋放的子節(jié)點(diǎn)占用的內(nèi)存總和。

            從上圖可以看出,標(biāo)記為 4583 的對(duì)象,它的鍵為 d 的數(shù)組下真實(shí)分配了 80 000 016 字節(jié)大小的數(shù)據(jù),占總堆分配的數(shù)據(jù)的 98%,點(diǎn)擊它查看詳情,可以看到它以 c 這個(gè)鍵存在于標(biāo)記為 4585 對(duì)象下,查看 4585 對(duì)象可以看到,它以 b 這個(gè)鍵存在于標(biāo)記為 4587 的對(duì)象下:

            查看標(biāo)記為 4587 的對(duì)象可以看到,它直接存在于垃圾回收根節(jié)點(diǎn)上 GCRoot,與代碼完全對(duì)應(yīng),相關(guān)對(duì)應(yīng)關(guān)系如下:

          const A = { // 4587
          b: { // 4585
          c: { // 4583
          d: e,
          }
          }
          }

          2.6.2 本案例中的內(nèi)存泄漏問題

            在本案例中,也發(fā)現(xiàn)其一些任務(wù)始終存在于內(nèi)存中,下圖為時(shí)間間隔為一天后內(nèi)存的占用量,可以看出內(nèi)存占用量提升的非常快,

            查看其內(nèi)存占用后發(fā)現(xiàn)是線程池中部分任務(wù),由于進(jìn)程間通信超時(shí),始終沒有得到釋放,解決進(jìn)程間通信超時(shí)問題,并且設(shè)置一個(gè) timeout 超時(shí)釋放即可。

          2.7 npm 包發(fā)布流程

            在一個(gè)大型項(xiàng)目中,往往需要用到多方面的技術(shù),如果各方面內(nèi)容的實(shí)現(xiàn)都放在一起,會(huì)比較雜亂,耦合度高,難以閱讀和維護(hù)。因此,需要對(duì)程序的模塊進(jìn)行劃分,對(duì)每一個(gè)模塊進(jìn)行良好的設(shè)計(jì),讓每一個(gè)模塊都各司其職,最后組成整個(gè)程序。

            在本項(xiàng)目中的 nodejs-i-p-c 進(jìn)程間通信庫(kù),nodejs-threadpool 線程池均以包的形式發(fā)布到了 npm 上。接下來(lái)將介紹基本的 npm 包發(fā)布流程。

          1. 注冊(cè) npm 賬號(hào)(https://www.npmjs.com/)?在 npm 官網(wǎng)使用郵箱注冊(cè)賬號(hào),需要注意的是 npm 官網(wǎng)登錄只能使用用戶名 + 密碼登錄,而不能使用郵箱 + 密碼登錄!

          2. 初始化本地 npm 包。在一個(gè)本地的空文件夾中運(yùn)行?npm init?指令,創(chuàng)建一個(gè) npm 倉(cāng)庫(kù),倉(cāng)庫(kù)的名稱即為將要發(fā)布的包的名稱。(package.json 文件中的 name 字段)

          3. 登錄 npm 賬號(hào) 在本地命令行中運(yùn)行?npm login?指令即可進(jìn)行登錄操作,在輸入用戶名、密碼、郵箱后即可完成,登錄成功則會(huì)提示?Logged in as on https://registry.npmjs.org/.?npm whoami 指令可以查看當(dāng)前登錄的賬戶。

          4. 在(2)中初始化的倉(cāng)庫(kù)中運(yùn)行?npm publish?即可快速發(fā)布當(dāng)前包 如果發(fā)布失敗,可能是因?yàn)榘貜?fù),提示沒有權(quán)限發(fā)布該包,需要更改包名重新發(fā)布。

          5. 使用?npm view ?驗(yàn)證包發(fā)布,如果出現(xiàn)該包的詳細(xì)信息則說(shuō)明包發(fā)布成功了!

            在包發(fā)布成功之后其他人都能夠訪問到該包,通過(guò)?npm i ?即可安裝您發(fā)布的包使用啦。

          3. 成果展示

            處理前:日志解密大量失敗,一些日志持續(xù)停留在解密中狀態(tài)




          ?   處理后:解密全部成功,無(wú)其它異常。


          Node 社群


          我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


          ???“分享、點(diǎn)贊在看” 支持一波??

          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  午夜三级无码在线看 | 中国成人毛片 | 91免费成人 | 亚洲精品乱码久久久久久蜜桃不卡 | 黄色一级视 |