Skip to content
StackPractices
intermediate Por Mathias Paulenko

Escribir Archivos Grandes

Cómo escribir archivos grandes de forma eficiente usando salida bufferizada y streaming.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Visión General

Escribir datasets o logs masivos en disco requiere técnicas bufferizadas y de streaming para evitar picos de memoria y cuellos de botella de I/O. A continuacion se cubre patrones eficientes de escritura de archivos en Python, JavaScript y Java.

Cuándo Usar

Usa este recurso cuando:

  • Generas archivos de exportación grandes (CSV, JSONL, XML) desde consultas a base de datos
  • Añades registros a archivos de log en servicios de larga duración
  • Transmites datos transformados a disco sin mantener todo el payload en memoria

Solución

Python

# Escritura bufferizada de texto
with open('output.log', 'w', encoding='utf-8') as f:
    for record in data_source:
        f.write(f"{record}\n")

# Escritura binaria por chunks
with open('output.bin', 'wb') as f:
    for chunk in byte_generator():
        f.write(chunk)

JavaScript

const fs = require('fs');

// Stream writer
const stream = fs.createWriteStream('output.log');
for (const record of dataSource) {
    stream.write(`${record}\n`);
}
stream.end();

// Finalización basada en Promise
await new Promise((resolve, reject) => {
    stream.on('finish', resolve);
    stream.on('error', reject);
});

Java

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class LargeFileWriter {
    // Escritor de texto bufferizado
    public void writeLines(String path, Iterable<String> lines) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(path))) {
            for (String line : lines) {
                writer.write(line);
                writer.newLine();
            }
        }
    }

    // Escritor binario por chunks
    public void writeChunks(String path, Iterable<byte[]> chunks) throws IOException {
        try (FileChannel channel = FileChannel.open(Paths.get(path),
                StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            for (byte[] chunk : chunks) {
                channel.write(ByteBuffer.wrap(chunk));
            }
        }
    }
}

Explicación

Los escritores bufferizados reducen la cantidad de system calls acumulando datos en memoria antes de volcarlos a disco. Las escrituras con streaming procesan y emiten datos incrementalmente, manteniendo el uso de memoria constante sin importar el tamaño total de salida. FileChannel en Java provee transferencias directas buffer-a-canal, minimizando copias entre espacio de usuario y kernel.

Variantes

TecnologíaEnfoqueNotas
Pythontempfile + renombrado atómicoEscribe en temp, luego mueve para seguridad ante fallos
JavaScriptpipeline()Piping consciente de backpressure entre streams
JavaFileOutputStream con BufferedOutputStreamIO clásica, más simple pero ligeramente más lenta que NIO

Lo que funciona

  1. Siempre cierra o finaliza streams para volcar buffers internos y liberar descriptores
  2. Usa patrones de renombrado atómico (escribir en archivo temporal, luego renombrar) para prevenir archivos parciales ante un crash
  3. Ajusta tamaños de buffer basados en el tamaño de bloque del disco (típicamente 4 KB u 8 KB)
  4. Maneja errores de stream para evitar pérdida silenciosa de datos
  5. Para escritores concurrentes, usa bloqueo de archivos o modos append-only

Errores Comunes

  1. Construir un string gigante en memoria antes de escribir en lugar de hacer streaming
  2. Ignorar errores del stream de escritura, que pueden dejar archivos truncados
  3. Usar llamadas de escritura síncronas en bucles críticos de rendimiento
  4. No hacer flush antes de salir del proceso, perdiendo datos bufferizados
  5. Sobrescribir archivos originales in-place sin una estrategia de backup

Preguntas Frecuentes

¿Debo usar modo append o reescribir?

Usa append ('a' en Python, flag 'a' en Node, StandardOpenOption.APPEND en Java) para logs. Usa renombrado atómico para archivos de datos que deben mantenerse consistentes.

¿Cómo manejo errores de disco lleno?

Captura IOException (Java), evento error en streams (JS) o OSError (Python). Pre-verificar espacio disponible con shutil.disk_usage (Python) o fs.statvfs (Node) puede ayudar.

¿Es BufferedWriter más rápido que FileWriter?

Sí. BufferedWriter agrupa escrituras, reduciendo syscalls. La diferencia es dramática para muchas escrituras pequeñas y negligible para escrituras de bloques grandes.

Soluciones Avanzadas

Python: Escritura atómica con tempfile y seguimiento de progreso

import os
import tempfile
from pathlib import Path
from typing import Iterable

