Неожиданные ошибки при быстрой записи в файлы JSON

#node.js #json #fs

Вопрос:

Я пытаюсь реализовать журналы JSON для своего Node.js сервер; однако, когда я быстро отправляю запросы, 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-страницами, которые сами по себе делали больше запросов, гораздо меньшее количество запросов вызывало бы эти ошибки.

В чем причина этих ошибок и как я могу их исправить, особенно вторую?

Комментарии:

1. Я верю, что вы могли бы просто await fsp.writeFile(...) . Вы этого не ждете, у меня есть интуиция, что это может решить проблему

Ответ №1:

Одновременные запросы к вашему appendJson() методу являются причиной вашей проблемы. Пока выполняется один веб-запрос, поступает другой. Вы должны организовать доступ к файлу журнала таким образом, чтобы в любое время выполнялся только один одновременный доступ.

Что-то подобное может сработать, если у вас есть только один файл журнала.

Там есть 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
}
 

Вероятно, вы сможете заставить это работать. Но, при всем уважении, это плохой способ ведения журнала.Почему? Нагрузка на процессор и ввод-вывод при записи каждого элемента файла журнала пропорциональна количеству элементов, уже содержащихся в файле журнала. На жаргоне compsci big-O это означает, что запись файла loc равна O(n в квадрате).

Это означает, что чем успешнее становится ваше приложение, тем медленнее оно будет работать.

Есть причина, по которой файлы журналов содержат отдельные строки журнала, а не полные объекты JSON: чтобы избежать такого снижения производительности. Если вам нужен объект JSON для обработки строк журнала, создайте его при чтении журнала, а не при его записи.

Комментарии:

1. без вашего последнего абзаца: Один из способов избежать необходимости читать и записывать весь файл каждый раз, когда строка регистрируется , — это добавить одну строку JSON новую строку . Существуют библиотеки для чтения файлов JSON с разделителями новой строки (это очень простой формат).