Saltar al contenido

Blog técnico | Running Node.js en producción

Como toda empresa que trabaja con productos en la web, uno de los lenguajes que usamos es Javascript. Mientras que casi todos los equipos de ingeniería de productos usan Javascript de alguna forma, también tenemos alrededor de 12 de 33 equipos usando Node.js. Otros lenguajes populares en la compañía incluyen C# y Python. Este post se centrará específicamente en Node.js y en la experiencia de desplegar el tiempo de ejecución en producción durante los últimos años. En términos generales, yo daría estas recomendaciones y consejos a cualquier equipo que desarrolle sistemas de producción.

Como cualquier herramienta o lenguaje, los nodos pueden enmarcar la forma en que uno piensa acerca de un problema de manera expansiva, pero antes de elegir una tecnología se debe comenzar con una declaración del problema. El nodo puede ser una buena opción si algunos de los siguientes puntos se aplican a su dominio del problema.

Blog técnico | Running Node.js en producción
Blog técnico | Running Node.js en producción
  1. Tiene muchos eventos pequeños que necesitan ser procesados con lógica de negocios en marcos de tiempo subsegundos
  2. Necesidad de Iterar en el espacio problemático rápidamente
  3. Quiere aprovechar las bibliotecas de código abierto y las API
  4. Interactuará con múltiples bases de datos y protocolos de red
  5. Quiere renderizar HTML
  6. Necesita un backend para un móvil o una aplicación de una sola página
  7. Usando JSON como formato de serialización

Node fue una respuesta a la ejecución de un proceso por hilo detrás de Apache y NGINX o a veces tratando de incrustar la lógica directamente en NGINX. Tal vez clasificar Node.js como un enrutador de eventos de aplicación sea útil. Node sobresale en la capa por encima de los equilibradores de carga de la capa 7 del modelo estándar de OSI. Desafortunadamente la palabra aplicación está sobrecargada aquí.

Una muestra de algunos espacios problemáticos Node.js puede no ser un buen candidato para include1. Compiladores2. Simuladores de física3. Sistemas de orientación4. Bases de datos5. Equilibradores de carga de la capa 4

Antes de saltar a los desafíos y beneficios de ejecutar Nodo en producciones, echemos un vistazo a algunos malentendidos comunes.

Malentendidos comunes

El nodo es de un solo hilo

La respuesta a esto es más complicada que en otros lenguajes y depende de qué parte de la pila se habla de V8, libuv o de las API del sistema. La clave aquí es mantener el tiempo de la cpu en el bucle de eventos al mínimo, unos pocos milisegundos antes de programar en el threadpool. El tiempo de E/S y de la base de datos deberían ser los cuellos de botella. El código en la tierra de Javascript debería actuar como coordinador de la memoria, el disco y la red. Si los tiempos de respuesta se convierten en un problema, mira tareas como la compresión, encriptación, regex e incluso serialización para descargar a un trabajador. Aquí hay una sección de “No bloquear el bucle de eventos” de nodejs.org.

Artículo completo No bloquees el bucle de eventos

El nodo es javascript y javascript es lento

Node.js es un tiempo de ejecución de JavaScript y hay muchos otros. Para otros ejemplos de tiempo de ejecución mira en Firefox, Edge y Safari. Junto con un JIT de Javascript cada navegador tiene un conjunto de APIs para interactuar con el sistema operativo. Chrome y Node comparten V8.

Gran parte del Nodo está escrito en C o C++. V8, libuv, y las API del sistema, todas corren en tierra nativa. Con la nueva N-API para módulos nativos se espera que más código se ejecute en C++ o incluso Rust cuando el paso de datos a través de la frontera del lenguaje tenga sentido. Vea la encuadernación de neón para ejemplos de Rust. Si su problema requiere optimización de la computación asegúrese de hacer un perfil primero.

