快速写入 JSON 个文件时出现意外错误
Unexpected errors when rapidly writing to JSON files
我正在尝试为我的 Node.js 服务器实现 JSON 日志;但是,当我快速发送请求时,JSON.parse()
会抛出错误。我相信这可能是由于并发读取和写入我的日志文件引起的,因为 fs
方法是异步的。
我收到的其中一个错误是:
SyntaxError: Unexpected end of JSON input
这可以通过降低请求速率来解决。
然而,其他时候,JSON 本身会出现语法错误,并且在我删除它们并重新启动服务器之前无法解析日志:
SyntaxError: Unexpected token [TOKEN] in JSON at position [POSITION]
有时日志的结尾看起来像这样,以额外的 ]
:
结尾
[
...,
{
"ip": ...,
"url": ...,
"ua": ...
}
]]
或者这样:
[
...,
{
"ip": ...,
"url": ...,
"ua": ...
}
]
]
这是我的服务器的一个非常简化的版本:
"use strict"
const fsp = require("fs").promises
const http = require("http")
const appendJson = async (loc, content) => {
const data = JSON.parse(
await fsp.readFile(loc, "utf-8").catch(err => "[]")
)
data.push(content)
fsp.writeFile(loc, JSON.stringify(data))
}
const logReq = async (req, res) => {
appendJson(__dirname + "/log.json", {
ip: req.socket.remoteAddress,
url: req.method + " http://" + req.headers.host + req.url,
ua: "User-Agent: " + req.headers["user-agent"],
})
}
const html = `<head><link rel="stylesheet" href="/main.css"></head><script src="/main.js"></script>`
const respond = async (req, res) => {
res.writeHead(200, { "Content-Type": "text/html" }).end(html)
logReq(req, res)
}
http.createServer(respond).listen(8080)
我测试了在 Firefox 和 Chromium 中发送大量请求(但出于某种原因,使用 cURL 发送甚至数千个请求都没有导致错误),方法是快速刷新页面,或在浏览器控制台中打开多个选项卡:
for (let i = 0; i < 200; i++)
window.open("http://localhost:8080")
通常情况下,完整的 HTML 页面会自行发出更多请求,更少的请求会导致这些错误。
这些错误的原因是什么,我该如何解决它们,尤其是第二个错误?
对您的 appendJson()
方法的并发请求是导致问题的原因。当一个 Web 请求正在进行时,另一个 Web 请求进入。您必须组织对日志文件的访问,以便任何时候都只有一个并发访问在进行中。
如果您只有一个日志文件,这样的方法可能会奏效。
有一个 fileAccessInProgress
标志和一个要写入文件的项目队列。每个新项目都会附加到队列中。然后,如果文件访问未激活,队列的内容将被写出。如果新项目在访问过程中到达,它们也会附加到队列中。
let fileAccessInProgress = false
let logDataQueue = []
const appendJson = async (loc, content) => {
logDataQueue.push(content)
if (fileAccessInProgress) return
fileAccessInProgress = true
while (logDataQueue.length > 0) {
const data = JSON.parse(
await fsp.readFile(loc, "utf-8").catch(err => "[]")
)
while (logDataQueue.length > 0) data.push(logDataQueue.shift())
await fsp.writeFile(loc, JSON.stringify(data))
}
fileAccessInProgress = false
}
你或许可以让它工作。但恕我直言,处理日志记录的方式很糟糕。 为什么?写入每个日志文件项的 CPU 和 I/O 工作量与日志文件中已有的项数成正比。在 compsci big-O 术语中,这意味着 loc 文件的写入是 O(n 平方).
这意味着您的应用越成功,它就会越慢运行。
日志文件包含单独的日志行而不是完整的 JSON 对象是有原因的:避免这种性能损失。如果您需要一个 JSON 对象来处理您的日志行,请在读取日志时创建它,而不是在写入时创建它。
我正在尝试为我的 Node.js 服务器实现 JSON 日志;但是,当我快速发送请求时,JSON.parse()
会抛出错误。我相信这可能是由于并发读取和写入我的日志文件引起的,因为 fs
方法是异步的。
我收到的其中一个错误是:
SyntaxError: Unexpected end of JSON input
这可以通过降低请求速率来解决。
然而,其他时候,JSON 本身会出现语法错误,并且在我删除它们并重新启动服务器之前无法解析日志:
SyntaxError: Unexpected token [TOKEN] in JSON at position [POSITION]
有时日志的结尾看起来像这样,以额外的 ]
:
[
...,
{
"ip": ...,
"url": ...,
"ua": ...
}
]]
或者这样:
[
...,
{
"ip": ...,
"url": ...,
"ua": ...
}
]
]
这是我的服务器的一个非常简化的版本:
"use strict"
const fsp = require("fs").promises
const http = require("http")
const appendJson = async (loc, content) => {
const data = JSON.parse(
await fsp.readFile(loc, "utf-8").catch(err => "[]")
)
data.push(content)
fsp.writeFile(loc, JSON.stringify(data))
}
const logReq = async (req, res) => {
appendJson(__dirname + "/log.json", {
ip: req.socket.remoteAddress,
url: req.method + " http://" + req.headers.host + req.url,
ua: "User-Agent: " + req.headers["user-agent"],
})
}
const html = `<head><link rel="stylesheet" href="/main.css"></head><script src="/main.js"></script>`
const respond = async (req, res) => {
res.writeHead(200, { "Content-Type": "text/html" }).end(html)
logReq(req, res)
}
http.createServer(respond).listen(8080)
我测试了在 Firefox 和 Chromium 中发送大量请求(但出于某种原因,使用 cURL 发送甚至数千个请求都没有导致错误),方法是快速刷新页面,或在浏览器控制台中打开多个选项卡:
for (let i = 0; i < 200; i++)
window.open("http://localhost:8080")
通常情况下,完整的 HTML 页面会自行发出更多请求,更少的请求会导致这些错误。
这些错误的原因是什么,我该如何解决它们,尤其是第二个错误?
对您的 appendJson()
方法的并发请求是导致问题的原因。当一个 Web 请求正在进行时,另一个 Web 请求进入。您必须组织对日志文件的访问,以便任何时候都只有一个并发访问在进行中。
如果您只有一个日志文件,这样的方法可能会奏效。
有一个 fileAccessInProgress
标志和一个要写入文件的项目队列。每个新项目都会附加到队列中。然后,如果文件访问未激活,队列的内容将被写出。如果新项目在访问过程中到达,它们也会附加到队列中。
let fileAccessInProgress = false
let logDataQueue = []
const appendJson = async (loc, content) => {
logDataQueue.push(content)
if (fileAccessInProgress) return
fileAccessInProgress = true
while (logDataQueue.length > 0) {
const data = JSON.parse(
await fsp.readFile(loc, "utf-8").catch(err => "[]")
)
while (logDataQueue.length > 0) data.push(logDataQueue.shift())
await fsp.writeFile(loc, JSON.stringify(data))
}
fileAccessInProgress = false
}
你或许可以让它工作。但恕我直言,处理日志记录的方式很糟糕。 为什么?写入每个日志文件项的 CPU 和 I/O 工作量与日志文件中已有的项数成正比。在 compsci big-O 术语中,这意味着 loc 文件的写入是 O(n 平方).
这意味着您的应用越成功,它就会越慢运行。
日志文件包含单独的日志行而不是完整的 JSON 对象是有原因的:避免这种性能损失。如果您需要一个 JSON 对象来处理您的日志行,请在读取日志时创建它,而不是在写入时创建它。