Enumerable: cómo generar un valor comercial

Este artículo es una breve explicación acerca de cómo el uso de palabras clave de un lenguaje común puede influir en el presupuesto de infraestructura de TI de un proyecto o ayudar a lograr algunas limitaciones / restricciones de la infraestructura de alojamiento y, además, será una buena señal de la calidad y madurez del código fuente.

Para la demostración de ideas, en el artículo se utilizará el lenguaje C #, pero la mayoría de las ideas se pueden traducir a otros idiomas.

Desde el conjunto de características del lenguaje, desde mi punto de vista, 'rendimiento' es la palabra clave más infravalorada. Puede leer la documentación y encontrar una gran cantidad de ejemplos en Internet. Para ser cortos, digamos que 'rendimiento' permite crear 'iteradores' implícitamente. Por diseño, un iterador debe exponer una fuente IEnumerable para uso 'público'. Y aquí comienza lo complicado. Porque tenemos muchas implementaciones de IEnumerable en el lenguaje: lista, diccionario, hashset, cola y etc. Y desde mi experiencia, la elección de uno de ellos para satisfacer los requisitos de alguna tarea comercial es incorrecta. Además, todo esto se ve agravado por cualquier implementación que se elija, el programa 'simplemente funciona': esto es lo que realmente necesita para los negocios, ¿no? Comúnmente funciona, pero solo hasta que el servicio se implemente en un entorno de producción.

Para una demostración del problema, sugiero elegir un caso / flujo de negocios muy común para la mayoría de los proyectos empresariales que podemos extender durante el artículo y sustituir alguna parte de este flujo para comprender una escala de influencia de este enfoque en los proyectos empresariales. Y debería ayudarlo a encontrar su propio caso en este conjunto para solucionarlo.

Ejemplo de la tarea:

  1. Cargue en línea un conjunto de registros de un archivo o base de datos en la memoria.
  2. Para cada columna del registro, cambie el valor a otro valor.
  3. Guarde los resultados de la transformación en un archivo o base de datos.

Asumamos varios casos donde esta lógica puede ser aplicable. En este momento, veo dos casos:

  1. Tal vez sea parte del flujo de alguna aplicación ETL de consola.
  2. Es quizás una lógica dentro de la acción en la aplicación Controlador de MVC.

Si parafraseamos la tarea de una manera más técnica, puede sonar así: "(1) Asignar una cantidad de memoria, (2) cargar información en la memoria desde el almacenamiento de persistencia, (3) modificar y (4) eliminar registros cambios en la memoria para el almacenamiento de persistencia ". Aquí la primera frase en la descripción "(1) Asignar una cantidad de memoria" puede tener una correlación real con sus requisitos no funcionales. Debido a que su trabajo / servicio debe 'vivir' en algún entorno de alojamiento que puede tener algunas limitaciones / restricciones (por ejemplo, 150Mb por microservicio) y para predecir los gastos en su servicio en el presupuesto, debemos predecir, en nuestro caso, la cantidad de memoria qué servicio usará (comúnmente decimos sobre las cantidades máximas de memoria). En otras palabras, debemos determinar una "huella" de memoria para su servicio.

Consideremos una huella de memoria para una implementación realmente común que observo de vez en cuando en diferentes bases de código de proyectos empresariales. Además, también puede intentar encontrarlo en sus proyectos, por ejemplo, 'bajo el capó' de la implementación del patrón 'repositorio', solo intente encontrar esas palabras: 'ToList', 'ToArray', 'ToReadonlyCollection' y etc. Toda esta implementación significa que:

1. Para cada línea / registro en el archivo / db, asigna memoria para mantener las propiedades del registro del archivo / db (es decir, var user = new User () {FirstName = 'Test', LastName = 'Test2'})

2. Luego, con la ayuda de, por ejemplo, 'ToArray' o manualmente, las referencias del objeto se mantienen en alguna colección (es decir, var users = new List (); users.Add (user)). Por lo tanto, se le asigna cierta cantidad de memoria para cada registro de un archivo y, para no olvidarlo, la referencia se almacena en alguna colección.

Aquí hay un ejemplo:

