Node.js
Node.js

Seguir

24 de febrero de 2017 – 14 min read

Este artículo viene de Tomislav Capan, consultor técnico y entusiasta de Node.js. Tomislav publicó esto originalmente en agosto de 2013 en el blog de Toptal – puedes encontrar el post original aquí; el blog ha sido ligeramente actualizado. El siguiente tema se basa en la opinión y experiencias de este autor.

La creciente popularidad de JavaScript ha traído consigo muchos cambios, y la cara del desarrollo web hoy en día es dramáticamente diferente. Las cosas que podemos hacer en la web hoy en día con JavaScript que se ejecuta en el servidor, así como en el navegador, eran difíciles de imaginar hace apenas unos años, o estaban encapsuladas dentro de entornos de caja de arena como Flash o Java Applets.

Antes de profundizar en Node.js, es posible que desee leer sobre los beneficios del uso de JavaScript a través de la pila, que unifica el lenguaje y el formato de datos (JSON), lo que le permite reutilizar de manera óptima los recursos del desarrollador. Como esto es más un beneficio de JavaScript que de Node.js específicamente, no lo discutiremos mucho aquí. Pero es una ventaja clave para incorporar Node.js en su pila.

Node.js es un entorno de ejecución de JavaScript construido sobre el motor de JavaScript V8 de Chrome. Cabe destacar que Ryan Dahl, el creador de Node.js, pretendía crear sitios web en tiempo real con capacidad push, «inspirados en aplicaciones como Gmail». En Node.js, dio a los desarrolladores una herramienta para trabajar en el paradigma de la E/S sin bloqueo y basada en eventos.

En una frase: Node.js brilla en las aplicaciones web en tiempo real que emplean tecnología push sobre websockets. ¿Qué hay de revolucionario en esto? Bueno, después de más de 20 años de web sin estado basada en el paradigma petición-respuesta sin estado, por fin tenemos aplicaciones web con conexiones bidireccionales en tiempo real, donde tanto el cliente como el servidor pueden iniciar la comunicación, lo que les permite intercambiar datos libremente.

Esto contrasta con el típico paradigma de respuesta web, donde el cliente siempre inicia la comunicación. Además, todo se basa en la pila web abierta (HTML, CSS y JS) que se ejecuta a través del puerto estándar 80.

Uno podría argumentar que hemos tenido esto durante años en la forma de Flash y Java Applets – pero en realidad, esos eran sólo entornos de caja de arena utilizando la web como un protocolo de transporte para ser entregado al cliente. Además, se ejecutaban de forma aislada y a menudo funcionaban en puertos no estándar, lo que podía requerir permisos adicionales y demás.

Con todas sus ventajas, Node.js desempeña ahora un papel fundamental en la pila tecnológica de muchas empresas de alto perfil que dependen de sus ventajas únicas. La Fundación Node.js ha consolidado todo el mejor pensamiento en torno a por qué las empresas deben considerar Node.js en una breve presentación que se puede encontrar en la página de Estudios de Caso de la Fundación Node.js.

En este post, voy a discutir no sólo cómo se logran estas ventajas, sino también por qué es posible que desee utilizar Node.js – y por qué no – utilizando algunos de los modelos clásicos de aplicaciones web como ejemplos.

¿Cómo funciona?

La idea principal de Node.js: utilizar E/S sin bloqueo y basada en eventos para seguir siendo ligero y eficiente frente a las aplicaciones en tiempo real con uso intensivo de datos que se ejecutan a través de dispositivos distribuidos.

Eso es un trabalenguas.

Lo que realmente significa es que Node.js no es una nueva plataforma de plata que dominará el mundo del desarrollo web. Por el contrario, es una plataforma que satisface una necesidad particular. Y entender esto es absolutamente esencial. Definitivamente no querrás usar Node.js para operaciones que requieran un uso intensivo de la CPU; de hecho, usarlo para computación pesada anulará casi todas sus ventajas. Donde Node.js realmente brilla es en la construcción de aplicaciones de red rápidas y escalables, ya que es capaz de manejar un gran número de conexiones simultáneas con un alto rendimiento, lo que equivale a una alta escalabilidad.

