Como criar um aplicativo multilocatário de um aplicativo não-inquilino

imagem


Não darei uma definição de multilocação, eles já escreveram sobre isso várias vezes aqui e aqui . E é melhor ir direto ao tópico do artigo e começar com as seguintes perguntas:


Por que o aplicativo não é multitenant imediatamente?


Acontece que o aplicativo foi desenvolvido inicialmente para instalação apenas no lado do cliente. Você pode chamar esse aplicativo em caixa ou software como um produto . Um cliente compra uma caixa e implanta o aplicativo em seus servidores (há muitos exemplos desses aplicativos).


Porém, com o tempo, a empresa desenvolvedora pode pensar que seria bom colocar o aplicativo na nuvem para que ele seja alugado (software como serviço). Esse método de implantação possui vantagens para os clientes e a empresa desenvolvedora. Os clientes podem obter rapidamente um sistema operacional e não se preocupar com implantação e administração. Ao alugar um aplicativo, você não precisa de grandes investimentos únicos.


E a empresa desenvolvedora receberá novos clientes, bem como novas tarefas: implantar o aplicativo na nuvem, administrar, atualizar para novas versões, migrar dados durante a atualização, backup de dados, monitorar velocidade e erros, corrigir problemas, se ocorrerem.


Por que o aplicativo na nuvem deve ser multilocatário?


Para colocar um aplicativo na nuvem, não é necessário torná-lo multilocatário. Porém, haverá o seguinte problema: para cada cliente, você precisará implantar um suporte dedicado na nuvem com o aplicativo concedido, e isso já é caro, tanto em termos do consumo de recursos do suporte em nuvem quanto em termos de administração. É mais lucrativo implementar a multilocação no aplicativo para que uma instância possa atender a vários clientes (organizações).


Se o aplicativo atrair 1000 usuários trabalhando simultaneamente, é vantajoso agrupar clientes (organizações) para que, no total, eles ofereçam a carga desejada de 1.000 usuários por instância do aplicativo. E haverá o consumo ideal de recursos da nuvem.


Suponha que o aplicativo seja alugado por uma organização para 20 usuários (funcionários da organização). Então você precisa agrupar 50 dessas organizações para alcançar a carga certa. É importante isolar as organizações umas das outras. Uma organização aluga um aplicativo, deixa apenas seus funcionários irem para lá, armazena apenas seus dados e não vê que outras organizações também sejam atendidas pelo mesmo aplicativo.


A implementação da multilocação não significa que o aplicativo não pode mais ser implantado localmente no servidor da organização. Você pode suportar dois métodos de implantação ao mesmo tempo:


  • aplicativo multilocatário na nuvem;
  • aplicativo de inquilino único no servidor do cliente.

Nosso aplicativo foi de maneira semelhante: de não-inquilino a multi-inquilino. E neste artigo, compartilharei algumas abordagens no desenvolvimento da multilocação.


Como implementar a multilocação em um aplicativo projetado como não-inquilino?


Limitaremos imediatamente o tópico, consideraremos apenas o desenvolvimento, não abordaremos questões de teste, lançamento de versão, implantação e administração. Em todas essas áreas, o surgimento da multilocação também deve ser levado em consideração, mas, por enquanto, falaremos apenas sobre desenvolvimento.


Para entender o que é um aplicativo que não era arrendatário e se tornou multilocatário, descreverei seu objetivo, uma lista de serviços e tecnologias utilizadas.


Este é um sistema ECM (DirectumRX), que consiste em 10 serviços (5 serviços monolíticos e 5 microsserviços). Todos esses serviços podem ser colocados em um servidor poderoso ou em vários servidores.


Os serviços são
  • Serviço da Web - para atender clientes da Web (navegadores).
  • Serviço WCF - para atender clientes de desktop (aplicativos WPF).
  • Serviço para aplicativos móveis.
  • Serviço para executar processos em segundo plano.
  • Serviço para o planejamento de processos em segundo plano.
  • Serviço de execução de esquema de fluxo de trabalho
  • Serviço de Execução de Bloco de Fluxo de Trabalho
  • Serviço de armazenamento de documentos (dados binários).
  • Serviço para converter documentos em html (visualização em um navegador).
  • Serviço para armazenar resultados de conversão em html

