O multi-tenancy é uma evolução natural para boa parte das aplicações. Startups e seus produtos precisam dele para seu modelo SaaS, empresas e seus sistemas o querem para o reuso de código. Essa palestra irá mostrar técnicas e modelos para a aplicação de multi-tenancy, e erros comuns no processo.
2. Sobre o palestrante
Co-fundador e CTO da LQDI Digital
Projetos para empresas como Porto Seguro, Nestlé, Garoto,
Editora FTD, Tishman Speyer e Ambev
12 anos trabalhando com PHP
Desde a época que o PHPClasses era a onda :)
Aryel Tupinambá
3. Single tenant
- Um único "cliente" para a
aplicação
- Forma tradicional, instalada no
servidor do cliente
- Faz sentido quando a aplicação
é um PROJETO
4. Agora com mais um cliente… e mais outro...
- Seu PROJETO se tornou um
PRODUTO
- Diversos clientes usando a mesma
aplicação
- Estruturas separadas para cada novo
cliente
- Nada é compartilhado; tudo é isolado
5. E agora, como faço...
… atualizações de segurança?
… correção de bugs?
… features novas para todo mundo?
E o espaço que toda essa galera ocupa?
E o custo que tudo isso gera?
6. Arquitetura multi-tenant
- Tenant = inquilino
- Uma única aplicação para vários
clientes
- Uma estrutura comum entre os clientes
- As benfeitorias servem para todos
- A customização de um cliente não
interfere nos demais, nem previne que
ele utilize da estrutura comum
7. Principais desafios
DATA STORAGE CODEBASE CUSTOMIZAÇÃO
Como segmentar a
base de dados para
cada cliente?
Como segmentar os
arquivos enviados?
Como fica o código
fonte da aplicação?
Como ficam as
customizações?
Como lidamos com
updates e novas
features?
Como podemos
customizar as
funcionalidades da
aplicação?
Como permitir que
atualizações e novas
features não sejam
impedidas por
customizações?
8. Não existe escolha certa ou errada
O que existe são escolhas que fazem mais sentido para determinados cenários.
10. Três formatos mais comuns
Data storage
Instâncias separadas
Bancos de dados separadas
Banco de dados compartilhado
Cada cliente possui uma instância do seu SGDB (MySQL,
Postgres, Mongo, etc), com seus dados isolados.
Mais isolado
Mais compartilhado
Cada cliente tem um banco de dados (ou schema) diferente,
na mesma instância
Todos os clientes estão na mesma base de dados; o cliente é
identificado por meio de uma coluna nas tabelas (tenantID)
Referência / diagramas: Microsoft - https://msdn.microsoft.com/en-us/library/aa479086.aspx
11. Três formatos mais comuns
Data storage
Instâncias separadas
Bancos de dados separadas
Banco de dados compartilhado
Mais isolado
Mais compartilhado
Todos os clientes estão na mesma base de dados; o cliente é
identificado por meio de uma coluna nas tabelas (tenantID)
Referência / diagramas: Microsoft - https://msdn.microsoft.com/en-us/library/aa479086.aspx
12. Três formatos mais comuns
Data storage
Instâncias separadas
Bancos de dados separadas
Banco de dados compartilhado
Mais isolado
Mais compartilhado
Cada cliente tem um banco de dados (ou schema) diferente,
na mesma instância
Referência / diagramas: Microsoft - https://msdn.microsoft.com/en-us/library/aa479086.aspx
13. Três formatos mais comuns
Data storage
Instâncias separadas
Bancos de dados separadas
Banco de dados compartilhado
Mais isolado
Mais compartilhado
Cada cliente possui uma instância do seu SGDB (MySQL,
Postgres, Mongo, etc), com seus dados isolados.
Referência / diagramas: Microsoft - https://msdn.microsoft.com/en-us/library/aa479086.aspx
16. Três formatos mais comuns
Data storage
Instâncias separadas
Bancos de dados separadas
Banco de dados compartilhado
Sugestão de uso:
- Aplicações com pouca ou nenhuma variabilidade /
customização
- Aplicações em que a principal customização é visual
- Aplicações com alto número de usuários (500k+)
Mais isolado
Mais compartilhado
17. Três formatos mais comuns
Data storage
Instâncias separadas
Bancos de dados separadas
Banco de dados compartilhado
Sugestão de uso:
- Aplicações em que deve haver maior flexibilidade para
customização
- Aplicações em que a infra-estrutura sempre será
responsabilidade sua
- Planos baseados em volume de consumo ou
performance
Mais isolado
Mais compartilhado
18. Três formatos mais comuns
Data storage
Instâncias separadas
Bancos de dados separadas
Banco de dados compartilhado
Sugestão de uso:
- Projetos em que a separação física do hardware se faz
necessária (instalações on-premises, compliance
corporativo ou governamental, decisões de
performance);
- Alta disparidade no volume de dados ou de acessos
entre clientes (quando alguns clientes consomem
demais)
- Quando você precisa monitorar o consumo individual de
hardware para cada cliente (e seu SGDB não faz isso
por database/schema)
Mais isolado
Mais compartilhado
19. - Em regra geral, a FLEXIBILIDADE necessária deve ser o fator decisório
- Customizar funcionalidades pode ser um desafio quando você tem um modelo de
dados fixo; alterar o modelo de dados de toda uma aplicação só para atender um
cliente / uma feature pode gerar inconsistência e dificultar a manutenção
- Use o pattern de "migrations" e "seeds" para criar e manter o database schema;
muito importante criar migrations que sejam, na medida do possível, 100%
reversíveis.
- Você não necessariamente precisa escolhar um único modelo para todos os dados
da aplicação; se sua aplicação tem interações entre os clientes mas é altamente
customizável, por exemplo, faz sentido segmentar o que faz parte do core em um
único database, e o que faz parte da customização em um database separado
Data storage
25. Codebase
- Dois cenários: uma instância para todos, ou uma instância por tenant
app
código fonte
cliente1.app.com
cliente2.app.com
cliente3.app.com
config 1
config 2
config 3
cliente1.app.com
código fonte
config 1
cliente2.app.com
código fonte
config 2
cliente3.app.com
código fonte
config 3
custom code
Uma instância para todos
Uma instância por cliente
26. Codebase
- Uma instância para todos
- Atualizações são sincronizadas sem custo ou esforço
- Nenhum ou pouquíssimo "housekeeping" necessário
- Não é muito flexível para customizações
- Mais fácil de escalar quando o padrão de consumo de todos os clientes é semelhante
app
código fonte
cliente1.app.com
cliente2.app.com
bigcorp.app.com
config 1
config 2
config 3
27. Codebase
- Uma instância para todos
- Atualizações são sincronizadas sem custo ou esforço
- Nenhum ou pouquíssimo "housekeeping" necessário
- Não é muito flexível para customizações
- Mais fácil de escalar quando o padrão de consumo de todos os clientes é semelhante
app
código fonte
cliente1.app.com
cliente2.app.com
bigcorp.app.com
config 1
config 2
config 3
Nginx sitesvia DB, cachedGit repository
28. Codebase
- Uma instância por cliente
- Atualizações não são sincronizadas, exceto se automatizadas
- Necessário um trabalho de "housekeeping" para manutenção das instâncias
- Bastante flexível para customizações
- Qualquer cliente pode ter sua versão da aplicação "congelada"
- O código do próprio "core" da aplicação pode ser forkado e alterado a qualquer momento (se em
algum momento isso fizer sentido para o negócio)
bigcorp.app.com
código fonte
config 1
cliente1.app.com
código fonte
config 2
cliente2.app.com
código fonte
config 3
custom code
29. Codebase
- Uma instância por cliente
- Atualizações não são sincronizadas, exceto se automatizadas
- Necessário um trabalho de "housekeeping" para manutenção das instâncias
- Bastante flexível para customizações
- Qualquer cliente pode ter sua versão da aplicação "congelada"
- O código do próprio "core" da aplicação pode ser forkado e alterado a qualquer momento (se em
algum momento isso fizer sentido para o negócio)
bigcorp.app.com
código fonte
config 1
cliente1.app.com
código fonte
config 2
cliente2.app.com
código fonte
config 3
Git repo
.env files
custom code
30. Codebase
- Uma instância por cliente
- Atualizações não são sincronizadas, exceto se automatizadas
- Necessário um trabalho de "housekeeping" para manutenção das instâncias
- Bastante flexível para customizações
- Qualquer cliente pode ter sua versão da aplicação "congelada"
- O código do próprio "core" da aplicação pode ser forkado e alterado a qualquer momento (se em
algum momento isso fizer sentido para o negócio)
bigcorp.app.com
código fonte
config 1
cliente1.app.com
código fonte
config 2
cliente2.app.com
código fonte
config 3
Nginx sites + PHP-FPM pools
v1.4.0 v1.4.1 v1.4.0
custom code
31. Codebase
- De novo, o maior fator decisório aqui deve ser a FLEXIBILIDADE: o quanto você
espera que o código da aplicação se altere com customizações de clientes?
- A escalabilidade, embora seja um desafio (e mereceria uma palestra a parte), não
deve ser dificultada em nenhum dos cenários, DESDE QUE você automatize
todos os processos e fique de olho na consistência
- Instâncias isoladas permitem que você escale clientes mais "barulhentos" (alto
volume) para hardware melhor
- Você pode também trabalhar com instâncias segmentadas por tier ou plano, e
"empacotar" menos clientes por instâncua em tiers mais elevados/caros.
33. Variabilidade e customização
- O calcanhar de aquiles da arquitetura multi-tenant
- Requer bastante meditação sobre os requisitos
- Aqui é um dos melhores lugares onde design patterns e uma arquitetura sã e
consistente brilham de verdade
- Vou falar sobre algumas técnicas e formatos interessantes
35. Variabilidade e customização
- Para configuração:
- Para instâncias de código isoladas: um arquivo de configuração (biblioteca DotEnv)
- Para instâncias unificadas: parâmetros de configuração no banco/repositório de tenant data
.env
36. Variabilidade e customização
- Para o data storage:
- Migrations: funcionam como um controle
de versão do seu modelo de dados
- A maioria dos frameworks/ORMs possuem
suporte built-in (Laravel, CakePHP,
Doctrine), e há libs standalone para outros
formatos.
- Sempre criar migrations "reversíveis", ou
seja, que possam sofrer rollback
37. Variabilidade e customização
- Para o código / funcionalidade: EVENTS
Transação de negócio
(ex: novo pedido realizado)
Gerenciador de eventos
(via Framework, biblioteca ou
hand-made)
Envio de e-
mail para o
usuário
Registro no log
de atividade do
usuário
Sub-serviços do core app
Módulo customizado: NF-E
Geração de NF-E
Módulo customizado: Tracking
Registrar nova carga à entregar
dispatchEvent('order_created', $order);
38. - Para o código / funcionalidade: EVENTS
Variabilidade e customização
Transação de negócio
(ex: novo pedido realizado)
Gerenciador de eventos
(via Framework, biblioteca ou
hand-made)
Envio de e-
mail para o
usuário
Registro no log
de atividade do
usuário
Sub-serviços do core app
Módulo customizado: NF-E
Geração de NF-E
Módulo customizado: Tracking
Registrar nova carga à entregar
dispatchEvent($order);
CORE APP
CUSTOM CODE
39. return [
['Entregas','abc.delivery.index'],
['Configurar Frete','abc.delivery.config']
];
- Para o código / funcionalidade: HOOKS
Variabilidade e customização
Renderizar menu principal Gerenciador de hookstriggerHook('render_menu');
return [['Pedidos','core.orders.index']];
return [['Estoque','core.inventory.index']];
Módulo de Pedidos
Módulo de Estoque
Módulos do core app
Módulo de Entregas
Custom code do cliente ABC
return [...];
40. Variabilidade e customização
- Para o código / funcionalidade: DEPENDENCY INJECTION
Order
BillingGatewayContract
PayPalBillingGateway
implementa
DI Container
registra
recebe PayPalBillingGateway
pede BillingGatewayContract
41. Variabilidade e customização
- Para o código / funcionalidade: DEPENDENCY INJECTION
Order
BillingGatewayContract
PayPalBillingGateway
Core app
Order
BillingGatewayContract
PayPalBillingGateway
Core app
Order
BillingGatewayContract
PayPalBillingGateway
Core app
TENANT 1 TENANT 2 TENANT 3
42. PagSeguroBillingGateway
Variabilidade e customização
- Para o código / funcionalidade: DEPENDENCY INJECTION
Order
BillingGatewayContract
PayPalBillingGateway
Core app
Order
BillingGatewayContract
PayPalBillingGateway
Core app
Order
BillingGatewayContract
PayPalBillingGateway
Core app
TENANT 1 TENANT 2 TENANT 3
Custom code
43. Variabilidade e customização
- Para o frontend / interface:
- Dê preferência para usar uma engine de templates (Blade, Twig, etc);
44. Variabilidade e customização
- Para o frontend / interface:
- Use Sass ou outro pré-compilador de CSS
- Você pode gerar o config via PHP, e então rodar
o compilador (automático ou manual)
47. Variabilidade e customização
- Para infraestrutura / deploy:
http://forge.laravel.com
http://deployhq.com
http://envoyer.io
Use ferramentas ou serviços de automatização de deploy
(imagine atualizar manualmente a versão de 600 clientes)
48. Variabilidade e customização
- Para infraestrutura / deploy:
Use o Composer para gerenciar core
app e código customizável
49. Variabilidade e customização
- Para infraestrutura / deploy:
Automatize (se aplicável) o processo de provisionamento para novos clientes
Novo cliente cadastrado
Script / microservice de
PROVISIONAMENTO
Criar pasta no servidor e site no Nginx
Criar banco de dados
Gerar .env de configuração
Criar registro no DB de tenants
Cadastrar no Envoyer e realizar primeiro deploy
Gerar composer.json com Core + Custom code
Disparar e-mail para o cliente
Gerar config do Sass e compilar CSS final
50. Variabilidade e customização
- Para infraestrutura / deploy:
Automatize (se aplicável) o processo de provisionamento para novos clientes
Novo cliente cadastrado
Script / microservice de
PROVISIONAMENTO
Criar pasta no servidor e site no Nginx
Criar banco de dados
Gerar .env de configuração
Criar registro no DB de tenants
Cadastrar no Envoyer e realizar primeiro deploy
Gerar composer.json com Core + Custom code
Disparar e-mail para o cliente
Gerar config do Sass e compilar CSS final
51. Variabilidade e customização
- Para infraestrutura / deploy:
- O custo e tempo para provisionamento interfere diretamente em um KPI de negócio,
o CAC (Custo de Aquisição de Cliente)
- Provavelmente toda sua infra-estrutura pode ser administrada via API; use isso a seu favor
- Amazon EC2 e DigitalOcean tem APIs para provisionamento de servidores
- Amazon S3 e Rackspace Cloud tem APIs para criação de novos "buckets" de arquivos
- Amazon Route 51 e DigitalOcean tem APIs para gerenciar o DNS (domínios customizáveis)
52. TL;DR
- Oriente sua arquitetura principalmente pela sua
necessidade de flexibilidade e customização
- Mais customizável -> mais isolada
- Mais uniforme -> mais compartilhada
- Use as melhores práticas de SOLID, principalmente no
que diz respeito a Inversão de Dependência; a
arquitetura multitenant é provavelmente o melhor use-
case de substituição de implementações de uma
interface em runtime
53. TL;DR
- Automatize TUDO que diz respeito a provisionamento,
deploy e housekeeping de infraestrutura: manutenção
manual de instâncias é uma eterna dívida técnica que se
acumula
- TDD é PRIMORDIAL; lembre-se que em um ambiente
sincronizado, uma atualização impacta TODOS os seus
clientes
55. A está contratando!
Procuramos desenvolvedores front-end e back-end, de TODOS os níveis de experiência,
apaixonados pelo que fazem e a fim de aprender e ensinar
Ambiente de trabalho bacana, descontraído, com remuneração competitiva, horários
flexíveis e bastante abertura para novas idéias. Sem melindres, sem preciosismo e
puxação de saco :D
Plano de carreira sólido e flexível, com espaço para crescimento em gestão e
especialização, e programas de feedback contínuo entre a equipe e os gestores.
Manda um e-mail pra work@lqdi.net e cite a palestra da PHP Conference
Ou me chame pessoalmente agora para trocarmos uma idéia
Começe 2016 de trampo novo!
Boa tarde, pessoal! Vamos começar?Primeiramente eu gostaria de agradecer a presença de todos vocês, e o pessoal da Tempo Real que está organizando esse evento. A PHPConf tá completando 10 anos, e é realmente notável a diferença que o engajamento da comunidade tem feito na qualidade e experiência dos desenvolvedores no Brasil. Como alguém que está sempre entrevistando e contratando gente, eu vi o nível da galera subir muito nos últimos anos, e atribuo muito disso ao engajamento e espírito de comunidade da galera do PHP. É isso aí, continuem vindo nos eventos, aprendam, repassem o que aprenderam, voltem como palestrantes, ajudem a disseminar o conhecimento que todos saímos ganhando. O mercado é gigantesco, nenhum dev bom fica sem emprego, então a gente tem motivo de sobra pra ajudar todo mundo.
Vou falar bem rapidamente de mim. Meu nome é Aryel Tupinambá, sou fundador e CTO da Liquidi, uma agência com bastante foco em tecnologia. Esse aí na foto sou eu, fingindo ser um cara sério e normal, mas é tudo fachada! A gente aqui sabe que não existe desenvolvedor que seja normal, né? Eu trabalho com PHP a mais ou menos 12 anos, desde aquela época em que o PHPClasses era a onda :)
Desde aquela época, até começar essa onda de startups e software como servico, esse aqui era o padrão de projetos web. O software tinha um único cliente, que era instalado no servidor do cliente, ou em algum servidor compartilhado, e rodava lá sozinho. Esse é um modelo que chamamos de "single tenant", ou "um único cliente". A gente compara esse modelo com uma casa, solitária e ocupando todo o terreno sozinha.
Só que aí, quando tudo dava certo e sua equipe comercial começava a bombar, o mesmo projeto que você fez pra um cliente, você começa a fazer pra outro, e mais outro, e mais outro. Seu projeto virou um produto, com vários clientes diferentes, mas sua estrutura continua a mesma. Pra cada cliente você tem uma estrutura diferente, com um código que pode até ser igual, mas que está replicado integralmente em cada um dos clientes. Aqui, a gente compara o modelo com um bairro. Tem várias casas, com vários clientes, mas tá todo mundo em seu terreninho, isolado do resto, cuidando do próprio umbigo. E todo esse isolamento, toda essa independência gera uma série de problemas.
Preciso fazer uma atualização de segurança, ou preciso corrigir um bug. E agora? Vou sair copiando e colando a atualização na pastinha de cada um dos clientes? Vou entrando no FTP, no SSH, cliente por cliente, e atualizando. E se eu customizei a instalação desse produto pra alguém? E se eu quiser pegar a customização de um e replicar pra outro, que tem outra customização? E esse espaço todo que essa galera tá ocupando? E o mais importante pro negócio, e o custo que tudo isso gera? Se eu tenho um custo que aumenta tão rápido conforme eu ganho novos clientes, como que o negócio vai crescer?
Aí que entra a arquitetura multi-tenant. Tenant é a palavra em inglês para inquilino. O conceito primário dela é que a gente usa uma única aplicação para vários clientes, e que esses clientes compartilham de uma estrutura comum. É igual a um prédio ou um condomínio, por que tá todo mundo compartilhando do mesmo terreno, o mesmo prédio. Daí que veio o termo "multi-inquilino". As benfeitorias servem pra todos, e o produto base é igual pra todos. E, igual a um apartamento, nada impede de um inquilino fazer uma "reforminha" no apê dele; ele vai estar com o mesmo apê que os outros, mas com uma ou outra coisa diferente. Parece ótimo né? Os olhinhos até brilham, tanto pros devs quanto pra empresa.
Mas naturalmente, uma arquitetura dessas traz vários desafios na hora de desenhar a aplicação. Eu separei ele em 3 pilares: data storage, que seria banco de dados e arquivos de cliente; codebase, que seria o código do aplicativo em si, back-end e front-end; e customização. E aí ficamos com algumas perguntas: no data storage, como que a gente segmenta a base de dados pra cada cliente? E os arquivos enviados? No code base, como fica o código fonte e as customizações? Como que a gente lida com updates e features novas? Na customização, como que a gente vai customizar? Como que a gente atualiza o produto base sem quebrar as customizações, ou vice versa?
Primeira coisa: não existe resposta certa, nem resposta errada. Pra cada uma dessas perguntas existe uma infinidade de respostas, assim como praticamente qualquer decisão na área de engenharia. Eu vou compartilhar aqui um pouco da minha experiência com projetos desse tipo, lá na Liquidi e em outras empresas que eu trabalhei e dei consultoria. Pra algumas pessoas, pode ser que a palestra fique um pouco superficial, por que o assunto é bem extenso e eu optei por falar mais sobre a arquitetura e menos sobre a implementação. O objetivo aqui é que vocês levem esses conceitos pra casa pra estudar com mais detalhe, testar, experimentar e pesquisar mais, e ver onde e como aplicar isso nos projetos de vocês, okay?
Vamos lá, data storage.
A gente tem 3 formatos mais comuns de segmentar um banco de dados no modelo multi-tenant, indo de um modelo mais compartilhado e unificado, e indo para um modelo mais isolado e independente. Vamos ver de baixo pra cima.
Banco de dados compartilhado, basicamente o sistema roda todo em cima de um único banco de dados, e a gente segmenta os clientes por uma coluna com o ID do cliente. Nesse caso aqui, tem a coluna TenantID que especifica qual o cliente.
Banco de dados separados, a gente cria um database pra cada cliente, dentro da mesma instância. O cliente é identificado pela database que ele está usando.
E por fim, instâncias completamente separadas, com seus dados isolados. Esse formato é um pouco incomum, por causa do overhead de usar uma instância única pra cada cliente, mas faz sentido quando você tem altos requisitos de segurança e compliance.
Beleza, mas e agora, quando eu uso cada modelo?
A gente tem duas preocupações primárias na hora de segmentar o banco: custo de desenvolvimento e manutenção, e flexibilidade de extensão. Esse gráfico aqui mostra a progressão de custo sobre tempo das duas pontas do espectro. A gente vê aqui que o modelo Compartilhado tem um custo de desenvolvimento inicial maior, mas ganha no custo de manutenção futura. O modelo isolado é mais barato, mas encarece o processo de manutenção. A gente vai ver por que um pouco mais a frente. O custo por si só não deve ser o fator decisivo de um modelo ou outro.
Esse gráfico mostra a relevância de decisão entre os formatos. Ter um volume alto de clientes puxa a decisão mais próxima do modelo compartilhado, principalmente por motivos de custo. Puxando pro outro lado, a gente tem, primeiramente, o tamanho do banco por cliente e a quantidade de usuários por cliente. Isso por que escalar um banco de dados gigantesco e homogêneo vai se tornando um desafio progressivo conforme o volume por cliente vai aumentando. Quando você já sabe que esse volume vai ser alto, você já pode prever, num processo de sharding do banco por exemplo, que cada tenant ficaria em um shard. Naturalmente ficaria mais barato prever essa separação desde o início. Agora, o que eu considero o fator mais decisório é o último aqui da lista, que é o serviço agregado por cliente, ou seja, as customizações que um cliente vai ter na aplicação. Se você tá prevendo que seu produto vai ser um produto único, cujo comportamento nunca muda entre um ou outro cliente, o modelo compartilhado faz muito mais sentido. Se você está imaginando que alguns dos seus clientes vão querer acrescentar módulos ou alterar comportamentos dentro do seu produto, o modelo de isolamento é melhor, por que você fica livre pra alterar o modelo de dados, criar novas tabelas e colunas sem se preocupar em sujar os dados dos outros clientes que não tem essa mesma customização.
Então, sugestões de uso pra cada formato. O modelo compartilhado é ideal para aplicações em que pouca coisa, ou nada mesmo, vai mudar entre um cliente e outro. Esse modelo é o mais comum quando se fala de SaaS "para as massas", aquele tipo de aplicação que qualquer um assina num formulário web, coloca o número de cartão e sai usando. Geralmente essas aplicações tem um ticket médio baixo, e não faz muito sentido para o seu cliente customizar a experiência além daquilo que você previu desde o início.
O modelo de bancos separados é ideal pra quando você já está prevendo customizações e módulos adicionais. Aqui você fica livre para ter modelos de dados que variam entre clientes. Nesse modelo você consegue segmentar melhor também aqueles clientes que tem mais ou menos consumo, ou que tem níveis maiores ou menores. Você pode empacotar mais ou menos clientes em uma instância de banco dependendo do tier que ele contratou, ou mover clientes mais "barulhentos" para outras instâncias.
O modelo mais isolado, em que cada cliente tem uma instância isolada do banco, faz sentido quando você tem isso como pré-requisito de alguns clientes, como governos ou bancos. Aqui você consegue ter um controle bem maior da performance e do consumo de cada cliente.
Na regra geral, o que deve te motivar mais na decisão do modelo é a flexibilidade que você precisa para mexer no banco de dados. Fica muito difícil você customizar um software, colocar funcionalidades ou alterar fluxos quando o seu modelo de dados é praticamente intocável.
Uma coisa muito importante, principalmente quando você tá indo para um modelo mais isolado, é usar o modelo de migrations e seeds para criar e manter o seu modelo de dados. As migrations funcionam como um controle de versão do banco, e é o que vai permitir que você crie customizações para clientes sem quebrar a aplicação base.
Você não precisa ter um único modelo entre os 3, necessariamente. Dá pra fazer um híbrido entre um e outro, por exemplo, quando você tem interações entre clientes dentro da sua aplicação, mas em um cenário em que ela ainda é customizável. Nós tivemos esse cenário na LQDI na nossa ferramenta de gestão de projetos.
Lembrando de novo, gente, não tem resposta certa ou errada. Estude bem esses 3 modelos, veja o artigo que eu citei da Microsoft, avalie bem os requisitos do seu projeto e aí você consegue escolhar a mais adequada.
Na prática, como a gente poderia implementar essas divisões no código? Vou usar alguns exemplos usando Laravel, que é o framework que a gente mais usa na LQDI, e é super tranquilo de fazer. Outros frameworks e ORMs tem soluções parecidas, e você consegue usar esses componentes aqui apartados do Laravel se precisar.
No modelo compartilhado, a implementação mais fácil é criar uma query scope global, que condiciona as consultas baseadas no seu tenant ID. Aí você pode criar uma trait que aplica essa query scope global nos models que são multi-tenant. Nesse exemplo aqui, o Tenant ID vem de uma variável de ambiente, mas poderia vir de qualquer lugar; do subdomínio ou do domínio, de um campo na URL, de uma variável de sessão, e por aí vai.
No modelo isolado é ainda mais tranquilo: a gente cria duas conexões, sendo uma específica para os modelos multitenant, e aí usa essa conexão como padrão nos models que são multitenant. Na configuração daquele cliente, você tem tanto os dados do banco único ali em MASTER_DB, que tem os dados compartilhados, quanto os dados do cliente, ali em TENANT_DB.
Falando um pouco agora do codebase, do código fonte da aplicação.
A gente tem dois modelos mais comuns pra trabalhar: uma instância pra todo mundo, ou uma instância por cliente. Eu tô usando o termo instância aqui me referindo a instância do codebase, e não necessariamente uma instância de servidor. Dá pra notar que temos mais ou menos a mesma polarização que temos nos modelos de dados: uma opção mais compartilhada e uma opção mais isolada. Os pros e contras são basicamente os mesmos
No modelo de instância única, a gente tem algumas boas vantagens. As atualizações são sempre sincronizadas com todo mundo, já que todo mundo acessa o mesmo codebase. Também não tem muito housekeeping, muito trabalho de manutenção ou monitoramento pra cada cliente, uma vez que tá rodando tudo junto, e é um dos motivos pelo qual esse modelo fica mais barato ao longo do tempo.
Agora, naturalmente, esse modelo não é muito flexível pra customizar a experiência por cliente. Ele é um modelo legal quando você tem módulos pré-definidos que são opcionais, e aí pra cada cliente você ativa ou desativa os módulos relevantes. Uma customização mais específica para um cliente fica mais difícil.
Esse modelo também é bem fácil de escalar, principalmente se o padrão de consumo dos clientes é o mesmo. Dá pra colocar ele em um serviço Platform as a Service, tipo o Azure Websites ou o Amazon Elastic Beanstalk, e escalar ele só arrastando um slider no painel.
Na prática, o formato que a gente usa pra implementar esse modelo: o repositório de código é único, sincronizado via git. Cada cliente tem o seu config, que pode vir do banco mestre ou de um microservice e pode ficar cacheado na sessão, e aí a gente pode criar um site no nginx pra cada cliente, todos apontando para o mesmo lugar, que é o repositório do app. Sua aplicação fica responsável por rotear o domínio pro cliente correto, pegar os dados desse cliente e carregar a configuração dele.
O outro modelo é ter uma instância por cliente. A idéia aqui é que cada cliente tem sua pastinha separada, tendo exatamente o mesmo código fonte. Aqui a configuração dele pode estar na própria pasta, e essa pasta que vai conter qualquer código de customização específica de um cliente. As atualizações de código aqui não são sincronizadas por padrão, mas você pode, e deve, automatizar isso. Você acaba tendo um certo trabalho de housekeeping, já que cada cliente tem uma pasta com arquivos separados, e qualquer problema durante o deploy, execução ou instalação de customização pode exigir uma intervenção mais manual. O legal dele é que você fica livre pra tratar cada cliente de forma excepcional: fazer customizações, colocar e tirar funcionalidades, travar um cliente em uma versão do app ou colocá-lo pra rodar uma versão beta. Se em algum momento isso fizer sentido, você pode até criar um fork da sua aplicação pra atender um único cliente, como por exemplo em um projeto customizado pra um grande cliente.
Na prática, a gente pra cada pastinha um repositório git, sincronizado via um repositório central tipo Github ou Bitbucket. A configuração de cada ambiente fica mais fácil, podendo ser feita por exemplo em um arquivo DotEnv na raiz de cada cliente. E o código customizado entra, quando aplicável, individualmente na pasta do cliente
Nesse modelo é o mesmo esquema, cada cliente tem um site no Nginx, e aqui a gente pode até ter um pool separado do PHP-FPM por cliente, ganhando um certo nível de controle sobre a performance e carga de cada cliente em cima do servidor da aplicação.
Então, pro codebase o fator decisório principal também é a flexibilidade, até mais que no banco de dados. Você vai precisar de muita gambiarra pra ter o mesmo nível de flexibilidade do modelo isolado no modelo compartilhado, e dificilmente vai valer a pena.
A escalabilidade é um desafio pra maioria dos projetos multitenant, mas merece uma palestra a parte. A escolha do modelo, desde que você automatize bem os processos e tenha uma arquitetura consistente, não deve te impedir ou dificultar de escalar a aplicação.
Ter as instâncias isoladas te dá mais controle pra isolar clientes mais ou menos exigentes, e com mais ou menos volume. Igual ao caso do data storage, a gente pode segmentar as instâncias por tier ou plano do cliente, e colocar menos clientes por servidor nos tiers mais caros. Dá pra atingir o mesmo resultado com uma instância compartilhada, mas você tem um adicional de código e complexidade de ter que manter o controle de qual cliente está em qual servidor.
O terceiro desafio, e o mais, digamos, difícil de resolver, é o de Variabilidade.
É o real calcanhar de aquiles da arquitetura multitenant, e precisa de bastante atenção e estudo, pra não gerar uma dívida técnica desnecessária.
É nesse tipo de arquitetura que a gente vê os design patterns, os princípios do SOLID, realmente brilharem e te ajudarem pra caramba.
Vou falar aqui sobre algumas técnicas e formatos legais de trabalhar.
Lembrando de novo, não existe solução certa, ainda mais nesse quesito.
Pra configuração, nas instâncias de código isolado a gente pode ter um arquivo de configuração em cada instância. Eu sugiro aqui usar a biblioteca DotEnv, que permite você usar um arquivo tipo esse aqui, e carregar essas variáveis só chamando uma função no código. Esse arquivo fica no gitignore e você só gera ele quando for provisionar um novo cliente.
Na instância única, você vai ter mais ou menos a mesma configuração, só que dentro da tabela de tenants no banco principal. É legal, nesse formato, fazer um cache dessa informação, ou usar um banco de dados in-memory como o Redis, pois você provavelmente vai precisar consultar os dados a cada requisição.
Pro data storage, ressalto a importância das migrations. Elas funcionam como um controle de versão do seu banco, onde a cada alteração você roda o script que cria novas tabelas ou altera as existentes, e ao fazer uma instalação do zero, todas as migrations são rodadas na ordem certa. É importante aqui sempre criar migrations que sejam reversíveis, problemas acontecem e você vai precisar voltar versões do modelo de dados ou desfazer customizações em alguns casos.
A maioria dos frameworks modernos e ORM tem alguma forma de migration, e se não tiver, você pode usar as libs separadas do Laravel, que ficam no pacote Iluminate.
Pra código, pra customização de comportamento, a gente tem vários patterns legais de trabalhar.
Events. Aconteceu algum evento ou transação relevante na sua aplicação base, você dispara um evento, e passa os parâmetros dessa transação junto. Com esse evento, quem estiver escutando recebe os dados e pode fazer o que for necessário. Nesse exemplo aqui: criei um pedido novo e disparei o evento. Um módulo customizado de um cliente específico, por exemplo, estava escutando e gerou uma nota fiscal eletrônica desse pedido. Pra outro cliente, esse evento pode registrar uma carga que vai ser rastreada na logística. O legal é que você pode usar esse padrão dentro da sua própria aplicação base. Você pode ter atividades que não são relacionadas necessariamente o domínio original da transação, como por exemplo mandar um e-mail de aviso pro usuário ou registrar a atividade do cara num log.
Aqui é o código dividido em core app, que é o código que todos os clientes tão rodando, e o custom code. O custom code não precisou mexer em nenhum código ou arquivo do core app pra poder tomar ações quando a pedido foi criado.
Hooks:
são tipo eventos, só que tem um certo retorno
o Wordpress usa bem esse modelo nos plugins dele
um exemplo legal: na hora de construir o menu, você dispara um hook, e cada módulo ou serviço registrado responde com os ítens que quer adicionar ao menu.
desse jeito, ninguém precisa mexer na view do core app pra customizar os ítens do menu
Injeção de dependência, uma coisa que eu sei que tem muita gente que ainda não sacou pra que serve, acho que agora vai ficar bem claro. Aqui nesse exemplo:
O model de pedido da aplicação não sabe quem é o gateway de pagamento, mas ele sabe que precisa de UM gateway de pagamento
então a gente estabelece um contrato, uma interface que determina o que um gateway de pagamento deve poder fazer
O model de order pede pra um container "olha, me dá uma instância de alguém que implementa esse contrato", e esse container olha na configuração da aplicação e nos serviços e módulos registrados quem é a instância mais indicada.
aqui ainda estamos no core app, e temos a implementação do gateway do PayPal. Então o container retorna pro model de pedido essa implementação, e o pagamento vai rolar via paypal
Aí você tem sua lojinha funcionando, e um belo dia um cliente fala pra vc que quer usar um gateway diferente. Como o model de pedido não tá lidando com a implementação do PayPal direta, e sim com o contrato, essa solicitação fica muito mais simples
A gente escreve uma nova implementação desse mesmo contrato, como por exemplo o PagSeguro.
Depois, a gente fala pro container que a classe mais indicada pra lidar com esse contrato é a implementação do PagSeguro, e não do PayPal
Pronto, trocamos de gateway, e não colocamos um dedo dentro do core, só mexemos na customização. A gente pode atualizar livremente o core app, sem quebrar o PagSeguro pra esse cliente, e nem o PayPal para os outros.
O legal é que depois a gente pode pegar essa implementação do PagSeguro, e oferecer pra qualquer um dos clientes. O cliente quer, é só colocar o componente que registra essa implementação no container, e pronto.
Geralmente containers de injeção de dependência, como o do Laravel, tem uma infinidade de controles e fine tuning. Você pode ser bem específico, por exemplo, sobre qual instância vai ser retornada caso a caso. Por exemplo, quando o model Pedido pedir um gateway, retornar PayPal. Quando a classe Assinatura pedir um gateway, retornar PagSeguro. O container cuida de decidir qual implementação vai ser usada.
Falando agora um pouco de frontend. Sempre que der, usem engines de template pro projeto. Dá pra facilitar bastante a customização. Esse aqui é um exemplo da engine Blade, que vem no Laravel. Olha só, no meu layout aqui eu defino algumas rotas por nome, e não por caminho absoluto, então eu posso sobrescrever essas rotas se eu quiser. Aí aqui eu uso especifico o output de algumas seções, sidebar e content. Esse aqui é o arquivo de layout.
Aí eu posso criar um sublayout, que extende do layout principal, e que define algum conteúdo pra sidebar, por exemplo. Aí eu tenho uma página desse módulo, que usa o sublayout específico, e define uma outra seção. Reparem como fica limpo e fácil de visualizar e manter o HTML consistente, e de reutilizar bastante coisa, por exemplo, numa customização. Dá pra jogar uma série de layouts, componentes e sections na mão do programador front-end, e ele vai tocar muito mais tranquilamente o desenvolvimento, mexendo só no que precisa e reutilizando muita coisa.
Outra coisa importante pra incorporar no projeto é o Sass. Esse cara facilita muito a customização de layout e de visual entre um cliente e outro. Você define uma série de variávels que vão ser sobreescritas pelo cliente, escreve o estilo da aplicação e pronto. No seu processo de provisionamento, você compila o Sass e o CSS já sai pronto e customizado pro cliente. Dá pra você colocar tudo isso na mão do cliente, gerar o config via PHP carregando direto do banco e compilando o Sass. Tudo isso sem precisar sujar o HTML de estilos inlines que chamam variáveis carregadas de algum lugar, e que fica um inferno de administrar e manter depois.
Pra infra-estrutura e deploy, tem uma coisa que é primordial pra projetos multitenant
Automatizem tudo! De verdade, gente. Infra e deploy é o maior custo de overhead que um projeto desses vai acumular com o tempo, e quanto antes você automatizar esses processos, melhor. É importante já pensar nos processos automatizados na hora de planejar e desenhar a aplicação.
Pra deploy, tem várias ferramentas. Na LQDI a gente usa essas 3, e são ridiculamente fáceis de usar e configurar. O Envoyer é bem focado em deploy de PHP, o Forge é um automatizador de provisionamento, instalação, configuração, deploy de servidores feito pelo criador do Laravel, super completo. O DeployHQ é uma plataforma praticamente que universal de deploys, aceita diversos targets, faz deploy usando Git, FTP, Amazon S3, rsync e o que mais vc precisar, dá pra customizar bastante o processo, é a ferramenta mais completa de deploy que a gente já usou.
O Composer é melhor solução pra gerenciar e versionar tanto o seu core app quanto as customizações e módulos diferentes, principalmente no caso das instâncias isoladas. Você consegue criar repositórios privados usando uma ferramenta chamada Satis. Aí você gera um composer.json na hora de provisionar o cliente, e já especifica qual a versão do core code que você quer e quais módulos de customização. Pronto, os módulos viram packages e ficam isolados nas pastinhas dentro do vendor, e você evita a maioria dos problemas de housekeeping. Depois que você se acostuma a trabalhar desse jeito, fica fácil criar uma feature pra um cliente que pode ser revendida pra qualquer outro cliente.
Ainda na linha de automatizar tudo, o próprio processo de provisionamento é importante de ser automatizado. Provisionamento é algo que cabe bem dentro de um microservice, por exemplo, e economiza um tempo danado da equipe de desenvolvimento e dos sysadmins, e facilita a configuração e customização de alguns clientes. Aqui é um exemplo de um script de provisionamento: o cliente se cadastrou, a gente dá um trigger pro serviço de provisionamento. Esse serviço começa criando um registro do tenant no banco. Aí ele vai, cria a pastinha do cliente do servidor e já gera um arquivo de site pro Nginx. Cria o banco, gera o .env com a configuração dos bancos. Enquanto ele vai fazendo isso, ele vai atualizando o registro do tenant no banco; o usuário pode até acompanhar o processo de provisionamento em tempo real. Aí vai, gera o composer.json com os módulos selecionados, gera o config do Sass e compila o CSS, cadastra o tenant no Envoyer via API e dá o trigger no deploy. O envoyer vai clonar os arquivos do core, instalar as dependências do composer, rodar as migrations no banco e reiniciar o nginx quando terminar. No final, dispara um email pro cliente já com a URL do ambiente dele.
Só nas fases de customização, imagina o tempo que você não perderia fazendo isso manualmente. Criando arquivo, editando os campos, copiando e colando. Enquanto isso, seu cliente tá lá esperando. Se ele se inscreve direto do site, então, já era.
É importante essa automatização por que o processo de provisionamento interfere direto num KPI de negócio, que é o custo de aquisição de cliente. Se para cada novo cliente o negócio vai precisar que você aloque algumas horas configurando arquivos e mexendo em paineis dos serviços, esse custo sobe. É um custo que afeta diretamente a capacidade do negócio crescer, dificulta e encarece um modelo trial ou uma opção freemium, e por aí vai.
Uma coisa importante pra lembrar na hora de pensar na automatização do provisionamento é que provavelmente, sua infra inteira tem APIs para serem administradas programaticamente. EC2 e DigitalOcean dá pra provisionar servidores virtuais via API. S3, Rackspace Cloud e Google Storage, dá pra criar buckets de arquivos via API. Route 51 e DigitalOcean, dá pra gerenciar o DNS pra criar domínios custom pros clientes. Tá muito mais fácil do que antigamente automatizar tudo isso.
Então, pra fechar gente, os principais pontos e dicas que eu posso dar pra arquitetura multitenant.
Primeiro, foca sempre na sua necessidade de flexibilidade de customização na hora de decidir o modelo de segmentação de banco e de codebase. Quanto mais customizável, mais o modelo isolado é indicado. Quanto mais uniforme, mais o modelo compartilhado performa melhor.
Segundo, tente usar sempre as melhores práticas do SOLID no projeto. Se você não conhece o SOLID ou ainda não entendeu muito bem, me manda um email que eu mando alguns materiais bem legais de SOLID. Inversão de dependência é uma coisa que te ajuda muito nesse tipo de projeto; é praticamente garantido que você vai ter casos de uso de ter que trocar a implementação algum serviço.
Terceiro, automatize TUDO que diz respeito a provisionamento, deploy e housekeeping. Esse trio é o responsável por acumular uma constante divida técnica no seu projeto, e é difícil você automatizar depois de ter uma gama de clientes, pq você acaba correndo um risco de ter inconsistências entre quem veio antes e quem veio depois. Pra diagnosticar isso depois é difícil.
Quarto e último ponto: escreva testes pra tudo que estiver dentro do core. Lembra que qualquer problema que passar vai ser propagado por todos os clientes. É muito importante testar também por que as customizações que você for escrever no futuro dependem de um core estável; você precisa saber se a sua customização quebrou algo ou não, e os testes vão te dizer isso.
É isso gente! Perguntas?
Rapidinho antes de fechar gente, um recado: a gente tá contratando lá na LQDI. A gente tá procurando desenvolvedores de todos os níveis de experiência, de júnior a sênior e especialista. A LQDI é uma empresa bem legal pra trampar, descontraída, com remuneração competitiva, horários flexíveis, bastante abertura pra novas idéias e muito projeto legal pra fazer. É uma oportunidade de fazer carreira numa empresa ainda pequena que está crescendo com bastante velocidade.
Se alguém se interessar, é só mandar um e-mail pra work@lqdi.net citando a indicação da palestra. Se quiser saber mais, pode vir falar comigo agora também. É a chance de começar 2016 de trampo novo, hein gente!
E é isso, pessoal. Muitíssimo obrigado pela atenção de vocês, e até a próxima! Meus contatos tão aí pra quem quiser tirar dúvida ou precisar de ajuda. E quem quiser os slides, é só pegar no meu Slideshare via QRCode. Valeu!