Como acelerar o acesso aos aplicativos API de maneira rápida e fácil?

A resposta é simples: usar ferramentas comprovadas, como cache e dimensionamento horizontal. Devemos dizer imediatamente que essas não são as únicas ferramentas, mas na maioria das vezes são as abordagens clássicas comprovadas que se revelam mais eficazes, mesmo nas condições modernas. Vejamos um exemplo prático.



Sobre o problema original



A plataforma de vídeo PREMIER, como convém a um recurso moderno, criou um serviço de recomendação para seus clientes baseado em aprendizado de máquina. Muitos usuários recorrem à plataforma de vídeo - cerca de um milhão por dia, PREMIER é muito popular - e as ligações vêm tanto por meio da web, de aplicativos para dispositivos móveis quanto da Smart TV.



Os dados iniciais com base nos quais o aprendizado de máquina de nosso serviço funciona são armazenados no DBMS ClickHouse colunar. De acordo com o cronograma, os dados são processados ​​em segundo plano para construir modelos (que serão usados ​​para emitir recomendações finais). Os resultados dos cálculos são salvos no DBMS relacional PostgreSQL.



A solução para o problema de interação rápida entre o servidor de aplicação e o cliente em um curto espaço de tempo permanece sempre relevante. Para garantir a velocidade de trabalho necessária - e o tempo de resposta não deveria ser superior a 50 ms - tivemos que otimizar a estrutura do banco de dados relacional, implementar cache e escalonamento horizontal. Falaremos sobre algumas das técnicas agora.







Sobre cache



O cache é uma técnica de otimização comum. Criamos um armazenamento intermediário adicional, mais rápido que o principal, e colocamos os dados mais demandados nele. O acesso aos dados armazenados em cache é dramaticamente acelerado e isso aumenta significativamente a velocidade do aplicativo. Ao desenvolver um aplicativo de alta carga, o armazenamento em cache é a opção mais comum para otimizar o desempenho do aplicativo sem expandir os recursos de hardware. O armazenamento em cache pode causar alguma economia em termos de uso de recursos para gerar novamente a mesma saída para a mesma entrada.



A capacidade do cache é limitada - quanto mais rápido o armazenamento, mais caro ele é - portanto, ele precisa ser usado com eficiência. Obviamente, você pode, teoricamente, dispensar o armazenamento em cache se o armazenamento principal for rápido o suficiente. Mas será economicamente não lucrativo, uma vez que você terá que realizar uma atualização de hardware significativa, muitas vezes reduzindo para um aumento na RAM e / ou substituindo os discos de HDD para SSD. Essa. aumentar significativamente os requisitos de infraestrutura, o que afetará os parâmetros econômicos de todo o aplicativo que está sendo criado. Sem o cache testado pelo tempo, na maioria dos casos, dificilmente será possível criar um produto em massa.



No entanto, adicionar uma camada de cache não resolve todos os problemas automaticamente. É necessário também refletir sobre as regras para o seu preenchimento, que dependem das características da tarefa, que podem mudar conforme a situação e com o desenvolvimento do serviço. E tenha em mente que isso não é uma panacéia, mas apenas um remédio que irá aliviar os sintomas de problemas de desempenho em partes específicas do seu aplicativo. Se o seu aplicativo tiver problemas de arquitetura profundos, um ambiente de execução medíocre, o armazenamento em cache provavelmente aumentará os problemas.



Existem várias opções de onde armazenar recursos em cache: localmente - na instância do cliente no navegador, em um serviço CDN de terceiros, no lado do aplicativo. Falaremos sobre armazenamento em cache no aplicativo. A memória do processo do aplicativo é talvez a opção de armazenamento em cache de dados mais comum que você encontrará. No entanto, esta solução tem suas desvantagens, uma vez que a memória está associada a um processo que executa uma tarefa específica. Isso é ainda mais importante se você planeja expandir seu aplicativo, uma vez que a memória não é alocada entre os processos, ou seja, não estará disponível em outros processos, como os responsáveis ​​pelo processamento assíncrono. Sim, o cache funciona, mas realmente não tiramos todos os benefícios dele.



Cache do recomendador







Voltando ao projeto, entendemos a necessidade de uma solução de cache centralizada. Para um cache compartilhado, você pode usar, por exemplo, Memcached: se um aplicativo estiver conectado à mesma instância, você pode usá-lo em vários processos. Por um lado, Memcached é uma solução simples e conveniente, por outro lado, é bastante limitado quando se trata de gerenciamento preciso de invalidação, tipagem de dados e consultas mais complexas ao armazenamento de dados em cache. Agora, de fato, o armazenamento Redis se tornou o padrão em tarefas de cache, que não tem as desvantagens do Memcached.



