Max Indelicato, um Diretor de Desenvolvimento de Software e ex Chefe de Arquitetura, escreveu um post falando sobre como modelar uma aplicação web visando escalabilidade. Ele sugere escolher soluções adequadas para instalações e armazenamento, um banco de dados escalável e usar camadas de abstração.
A Ferramenta correta para o Trabalho
O primeiro conselho de Indelicato é escolher "a ferramenta correta para o trabalho" selecionando uma das seguintes soluções arquiteturais:
- Usando uma instalação cloud
- Usando uma solução escalável de banco de dados tal como MongoDB, CouchDB, Cassandra ou Redis
- Adicionando uma camada de cache como o Memcached
Esse relatório considera que nenhuma dessas soluções é mandatária no início da aplicação, mas é inteligente escolher uma solução escalável para armazenamento de dados para evitar ter que mudar no futuro. Instalar em uma cloud traz algumas vantagens especialmente para startups as quais não conseguem determinar com precisão o uso de sua aplicação após o lançamento. Instalar em um cloud permitiria a aplicação uma boa escalabilidade se essa necessidade surgisse. Muitos arquitetos de software contam histórias de como suas aplicações cresceram e eles introduziram uma camada de cache, resolvendo boa parte do problema. Essa solução de cache não precisa necessariamente ser considera na fase de modelagem. Isso pode facilmente ser implementado ao longo do desenvolvimento.
Um Banco de Dados Escalável
Indelicato continua sugerindo escolher um banco de dados que suporte particionamento, replicarão e que seja elástico, um dos seguintes: MongoDB, Cassandra, Redis, Tokyo Cabinet, Project Voldemort ou MySQL para um banco de dados relacional. Isso é desejável visto que o particionamento será necessário de qualquer forma ao longo do tempo de vida da aplicação. A replicação não é necessária para fins de escalabilidade mas sim por "assegurar grandes níveis de disponibilidade". Elasticidade é boa para rapidamente adicionar mais nós quando picos de tráfego ocorrerem, mas também quando certa "manutenção é requerida em um nó devido a uma falha ou atualização de hardware, uma mudança de grande escala em um schema, ou quaisquer outras razões que precisem interromper um nó."
Um Schema de Dados Escalável
Indelicato sugere a criação de um schema que permita sharding (método de particionamento horizontal de banco de dados) com facilidade, dando como exemplo os seguintes componentes casuais, uma entidade User e UserFeedEntry:
Collection (ou Tabela, ou Entries, etc) User { UserId : guid, unique, key Username : string PasswordHash : string LastModified : timestamp Created : timestamp }Collection (ou Tabela, ou Entries, etc) UserFeedEntry { UserFeedEntryId : guid, unique, key UserId : guid, unique, foreign key Body : string LastModified : timestamp Created : timestamp }
E continua sugerindo um particionamento no UserId:
Por particionar o campo UserId, em ambas coleções User e UserFeedEntry nós juntaremos dois dados relacionados num mesmo nós. Todos os registros no UserFeedEntry com um UserId xxx-xxx-xxx-xxx estarão contidas no mesmo shard assim como um registro em User com um UserId com valor xxx-xxx-xxx-xxx.
Por que isso é escalável? Dado que a nossa necessidade de tornar essa aplicação ideal para para a distribuição de dados. A medida que cada usuário visita a página de perfil do usuário, uma requisição será feita para um único shard para recuperar a entidade User apresentando os detalhes de tal usuário e então uma segunda requisição será feita para o mesmo shard para recuperar as entidades UserFeedEntries de tal usuário. Duas requisições, uma para uma única linha e outra para o número de linhas contidas no mesmo shard. Assumindo que os acessos aos perfis de usuários são os mesmos ao longo do dia, nós modelamos um esquema escalável que atende as necessidades da nossa aplicação web.
Usando Camadas de Abstração
A última sugestão de Indelicato é para usar as seguintes camadas de abstração entre outras: Repository, Caching e Service. Ao criar a camada Repository, ele recomenda que:
- Não nomeie métodos de maneira particular a mecanismo de armazenamento que você está abstraindo. Por exemplo, se você está abstraindo o armazenamento relacional, é comum ver funções definidas como Select(), Insert(), Delete(), Update() para executar consultas e comandos SQL. Não faça isso. No lugar, nomeie suas funções de forma menos específica como Fetch(), Put(), Delete() e Replace(). Isso assegurará que você está seguindo a idéia do padrão Repository de forma mais adequada e tornará sua vida fácil caso precise trocar o mecanismo de armazenamento.
- Use Interfaces (ou classes abstractas, etc) se possível. Passe essas interfaces entre as camadas da aplicação assim você nunca fará referência direta a implementação concreta do repositório. Isso é ótimo para construção de testes unitários também porque você pode escrever implementações alternativas preenchidas com dados para os casos de teste.
- Encapsule todo código específico em uma classe (ou módulo, etc) de forma que os repositórios correntes referenciem ou herdem dela. Somente coloque necessidades específicas de uma função de acesso em cada função (texto de consulta, etc).
- Sempre lembre-se de que nem todos os Repositórios precisam abstrair a mesma solução de armazenamento de dados. Você sempre pode ter os dados de Users armazenados no MySQL e os dados de UserFeedEntries armazenados no MongoDB se você quiser, e os Repositórios devem ser implementados de tal maneira para que eles suportem esse tipo de implementação sem muito esforço. Os três últimos pontos ajudam indiretamente com isso.
Para a camada de Caching Indelicato diz que ele muitas vezes começa com um "nível simples de caching ou um cache na Camada de Serviço uma vez que nesses dois pontos é comum ver o estado mudando frequentemente."
Indelicato considera que a camada de Serviço precisa de abstração assim é possível, de maneira fácil, trocar a implementação interna do serviço sem muito esforço quando a necessidade surgir.
Alguns consideram que uma aplicação pode ser construída sem se preocupar com detalhes de escalabilidade, devido a esses problemas poderem ser resolvidos quando necessário. Mas se a escalabilidade for considerada desde o ínicio, o que você sugeriria?