private static IEnumerable<User> LoadUsers2() { var list = new List<User>(); foreach(var line in File.ReadLines("text.txt")) { var splittedLine = line.Split(';'); list.Add(new User() { FirstName = splittedLine[0], LastName = splittedLine[1] }); } return list; // or return File.ReadLines("text.txt") .Select(line => line.Split(';')) .Select(splittedLine => new User() { FirstName = splittedLine[0], LastName = splittedLine[1] }).ToArray(); } 

Resultados del generador de perfiles de memoria:

imagen

Exactamente esa imagen que vi cada vez en un entorno de producción antes de que el contenedor se detenga / recargue debido a la limitación de recursos del hosting por contenedor.

Entonces, una huella para este caso, aproximadamente, depende de la cantidad de registros en un archivo. Porque la memoria se asigna por registro en el archivo. Y, la suma de estas pequeñas cantidades de memoria nos da una cantidad máxima de memoria que nuestro servicio puede consumir: es la huella del servicio. ¿Pero es esta huella predecible? Aparentemente no. Porque no podemos predecir una cantidad de registros en el archivo. Y, en la mayoría de los casos, el tamaño del archivo excede la cantidad de memoria permitida en el alojamiento en varias ocasiones. Significa que es difícil usar dicha implementación en el entorno de producción.

Parece que es el momento de repensar tal implementación. La siguiente suposición puede darnos más oportunidades para calcular una huella para el servicio: "una huella debe depender del tamaño de UN solo registro en el archivo". Aproximadamente, en este caso, podemos calcular el tamaño máximo de cada columna de un solo registro y sumarlos. Es bastante fácil predecir el tamaño de un registro en lugar de predecir el número de registros en el archivo.

Y realmente se pregunta si podemos implementar un servicio que pueda manejar una cantidad impredecible de registros y que constantemente consuma solo un par de megabytes con ayuda de solo una palabra clave: 'rendimiento' *.

El tiempo para un ejemplo:

 class Program { static void Main(string[] args) { // 1. Load byline a set of records from a file or DB into memory. var users = LoadUsers(); // 2. For each column of the record change the value to someone other value. users = ModifyFirstName(users); // 3. Save the results of transformation into a file or DB. SaveUsers(users); } private static IEnumerable<User> LoadUsers() { foreach(var line in File.ReadLines("text.txt")) { var splitedLine = line.Split(';'); yield return new User() { FirstName = splitedLine[0], LastName = splitedLine[1] }; } } private static IEnumerable<User> ModifyFirstName(IEnumerable<User> users) { foreach (var user in users) { user.FirstName += "_1"; yield return user; } } private static void SaveUsers(IEnumerable<User> users) { foreach(var user in users) { File.AppendAllLines("results.txt", new string []{ user.FirstName + ';' + user.LastName }); } } private class User { public string FirstName { get; set; } public string LastName { get; set; } } } 

Como puede ver en el ejemplo anterior, solo se asigna memoria para un objeto a la vez: 'return return new User ()' en lugar de crear una colección y llenarla con objetos. Es el principal punto de optimización que nos permite calcular una huella de memoria más predecible para el servicio. Porque solo necesitamos saber el tamaño de dos campos, en nuestro caso Nombre y Apellido. Cuando un usuario modificado se guarda en un archivo (consulte File.AppendAllLines), la instancia del objeto de usuario está disponible para la recolección de elementos no utilizados. Y la memoria que está ocupada por el objeto se desasigna (es decir, la próxima iteración de la declaración 'foreach' en LoadUsers), por lo que se puede crear la siguiente instancia del objeto de usuario. En otras palabras, aproximadamente, la misma cantidad de memoria reemplaza por la misma cantidad de memoria en cada iteración. Es por eso que no necesitamos más memoria que el tamaño de un solo registro en el archivo.

Resultados del generador de perfiles de memoria después de la optimización:

imagen

Desde otra perspectiva, si cambiamos ligeramente el nombre de un par de métodos en la implementación anterior, de modo que el uso pueda notar alguna lógica significativa para los Controladores en la aplicación MVC:

 private static void GetUsersAction() { // 1. Load byline a set of records from a file or DB into memory. var users = LoadUsers(); // 2. For each column of the record change the value to someone other value. var usersDTOs = MapToDTO(users); // 3. Save the results of transformation into a file or DB. OkResult(usersDTOs); } 

Una nota importante antes del listado de códigos: la mayoría de las bibliotecas importantes como EntityFramework, ASP.net MVC, AutoMapper, Dapper, NHibernate, ADO.net y etc. exponen / consumen fuentes IEnumerables. Entonces, significa en el ejemplo anterior que LoadUsers puede ser reemplazado por una implementación que utiliza EntityFramework, por ejemplo. Que carga datos fila por fila de la tabla DB, en lugar de un archivo. MapToDTO puede ser reemplazado por Automapper y OkResult puede ser reemplazado por una implementación 'real' de IActionResult en algún marco MVC o nuestra propia base de implementación en el flujo de red, por ejemplo:

 private static void OkResult(IEnumerable<User> users) { // you can use a networksteam implementation using(StreamWriter sw = new StreamWriter("result.txt")) { foreach(var user in users) { sw.WriteLine(user.FirstName + ';' + user.LastName); } } } 

Este ejemplo 'similar a mvc' nos muestra que todavía podemos predecir y calcular una huella de memoria también para aplicaciones web. Pero en este caso, dependerá del recuento de solicitudes también. Por ejemplo, los requisitos no funcionales pueden sonar de esta manera: "Cantidad máxima de memoria para 1000 solicitudes, no más de: 200 KB por objeto de usuario x 1000 solicitudes ~ 200 MB".

Dichos cálculos son muy útiles para la optimización del rendimiento en caso de escalar la aplicación web. Por ejemplo, necesita escalar su aplicación web en 100 contenedores / VM. Entonces, en este caso, para tomar una decisión sobre la cantidad de recursos que debe asignar del proveedor de alojamiento, para que pueda ajustar la fórmula de esta manera: 200 KB por objeto de usuario x 1000 solicitudes x 100VMs ~ 20GB. Además, esta es la cantidad máxima de memoria y esta cantidad está bajo el control del presupuesto de su proyecto.

Espero que la información de este artículo sea útil y permita ahorrar mucho dinero y tiempo en sus proyectos.

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


All Articles