C√≥mo reescrib√≠ un motor de b√ļsqueda de vuelos de PHP a NodeJS

Hola Mi nombre es Andrey, soy estudiante de posgrado en una de las universidades t√©cnicas de Mosc√ļ y a tiempo parcial muy modesto Emprendedor y desarrollador novato. En este art√≠culo, decid√≠ compartir mi experiencia de cambiar de PHP (que una vez me gust√≥ por su simplicidad, pero eventualmente me odiaron; explico por qu√© bajo el corte) a NodeJS. Aqu√≠ se pueden dar tareas muy triviales y aparentemente elementales, que, sin embargo, ten√≠a curiosidad personal por resolver durante mi conocimiento de NodeJS y las caracter√≠sticas del desarrollo del lado del servidor en JavaScript. Tratar√© de explicar y demostrar claramente que PHP finalmente ha entrado en la puesta del sol y ha dado paso a NodeJS. Tal vez incluso sea √ļtil para alguien aprender algunas caracter√≠sticas de la representaci√≥n de p√°ginas HTML en Node, que originalmente no est√° adaptado a esto de la palabra.


Introduccion


Mientras escribía el motor, utilicé las técnicas más simples. Sin gestores de paquetes, sin enrutamiento. Solo carpetas hardcore, cuyo nombre coincide con la ruta solicitada, e index.php en cada una de ellas, configuradas por PHP-FPM para admitir el grupo de procesos. Más tarde se hizo necesario usar Composer y Laravel, que fue el colmo para mí. Antes de pasar a la historia de por qué incluso decidí reescribir todo, desde PHP hasta NodeJS, te contaré un poco sobre los antecedentes.


Administrador de paquetes


A finales de 2018, pas√© a trabajar con un proyecto escrito en Laravel. Fue necesario corregir varios errores, realizar cambios en la funcionalidad existente, agregar algunos botones nuevos en la interfaz. El proceso comenz√≥ instalando el paquete y el administrador de dependencias. En PHP, Composer se usa para esto. Luego, el cliente proporcion√≥ un servidor con 1 n√ļcleo y 512 megabytes de RAM y esta fue mi primera experiencia con Composer. Al instalar dependencias en un servidor privado virtual con 512 megabytes de memoria, el proceso se bloque√≥ debido a la falta de memoria.


Wut?


Para m√≠, como persona familiarizada con Linux y con experiencia trabajando con Debian y Ubuntu, la soluci√≥n a este problema era obvia: instalar un archivo SWAP (archivo de intercambio) para aquellos que no est√°n familiarizados con la administraci√≥n de Linux). Un desarrollador inexperto novato que instal√≥ su primera distribuci√≥n de Laravel en Digital Ocean, por ejemplo, solo va al panel de control y aumenta la tarifa hasta que la instalaci√≥n de dependencias se detiene con un error de segmentaci√≥n de memoria. ¬ŅQu√© hay de NodeJS?
Y NodeJS tiene su propio administrador de paquetes: npm. Es mucho m√°s f√°cil de usar, m√°s compacto, puede funcionar incluso en un entorno con una cantidad m√≠nima de RAM. En general, no hay nada que culpar a Composer contra NPM, sin embargo, en caso de errores al instalar paquetes, Composer se bloquear√° como una aplicaci√≥n PHP normal y nunca sabr√° qu√© parte del paquete se instal√≥ y si se instal√≥ al final termina En general, para el administrador de Linux, la instalaci√≥n dpkg --configure -a = flashbacks en modo de rescate y dpkg --configure -a . Para cuando me sorprendieron tales "sorpresas", no me gustaba PHP, pero estos fueron los √ļltimos clavos en el ata√ļd de mi gran amor por PHP.


Soporte a largo plazo y problemas de versiones


