Implementação da interface do usuário do OpenStack LBaaS



Quando implementei a interface do usuário do balanceador de carga para uma nuvem privada virtual, tive que enfrentar dificuldades significativas. Isso me levou a refletir sobre o papel do frontend, que quero compartilhar em primeiro lugar. E justifique seus pensamentos, usando o exemplo de uma tarefa específica.

Na minha opinião, a solução para o problema acabou sendo bastante criativa e eu tive que procurá-lo em uma estrutura muito limitada, então acho que pode ser interessante.

Função de front-end


Devo dizer imediatamente que não finjo a verdade e levanto uma questão controversa. Estou um pouco deprimido pela ironia do front-end e da web em particular, como algo insignificante. E é ainda mais deprimente que às vezes isso aconteça razoavelmente. Agora a moda já está adormecida, mas houve um tempo em que todos estavam andando com estruturas, paradigmas e outras entidades, eles disseram em voz alta que tudo isso é super importante e super necessário, e em troca eles receberam a ironia de que o front-end lida com a saída de formulários e processar cliques nos botões, o que pode ser feito “no joelho”.

Agora, ao que parece, tudo voltou mais ou menos ao normal. Ninguém realmente quer falar sobre cada versão menor do próximo framework. Poucas pessoas procuram a ferramenta ou abordagem perfeita, devido à crescente conscientização de sua utilidade. Mas mesmo isso, por exemplo, não interfere na repreensão quase irracional do Electron e nas aplicações nele. Acho que isso se deve à falta de entendimento da tarefa que está sendo resolvida pelo front-end.

O frontend não é apenas um meio de exibir informações fornecidas pelo back-end e não apenas um meio de processar ações do usuário. O frontend é algo mais, algo abstrato, e se você der uma definição simples e clara, o significado será inevitavelmente perdido.

O frontend está em alguma "estrutura". Por exemplo, em termos técnicos, está entre a API fornecida pelo back-end e a API fornecida pelos recursos de E / S. Em termos de tarefas, é entre as tarefas da interface do usuário que o UX resolve e as tarefas que o back-end resolve. Assim, é obtida uma especialização frontend bastante estreita, uma especialização da camada. Isso não significa que os provedores de front-end não possam exercer influência em áreas fora de sua especialização, mas no momento em que essa influência é impossível, surge a verdadeira tarefa de front-end.

Este problema pode ser expresso através de uma contradição. Não é necessário que a interface do usuário esteja em conformidade com os modelos de dados e o comportamento de back-end. O comportamento e os modelos de dados do back-end não são necessários para atender às tarefas da interface do usuário. E então a tarefa do front-end é eliminar essa contradição. Quanto maior a discrepância entre as tarefas do back-end e a interface do usuário, mais importante é o papel do front-end. E para deixar claro o que estou falando, darei um exemplo em que essa discrepância, por alguma razão, se mostrou significativa.

Declaração do problema


O OpenStack LBaaS, na minha opinião, é um complexo de ferramentas de hardware e software necessário para equilibrar a carga entre servidores. É importante para mim que sua implementação dependa de fatores objetivos, da exibição física. Por esse motivo, existem algumas peculiaridades na API e nas maneiras de interagir com essa API.

Ao desenvolver uma interface de usuário, o principal interesse não são os recursos técnicos do back-end, mas seus recursos fundamentais. A interface é criada para o usuário, e o usuário precisa de uma interface para gerenciar os parâmetros de balanceamento, e o usuário não precisa se aprofundar nos recursos internos da implementação de back-end.

O back-end é em grande parte desenvolvido pela comunidade e é possível influenciar seu desenvolvimento em quantidades muito limitadas. Um dos principais recursos para mim é que os desenvolvedores de back-end estão prontos para sacrificar a conveniência e a simplicidade dos controles por uma questão de desempenho, e isso é absolutamente justificado, pois é uma questão de equilibrar a carga.

Há mais um ponto sutil, e quero descrevê-lo imediatamente, alertando algumas perguntas. É claro que no OpenStack e em sua API a luz não convergiu. Você sempre pode desenvolver seu próprio conjunto de ferramentas ou uma "camada" que funcionará com a API do OpenStack, produzindo sua própria API que é conveniente para as tarefas do usuário. A única questão é a conveniência. Se as ferramentas inicialmente disponíveis permitem implementar a interface do usuário como pretendida, faz sentido produzir entidades?

A resposta a esta pergunta é multifacetada e, para os negócios, dependerá de desenvolvedores, emprego, competência, questões de responsabilidade, suporte e assim por diante. No nosso caso, foi mais conveniente resolver algumas das tarefas no front-end.

