Publi

Programación de tareas en segundo plano para nuestras aplicaciones web (Introducción. Parte 1 de 3)

Tareas programadas en segundo plano

Las aplicaciones web se van complicando cada vez más. Es un hecho y es algo bueno. Los ordenadores tienen más potencia, y los creadores cada vez más opciones e imaginación para destacar sobre la competencia. Una práctica interesante es la posibilidad de realizar tareas en segundo plano o en un momento concreto del tiempo sin que exista una mediación por parte del usuario.

Tenemos varias posibilidades y en estos posts vamos a centrarnos en cada una de ellas. Por otro lado, aunque este post es más general e independiente del lenguaje, quiero centrarme en PHP. Por un lado, es un lenguaje con el que he trabajado mucho, por otro, aunque es posible que diferentes partes de un proyecto estén realizadas en diferentes lenguajes, y PHP no sea un lenguaje pensado a priori para esto, en ocasiones, es más fácil y barato para una empresa pequeña recolocar a alguien en otro módulo de la apliación si todo está desarrollado en el mismo lenguaje. Como quiero hacer este post más o menos completo y relatar mi experiencia en cada uno de los puntos, se me ha alargado mucho, por eso he decidido dividirlo en tres partes, incluso puede que algún ejemplo dé para una cuarta.

¿Qué podemos hacer con estas tareas?

Bueno, ¿para qué vale todo esto? Es cierto que una web que tenga sus usuarios, envíe correos y sirva cierta información al usuario no tiene por qué ejecutar nada en segundo plano. Todo se servirá y se realizará al ritmo que los usuarios lo pidan. Eso está bien, en principio. Pero claro, imaginad que la web empieza a ganar visitantes. Lo primero que puede darnos problema es el envío de correo.

Puede que si se registran 10 usuarios por hora no pase nada pero, ¿10 usuarios por segundo? A los que debemos enviar confirmaciones de registro (no son muchos), pero luego enviaremos alertas para ciertos eventos, recordatorios de contraseña, avisos de que están intentando entrar con esa cuenta de correo… en definitiva, nuestra aplicación deberá establecer varias conexiones por segundo con el sistema de correo, sistema que muchas veces es externo y seguro que se cabrea (y nos expulsa por establecer tantas conexiones). Seguro que estaría bien coger cada 10, 15 o 20 segundos, y ejecutar una tarea que envíe todos los mensajes que tenga pendiente, lo que se llama un spool de correo, incluso enviar todos los correos pendientes aprovechando una misma conexión al servidor de correo, de manera mucho más eficiente y ordenada.

Supongamos que queremos tener un control de la llegada de dichos mensajes, o al menos comprobar que el e-mail sigue siendo válido para determinar si nuestros usuarios son legítimos o tenemos algún problema con sus datos. En este caso, cuando el servidor de correo de destino está recibiendo el mail normalmente contestará. Puede que el correo se entregue bien, o puede que estemos en una lista negra, el servidor de destino puede aplicar listas grises, nuestro correo puede ser tachado como Spam, o incluso que el servidor de destino no funcione ahora mismo. En caso de fallo, nuestro servidor de correo nos enviará un mensaje con el motivo por el que no se ha podido entregar el correo. Dichos mensaje podemos leerlos periódicamente y asignar un código de estado a los mensajes que hemos enviado a modo de diagnóstico, o para hacer un reintento pasados unos minutos, desactivar usuarios, etc.

Una web actual, normalmente también descarga contenidos de otros lados, y los procesa. Puede que descarguemos tweets, una captura de una web, extraigamos una imagen o una descripción de un post, la cotización del euro o de alguna criptomoneda. Esto son tareas que no dependen de nosotros. Así que no podemos controlar que el tiempo que tardamos en ellas sea fijo, o que nos pueda echar por tierra una página. También tenemos que pensar en que si cada vez que un visitante requiere alguno de estos datos desde nuestro sitio tenemos que acceder a otro lado para sacarlo, tal vez el otro lado se harte de nosotros, o que no responda en algún momento debido a una caída. Lo que podemos hacer es acceder periódicamente a esos sitios sin que exista mediación por parte de nuestros usuarios y guardar esos contenidos en una memoria o base de datos, de modo que nosotros podamos acudir a esos datos de forma muy rápida. Nuestro dato tal vez no sea el más actualizado, en el caso de las cotizaciones, tal vez tengamos un cierto desfase (aunque podemos aplicar otras técnicas), pero si se trata de tweets, posts y demás, esos contenidos no van a variar con el tiempo.

