Etiqueta: colas

  • Reciclaje de memoria en workers PHP de larga ejecución

    Reciclaje de memoria en workers PHP de larga ejecución

    Resumen: técnicas prácticas para controlar y reciclar memoria en procesos PHP de larga ejecución (queues, daemons, RoadRunner/Swoole) y mantener la huella de memoria estable.

    Introducción

    Los workers PHP de larga ejecución (colas, daemons, RoadRunner, Swoole) tienden a crecer en memoria con el tiempo: referencias no liberadas, objetos grandes retenidos y fragmentación del gestor de memoria provocan aumento del RSS y degradación del rendimiento.

    Este artículo condensa estrategias prácticas para reciclar memoria sin recurrir a reinicios agresivos por defecto: límites controlados, limpieza explícita y patrones de procesamiento que reducen la presión de memoria.

    • Referencias retenidas en closures y objetos con referencias circulares.
    • Variables estáticas que acumulan cachés ilimitados.
    • Procesamiento en lotes demasiado grandes (arrays/objetos en memoria).
    • Fragmentación interna del gestor de memoria de Zend o fugas en extensiones (ej.: pdo_mysql).

    Prerrequisitos

    Antes de aplicar estas técnicas asegúrate de tener control sobre la ejecución del worker y acceso a las extensiones/funciones necesarias.

    • PHP CLI con soporte para pcntl si vas a implementar reinicios controlados.
    • Capacidad de supervisión/respawn (supervisor, systemd, o el propio orquestador de RoadRunner/Swoole).
    • Acceso a herramientas de monitorización (ps, pmap, logs de memoria).

    Desarrollo

    Procedimiento

    1. Establecer límites y reinicios controlados.
    2. Eliminar referencias y forzar recolección de ciclos.
    3. Procesar en chunks para evitar picos de memoria.
    4. Reutilizar objetos pesados (pooling) en vez de recrearlos constantemente.
    5. Monitorizar tendencias y métricas de uso de memoria.

    A continuación ejemplos de implementación de cada punto crítico.

    1) Reinicio controlado: comprobar uso de memoria y salir para que el supervisor recree el proceso.

    <?php
    // dentro del loop principal del worker
    if (memory_get_usage(true) > 256 * 1024 * 1024) {
        // salir para que el supervisor o manager respawnee un proceso limpio
        exit(1);
    }
    Lenguaje del código: PHP (php)

    2) Limpieza explícita de referencias y recolección de ciclos: unset no siempre basta si existen referencias circulares.

    <?php
    // después de procesar una tarea pesada
    unset($largeStructure);
    // forzar recolección de ciclos si hay objetos con referencias circulares
    gc_collect_cycles();
    Lenguaje del código: PHP (php)

    3) Chunking: evitar cargar datasets completos en memoria. Procesa por lotes y libera cada lote.

    <?php
    foreach (array_chunk($rows, 5000) as $chunk) {
        process($chunk);
        unset($chunk);
        gc_collect_cycles();
    }
    Lenguaje del código: PHP (php)

    4) Reutilización (pooling): evita crear y destruir objetos pesados frecuentemente. Mantén conexiones o parsers en pool cuando sea seguro.

    5) Monitorización: usa herramientas del sistema y funciones internas para trazar tendencias de memoria y detectar fugas tempranas.

    ps -o pid,rss,cmd -C php
    # o para inspección puntual de PID
    pmap <PID> | tail -n 1
    Lenguaje del código: Bash (bash)

    Ejemplos

    Ejemplo de loop de worker que combina chequeo de memoria, chunking y recolector de ciclos.

    <?php
    // worker.php - bucle simplificado
    while (true) {
        $rows = fetchPendingRows(50000); // pseudocódigo
    
        foreach (array_chunk($rows, 5000) as $chunk) {
            process($chunk);
            unset($chunk);
            gc_collect_cycles();
        }
    
        // chequeo de memoria: salir si supera 256MB para que el supervisor reinicie
        if (memory_get_usage(true) > 256 * 1024 * 1024) {
            exit(1);
        }
    
        // dormir o esperar por nueva tarea
        usleep(100000);
    }
    Lenguaje del código: PHP (php)

    Checklist

    1. Habilitar y probar reinicios controlados (pcntl o supervisión externa).
    2. Identificar y eliminar referencias circulares; usar gc_collect_cycles tras tareas pesadas.
    3. Procesar en chunks y liberar cada lote explícitamente.
    4. Reutilizar objetos pesados mediante pooling cuando sea seguro.
    5. Registrar y monitorizar memoria (memory_get_peak_usage, ps, pmap) para detectar tendencias.

    Trata la memoria como una moneda: mézclala, recíclala y no la derroches en procesos de larga vida.

    Conclusión

    Workers PHP de larga ejecución son útiles, pero requieren disciplina: límites claros, limpieza explícita y monitorización. Implementa reinicios controlados, limpia referencias, procesa por chunks y registra tendencias para mantener la huella de memoria saneada.

    Si no tienes información para afirmar compatibilidades o comportamientos específicos de versiones en tu entorno, prueba primero en un entorno de staging y mide antes de aplicar en producción.

  • Laravel a escala: 5 trucos para consultar millones de registros

    Laravel a escala: 5 trucos para consultar millones de registros

    Este artículo resume cinco técnicas prácticas para consultar millones de filas en Laravel sin agotar memoria ni tiempo CPU. Cada técnica incluye una explicación breve y ejemplos de código listos para usar.

    Aplica estas prácticas en procesos de reporting, ETL o consultas masivas para mantener tiempos de respuesta y uso de recursos controlados.

    Introducción

    En consultas a tablas masivas, prácticas comunes como SELECT * o cargar todo con get() provocan consumo excesivo de RAM y CPU. Las siguientes cinco estrategias reducen uso de recursos y aceleran procesos.

    Prerrequisitos

    Antes de aplicar las técnicas: asegúrate de contar con un esquema de base de datos razonable y mecanismos de cacheo/colas si vas a usar preagregados o jobs en segundo plano.

    • Acceso al código de tus modelos/Eloquent.
    • Permiso para crear índices y migraciones en la base de datos.
    • Un sistema de cache (Redis, Memcached) y, si procede, soporte para jobs/colas.

    Desarrollo

    Procedimiento

    A continuación se presentan las cinco técnicas: seleccionar columnas esenciales, procesar por chunks o stream, indexar y optimizar WHERE, preagregar datos y cachear resultados pesados.

    1. Seleccionar sólo las columnas esenciales

    Evita SELECT * cuando sólo necesitas unas pocas columnas. Reducir columnas reduce CPU, memoria y ancho de banda.

    <?php
    // ❌ Evitar: carga todas las columnas
    $orders = Order::all();
    
    // ✅ Seleccionar sólo lo necesario
    $orders = Order::select('id', 'amount', 'created_at')
        ->whereBetween('created_at', [$start, $end])
        ->get();
    Lenguaje del código: PHP (php)

    En el ejemplo base, reducir de 20 columnas a 3 puede bajar el consumo de memoria de ~1.2 GB a ~200 MB en un conjunto grande de filas.

    2. Procesar por chunks o con cursor (stream)

    No uses get() para millones de filas. chunk() y cursor() procesan en partes y mantienen baja la memoria.

    <?php
    // Procesar en bloques de 10.000
    Order::where('status', 'completed')
        ->select('id', 'amount')
        ->chunk(10000, function ($orders) {
            foreach ($orders as $order) {
                // generar fila de reporte
            }
        });
    
    // O procesar con cursor para stream perezoso
    $orders = Order::cursor()->filter(fn ($order) => $order->amount > 1000);
    Lenguaje del código: PHP (php)

    En pruebas citadas, procesar 5M de filas con cursor/chunks reduce consumo de memoria a decenas de MB frente a cientos o más.

    3. Índices y cláusulas WHERE eficientes

    Filtros y ordenamientos sin índices provocan full table scans. Indexa columnas usadas en WHERE, JOIN y ORDER BY y evita patrones ineficientes como LIKE ‘%…%’.

    <?php
    // Ejemplo de migración para agregar índices
    Schema::table('orders', function (Blueprint $table) {
        $table->index('status');
        $table->index('created_at');
    });
    Lenguaje del código: PHP (php)

    Un ejemplo reportado mostró una reducción de tiempo de consulta de 120 s a 0.8 s tras indexar columnas utilizadas en filtros.

    4. Preagregar datos para reportes pesados

    Evita agrupar o sumar decenas de millones de filas en tiempo real. Precalcula resúmenes con jobs nocturnos o usa vistas/materialized views si la BD lo permite.

    <?php
    class GenerateOrderSummary implements ShouldQueue {
        public function handle() {
            $summary = Order::selectRaw('
                DATE(created_at) AS date,
                COUNT(*) AS total_orders,
                SUM(amount) AS revenue
            ')
            ->whereDate('created_at', today()->subDay())
            ->groupBy('date')
            ->first();
    
            Report::updateOrCreate(['date' => $summary->date], (array)$summary);
        }
    }
    Lenguaje del código: PHP (php)

    Con preagregados, la carga de un reporte puede pasar de minutos a decenas de milisegundos; en el ejemplo base, un reporte cargó en ~50 ms.

    5. Cachear consultas costosas

    Para consultas repetidas, guarda el resultado en cache con expiración y/o tags. Invalida o refresca la cache cuando los datos subyacentes cambien.

    <?php
    $report = Cache::remember("daily_sales_report:{$date}", 3600, fn() =>
        Order::whereDate('created_at', $date)
            ->selectRaw('COUNT(*) AS orders, SUM(amount) AS revenue')
            ->first()
    );
    Lenguaje del código: PHP (php)

    Usa Redis o Memcached según tu infraestructura y define políticas claras de invalidación para evitar datos obsoletos.

    Ejemplos

    Ejemplos concretos ya mostrados: selección de columnas, chunk/cursor, migración para índices, job para preagregados y cacheo con Cache::remember.

    Adapta los tamaños de chunk y la expiración del cache a la carga real y al patrón de acceso de tu sistema.

    Checklist

    1. Revisar consultas y eliminar SELECT *.
    2. Usar chunk() o cursor() para procesado masivo.
    3. Indexar columnas usadas en WHERE, JOIN y ORDER BY.
    4. Preagregar resultados pesados con jobs o vistas.
    5. Cachear consultas repetidas y definir estrategia de invalidación.

    Conclusión

    Combinando selección precisa de columnas, procesamiento por partes, buenos índices, preagregados y cacheo, puedes consultar millones de registros en Laravel con uso de recursos razonable y tiempos de respuesta aceptables.

    Empieza por los cambios menos invasivos (select, chunk, cache) y mide impacto antes de introducir cambios más estructurales como materialized views o rediseño de esquemas.

  • Evolución de las colas en Laravel: workers, deferred y background

    Evolución de las colas en Laravel: workers, deferred y background

    Este artículo resume la evolución del sistema de colas en Laravel: desde los workers tradicionales hasta el driver background basado en concurrencia. Explica usos, diferencias y ejemplos prácticos de implementación.

    Introducción

    Laravel ha transitado de un sistema de colas basado en workers persistentes a opciones que reducen la complejidad operativa: drivers deferred y background (impulsado por concurrencia).

    Prerrequisitos

    Antes de aplicar cualquiera de las estrategias descritas, confirma lo siguiente:

    • Proyecto Laravel con entorno PHP configurado.
    • Driver de colas definido (Redis, database, etc.) si planeas usar workers tradicionales.
    • En caso de background driver, la configuración de conexiones en config/queue.php debe incluir la conexión ‘background’ (ver ejemplo).

    Desarrollo

    Procedimiento

    A continuación se describen las tres aproximaciones principales y cómo elegir entre ellas según la carga y la tolerancia a fallos.

    1. Workers tradicionales: ejecutan jobs mediante procesos persistentes (p. ej. php artisan queue:work redis), adecuados para cargas pesadas y con control avanzado de reintentos y fallos.
    2. Driver deferred: ejecuta el job después de enviar la respuesta HTTP, sin worker. Adecuado para tareas ligeras y no críticas.
    3. Driver background (concurrencia): ejecuta jobs en procesos PHP separados, sin worker persistente; equilibrio entre robustez y simplicidad operativa.

    Ventajas y limitaciones resumidas:

    • Workers tradicionales: alto rendimiento y control; requieren DevOps para gestionar procesos.
    • Deferred: baja sobrecarga; limitado en reintentos y duración de tarea.
    • Background: sin workers dedicados, más robusto que deferred; útil para tareas de duración media.
    php artisan queue:work redis
    Lenguaje del código: Bash (bash)
    <?php
    return [
        'connections' => [
            'background' => [
                'driver' => 'background',
            ],
        ],
    ];
    Lenguaje del código: PHP (php)

    El ejemplo anterior muestra cómo declarar una conexión ‘background’ en config/queue.php. Su dispatch usa la conexión para ejecutar el job en otro proceso PHP.

    Ejemplos

    Ejemplo de clase Job que implementa ShouldQueue y se serializa para su ejecución en background o por un worker.

    <?php
    namespace App\Jobs;
    
    use Illuminate\Bus\Queueable;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Foundation\Bus\Dispatchable;
    use Illuminate\Queue\SerializesModels;
    
    class RecordDelivery implements ShouldQueue
    {
        use Dispatchable, Queueable, SerializesModels;
    
        public function __construct(public $order)
        {
        }
    
        public function handle()
        {
            // lógica de procesamiento
        }
    }
    Lenguaje del código: PHP (php)

    Dispatch desde un controlador usando la conexión background para evitar bloquear la petición HTTP.

    <?php
    use App\Jobs\RecordDelivery;
    
    class DeliveryController
    {
        public function store(Request $request)
        {
            $order = Order::create([...]);
            RecordDelivery::dispatch($order)->onConnection('background');
    
            return response()->json(['status' => 'processing'], 202);
        }
    }
    Lenguaje del código: PHP (php)

    Checklist

    1. Determina la criticidad de la tarea (alta → worker tradicional).
    2. Si la tarea es ligera y no crítica, considera deferred.
    3. Para equilibrio operativo (sin workers pero más robusto que deferred), usa el driver background.
    4. Configura reintentos, timeouts y monitoreo según el método elegido.

    Conclusión

    La evolución de las colas en Laravel ofrece alternativas para distintos perfiles de carga operativa. Elegir entre workers, deferred o background depende de la carga, la tolerancia a fallos y la capacidad operativa del equipo.

    Si buscas simplificar la operación sin renunciar a capacidad de procesamiento, el driver background —impulsado por concurrencia— es una opción intermedia interesante.

  • Registrar cada llamada API sin ralentizar el servidor

    Registrar cada llamada API sin ralentizar el servidor

    Resumen: Cómo registrar cada llamada a tu API sin degradar el rendimiento. Enfoque práctico: logging asíncrono, buffering, offload a workers, enmascarado de datos y rotación.

    Introducción

    Registrar cada petición API es crítico para depuración, auditoría y análisis, pero hacerlo mal puede convertir el logging en el principal cuello de botella.

    La regla de oro: nunca hagas que el manejador principal espere a que los logs se escriban. El registro debe ser asíncrono y, preferiblemente, salir del camino de ejecución principal.

    Prerrequisitos

    Los ejemplos usan Node.js y bibliotecas comunes; adapta las ideas a otros runtimes si es necesario.

    • Dependencia de un logger de alto rendimiento (p. ej. Pino).
    • Mecanismo para buffering o una cola (memoria, Redis, Kafka).
    • Worker o servicio externo para persistencia y envío a sistemas centralizados.

    Desarrollo

    Procedimiento

    Paso 1 — Decide qué registrar: evita sobre-logging. Campos típicos: timestamp, método, ruta, status, latencia, IP, userId, requestId y payload con datos sensibles enmascarados.

    {
      "time": "2025-08-11T14:23:45.123Z",
      "method": "POST",
      "url": "/api/orders",
      "status": 201,
      "responseTimeMs": 123,
      "userId": "usr_1a2b3c",
      "ip": "203.0.113.42",
      "userAgent": "Mozilla/5.0 (Macintosh...)",
      "requestId": "req_abcd1234"
    }
    Lenguaje del código: JSON / JSON con comentarios (json)

    Paso 2 — Usa un logger no bloqueante. Olvida console.log en producción; emplea un logger diseñado para escritura asíncrona y serialización rápida.

    const pino = require('pino');
    const logger = pino({
      level: 'info',
      transport: {
        target: 'pino-pretty'
      }
    });
    
    app.use((req, res, next) => {
      const start = Date.now();
      
      res.on('finish', () => {
        logger.info({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          responseTime: Date.now() - start
        });
      });
      
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 3 — Bufferiza en memoria y envía en lotes. Convierte miles de escrituras pequeñas en pocas escrituras grandes.

    let logBuffer = [];
    const BUFFER_SIZE = 50;
    const FLUSH_INTERVAL = 5000; // ms
    
    function flushLogs() {
      if (logBuffer.length > 0) {
        logger.info({ batch: logBuffer });
        logBuffer = [];
      }
    }
    
    setInterval(flushLogs, FLUSH_INTERVAL);
    
    app.use((req, res, next) => {
      const start = Date.now();
    
      res.on('finish', () => {
        logBuffer.push({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          time: Date.now(),
          latency: Date.now() - start
        });
    
        if (logBuffer.length >= BUFFER_SIZE) {
          flushLogs();
        }
      });
    
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 4 — Desacopla mediante cola y workers: para tráfico intenso, envía logs a una cola y procesa desde procesos independientes.

    const Redis = require('ioredis');
    const pub = new Redis();
    
    app.use((req, res, next) => {
      const start = Date.now();
    
      res.on('finish', () => {
        pub.publish('api_logs', JSON.stringify({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          latency: Date.now() - start
        }));
      });
    
      next();
    });
    Lenguaje del código: JavaScript (javascript)
    const sub = new Redis();
    sub.subscribe('api_logs');
    sub.on('message', (channel, message) => {
      const logData = JSON.parse(message);
      // Persistir logData de forma asíncrona (BD, ELK, S3...)
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 5 — Envío remoto asíncrono: si mandas logs a sistemas como ELK o CloudWatch, hazlo en lotes o por workers para evitar latencia en el request path.

    const winston = require('winston');
    require('winston-cloudwatch');
    
    winston.add(new winston.transports.CloudWatch({
      logGroupName: 'my-api-logs',
      logStreamName: 'production',
      awsRegion: 'us-east-1',
      jsonMessage: true
    }));
    
    app.use((req, res, next) => {
      const start = Date.now();
      res.on('finish', () => {
        winston.info({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          latency: Date.now() - start
        });
      });
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 6 — Enmascara datos sensibles antes de loguear: nunca escribas contraseñas, números de tarjeta o tokens sin protección.

    function maskSensitive(obj) {
      const clone = { ...obj };
      if (clone.password) clone.password = '******';
      if (clone.cardNumber) clone.cardNumber = '**** **** **** ' + clone.cardNumber.slice(-4);
      return clone;
    }
    
    app.use(express.json());
    app.use((req, res, next) => {
      req.body = maskSensitive(req.body);
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 7 — Mide y ajusta: compara latencias y uso de CPU/memoria antes y después del cambio. Usa herramientas de carga y APM para validar impacto.

    autocannon -c 100 -d 30 http://localhost:3000
    Lenguaje del código: Bash (bash)

    Paso 8 — Rotación y archivo: configura rotación por tamaño o día y mueve logs antiguos a almacenamiento económico.

    pino server.js | tee >(pino-pretty) | rotatelogs ./logs/api-%Y-%m-%d.log 86400
    Lenguaje del código: Bash (bash)

    Paso 9 — Distribuye a gran escala: evita escribir localmente, usa shippers (Filebeat/Fluent Bit), Kafka para ingesta masiva y sampling para reducir volumen de success logs.

    Paso 10 — Arquitectura de producción (resumen): gateway → collectors/colas → workers → almacenamiento indexable → dashboards y alertas.

    Ejemplos

    Los ejemplos anteriores cubren patrones comunes: logger rápido (Pino), buffering en proceso, publicación a Redis y consumo por workers, y envío asíncrono a servicios remotos.

    Checklist

    1. No bloquear el request path: logging asíncrono.
    2. Usar un logger de alto rendimiento (p. ej. Pino).
    3. Bufferizar y enviar en lotes.
    4. Offload a workers o colas para tráfico intenso.
    5. Enmascarar datos sensibles antes de persistir.
    6. Rotación y archivado de logs.
    7. Medir impacto y ajustar frecuencias/ tamaños de lote.

    Conclusión

    Registrar cada llamada API es viable si se diseña para no bloquear el flujo principal: emplea loggers rápidos, buffering, colas y workers; enmascara datos; y monitoriza el costo del logging.

    Aplica estas prácticas progresivamente y verifica con pruebas de carga y APM antes de desplegar en producción.