Recursos do OpenStack LBaaS


Quero identificar apenas os recursos que tiveram uma forte influência no frontend. As perguntas sobre por que esses recursos surgiram ou em que eles confiam já estão além do escopo deste artigo.

Trabalho com documentação pronta e tenho que aceitar seus recursos. Quem estiver interessado no que é o OpenStack Octavia, por dentro, pode se familiarizar com a documentação oficial . Octavia é o nome de um conjunto de ferramentas projetadas para equilibrar a carga no ecossistema OpenStack.

O primeiro recurso que encontrei durante o desenvolvimento é o grande número de modelos e relacionamentos necessários para exibir o estado do balanceador. A API Octavia descreve 12 modelos, mas apenas o 7. é necessário para o lado do cliente.Esses modelos têm conexões, geralmente desnormalizadas, a imagem abaixo mostra um diagrama aproximado:



"Seven" não parece muito impressionante, mas, na realidade, para garantir a operação completa da interface, no momento em que escrevi este texto, eu tive que usar 16 modelos de dados e cerca de 30 relacionamentos entre eles. Como o Octavia é apenas um balanceador, ele requer outros módulos do OpenStack para funcionar. E tudo isso é necessário para apenas duas páginas na interface do usuário.

O segundo e o terceiro recurso são o Octavia assíncrono e transacional. Os modelos de dados têm um campo de status que reflete o estado das operações executadas em um objeto.
StatusDescrição do produto
ATIVOObjeto em bom estado
EXCLUÍDOObjeto excluído
ErroO objeto está corrompido
PENDING_CREATEObjeto em construção
PENDING_UPDATEObjeto no processo de atualização
PENDING_DELETEObjeto no processo de exclusão
A operação de leitura de um objeto ocorre de forma síncrona e não possui restrições. Mas as operações de criação, atualização e exclusão podem levar um tempo indefinido. Isso se deve precisamente ao fato de os modelos de dados terem, grosso modo, significado físico.

Depois de enviar uma solicitação de criação, podemos saber que o registro apareceu, podemos lê-lo, mas até que a operação de criação seja concluída, não podemos executar nenhuma outra operação nesse registro. Qualquer tentativa resultará em erro. A operação de alteração de um objeto pode ser iniciada apenas quando o objeto estiver no status ATIVO ; você pode enviar um objeto para exclusão nos status ATIVO e ERRO .

Esses status podem ser obtidos via WebSockets, o que facilita muito o processamento, mas as transações são um problema muito maior. Ao fazer alterações em qualquer objeto, todos os modelos relacionados também participarão da transação. Por exemplo, ao fazer alterações em Membro , o Pool , o Ouvinte e o Loadbalancer associados serão bloqueados. É assim que se parece em termos de eventos recebidos em soquetes da web:

  • os quatro primeiros eventos são a transferência de objetos para o status PENDING_UPDATE : o campo de destino contém o nome do modelo do objeto que participa da transação;
  • o quinto evento é apenas uma duplicata (não sei com o que ele está conectado);
  • os quatro últimos são um retorno ao status ATIVO . Nesse caso, é uma operação de alteração de peso e leva menos de um segundo, mas às vezes leva muito mais tempo.

Você também pode ver na captura de tela que a ordem dos eventos não precisa ser rigorosa. Assim, verifica-se que, para iniciar qualquer operação, é necessário conhecer não apenas o status do próprio objeto, mas também o status de todas as dependências que também participarão da transação.

Recursos da interface do usuário


Agora imagine-se no lugar de um usuário que precisa conhecer um local que seja para equilibrar entre dois servidores:

  1. É necessário criar um ouvinte no qual o algoritmo de balanceamento será definido.
  2. Crie um pool.
  3. Atribua um pool ao ouvinte.
  4. Adicione links para portas balanceadas no pool.

Cada vez é necessário aguardar a conclusão da operação, que depende de todos os objetos criados anteriormente.

Como um estudo interno mostrou, na visão do usuário comum, existe apenas uma percepção aproximada de que o balanceador deve ter um ponto de entrada, deve haver pontos de saída e os parâmetros do balanceamento a serem realizados: algoritmo, peso e outros. O usuário não precisa saber o que é o OpenStack.

Não sei o quão complicada a interface deve ser para a percepção, onde o próprio usuário deve seguir todos os recursos técnicos do back-end descrito acima. Para o console, isso pode ser permitido, pois seu uso implica um alto nível de imersão em tecnologia, mas para a web essa interface é horrível.

