O desenvolvimento de aplicações buscando o conceito de Software as a Service (SaaS), ou Software como Serviço, tem se difundido nos últimos anos. A ideia é que as aplicações sejam criadas e vendidas como serviços, onde os compradores fazem uso e pagam de forma periódica. Nesse caso, porém, onde ficam as experiências únicas para os usuários? É nesse contexto que entram as aplicações Multi-Tenant, que basicamente buscam criar diferentes instâncias da mesma aplicação para diferentes usuários, com configurações baseadas nas necessidades do mesmo. Ao longo desse artigo, vamos entender o funcionamento desse tipo de aplicação e em que situações a mesma é interessante.

O que são aplicações Multi-Tenant?

Do começo da tecnologia da informação até alguns anos atrás, o que víamos era a construção dos chamados softwares de caixa: uma determinada empresa oferecia uma solução e vários usuários iam lá e compravam a mesma, sem nenhum tipo de alteração no produto que utilizavam. Com as tecnologias de banda larga e aplicações web evoluindo, esse modelo foi perdendo força, e hoje está limitado a uma pequena fatia do mercado. Os consumidores foram ficando mais exigentes, querendo algo que melhor se adapte à suas necessidades, e nesse contexto as aplicações Multi-Tenant são excelentes.

A técnica do Multi-Tenant é muito simples. Vamos começar analisando o contexto mostrado na Figura 1. Repare que temos uma aplicação base que chama um código “switch”. Esse código irá, portanto, selecionar o consumidor e entregar uma experiência customizada para o mesmo. Funcionalmente, está certo. Tecnicamente, também. Porém, imagine como iremos controlar todas essas instâncias, uma vez que o número “N” crescer? O código ficaria muito difícil de ser entendido, além de ter uma quantidade grande de código repetido. Adicione a esse problema vários consumidores acessando uma base de dados, ou várias, e teremos um problema enorme em nossas mãos.

Diagrama de vários consumidores em uma
mesma aplicação base

Figura 1. Diagrama de vários consumidores em uma mesma aplicação base

Agora, vamos analisar a Figura 2, que nos mostra como uma aplicação Multi-Tenant deve ser estruturada. Note que temos um único código base, baseado em funcionalidades. Essas funcionalidades irão definir tudo que a aplicação irá fazer para os inúmeros usuários (ou tenants). Então, temos o conceito de configuração, que aproveita um conjunto de uma ou mais funcionalidade para criar uma experiência única para o usuário. Note que teremos tantas configurações quanto tivermos usuários, e a adição e remoção de funcionalidades é muito simples em uma aplicação assim. Se quisermos adicionar uma nova, basta fazermos essa adição, alterando então as configurações específicas dos usuários que querem essa nova funcionalidade.

Diagrama da estrutura básica das
aplicações multi-tenant

Figura 2. Diagrama da estrutura básica das aplicações multi-tenant

Esse tipo de abordagem trará algumas vantagens que são, de certa forma, muito simples. A instalação da aplicação para diferentes usuários torna-se um simples caso de preparar uma configuração e criar um domínio para aquele novo usuário. Nesse caso, a autenticação será realmente importante para definir quem é quem dentro da aplicação, para evitar que a malícia de alguns se sobressaia à segurança do software criado. Outro vantagem é a correção de erros, que torna-se mais simples. Um erro corrigido em uma funcionalidade será automaticamente transferido para todos os usuários que fazem uso dela. A manutenção da infraestrutura também tende a ser beneficiada, embora isso não dependa exclusivamente do desenvolvimento multi-tenant – além do fato de que isso não é tão importante com o uso massivo de plataformas na nuvem, como o Windows Azure.

Em poucas palavras, podemos ver que as aplicações Multi-Tenant podem ser uma ferramenta de muita validade para a criação de aplicações web utilizadas por múltiplos usuários. Como vamos ver ao longo do artigo, elas trazem uma ideia de muito fácil manutenção com relação à forma como os usuários acessam as funcionalidades e criam suas experiências individuais. Entretanto, é preciso atenção ao design da aplicação, uma vez que falhas nesse âmbito podem criar muito mais problemas do que resolvem para nós.