La forma en que funciona bajo el capó es bastante interesante. En comparación con las técnicas tradicionales de servicio web, en las que cada conexión (solicitud) genera un nuevo hilo, ocupando la RAM del sistema y llegando al límite de la cantidad de RAM disponible, Node.js opera en un único hilo, utilizando llamadas de E/S no bloqueantes, lo que le permite soportar decenas de miles de conexiones simultáneas (mantenidas en el bucle de eventos).

*Imagen tomada de la entrada original del blog.

Un cálculo rápido: suponiendo que cada hilo tiene potencialmente un acompañamiento de 2 MB de memoria con él, corriendo en un sistema con 8 GB de RAM nos pone en un máximo teórico de 4000 conexiones concurrentes (cálculos tomados del artículo de Michael Abernethy «Just what is Node.js?», publicado en IBM developerWorks en 2011; desafortunadamente, el artículo ya no está disponible), más el coste de la conmutación de contexto entre hilos. Ese es el escenario con el que se suele lidiar en las técnicas tradicionales de servicio web. Evitando todo eso, Node.js alcanza niveles de escalabilidad de más de 1M de conexiones concurrentes, y más de 600k conexiones de websockets concurrentes.

Hay, por supuesto, la cuestión de compartir un único hilo entre todas las peticiones de los clientes, y es un escollo potencial de escribir aplicaciones Node.js. En primer lugar, la computación pesada podría ahogar el hilo único de Node y causar problemas para todos los clientes (más sobre esto más adelante), ya que las solicitudes entrantes se bloquearían hasta que se completara dicha computación. En segundo lugar, los desarrolladores deben tener mucho cuidado de no permitir que una excepción suba al bucle de eventos del núcleo (más alto) de Node.js, lo que provocará que la instancia de Node.js termine (lo que efectivamente hará que el programa se detenga).

La técnica utilizada para evitar que las excepciones suban a la superficie es pasar los errores de vuelta a la persona que llama como parámetros de devolución de llamada (en lugar de lanzarlos, como en otros entornos). Incluso si alguna excepción no manejada consigue burbujear, se han desarrollado herramientas para monitorizar el proceso de Node.js y realizar la recuperación necesaria de una instancia colapsada (aunque probablemente no podrás recuperar el estado actual de la sesión de usuario), siendo la más común el módulo Forever, o utilizando un enfoque diferente con herramientas externas del sistema upstart y monit, o incluso sólo upstart.

npm: El gestor de paquetes de Node

Cuando se habla de Node.js, algo que definitivamente no se debe omitir es el soporte incorporado para la gestión de paquetes utilizando la herramienta npm que viene por defecto con cada instalación de Node.js. La idea de los módulos npm es bastante similar a la de Ruby Gems: un conjunto de componentes disponibles públicamente y reutilizables, disponibles mediante una fácil instalación a través de un repositorio en línea, con gestión de versiones y dependencias.

Se puede encontrar una lista completa de módulos empaquetados en el sitio web de npm, o acceder a ellos utilizando la herramienta npm CLI que se instala automáticamente con Node.js. El ecosistema de módulos está abierto a todos, y cualquiera puede publicar su propio módulo que aparecerá en el repositorio npm. Se puede encontrar una breve introducción a npm en una Guía para principiantes, y detalles sobre la publicación de módulos en el Tutorial de publicación de npm.