Redis é um armazenamento rápido de valores-chave. Aumenta a eficiência de trabalhar com dados, porque torna-se possível definir a estrutura. Fornece controle granular sobre deficiência e preempção, permitindo que você escolha entre seis políticas diferentes. O Redis oferece suporte para preempção preguiçosa e ansiosa, bem como para preempção de tempo. O uso de estruturas de dados do Redis pode fornecer otimizações tangíveis, dependendo das entidades de negócios. Por exemplo, em vez de armazenar objetos como strings serializadas, os desenvolvedores podem usar uma estrutura de dados hash para armazenar e manipular campos e valores por chave. O hash elimina a necessidade de buscar a string inteira, desserializá-la e substituí-la no cache por um novo valor a cada atualização, o que significa menos consumo de recursos e melhor desempenho. Outras estruturas de dados,As “folhas”, “conjuntos”, “conjuntos classificados”, “hiperlogs”, “bitmaps” e “geo-índices” sugeridos pelo Redis podem ser usados ​​para implementar cenários ainda mais complexos. Conjuntos classificados para análise de dados de séries temporais oferecem complexidade e volume reduzidos no processamento e transferência de dados. A estrutura de dados HyperLogLog pode ser usada para contar itens únicos em um conjunto usando apenas uma pequena quantidade persistente de memória, especificamente 12 KB para cada HyperLogLog (mais alguns bytes para a própria chave). Uma parte significativa dos cerca de 200 comandos disponíveis no Redis são dedicados a operações de processamento de dados e integração de lógica no próprio banco de dados usando scripts Lua.Comandos integrados e recursos de script fornecem flexibilidade para processar dados diretamente no Redis sem ter que enviar dados pela rede para seu aplicativo, reduzindo a sobrecarga de implementação de lógica de cache adicional. Ter os dados do cache disponíveis imediatamente após uma reinicialização pode reduzir significativamente o tempo de aquecimento do cache e aliviar a carga de recalcular o conteúdo do cache do armazenamento de dados principal. Falaremos sobre os recursos da configuração do Redis e as perspectivas de cluster nos artigos a seguir.Falaremos sobre os recursos da configuração do Redis e as perspectivas de cluster nos artigos a seguir.Falaremos sobre os recursos da configuração do Redis e as perspectivas de cluster nos artigos a seguir.



Após a escolha de uma ferramenta de cache, o principal problema era a sincronização dos dados armazenados no cache e dos dados armazenados na aplicação. Existem diferentes maneiras de sincronizar dados, dependendo da lógica de negócios de seu aplicativo. No nosso caso, a dificuldade estava na criação de um algoritmo de invalidação de dados. Os dados colocados na cache ficam aí armazenados por um tempo limitado, desde que haja necessidade de utilização na situação atual, ou pelo menos a probabilidade de tal. À medida que a situação se desenvolve, eles devem abrir espaço para outros dados que, nas condições alteradas, são mais necessários. A principal tarefa neste caso é a seleção dos critérios pelos quais os dados serão removidos do cache. Na maioria das vezes, esse é o momento da relevância dos dados, porém, vale lembrar sobre outros parâmetros: sobre o volume, classificação (desde que iguais, com tolerâncias,tempo de vida), categoria (dados principais ou auxiliares), etc.



A maneira básica e comum de manter os dados atualizados é a obsolescência do tempo. Este método, dada a atualização centralizada periódica dos dados do aplicativo de recomendação, também é utilizado por nós. Porém, nem tudo é tão simples como parece à primeira vista: neste caso, é extremamente importante monitorar a classificação para que apenas dados realmente populares entrem no cache. Isso se torna possível graças à coleta de estatísticas de consulta e à implementação de "pré-aquecimento de dados", ou seja, pré-carregar dados no cache no início do aplicativo. O gerenciamento do tamanho do cache também é um aspecto importante do armazenamento em cache. Em nosso aplicativo, cerca de milhões de recomendações são geradas, portanto, não é realista armazenar todos esses dados no cache. O gerenciamento do tamanho do cache é feito removendo dados do cache para liberar espaço para novos dados.Existem vários métodos padrão: TTL, FIFO, LIFO, último acesso. Por enquanto, estamos usando TTL, porque A instância do Redis não vai além dos recursos de memória e disco alocados.



Lembre-se de que o cache é diferente. Na maioria das vezes, existem duas categorias: write-through e diferido. No primeiro caso, a gravação é realizada de forma síncrona no cache e no armazenamento principal. No segundo, inicialmente, a gravação é realizada apenas no cache, e a gravação no armazenamento principal será adiada até que os dados alterados sejam substituídos por outro bloco de cache. O cache de write-back é mais difícil de implementar, pois requer o monitoramento dos dados no cache para gravação subsequente no armazenamento principal quando ele é removido do cache. Usamos a primeira opção em conjunto com o procedimento de aquecimento do cache. Observe que o "aquecimento" em si é uma tarefa importante e difícil, nossa solução será discutida nos próximos artigos.