Entendendo a arquitetura de dados Multi-Tenant

A estratégia de implementação das aplicações Multi-Tenant é uma parte tão importante quando a implementação em si. A ideia é que tenhamos uma aplicação capaz de, de forma flexível, atender os requisitos de vários consumidores. Em outras palavras, devemos criar uma aplicação capaz de suportar diversas diferentes implementações. É claro que essa estratégia irá variar de acordo com o nosso alvo, uma vez que uma solução para milhares de consumidores com pouca customização é diferente de uma para poucos consumidores com instâncias quase que completamente diferentes.

Uma das partes mais importantes da estratégia de desenvolvimento diz respeito à forma como os dados são utilizados pelos usuários. Precisamos definir se cada um deles precisará de uma base de dados diferentes, com um esquema diferente, ou se um único esquema é suficiente. Além disso, é preciso definir se a instância da base de dados que os usuários estarão acessando será diferente. Para o último caso, normalmente a resposta é sim, uma vez que vários usuários acessando uma mesma instância pode gerar atrasos dentro da aplicação. Já o esquema da base de dados pode ou não variar, dependendo da aplicação que estamos construindo.

Mas como vamos implementar esse tipo de coisa? Vamos começar analisando o que precisamos garantir:

  1. Isolamento dos dados dos tenants (usuários): um usuário não pode, sob hipótese alguma, ter acesso aos dados dos demais;
  2. Solução extensível: o modelo da aplicação precisa ser extensível, de forma que o usuário possa ter suas próprias customizações;
  3. Solução escalável: a mesma solução que funciona para três usuários, por exemplo, precisa funcionar para um milhão.

Baseando-nos nesses três pontos, vamos criar uma arquitetura de dados para uma aplicação multi-tenant. O mais comum em aplicações ASP.NET é a utilização do Windows Azure como servidor, e isso nos permite algumas configurações até certo ponto fáceis para nossa aplicação. Essas configurações referem-se a esquemas de particionamento da base de dados para os múltiplos usuários da aplicação, evitando que um tenha acesso ao que é do outro e vice-versa. Cada uma dessas opções é interessante para determinados casos, como podemos notar na Tabela 1.

Tabela 1. Esquemas de particionamento dos dados

Esquema de particionamento

Descrição

Uma subscrição por usuário

Cada usuário (tenant) possui uma subscrição para armazenamento de dados.

Isso faz com que seja fácil separar as necessidades de cada um dos usuários. Entretanto, pode trazer problemas de atraso devido às diferentes localizações das bases de dados de cada usuário.

Agrupamento de usuários por subscrição

Cada subscrição para armazenamento de dados corresponde a um grupo de usuários.

Interessante em casos em que temos grupos de acesso à aplicação (como usuários “simples” e “avançados”), pois é mais fácil de entender os custos de armazenamento. Entretanto, ainda é preciso particionar os dados dentro de cada grupo de acesso.


A Tabela 1 mostra o particionamento que pode ser feito de acordo com o Windows Azure. Existem outros tipos que se encaixam melhor em casos específicos, mas ficaremos nesses dois por enquanto. Note que temos uma arquitetura clara de dados, dependente da aplicação que estamos criando. Nesse sentido, não adianta trazermos uma “receita de bolo” aqui. É preciso que o leitor entenda que cada caso é um caso e é preciso uma análise mais profunda antes da implementação de qualquer arquitetura multi-tenant.

Com isso, temos uma ideia a respeito do particionamento dos dados. Agora, precisamos entender como fazer para garantir a extensibilidade de nossas aplicações Multi-tenant. Para isso, precisamos entender as três formas que temos para garantir esse tipo de comportamento em nossas aplicações, que são:

  1. Tabelas separadas por usuário;
  2. Única tabela com esquemas múltiplos (um por usuário);
  3. Esquema comum com tabelas customizadas complementares por usuário.

