Enumerável: como gerar um valor comercial

Este artigo é uma breve explicação sobre como o uso de palavras-chave em um idioma comum pode influenciar o orçamento da infraestrutura de TI de um projeto ou ajudar a alcançar algumas limitações / restrições da infraestrutura de hospedagem e, além disso, será um bom exemplo da qualidade e maturidade do código fonte.

Para a demonstração de idéias, o artigo usará a linguagem C #, mas a maioria das idéias poderá ser traduzida para outros idiomas.

Do conjunto de recursos do idioma, do meu ponto de vista, 'yield' é a palavra-chave mais subvalorizada. Você pode ler a documentação e encontrar muitos exemplos na Internet. Para ser breve, digamos que 'yield' permita a criação de 'iteradores' implicitamente. Por design, um iterador deve expor uma fonte IEnumerable para uso 'público'. E aqui o começo complicado. Porque temos muitas implementações de IEnumerable no idioma: lista, dicionário, hashset, fila e etc. E, pela minha experiência, a escolha de um deles para os requisitos de satisfação de alguma tarefa comercial está errada. Além disso, tudo isso é agravado por qualquer implementação escolhida, o programa 'simplesmente funciona' - é isso que realmente precisa para os negócios, não é? Geralmente, funciona, mas apenas até que o serviço seja implantado em um ambiente de produção.

Para uma demonstração do problema, sugiro escolher um caso / fluxo de negócios muito comum para a maioria dos projetos empresariais, que podemos estender durante o artigo e substituir uma parte desse fluxo por entender uma escala de influência dessa abordagem em projetos empresariais. E isso deve ajudá-lo a encontrar seu próprio caso neste conjunto para corrigi-lo.

Exemplo da tarefa:

  1. Carregue byline um conjunto de registros de um arquivo ou banco de dados na memória.
  2. Para cada coluna do registro, altere o valor para outro valor.
  3. Salve os resultados da transformação em um arquivo ou banco de dados.

Vamos assumir vários casos em que essa lógica pode ser aplicável. Neste momento, vejo dois casos:

  1. Talvez seja parte do fluxo para alguns aplicativos ETL de console.
  2. Talvez seja uma lógica dentro da ação no aplicativo Controller of MVC.

Se parafrasearmos a tarefa de uma maneira mais técnica, poderá parecer assim: "(1) aloque uma quantidade de memória, (2) carregue informações na memória do armazenamento persistente, (3) modifique e (4) libere registros alterações na memória para o armazenamento de persistência ". Aqui, a primeira frase da descrição "(1) alocar uma quantidade de memória" pode ter uma correlação real com seus requisitos não funcionais. Como seu trabalho / serviço deve 'viver' em algum ambiente de hospedagem que possa ter algumas limitações / restrições (por exemplo, 150 Mb por microsserviço) e prever gastos com seu serviço no orçamento, devemos prever, no nosso caso, quantidade de memória qual serviço usará (geralmente dizemos sobre quantidades máximas de memória). Em outras palavras, devemos determinar uma "pegada" de memória para o seu serviço.

Vamos considerar um espaço de memória para uma implementação realmente comum que observo de tempos em tempos em diferentes bases de código de projetos empresariais. Além disso, você também pode tentar encontrá-lo em seus projetos, por exemplo, 'por trás da implementação do padrão' repositório ', apenas tente encontrar as seguintes palavras:' ToList ',' ToArray ',' ToReadonlyCollection 'e etc. Toda essa implementação significa que:

1. Para cada linha / registro no arquivo / db, aloca memória para manter as propriedades do registro do arquivo / db (ou seja, var user = new User () {Nome = 'Teste', Sobrenome = 'Teste2'})

2. Em seguida, com a ajuda de, por exemplo, 'ToArray' ou manualmente, as referências do objeto são mantidas em alguma coleção (ou seja, var users = new List (); users.Add (user)). Portanto, é alocada uma certa quantidade de memória para cada registro de um arquivo e, para não esquecer, a referência é armazenada em alguma coleção.

Aqui está um exemplo:

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 do perfilador de memória:

imagem

Exatamente essa imagem que eu vi todas as vezes no ambiente de produção antes do contêiner parar / recarregar devido à limitação de recursos da hospedagem por contêiner.

