<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[50MB Node.js бандл и потоки: читаем гигабайты логов без OOM]]></title><description><![CDATA[<p dir="auto">Когда Node.js приложение начинает жрать память при обработке больших файлов логов, виноват обычно не сам язык, а неправильная стратегия чтения данных. Большинство новичков загружают весь файл в оперативку через <code>fs.readFile()</code>, а потом удивляются, почему приложение падает с Out-of-Memory на 100MB файле, хотя процесс стартует с 50MB бандла.</p>
<p dir="auto">В этой статье разберёмся, как правильно работать с потоками, почему они спасают память и какие грабли подстерегают даже опытных разработчиков. Не будет никаких поверхностных советов — только под капот, только практика.</p>
<h2>Почему памяти всегда не хватает?</h2>
<p dir="auto">Напомню: Node.js работает на одном потоке в цикле событий (event loop). Когда ты вызываешь <code>fs.readFile()</code>, весь файл читается в буфер целиком, и этот буфер живёт в V8 heap до тех пор, пока ты его не отпустишь. Если файл больше свободной памяти — привет, OOM.</p>
<p dir="auto">Дело усугубляется, если приложение уже занимает 50MB бандлом (модули, парсеры, все эти дело). Остаётся жалкие 100-150MB, если контейнер выделил 256MB памяти по умолчанию. Один большой файл — и система просит кредит у своп-памяти, затем вообще падает.</p>
<p dir="auto">Потоки (streams) решают эту проблему элегантно: вместо загрузки всего файла сразу, ты читаешь его чанками. Каждый кусок обрабатывается и выбрасывается, освобождая память. <strong>Это не оптимизация — это правильная архитектура для работы с большими данными</strong>.</p>
<h2>Streams: как они работают под капотом</h2>
<p dir="auto">Поток в Node.js — это по сути конвейер. На одном конце входные данные (readable), на другом — результат (writable), посредине — трансформация (transform). Система автоматически регулирует скорость чтения в зависимости от скорости обработки, чтобы буфер не переполнялся.</p>
<p dir="auto">Когда ты создаёшь <code>fs.createReadStream()</code>, Node.js не читает весь файл в память. Вместо этого он берёт кусок (по умолчанию 64KB), эмитит событие <code>data</code>, ждёт, пока ты обработаешь этот кусок, потом берёт следующий. Если обработка медленная, поток паузирует и ждёт. Это называется <strong>backpressure</strong> — естественное регулирование потока данных.</p>
<p dir="auto">Вот классический пример, который часто делают неправильно:</p>
<pre><code class="language-javascript">// Плохо: весь файл в памяти
const data = fs.readFileSync('huge.log');
const lines = data.toString().split('\n');
lines.forEach(line =&gt; processLine(line));

// Хорошо: потоки и трансформация
const readline = require('readline');
const fs = require('fs');

const stream = fs.createReadStream('huge.log');
const rl = readline.createInterface({
  input: stream,
  crlfDelay: Infinity
});

rl.on('line', (line) =&gt; {
  processLine(line);
});
</code></pre>
<p dir="auto">Второй вариант обработает файл в сотни раз большего размера на тех же 50MB памяти, потому что в каждый момент времени в буфере живёт только несколько строк.</p>
<h2>Реальная задача: парсим логи и считаем ошибки</h2>
<p dir="auto">Представь: нужно прочитать логи размером в 5GB и найти все строки с ошибками, сгруппировать их по типам. Если просто загрузить в память — мертвец. Потоки же позволяют обработать это за несколько минут, не упав.</p>
<p dir="auto">Вот как строится такое решение: чтение потоком, парсинг каждой строки, фильтрация и накопление статистики. Вся работа идёт параллельно в рамках одного потока цикла событий (помнишь, это не системные потоки, а асинхронные операции).</p>
<p dir="auto">Примерная архитектура:</p>
<pre><code class="language-javascript">const fs = require('fs');
const { Transform } = require('stream');
const readline = require('readline');

const errors = {};
const stream = fs.createReadStream('app.log');

const logParser = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    try {
      const line = chunk.toString();
      const parsed = JSON.parse(line);
      if (parsed.level === 'ERROR') {
        this.push(parsed);
      }
    } catch (e) {
      // Пропускаем невалидные строки
    }
    callback();
  }
});

