快速写入 JSON 文件时出现意外错误
Posted
技术标签:
【中文标题】快速写入 JSON 文件时出现意外错误【英文标题】:Unexpected errors when rapidly writing to JSON files 【发布时间】:2021-08-21 10:48:09 【问题描述】:我正在尝试为我的 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 页面自己发出更多请求,那么会导致这些错误的请求要少得多。
这些错误的原因是什么,我该如何解决它们,尤其是第二个?
【问题讨论】:
我相信你可以简单地await fsp.writeFile(...)
。你不是在等待它,我有直觉它可能会解决问题
【参考方案1】:
对您的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 lingo 中,这意味着 loc 文件的写入是 O(n 平方)。
这意味着您的应用越成功,运行速度就越慢。
日志文件包含单独的日志行而不是完整的 JSON 对象是有原因的:以避免这种性能损失。如果您需要 JSON 对象来处理日志行,请在读取日志时创建它,而不是在编写时创建。
【讨论】:
w/r/t 你的最后一段:防止每次记录一行时都必须读取和写入整个文件的一种方法是append line of JSON + a newline。存在读取换行符分隔的 JSON 文件的库(这是一种非常简单的格式)。以上是关于快速写入 JSON 文件时出现意外错误的主要内容,如果未能解决你的问题,请参考以下文章
在“...ge-2.2.1.tgz”,“engin”附近解析时出现错误的 JSON 输入意外结束
尝试在python子进程中运行rsync时出现意外的远程arg错误
SQLCL - 引用文件中存在 SQL 错误时出现意外 Java 异常
C# HttpWebRequest POST => !!处理请求时出现意外错误:无效的 %-encoding