Algunos de los módulos npm más útiles hoy en día son:

  • express – Express.js, un marco de desarrollo web inspirado en Sinatra para Node.js, y el estándar de facto para la mayoría de las aplicaciones Node.js que existen hoy en día.
  • hapi – un marco de trabajo centrado en la configuración, muy modular y sencillo de usar, para construir aplicaciones web y de servicios
  • connect – Connect es un marco de trabajo de servidor HTTP extensible para Node.js, que proporciona una colección de «plugins» de alto rendimiento conocidos como middleware; sirve como base para Express.
  • socket.io y sockjs – Componente del lado del servidor de los dos componentes de websockets más comunes que existen actualmente.
  • pug (antes Jade) – Uno de los motores de plantillas más populares, inspirado en HAML, por defecto en Express.js.
  • mongodb y mongojs – Envoltorios de MongoDB para proporcionar la API para las bases de datos de objetos de MongoDB en Node.js.
  • redis – Biblioteca cliente de Redis.
  • lodash (underscore, lazy.js) – El cinturón de utilidades de JavaScript. Underscore inició el juego, pero fue derrocado por uno de sus dos homólogos, principalmente debido a un mejor rendimiento y una implementación modular.
  • forever – Probablemente la utilidad más común para asegurar que un determinado script de Node se ejecute continuamente. Mantiene tu proceso Node.js en producción ante cualquier fallo inesperado.
  • bluebird – Una implementación completa de Promises/A+ con un rendimiento excepcionalmente bueno
  • moment – Una librería ligera de fechas en JavaScript para analizar, validar, manipular y formatear fechas.

La lista continúa. Hay toneladas de paquetes realmente útiles por ahí, disponibles para todos (sin ofender a los que he omitido aquí).

Donde se debe usar Node.js

Chat es la aplicación más típica en tiempo real y multiusuario. Desde el IRC (en su día), pasando por muchos protocolos propietarios y abiertos que se ejecutan en puertos no estándar, hasta la capacidad de implementar todo hoy en día en Node.js con websockets que se ejecutan a través del puerto estándar 80.

La aplicación de chat es realmente el ejemplo de punto dulce para Node.js: es una aplicación ligera, de alto tráfico, intensiva en datos (pero de bajo procesamiento/computación) que se ejecuta a través de dispositivos distribuidos. También es un gran caso de uso para el aprendizaje, ya que es simple, pero cubre la mayoría de los paradigmas que se utilizan en una aplicación típica de Node.js.

Tratemos de describir cómo funciona.

En el escenario más simple, tenemos una sola sala de chat en nuestro sitio web donde la gente viene y puede intercambiar mensajes en uno-a-muchos (en realidad todos). Por ejemplo, digamos que tenemos tres personas en el sitio web todos conectados a nuestro tablero de mensajes.

En el lado del servidor, tenemos una simple aplicación Express.js que implementa dos cosas: 1) un manejador de peticiones GET ‘/’ que sirve la página web que contiene tanto un tablero de mensajes como un botón ‘Send’ para inicializar la entrada de nuevos mensajes, y 2) un servidor de websockets que escucha los nuevos mensajes emitidos por los clientes de websocket.

En el lado del cliente, tenemos una página HTML con un par de manejadores configurados, uno para el evento de clic del botón ‘Send’, que recoge el mensaje de entrada y lo envía por el websocket, y otro que escucha los nuevos mensajes entrantes en el cliente de websockets (es decir, mensajes enviados por otros usuarios, que el servidor ahora quiere que el cliente muestre).

Cuando uno de los clientes publica un mensaje, esto es lo que sucede:

  • El navegador capta el clic del botón ‘Enviar’ a través de un controlador de JavaScript, recoge el valor del campo de entrada (es decir, el texto del mensaje), y emite un mensaje websocket utilizando el cliente websocket conectado a nuestro servidor (inicializado en la inicialización de la página web).
  • El componente del lado del servidor de la conexión websocket recibe el mensaje y lo reenvía a todos los demás clientes conectados utilizando el método broadcast.
  • Todos los clientes reciben el nuevo mensaje como un mensaje push a través de un componente websockets del lado del cliente que se ejecuta dentro de la página web. A continuación, recogen el contenido del mensaje y actualizan la página web en el lugar añadiendo el nuevo mensaje al tablero.

Imagen tomada del blog original.