const aggregator = new Transform({
  objectMode: true,
  transform(errorObj, encoding, callback) {
    const key = errorObj.code || 'unknown';
    errors[key] = (errors[key] || 0) + 1;
    callback();
  }
});

stream
  .pipe(logParser)
  .pipe(aggregator)
  .on('finish', () =&gt; {
    console.log('Обработано, результаты:', errors);
  });
</code></pre>
<p dir="auto">Видишь? Два Transform потока цепляются через <code>pipe()</code>, и данные текут от одного к другому. Если парсер перегружен, чтение замедляется. Если агрегатор не справляется, парсер ждёт. <strong>Система сама балансирует нагрузку</strong>.</p>
<h2>Частые ошибки при работе с потоками</h2>
<p dir="auto">Даже когда понимаешь теорию, можно наступить на грабли. Вот типичные проблемы:</p>
<p dir="auto"><strong>Накопление данных вместо обработки.</strong> Многие пишут код, который слушает <code>data</code> и складывает всё в массив:</p>
<pre><code class="language-javascript">const chunks = [];
readable.on('data', chunk =&gt; {
  chunks.push(chunk); // Это же OOM, только медленнее!
});
</code></pre>
<p dir="auto">Это то же самое, что загрузить файл целиком. Нужно обрабатывать данные <em>сразу</em>, когда они приходят.</p>
<p dir="auto"><strong>Забывчивость про backpressure.</strong> Если не слушать сигнал <code>drain</code>, поток может переполниться:</p>
<pre><code class="language-javascript">// Плохо: игнорируем backpressure
readable.on('data', chunk =&gt; {
  writable.write(chunk); // Может выбросить всё в памяти
});

// Хорошо: учитываем сигнал
readable.on('data', chunk =&gt; {
  const flushed = writable.write(chunk);
  if (!flushed) {
    readable.pause();
  }
});

writable.on('drain', () =&gt; {
  readable.resume();
});
</code></pre>
<p dir="auto">К счастью, <code>pipe()</code> делает это автоматически, поэтому используй его, где возможно.</p>
<p dir="auto"><strong>Утечки памяти при обработке ошибок.</strong> Если поток упадёт с ошибкой и не закроется, буферы останутся в памяти:</p>
<pre><code class="language-javascript">stream
  .pipe(transform)
  .pipe(writable)
  .on('error', (err) =&gt; {
    console.error('Ошибка:', err);
    stream.destroy(); // ОБЯЗАТЕЛЬНО закрой поток!
    // иначе ждёшь утечку памяти
  });
</code></pre>
<p dir="auto"><strong>Мешанина с объектными потоками.</strong> Когда используешь <code>objectMode: true</code>, нельзя просто так пайпить в обычный writable. Нужна промежуточная сериализация.</p>
<h2>Практические трюки для массивных логов</h2>
<p dir="auto">Есть несколько техник, которые облегчают жизнь конкретно при работе с логами:</p>
<ul>
<li>
<p dir="auto"><strong>Разбиение по размеру.</strong> Не обрабатывай один 100GB файл. Разбей на части по 100MB, обрабатывай параллельно (но не более 4-5 потоков, чтобы не упал бекенд). Merge результаты в конце.</p>
</li>
<li>
<p dir="auto"><strong>Прессование данных.</strong> Если логи уже гжипованы, Node.js умеет распаковывать их прямо в потоке через <code>zlib.createGunzip()</code>. Это даже экономнее, чем распаковывать файл на диск.</p>
</li>
</ul>
<pre><code class="language-javascript">const zlib = require('zlib');

fs.createReadStream('logs.gz')
  .pipe(zlib.createGunzip())
  .pipe(logParser)
  .pipe(aggregator);