Pilha de tecnologias utilizadas:
.NET + SQLServer / Postgres + NHibernate + IIS + RabbitMQ + Redis


Então, o que fazer com que os serviços se tornem multilocatários? Para fazer isso, você precisa refinar os seguintes mecanismos nos serviços, a saber, adicionar conhecimento sobre inquilinos a:


  • armazenamento de dados;
  • ORM;
  • cache de dados;
  • solicitação de processamento;
  • processamento de mensagens na fila;
  • configuração;
  • registro;
  • executando tarefas em segundo plano;
  • interação com microsserviços;
  • interação com o intermediário de mensagens.

No caso de nossa aplicação, esses foram os principais locais que exigiram melhorias. Vamos considerá-los separadamente.


Escolhendo um método de armazenamento de dados


Quando você lê artigos sobre multilocação, a primeira coisa que eles resolvem é como organizar o armazenamento de dados. De fato, o ponto é importante.


Para o nosso sistema ECM, o armazenamento principal é um banco de dados relacional, que possui cerca de 100 tabelas. Como organizar o armazenamento de dados de muitas organizações para que a organização A não veja de forma alguma os dados da organização B?


Vários esquemas são conhecidos (muito já foi escrito sobre esses esquemas):


  • crie seu próprio banco de dados para cada organização (para cada inquilino);
  • use um banco de dados para todas as organizações, mas para cada organização faça seu próprio esquema no banco de dados;
  • use um banco de dados para todas as organizações, mas adicione uma coluna "chave de inquilino / organização" em cada tabela.

A escolha do esquema não é acidental. No nosso caso, basta considerar os casos de administração do sistema para entender a opção preferida. Os casos são os seguintes:


  • adicionar inquilino (uma nova organização aluga um sistema);
  • remover inquilino (a organização se recusou a alugar);
  • transferir inquilino para outro estande de nuvem (redistribua a carga entre os estandes de nuvem quando um deles parar de lidar com a carga).

Considere um caso de transferência de inquilino. A principal tarefa da transferência é transferir os dados da organização para outro estande. A transferência não é difícil de fazer se o inquilino tiver seu próprio banco de dados, mas será uma dor de cabeça se você misturar os dados de diferentes organizações em 100 tabelas. Tente extrair apenas os dados necessários das tabelas, transfira-os para outro banco de dados, onde já existem dados de outros inquilinos e para que seus identificadores não se cruzem.


O próximo caso é a adição de um novo inquilino. O caso também não é simples. A adição de inquilino é a necessidade de preencher diretórios, usuários, direitos do sistema, para que você possa efetuar login no sistema. Essa tarefa é melhor resolvida pela clonagem de um banco de dados de referência, que já possui tudo o que você precisa.


O caso de remoção de inquilino é resolvido com muita facilidade desativando o banco de dados do inquilino.


Por esses motivos, escolhemos um esquema: um inquilino - um banco de dados .


ORM


Escolhemos o método de armazenamento de dados, a próxima pergunta: como ensinar o ORM a trabalhar com o esquema selecionado?


Nós usamos o Nhibernate. Era necessário que o Nhibernate trabalhasse com vários bancos de dados e alternasse periodicamente para o correto, por exemplo, dependendo da solicitação http. Se processarmos a solicitação da organização A, o banco de dados A foi usado e, se a solicitação for da organização B, o banco de dados B.


O NHibernate tem essa oportunidade. Você precisa substituir a implementação do NHibernate.Connection.DriverConnectionProvider . Sempre que o NHibernate deseja abrir uma conexão com o banco de dados, ele chama DriverConnectionProvider para obter uma cadeia de conexão. Aqui vamos substituí-lo pelo necessário:


public class MyDriverConnectionProvider : DriverConnectionProvider { protected override string ConnectionString => TenantRegistry.Instance.CurrentTenant.ConnectionString; } 

O que é TenantRegistry.Instance.CurrentTenant Vou contar um pouco mais tarde.