def atomic_write_lines(path: str | Path, lines: Iterable[str],
                       encoding: str = 'utf-8',
                       buffer_size: int = 8192) -> None:
    """Escribe líneas atómicamente: escribe en archivo temporal, luego renombra."""
    path = Path(path)
    tmp_fd, tmp_path = tempfile.mkstemp(
        dir=path.parent, suffix='.tmp', prefix=path.name
    )
    try:
        with os.fdopen(tmp_fd, 'w', encoding=encoding, buffering=buffer_size) as f:
            for line in lines:
                f.write(line)
                f.write('\n')
        os.replace(tmp_path, path)  # Atómico en POSIX y Windows
    except Exception:
        os.unlink(tmp_path)
        raise

def write_csv_streaming(path: str | Path, rows: Iterable[dict],
                        headers: list[str],
                        chunk_size: int = 10000) -> None:
    """Stream de exports CSV grandes sin cargar todas las filas en memoria."""
    import csv
    path = Path(path)
    tmp_fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix='.tmp')
    try:
        with os.fdopen(tmp_fd, 'w', encoding='utf-8', newline='') as f:
            writer = csv.DictWriter(f, fieldnames=headers)
            writer.writeheader()
            count = 0
            for row in rows:
                writer.writerow(row)
                count += 1
                if count % chunk_size == 0:
                    f.flush()  # Flush periódico para exports de larga duración
        os.replace(tmp_path, path)
        print(f"Escritas {count} filas en {path}")
    except Exception:
        os.unlink(tmp_path)
        raise

# Uso
# atomic_write_lines('/etc/app/config.conf', generate_config())
# write_csv_streaming('/exports/users.csv', query_users(), ['id', 'name', 'email'])

JavaScript: Pipeline con backpressure y seguimiento de progreso

const { pipeline } = require('stream');
const fs = require('fs');
const { Transform } = require('stream');
const path = require('path');

async function writeLargeCsv(destPath, rowGenerator, headers) {
    const tmpPath = destPath + '.tmp';
    const writeStream = fs.createWriteStream(tmpPath, { highWaterMark: 64 * 1024 });

    // Transform stream personalizado para formateo CSV
    const csvTransform = new Transform({
        objectMode: false,
        highWaterMark: 64 * 1024,
        transform(chunk, encoding, callback) {
            const line = Array.isArray(chunk) ? chunk.join(',') : chunk;
            callback(null, line + '\n');
        },
    });

    // Escribir cabecera
    writeStream.write(headers.join(',') + '\n');

    // Stream de filas con manejo de backpressure
    let count = 0;
    for await (const row of rowGenerator) {
        const canContinue = writeStream.write(row.join(',') + '\n');
        count++;
        if (!canContinue) {
            await new Promise(resolve => writeStream.once('drain', resolve));
        }
        if (count % 10000 === 0) {
            console.log(`Progreso: ${count} filas escritas`);
        }
    }

    writeStream.end();
    await new Promise((resolve, reject) => {
        writeStream.on('finish', resolve);
        writeStream.on('error', reject);
    });

    // Renombrado atómico
    await fs.promises.rename(tmpPath, destPath);
    console.log(`Listo: ${count} filas en ${destPath}`);
}

// Uso
// async function* generateRows() { for (let i = 0; i < 1000000; i++) yield [i, `user${i}`]; }
// writeLargeCsv('./users.csv', generateRows(), ['id', 'name']);

Java: Escritura con memory-mapped files para archivos binarios grandes

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.util.stream.Stream;

public class LargeFileWriter {

    // Escritura memory-mapped para archivos binarios grandes
    public void writeMapped(String path, byte[] data, int chunkSize) throws IOException {
        try (FileChannel channel = FileChannel.open(
                Paths.get(path),
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE,
                StandardOpenOption.TRUNCATE_EXISTING)) {

            long position = 0;
            int offset = 0;
            while (offset < data.length) {
                int remaining = data.length - offset;
                int size = Math.min(chunkSize, remaining);
                MappedByteBuffer buffer = channel.map(
                    FileChannel.MapMode.READ_WRITE, position, size);
                buffer.put(data, offset, size);
                offset += size;
                position += size;
            }
            channel.force(true);  // Forzar flush a disco
        }
    }

    // CSV streaming con BufferedWriter y flush periódico
    public void writeCsvStreaming(String path, Stream<String[]> rows, String[] headers)
            throws IOException {
        Path tmpPath = Paths.get(path + ".tmp");
        try (BufferedWriter writer = Files.newBufferedWriter(tmpPath)) {
            writer.write(String.join(",", headers));
            writer.newLine();
            long count = 0;
            for (String[] row : (Iterable<String[]>) rows::iterator) {
                writer.write(String.join(",", row));
                writer.newLine();
                count++;
                if (count % 10000 == 0) {
                    writer.flush();
                }
            }
            System.out.println("Escritas " + count + " filas");
        }
        Files.move(tmpPath, Paths.get(path), StandardCopyOption.ATOMIC_MOVE);
    }