</code></pre>
<ul>
<li>
<p dir="auto"><strong>Worker threads для тяжёлого парсинга.</strong> Если парсинг логов требует вычислений (regex, шифрование, что-то сложное), распредели это на worker threads. Main thread будет читать и писать, workers — обрабатывать.</p>
</li>
<li>
<p dir="auto"><strong>Прогрессивная запись результатов.</strong> Не жди, пока обработаются все логи, чтобы написать результат. Пиши постепенно в базу или файл. Это разгружает память.</p>
</li>
</ul>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Метод</th>
<th>Скорость</th>
<th>Память</th>
<th>Сложность</th>
<th>Для логов</th>
</tr>
</thead>
<tbody>
<tr>
<td>readFile</td>
<td>Быстро</td>
<td>Ужас</td>
<td>Просто</td>
<td>Нет</td>
</tr>
<tr>
<td>Streams + pipe</td>
<td>Нормально</td>
<td>Отлично</td>
<td>Простой</td>
<td>Да</td>
</tr>
<tr>
<td>Worker threads</td>
<td>Быстро</td>
<td>Хорошо</td>
<td>Средне</td>
<td>Да</td>
</tr>
<tr>
<td>Разбиение по частям</td>
<td>Зависит</td>
<td>Хорошо</td>
<td>Сложно</td>
<td>Да</td>
</tr>
</tbody>
</table>
<h2>Когда streams — оverkill?</h2>
<p dir="auto">Потоки — мощный инструмент, но не всегда нужны. Если файл 1MB и памяти в системе 2GB, можешь просто читать целиком. Не усложняй код без причины.</p>
<p dir="auto">А вот когда потоки действительно спасают жизнь: обработка логов на боевом сервере, экспорт больших таблиц из БД, трансформация видео, генерация PDF из огромных шаблонов. Везде, где данные больше, чем разумно держать в памяти.</p>
<p dir="auto">Помни также: потоки — это не только про экономию памяти. Это про асинхронность и неблокирующую обработку. Читаешь с диска не блокируя весь процесс, обрабатываешь параллельно, пишешь результат. Идеально для микросервисов, где каждая миллисекунда задержки — потеря денег.</p>
<h2>Отладка и профилирование потоков</h2>
<p dir="auto">Когда память всё равно растёт, нужна диагностика. Node.js встроенный инспектор поможет: запусти приложение с флагом <code>--inspect</code> и подключайся из DevTools.</p>
<p dir="auto">Обрати внимание на heap snapshots: сравни снимки памяти в начале обработки и в конце. Если осталась куча объектов, которых не должно быть, есть утечка. Обычно виноваты event listeners, которые не удалили после завершения потока.</p>
<p dir="auto">Есть и простой способ: используй <code>console.log(process.memoryUsage())</code> в нескольких местах обработки. Если свободная память только снижается, значит что-то накапливается.</p>
<pre><code class="language-javascript">const used = process.memoryUsage();
console.log(`Heap used: ${Math.round(used.heapUsed / 1024 / 1024)}MB`);
console.log(`External: ${Math.round(used.external / 1024 / 1024)}MB`);
</code></pre>
<h2>Вот так потоки решают проблему целиком</h2>
<p dir="auto">Популярный миф: чтобы работать с большими данными в Node.js, нужны worker threads или Go. На самом деле обычные потоки справляются с гигабайтами данных, если используешь их правильно. 50MB бандл приложения и 5GB лог-файл — не конкурирующие ресурсы, потому что на диск/сеть идёт асинхронная операция, не блокирующая цикл событий.</p>
<p dir="auto">Но есть нюансы: потоки — это не серебряная пуля. Если обработка каждой строки требует вычислений, это всё равно замораживает event loop на микросекунды. Для действительно heavy lifting нужны worker threads. Но для чтения, фильтрации и агрегации — потоки идеальны и простоваты.</p>
]]></description><link>https://forum.exlends.com/topic/2163/50mb-node.js-bandl-i-potoki-chitaem-gigabajty-logov-bez-oom</link><generator>RSS for Node</generator><lastBuildDate>Thu, 23 Apr 2026 14:19:34 GMT</lastBuildDate><atom:link href="https://forum.exlends.com/topic/2163.rss" rel="self" type="application/rss+xml"/><pubDate>Wed, 22 Apr 2026 11:40:59 GMT</pubDate><ttl>60</ttl></channel></rss>