快速写入 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 对象来处理您的日志行,请在读取日志时创建它,而不是在写入时创建它。