A primeira dessas abordagens permite que cada tabela possua esquemas únicos para os usuários, algo como as tabelas “Usuario1Pessoa” e “Usuario2Pessoa”, mostradas na Figura 3. Essa abordagem pode ser interessantes para sistemas menores, com poucos usuários, mas acaba necessitando muito espaço para armazenamento dos esquemas caso o sistema aumente muito sua utilização.

Tabelas separadas por usuário

Figura 3. Tabelas separadas por usuário

Já a segunda abordagem, mostrada na Figura 4, permite que tenhamos uma única tabela que aceita diferentes esquemas, de acordo com o usuário que a está utilizando. Essa abordagem é mais organizada do ponto de vista do sistema como um todo, uma vez que há apenas uma tabela de cada tipo, ainda que tenhamos vários esquemas para elas.

Única
tabela com esquemas múltiplos

Figura 4. Única tabela com esquemas múltiplos

Já a terceira e última abordagem permite que haja uma tabela com campos em comum a todos os usuários. Então, cada usuário terá seus próprios campos customizados, como uma extensão à primeira. Essa abordagem é a melhor para a maior parte dos casos, uma vez que não serão todos os usuários que utilizarão um esquema customizado, fazendo com que o esquema padrão cubra esses casos mais simples. Esses esquema está mostrado na Figura 5.

Esquema
comum com tabelas customizadas complementares

Figura 5. Esquema comum com tabelas customizadas complementares

A escalabilidade da aplicação é outro ponto que merece destaque. Como não sabemos de antemão (na grande parte das vezes) quantos usuários teremos, precisamos criar uma aplicação que tenha uma solução, ao menos, funcional com 1 ou vários usuários. A ideia é que a aplicação seja capaz de ser expandida sem prejudicar o funcionamento da mesma, de preferência simplesmente alocando mais recursos para utilização. Esse é o terceiro ponto essencial referente a SaaS (Software as a Service) ou, mais especificamente, às aplicações Multi-Tenant.

É interessante notarmos que as três áreas essenciais das aplicações multi-tenant são interdependentes. Isso significa que a escolha de uma ação para garantir a extensibilidade pode prejudicar a escalabilidade da aplicação. Além disso, conforme temos mais usuários, podemos ter atrasos de utilização do sistema, entre outros problemas. Por isso o planejamento é tão ou mais importante em aplicações desse tipo do que em outras aplicações. A ideia é que tenhamos esses três elementos bem organizados, e iremos mostrar como fazer isso a seguir.

É complicado falarmos em escalabilidade de forma individual. Essa, em especial, é uma área muito dependente da forma como armazenaremos os dados. Por exemplo, se utilizarmos um banco de dados SQL, teremos um conceito conhecido como “Federação” para nos auxiliar no particionamento dos dados. Esse conceito auxilia na hora de particionar múltiplas bases de dados horizontalmente. O particionamento horizontal (também conhecido como sharding), por sua vez, implica em pegar alguns dos dados de uma tabela e movê-los para uma nova tabela. No caso da utilização da “Federação”, é interessante utilizarmos o identificador (ID) do usuário na chave primária para atingirmos a escalabilidade que desejamos em nossa aplicação.

Preparando uma aplicação multi-tenant: exemplo prático

Como um exemplo básico, vamos criar uma aplicação multi-tenant com ASP.NET MVC. Note que a escolha da tecnologia é muito importante para uma aplicação desse tipo e, atualmente, o ASP.NET MVC traz uma excelente equação entre facilidade de manutenção, desempenho e facilidade de desenvolvimento. Além do mais, fornece extrema facilidade de trabalho com bases de dados, com a utilização de tecnologias como o LINQ, Entity Framework ou NHibernate, por exemplo. Sem contar que a utilização de IoC (Inversion of Control) se dá de forma simples, o que é importante para as aplicações multi-tenant. Outra vantagem é a proximidade com o Windows Azure, que me parece a ferramenta mais completa nesse tipo de operações. O conceito de IaaS (Infrastructure as a Service) utilizado pela plataforma é importante, pois permite que a escalabilidade, especialmente, seja atingida mais facilmente.

