Diff at the edge with serverless cloud functions
Hace poco estaba descargando paquetes con el [administrador de paquetes de npm](https://www.npmjs.com/) y me di cuenta de que, aunque a menudo tenga una versión anterior de un paquete ya instalada, npm tiene que descargar todo el tarball de la nueva versión para instalar la actualización de un módulo, lo cual no parece muy eficiente.
Solicitar la diferencia entre dos archivos almacenados en caché anteriormente —usando solo una configuración de CDN y una función de entorno informático en la nube sin servidores— es un ejemplo magnífico de aprovechamiento de servicios de edge computing e informática sin servidores para aumentar la eficiencia y el rendimiento de tu sitio web y reducir los costes de ancho de banda. En esta entrada, presentaré una solución que puede reducir drásticamente el consumo de ancho de banda para sitios que alojen activos descargables que tengan versiones, como software, documentos y juegos guardados.
![Diagrama 1 de diff en el edge](//images.contentful.com/6pk8mg3yh2ee/3vd8iGLYk8Okuki4I8Q6O6/6d16ae671a7c4979deeb8aa679f4b373/diff2.jpg)
Tomemos el servicio de código abierto que he creado, [Polyfill.io](https://polyfill.io/). Está publicado en forma de módulo npm, cuya última versión es de 11 MB comprimida con gzip, y 99 MB sin comprimir. Mediante [bsdiff](http://www.daemonology.net/bsdiff/), podemos generar un parche que resuma los cambios operados desde la penúltima versión a la última:
$ bsdiff polyfill_io-3.16.0.tar polyfill_io-3.17.0.tar polyfill_io-3.16.0...3.17.0.patch
$ ls -lah
total 424
drwxr-xr-x 5 me staff 170B 18 Apr 15:55 .
drwxr-xr-x 14 me staff 476B 18 Apr 16:32 ..
-rw-r--r-- 1 me staff 209K 18 Apr 17:27 polyfill_io-3.16.0...3.17.0.patch
-rw-r--r-- 1 me staff 99M 18 Apr 15:54 polyfill_io-3.16.0.tar
-rw-r--r-- 1 me staff 97M 18 Apr 15:53 polyfill_io-3.17.0.tar
Así, si el cliente ya tiene la versión 3\.16\.0, para actualizar a la 3\.17\.0 basta una descarga de solo 209 KB, **tan solo un 1,8 % de los 11 MB** \(comprimidos con gzip desde 99 MB\) que, de otro modo, necesitarías para el tarball completo.
Sin embargo, los servicios de alojamiento de módulos como npm suelen almacenar sus módulos en entornos de alojamiento estáticos como Amazon S3 o Google Cloud Storage. Por ello, la capacidad de agregar este tipo de función de generación de contenido dinámico es escasa o nula, y la generación previa de un diff entre cada par de versiones de cada módulo parece un uso poco eficiente de los recursos informáticos o de almacenamiento.
¿Se puede hacer esto a nivel de CDN?
------------------------------------
Por supuesto. Aquí mostramos cómo se podría hacer en la CDN de Fastly:
![Diagrama 2 de diff en el edge](//images.contentful.com/6pk8mg3yh2ee/TZiC5JSyisgSu0aS4kMQw/b61eac339b0a22f3d8155cf10cd0fcd0/diff1.jpg)
Para enrutar peticiones «diff» a un servicio de generación de parches, podría utilizarse una CDN que permita seleccionar los servicios de origen en función de las características de la petición. Con la CDN de Fastly, podemos hacerlo en VCL \([Varnish Configuration Language](https://docs.fastly.com/guides/vcl/), que hemos puesto a disposición de nuestros clientes\). En primer lugar, se define un backend específico:
backend be_diff_service {
.dynamic = true;
.port = "443";
.host = "<>";
.ssl_sni_hostname = "<>";
.ssl_cert_hostname = "<>";
.ssl = true;
.probe = {
.timeout = 10s;
.interval = 10s;
.request = "GET /healthcheck HTTP/1.1" "Host: <>" "Connection: close" "User-Agent: Fastly healthcheck";
}
}
En segundo lugar, ideamos una sintaxis especial que utilizar con las peticiones de parches y añadimos un pequeño elemento a `vcl_recv` que detecte esta sintaxis y enrute la solicitud al backend específico:
sub vcl_recv {
....
declare local var.diffUrlPrefix STRING;
declare local var.diffUrlSuffix STRING;
if (req.url ~ "^(/.*\/\-\/.*)\-(\d+\.\d+\.\d+)...(\d+\.\d+\.\d+)(\.tgz)\.patch") {
set var.diffUrlPrefix = if (req.http.Fastly-SSL, "https://", "http://") req.http.Host ".global.prod.fastly.net" re.group.1 "-";
set var.diffUrlSuffix = re.group.4;
set req.backend = be_diff_service;
set req.http.Host = "<>";
set req.http.Backend-Name = "diff";
set req.url = "/compareURLs?from=" var.diffUrlPrefix re.group.2 var.diffUrlSuffix "&to=" var.diffUrlPrefix re.group.3 var.diffUrlSuffix;
}
....
}
Las descargas de npm utilizan URL como `/module-name/-/module-name-1.2.3.tgz`, así que vamos a admitir también `/module-name/-/module-name-1.2.3...1.2.4.tgz.patch` como petición diff. La expresión regular contenida en el código VCL anterior captura las peticiones que entran en esta categoría y, a continuación, hace lo siguiente:
1. cambia el backend de modo que señale al servicio de diffs;
2. actualiza el encabezado `Host` de modo que enviemos el dominio del origen correcto en la petición destinada al servicio;
3. vuelve a escribir la ruta de modo que coincida con la sintaxis del servicio generador de diffs.
*Para obtener más información sobre cómo empezar a ejecutar tu propio código VCL en la plataforma de edge cloud de Fastly, consulta nuestra [guía introductoria a VCL](https://docs.fastly.com/guides/vcl/guide-to-vcl).*
Hasta aquí todo muy bien, pero los nodos de caché de CDN no pueden generar parches de diferencias por sí solos. Este es, precisamente, un magnífico ejemplo de uso de servicios informáticos sin servidores como AWS Lambda o Google Cloud Functions \(GCF\). Para llevar a cabo esta tarea, vamos a utilizar una función de Google Cloud Functions.
Si quieres usar GCF y no lo tienes configurado aún, Google tiene una [excelente guía de inicio rápido](https://cloud.google.com/functions/docs/quickstart) que te ayudará a ponerlo en marcha.
La fuente de la función en la nube que necesitamos tiene este aspecto:
const url = require('url');
const zlib = require('zlib');
const fetch = require('node-fetch');
const bsdiff = require('node-bsdiff').diff;
exports.compareURLs = function compareURLs (req, res) {
Promise.resolve()
.then(() => {
return Promise.all(['from', 'to'].map(param => {
return fetch(req.query[param])
.then(resp => {
const name = url.parse(req.query[param]).pathname.replace(/^.*\/([^\/]+)\/?$/, '$1');
const isCompressed = Boolean(resp.headers.get('Content-Encoding') === 'gzip' || name.match(/\.(tgz|gz|gzip)$/));
const respStream = isCompressed ? resp.body.pipe(zlib.createGunzip()) : resp.body;
const bufs = [];
respStream.on('data', data => bufs.push(data));
return new Promise(resolve => {
respStream.on('finish', () => {
resolve(Buffer.concat(bufs));
});
});
})
;
}))
})
// Create patch and serve it
.then(([from, to]) => {
const patch = bsdiff(from, to);
res.status(200);
res.send(patch);
})
;
};
Voy a utilizar dos módulos públicos de npm: [node\-fetch](https://www.npmjs.com/package/node-fetch), que implementa la API de Fetch WHATWG \(que ya es estándar\) en NodeJS \(que en el momento en que escribo esto no es compatible de forma nativa con Node\), y [node\-bsdiff](https://www.npmjs.com/package/node-bsdiff), que implementa el increíble [algoritmo de diffs binarios](http://www.daemonology.net/bsdiff/) inventado por [Colin Percival](https://twitter.com/cperciva).
Este código no incluye el control ni la validación de errores. También podemos mejorar la respuesta del parche agregando información adecuada de `Cache-Control` \(el parche se puede almacenar en caché durante el mismo tiempo que el archivo que menos tiempo pueda almacenarse en caché de los dos que se están comparando\) y pasando por los encabezados [surrogate\-key](https://docs.fastly.com/guides/purging/getting-started-with-surrogate-keys) que estén presentes en los archivos de entrada. [He cargado una solución integral en GitHub](https://github.com/fastly/diff-service) que incluye comentarios; podéis utilizarla libremente.
Realización de pruebas
----------------------
Para probar el nuevo punto de conexión, he inventado `differentnpm.com`, nuevo nombre de dominio ficticio destinado al registro npm para el que podría crear un servicio Fastly, y lo he configurado de modo que el registro npm real sea su servidor de origen. Con una petición de descarga del tarball completo de lodash 4\.17\.4, que es uno de los módulos más populares de npm, vemos que el nuevo servicio se comporta como el registro npm:
```shell
$ curl "http://differentnpm.com.global.prod.fastly.net/lodash/-/lodash-4.17.4.tgz" -vs 1>/dev/null
< HTTP/1.1 200 OK
< Cache-Control: max-age=21600
< Content-Type: application/octet-stream
< Content-Length: 310669
< X-Served-By: cache-sjc3143-SJC, cache-sjc3628-SJC
< X-Cache: HIT, HIT
```
Esta petición se enruta al registro real de npm y da como resultado un archivo de 310 KB \(consulta el encabezado Content\-Lenght\). Como cabía esperar, esto es un acierto de caché porque es un archivo popular, por lo que es probable que esté disponible en el nodo de caché de CDN local.
Sin embargo, este nuevo registro también admite con transparencia las nuevas URL de diferencias:
```shell
$ curl "http://differentnpm.com.global.prod.fastly.net/lodash/-/lodash-4.17.3...4.17.4.tgz.patch" -vs 1>/dev/null
< HTTP/1.1 200 OK
< Cache-Control: max-age=21600
< content-type: application/octet-stream
< Content-Length: 1207
< Connection: keep-alive
< X-Served-By: cache-sjc3132-SJC
< X-Cache: HIT
```
En este caso, la petición de la diferencia entre lodash 4\.17\.3 y 4\.17\.4 es un parche de **tan solo 1207 bytes, apenas el 0,3 % del tamaño original** .
Bsdiff se envía con una herramienta bspatch complementaria, que coge el archivo antiguo y el parche, y genera el nuevo:
$ ls -la
-rw-r--r-- 1 me staff 2254848 18 Apr 16:30 lodash-4.17.3.tar
-rw-r--r-- 1 me staff 1207 19 Apr 17:35 lodash-4.17.3...4.17.4.tgz.patch
$ bsdiff lodash-4.17.3.tar lodash-4.17.4.tar lodash-4.17.3...4.17.4.tgz.patch
$ tar tf lodash-4.17.4.tar
package/package.json
package/README.md
package/LICENSE
package/_baseToString.js
....
Ahorros
-------
Para lograr entender la enorme utilidad que podría tener este tipo de cosas, he hecho una lista de los [módulos con más dependencias de npm](https://www.npmjs.com/browse/depended) y, por cada uno, he recopilado los siguientes datos:
* Número de descargas durante un período de prueba \(he utilizado abril de 2017\)
* Tamaño de la versión más reciente del tarball
* Tamaño del diff entre la versión más reciente del tarball y la penúltima
Un dato que no podemos obtener a partir de la información pública es la frecuencia con la que los usuarios cuentan ya con una versión previa del archivo almacenada en la caché local. Vamos a analizar cómo afectaría si esta frecuencia fuera del 5 %, el 15 % y el 50 %:
| |||| Tamaño del parche || Ahorro mensual de datos, GB, por proporción de caché |||
| Módulo | Descargas \(en miles\) | Tamaño \(bytes\) | Transferencia mensual \(GB\) | Abs \(b\) | rel \(%\) | 5 % | 15 % | 50 % |
|------------------------------------|--------------------------|--------------------|--------------------------------|-------------|-------------|----------|----------|------------|
| lodash | 42 866 | 310 669 | 12 403 | 1207 | 0,39 % | 618 | 1853 | 6177 |
| request | 24 756 | 56 636 | 1306 | 3248 | 5,73 % | 62 | 185 | 615 |
| async | 43 923 | 97 968 | 4008 | 23 083 | 23,56 % | 153 | 459 | 1532 |
| express | 11 577 | 52 372 | 565 | 602 | 1,15 % | 28 | 84 | 279 |
| chalk | 21 045 | 5236 | 103 | 1027 | 19,61 % | 4 | 12 | 41 |
| bluebird | 14 327 | 135 089 | 1803 | 2669 | 1,98 % | 88 | 265 | 883 |
| underscore | 12 229 | 34 172 | 389 | 6879 | 20,13 % | 16 | 47 | 155 |
| commander | 26 118 | 13 425 | 327 | 1309 | 9,75 % | 15 | 44 | 147 |
| debug | 45 226 | 16 144 | 680 | 588 | 3,64 % | 33 | 98 | 328 |
| moment | 9219 | 497 477 | 4271 | 891 | 0,18 % | 213 | 640 | 2132 |
| Total \(10 módulos principales\) | **251 286** | | **25 853** | || **1229** | **3687** | **12 290** |
| |||| Ahorro relativo || 4,75 % | 14,26 % | 47,54 % |
Los tamaños de los diffs varían, como es obvio, y los módulos de npm más populares también tienden a ser bastante pequeños. Sin embargo, si algún porcentaje de las peticiones de módulos de npm pudiera ser de tipo diff, estos datos sugieren que se eliminaría prácticamente ese mismo porcentaje de consumo de su ancho de banda.
Otros casos de uso
------------------
Los administradores de paquetes no son el único tipo de negocio que podría beneficiarse de esto. Android utiliza diffs binarios para actualizar las aplicaciones de la tienda Google Play, y cualquier situación en la que tengas que enviar a un usuario una simple actualización de algo que ya tenga, los diffs pueden contribuir a optimizar el uso de tu ancho de banda.