Armazenamento em cache de dados


Os serviços geralmente armazenam em cache os dados para minimizar as consultas ao banco de dados ou não calcular a mesma coisa várias vezes. O problema é que os caches devem ser divididos por inquilinos se os dados do inquilino forem armazenados em cache. Não é aceitável que o cache de dados de uma organização seja usado ao processar uma solicitação de outra organização. A solução mais simples é adicionar um identificador de inquilino à chave de cada cache:


 var tenantCacheKey = cacheKey + TenantRegistry.Instance.CurrentTenant.Id; 

Esse problema deve ser lembrado ao criar cada cache. Existem muitos caches em nossos serviços. Para não esquecer de levar em consideração o identificador de inquilino em cada um, é melhor unificar o trabalho com caches. Por exemplo, crie um mecanismo de armazenamento em cache geral que será armazenado em cache imediatamente no contexto dos inquilinos.


Registo


Cedo ou tarde, algo dará errado no sistema, você precisará abrir o arquivo de log e começar a estudá-lo. A primeira pergunta é: em nome de qual usuário e qual organização essas ações foram cometidas?


É conveniente quando em cada linha do log há um identificador de inquilino e um nome de usuário de inquilino. Essas informações se tornam tão necessárias quanto, por exemplo, o tempo da mensagem:


 2019-05-24 17:05:27.985 <message> [User2 :Tenant1] 2019-05-24 17:05:28.126 <message> [User3 :Tenant2] 2019-05-24 17:05:28.173 <message> [User4 :Tenant3] 

O desenvolvedor não deve pensar em qual inquilino gravar no log, ele deve ser automatizado, oculto "sob o capô" do sistema de log.


Nós usamos o NLog, então eu darei um exemplo. A maneira mais fácil de proteger o identificador de inquilino é criar NLog.LayoutRenderers.LayoutRenderer , que permite obter identificador de inquilino para cada entrada de log:


  [LayoutRenderer("tenant")] public class TenantLayoutRenderer : LayoutRenderer { protected override void Append(StringBuilder builder, LogEventInfo logEvent) { builder.Append(TenantRegistry.Instance.CurrentTenant.Id); } } 

E use este LayoutRenderer no modelo de log:


 <target layout="${odate} ${message} [${user} :${tenant}]"/> 

Execução de código


Nos exemplos acima, eu costumava usar o seguinte código:


 TenantRegistry.Instance.CurrentTenant 

É hora de dizer o que isso significa. Mas primeiro você precisa entender a abordagem que seguimos nos serviços:


Qualquer execução de código (processando uma solicitação http, processando uma mensagem na fila, executando uma tarefa em segundo plano em um encadeamento separado) deve estar associada a algum inquilino.

Isso significa que, em qualquer local da execução do código, é possível perguntar: "Para qual inquilino esse encadeamento funciona?" ou de outra maneira: "Qual é o inquilino atual?"


TenantRegistry.Instance.CurrentTenant é o inquilino atual do fluxo atual. Stream e inquilino podem ser vinculados em nossos aplicativos. Eles estão conectados temporariamente, por exemplo, ao processar uma solicitação http ou ao processar uma mensagem da fila. Uma maneira de vincular o inquilino a um fluxo é feita assim:


 //    . using (TenantRegistry.Instance.SwitchTo(tenantId)) { // ,     . var tenant = TenantRegistry.Instance.CurrentTenant; //     . var connectionString = tenant.ConnectionString; //  . var id = tenant.Id; } 

Um inquilino vinculado a um encadeamento pode ser obtido em qualquer lugar do código, entrando em contato com TenantRegistry - este é um singleton, um ponto de acesso para trabalhar com inquilinos. Portanto, Nhibernate e NLog podem acessar esse singleton (nos pontos de extensão) para descobrir a cadeia de conexão ou o identificador de inquilino.


Tarefas em segundo plano