Otro uso muy útil es la importación, exportación y cálculo de datos. En sistemas de estadística, puede que nuestra aplicación deba importar gran cantidad de datos procedente de diversas fuentes (webs externas, ficheros de usuario, APIs, etc). Esta tarea puede ser muy rápida o llegar a ser muy lenta en su ejecución. ¿Por qué no hacerla en segundo plano y avisar al usuario cuando terminemos? Por un lado, evitamos que el usuario esté eternamente esperando una respuesta (y que se estrese poniéndose a pedir los mismos datos una y otra vez), por otro, si la tarea hace un uso muy intensivo de CPU, podemos limitarlo (la tarea puede realizarse de forma más lenta, pero al menos no nos tira el servicio). Tanto si estamos introduciendo datos, generando un fichero de reporte, realizando cálculos, que también puede ser convirtiendo un vídeo u obteniendo una miniatura de un vídeo, son tareas que pueden estresar al usuario y no deberían influir en la carga de nuestras páginas.

También puede que nos interese eliminar sesiones de usuarios inactivos, invalidar o generar cachés de contenidos complicados. El objetivo es que el usuario final de nuestra página tenga que esperar lo menos posible para la realización de las tareas antes de ver un resultado en la página web. Por supuesto, luego podemos consultar por Ajax si una tarea ha finalizado para avisar al usuario o, como es el caso del envío de correos, es algo que al usuario no le importa, pero a nosotros sí.

Escalabilidad

Algo que tenemos que tener en cuenta cuando estamos inmersos en un proyecto es su escalabilidad. En un primer momento, y si no nos queda otra, cuando empezamos a tener visitas a nuestros servicios podremos optar por una escalabilidad vertical. Es decir, utilizar máquinas más potentes para servir nuestros contenidos. Aunque pronto empiezan a surgir más problemas si hacemos de esta práctica algo común. Los costes suelen dispararse, es más barato comprarse 4 ordenadores pequeños que uno cuya potencia englobe a los cuatro. Vale, no siempre es así, pero si nos ponemos a mirar presupuestos de ordenadores llega un momento en el que sí se cumple. Por otro lado, si tenemos nuestro servicio alojado únicamente en una máquina, sólo hace falta que se rompa una máquina para echar todo al traste. Parece una frase tonta pero es así, mientras que si tenemos nuestro servicio replicado en cuatro máquinas, si se rompe una nos fastidia, pero no nos pillamos los dedos. Bueno, tras todo esto tendríamos que pensar qué parte debemos escalar y cómo, porque puede que escalar toda una aplicación mastodóntica no sea la solución, debemos analizar los cuellos de botella y optar por una solución inteligente.

El caso es que muchas veces este tipo de tareas pueden pasarse a otra máquina. Es decir, nuestra máquina principal (o máquinas), como punto de entrada servirán webs y punto. Pero para las tareas programadas, que puede que algunas requieran mucha CPU vamos a utilizar otra máquina, que incluso será privada y funcionará de forma independiente, incluso si vemos que las tareas son muy exigentes podemos dividirlas en varias máquinas a nuestro gusto. De esta manera podremos hacer crecer nuestra aplicación.

Informes de errores

Aunque muchos programadores dejan esto para el final, en mi opinión es lo primero que deberíamos implementar. Cada vez que ejecutamos una de estas tareas deberíamos escribir en algún lado (base de datos, fichero log, servicio de logs externo, etc) que estamos empezando a realizar la tarea. Deberíamos escribir los errores que nos encontramos y los pasos que damos a la hora de ejecutar tareas, así como su finalización. Con esto tendremos un seguimiento que nos puede ayudar a corregir errores o saber si alguna tarea se ha quedado bloqueada. Más tarde podremos desactivar mensajes para que solo muestre inicio de tarea, fin de tarea y errores, así sabemos que nuestro programa hace su trabajo.

Tareas tras la carga de la web

Una técnica que se ha utilizado durante mucho tiempo es que tras la carga completa de la web se ejecute cierto código de mantenimiento. Es cierto que durante mucho tiempo, una vez se ha cargado la web, se comprueba si se deben realizar tareas y se realizan en caso afirmativo. Muchos sitios lo hacen cuando carga una web, otros sitios hacen una petición Ajax para ejecutar tareas… el problema es que en cualquier caso, estamos haciendo que el usuario intervenga en el disparo de dichas tareas. Eso puede causar que la página cargue más lenta, que haya demasiadas peticiones innecesarias al sistema de tareas, que si la tarea tarda mucho en ejecutarse se cancele debido a los timeouts de PHP o del servidor web, o que, si ningún usuario entra a la web en un tiempo, no se lance ninguna tarea. En cualquier caso, yo soy de la idea de que “son cosas que al usuario no le interesan“, incluso en algunos sistemas, podemos ver tras la petición (sobre todo las Ajax), si se ha ejecutado la tarea y cómo ha ido dicha ejecución, cosa que interesa mucho menos al usuario.