Vamos começar o desenvolvimento, portanto. O primeiro passo é a criação de um projeto novo de “Web Application”. Nesse exemplo, vamos criar um projeto com o template “Empty”, como mostra a Figura 6. Note que estamos escolhendo a criação dos diretórios padrão para projetos MVC, que são importantes para nós nesse caso.

Criação
do projeto com template Empty

Figura 6. Criação do projeto com template Empty

O próximo passo é a configuração de algumas propriedades do projeto para que ele possa estar preparado para uma aplicação multi-tenant. Nas propriedades do projeto (botão direito sobre ele -> Properties, ou simplesmente Alt + Enter), temos a opção Web, que permite a escolha de uma ação de inicialização do site, bem como outros detalhes como servidores e depuradores de código. Nesse caso, estaremos alterando a ação de inicialização (Start Action) como mostra a Figura 7. Repare que estamos indicando que não queremos abrir nenhuma página durante a inicialização da aplicação. Isso é essencial, uma vez que estaremos esperando uma requisição de um usuário para então abrir o website específico dele.

Ação
de inicialização

Figura 7. Ação de inicialização

O próximo é a criação de nossa camada de dados. Note que, como não estaremos criando uma aplicação complexa, apenas estaremos criando as diferentes views para os usuários. Para isso, vamos utilizar uma tabela Tenant, com informações a respeito dos usuários. A ideia é que as views sejam carregadas de acordo com as informações desse Tenant, como Id, Nome, Host e Título. Note que as informações são importantes para fazermos a importação dos dados conforme o usuário que estará logando na aplicação. Na questão do login não estaremos implantando segurança por estar fora do escopo do artigo. Esses dados armazenados serão recuperados pela aplicação durante a inicialização, como iremos definir posteriormente – e o motivo para termos tomado a atitude mostrada na Figura 7.

Vamos começar criando uma nova base de dados, utilizando o Data Source “Microsoft SQL Server Database File”. Para realizar essa criação, vamos abrir o “Server Explorer” do Visual Studio e adicionar uma nova conexão. Então, adicionamos a base de dados do SQL Server em qualquer lugar que desejamos. Com isso, teremos nossa conexão aparecendo no Server Explorer. A partir daí, basta que criemos a tabela Tenant com as informações que discutimos anteriormente. O código para criação da tabela está mostrado na Listagem 1. Depois da criação, vamos criar dois usuários com dados fictícios, para termos uma variedade no que veremos a seguir.

Listagem 1. Código T-SQL para criação da tabela Tenant

CREATE TABLE [dbo].[Tenant]
  (
  [Id] INT NOT NULL PRIMARY KEY, 
   [Nome] NVARCHAR(50) NOT NULL, 
   [Host] NVARCHAR(50) NOT NULL, 
   [Titulo] NVARCHAR(50) NOT NULL
  )

Na sequência, estaremos realizando a criação das classes de acesso à essa base de dados que acabamos de criar. Para isso, faremos uso do LINQ to SQL, por sua facilidade de utilização. Em alguns casos de aplicações Multi-Tenant ele pode não ser a melhor solução, entretanto, uma vez que, para qualquer alteração na base de dados, é necessário que haja uma alteração completa no mapeamento dos dados nas classes do LINQ to SQL, o que pode dar muito trabalho.

As classes serão criadas em um diretório recém criado: Dados. Esse diretório receberá as classes de dados, apenas. A criação das classes do LINQ to SQL (no caso desse exemplo, classe) pode ser realizada através de um diagrama de classes. Para isso, basta que adicionemos um novo item, “LINQ to SQL Classes”, como mostra a Figura 8. Essa criação abrirá o Object Relational Designer, ou Designer Objeto-Relacional, que permite que, apenas arrastando a tabela da base de dados presente no Server Explorer nós a adicionemos ao projeto. O resultado dessa ação está mostrado na Figura 9.

Criando
classe LINQ to SQL

Figura 8. Criando classe LINQ to SQL

