Buenas prácticas de desarrollo para el rendimiento de microservicios

En el ultimo desarrollo que hemos trabajado durante meses, se planteó una arquitectura de microservicios para crear un gestor de contenidos a medida. Separar la responsabilidad en pequeños servicios nos proporcionó ciertas ventajas respecto al legacy monolítico que heredábamos, ya que nos proporcionaba al equipo trabajar sobre ciertos aspectos independientes desacoplando cada pieza. Esto proporciona ventajas que ya conocemos de este tipo de arquitecturas, como la independencia o el despliegue independiente, sin embargo no debemos de olvidarnos del rendimiento.

Aún teniendo siempre una visión global, al comienzo del desarrollo pudimos paralelizar bien el trabajo de cada desarrollador, avanzando con gran velocidad con cada uno de los servicios. Sin embargo, nos dimos cuenta que hay que tener la madurez suficiente como equipo para orquestar todos los servicios para servir la funcionalidad necesaria a cada tercero. Aunque este tipo de arquitecturas proporciona ciertas ventajas, hay que tener en mente que hay otros aspectos que pueden penalizar como pueden ser el rendimiento.

El sistema que construimos no solo iba a servir contenido a un website, si no que iba a ser fuente de consumo de muchos otros servicios. Otro día hablaremos de la arquitectura completa, pero el objetivo de este artículo es proporcionar algunos consejos de rendimiento a la hora de conectar los microservicios a través de los aprendizajes que fuimos adquiriendo.

Utilizar el modelo CRUD en cada servicio

Respecto al performance, en nuestro caso abrimos dos versiones de CRUD por cada entidad. Una versión privada con mayor serializado de información y tratamiento de datos privados para ser accesibles por los distintos roles que accedían al backoffice del gestor de contenidos. Por otro lado, construimos unos endpoints de lectura públicos mucho más ligeros con la información pública necesaria. Estos endpoints estaban optimizados para las lecturas y con una pequeña capa de cache HTTP

Proveer batch APIs

En algunos casos utilizamos esta técnica para premiar el performance, pero en otros casos permitimos esa consulta por array de ids pudiendo recuperar en una sola llamada todos los avatares y asignándolos en el servicio de pegamento a cada usuario. Esto justamente es lo que hace React Admin por defecto en sus listados, y como utilizamos esta herramienta para la construcción del backoffice dotamos a los GET de esa funcionalidad. Por ejemplo, teníamos un servicio que gestionaba todos los assets del sistema por lo que toda entidad que debiera de tener un asset en sus listados debiera de llamar de la siguiente forma:

Request:

GET /api/admin/v1/images?filter={“id”:[381168,381123,381055,381026,381023,380298,381126,381040,381041]}

Response

[{
“id”: 381168,
“name”: “Image Name”,
“caption”: “Image Caption”,
“slug”: “image-slug”,
“watermakedUrl”: null,
“watermarked”: false,
“resizes”: [{
“url”: “https://mydomain.com/xsmall.png",
“type”: “xsmall”,
“createdAt”: “2019–07–15T10:20:04+00:00”,
“updatedAt”: “2019–07–15T10:20:04+00:00”
}, {
“url”: “https://mydomain.com/small.png",
“type”: “small”,
“createdAt”: “2019–07–15T10:20:04+00:00”,
“updatedAt”: “2019–07–15T10:20:04+00:00”
}, {
“url”: “https://mydomain.com/medium.png",
“type”: “medium”,
“createdAt”: “2019–07–15T10:20:04+00:00”,
“updatedAt”: “2019–07–15T10:20:04+00:00”
}, {
“url”: “https://mydomain.com/large.png",
“type”: “large”,
“createdAt”: “2019–07–15T10:20:04+00:00”,
“updatedAt”: “2019–07–15T10:20:04+00:00”
}, {
“url”: “https://mydomain.com/xlarge.png",
“type”: “xlarge”,
“createdAt”: “2019–07–15T10:20:05+00:00”,
“updatedAt”: “2019–07–15T10:20:05+00:00”
}],
“asset”: {
“id”: 397231,
“headers”: {
“ContentType”: “image\/png”
},
“fileDir”: “https://mydomain.com/original.png",
“filename”: “filename.png”,
“creator”: null,
“createdAt”: “2019–07–15T10:18:41+00:00”,
“updatedAt”: “2019–07–15T10:18:41+00:00”,
“trash”: false
},
“metadatas”: [],
“createdAt”: “2019–07–15T10:19:14+00:00”,
“updatedAt”: “2019–07–15T10:19:14+00:00”,
“tags”: [“tag1”, “tag2”, “tag3”],
“trash”: false,
“publishedAt”: null
}]

Utiliza el modelo asíncrono de peticiones

En nuestro caso, además de paralelizar las peticiones, nos apoyamos en una capa sobre Redis para cachear esas responses evitando la llamada, en nuestro caso por comunicación HTTP. Para paralelizar las llamadas nos apoyamos en el modelo de promises que proporciona Guzzle. Aunque es importante decir aquí que el cliente httpque ha construido Symfony ya proporciona de este mecanismo (Gist) y recomiendo echarle un vistazo.

``

/**

* @return array

*/

private function executeConcurrentRequests(array $requests)

{

$client = new Client();

return Pool::batch($client, $requests, ['concurrency' => count($requests]);

}

Usar la ruta mas corta

Si trabajamos con un proveedor de Cloud, también es muy importante tener en cuenta los datacenter donde se despliegan cada pieza para que tampoco tengamos una gran latencia de red por la ubicación. Aunque tu desarrollo funcione correctamente en una infraestructura de desarrollo, debe de probarse cuanto antes en la infraestructura real para poder detectar este tipo de problemas.

Simplificar el modelo de seguridad

Debuggear las llamadas http

Conclusión

Full Stack Web Developer — adrianalonso.es

Full Stack Web Developer — adrianalonso.es