Libuv es el bucle de eventos asíncronos y otros proyectos como el Kestrel de .NET Core lo han usado en el pasado (aunque ahora está basado en sockets gestionados). V8 es un “motor” de Javascript construido en Google escrito en C++. Para muchos sistemas los cuellos de botella no estarán en la CPU sino en algún lugar de la arquitectura general de redes y bases de datos. Recuerde considerar el tiempo de desarrollo y la velocidad de iteración también antes de cambiar a tiempos de ejecución de menor nivel.

El nodo es para proyectos de juguetes

Tal vez parte de esta percepción proviene del hecho de que los servidores son increíblemente sencillos de iniciar en el Nodo. Unas pocas docenas de líneas de código, se despliegan al servicio como ahora y tu proyecto está disponible para el mundo, pero esto no es todo lo que ofrece Node. Node se usa para servir frontales y backends para algunos de los sitios y aplicaciones más activos del mundo como Walmart y Netflix. Bajo el Nodo de Electrones también se utiliza ahora para respaldar las aplicaciones de escritorio populares ver Slack y VS Code. Todas las herramientas necesarias están en su lugar, incluyendo el empaquetado, las pruebas, el monitoreo, el rastreo, el perfilado y el CI/CD para desplegar un sistema confiable y mantenible.

Dicho esto, el Nodo no está exento de desafíos. Algunos de los principales escollos para los desarrolladores que vienen de otros lenguajes tienden a girar alrededor de estas ideas relacionadas con el uso ubicuo de las llamadas de retorno, pasando las funciones como argumentos y la E/S asíncrona. Aquí hay algunos problemas que hemos visto en y sugerencias para evitarlos.

Desafíos de la producción

Async all the things

Hay demasiadas formas de escribir código asíncrono en el Nodo. Si pudiera elegir sólo un área para que los equipos y la comunidad en general se centren en ella sería esta. Streams, callbacks, promesas, librerías asíncronas como co.wrap con generadores y Rxjs son sólo algunas de las formas de tratar con el código asíncrono y para empeorar las cosas muchos conceptos se solapan. Pensar en esto combinado con el manejo de errores puede ser abrumador. Si las preguntas debajo de cada ejemplo no pueden ser respondidas en unos pocos minutos, considere algunos talleres sobre estos temas. Este tiempo se pagará por sí mismo con la velocidad de desarrollo y la reducción de errores. Aquí están los patrones mínimos con los que yo verificaría la comprensión y las estrategias de prueba.

La configuración de los oyentes de los eventos es a menudo el “principal” de un programa de Nodo.

constserver=http.createServer((req,res)=,{res.setHeader($0027Content-Type$0027,$0027text/html$0027)res.setHeader($0027X-Foo$0027,$0027bar$0027)res.writeHead(200,{$0027Content-Type$0027:$0027text/plain$0027})res.end($0027ok$0027)})
  1. ¿Qué tipo de cosas se requieren?
  2. ¿Qué tipo de cosa es la res?
  3. ¿Qué sucede en la red cuando se llaman res.writeHead y res.end?
  4. ¿Cómo se probaría la función que se pasó como primer argumento?
  5. ¿Cómo manejaría esta función los errores?

Envolver las llamadas anidadas en una promesa es increíblemente común y puede suavizar la interacción con todo tipo de complicados flujos de control. No confíe solamente en herramientas como util.promisify() ya que el código de sincronización subyacente puede no tener la (err, data) => {} interfaz