Este es el ejemplo más sencillo. Para una solución más robusta, podrías utilizar una caché simple basada en el almacén Redis. O en una solución aún más avanzada, una cola de mensajes para manejar el enrutamiento de los mensajes a los clientes y un mecanismo de entrega más robusto que pueda cubrir las pérdidas temporales de conexión o el almacenamiento de mensajes para los clientes registrados mientras están fuera de línea. Pero independientemente de las mejoras que realice, Node.js seguirá operando bajo los mismos principios básicos: reaccionar a los eventos, manejar muchas conexiones concurrentes y mantener la fluidez en la experiencia del usuario.

API SOBRE UNA BD DE OBJETOS

Aunque Node.js realmente brilla con aplicaciones en tiempo real, es bastante natural para exponer los datos de las BD de objetos (por ejemplo, MongoDB). Los datos almacenados en JSON permiten que Node.js funcione sin el desajuste de impedancia y la conversión de datos.

Por ejemplo, si estás usando Rails, convertirías de JSON a modelos binarios, y luego los expondrías de nuevo como JSON a través del HTTP cuando los datos son consumidos por React.js, Angular.js, etc., o incluso por simples llamadas jQuery AJAX. Con Node.js, puedes simplemente exponer tus objetos JSON con una API REST para que el cliente los consuma. Además, no tienes que preocuparte de convertir entre JSON y cualquier otra cosa cuando leas o escribas desde tu base de datos (si estás usando MongoDB). En resumen, puede evitar la necesidad de múltiples conversiones mediante el uso de un formato de serialización de datos uniforme a través del cliente, el servidor y la base de datos.

ENTRADAS DE CONJUNTO

Si está recibiendo una gran cantidad de datos concurrentes, su base de datos puede convertirse en un cuello de botella. Como se muestra arriba, Node.js puede manejar fácilmente las conexiones concurrentes por sí mismo. Pero como el acceso a la base de datos es una operación de bloqueo (en este caso), nos encontramos con problemas. La solución es reconocer el comportamiento del cliente antes de que los datos se escriban realmente en la base de datos.

Con ese enfoque, el sistema mantiene su capacidad de respuesta bajo una carga pesada, lo que es particularmente útil cuando el cliente no necesita una confirmación firme de la escritura de datos con éxito. Ejemplos típicos incluyen: el registro o la escritura de datos de seguimiento de usuarios, procesados en lotes y no utilizados hasta un momento posterior; así como las operaciones que no necesitan reflejarse al instante (como la actualización de un recuento de ‘Likes’ en Facebook) donde la consistencia eventual (tan a menudo utilizada en el mundo NoSQL) es aceptable.

Los datos se ponen en cola a través de algún tipo de infraestructura de almacenamiento en caché o de cola de mensajes (MQ) (por ejemplo, RabbitMQ, ZeroMQ) y digeridos por un proceso separado de escritura por lotes de la base de datos, o servicios de backend de procesamiento intensivo de computación, escritos en una plataforma de mejor rendimiento para tales tareas. Se puede implementar un comportamiento similar con otros lenguajes/marcos, pero no en el mismo hardware, con el mismo rendimiento elevado y mantenido.

Imagen tomada del artículo original.

En resumen: con Node, puedes apartar las escrituras de la base de datos y tratarlas más tarde, procediendo como si tuvieran éxito.

STREAMING DE DATOS

En las plataformas web más tradicionales, las peticiones y respuestas HTTP se tratan como eventos aislados; de hecho, en realidad son flujos. Esta observación puede ser utilizada en Node.js para construir algunas características interesantes. Por ejemplo, es posible procesar archivos mientras se están cargando, ya que los datos llegan a través de un flujo y podemos procesarlos de forma online. Esto podría hacerse para la codificación de audio o vídeo en tiempo real, y el proxy entre diferentes fuentes de datos (véase la siguiente sección).

PROXY

Node.js se emplea fácilmente como un proxy del lado del servidor donde puede manejar una gran cantidad de conexiones simultáneas de manera no bloqueante. Es especialmente útil para proxyar diferentes servicios con diferentes tiempos de respuesta, o recoger datos de múltiples puntos de origen.