¬ŅRecuerdas qu√© tipo de exageraci√≥n y asombro caus√≥ PHP7 cuando los desarrolladores lo presentaron por primera vez? ¬°Aumente la productividad en m√°s de 2 veces, y en algunos componentes hasta 5 veces! ¬ŅRecuerdas cuando naci√≥ la s√©ptima versi√≥n de PHP? ¬°Y qu√© tan r√°pido gan√≥ WordPress! Era diciembre de 2015. ¬ŅSab√≠a que PHP 7.0 ahora se considera una versi√≥n obsoleta de PHP y se recomienda actualizarlo ... No, no a la versi√≥n 7.1, sino a la versi√≥n 7.2. Seg√ļn los desarrolladores, la versi√≥n 7.1 ya est√° privada de soporte activo y solo recibe actualizaciones de seguridad. Y despu√©s de 8 meses, esto se detendr√°. Cesar√°, junto con el soporte activo y la versi√≥n 7.2. Resulta que para fines de este a√Īo, PHP solo tendr√° una versi√≥n actual: 7.3.


Versiones PHP actuales


De hecho, esto no ser√≠a una trampa y no atribuir√≠a esto a las razones de mi partida de PHP si los proyectos que escrib√≠ en PHP 7.0. * Ya no caus√≥ una advertencia de desaprobaci√≥n cuando lo abr√≠. Volvamos al proyecto donde se bloque√≥ la instalaci√≥n de dependencias. Este fue un proyecto escrito en 2015 en Laravel 4 con PHP 5.6. Parec√≠a que solo hab√≠an pasado 4 a√Īos, pero no: un mont√≥n de advertencias de desaprobaci√≥n, m√≥dulos obsoletos, la incapacidad de actualizar a Laravel 5 normalmente debido a un mont√≥n de actualizaciones del motor ra√≠z.


Y esto se aplica no solo a Laravel. Intente escribir cualquier aplicación PHP durante el soporte activo de las primeras versiones de PHP 7.0 y prepárese para pasar la noche buscando soluciones a los problemas que surgieron en módulos PHP obsoletos. Finalmente, un hecho interesante: el soporte para PHP 7.0 se suspendió antes que el soporte para PHP 5.6. Por un segundo


¬ŅQu√© hay de NodeJS? No dir√≠a que aqu√≠ todo es mucho mejor y que los per√≠odos de soporte para NodeJS son fundamentalmente diferentes de PHP. No, es casi lo mismo aqu√≠: cada versi√≥n LTS es compatible durante 3 a√Īos. Pero NodeJS tiene un poco m√°s de estas versiones m√°s actuales.


Versiones actuales de NodeJS


Si necesita implementar una aplicaci√≥n escrita en 2016, aseg√ļrese de que no tendr√° absolutamente ning√ļn problema con esto. Por cierto, la versi√≥n 6. * ya no ser√° compatible solo en abril de este a√Īo. Y en frente hay 8, 10, 11 y los pr√≥ximos 12.


Dificultades y sorpresas al cambiar a NodeJS


Comenzaré, tal vez, con la pregunta más emocionante para mí sobre cómo representar páginas HTML en NodeJS. Pero primero recordemos cómo se hace esto en PHP:


  1. Incruste HTML directamente en el c√≥digo PHP. Tambi√©n lo hacen todos los novatos que a√ļn no han llegado a MVC. Y as√≠ se hace en WordPress, lo cual es absolutamente horrible.
  2. Use MVC, que deber√≠a simplificar la interacci√≥n del desarrollador y proporcionar alg√ļn tipo de divisi√≥n del proyecto en partes, pero en realidad este enfoque solo complica todo a veces.
  3. Utiliza un motor de plantillas. La opción más conveniente, pero no en PHP. Solo mire la sintaxis sugerida en Twig or Blade con llaves y porcentajes.

Soy un ardiente oponente de combinar o fusionar varias tecnologías juntas. HTML debe existir por separado, los estilos para él por separado, JavaScript por separado (en React, esto generalmente parece monstruoso: HTML y JavaScript están mezclados). Es por eso que la opción ideal para desarrolladores con preferencias como la mía es un motor de plantillas. No tuve que buscarlo para una aplicación web en NodeJS durante mucho tiempo y opté por Jade (PugJS). Simplemente aprecie la simplicidad de su sintaxis:


  div.row.links div.col-lg-3.col-md-3.col-sm-4 h4.footer-heading . div.copyright div.copy-text 2017 - #{current_year} . div.contact-link span : a(href='mailto:hello@flaut.ru') hello@flaut.ru 

