github地址:戳這裡
簡介
目標:寫一個基於事件驅動 ,非阻塞i/o 的web伺服器,以達到更高的效能。構建快速,可伸縮的網路應用平臺
js開發效能低,事件驅動應用
node強制不共享任何資源的 單執行緒 ,單程式系統,包含十分適宜網路的庫
應用:
- 訪問本地檔案
- 搭建websocket服務端
- 連線資料庫
- web workers多程式(不處理ui)
特點:
- 依舊基於作用域和原型鏈
- 非同步i/o
兩個readFile的操作最終時間為最慢的那一個
- 事件和回撥函式
事件程式設計方式:輕量級,輕耦合,只關注事務點等優勢
-
單執行緒
特點:
- js與其他執行緒是無法共享任何狀態
- 不用像多執行緒一樣處處在意狀態的同步
- 沒有死鎖
- 沒有執行緒上下文交換帶來的效能上的開銷
弱點:
- 無法利用多核cpu
- 錯誤會引起整個應用退出,應用的健壯性值得考研
- 大量計算佔用cpu導致無法呼叫非同步i/o
- js與ui共用一個執行緒,長時間執行會導致ui的渲染和響應被中斷
解決:
- web workers能夠建立工作執行緒來進行計算,以解決js大計算阻塞ui渲染的問題
- child_process子程式,將計算分發到各個子程式,可以將大量計算分解掉
應用場景:
- i/o密集型,利用事件迴圈的處理能力
- cpu非密集型,i/o阻塞造成的效能浪費遠比cpu的影響小
- 分散式應用,利用高效並行i/o,可以高效使用資料庫
模組機制
前言:
-
web 1.0 : JavaScript用於表單校驗和網頁特效,只有對bom,dom的支援
-
web 2.0 : 提升了網頁的使用者體驗,bs應用展現出了比cs(需要裝客戶端)應用優越的地方。h5嶄露頭角
此過程經歷了工具-元件-框架-應用的變遷
js的規範缺陷:
- 沒有模組系統
- 標準庫較少
- 沒有標準介面
- 缺乏包管理系統
commonjs模組規範
- 模組引入
require()
- 提供exports物件用於匯出當前模組的方法或者變數
- 模組標識,就是require的引數,必須駝峰命名,相對路徑或者絕對路徑,可以沒有字尾
同步,為後端js指定的規範,並不完全適合前端的應用場景
模組實現
模組分為兩類:
- node提供的 核心模組
已被編譯進了二進位制執行檔案,node啟動時就被載入進記憶體,所以1.2步驟可以省略。且載入速度最快
- 使用者編寫的 檔案模組
動態載入,速度比核心模組慢
優先從快取載入
- node快取的是 編譯執行後的物件
- 不論核心模組還是使用者模組,對應相同模組的二次載入都是快取優先
在node中引入模組要經過下面三個步驟
-
路徑分析
- 識別符號分析:
- 核心模組
..
或者.
相對路勁模組- 以
/
開頭的絕對路徑模組 - 非路徑形式的模組,如自定義的
connect
模組
-
如果想載入與核心模組識別符號相同的模組,必須選擇 不同的識別符號 或者 換用路徑 的方法
-
以
.
,..
,/
開頭的識別符號,會將路徑轉換成真實路徑 -
自定義模組是最費時的
module.paths
模仿搜尋路徑規則如下:
- 當前檔案目錄下的node_modules
- 父目錄下的node_modules
- 沿路徑向上逐級遞迴直到根目錄下的node_modules
- 識別符號分析:
-
檔案定位
-
副檔名
-
.js
.node
.json
順序補齊 -
fs
模組同步阻塞式的判斷檔案是否存在,如果是.node
和.json
檔案,帶上副檔名再配合快取可以加快速度
-
-
目錄和包的處理
- 如果得到的是一個目錄,則會被當做包來處理。這時先進入包目錄,查詢
package.json
,取出main
屬性指定的檔名定位。 - 如果找不到這個檔案或者沒有
package.json
, 會將index
作為預設檔名
- 如果得到的是一個目錄,則會被當做包來處理。這時先進入包目錄,查詢
-
-
編譯執行
node會新建一個模組物件,然後根據路徑載入並編譯,對應不同副檔名,載入方法不同:
.js
通過fs
同步讀取.node
通過dlopen()
載入.json
通過fs讀取,再JSON.parse
- 其餘副檔名都被當做
.js
每一個編譯成功的模組都會被繫結在 Module._cache
上
編譯過程對檔案內容進行頭尾包裝
// 通過vm原生模組runInThisContext方法執行,不汙染全域性
(function (exports, require, module, __filename, __dirname) {
})
複製程式碼
另外,這樣會出錯
exports = function () {
// My class
}
複製程式碼
原因在於,exports物件是通過形參的方式傳入的,直接賦值會改變形參的作用,但並不能改變作用域外的值。
js核心模組的編譯過程
- 轉存為c/c++程式碼
- 編譯js核心模組
c/c++核心模組編譯過程
- 內建模組的組織方式
c++模組主內完成核心,js主外實現封裝
效能優於指令碼語言
被編譯成二進位制檔案,一旦node開始執行,就直接載入進快取
- 內建模組匯出
依賴關係:檔案模組 <--
核心模 塊<--
內建模組
包與npm
包結構
-
package.json
包描述檔案- name:包名,不允許出現空格
- description:包簡介
- version:版本號
- keywords:關鍵詞陣列
- maintainers:包維護者列表,每個維護者有name,email,web
- dependencies:所需要的依賴包列表
- devDependencies:只在開發時需要的依賴
- scripts:指令碼說明物件
- main:模組引入方法require在引入包時,會優先檢查這個欄位,並將其作為包中其餘模組的入口
- bin:一些包作者希望包可以作為命令列工具,配置好bin後,通過npm install package_name -g將指令碼新增到執行路徑中,之後可以再命令列直接執行
-
bin
存放可執行二進位制檔案的目錄 -
lib
存放js的程式碼目錄 -
doc
存放文件 -
test
存放單元測試用例
常用功能
-
檢視幫助
npm help
-
安裝依賴包
npm install --save/--save-dev express
- 全域性安裝
-g是講一個包安裝到全域性可用的可執行命令。它根據包描述檔案中的bin欄位配置,將實際指令碼連線到與node可執行檔案相同的路徑下
如果node可執行檔案的位置是
/usr/local/bin/node
,那麼模組目錄就是/usr/local/lib/node_modules
。最後通過軟連結方式將bin欄位配置的可執行檔案連結到node的可執行目錄下-
本地安裝
換源:
npm install underscore --registry=http:registry.url
npm config set registry http:registry.url
-
npm鉤子
-
釋出包
- 編寫模組
- 初始化包描述檔案
- 註冊包倉庫賬號
npm adduser
- 上傳包
npm publish<folder>
- 管理包許可權
npm owner ls <package_name>
npm owner add <user> <package_name>
npm owner rm <user> <package_name>
6. 分析包npm ls
模組考察點
- 良好的測試
- 良好的文件
- 良好的測試覆蓋率
- 良好的編碼規範
- 更多條件
前後端共用模組
node模組引入幾乎都是同步的,但如果前端模組也採用同步的方式來引入,使用者體驗會造成問題
AMD規範
需要用define來明確定義一個模組,而在node實現中是隱式包裝的。
所有的依賴,通過形參傳遞到依賴模組內容中
define(['dep1', 'dep2'], function (dep1, dep2) {
return function () {}
})
複製程式碼
目的是作用域隔離
內容需要返回的方式實現匯出
define(function () {
var exports = {};
exports.sayHello = function () {
...
}
return exports
})
複製程式碼
CMD規範
更接近commonjs規範
define(function (require, exports, module) {
// ...
})
複製程式碼
require,exports, module通過形參傳遞給模組。
相容多種模組規範
;(function (name, definition) {
var hasDefine = typeof define === 'function';
var hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) { // AMD或者CMD
define(definition);
} else if(hasExports) { // 定義為普通模組
module.exports = definition()
} else {
this[name] = definition()
}
})('hello', function () {
var hello = function () {}
return hello
})
複製程式碼
非同步i/o
-
node面向網路而設計
-
利用單執行緒,原理多執行緒死鎖,狀態同步問題
-
利用非同步i/o,讓單執行緒原理阻塞,更好的利用cpu
-
核心在進行檔案i/o的操作時,通過檔案描述符進行管理,檔案描述符類似於應用程式與系統核心之間的憑證。
-
阻塞i/o造成cpu等待浪費,非阻塞卻要 輪詢 去確認是否完全完成資料獲取
-
理想非阻塞非同步i/o:發起非阻塞呼叫後,可以直接處理下一個任務,只需i/o完成後通過訊號或回撥將資料傳遞給應用程式
-
顯示的非同步i/o:通過讓部分執行緒進行阻塞i/p或者非阻塞i/o加輪詢技術來完成資料獲取,讓一個執行緒進行計算處理,通過執行緒之間的通訊將i/o得到的資料進行傳遞
為什麼要非同步i/o
-
使用者體驗
如果是同步,js執行ui渲染和響應將處於停滯狀態
採用非同步,在下載資源期間,js和ui的執行都不會處於等待狀態
採用非同步方式所花時間為max(m, n)
-
資源分配
- 單執行緒序列依次執行
缺點:
單執行緒同步程式設計模型會因為阻塞i/o導致效能差,
- 多執行緒並行完成
缺點:
代價在於建立執行緒和執行期執行緒上下文切換的開銷較大
多執行緒常面臨鎖,狀態同步問題
優點:
但是能有效提升cpu利用率
node的非同步i/o
模型基本要素:事件迴圈,觀察者,請求物件,i/o執行緒池
node自身其實是多執行緒的,只是i/o執行緒使用的cpu較少
- 事件迴圈
- 觀察者
每個事件迴圈中有一個或者多個觀察者
- 請求物件
非同步i/o過程中的重要中間產物,所有的狀態都儲存在這個物件中,包括送入執行緒池等待執行以及i/o操作完畢後的回撥處理
- 執行回撥
非i/o得非同步api
- 定時器,setTimeout和setInterval
建立的定時器會被插入到定時器觀察者內部的一個紅黑樹中
每次Tick執行時,會從紅黑樹中迭代取出定時器物件,檢查是否超過定時時間。如果超過,就形成一個時間,它的回撥函式將立即執行
時間複雜度O(lg(n)) 2. process.nextTick
將回撥函式放入佇列,在下一輪Tick時取出執行
時間複雜度 0(1)
事件驅動與高效能伺服器
伺服器模型:
- 同步式。一次只能處理一個請求,其他請求都在等待
- 每程式/每請求。為每個請求啟動一個程式,這樣可以處理多個請求,但是系統資源只有那麼多,所以不具備擴充套件性
- 每執行緒/每請求。為每個請求啟動一個執行緒來處理。當大併發請你去到來時,記憶體將用光。
node高效能:
- node通過實踐驅動的方式處理請求,無須為每一個請求建立額外的對應執行緒
- 省掉建立和銷燬執行緒的開銷。
- 執行緒少,上線文切換的代價少
非同步程式設計
函數語言程式設計
- 高階函式,將函式作為輸入或返回值
- 偏函式,建立一個呼叫另外一部分--引數或變數已預置的函式---的函式的用法。
var toString = Object.prototype.toString;
var isType = function (type) {
return function (obj) {
return toString.call(obj) == '[object' + type + ']'
}
}
var isFunction = isType('Function')
複製程式碼
優勢
- 基於事件驅動的非阻塞i/o模型
- 使cpu與i/o並不相互依賴等待
- 並行帶來的想象空間更大,延展開來是分散式和雲
難點
- 異常處理
非同步i/o提交請求和處理結果兩個階段中間,有事件迴圈的排程。非同步方法則通常在提交請求後立即返回,因為一場並不一定發生在這個階段,所以try/catch在這裡無效
try/catch對於callback執行時丟擲的異常無能為力
- 回撥煉獄
- 阻塞程式碼,由於沒有sleep,用setTimeout代替
- 多執行緒程式設計:web workers和child_process
- 非同步轉同步
非同步程式設計解決方案
-
事件釋出/訂閱模式
- 繼承events模組
var events = require('events'); function Stream () { events.EventEmitter.call(this) } util.inherits(Stream, events.EventEmitter) 複製程式碼
-
利用事件佇列解決雪崩問題,once方法
-
多非同步之間的寫作方案
- 利用哨兵變數
- EventProxy
-
Promise/Deferred
-
Promise/A
-
只有三種狀態:rejected,fullfiled, rejected
-
只能未完成到完成,或者失敗,不能逆反
-
狀態不能更改
-
-
-
流程控制庫
- 尾觸發和next
- async的parallel,waterful等方法
- step
- wind
記憶體控制
-
js在瀏覽器的應用場景,由於執行時間短,隨著程式的推出,記憶體會釋放,幾乎沒有記憶體管理的額必要
-
記憶體控制正式在海量請求和長時間執行的前提下進行探討的。
-
在伺服器端,資源寸土寸金
-
對於效能敏感的伺服器端程式,記憶體管理的好壞,垃圾回收狀況的優良,影響很大
js引擎V8(虛擬機器)
記憶體限制
在node中通過js使用記憶體時,只能使用部分,無法直接操作大記憶體物件
64位系統下約為1.4GB,32位系統下約為0.7GB
node中使用js物件,都是通過V8來進行分配和管理的
物件分配
js物件通過堆來分配
當在程式碼中生命變數並賦值時,所使用物件的記憶體就分配在堆中。如果已申請的堆空閒記憶體不夠分配新的物件,將繼續申請堆記憶體,直到堆得大小超過V8的限制為止
V8為何限制堆得大小:表層原因是起初為瀏覽器而設計,限制值已經綽綽有餘。深層原因是V8的垃圾回收機制的限制,做一次非增量式的垃圾回收時間花銷大
垃圾回收機制
V8垃圾回收策略主要基 分代式垃圾回收機制
垃圾回收演算法:
- V8的記憶體分帶
將記憶體分為 新生代 和 老生代
新生代中的物件為存活時間較短的物件,老生代的物件為存活時間較長或常駐記憶體的物件
-
Scavenge演算法
-
具體實現主要採用Cheney演算法
-
採用複製的方式實現垃圾回收演算法。
-
將堆記憶體一分為二。每一份空間成為semispace。處於閒置狀態的稱為To空間,處於使用狀態的稱為From空間。
-
當開始進行垃圾回收時,會檢查From空間的存活物件,這些存活物件會被複制到To空間。非存活物件佔用空間會被釋放
-
缺點:用空間換時間
-
當一個物件經過多次複製依然存活時,被認為是生命週期較長的物件。被移到老生代中。稱為晉升
-
物件晉升的條件:
- 一個物件經歷過Scavenge回收
通過檢查它的記憶體地址來判斷。如果經歷過了,從From複製到老生代
- To空間的記憶體佔用比超過限制25%
-
缺點:1. 存活物件較多時,複製存活物件的效率低。 2. 浪費一般空間
-
Mark-Sweep(標記清除)
- 遍歷堆中的所有物件,標記存活物件。在清除階段只清除沒有被標記的物件。
- 標記清除後 記憶體空間出現不連續 的狀態,如果需要分配一個大物件,就無法完成
-
Mark-Compat(標記整理)
-
物件在標記為死亡後,整理過程中,將活著的物件往一端移動。完成後,直接清理掉邊界外的記憶體
-
在空間不足以對從新生代晉升過來的物件進行分配時才使用
-
-
Incremental Marking
- 上述基本演算法都需要將應用邏輯暫停下來,執行完垃圾回收後再恢復,這種行為成為 全停頓
- 全堆垃圾回收的標記,清理,整理等動作造成停頓
- 將一口氣完成的標記改為增量標記,拆分成許多小“步進”
-
延遲清理和增量清理
-
並行標記和並行清理
小結:
- web伺服器的會話實現,一般通過記憶體來儲存,但在訪問了大的到時候會導致老生代中的存活物件驟增,不盡造成清理/整理過程費時,還會造成記憶體緊張,甚至溢位
檢視垃圾回收日誌
node --trace_gc -e "..."
可以瞭解垃圾回收的執行狀況,找出哪些階段比較費時
node --prof xx.js
會在該目錄下生成v8.log檔案,得到效能分析資料
node --prof-process isolate-0x103001200-v8.log
由於日誌檔案不具備可讀性,故這樣可以統計日誌資訊
高效使用記憶體
-
作用域
- 函式呼叫,被呼叫時建立對應作用域,執行結束後作用域摧毀。
var foo = function () { var local = {}; } foo(); 複製程式碼
記憶體回收過程:只被區域性變數引用的物件存活週期較短,會被分配在新生代的From空間,在作用域釋放後,區域性變數local失效,引用的物件會在下次垃圾回收時被釋放
- with
- 全域性作用域
識別符號查詢:
js在執行時回去找該變數在哪裡定義,在當前作用域沒有查到,將會向上級的作用域裡查詢,直到查到為止
作用域鏈:
根據在內部函式可以訪問外部函式變數的這種機制,用鏈式查詢決定哪些資料能被內部函式訪問。
執行環境:
js為每一個執行環境關聯了一個變數物件。環境中定義的所有變數和函式都儲存在這個物件中。
變數的主動釋放:
全域性變數,直到程式退出才釋放。引用的物件常駐記憶體(老生代)。
可以用delete操作和重新賦值(null或者undefined)
- 閉包
實現外部作用域訪問內部作用域中變數的方法
作用域中產生的記憶體佔用不會得到釋放。除非不再有引用,才會逐步釋放
記憶體指標
程式的記憶體一部分是rss,其餘部分在交換區或者檔案系統中
$ node
> process.memoryUsage()
{
rss: // 常駐記憶體
heapTotal: // 總申請的記憶體量
heapUsed: // 使用中的記憶體量
}
> os.totalmem() // 總記憶體
> os.freemem() // 閒置記憶體
複製程式碼
Buffer物件並非通過V8分配,沒有堆記憶體的大小閒置
小結:受V8的垃圾回收限制的主要是V8堆記憶體
記憶體洩漏
哪怕一位元組的記憶體洩漏也會造成堆積,垃圾回收過程中將會耗費更多時間進行物件描述,應用響應緩慢,直到程式記憶體溢位,應用奔潰
原因:
- 快取
快取中儲存的鍵越多,長期存活物件也就越多,常駐在老生代
普通物件無過期策略
var cached = {};
function get (key) {
if (cached[key]) {
return cached[key]
} else {
}
}
function set (key, value) {
cached[key] = value;
}
複製程式碼
解決:
-
快取限制策略
超過數量,先進先出的方式進行淘汰
設計模組時,應新增清空佇列的相應介面
-
快取的解決方案
程式間無法共享記憶體
- 將快取轉移到外部,減少常駐記憶體的物件的數量,讓垃圾回收更高效
- 程式之間可以共享快取
- 佇列消費不及時
佇列消費速度低於生產速度,將會形成堆積。而js相關作用域也不會得到釋放,記憶體佔用不會回落,從而出現記憶體洩漏
解決方案:
- 表層:換用消費速度更高的技術
- 深度:監控佇列的長度
- 任意非同步呼叫都應該包含超時機制
- 作用域未釋放
大記憶體應用
node中大多數模組都有stream應用。由於V8記憶體限制,採用流實現對大檔案的操作
如果不需要進行字串層面的操作,則不需要V8來處理,嘗試進行純粹的Buffer操作
Buffer
特點
- Buffer 類的例項類似於 整數陣列 ,但 Buffer 的大小是固定的、且在 V8 堆外分配實體記憶體
- Buffer 的大小在被建立時確定,且無法調整。
- 效能相關部分由c++實現,非效能相關由js實現
記憶體分配
- 在node的c++層面實現記憶體的申請,在js中分配記憶體
- 使用slab分配機制
- 預先申請,事後分配
- slab狀態:
- full,完全分配狀態
- partial,沒有分配誒狀態
- empty,沒有被分配狀態
- 同一個slab可能分配給多個buffer物件
- 分配大Buffer物件,直接由c++層面提供的記憶體,而無需細膩的分配操作
亂碼
- 緩衝器的大小取決於傳遞給流建構函式的 highWaterMark 選項
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var data = ''
reader.on('data', function (chunk) {
data += chunk
})
reader.on('end', function () {
console.log(data)
})
複製程式碼
- buffer物件的長度為11,可讀流要讀取很多次才能完成完整的讀取
- 寬位元組字串可能存在被截斷的情況。
解決亂碼
- 設定編碼
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
render.setEncoding('utf8')
複製程式碼
setEncoding的時候,可讀流物件在內部設定了一個decoder物件。每次data事件都通過該decoder物件進行Buffer到字串的解碼。
decoder的物件會暫時儲存,buffer讀取的剩餘位元組
- 將小buffer物件合併
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var chunks = [];
var size = 0;
reader.on('data', function (chunk) {
chunks.push(chunk);
size += chunk.length;
})
reader.on('end', function () {
var buf = Buffer.concat(chunks, size);
console.log(buf.toString())
})
複製程式碼
Buffer與效能
- 通過預先轉換靜態內容為Buffer物件,可以有效地減少cpu的重複使用,節省伺服器資源
- highWaterMark值的大小與讀取速度的關係:該值越大,讀取速度越快
網路程式設計
前言
在web領域,大多數的程式語言需要專門的web伺服器作為容器,如ASP、ASP.NET需要IIS作為伺服器,PHP需要打在Apache或Nginx環境等,JSP需要Tomcat伺服器等。但對於Node而言,只需要幾行程式碼即可構建伺服器,無需額外的容器。
構建TCP服務
-
TCP
- 面向連線的協議
- 建立會話的過程,服務端和客戶端分別提供一個套接字,共同形成連線。
- 如果客戶端要與另一個TCP服務通訊,需要另建立一個套接字來完成連線
-
建立TCP伺服器端
const net = require('net');
let server = net.createServer();
server.on('connection', function (socket) {
console.log('connection')
})
server.listen(8000)
複製程式碼
- TCP服務的事件
- 伺服器事件
- listening,在呼叫server.listen繫結埠或者Domain Socket後出發
- connection,每個客戶端套接字連線到伺服器端時觸發,簡潔寫法為通過net.createServer,最後一個引數傳遞
- close,當伺服器關閉時觸發。server.close後,伺服器將停止接受新的套接字連線
- error,當伺服器發生異常時觸發
- 連線事件
- data,當一端呼叫write傳送資料時,另一端會觸發data事件
- end,當任意一端傳送FIN資料時觸發
- connect,用於客戶端,當套接字與服務的連線成功時觸發
- drain,當任意一端呼叫write傳送資料時,當前這段會觸發者事件
- error
- close,當套接字完全關閉時,觸發
- timeout,當連線被閒置時觸發
- 伺服器事件
構建UDP服務
UDP不是面向連線的。
一個套接字可以與多個UDP服務通訊,它雖然提供面向事務的簡單不可靠資訊傳輸服務,在網路差的情況下存在丟包嚴重的問題
優點:無連線,資源消耗低,處理快速且靈活
應用:音訊,視訊,dns服務
- 建立UDP
const dgram = require('dgram');
const server = dgram.createSocket('udp4')
server.on('error', (err) => {
console.log(`伺服器異常:\n${err.stack}`);
server.close();
});
server.on('message', (msg, rinfo) => {
console.log(`伺服器收到:${msg} 來自 ${rinfo.address}:${rinfo.port}`);
});
server.on('listening', () => {
const address = server.address();
console.log(`伺服器監聽 ${address.address}:${address.port}`);
});
server.bind(1000)
複製程式碼
- UDP套接字事件
- message,當UDP套接字偵聽網路卡埠後,接收到訊息時觸發該事件
- listening
- close
- error
HTTP
特點:
- 基於請求響應式,以一問一答的方式實現服務,雖然基於TCP會話,但是本身卻並無會話的特點
- 瀏覽器,其實是一個HTTP的代理,使用者的行為將會通過它轉化為HTTP請求報文傳送給服務端,服務端處理請求後,傳送響應報文給代理,代理在解析報文後,將使用者需要的內容呈現在介面上。
- TCP服務以connection為單位進行服務,HTTP服務以request為單位進行服務。http是將connection到request進行了封裝
- 一旦開始了資料傳送,writeHead和setHeader將不再生效。
res.writeHead(()
res.write() // 傳送資料
res.end()
複製程式碼
-
http服務端事件
- connection,在http請求前,建立tcp時觸發
- request,當請求資料傳送到服務端,在解析出http請求頭後觸發
- close,當tcp連線斷開
- checkContinue,和request事件互斥。當客戶端在傳送較大資料的時候,並不會將資料直接傳送,而是先傳送一個頭部帶Expect:100-continue的請求到伺服器,這是伺服器會觸發checkContinue
- connect, 當客戶端發起CONNECT請求時觸發,而發起CONNECT請求通常在http代理時出現。
- upgrade,當客戶端要求升級連線的協議時,需要和服務端協商
- clientError,連線的客戶端觸發error事件,傳遞到服務端
-
http客戶端
示例:
var req = http.request(options, function (res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log(chunk)
})
})
複製程式碼
- http代理
在keepalive的情況下,一個底層會話連線可以多次用於請求。為了重用tcp連線,可以用http.globalAgent客戶端代理物件
預設情況下,通過ClientRequest物件對同一個伺服器發起的http請求最多可以建立五個連線
如需改變,可在options中傳遞agent選項
var agent = new http.Agent({
maxSockets: 10
})
var options = {
hostname: '127.0.0.1',
port: 1334,
path: '/',
method: 'GET',
agent: agent
}
複製程式碼
- http客戶端事件
- response:客戶端在請求後得到服務端響應時觸發
- socket:當底層連線池中建立的連線分配給當前請求物件時觸發
- connect: 當客戶端向瀏覽器發起CONNECT請求時,如果伺服器端響應了200狀態碼,客戶端會觸發該事件
- upgrade,客戶端向伺服器發起upgrade請求時,如果服務端響應了101 switching protocol狀態
- continue,客戶端向服務端發起Expect:100-continue以試圖傳送大資料量
websocket服務
特點:
- 基於事件程式設計模型(事件驅動)
- 長連線
- 更接近於傳輸層協議,分為握手(由http完成)和資料傳輸兩部分
好處:
- 客戶端與服務端只建立一個TCP連線,可以使用更少的連線
- websocket服務端可以推送資料到客戶端,比http請求響應模式更靈活,更高效
- 更輕量級的協議頭,減少資料傳送量
構建過程
- 握手
- 資料傳輸
握手完成後,不再進行http互動,客戶端的onopen將會觸發執行
當客戶端呼叫send傳送資料時,服務端觸發onmessage事件;當服務端呼叫send傳送資料時,客戶端觸發message事件。
當send傳送一條資料時,協議可能將這個資料封裝為一幀或多幀資料,然後逐幀傳送
網路安全
- tls/ssl
交換公鑰過程中,可能遇到中間人攻擊,所以應引入數字證照來認證。
建立私鑰:
openssl genrsa -out ryans-key.pem 2048
生成csr
openssl req -new -sha256 -key ryans-key.pem -out ryans-csr.pem
生成自簽名證照
openssl x509 -req -in ryans-csr.pem -signkey ryans-key.pem -out ryans-cert.pem
驗證:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('./ryans-key.pem'),
cert: fs.readFileSync('./ryans-cert.pem')
}
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end('hello world')
}).listen(2000)
複製程式碼
-k忽略掉證照的驗證
curl -k https://localhost:2000
構建web應用
基礎功能
請求方法
HTTP_Parser在解析請求報文的時候,將報文頭抽取出來,設定為req.method。有諸如:GET, POST, HEAD, PUT, DELETE, OPTIONS, TRACE, CONNECT
路徑解析
路徑部分存在於報文的第一行的第二部分,如:
GET /path?foo=bar HTTP/1.1
HTTP_Parser將其解析為req.url, 一般而言,完整的url地址如下
http://user:pass@host.com:8080/p/a/t/h?query=string#hash
這裡hash部分會被丟棄,不會存在於報文的任何地方, 下列的url物件不是報文中的,故有hash
解析出來的url物件
Url {
protocol: 'https:',
slashes: true,
auth: 'user:pass',
host: 'sub.host.com:8080',
port: '8080',
hostname: 'sub.host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash' }
複製程式碼
查詢字串
查詢字串,如果鍵出現多次,那麼它的值會是一個陣列
foo=bar&foo=baz
複製程式碼
var query = url.parse(req.url, true).query;
{
foo: ['bar', 'baz']
}
複製程式碼
cookie
cookie處理:
- 伺服器向客戶端傳送cookie
- 瀏覽器將cookie儲存
- 之後每次瀏覽器都會將cookie發向伺服器端
Set-Cookie: name=vale; Path=/;Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
path表示cookie影響路徑,表示伺服器目錄下的子html都能訪問
expires和max-age表示過期時間,一個是絕對時間,一個是相對時間
httpOnly告知瀏覽器不能通過document.cookie獲取
secure為true表示在https才有效
domain:子域名訪問父域名
**效能影響:**大多數cookie並不需要每次都用上,因為這會造成頻寬的部分浪費
解決:
- 減少cookie體積,設定path和domain
- 為不需要cookie的元件換個域名
- 減少dns查詢
session
session的資料只保留在伺服器端,客戶端無法修改。
應用:
- 基於cookie來實現使用者和資料的對映
將口令放在cookie中,口令一旦被褚昂愛,就丟失對映關係。通常session的有效期通常短,過期就將資料刪除
一旦伺服器檢查到使用者請求cookie中沒有攜帶session_id,它會為之生成一個值,這個值是唯一且不重複的值,並設定超時時間。如果過期就重新生成,如果沒有過期,就更新超時時間
var sessions = {};
var key = 'session_id';
var EXPIRES = 20*60*1000;
var generate = function () {
var session = {};
session.id = (new Date().getTime()) + Math.random();
session.cookie = {
expire: (new Date()).getTime() + EXPIRES
}
sessions[session.id] = session
}
function (req, res) {
var id = req.cookies[key];
if (!id) {
req.session = generate();
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > new Date().getTime()) {
session.cookie.expire = new Date().getTime() + EXPIRES;
req.session = session;
} else {
delete sessions[id];
req.session = generate();
}
} else {
req.session = generate();
}
}
}
複製程式碼
- 通過檢查字串來實現瀏覽器端和伺服器端資料的對應
原理:檢查查詢字串,如果沒有值,會生成新的帶值的url
var getURL = function (_url, key, value) {
var obj = url.parse(_url, true);
obj.query[key] = value;
return url.format(obj);
}
function (req, res) {
var redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end();
}
var id = req.query[key];
if (!id) {
var session = generate();
redirect(getURL(req.url), key, session.id);
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > new Date().getTime()) {
session.cookie.expire = new Date().getTime() + EXPIRES;
req.session = session;
handle(req, res);
} else {
delete sessions[id];
var session = generate();
redirect(getURL(req.url), key, session.id)
}
} else {
var session = generate();
redirect(getURL(req.url), key, session.id)
}
}
}
複製程式碼
隱患
由於session儲存在sessions物件中,故在記憶體中,若資料量加大,會引起垃圾回收的頻繁掃描,引起效能問題。
為了利用多核cpu而啟動多個程式,使用者請求的連線將可能隨意分配到各個程式中,node的程式與程式之間不能直接共享記憶體,使用者的session可能會引起錯亂
解決方案
將session集中化,將可能分散在多個程式裡的資料,統一轉移到集中資料儲存中。目前常用工具是redis,memcached。node無需在內部維護資料物件。
問題: 會引起網路訪問
session與安全
- 將口令通過私鑰加密,使得偽造的成本較高
快取
- 新增expires或者cache-control到報文頭中
- 配置etags
- 讓ajax可快取
設定last-modified
var handle = function (req, res) {
fs.stat(filename, function (err, stat) {
var lastModified = stat.mtime.toUTCString();
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, 'Not Modified');
res.end()
} else {
fs.readFile(filename, function (err, file) {
var lastModified = stat.mtime.toUTCString();
res.setHeader('Last-modified', lastModified);
res.writeHead(200, 'ok');
res.end(file);
})
}
})
}
複製程式碼
缺陷:
- 檔案的時間戳改動但內容不一定改動
- 時間戳只能精確到秒級別
設定etag
var getHash = function (str) {
var shasum = crypto.createHash('sha1');
return shasum.update(str).digest('base64');
}
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
var hash = getHash(file);
var noneMatch = req['if-none-match'];
if (hash === noneMath) {
res.writeHead(304, "Not Modified");
res.end()
} else {
res.setHeader("ETag", hash);
res.writeHead(200, "ok");
res.end(file);
}
})
}
複製程式碼
強制快取
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
res.setHeader("Cache-Control", "max-age=" + 10*365*24*60*60*1000);
res.writeHead(200, "ok");
res.end(file);
})
}
複製程式碼
用expires可能導致瀏覽器端與伺服器端時間不同步帶來的不一致性問題
清除快取
瀏覽器是根據url進行快取,那麼一旦內容有所更新時,我們就讓瀏覽器發起新的url請求,使得新內容能夠被客戶端更新。
資料上傳
var hasBody = function (req) {
return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
}
function (req, res) {
if (hasBody(req)) {
var buffers = [];
req.on('data', functino (chunk) {
buffers.push(chunk);
})
req.on('end', function () {
req.rawBody = Buffer.concat(buffers).toString(); // 拼接buffer
handle(req, res);
})
} else {
handle(req, res);
}
}
複製程式碼
處理json格式
// application/json;charset=utf-8;
var mime = function (req) {
var str = req.headers['content-type'] || '';
return str.split(';')[0]
}
var handle = function (req, res) {
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch(e) {
res.writeHead(400);
res.end("Invalid JSON");
return
}
}
todo(req, res)
}
複製程式碼
處理xml檔案
var xml2js = require('xml2.js');
var handle = function (req, res) {
if (mime(req) === 'appliction/xml') {
xml2js.parseString(req.rawBody, function (err, xml) {
if (err) {
res.writeHead(400);
res.end('Invalid XML');
return;
}
req.body = xml;
todo(req, res);
})
}
}
複製程式碼
圖片上傳
var formidable = require('formidable'),
http = require('http'),
util = require('util'),
fs = require('fs');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
fs.renameSync(files.upload.path,"./tmp/text.jpeg"); // 另存圖片
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
res.end(util.inspect({fields: fields, files: files}));
});
return;
}
if (req.url == '/')
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
);
}).listen(8080);
複製程式碼
資料上傳與安全
- 記憶體限制
在解析表單,json和xml部分,我們採取的策略是先儲存使用者提交的所有資料,然後再解析處理,最後才傳遞給業務邏輯。
弊端:資料量大,佔記憶體
解決方案:
- 限制上傳內容的大小,一旦超過限制停止接收資料,並相應400狀態碼
- 通過流式解析,將資料導向到磁碟中,node只儲存檔案路徑等小資料
限制大小方案程式碼:
var bytes = 1024;
function (req, res) {
var received = 0;
var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
if (len && len > bytes) {
res.writeHead(413);
res.end();
return;
}
req.on('data', function (chunk) {
received += chunk.length;
if (received > bytes) {
req.destroy();
}
})
handle(req, res);
}
複製程式碼
- csrf
var generateRandom = function (len) {
return crypto.randomBytes(Math.ceil(len*3/4)).toString('base64').slice(0, len);
}
var token = req.session._csrf || (req.session._crsf = generateRandom(24));
// 做頁面渲染的時候伺服器端渲染這個_csrf
複製程式碼
function (req, res) {
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
var _csrf = req.body._csrf;
if (token !== _csrf) {
res.writeHead(413);
res.end("禁止訪問");
} else {
handle(req, res);
}
}
複製程式碼
路由解析
檔案路徑型
- 靜態檔案,其url的路徑與網站目錄的路徑一致,無需轉換。
- 動態檔案,根據路徑執行動態指令碼,原理: web伺服器根據url路徑找到對應的檔案,如index.asp或者index.php。根據字尾尋找指令碼的解析器,並傳入http請求的上下文。然而node中無需按這種方式
mvc工作模式
- 路由解析,根據url尋找到對應的控制器和行為
- 行為呼叫相關的模型,進行資料操作
- 資料操作結束後,呼叫檢視和相關資料進行頁面渲染,輸出到客戶端
手工對映
自由對映,從入口程式中判斷url,然後執行對應的邏輯。
匹配的時候,能夠正則匹配
自然對映
/controller/action/param1/param2/param3
按約定去找controllers目錄下的user檔案,將其require出來,呼叫這個檔案模組的setting方法,其餘的引數直接傳遞到這個方法中
RESTful(representational state transfer)
需要區分請求方法
一個地址代表了一個資源,對這個資源的操作,主要體現在http請求方法上,不是體現在url上
設計:
POST,GET,PUT,DELETE
POST /user/add?username=jack
GET /user/remove?username=jack
複製程式碼
中介軟體
含義:指底層封裝細節,為上層提供更方便服務的意義,為我們封裝所有http請求細節處理的中介軟體
中介軟體效能
- 編寫高效的中介軟體
快取需要重複計算的結果,避免不必要的計算。
- 合理使用路由,是的不必要的中介軟體不參與請求處理過程
頁面渲染
內容響應
響應頭中的content-*欄位十分重要。
示例
Content-Encoding:gzip
Content-Length:21170
Content-Type:text/javascript;charfset=utf-8
複製程式碼
客戶端在接收到後,通過gzip來解碼報文體重的內容,用長度校驗報文體內容是否正確,然後在以字符集utf-8將解碼後的指令碼插入到文件節點中
- MIME
application/json, application/xml, application/pdf
- 附件下載
背景:無論響應的內容是什麼MIME,只需要彈出並下載它
Content-Disposition
判斷是應該將報文資料當做及時瀏覽的內容,還是可下載的附件。
inline // 內容只需檢視
attachment // 資料可以存為附件
複製程式碼
還能指定儲存時使用的檔名
Content-Disposition:attachment;filename="filename.txt"
響應附件api
res.sendfile = (filepath) => {
fs.stat(filepath, (err, stat) => {
let stream = fs.createReadStream(filepath);
res.setHeader("Content-Type", mime.lookup(filepath));
res.setHeader("Content-length", stat.size);
res.setHeader("Content-Disposition", 'attachment;filename="'+ path.basename(filepath) +'"')
res.writeHead(200);
stream.pipe(res);
})
}
複製程式碼
- 響應json
res.json = function (json) {
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(json))
}
複製程式碼
- 響應跳轉
res.redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(200);
res.end('redirect to' + url)
}
複製程式碼
檢視渲染
res.render = function (view, data) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
var html = render(view, data);
res.end(html)
}
複製程式碼
模板要素:
- 模板語言
- 包含模板語言的模板檔案
- 擁有動態資料的資料物件
- 模板引擎
- 語法分解
- 處理表示式
- 生成待執行的語句
- 與資料一起執行,生成最終字串
- 模板安全,防止xss,就是轉譯
function render (str, data) {
var tpl = str.replace(/<%=([\s\S]+?)%>/g, function (match, code) {
return "' + obj." + code + "+ '";
})
tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
var compiled = new Function('obj', tpl);
return compiled(data);
}
複製程式碼
整合檔案系統
fs.readFile('file/path', 'utf8', function (err, txt) {
if(err) {
res.writeHead(500, {'Content-Type': 'text/html'});
res.end('模板檔案錯誤');
return;
}
res.writeHead(200, {"Content-Type": "text/html"});
var html = render(compile(text), data);
res.end(html);
})
複製程式碼
這樣做每次都需要讀取模板檔案,因此可設定cache={}
模板效能
- 快取模板檔案
- 快取檔案編譯後的函式
程式
一個程式只能利用一個核,如何充分利用多核cpu伺服器
單執行緒上丟擲的異常沒有被捕獲,如何保證程式的健壯性和穩定性
石器時代:同步
一次只為一個請求服務
青銅時代:複製程式
通過程式的賦值同時服務更多的請求和使用者。程式賦值會導致記憶體浪費
白銀時代:多執行緒
一個執行緒服務一個請求,執行緒相對於程式的開銷要小,執行緒之間可以共享資料,記憶體浪費問題得到解決
但是執行緒上線文切換會產生時間消耗
黃金時代:事件驅動
解決高併發問題
單執行緒避免不必要的記憶體開銷和上下文切換
php為每個請求都簡歷獨立的上下文
多執行緒架構
master.js實現程式的複製
let fork = require('child_process').fork;
let cpus = require('os').cpus();
for (let i = 0; i < cpus.length; i++) {
fork('./worker.js');
}
複製程式碼
worker.js
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {"Content-Type": "text/plain"});
res.end('hello')
}).listen(parseInt(Math.random()*10000), '127.0.0.1')
複製程式碼
ps aux | grep worker.js
檢視程式的數量
lejunjie 3306 0.0 0.0 4267752 868 s001 S+ 11:18上午 0:00.00 grep worker.js
lejunjie 3171 0.0 0.3 4893888 21656 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3170 0.0 0.3 4893888 21632 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3169 0.0 0.3 4893888 21708 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3168 0.0 0.3 4893888 21664 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
複製程式碼
通過fork複製的程式都是一個獨立的程式,啟動多個程式只是為了充分將cpu資源利用起來,而不是為了解決併發問題
建立子程式
- spawn,啟動一個子程式來執行命令
cp.spawn('node', ['worker.js']);
- exec,情動一個子程式來執行命令
sp.exec('node worker.js', () => {})
- execFile
啟動一個子程式來執行可執行檔案
- fork
建立node子程式只需要指定要執行的javascript檔案模組
程式間通訊
主執行緒與工作執行緒之間通過onmessage和postMessage進行通訊,子程式物件則由send方法實現主程式向子程式傳送資料
parent.js
var cp = require('child_process');
var n = cp.fork('./child.js');
n.on('message', function (data) {
console.log('parent data: ' + data.name);
})
n.send({name: 'parent'})
複製程式碼
child.js
process.on('message', function (data) {
console.log('child: ' + data.name);
})
process.send({name: 'child'})
複製程式碼
結果
child: parent
parent data: child
複製程式碼
ipc程式間通訊(inter-process communication)
node中實現ipc通道的是管道技術,具體由libuv提供
父程式在實際建立子程式之前,會建立ipc通道並監聽它,然後才真正建立子程式,並通過環境變數告訴子程式這個ipc通道的檔案描述符。
雙向通訊,在系統核心中完成通訊,不用經過實際的網路層
控制程式碼傳送
多個程式監聽通過埠會丟擲EADDRINUSE異常,這是埠被佔用的情況。可以通過代理,在代理程式上做適當的負載均衡,使得每個子程式可以較為均衡地執行任務。但是代理程式連線到工作程式的過程需要用掉兩個檔案描述符
控制程式碼是一種可以用來標識資源的應用,他的內部包含了只想物件的檔案描述符。比如控制程式碼可以用來表示一個伺服器端socket物件,一個客戶端socket物件,一個udp套接字,一個管道等。
傳送控制程式碼使得主程式接收到socket請求後,將這個socket直接發給工作程式,而不是重新與工作程式之間建立新的socket連線來轉發資料。解決檔案描述符的浪費問題
parent.js
const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('handled by parent');
})
server.listen(1338, () => {
child1.send('server', server);
child2.send('server', server);
})
複製程式碼
child.js
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', function (socket) {
socket.end('handled by child , pid is' + process.pid);
})
}
})
複製程式碼
讓請求都由子程式處理
parent
const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('handled by parent');
})
server.listen(1338, () => {
child1.send('server', server);
child2.send('server', server);
server.close();
})
複製程式碼
child
var http = require('http');
var server = http.createServer((req, res) => {
res.writeHead(200, {"Content-Type": "text/plain"});
res.end("handled by child, pid is" + process.pid);
})
process.on('message', (m, tcp) => {
if (m === 'server') {
tcp.on('connection', function (socket) {
server.emit('connection', socket);
})
}
})
複製程式碼
多個子程式可以同時監聽相同埠,再沒有EADDRINUSE異常發生
總結:
- 傳送到ipc管道的實際是要傳送的控制程式碼檔案描述符
- 連線了ipc通道的子程式可以讀取到父程式發來的訊息,將字串還原成物件,才出發message時間將訊息體傳遞給應用層使用
- 並非任意型別的控制程式碼都能在程式之間傳遞,除非有完整的傳送和還原的過程
- 多個程式監聽同個埠不引起EADDRINUSE異常的原因
獨立啟動的程式中,tcp伺服器端socket套接字的檔案描述符並不相同,導致監聽到相同的埠時會丟擲異常
多個應用監聽相同埠時,檔案描述符同一時間只能被某一個程式所用,所以是搶佔式的
程式事件
- error,當子程式無法被複制建立,無法被殺死,無法傳送訊息時觸發
- exit,子程式退出時觸發
- close,在子程式的標準輸入輸出終止時觸發該事件
- disconnect,在父程式或子程式中呼叫disconnect方法時觸發
自動重啟
程式退出時,讓所有工作程式退出。子程式退出時重新create
const cp = require('child_process');
var server = require('net').createServer();
var cpus = require('os').cpus();
var workers = {};
function create () {
var worker = cp.fork('./child.js');
worker.on('exit', function () {
console.log('worker: ' + worker.pid + 'exited');
})
worker.send('server', server);
workers[worker.pid] = worker;
console.log('create worker pid: ' + worker.pid);
}
for (var i = 0; i < cpus.length; i++) {
create();
}
process.on('exit', function () {
for (var pid in workers) {
workers[pid].kill();
}
})
複製程式碼
在極端情況下,所有工作程式都停止接受新的連線,全出在等待退出的狀態。但在等程式完全退出才重啟的過程中,所有新來的請求可能存在沒有工作程式為新使用者服務的情景,這會丟掉大部分請求
因此可在子程式中監聽uncaughtException,然後傳送自殺訊號
process.on('uncaughtException', function (err) {
process.send({act: 'suicide'});
worker.close(function () {
process.exit(1);
})
})
複製程式碼
負載均衡
node預設提供的機制是採用作業系統的搶佔式策略。
新的策略是輪叫排程。工作方式是由主程式接受連線,將其一次分發給工作程式。
狀態共享
在多個程式之間共享資料
- 第三方資料儲存
實現同步:子程式向第三方進行定時輪訓
- 主動通知
主動通知子程式,輪訓。
cluster模組
要建立單機node叢集,由於有許多細節需要處理,於是引入cluster,解決多核cpu的利用率問題
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主程式 ${process.pid} 正在執行`);
// 衍生工作程式。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('listening', () => {
console.log('listening')
})
cluster.on('exit', (worker, code, signal) => {
console.log(`工作程式 ${worker.process.pid} 已退出`);
});
} else {
// 工作程式可以共享任何 TCP 連線。
// 在本例子中,共享的是一個 HTTP 伺服器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作程式 ${process.pid} 已啟動`);
}
process.on('exit', () => {
console.log('exit')
})
複製程式碼
原理:cluster模組就是child_process和net模組的組合應用。在fork子程式時,將socket的檔案描述符傳送給工作程式。通過so_reuseaddr埠重用,從而實現多個子程式共享埠。
產品化
專案工程化
專案的組織能力
- 目錄結構
- 構建工具
- 編碼規範
- 程式碼審查
部署流程
程式碼流程--》stage普通測試環境--》pre-release預釋出環境--》product實際生產環境
部署操作
node file.js以啟動應用,會站住一個命令列視窗,視窗退出程式也退出
nohup node app.js &
不結束通話程式的方式
bash指令碼, 解決程式id不容易查詢的問題。重啟,中斷,啟動
效能
動靜分離:
讓node只處理動態請求,將靜態檔案引導到專業的靜態檔案伺服器。用nginx或者專業的cdn來處理
cdn快取,將檔案放在離使用者儘可能近的伺服器
對靜態請求使用不同的域名或者多個域名還能消除掉不必要的cookie傳輸和瀏覽器對下載執行緒數的限制
啟用快取
提升服務速度,避免不必要的計算
多程式架構
讀寫分離
對資料庫進行主從設計,這樣讀取資料操作不再受到寫入的影響,降低了效能的影響。
日誌
寫到磁碟上
資料庫寫入要經歷鎖表,日誌等操作,如果大量訪問會排隊,進而記憶體洩露。
- 訪問日誌
- 異常日誌
監控報警
監控
- 日誌監控
通過監控異常日誌檔案的變動,將新增的異常按異常型別和數量反應出來。
監控訪問日誌,體現業務qps值,pv/uv,預知訪問高峰
- 響應時間
在nginx類的反向代理上監控
通過應用自行產生的訪問日誌來監控
- 程式監控
檢查作業系統中執行的應用程式數,對於採用多程式架構的web應用,就需要檢查工作程式的數量,如果低於預估值,就應當發出報警
- 磁碟監控
監控磁碟的用量,設定警戒值
- 記憶體監控
健康的記憶體是有升有降的
- cpu佔用監控
cpu分為核心態,使用者態,iowait等。
使用者態佔用高: 伺服器上應用大量cpu開銷
核心態佔用高:伺服器花費大量時間程式排程或者系統呼叫。
- cpu load監控(cpu平均負載)
描述作業系統當前的繁忙程度
指標過高,在node中可能體現在用子程式模組反覆啟動新的程式
- i/o負載
反應磁碟讀寫情況
- 網路監控
流入流量和流出流量
-
應用狀態監控
-
dns監控
報警的實現
- 郵件報警
- 簡訊報警