Un ejemplo: considere una aplicación del lado del servidor que se comunica con recursos de terceros, tirando de los datos de diferentes fuentes, o el almacenamiento de activos como imágenes y vídeos a los servicios de la nube de terceros.

Aunque existen servidores proxy dedicados, el uso de Node en su lugar podría ser útil si su infraestructura de proxy es inexistente o si necesita una solución para el desarrollo local. Con esto quiero decir que podrías construir una aplicación del lado del cliente con un servidor de desarrollo Node.js para los activos y las solicitudes de proxy/stubbing API, mientras que en producción manejarías dichas interacciones con un servicio de proxy dedicado (nginx, HAProxy, etc.).

BROKERAGE – STOCK TRADER’S DASHBOARD

Volvamos al nivel de aplicación. Otro ejemplo en el que predomina el software de escritorio, pero que podría sustituirse fácilmente por una solución web en tiempo real, es el software de negociación de los corredores de bolsa, que se utiliza para seguir los precios de las acciones, realizar cálculos/análisis técnicos y crear gráficos/cuadros.

El cambio a una solución basada en la web en tiempo real permitiría a los corredores cambiar fácilmente de estación de trabajo o de lugar de trabajo. Pronto podríamos empezar a verlos en la playa de Florida… o en Ibiza… o en Bali.

Monitorización de aplicaciones DASHBOARD

Otro caso de uso común en el que Node-with-web-sockets encaja perfectamente: el seguimiento de los visitantes del sitio web y la visualización de sus interacciones en tiempo real. Podrías estar recopilando estadísticas en tiempo real de tu usuario, o incluso pasar al siguiente nivel introduciendo interacciones dirigidas con tus visitantes abriendo un canal de comunicación cuando lleguen a un punto específico de tu embudo – un ejemplo de esto se puede encontrar con CANDDi.

Imagina cómo podrías mejorar tu negocio si supieras lo que tus visitantes están haciendo en tiempo real – si pudieras visualizar sus interacciones. Con los sockets bidireccionales y en tiempo real de Node.js, ahora puede hacerlo.

SYSTEM MONITORING DASHBOARD

Ahora, visitemos el lado de la infraestructura. Imagina, por ejemplo, un proveedor de SaaS que quiere ofrecer a sus usuarios una página de monitorización de servicios (por ejemplo, la página de estado de GitHub). Con el bucle de eventos de Node.js, podemos crear un potente panel de control basado en la web que compruebe los estados de los servicios de forma asíncrona y empuje los datos a los clientes utilizando websockets.

Tanto los estados de los servicios internos (dentro de la empresa) como los públicos pueden ser informados en vivo y en directo utilizando esta tecnología. Empuje esa idea un poco más allá y trate de imaginar un Centro de Operaciones de Red (NOC) monitoreando aplicaciones en un operador de telecomunicaciones, proveedor de nube/red/alojamiento, o alguna institución financiera, todo ejecutado en la pila web abierta respaldada por Node.js y websockets en lugar de Java y/o Applets de Java.

Nota: No trate de construir sistemas de tiempo real duro en Node.js (es decir, sistemas que requieren tiempos de respuesta consistentes). Erlang es probablemente una mejor opción para esa clase de aplicaciones.

Aplicaciones web del lado del servidor

Node.js con Express.js también se puede utilizar para crear aplicaciones web clásicas en el lado del servidor. Sin embargo, aunque es posible, este paradigma de petición-respuesta en el que Node.js llevaría el HTML renderizado no es el caso de uso más típico. Hay argumentos a favor y en contra de este enfoque. Aquí hay algunos hechos a considerar:

Pros:

  • Si su aplicación no tiene ningún cálculo intensivo de la CPU, se puede construir en Javascript de arriba a abajo, incluso hasta el nivel de base de datos si se utiliza JSON almacenamiento de objetos DB como MongoDB. Esto facilita el desarrollo (incluyendo la contratación) significativamente.
  • Los rastreadores reciben una respuesta HTML completamente renderizada, que es mucho más amigable para el SEO que, por ejemplo, una aplicación de una sola página o una aplicación de websockets ejecutada sobre Node.js.