Aquí todo es bastante simple: escribí una plantilla, la descargué en la aplicación, la compilé una vez y luego la utilicé en cualquier lugar conveniente en cualquier momento conveniente. En mi opinión, el rendimiento de PugJS es aproximadamente 2 veces mejor que la representación al incrustar HTML en el código PHP. Si anteriormente en PHP el servidor generó una página estática en aproximadamente 200-250 milisegundos, ahora esta vez es de aproximadamente 90-120 milisegundos (no estamos hablando de la representación en PugJS, sino del tiempo tomado desde la solicitud de la página hasta la respuesta del servidor al cliente con HTML listo) ) Así es como se ve la carga y compilación de plantillas y sus componentes en la etapa de inicio de la aplicación:


 const pugs = {} fs.readdirSync(__dirname + '/templates/').forEach(file => { if(file.endsWith('.pug')) { try { var filepath = __dirname + '/templates/' + file pugs[file.split('.pug')[0]] = pug.compile(fs.readFileSync(filepath, 'utf-8'), { filename: filepath }) } catch(e) { console.error(e) } } }) //       return pugs.tickets({ ...config }) 

Parece incre√≠blemente simple, pero con Jade hubo una peque√Īa complejidad en la etapa de trabajar con HTML ya compilado. El hecho es que para implementar scripts en la p√°gina, se utiliza una funci√≥n asincr√≥nica, que toma todos los archivos .js del directorio y agrega la fecha de su √ļltimo cambio a cada uno de ellos. La funci√≥n tiene la siguiente forma:


 for(let i = 0; i < files.length; i++) { let period = files[i].lastIndexOf('.') // get last dot in filename let filename = files[i].substring(0, period) let extension = files[i].substring(period + 1) if(extension === 'js') { let fullFilename = filename + '.' + extension if(env === 'production') { scripts.push({ path: paths.production.web + fullFilename, mtime: await getMtime(paths.production.code + fullFilename)}) } else { if(files[i].startsWith('common') || files[i].startsWith('search')) { scripts.push({ path: paths.developer.scripts.web + fullFilename, mtime: await getMtime(paths.developer.scripts.code + fullFilename)}) } else { scripts.push({ path: paths.developer.vendor.web + fullFilename, mtime: await getMtime(paths.developer.vendor.code + fullFilename)}) } } } } 

En la salida, obtenemos una matriz de objetos con dos propiedades: la ruta al archivo y la hora en que se edit√≥ por √ļltima vez en la marca de tiempo (para actualizar el cach√© del cliente). El problema es que incluso en la etapa de recopilaci√≥n de archivos de secuencia de comandos de un directorio, todos se cargan en la memoria estrictamente alfab√©ticamente (ya que se encuentran en el directorio en s√≠ y los archivos se recopilan de arriba a abajo, desde el primero hasta el √ļltimo). Esto condujo al hecho de que el archivo app.js se carg√≥ al principio, y despu√©s vino el archivo core.min.js con polyfills y vendor.min.js al final. Este problema se resolvi√≥ de manera bastante simple: clasificaci√≥n muy banal:


 scripts.sort((a, b) => { if(a.path.includes('core.min.js')) { return -1 } else if(a.path.includes('vendor.min.js')) { return 0 } return 1 }) 

En PHP, todo tenía una apariencia monstruosa en forma de rutas a archivos JS previamente escritos en una cadena. Simple pero poco práctico.


NodeJS mantiene su aplicación en RAM


Esta es una gran ventaja. Todo est√° organizado para m√≠, de modo que en el servidor en paralelo e independientemente el uno del otro, hay dos sitios separados: la versi√≥n para el desarrollador y la versi√≥n de producci√≥n. Imagine que hice algunos cambios en los archivos PHP en el sitio de desarrollo y necesito implementar estos cambios en producci√≥n. Para hacer esto, debe detener el servidor o colocar un trozo de "lo siento, trabajo t√©cnico" y, en este momento, copiar los archivos individualmente de la carpeta del desarrollador a la carpeta de producci√≥n. Esto provoca alg√ļn tipo de tiempo de inactividad y puede provocar la p√©rdida de conversiones. La ventaja de la aplicaci√≥n en memoria en NodeJS es para m√≠ que todos los cambios en los archivos del motor se realizar√°n solo despu√©s de que se reinicie. Esto es muy conveniente, porque puede copiar todos los archivos necesarios con los cambios y solo luego reiniciar el servidor. El proceso no lleva m√°s de 1-2 segundos y no causa tiempo de inactividad.
El mismo enfoque se usa en nginx, por ejemplo. Primero edita la configuración, nginx -t con nginx -t y solo luego realice cambios con el service nginx reload


Agrupación de una aplicación NodeJS


NodeJS tiene una herramienta muy conveniente: el administrador de procesos pm2 . ¬ŅC√≥mo solemos ejecutar aplicaciones en Node? Entramos en la consola y escribimos el node index.js . Tan pronto como cerramos la consola, la aplicaci√≥n se cierra. Al menos esto es lo que sucede en un servidor con Ubuntu. Para evitar esto y mantener la aplicaci√≥n ejecut√°ndose siempre, simplemente agr√©guela a pm2 con el simple comando de pm2 start index.js --name production . Pero eso no es todo. La herramienta permite el monitoreo ( pm2 monit ) y la agrupaci√≥n de aplicaciones.


Recordemos c√≥mo se organizan los procesos en PHP. Supongamos que tenemos nginx sirviendo solicitudes http y necesitamos pasar la solicitud a PHP. Puede hacer esto directamente y luego con cada solicitud se generar√° un nuevo proceso PHP, y cuando se complete, se eliminar√°. O puede usar un servidor fastcgi. Creo que todos saben lo que es y no hay necesidad de entrar en detalles, pero por si acaso, aclarar√© que PHP-FPM se usa con mayor frecuencia como fastcgi y su tarea es generar muchos procesos PHP que est√°n listos para aceptar y procesar una nueva solicitud en cualquier momento. ¬ŅCu√°l es la desventaja de este enfoque?


La primera es que nunca se sabe cu√°nta memoria consumir√° su aplicaci√≥n. En segundo lugar, siempre estar√° limitado en el n√ļmero m√°ximo de procesos y, en consecuencia, con un salto brusco en el tr√°fico, su aplicaci√≥n PHP usar√° toda la memoria disponible y se bloquear√°, o descansar√° contra el l√≠mite permitido de procesos y comenzar√° a eliminar los viejos. Esto se puede evitar estableciendo No recuerdo qu√© par√°metro del archivo de configuraci√≥n PHP-FPM es din√°mico y luego se generar√°n tantos procesos como sea necesario en este momento. Pero de nuevo, un ataque DDoS elemental consumir√° toda la RAM y pondr√° su servidor. O, por ejemplo, un script de error consumir√° toda la RAM y el servidor se congelar√° por alg√ļn tiempo (hubo precedentes en el proceso de desarrollo).


La diferencia fundamental en NodeJS es que la aplicaci√≥n no puede consumir m√°s de 1,5 gigabytes de RAM. No hay restricciones de proceso; solo hay un l√≠mite de memoria. Esto lo alienta a escribir programas lo m√°s livianos posible. Adem√°s, es muy simple calcular la cantidad de cl√ļsteres que podemos permitirnos, dependiendo del recurso de CPU disponible. Se recomienda que no se cuelgue m√°s de un cl√ļster en cada n√ļcleo (exactamente como en nginx, no m√°s de un trabajador por n√ļcleo de CPU).


Agrupación en PM2


Una ventaja de este enfoque es que PM2 recarga todos los cl√ļsteres a su vez. Volviendo al p√°rrafo anterior, que hablaba del tiempo de inactividad de 1-2 segundos durante el reinicio. En el modo de cl√ļster, cuando reinicia el servidor, su aplicaci√≥n no experimentar√° un milisegundo de tiempo de inactividad.


NodeJS es un buen cuchillo suizo


Ahora existe tal situaci√≥n cuando PHP act√ļa como un lenguaje para escribir sitios, y Python act√ļa como una herramienta para rastrear estos sitios. NodeJS es 2 en 1, por un lado es un tenedor, por el otro es una cuchara. Puede escribir aplicaciones y rastreadores web r√°pidos y potentes en el mismo servidor dentro de la misma aplicaci√≥n. Suena tentador Pero, ¬Ņc√≥mo se puede realizar esto, preguntas? Google mismo lanz√≥ la API oficial de Chromium: Puppeteer. Puede iniciar Headless Chrome (un navegador sin interfaz de usuario, Chrome "sin cabeza") y obtener el mayor acceso posible a la API del navegador para rastrear p√°ginas. La forma m√°s sencilla y accesible de trabajar con Puppeteer .


Por ejemplo, en nuestro grupo VKontakte hay una publicaci√≥n regular de descuentos y ofertas especiales a varios destinos desde las ciudades de la CEI. Generamos im√°genes para publicaciones en modo autom√°tico, y para que sean hermosas, necesitamos bellas im√°genes. No me gusta vincularme a varias API y crear cuentas en docenas de sitios, por lo que escrib√≠ una aplicaci√≥n simple que imita a un usuario com√ļn con el navegador Google Chrome que recorre el sitio con im√°genes de archivo y recoge al azar la imagen encontrada por la palabra clave. Sol√≠a ‚Äč‚Äčusar Python y BeautifulSoup para esto, pero ahora esto ya no es necesario. Y la caracter√≠stica principal y la ventaja de Puppeteer es que puede enga√Īar f√°cilmente incluso a los sitios de SPA, porque tiene a su disposici√≥n un navegador completo que comprende y ejecuta el c√≥digo JavaScript en los sitios. Es dolorosamente simple:


 const browser = await puppeteer.launch({headless: true, args:['--no-sandbox']}) const page = (await browser.pages())[0] await page.goto(`https://pixabay.com/photos/search/${imageKeyword}/?cat=buildings&orientation=horizontal`, { waitUntil: 'networkidle0' }) 

Entonces, en 3 líneas de código, lanzamos el navegador y abrimos la página del sitio con imágenes de archivo. Ahora podemos seleccionar un bloque aleatorio con la imagen en la página y agregarle una clase, en la que luego podemos acceder de la misma manera e ir directamente a la página con la imagen para cargar más:


 var imagesLength = await page.evaluate(() => { var photos = document.querySelectorAll('.search_results > .item') if(photos.length > 0) { photos[Math.floor(Math.random() * photos.length)].className += ' --anomaly_selected' } return photos.length }) 

Recuerde cu√°nto c√≥digo se necesitar√≠a para escribir esto en PhantomJS (que, por cierto, cerr√≥ y entr√≥ en estrecha colaboraci√≥n con el equipo de desarrollo de Puppeteer). ¬ŅPuede una herramienta tan maravillosa evitar que alguien cambie a NodeJS?


NodeJS proporciona asincronía fundamental


Esto puede considerarse una gran ventaja de NodeJS y JavaScript, especialmente con el advenimiento de async / wait en ES2017. A diferencia de PHP, donde cualquier llamada se realiza sincr√≥nicamente. Dar√© un ejemplo simple. Anteriormente, en el motor de b√ļsqueda, las p√°ginas se generaban en el servidor, pero algo ten√≠a que mostrarse en la p√°gina que ya estaba en el cliente usando JavaScript, pero en ese momento Yandex a√ļn no pod√≠a usar JavaScript en los sitios web y tuvo que implementar un mecanismo de instant√°nea (instant√°neas de p√°gina) espec√≠ficamente para √©l. usando Prerender. Las instant√°neas se almacenaron en nuestro servidor y se enviaron al robot a pedido. El dilema era que estas im√°genes se generaban en 3-5 segundos, lo cual es completamente inaceptable y puede afectar la clasificaci√≥n del sitio en los resultados de b√ļsqueda. Para resolver este problema, se invent√≥ un algoritmo simple: cuando el robot solicita alguna p√°gina, una instant√°nea de la que ya tenemos, simplemente le damos la instant√°nea existente, despu√©s de lo cual realizamos la operaci√≥n para crear una nueva instant√°nea en segundo plano y reemplazarla. Ya disponible. C√≥mo se hizo en PHP:


 exec('/usr/bin/php ' . __DIR__ . '/snapshot.php -a ' . $affiliation_type . ' -l ' . urlencode($full_uri) . ' > /dev/null 2>/dev/null &'); 

Nunca hagas eso.
En NodeJS, esto se puede lograr llamando a la función asincrónica:


 async function saveSnapshot() { getSnapshot().then((res) => { db.saveSnapshot().then((status) => { if(status.err) console.error(err) }) }) } /** *     await * ..    resolve()   */ saveSnapshot() 

En resumen, no está tratando de evitar el sincronismo, pero decide cuándo usar la ejecución de código síncrono y cuándo usarlo de forma asíncrona. Y es realmente conveniente. Especialmente cuando conoces las posibilidades de Promise.all ()


El motor de b√ļsqueda de vuelos en s√≠ est√° dise√Īado de tal manera que env√≠a una solicitud a un segundo servidor que recopila y agrega datos, y luego recurre a √©l para obtener datos listos para emitir. Las p√°ginas de direcci√≥n se utilizan para atraer tr√°fico org√°nico.


Por ejemplo, para la consulta "Vuelos Mosc√ļ San Petersburgo" se emitir√° una p√°gina con la direcci√≥n / tickets / moscow / saint-petersburg / , y necesita datos:


  1. Precios de las aerolíneas en esta dirección para el mes actual
  2. Precios de las aerol√≠neas en esta direcci√≥n para el pr√≥ximo a√Īo (precio promedio de cada mes durante los pr√≥ximos 12 meses)
  3. Programe vuelos en esta dirección
  4. Destinos populares desde la ciudad de despacho - desde Mosc√ļ (para vincular)
  5. Los destinos populares de la ciudad de llegada son desde San Petersburgo (para vincular)

En PHP, todas estas solicitudes se ejecutaron sincrónicamente, una tras otra. El tiempo promedio de respuesta de la API por solicitud es de 150-200 milisegundos. Multiplicamos 200 por 5 y obtenemos, en promedio, un segundo solo para cumplir con las solicitudes al servidor con datos. NodeJS tiene una gran función llamada Promise.all , que ejecuta todas las solicitudes en paralelo, pero escribe el resultado uno por uno. Por ejemplo, el código de ejecución para las cinco solicitudes anteriores se vería así:


 var [montlyPrices, yearlyPrices, flightsSchedule, originPopulars, destPopulars] = await Promise.all([ getMontlyPrices(), getYearlyPrices(), getFlightSchedule(), getOriginPopulars(), getDestPopulars() ]) 

Y obtenemos todos los datos en 200-300 milisegundos, reduciendo el tiempo de generación de datos para la página de 1-1.5 segundos a ~ 500 milisegundos.


Conclusión


El cambio de PHP a NodeJS me ayud√≥ a familiarizarme con JavaScript asincr√≥nico, aprender a trabajar con promesas y async / wait. Despu√©s de que el motor fue reescrito, la velocidad de carga de la p√°gina se optimiz√≥ y difiri√≥ dr√°sticamente de los resultados que mostr√≥ el motor en PHP. En este art√≠culo, tambi√©n podr√≠amos hablar sobre c√≥mo se usan los m√≥dulos simples para trabajar con el cach√© (Redis) y pg-promise (PostgreSQL) en NodeJS y compararlos con Memcached y php-pgsql, pero este art√≠culo result√≥ ser bastante voluminoso. Y conociendo mi "talento" para escribir, tambi√©n result√≥ estar mal estructurada. El prop√≥sito de este art√≠culo es atraer la atenci√≥n de los desarrolladores que todav√≠a est√°n trabajando con PHP y no est√°n al tanto de las delicias de NodeJS y el desarrollo de aplicaciones basadas en la web utilizando un ejemplo de un proyecto de la vida real que alguna vez se escribi√≥ en PHP, pero debido a las preferencias su due√Īo fue a otra plataforma.


Espero haber podido transmitir mis pensamientos y más o menos estructurado para expresarlos en este material. Al menos lo intenté :)


Escriba cualquier comentario, amigable o enojado. Contestaré a cualquier constructivo.

Source: https://habr.com/ru/post/444400/


All Articles