functionwrap_with_promise(interval,verify,timeout=1000){returnnewPromise((resolve,reject)=; {rechazar("timeout")},timeout)}función de sincronizaciónfoo(){constatar=(cb)=// cb(null, "what_we_want")cb(null, "not_what_we_want")}consult=esperar_con_la_consola(10,verificar). log(result)}foo()
  1. ¿Para qué sirve wrap_with_promise()?
  2. ¿Hay algún bicho?
  3. ¿Qué cambia si la línea cb(null, “what_we_want”) no está comentada?
  4. Este programa se puede escribir más claramente con la espera y un bucle de tiempo ¿puedes ver cómo?
  5. ¿Cómo probaría esta función?

Como último ejemplo, considere bifurcarse y unirse al código de sincronización. Echa un vistazo al mismo problema en otros idiomas y compara, ya que a menudo son mucho más complicados.

functionfork_and_join(data){constpromise1=async_thing_1(data[0])constpromise2=async_thing_2(data[1])returnPromise.all([promise1,promise2])}
  1. ¿Por qué no usamos wait before async_thing_1(data[0])?
  2. ¿Qué sucede si la promesa1 tiene una latencia significativamente mayor que la promesa2?
  3. ¿Hay alguna forma de que se pueda agotar el tiempo?
  4. ¿Cómo se puede hacer esto sin promesas? (este es un ejercicio divertido y mejorará la comprensión del bucle de eventos)

Los arroyos son otra área donde los desarrolladores se tropiezan pero no es tan común interactuar con ellos directamente. Si se necesitan grandes tamaños de carga o altas tasas de rendimiento para su proyecto, dedique un par de días a este concepto construyendo pruebas con las cargas apropiadas. Será interesante ver cómo la iteración de la asíncrona influye en este espacio y algunos experimentos en el Nodo v10 se ven bien hasta ahora.

función de sincronización*readLines(path){letfile=awaitfileOpen(path)try{while(!file.EOF){yieldawaitfile.readLine()}}finalmente{awaitfile.close()}}

Para más información sobre este concepto, véase la Propuesta de Iteración Asíncrona del TC39

Después de que un equipo se haya tomado el tiempo para entender las diversas estrategias asincrónicas deciden unos pocos patrones estándar. Considere cualquier cosa que no siga estos patrones deuda tecnológica y abogar por ciclos para limpiar ese código desactualizado.

Resolución del DNS

Si se observan picos ocasionales de latencia y el proceso llama a recursos externos sin direcciones IP directas, el DNS puede ser el culpable. En el pasado, el Nodo no ha almacenado en caché el DNS e incluso puede ser la mayor parte de la latencia de su solicitud. Tenga cuidado con el almacenamiento en caché del DNS en los servicios de escalado dinámico y, si utiliza bibliotecas de ayuda, verifique que respeta el TTL.

Para más información sobre este tema, véase1. Documentación del DNS2. Número 3 de Github. Petición de tirar

Manejo de errores

El código de sincronización y el manejo de errores en muchos idiomas está plagado de trampas y javascript no es una excepción. Estén atentos a códigos como el siguiente. Cualquier cosa que tire do_other_async() se perderá o, peor aún, bloqueará su proceso. El signo revelador es cualquier función de asincronía que no se espera o donde el retorno no se captura en una variable.

functionevent_handler(event){constresult_1=awaitdo_some_async(event)do_other_async(event)// danger danger!!!returnresultado_1}

Mi consejo general es evitar tirar errores en javascript completamente y en su lugar tratar los errores como valores. En otras palabras, atrapar cualquier error y devolverlo en la misma ruta de código como un resultado válido. Esta fue una ventaja del estilo de devolución con el error como el primer argumento a veces llamado errbacks o error first callbacks. El movimiento de la comunidad alejándose de los errores como valores es una regresión en mi opinión. Los errores son datos, ¿por qué tirarlos a la basura?

functionsome_callback(err,data){// el error es un valor y podemos usar un flujo de control regular // en lugar de anidar más intente declaraciones de catchif(err){// tratar con errores}// usar datos}

Aquí hay un ejemplo de cómo integrar esta idea en el código usando “async await”. Si el paso 1 y el paso 2 también siguen el patrón, se puede evitar el try catch y sólo será necesario en los bordes o donde el código interactúe con sistemas externos.

asyncfunctionerrors_as_values_function(data){// ejecutar el primer paso y volver antes si errorconst[error1,result1]=esperarpaso1(data)if(error1)return[error1,null]// ejecutar el segundo paso y volver antes si errorconst[error2, result2]=esperaelpaso2(result1)if(error2)return[error2,null]// posiblemente transformar la carga útil de retorno, de lo contrario return return[null,result2]}// usando la función anterior seguirá el mismo patrón// aquí en el contexto de un servidor api podría parecer algo comoeconstserver=http. createServer((req,res)=>{const[error,result]=awaiterrors_as_values_function(req)if(error){res.writeHead(500,{$0027Content-Type$0027:$0027application/json$0027});res. end({error:error.toString()});}else{res.writeHead(200,{$0027Content-Type$0027:$0027application/json$0027});res.end(result);}})

Cada paso de async devuelve una lista de dos elementos con el error como primer argumento

Relacionado con este problema está la falta de manejo de errores de alto nivel. Asegúrate de tener algo que se parezca a esto en el punto de entrada de cualquier código de servidor a menos que quieras que el proceso se bloquee por errores no detectados y prometa rechazos. Recuerde recoger los recuentos de errores en algún lugar con monitoreo y alarmas!

process.on($0027unhandledRejection$0027,(reason,p)= >{log_or_metric($0027unhandledRejection$0027,reason)})process.on($0027uncaughtException$0027,err= >{log_or_metric($0027uncaughtException$0027,err)})

Fugas de memoria

Aunque no es un problema común en las fugas de memoria han ocurrido. Tienden a maximizar el montón durante decenas de horas según nuestras tarifas de solicitud. Aunque no es una excusa para dejar una fuga en el lugar, una solución es reiniciar, drenar o matar los procesos a intervalos regulares. Para encontrar el origen del problema, las siguientes herramientas pueden ayudar1. Para tomar una foto del montón de basura2. Para verificar que las pilas de llamadas se vean correctamente dtrace

Fugas en los enchufes

Aunque es trivial crear muchas conexiones en el Nodo, lo que no está tan claro es limpiarlas. Cuando se trabaja con Node será importante monitorizar los sockets abiertos a recursos compartidos como bases de datos, colas de mensajes y APIs del sistema, considerar el uso de la agrupación de conexiones o compartir una conexión a través de eventos si este es el caso. Una herramienta útil para este tipo de problemas es lsof

Validación del tipo de tiempo de ejecución

Hay algunas formas de trabajar con tipos en Javascript, Typescript y Flow es lo que viene a la mente de la mayoría de los desarrolladores en estos días. Sin embargo, igual de importante es la validación del tiempo de ejecución. Como mínimo, cuando se trabaja a través de las fronteras de servicios y mensajes, los programas deberían validar los tipos, incluso mejor para verificar a través de cualquier E/S. Un ejemplo de la excelente biblioteca joi

constschema=Joi.object().keys({nombredeusuario:Joi.string().alphanum().min(3).max(30).required(),password:Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),access_token:[Joi. string(),Joi.number()],año de nacimiento:Joi.number().integer().min(1900).max(2013),email:Joi.string().email({minDomainAtoms:2})}).with($0027username$0027,$0027birthyear$0027). sin($0027password$0027,$0027access_token$0027);// Devuelve result.constresult=Joi.validate({nombredeusuario:$0027abc$0027,añodenacimiento:1994},esquema);// resultado. error === null -----; valid// También puede pasar una llamada de retorno que será llamada sincronizadamente con el resultado de la validación.Joi.validate({username:$0027abc$0027,birthyear:1994},schema,function(err,value){});// err === null -----; valid

Fuera de la memoria

Los errores de memoria tienden a tomar desprevenidos a los desarrolladores en Node por algunas razones. La principal para tener en cuenta que V8 utiliza un tamaño de pila por defecto de alrededor de 1,5 GB. Este problema aparece a menudo cuando se ejecutan procesos de arranque de bases de datos o cuando se transmiten artefactos como imágenes o archivos comprimidos. Si el proceso está sirviendo a una API ocupada y la E/S no está todavía saturada, eche un vistazo a los flujos para mantener el uso de la memoria bajo. Si aumentar el tamaño de la pila es una opción, ver –v8-options y –max_old_space_size para más información.

Estos problemas suelen pasar desapercibidos en un entorno de desarrollo o de puesta en escena porque los casos de prueba no son lo suficientemente grandes. Valide cualquier cambio en el tamaño de la pila contra sus requisitos de distribución de latencia ya que puede tener impactos de recolección de basura. Recientemente se han realizado algunas mejoras en la recogida de basura y el marcado concurrente está habilitado por defecto en Chrome 64 y Node.js v10. Para saber más sobre el nuevo GC vea el marcado concurrente

Seguridad

El reciente evento [protegido por correo electrónico] fue un buen recordatorio de que la inyección en la cadena de suministro siempre será un vector al que hay que prestar atención. Sin embargo, la mayoría de los gestores de paquetes son vulnerables al mismo tipo de ataque. El NPM es un objetivo extra atractivo porque el Nodo fomenta muchas pequeñas dependencias y la comunidad es gigantesca. El jurado aún no ha decidido cuál es el mejor enfoque para solucionar este problema, pero un buen comienzo sería que el NPM requiriera que todas las escrituras de un módulo sean 2FA.

Para ayudar a defenderse de los exploits del código, mira la auditoría npm como parte de tu proceso de construcción

Estructura del proyecto

Utilizamos el concepto de Contextos Limitados y animamos a cada contexto a utilizar un único depósito de git. Hay muchas herramientas para ayudar a gestionar grandes proyectos de Nodos, sin embargo considere cuidadosamente si la complejidad añadida es necesaria dada la accesibilidad del NPM. A continuación se muestra un diseño de proyecto que ha sido escalado sin necesidad de herramientas adicionales.

Nombre del contexto (root) - bibliotecas - lib_1 - src index.js index.test.js paquete.json - lib_2 -src index.js index.test.js paquete.json - servicios - service_1 - src index.js index.test.js paquete.json - service_2 - src index.js index.test.js paquete.json Readme.md

Cada carpeta en los servicios es un proyecto de despliegue independiente ligado a una tubería de CI. Cualquier código que necesite ser compartido entre servicios se convertirá en una biblioteca, por ejemplo, modelos, registros y envoltorios de API. Las bibliotecas se despliegan en NPM y luego se agregan como una dependencia de un servicio. Los cambios de ruptura en las bibliotecas pueden actualizarse de forma incremental en los servicios. Las pruebas viven en la misma carpeta que el código.

Estas son algunas de las partes más difíciles de trabajar con Node, pero pueden ser superadas con un poco de práctica y las herramientas son cada vez mejores cada día. Ahora, sobre algunos de los puntos positivos de trabajar con Nodo.

Beneficios

Asincronía por defecto

El nodo ha estado sincronizado desde el principio y todas las librerías y drivers han sido escritos para aprovechar el bucle de eventos. Otros lenguajes y marcos que lo han añadido después del hecho tienden a tener problemas con varias dependencias usando llamadas síncronas.

LTS

El nodo es unas pocas liberaciones en la línea LTS ahora que ha sido maravilloso para trabajar en un entorno empresarial. Pedimos que cada servicio esté en una versión actualizada de LTS. Esto, combinado con el compromiso de V8 con Node y TC39 integrando nuevas propuestas de Javascript, le da a Node un camino sólido para la evolución futura.

Compilar en javascript

utiliza Babel y Typescript en algunas configuraciones en muchos de nuestros proyectos. Algunas de las configuraciones iniciales pueden ser frustrantes pero los beneficios de escribir contra una sintaxis común y transponer a los diversos objetivos de navegadores y tiempo de ejecución ahorra tiempo a largo plazo. Felicitaciones a Microsoft por el gran trabajo en torno a Typescript, la capacidad de añadir un sistema de tipos ligero es una ventaja para proyectos más grandes. Esto también contribuye al camino de la evolución, ya que las nuevas características pueden obtener retroalimentación rápidamente de la comunidad. Un punto de precaución aquí es usar los sistemas de tipografía sin eliminar la naturaleza dinámica de javascript. Si su código y estructura de proyecto comienza a parecerse a C# o Java con sintaxis genérica en muchas funciones, tal vez esté usando el tiempo de ejecución equivocado.

NPM

La cantidad de proyectos sobre NPM es asombrosa. Tal vez más grande que el siguiente puñado de paquetes combinados. Aunque la cantidad no es siempre el mejor indicador, todavía tengo que encontrar un espacio problemático donde ni siquiera un módulo de baja calidad existe para ayudarme a empezar y una de las bellezas de la OSS está contribuyendo a hacer mejoras. Las mejoras del NPM en sí mismo son también una fuente positiva para el Nodo. Poner en marcha un módulo, probarlo y publicarlo es sencillo y hace que la colaboración sea simple. Las adiciones al cliente como cache, ci, prune, shrinkwrap y audit están puliendo algunas asperezas en los despliegues de producción. En muchos sentidos NPM es un líder y hay una gran competencia y colaboración entre otros administradores de paquetes de javascript e incluso otros tiempos de ejecución.

Edificio & Despliegue de artefactos

El desarrollo local y el establecimiento de tuberías de CI/CD es un territorio bien conocido ahora. Gran parte de las herramientas de la industria incluyen tuberías y documentación para ejecutar Node en instancias de nube, contenedores y plataformas sin servidores.

Sin servidor

Aunque el término “sin servidor” puede no ser la mejor opción, hay una creciente tendencia a desplegar código a capas de orquestación programadas automáticamente. La rápida hora de inicio del nodo y la arquitectura basada en eventos lo convierten en un ajuste natural para estos entornos. Probablemente no estamos muy lejos de que los despliegues de borde global se conviertan en una práctica estándar. Algunos consejos de aplicación general para operar en los diversos proveedores de nubes.

  1. Mantenga el tamaño del artefacto pequeño
  2. Presta atención a los tiempos de arranque en frío y mide la distribución de la latencia
  3. Tenga en cuenta que las solicitudes pueden ejecutarse varias veces
  4. Busque los límites de las tasas de las cuentas compartidas
  5. Aprovechar su naturaleza efímera y su escala a cero
  6. Se desarrollan con los TTL en mente típicamente entre 60 y 300 segundos

Ver los trabajadores de Cloudflare y Fly.io para saber cómo podría ser el futuro de las funciones de borde

Pruebas

En este punto, las herramientas de prueba en la comunidad de Nodos son fuertes. Mocha y Jest son ambos muy buenos para trabajar y muchos proyectos vienen con pruebas incorporadas ahora. El flujo de trabajo de pruebas npm install -> npm a menudo funciona sin necesidad de pensar en la plataforma o compilar objetivos.

Conclusión

Aunque Node tuvo una historia de gobierno rocosa, su alcance y productividad son difíciles de negar. El proceso de LTS ha aumentado la confianza y prácticamente todas las grandes empresas de tecnología están contribuyendo al proyecto. Superar el cambio al pensamiento asíncrono puede requerir algún esfuerzo pero vale la pena el tiempo, abrazar las llamadas de retorno y los tipos dinámicos para ver los beneficios. La necesidad de un tiempo de ejecución ligero basado en eventos está creciendo con el cambio a la programación computacional efímera y Node es una sólida elección para ese espacio problemático.

Categorías: technicalTags: javascript, devops, deep dive