Na web, o usuário espera preencher um formulário claro e lógico, pressionar um botão, aguardar e tudo funcionará. Talvez isso possa ser discutido, mas proponho me concentrar nos recursos que afetam a implementação do frontend.

A interface foi projetada de forma a envolver o uso em cascata de operações: uma ação na interface pode envolver várias operações. A interface não implica que o usuário possa executar ações que atualmente não são possíveis, mas assume que o usuário deve entender por que isso é assim. A interface é um todo único e, portanto, seus elementos individuais podem usar informações de várias entidades dependentes, incluindo meta-informações.



Se levarmos em conta que existem alguns recursos da interface que não são exclusivos do balanceador, como opções, acordeões, guias, um menu de contexto e assumimos que seus princípios operacionais são claros inicialmente, acho que para um usuário que sabe o que é o balanceamento de carga, não será muito difícil ler a maior parte da interface acima e supor como gerenciá-la. Mas destacar quais partes da interface estão ocultas atrás dos modelos do balanceador, ouvinte, pool, membro e outras entidades não é mais a tarefa mais óbvia.

Resolvendo Contradições


Espero ter conseguido mostrar que os recursos do back-end não se encaixam bem na interface e que esses recursos nem sempre podem ser eliminados pelo back-end. Junto com isso, os recursos da interface não se encaixam bem no back-end e nem sempre podem ser eliminados sem complicar a interface. Cada uma dessas áreas resolve seus próprios problemas. A responsabilidade do front-end é resolver problemas para garantir o nível necessário de interação entre a interface e o back-end.

Na minha prática, imediatamente corri para a piscina com a cabeça, sem prestar atenção, ou melhor, nem tentando descobrir os recursos mais altos, mas tive sorte ou a experiência ajudou (e o vetor correto foi escolhido). Reparei repetidamente por mim mesmo que, ao usar uma API ou biblioteca de terceiros, é muito útil se familiarizar com a documentação com antecedência: quanto mais detalhado, melhor. A documentação geralmente é semelhante entre si, as pessoas ainda confiam na experiência de outras pessoas, mas há uma descrição dos recursos de cada sistema individual e está contida nos detalhes.

Se eu passasse algumas horas extras estudando a documentação, em vez de extrair as informações necessárias por palavras-chave, teria pensado nos problemas que encontraria e esse conhecimento poderia ter um impacto na arquitetura do projeto desde os estágios iniciais. Voltar para eliminar os erros cometidos no início é muito desmoralizante. E sem um contexto completo, às vezes você precisa voltar várias vezes.

Como opção, você pode dobrar sua linha, gerando gradualmente mais e mais códigos "com uma mordida", mas quanto mais esse monte de código for, mais ele será processado no final. Ao projetar a arquitetura, é claro, não se deve mergulhar muito fundo, levar em consideração todas as opções possíveis e impossíveis, gastando uma quantidade enorme de tempo nela, é importante encontrar um equilíbrio. Porém, o conhecimento mais ou menos detalhado da documentação costuma ser um investimento muito útil e não leva muito tempo.

No entanto, desde o início, tendo visto um grande número de modelos envolvidos, percebi que seria necessário criar um mapeamento do estado de back-end para o cliente com todas as conexões preservadas. Depois que eu consegui exibir todas as informações necessárias no cliente, com todas as conexões e assim por diante, foi necessário organizar uma fila de tarefas.

Os dados são atualizados de forma assíncrona, a disponibilidade das operações é determinada por uma variedade de condições e, quando são necessárias operações em cascata, nenhuma fila pode ser dispensada nessas condições. Talvez, em poucas palavras, essa seja toda a arquitetura da minha solução: armazenamento com um reflexo do estado de back-end e da fila de tarefas.

Arquitetura da solução


Devido ao número indefinido de modelos e relacionamentos, coloco escalabilidade na estrutura do repositório fazendo isso usando uma fábrica que retorna uma descrição declarativa das coleções do repositório. A coleção possui um serviço, uma classe de modelo simples com CRUD. Seria possível fazer uma descrição dos links no modelo, como é feito, por exemplo, no RoR ou no bom e antigo Backbone, mas isso exigiria que uma grande quantidade de código fosse alterada. Portanto, a descrição das relações fica próxima à classe do modelo:



