快速写入 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

通过 Google 表格解析时出现“JavaScript 运行时意外退出”错误

运行conf.js文件时出现意外的标识符错误