在 node.js 當中透過 fs
模組來操作檔案是相當常見的操作,然而如果在吞吐量比較大的情況下,任何有關 IO 的操作都應該非常小心。舉例來說,這是讀取檔案時相當常見的程式碼:
const fs = require('fs')
fs.readFile('./text.txt', (err, data) => {
if (!err) {
console.log(data.toString())
}
})
這個寫法在讀取小檔案時沒有什麼大問題,但如果檔案過大會造成記憶體的 footprint 負載龐大。node.js
當中會根據平台的整數指標長度來決定最大 Buffer 大小。
console.log(buffer.constants.MAX_LENGTH)
// 4294967296 = 4GB
也就是說檔案大於 4GB 時上面的寫法就有問題了,在 node.js
當中會直接噴錯。
透過 stream 操作
如果是比較有經驗的開發者,為了避免檔案大小造成的問題,通常都會用 fs.createReadStream
來操作檔案,跟 readFile
最大的差別在於透過 stream 的寫法我們可以將一大坨檔案拆成數個 chunk,每個 chunk 只有數十 KB 的大小。對於網路服務來說,透過 stream 來傳遞資料是相當自然的一件事,透過 stream 甚至能讓瀏覽器在接收到部分的 HTML 內容時就開始執行渲染,不用等到全部內容傳完。
透過 createReadStream
改寫會像下面這樣:
const fs = require('fs')
let data = ''
const stream = fs.createReadStream('./text.txt')
stream.on('data', chunk => {
data += chunk
})
stream.on('end', () => {
console.log(data)
})
乍看之下沒有什麼問題,程式也運作的很好,甚至可以套用在大多數的檔案處理當中。不過仔細看 data += chunk
會發現事有蹊蹺。有些開發者會將 chunk
很自然地當作字串來處理,但 stream 回傳的資料其實是 buffer
,所以 data += chunk
其實背後是將 chunk.toString()
後才拼接起來的。看到這裡,應該某些開發者的警鈴大作了。
沒錯!在字串當中最重要的就是編碼問題,在預設的情況下 buffer 轉字串是用 utf-8。因此這樣 data += chunk
的寫法有可能導致不正確的結果,因為 UTF-8 可以分別用 1, 2, 3, 4 個 bytes 來表示一個字。為了方便展示,我把 highWaterMark
調整為 5 bytes。
// text.txt
這是一篇部落格PO文
const fs = require('fs')
let data = ''
const stream = fs.createReadStream('./text.txt', { highWaterMark: 5 })
stream.on('data', chunk => {
data += chunk
})
stream.on('end', () => {
console.log(data)
})
顯示結果為:
這��一���部落��PO��
因為一個 chunk 最多 5 bytes,因此在 chunk.toString()
有可能出現不正確的編碼,因為還沒有收到全部的資料。
正確拼接方式:Buffer.concat
如果要正確使用 Buffer 的話,最好用 node.js
提供的 API 操作再轉為字串,避免發生編碼的問題。
const fs = require('fs')
let data = []
let size = 0
const stream = fs.createReadStream('./text.txt', { highWaterMark: 5 })
stream.on('data', chunk => {
data.push(chunk)
size += chunk.length
})
stream.on('end', () => {
Buffer.concat(data, size).toString()
})
這樣一來就避免了編碼的問題。不過這樣寫說真的還挺麻煩的,如果只是做簡單的分析,直接用 readFile
、readFileSync
並沒有什麼不好,不過如果是要處理大檔案的分析或是高吞吐的話,這些就是不得不注意的細節了。(吐槽:不過這時候可能就是用其他語言來寫了)
結語
大檔案操作不要直接整坨進出記憶體,盡量以 stream 來操作,而在傳輸時盡量以 Buffer
來操作提高吞吐量,並避免不必要的編碼,但同時也要注意編碼時的相關操作。