    // Append con bloqueo de archivo para escritores concurrentes
    public void appendWithLock(String path, String line) throws IOException {
        try (FileChannel channel = FileChannel.open(
                Paths.get(path),
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE,
                StandardOpenOption.APPEND)) {
            long position = channel.size();
            channel.lock(position, line.length() + 1, false);
            ByteBuffer buffer = ByteBuffer.wrap((line + "\n").getBytes());
            channel.write(buffer, position);
        }
    }
}

Bash: Escritura de archivos grandes con dd y split

#!/usr/bin/env bash
set -euo pipefail

# Escribir un archivo grande con dd usando un tamaño de bloque específico
write_large_file() {
    local output="$1"
    local size_mb="${2:-100}"
    local block_size="${3:-1M}"

    dd if=/dev/zero of="$output" bs="$block_size" count="$size_mb" status=progress
    echo "Creado $output (${size_mb}MB)"
}

# Dividir un archivo grande en chunks más pequeños
split_large_file() {
    local input="$1"
    local prefix="${2:-chunk_}"
    local chunk_size="${3:-100M}"

    split -b "$chunk_size" -d --numeric-suffixes=1 -a 3 "$input" "$prefix"
    echo "Dividido en chunks de ${chunk_size}"
}

# Escritura atómica usando archivo temporal y renombrado
atomic_write() {
    local output="$1"
    local content="$2"
    local tmp="${output}.tmp.$$"

    printf '%s' "$content" > "$tmp"
    mv "$tmp" "$output"
    echo "Escritura atómica en $output"
}

# Stream de export de base de datos a archivo comprimido
stream_db_export() {
    local db_url="$1"
    local output="$2"

    psql "$db_url" -c "COPY (SELECT * FROM users) TO STDOUT WITH CSV HEADER" \
        | gzip -c > "${output}.tmp"
    mv "${output}.tmp" "$output"
    echo "Exportado y comprimido en $output"
}

# Uso
# write_large_file /tmp/large.bin 500 4M
# split_large_file /tmp/large.bin chunk_ 50M
# atomic_write /etc/app/config.txt "key=value"

Mejores Prácticas Adicionales

  1. Usa fsync o force para garantías de durabilidad. Después de escribir datos críticos, fuerza al SO a volcar su page cache al disco físico. Sin esto, un corte de energía puede perder datos incluso después de un write() exitoso:
import os
with open('critical.dat', 'wb') as f:
    f.write(data)
    f.flush()       # Volcar buffer de Python al SO
    os.fsync(f.fileno())  # Forzar al SO a escribir a disco
// Java: forzar flush a disco
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE)) {
    channel.write(buffer);
    channel.force(true);  // true = flush datos + metadatos
}
  1. Ajusta tamaños de buffer para tu carga de trabajo. Los tamaños de buffer por defecto (4-8KB) funcionan para uso general. Para escrituras secuenciales grandes, buffers de 64KB-1MB reducen el conteo de syscalls considerablemente:
# Python: usar buffer más grande para escrituras secuenciales
with open('large_output.bin', 'wb', buffering=1024*1024) as f:  # Buffer de 1MB
    for chunk in data_source:
        f.write(chunk)
// Node.js: aumentar highWaterMark para write streams
const stream = fs.createWriteStream('output.bin', { highWaterMark: 1024 * 1024 });
  1. Usa pipeline() en lugar de .pipe() manual en Node.js. pipeline() maneja errores correctamente y limpia todos los streams ante fallos, previniendo fugas de memoria y descriptores colgados:
const { pipeline } = require('stream');
const { promisify } = require('util');
const pipelineAsync = promisify(pipeline);

// Correcto: pipeline maneja limpieza ante errores
await pipelineAsync(readableSource, transformStream, fs.createWriteStream('output.dat'));

// Evitar: pipe manual no maneja errores correctamente
// readableSource.pipe(transformStream).pipe(fs.createWriteStream('output.dat'));

Errores Comunes Adicionales

  1. No manejar backpressure en streams de Node.js. Escribir más rápido de lo que el destino puede manejar causa crecimiento de memoria sin límite. Verifica el valor de retorno de write() y espera el evento drain:
// Incorrecto: ignora backpressure, puede causar agotamiento de memoria
for (const chunk of hugeDataSource) {
    stream.write(chunk);  // Retorna false cuando el buffer está lleno
}