El origen de todo esto es porque muchos servidores (sobre todo los compartidos), históricamente no nos dejaban hacer llamadas a otras aplicaciones de gestión de tareas o incluso la creación de tareas programadas (cron jobs). Actualmente, hasta los hospedajes compartidos nos dejan hacer cosas así, de todas formas, yo te recomiendo montarte por lo menos un VPN para tener más libertar con estas cosas. Aún así, las necesidades de ejecución de tareas en nuestras aplicaciones están creciendo.

Cron jobs

Reloj
Son las tareas programadas de toda la vida, de esta forma le podemos decir al sistema ejecuta este programa cada x días, h horas y m minutos. Normalmente un servidor ejecuta muchas tareas en segundo plano como pueden ser mantenimiento de logs, sincronización del reloj, comprobación de actualizaciones, envío de informes de salud, generación de estadísticas y algunas más. Así que, ¿por qué no introducir nuestras propias tareas?

Por un lado podríamos utilizar un script para cada tarea, por ejemplo invalidación de cachés o limpieza de base de datos, consulta de webs externas y envío de e-mails incluso introducir diferente periodicidad para cada una de ellas. Pero en muchas ocasiones podemos incluso centralizarlo todo en una única llamada. Si hablamos de PHP, podremos ejecutar un único archivo PHP o Python, o Java y que éste se encargue de revisar las tareas pendientes y ejecutar las que crea pertinentes. Habrá ejemplos en PHP en el siguiente post. Incluso si lo hacemos de forma centralizada, podremos introducir tareas que solo deban lanzarse una vez en el juego. Por ejemplo cuando exista una tarea que no tiene por qué ser periódica, como puede ser la obtención de los últimos posts de un blog que acaban de introducir en nuestra web (no es periódico, se realiza cuando un usuario da de alta su blog). Luego este cron job lo podemos ejecutar cada minuto o cada dos minutos, como lo creamos necesario. Basta con ejecutar:

crontab -e

Y luego añadir
1
2
# m h  dom mon dow   command
*/2 * * * * php /ruta/del/archivo/archivo.php

Con esto ejectaremos la tarea cada dos minutos. Seguro que buscando por la red encontramos mucha información sobre los cron jobs.

En nuestro script podremos hacer una consulta a base de datos para determinar las tareas que hay pendientes (periódicas o no) y ejecutar la que toque, incluso cada tarea podrá realizar llamadas a otros procesos, y todo sin que el usuario tenga que intervenir y sin costarle tiempo a él.

Lo malo es que los cron jobs de Unix no pueden alcanzar una resolución de segundos por lo que si queremos lanzar una tarea cada segundo, cada dos, o cada quince segundos tenemos que andar ejecutando tareas con sleep delante, aunque podríamos tener problemas si una tarea tarda más de lo normal en ejecutarse.

Programador de tareas en ejecución

Otra opción es crear nosotros un programador de tareas en el lenguaje en el que estemos trabajando. Por un lado, si utilizamos una base de datos para el control de nuestras tareas no tendremos que iniciar conexiones cada vez que ejecutamos la tarea, lo que gasta tiempo y recursos, sino que podremos tener una conexión abierta todo el tiempo. Y, por otro lado, podemos mirar si tenemos una tarea pendiente cada menos tiempo, por ejemplo cada dos segundos. En este caso, si, en el caso de una tarea de envío de correo electrónico, comprobamos si tenemos que enviar e-mails cada minuto, en el peor de los casos, el mensaje tardará un minuto (más o menos en salir). Mientras que si comprobamos cada dos segundos, al usuario prácticamente le da tiempo a cambiar de ventana, acceder a su correo y comprobar si tiene algo nuevo cuando ya ha recibido el mensaje. Parece que no, pero da una mejor sensación a los usuarios.

Por otro lado, si utilizamos un programador de tareas de este estilo podremos tener varias instancias del programador corriendo en una misma máquina. En caso de que no sean tareas muy intensas computacionalmente, como son enviar e-mails, descargar información de webs externas, etc podríamos tener varias tareas en ejecución a la vez. Eso sí, deberíamos controlar muy bien que no se lance la misma tarea por dos programadores a la vez, lo que puede ser un problema. Así como determinar qué tareas pueden ejecutarse en varios sitios a la vez y cuáles no.

Como el artículo me ha quedado muy largo, he decidido dividirlo en tres. Así que para el próximo incluiré ejemplos de código fuente. La segunda parte estará disponible el día 18 de Julio.

Foto principal: unsplash-logoEstée Janssens

Foto (reloj): unsplash-logoÁlvaro Bernal

También podría interesarte....

Leave a Reply