No total, eu tenho dois tipos de conexões: um para um, um para muitos. O feedback também pode ser descrito. Além do tipo, é indicada a coleção de dependências, o campo ao qual a dependência encontrada é anexada e o campo no qual o ID do objeto dependente é lido (no caso de comunicação um para muitos, a lista de IDs é lida). Se a condição de comunicação de um objeto for mais complicada do que simples links para objetos, então na fábrica é possível descrever a função de testar dois objetos, cujos resultados determinarão a presença de uma conexão. Tudo parece um pouco "bicicleta", mas funciona sem dependências desnecessárias e exatamente como deveria.

O repositório possui um módulo para aguardar a adição e exclusão de um recurso, em essência está processando eventos únicos com verificação condicional e com uma interface promis. Ao se inscrever, o tipo de evento (adição, exclusão), função de teste e manipulador são aprovados. Quando um determinado evento ocorre e com um resultado de teste positivo, o manipulador é executado, após o qual o rastreamento é interrompido. Pode ocorrer um evento ao se inscrever de forma síncrona.

O uso desse padrão tornou possível afixar automaticamente relacionamentos arbitrariamente complexos entre modelos e fazê-lo em um só lugar. Este lugar eu chamei de rastreador. Ao adicionar um objeto ao repositório, ele começa a rastrear seus relacionamentos. O módulo em espera permite responder a eventos e verificar se há uma conexão entre o objeto monitorado e o objeto no armazenamento. Se o objeto já estava no repositório, o módulo de espera chama o manipulador imediatamente.

Esse dispositivo de armazenamento permite descrever qualquer número de coleções e os relacionamentos entre elas. Ao adicionar e excluir objetos, o armazenamento coloca ou redefine automaticamente as propriedades com o conteúdo dos objetos dependentes. As vantagens dessa abordagem são que todos os relacionamentos são descritos explicitamente e são monitorados e atualizados por um sistema; contras - na complexidade da implementação e depuração.

Em geral, esse repositório é bastante trivial e eu fiz isso sozinho, porque seria muito mais difícil integrar uma solução pronta a uma base de código existente, mas seria ainda mais difícil anexar uma fila de tarefas a uma solução pronta.

Todas as tarefas, como coleções, têm uma descrição declarativa e são criadas pela fábrica. As tarefas podem ter na descrição as condições para iniciar e uma lista de tarefas que precisarão ser adicionadas à fila após a conclusão da atual.


O exemplo acima descreve a tarefa de criar um pool. Nas dependências, o balanceador e o ouvinte são indicados, por padrão, uma verificação é realizada para o status ATIVO . O objeto do balanceador está bloqueado, porque as tarefas de processamento na fila podem ocorrer de forma síncrona, o bloqueio permite evitar conflitos no momento em que a solicitação de execução foi enviada, mas o status não foi alterado, mas supõe-se que será alterado. Em vez de PAI , se o pool for criado como resultado da cascata de tarefas, o ID será substituído automaticamente.

Depois de criar um pool, as tarefas serão adicionadas à fila para criar um monitor de disponibilidade e criar todos os membros desse pool. A saída é uma estrutura que pode ser totalmente convertida em JSON. Isso é feito para restaurar a fila em caso de falha.

A fila, com base na descrição da tarefa, monitora independentemente todas as alterações no repositório e verifica as condições que devem ser atendidas para executar a tarefa. Como eu já disse, os status vêm por meio de soquetes da web e é muito simples gerar os eventos necessários para a fila, mas, se necessário, não será um problema anexar um mecanismo de atualização de dados do timer (isso foi originalmente estabelecido na arquitetura, pois os soquetes da web eram por várias razões, pode não funcionar muito estável). Após a conclusão da tarefa, a fila informa automaticamente o repositório sobre a necessidade de atualizar os links nos objetos especificados.

Conclusão


A necessidade de escalabilidade levou a uma abordagem declarativa. A necessidade de exibir modelos e os relacionamentos entre eles levou a um único repositório. A necessidade de processar objetos dependentes levou à fila.

Combinar essas necessidades pode não ser a tarefa mais fácil em termos de implementação (mas esse é um problema separado). Porém, em termos de arquitetura, a solução é muito simples e permite eliminar todas as contradições entre as tarefas do back-end e a interface do usuário, estabelecer sua interação e estabelecer as bases para outros recursos possíveis de qualquer uma das partes.

Do lado do painel de controle Selectel , o processo de balanceamento é simples e direto, o que permite que os clientes do serviço não gastem recursos na implementação independente do balanceador, mantendo a capacidade de controlar o tráfego de maneira flexível.

Experimente o nosso balanceador em ação agora e escreva sua opinião nos comentários.

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


All Articles