Resumo comentado: Designing Data-Intensive Applications — Parte 1
Olá!
Um ponto de muitas dúvidas entre as pessoas desenvolvedoras são as fatídicas perguntas: como lidar com os dados de uma forma mais adequada? Há mundo além do SQL? Como escalar melhor queries pesadas? Como modelar os dados de uma forma mais performática?
Na maioria das aplicações, jogar tudo em um banco de dados relacional resolve. Porém, para sermos profissionais excelentes, temos que ir além e urge a necessidade de sabermos fazer sistemas além do CRUD.
Uma das formas que encontrei de aprender sobre o assunto dados de uma forma mais abrangente foi esse livro aqui:
O intuito é escrever um pouco sobre os principais conceitos contidos no livro em português para ajudar nossa querida comunidade de pessoas desenvolvedoras que falam brasileiro. Aparentemente, é essa linguagem que usamos para nos comunicar agora, segundo nosso amigos lusitanos.
O livro começa conceituando as 3 principais características de um software robusto, que são os conceitos de Confiabilidade, Escalabilidade e Manutenibilidade. Vou comentar brevemente sobre cada um deles.
Confiabilidade (Reliability): Conceito no qual se refere ao quanto um sistema consegue ser confiável, mediante tudo relacionado ao seu funcionamento que pode falhar, seja hardware (como um HD mecânico morrendo), software (por bugs, loops infinitos ou falhas em cascata) e erros humanos.
Esse último ponto vale um detalhamento. Uma piada comum dentro da comunidade de pessoas desenvolvedoras é que o usuário é o problema quase sempre em caso de problemas no uso de um determinado sistema. Eu gostaria de discordar desse ponto mencionando as recomendações do livro.
As recomendações para aumentar a confiabilidade são: pensar sistemas de uma forma que diminuam as chances de erro, usando validações para minimizar inputs inválidos, por exemplo. Além disso, usar encapsulamento adequado de APIs e afins, de uma forma que a própria aplicação sugira o jeito certo de uso também é uma boa ideia. Podemos também aplicar redução de danos em partes frágeis do sistema, utilizando-se de práticas como Canary Deployment, bem como aplicar testes em todas as camadas do sistema (como testes de unidade e de integração) e monitoramento adequado.
Ou seja, ao invés de reclamar do usuário, aceite que ele é um ponto de falha como qualquer outro e utilize técnicas adequadas para lidar com isso.
Escalabilidade (Scalability): Nada mais é do que o desempenho de um determinado sistema, dado uma determinada carga. Portanto, se o sistema foi feito para 10 usuários e suporta 100 em um pico, tecnicamente, ele escala (nem que seja até o limite de 100 usuários). O grande problema é que no mundo real, em empresas que tem problemas reais de escala, geralmente estamos falando na escala de milhões ou até bilhões de transações.
Aqui cabe o seguinte formalismo: vale elencar quais são os parâmetros de carga (load parameters) que serão utilizados para se avaliar se o sistema escala ou não. Alguns exemplos de parâmetros podem ser: quantidade de escritas no banco de dados, cache hit ou — talvez o mais comum — requisições por segundo.
Nesse ponto, a recomendação é utilizar estatística para avaliação de performance de um sistema, como percentis, desvio padrão e afins. Sobre isso, gostaria de comentar o seguinte: se você quer ser uma pessoa desenvolvedora profissional — e não somente um amador remunerado — e tem dúvidas se um sistema escala, prove com dados. Como exemplo: meça se no 99 percentil o tempo de resposta chega até 300 ms. Essa é uma métrica adequada para medir o desempenho de um sistema, além de ser um jeito mais adequado de se provar um argumento nesse assunto.
Além disso, vale mencionar: a arquitetura de um sistema tido como escalável é composta de blocos de construção que juntos formam um sistema adequado para uma determinada carga (mais detalhes sobre esses blocos em posts futuros dessa série). E tem mais um detalhe: caso esses pedaços tenham sido pensados de forma errada olhando o todo da solução técnica, eles se tornarão os gargalos, invariavelmente.
Conclusão do livro: escalar não é um problema trivial. Até aí, nada de novo sob o sol, certo?
Manutenibilidade (Maintainability): Para esse conceito, vale quebrar em partes menores o entendimento do mesmo, dado que é um conceito subjetivo e um pouco mais difícil de se medir com objetividade. Em outros termos, é o indicador do quanto é fácil a vida do time técnico ao mexer em um determinado sistema.
A primeira dessas partes é a necessidade de se pensar na Operabilidade (Operability), que envolvem uma série de técnicas de diagnóstico da saúde da operação de um sistema, como monitoramento adequado que indique de forma pró-ativa certos problemas, além de técnicas de configuração adequadas (ex: secure by default), como também coisas básicas como uma boa gestão do conhecimento (mesmo após a saída de membros do time, quem fica tem que conseguir operar o sistema com a documentação existente), bom suporte a automação e self-healing. Essas técnicas juntas fazem a vida das pessoas desenvolvedoras melhor e diminuem as chances delas quererem sair da empresa.
Para as demais partes (Simplicidade e Evolucionabilidade), eu gostaria de mencionar técnicas já conhecidas — apesar de não mencionadas dessa forma pelo autor — que são: os acrônimos KISS para simplicidade e o YAGNI para Evolucionabilidade. O conhecimento desses tópicos já serve para substituir a leitura dessa parte do livro.
Com isso, finalizamos o capítulo 1.
No capítulo 2, há uma breve conceituação sobre a diferença entre bancos de dados relacionais (que usam SQL) e não relacionais (popularmente conhecidos como NoSQL). Além de uma exemplo de alguns tipos de técnicas de modelagem de dados (como 1-N e N:N).
E é justamente na modelagem de dados — que é um dos blocos de construção de um sistema Data-Intensive — que é necessário pensar com cuidado.
Caso o domínio do problema que a aplicação resolve seja passível de ser representado com uma arquitetura hierárquica (como uma árvore ou similar), bancos de dados orientados a documentos se saem melhor e tem um resultado final mais simples, pois o processo de normalização que um banco relacional demanda, costuma gerar múltiplas entidades no processo, complicando a modelagem final.
O principal ponto positivo do modelo hierárquico é flexibilidade de schema (com uma representação dos dados em JSON), com as seguintes recomendações de uso: caso haja a necessidade de muitos tipos de objetos de aplicação, acaba sendo não prático modelar cada tipo em uma tabela diferente e caso seja necessário armazenar dados advindos de um sistema que você não possui controle sobre o formato (como armazenamento de respostas de API externas ou similares), também é recomendado armazenar esses dados em uma estrutura flexível.
Além da flexibilidade, esse modelo costuma fornecer a vantagem de localidade de referência (memory locality), que é bem óbvio se pararmos para pensar: se está tudo em um lugar só, eu gasto menos energia procurando em vários lugares (o que gera uma melhor performance que o relacional, que gasta ciclos de processamento juntando dados de diversas tabelas). É possível obter performance expressiva caso essa característica seja explorada corretamente, aqui há um exemplo no DynamoDB.
O modelo documental tem suas limitações, entretanto. Nesse caso podemos enumerar as seguintes: suporte ruins a agregações (joins) caso ocorram muitas associações N:N, além de um suporte não muito adequado a transações também na maioria dos casos. Aqui a mensagem é simples: se os dados são altamente relacionáveis (duh!), use banco de dados relacionais e não reinvente a roda. Se tudo tá meio que junto e não precisa de joins, use NoSQL. Excesso de joins em não-relacionais costuma gerar performance ruim na maioria dos casos, portanto, evite.
O livro também comenta sobre algumas linguagens de manipulação de dados (query languages) explicitando a diferença entre linguagens declarativas (como SQL) e linguagens imperativas de manipulação de dados como as disponíveis nos sistemas IMS e CODASYL. Nada muito útil, mas comento mesmo assim para fins de completude.
Se você nunca ouviu falar nesses termos, não se preocupe, pois então no livro somente por questões históricas e praticamente são irrelevantes hoje em dia.
A diferença entre os paradigmas declarativos e imperativos são: no primeiro você diz o que você quer e o computador decide o como. No segundo, você tem que explicitar como o computador irá realizar a operação de busca no conjunto de dados (inclusive escrevendo na mão os loops de busca). Um resquício de ideias imperativas no SQL (que é declarativo, vale salientar) é o uso de cursores. O uso desse tipo de mecanismo também só é indicado para casos de uso muito específicos e na maioria das vezes você não conseguirá um resultado melhor que o motor de busca do banco relacional, portanto, minha recomendação é evitar.
Há uma menção também ao algoritmo MapReduce, muito usado como abstração fundamental em muitos sistemas voltados para Big Data, que demandam alto poder de processamento em paralelo. Como exemplo, dá para citar o Hadoop. O mesmo algoritmo pode ser encontrado em Javascript também. Não é coincidência que JS foi a linguagem escolhida para escrever queries em bancos NoSQL. Acredito que tenha sido justamente devido a esse tipo de suporte nativo da linguagem ao MapReduce.
Caso o relacionamento entre as entidades tenham uma grande quantidade de relacionamentos N:N complexos, a recomendação é usar Grafos. Apesar de antiga (a ideia original é de 1736), acredito que muitos problemas que temos hoje em dia em bancos relacionais podem ser representados de uma forma mais adequada utilizando-se desse mecanismo.
Para ficar um pouco menos abstrato, gostaria de usar algumas imagens para explicar a diferença na carga cognitiva envolvida no entendimento de ambos os modelos (essas imagens não são do livro).
Segue o exemplo de um relacionamento N:N no modelo relacional:
Agora, um problema diferente, utilizando vértices e arestas:
O que é mais fácil de entender? E manipular? E explicar?
Lembre-se disso quando for modelar estruturas como redes de pessoas (N:N) e similares, é muito mais fácil representar essa abstração utilizando grafos como mecanismo. Além disso, escala melhor também, pois é a modelagem correta para esse tipo de problema. Talvez o banco de dados mais famoso desse paradigma seja o Neo4j. Não é coincidência que o Facebook tenha chegado a conclusão natural que utilizar essa tecnologia é uma boa ideia para o domínio do problema de uma rede social. Fonte? Aqui.
Há uma menção no livro a Datalog — que foi uma das primeiras linguagens de busca em grafos, baseada em Prolog — , além de SPARQL (utilizada no padrão RDF para web, que aparentemente não ficou popular, pois só ouvi falar desse padrão nesse livro) e a Cypher (utilizada no Neo4j).
Para fechar o capítulo 2, há também um comparativo de como utilizar SQL em estruturas de grafos, o que cabe o spoiler: apesar de possível, as linguagens próprias são obviamente mais adequadas para essa mesma tarefa, principalmente em termos de complexidade na escrita das queries. Fica a menção somente para fins de completude do artigo.
E você? O que achou dos conceitos contidos no livro até agora?
Por hora, eu paro por aqui para esse texto não ficar muito grande.
Até!
Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.