Cons:

  • Cualquier cálculo intensivo de la CPU bloqueará la capacidad de respuesta de Node.js, por lo que una plataforma roscada es un mejor enfoque. Alternativamente, usted podría tratar de escalar el cálculo(*).
  • Usar Node.js con una base de datos relacional es todavía un dolor (ver más abajo para más detalles). Hazte un favor y coge cualquier otro entorno como Rails, Django, o ASP.Net MVC si estás intentando realizar operaciones relacionales.

(*) Una alternativa a los cálculos intensivos de la CPU es crear un entorno respaldado por MQ altamente escalable con procesamiento de back-end para mantener Node como un «oficinista» de cara al frente para manejar las peticiones de los clientes de forma asíncrona.

Aplicación web del lado del servidor con una base de datos relacional detrás

Comparando Node.js con Express.js frente a Ruby on Rails, por ejemplo, hay una clara decisión a favor de este último cuando se trata de acceso a datos relacionales.

Las herramientas de BD relacionales para Node.js están todavía bastante poco desarrolladas, en comparación con la competencia. Por otro lado, Rails proporciona automáticamente una configuración de acceso a datos desde el primer momento, junto con herramientas de apoyo a las migraciones de esquemas de BD y otras gemas (juego de palabras). Rails y sus frameworks similares tienen implementaciones maduras y probadas de la capa de acceso a datos Active Record o Data Mapper, que echaremos de menos si intentamos replicarlas en JavaScript puro.(*)

Aún así, si realmente queremos seguir con JS todo el tiempo, echemos un vistazo a Sequelize y Node ORM2.

(*) Es posible, y no es raro, utilizar Node.js únicamente como una fachada pública, manteniendo su back-end Rails y su fácil acceso a una base de datos relacional.

COMPUTACIÓN/PROCESAMIENTO DEL LADO DEL SERVIDOR

Cuando se trata de computación pesada, Node.js no es la mejor plataforma. No, definitivamente no quieres construir un servidor de computación Fibonacci en Node.js. En general, cualquier operación intensiva de la CPU anula todos los beneficios de rendimiento que ofrece Node con su modelo de E/S basado en eventos y sin bloqueos, porque cualquier petición entrante se bloqueará mientras el hilo está ocupado con su cálculo de números.

Como se ha dicho anteriormente, Node.js es de un solo hilo y utiliza sólo un núcleo de CPU. Cuando se trata de añadir concurrencia en un servidor multinúcleo, el equipo del núcleo de Node está trabajando en un módulo de clúster. También puede ejecutar varias instancias del servidor Node.js con bastante facilidad detrás de un proxy inverso a través de nginx.

Con la agrupación, todavía debe descargar toda la computación pesada a los procesos de fondo escritos en un entorno más apropiado para eso, y hacer que se comuniquen a través de un servidor de cola de mensajes como RabbitMQ.

Aunque su procesamiento de fondo podría ejecutarse en el mismo servidor inicialmente, tal enfoque tiene el potencial de una escalabilidad muy alta. Esos servicios de procesamiento en segundo plano podrían ser fácilmente distribuidos a servidores de trabajadores separados sin la necesidad de configurar las cargas de los servidores web de cara al público.

Por supuesto, también se podría utilizar el mismo enfoque en otras plataformas, pero con Node.js obtienes ese alto rendimiento de peticiones/segundo del que hemos hablado, ya que cada petición es una pequeña tarea manejada muy rápida y eficientemente.

Conclusión

Hemos discutido Node.js desde la teoría a la práctica, comenzando con sus objetivos y ambiciones, y terminando con sus puntos dulces y escollos. Cuando la gente se encuentra con problemas con Node, casi siempre se reduce al hecho de que las operaciones de bloqueo son la raíz de todos los males – el 99% de los malos usos de Node vienen como consecuencia directa.

Recuerda: Node.js nunca fue creado para resolver el problema de escalado de computación. Fue creado para resolver el problema de escalamiento de E/S, lo cual hace muy bien.