// Correcto: respetar backpressure
for (const chunk of hugeDataSource) {
    if (!stream.write(chunk)) {
        await new Promise(resolve => stream.once('drain', resolve));
    }
}
  1. Usar writeFileSync en hot paths. Las escrituras síncronas bloquean el event loop y pueden estancar todo un proceso Node.js. Usa escrituras asíncronas o streams:
// Incorrecto: bloquea el event loop en cada escritura
for (const record of records) {
    fs.writeFileSync('output.log', record + '\n', { flag: 'a' });
}

// Correcto: agrupar escrituras con un stream
const stream = fs.createWriteStream('output.log', { flags: 'a' });
for (const record of records) {
    stream.write(record + '\n');
}
stream.end();
  1. No limpiar archivos temporales ante fallos. Si una escritura atómica falla después de crear el archivo temporal, el archivo temporal permanece en disco. Siempre usa try/except o context managers:
# Incorrecto: el archivo temporal se fuga ante error
tmp = path + '.tmp'
with open(tmp, 'w') as f:
    f.write(data)
    raise ValueError("algo salió mal")  # tmp se fuga!

# Correcto: limpiar ante fallo
tmp = path + '.tmp'
try:
    with open(tmp, 'w') as f:
        f.write(data)
    os.replace(tmp, path)
except Exception:
    Path(tmp).unlink(missing_ok=True)
    raise

FAQ Adicional

¿Cómo escribo a archivos concurrentemente desde múltiples hilos?

Usa bloqueo de archivos para prevenir escrituras intercaladas. En Python, usa fcntl.flock (Linux/macOS) o msvcrt.locking (Windows). En Java, usa FileChannel.lock(). En Node.js, usa el paquete proper-lockfile. Para logs append-only, el SO garantiza atomicidad para escrituras bajo el tamaño del pipe buffer (típicamente 4KB en Linux):

import fcntl

with open('shared.log', 'a') as f:
    fcntl.flock(f, fcntl.LOCK_EX)  # Bloqueo exclusivo
    f.write(f"{record}\n")
    fcntl.flock(f, fcntl.LOCK_UN)  # Liberar bloqueo

¿Esta solución está lista para producción?

Sí. El patrón de escritura atómica (archivo temporal + renombrado) es usado por PostgreSQL para escrituras WAL, SQLite para commits de journal, nginx para recargas de configuración y rsync para transferencias de archivos. BufferedWriter es la clase estándar de IO de Java usada en Spring Batch, Hadoop y productores de Kafka. fs.createWriteStream con pipeline() es usado por herramientas de build de Node.js, servidores de streaming y el SDK de AWS para subidas multipart a S3. Los memory-mapped files (MappedByteBuffer) son usados por Cassandra, Lucene y Netty para I/O binario de alto rendimiento. El patrón dd + split se usa en producción para benchmarking de disco, rotación de logs y procesamiento paralelo de datos.

¿Cuáles son las características de rendimiento?

BufferedWriter con buffer de 8KB alcanza 200-500MB/s para escrituras secuenciales de texto en SSD. FileChannel con direct buffers alcanza 500-900MB/s para escrituras binarias. MappedByteBuffer alcanza 1-2GB/s para escrituras memory-mapped, pero tiene alto coste de setup (~1ms por mapping). fs.createWriteStream con highWaterMark de 64KB alcanza 150-400MB/s en Node.js. open() de Python con buffering de 1MB alcanza 300-600MB/s. os.fsync() añade 5-50ms por llamada dependiendo del disco (SSD vs HDD). El renombrado atómico (os.replace) es casi instantáneo (<1ms) en el mismo filesystem. dd con bs=1M alcanza 80-90% del throughput de disco crudo. Dividir un archivo de 10GB en chunks de 100MB toma 15-30s en SSD. CSV streaming con csv.DictWriter procesa 100K-500K filas/s dependiendo de la complejidad de la fila.

¿Cómo depuro problemas con este enfoque?

Habilita el trazado de I/O de archivos en Python con strace -e trace=write -p <pid>. En Node.js, escucha stream.on('error') y registra err.code (ENOSPC, EACCES, EMFILE). En Java, captura IOException e inspecciona getMessage() para errores de disco lleno o permisos. Para escrituras lentas, verifica I/O de disco con iostat -x 1 (Linux) o Activity Monitor > Disk (macOS). Para problemas de memoria, monitorea RSS del proceso con ps aux | grep <pid> o top -p <pid>. Para corrupción de datos, compara checksums antes y después de escribir con sha256sum. Para datos faltantes tras un crash, verifica si se llamó fsync antes del crash. Para fugas de archivos temporales, busca archivos .tmp con find /path -name '*.tmp' -mtime +1. Para stalls de escritura, verifica si la page cache del SO está llena con free -m (Linux) y revisa la fila buffers/cache.