Entitats amb fitxers: tot junt o per parts?

Hi ha dues propostes per a pujar (actualitzar-les també) unes entitats que porten vinculades a la imatge:

  • [POST] /api/v1/imatge amb un cos html serialitzat tipus { desciption: “”, tags: [“”, “”,””] }
  • [POST] /api/v1/image/{id}/upload amb un multipart/form-www image=@adreçaImatge

Amb:

  • [POST] /api/v1/imatge amb un multipart/form-data description=”” ; tags=[“”,””,””]; image=@adreçaImatge

I aquí és on premia qui ha de fer ús de la API, quina utilització se li dona. El fet de treballar amb entitats sempre és com allò de que per a un martell tot són claus. Hauríem de separar:

  • Amb quina recurrència es pujaran imatges o serà un fet únic?
  • Les aplicacions clients volen crear l’entitat amb la imatge?
  • Les aplicacions clients com processen els multipart/form-data?

De les tres preguntes, les dues primeres són de disseny i la tercera d’arquitectura. Per allò del que els backends poden viure a esquenes de la realitat si ofereixen unes portes definides. Són importants perquè poden encorsetar bastant el processament de les dades (no és el mateix un form què un body serialitzat).

Amb NodeJS tenim multer què a mode intermediari (middleware) ens pot emmagatzemar la imatge en un directori, i posar-nos les dades del form-data al body com a propietats. En el següent exemple es fa servir la segona opció:

routes.js

  app.post("/v1/imatge", imatgeController.uploadImg,  imatgeController.save);

imatgeController.js

const multer = require('multer');

const fs = require('fs');

const imageStorage = multer.diskStorage({
  destination: function (req, file, cb) {

    if (!fs.existsSync('./imageUploads')) {
      fs.mkdirSync('./imageUploads');
    }

    cb(null, './imageUploads');
  },
  filename: function (req, file, cb) {
    cb(null, file.originalname);
  }
});

exports.uploadImg = multer({ storage: imageStorage }).single('image');
exports.save = async (req, res) => {

var entity = { description: req.body.description, image: req.file.filename, tags: req.body.tags };
...
}

El fet de que podem sobrecarregar amb diverses funcions les rutes també ens facilitarà altres tipus de processaments (autenticació, per exemple), però això ja és cosa més de NodeJS i ExpressJS.

La solució ens sobrecarrega bastant el què és la entitat: la carrega de imatge per un costat, què pot tenir més lògica (compressió, tractament, desplaçament a un lloc remot) i pot fer més pesat del que és en si el pujar unes dades per a una entitat.

Per a fer un punt comú entre client i servidor, el codi swagger.json descriurà els camps com a formData parant especial atenció en el type:

"/v1/imatges": {
      "post": {
        "tags": [
          "imatges"
        ],
        "summary": "Afegir una imatge",
        "description": "Ruta per afegir una nova imatge",
        "consumes": [
          "image"
        ],
        "parameters": [
          {
            "name": "imatge",
            "in": "formData",
            "description": "afegir nova imatge",
            "required": "true",
            "type": "file"
          },
          {
            "name": "descripcio",
            "in": "formData",
            "description": "descripció de la imatge",
            "required": "true",
            "type": "string"
          }

        ], 
...

Per al client, fem l’exemple de un swagger tipificat i l’utilitzem en un client, en aquest cas, NSwag amb destí C#. El resultat és una funció utilitzable per aquest client:

        /// <summary>Create a banner</summary>
        /// <param name="image">Adding new publication.</param>
        /// <param name="name">Banner name</param>
        /// <param name="isDisabled">Is disabled?</param>
        /// <param name="analyticCode">Analytic Code</param>
        /// <exception cref="SwaggerException">A server side error occurred.</exception>
        public System.Threading.Tasks.Task Banners4Async(FileParameter image, string name, bool isDisabled, string analyticCode)
        {
            return Banners4Async(image, name, isDisabled, analyticCode, System.Threading.CancellationToken.None);
        }

Quina seria la diferència de fer-lo per separat? En un altre post…

Latència de una Azure Function

Revisió gener 2020: Aquesta entrada va ser escrita al 2018 en plena fase de desplegament. Actualment les Azure functions v2 i v3 ofereixen el always on què disminueix dràsticament la latència de les funcions. Les funcions escrites amb Net Framework només poden anar amb V1 que ara mateix està en camí a deprecated.

Des Azure fa un temps els webjobs de les WebApps d’Azure s’estan traslladant a funcions. Això el teu els seus avantatges, com no consumir recursos de les instàncies de WebApp i consumir-ne sota demanda.

Tot és correcte fins que les versions actuals de serveis no ofereixen quelcom bàsic: estar sempre actives. Això és vital qual la funció es dedica a llegir una cua: o pot estar desactiva sempre o pot estar osiosa esperant a respondre. Les Azure Functions al no tenir Always On són el primer cas: desactiva fins que es digui el contrari. Doncs es plantegen dos solucions: (A) fer un pas enrere i tornar a workers o (B) fer un mecanisme de mantenir sempre viu.

Imatge
La latència de les respostes del encadenat de processos és alt degut al temps de warm up de una Azure Function

La solució A implica desplegar una sèrie de llibreries que tenen ús crític de la CPU a una instància WebApp que ja de per si ja té pics de CPU. Descongestionar la instància de aquests pics és una prioritat.

La solució B implica refórmular el codi de lectura de cues per a que quan a la senyal de vida, una senyal que anomenarem keep_alive. Un altre funció Azure inserirà a la cua cada X (definirem el temps com una variable de configuració per a trobar el rendiment òptim) un missatge de keep_alive que els diferents processos encadenats en cues només hauran de retransmetre de la cua d’entrada a la de sortida.

És una forma de mantenir els processos de la cua ociosos, tampoc és una novetat. Són principis que vaig veure a clustering amb heartbeat/pacemaker o més simples, per evitar que el IIS decidís que era bon moment posar-se en repòs.

El problema és què pot saturar les cues (1), pot sobrecarregar els serveis (2) i pot implicar un consum extra (3). Així que caldrà trobar el equilibri entre mantenir una latència optima: una repetició de la senyal keep_alive que no desajusti el sistema i dispari els costos.

Imatge
En vermell: un keep alive cada 10 segons, en blau cada 20 segons, en verd cada 30.

Provem 3 períodes per a l’enviament de keep_alive: cada 30 segons, cada 20 segons i després cada 10. La Azure Function té aproximadament un període de 10 segons per a aixecar-se (warm up a actiu) i de repòs molt menor (al acabar ja passa a repòs).

El fet de mantenir-lo a 30 s’aconsegueix que les peticions intercalades entre les senyals de vida passin de pics de 120 a 60. Segueix sent un temps d’espera elevat per al client, que voldria resoldre les peticions en el menor temps possible.

Reduint el keep_alive a 20 segons reduïm els pics a 30 segons. La meitat de la meitat. I a 10 segons aconseguim que els pics es redueixin a menys de 10 segons.

El curiós de tot això és que són processos que a la versió de desktop mai vam aconseguir que arribessin a aquests números. Les Azure Functions amb una bona conjunció de SQL Server i optimització poden arribar a mostrar molt bons resultats.