Resultado
do Object-Relational Designer

Figura 9. Resultado do Object-Relational Designer

Agora precisamos partir para a criação do provedor de dados. Como isso é um item que diz respeito ao acesso aos dados, não darei muito atenção a ele aqui. A ideia é que tenhamos uma interface que defina os métodos de acesso aos dados e que essa interface seja implementada, como mostra a Listagem 2. Repare que o único método sendo implementado é o GetTenant(), que irá buscar os dados da base de dados e utilizá-los para abrir a página referente ao usuário em questão. Ele estará buscando e retornando o objeto Tenant (usuário) referente ao host passado como parâmetro do método. A parte que retira o número da porta do host é importante pois, na base de dados, teremos apenas o nome da URL, sem a porta de acesso ao servidor, o que nem sempre é o caso no browser.

Listagem 2. Método GetTenant()

public Tenant GetTenant(string host)
  {
   if (string.IsNullOrEmpty(host))
   {
   throw new ArgumentNullException("host");
   }
   // Retira o número da porta do host, se existir
   int index = host.LastIndexOf(':');
   if (index > 0)
   {
   host = host.Substring(0, index);
   }
  
   DataClassesDataContext dados = new DataClassesDataContext();
   Tenant ret = (from tenant in dados.Tenants where tenant.Host == host select tenant).SingleOrDefault();
   return ret;
  }

O próximo passo é a criação de um modelo para as páginas. O ASP.NET MVC traz uma ideia de views fortemente tipadas (que possuem um model), e é isso que utilizaremos. Vamos criar um model para passarmos os dados para a view Index, nossa tela principal. Esse model, ModelPagina, trará apenas dados de Nome e Título da página.

Agora, vamos à parte mais importante, que é a criação do HomeController e da View Index. Esses dois elementos em conjunto vão controlar a parte visual da aplicação, como veremos a seguir.

O HomeController, através de um método HttpGet, como mostrado na Listagem 3, buscará o usuário através do “Host” passado no cabeçalho da requisição, ou seja, o host passado na URL do navegador. É através dele que o objeto Tenant será buscado e passará os dados para a view Index.

Listagem 3. Action Method Index() de HomeController

[HttpGet]
  public ActionResult Index()
  {
   string host = this.Request.Headers["Host"].ToLower();
   ITenantDataProvider provider = new TenantDataProvider();
  
   Tenant tenant = provider.GetTenant(host);
   ModelPagina pag = new ModelPagina()
   {
   Nome = "Inicial",
   Titulo = "Site sem Tenants"
   };
   if (tenant != null)
   {
   pag.Nome = tenant.Nome;
   pag.Titulo = tenant.Titulo;
   }
  
   return View(pag);
  }

A criação da View Index, por sua vez, é bastante simples, como mostra a Figura 10. Estamos utilizando um modelo, conforme comentamos (ModelPagina) para criar uma view fortemente tipada. Essa view irá simplesmente mostrar os dados de Nome e Titulo que criamos em nosso modelo. A grande sacada é o fato de a view ser alterada no código do Controller. Isso é feito quando o método GetTenant() é executado. Ele irá buscar na base de dados o Tenant referente ao Host executado no browser, conforme a URL digitada. Essa URL é dependente do que foi digitado mas, para fins de teste, vamos utilizar o localhost. Esse localhost é a URL que corresponde ao Tenant1 em nossa base de dados, e o resultado dessa chamada pode ser visto na Figura 11. Vale notar que poderíamos escolher a URL que desejássemos nesse caso, mas para isso precisaríamos alterar algumas configurações internas do servidor IIS.

Criação
da View Index

Figura 10. Criação da View Index

Resultado
para o Tenant1

Figura 11. Resultado para o Tenant1

Com isso, podemos ter uma noção do que são as aplicações multi-tenant e como podemos utilizá-las. Entretanto, é necessário cuidado, uma vez que existem vários elementos que podem ser complicadores, dependendo do design que fazemos para nossa aplicação. O ideal é que muitos testes sejam realizados antes da liberação para os usuários.