Os serviços geralmente têm tarefas em segundo plano que precisam ser executadas em um cronômetro. As tarefas em segundo plano podem acessar o banco de dados da organização e, em seguida, a tarefa em segundo plano deve ser executada para cada inquilino. Para fazer isso, não é necessário iniciar um cronômetro ou thread separado para cada inquilino. É possível executar uma tarefa em diferentes inquilinos em um único encadeamento / cronômetro. Para fazer isso, no manipulador de timer, classificamos os inquilinos, associamos cada inquilino a um fluxo e executamos uma tarefa em segundo plano:


 //    . foreach (var tenant in TenantRegistry.Instance.Tenants) { //    . using (TenantRegistry.Instance.SwitchTo(tenant.Id)) { //     . } } 

Dois inquilinos não podem ser conectados ao fluxo ao mesmo tempo; se anexarmos um, o outro será desconectado do fluxo. Utilizamos ativamente essa abordagem para não produzir threads / temporizadores para tarefas em segundo plano.


Como correlacionar uma solicitação http com um inquilino


Para processar a solicitação http do cliente, você precisa saber de qual organização ele veio. Se o usuário já estiver autenticado, o identificador de inquilino poderá ser armazenado no cookie de autenticação (se o trabalho com o aplicativo for realizado por meio do navegador) ou no token JWT. Mas e se o usuário ainda não tiver se autenticado? Por exemplo, um usuário anônimo abriu um site de aplicativo e deseja se autenticar. Para fazer isso, ele envia uma solicitação com um login e senha. No banco de dados de qual organização procurar esse usuário?


Além disso, serão recebidas solicitações anônimas para obter a página de logon no aplicativo e isso pode ser diferente para diferentes organizações, por exemplo, o idioma da localização.


Para resolver o problema de correlação entre solicitação e organização anônima de http (inquilino), usamos subdomínios para organizações. O nome do subdomínio é formado pelo nome da organização. Os usuários devem usar o subdomínio para trabalhar com o sistema:


 https://company1.service.com https://company2.service.com 

O mesmo serviço da web multilocatário está disponível nesses endereços. Mas agora o serviço entende de qual organização uma solicitação HTTP anônima virá, com foco no nome de domínio.
A ligação do nome do domínio e do inquilino é realizada no arquivo de configuração do serviço da web:


 <tenant name="company1" db="database1" host="company1.service.com" /> <tenant name="company2" db="database2" host="company2.service.com" /> 

Sobre a configuração de serviços será descrito abaixo.


Microsserviços. Armazenamento de dados


Quando eu disse que o sistema ECM precisa de 100 tabelas, falei sobre serviços monolíticos. Mas acontece que um microsserviço requer um armazenamento relacional, no qual são necessárias 2-3 tabelas para armazenar seus dados. Idealmente, cada microsserviço possui seu próprio armazenamento, ao qual somente ele tem acesso. E o microsserviço decide como armazenar dados no contexto dos inquilinos.


Mas seguimos o outro caminho: decidimos armazenar todos os dados da organização em um banco de dados. Se um microsserviço exigir armazenamento relacional, ele usará o banco de dados da organização existente para que os dados não sejam espalhados por diferentes armazenamentos, mas sejam coletados em um banco de dados. Serviços monolíticos usam o mesmo banco de dados.


Os microsserviços funcionam apenas com suas tabelas no banco de dados e não tentam trabalhar com tabelas de um monólito ou outro microsserviço. Existem prós e contras nessa abordagem.


Prós:


  • dados da organização em um só lugar;
  • fácil de fazer backup e restaurar dados da organização;
  • No backup, os dados de todos os serviços são consistentes.

Contras:


  • um banco de dados para todos os serviços é muito restrito ao escalar (os requisitos para os recursos do DBMS aumentam);
  • os microsserviços têm acesso físico às tabelas uns dos outros, mas não usam esse recurso.

Microsserviços. Nem sempre é necessário o conhecimento dos inquilinos.


Um microsserviço pode não saber que funciona em um ambiente de multilocatário. Considere um de nossos serviços, que está envolvido na conversão de documentos em html.