Expanda em um aplicativo de recomendação



Para fornecer acesso PREMIER ao aplicativo de recomendação, usamos o protocolo HTTP, que muitas vezes é a principal opção na interação dos aplicativos. É adequado para organizar a interação entre aplicativos, especialmente se o Kubernetes e o Ingress Controller forem usados ​​como o ambiente de infraestrutura. O uso do Kubernetes facilita o dimensionamento. A ferramenta é capaz de balancear automaticamente a solicitação entre pods no cluster para uma operação uniforme, tornando mais fácil para os desenvolvedores escalar. O módulo Ingress Controller é responsável por isso, que define as regras para conexão externa com aplicativos no Kubernetes. Por padrão, os aplicativos no Kubernetes não podem ser acessados ​​pela rede externa. Para fornecer acesso externo aos aplicativos, você deve declarar um recurso de entrada compatível com balanceamento automático.Estamos usando o Nginx Ingress Controller, que oferece suporte a SSL / TLS, regras de reescrita de URI e VirtualServer e VirtualServerRoute para rotear solicitações para diferentes aplicativos, dependendo do URI e do cabeçalho do host.



A configuração básica no Ingress Controller permite que apenas a funcionalidade básica do Nginx seja usada - este é o roteamento baseado em host e caminho, enquanto recursos adicionais, como regras de reescrita de URI, cabeçalhos de resposta adicionais e tempos limite de conexão não estão disponíveis. As anotações aplicadas ao recurso Ingress permitem que você use as funções do próprio Nginx (geralmente disponíveis por meio da configuração do próprio aplicativo) e altere o comportamento do Nginx para cada recurso Ingress.



Planejamos usar o controlador de entrada Nginx não apenas no projeto em consideração, mas também em vários outros aplicativos, sobre os quais falaremos mais tarde. Falaremos sobre isso nos próximos artigos.







Riscos e consequências do uso de cache



Qualquer equipe trabalhando para otimizar um aplicativo com uso intenso de dados terá muitas perguntas sobre como otimizar o cache. Ao mesmo tempo, o problema com o cache não pode ser resolvido "de uma vez por todas"; com o tempo, vários problemas surgem. Por exemplo, à medida que sua base de usuários cresce, você pode encontrar estatísticas conflitantes sobre a popularidade dos dados por categoria, e esse problema precisará ser resolvido.



Embora o cache seja uma ferramenta poderosa para acelerar um aplicativo, não é a única solução. Dependendo da sua situação, pode ser necessário otimizar a lógica do aplicativo, alterar a pilha e o ambiente de infraestrutura. Você também deve ter cuidado ao validar requisitos não funcionais. É possível que, após discussão com o proprietário do produto, os requisitos do aplicativo sejam exagerados. É preciso lembrar que cada uma das soluções possui características próprias.



Os riscos de fornecer dados desatualizados, aumentando a complexidade geral da solução e a probabilidade de introdução de bugs latentes devem ser tratados antes que qualquer método de cache seja aplicado a um projeto. Afinal, neste caso, o armazenamento em cache apenas complicará a solução de problemas, mas, em vez disso, simplesmente ocultará os problemas de desempenho e escalabilidade: as consultas ao banco de dados são lentas? - resultados de cache em armazenamento rápido! As chamadas de API são lentas? - resultados de cache no cliente! Isso ocorre porque a complexidade do código que gerencia o cache aumenta significativamente à medida que a complexidade da lógica de negócios aumenta.



Nas primeiras versões do aplicativo, o cache realmente tem um efeito tangível imediatamente após a implementação. Afinal, a lógica do negócio também é simples: você economiza o valor e o recebe de volta. A invalidação é fácil porque as dependências entre entidades de negócios são triviais ou inexistentes. No entanto, com o tempo, para melhorar o desempenho, você precisará armazenar em cache um número crescente de entidades de negócios.



O cache não é uma solução mágica para problemas de desempenho. Em muitos casos, otimizar seu código e armazenamento principal o fará bem a longo prazo. Além disso, a introdução do cache deve ser uma reação ao problema, e não uma otimização prematura.



Em conclusão, otimizar o desempenho do aplicativo e torná-lo escalável é um processo contínuo que visa atingir um comportamento previsível dentro de requisitos não funcionais especificados. O cache é necessário para reduzir o custo de hardware e o tempo de desenvolvimento gasto na melhoria do desempenho e escalabilidade.



Links:






All Articles