Portanto, uma pegada para esse caso depende aproximadamente do número de registros em um arquivo. Porque a memória é alocada por registro no arquivo. E a soma dessas pequenas quantidades de memória nos fornece uma quantidade máxima de memória que pode ser consumida pelo nosso serviço - é a pegada do serviço. Mas essa pegada é previsível? Aparentemente não. Porque não podemos prever um número de registros no arquivo. E, na maioria dos casos, o tamanho do arquivo excede a quantidade de memória permitida na hospedagem várias vezes. Isso significa que é difícil usar essa implementação no ambiente de produção.

Parece que é o momento de repensar essa implementação. A próxima suposição pode nos dar mais oportunidades para calcular uma pegada para o serviço: "uma pegada deve depender do tamanho de apenas UM registro no arquivo". Aproximadamente, nesse caso, podemos calcular o tamanho máximo de cada coluna de apenas um registro e somar. É muito fácil prever o tamanho de um registro em vez da previsão do número de registros no arquivo.

E é realmente surpreendente que possamos implementar um serviço que possa lidar com uma quantidade imprevisível de registros e consuma constantemente apenas alguns megabytes com a ajuda de apenas uma palavra-chave - 'yield' *.

A hora de um exemplo:

 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 você pode ver no exemplo acima, só há memória alocada para um objeto de cada vez: 'yield return new User ()' em vez de criar uma coleção e a preenche com objetos. É o principal ponto de otimização que nos permite calcular uma pegada de memória mais previsível para o serviço. Porque precisamos apenas saber o tamanho de dois campos, no nosso caso, FirstName e LastName. Quando um usuário modificado é salvo no arquivo (consulte File.AppendAllLines), a instância do objeto de usuário fica disponível para coleta de lixo. E a memória que é ocupada pelo objeto é desalocada (isto é, a próxima iteração da instrução 'foreach' em LoadUsers), para que a próxima instância do objeto de usuário possa ser criada. Em outras palavras, aproximadamente, a mesma quantidade de memória substitui pela mesma quantidade de memória em cada iteração. É por isso que não precisamos de mais memória do que o tamanho de um único registro no arquivo.

Resultados do criador de perfil de memória após a otimização:

imagem

De outra perspectiva, se renomearmos levemente alguns métodos na implementação acima, para que o uso possa observar alguma lógica significativa para os Controladores no aplicativo 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); } 

Uma observação importante antes da listagem de códigos: a maioria das bibliotecas importantes, como EntityFramework, ASP.net MVC, AutoMapper, Dapper, NHibernate, ADO.net e etc, expõe / consome fontes IEnumerables. Portanto, significa no exemplo acima que LoadUsers pode ser substituído por uma implementação que usa EntityFramework, por exemplo. Que carrega dados linha por linha da tabela DB, em vez de um arquivo. MapToDTO pode ser substituído por Automapper e OkResult pode ser substituído por uma implementação 'real' de IActionResult em alguma estrutura MVC ou em nossa própria base de implementação no fluxo de rede, por exemplo:

 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 exemplo 'mvc-like' mostra-nos que ainda somos capazes de prever e calcular um espaço de memória também para aplicativos da Web. Mas, neste caso, dependerá também da contagem de solicitações. Por exemplo, os requisitos não funcionais podem soar da seguinte maneira: "Quantidade máxima de memória para 1000 solicitações não mais que: 200 KB por objeto de usuário x 1.000 solicitações ~ 200 MB".

Esses cálculos são muito úteis para otimização de desempenho no caso de dimensionar o aplicativo da web. Por exemplo, você precisa dimensionar seu aplicativo Web em 100 contêineres / VMs. Portanto, nesse caso, para tomar uma decisão sobre a quantidade de recursos que você deve alocar do provedor de hospedagem, você pode ajustar a fórmula da seguinte maneira: 200 KB por objeto de usuário x 1000 solicitações x 100VMs ~ 20 GB. Além disso, essa é a quantidade máxima de memória e está sob o controle do orçamento do seu projeto.

Espero que as informações deste artigo sejam úteis e permitam economizar muito dinheiro e tempo em seus projetos.

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


All Articles