O que o serviço faz:


  1. Leva uma mensagem de uma fila RabbitMQ para converter um documento.
    • recupera o identificador de documento e o inquilino da mensagem
  2. Baixe um documento de um serviço de armazenamento de documentos.
    • pois isso gera uma solicitação na qual transmite o identificador do documento e o identificador do inquilino
  3. Converte um documento em html.
  4. Fornece html ao serviço para armazenar resultados de conversão.

O serviço não armazena documentos e não armazena resultados de conversão. Ele tem conhecimento indireto dos inquilinos: o identificador do inquilino passa pelo serviço em trânsito.


Microsserviços. Subdomínios não são necessários


Eu escrevi acima que subdomínios ajudam a resolver o problema de solicitações HTTP anônimas:


 https://company1.service.com https://company2.service.com 

Mas nem todos os serviços funcionam com solicitações anônimas, a maioria exige autenticação já aprovada. Portanto, os microsserviços que funcionam via http geralmente não se importam com o nome de host da solicitação, eles recebem todas as informações sobre o inquilino do token JWT ou do cookie de autenticação que acompanha cada solicitação.


Configuração


Os serviços precisam ser configurados para que eles saibam sobre os inquilinos. Ou seja:


  • especifique as strings para conectar-se ao banco de dados de inquilinos;
  • vincular nomes de domínio a inquilinos;
  • especifique o idioma e o fuso horário padrão do inquilino.

Os inquilinos podem ter muitas configurações. Para nossos serviços, definimos as configurações de inquilino nos arquivos de configuração xml. Este não é o web.config e não o app.config. Esse é um arquivo xml separado, cujas alterações devem poder ser capturadas sem a reinicialização dos serviços, para que a adição de um novo inquilino não reinicie o sistema inteiro.


A lista de configurações é mais ou menos assim:


 <!--  . --> <block name="TENANTS"> <tenant name="Jupiter" db="DirectumRX_Jupiter" login="admin" password="password" hyperlinkUriScheme="jupiter" hyperlinkFileExtension=".jupiter" hyperlinkServer="http://jupiter-rx.directum.ru/Sungero" helpAddress="http://jupiter-rx.directum.ru/Sungero/help" devHelpAddress="http://jupiter-rx.directum.ru/Sungero/dev_help" language="Ru-ru" isAttributesSignatureAbsenceAllowed="false" endorsingSignatureLocksSignedProperties="false" administratorEmail ="admin@jupiter-company.ru" feedbackEmail="support@jupiter-company.ru" isSendFeedbackAllowed="true" serviceUserPassword="password" utcOffset="5" collaborativeEditingEnabled="false" collaborativeEditingForced="false" /> <tenant name="Mars" db="DirectumRX_Mars" login="admin" password="password" hyperlinkUriScheme="mars" hyperlinkFileExtension=".mars" hyperlinkServer="http://mars-rx.directum.ru/Sungero" helpAddress="http://mars-rx.directum.ru/Sungero/help" devHelpAddress="http://mars-rx.directum.ru/Sungero/dev_help" language="Ru-ru" isAttributesSignatureAbsenceAllowed="false" endorsingSignatureLocksSignedProperties="false" administratorEmail ="root@mars-ooo.ru" feedbackEmail="support@mars-ooo.ru" isSendFeedbackAllowed="true" serviceUserPassword="password" utcOffset="-1" collaborativeEditingEnabled="false" collaborativeEditingForced="false" /> </block> 

Quando uma nova organização aluga um serviço, ela precisa adicionar um novo inquilino ao arquivo de configuração. E é desejável que outras organizações não sintam isso. Idealmente, não deve haver uma reinicialização dos serviços.


Em nós, nem todos os serviços são capazes de pegar uma configuração sem reiniciar, mas os serviços mais críticos (monólitos) são capazes de fazer isso.


Sumário


Quando um aplicativo se torna multilocatário, parece que a complexidade do desenvolvimento aumentou dramaticamente. Mas então você se acostuma à multitenalidade e trata seu suporte como um requisito normal.


Também é importante lembrar que a multilocação não é apenas desenvolvimento, mas também testes, administração, implantação, atualização, backups, migrações de dados. Mas melhor sobre eles outra vez.

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


All Articles