Segue um relato da experiência da boo-box com bancos de dados NoSQL. Os cases abaixo foram apresentados no The Developer’s Conference 2010 e são exemplos reais de como utilizamos o Redis em nosso sistema de tecnologia para exibição de anúncios em múltiplos websites.
Compartilhar estas soluções é uma das maneiras de agradecer à comunidade de desenvolvedores por usarmos software livre, difundir o conhecimento criado na empresa e melhorar nossa própria ferramenta.
Bancos NoSQL, entende-se “Not only SQL”, surgiram da necessidade de escalar bancos de dados relacionais com propriedades ACID em projetos web de alta disponibilidade que operam em larga escala. Suas principais características são alta performance, escalabilidade, fácil replicação e suporte a dados estruturados.
Este rompimento com os padrões SQL causa sempre grande repercussão e muitas discussões carregadas de sentimentos e emoções, mas a verdade é que os bancos de dados relacionais ainda servem para resolver muitos problemas que nem sempre (veja bem, nem sempre) poderão ser resolvidos com bancos NoSQL, como por exemplo:
- Necessidade de forte consistência de dados, tipagem bem definida, etc;
- Pesquisas complexas que exigem um modelo relacional dos dados para realizações de instruções e operações de junção, por exemplo;
- Dados que excedam a disponibilidade de memória do servidor, por mais que possamos utilizar swap; ninguém quer prejudicar a performance neste caso.
Ao escolher seu banco de dados, é importante considerar as funções e características específicas do sistema. Os bancos de dados NoSQL podem ser utilizados especialmente para funções descritas neste artigo. Vamos aqui abordar particulamente a nossa experiência com o Redis.
Sobre o banco de dados NoSQL Redis
O NoSQL Redis, que atualmente está na versão 2.0.1, é definido como advanced key-value store. Seu código é escrito em C sob a licença BSD e funciona em praticamente todos sistemas POSIX, como Linux ou Mac OS X. Ele foi idealizado e executado por @antirez para escalar o sistema da empresa LLOOGG. Hoje o repósitório é mantido por uma imensa comunidade e patrocinado pela VMWARE.
A simplicidade de operar um banco apenas setando o valor e uma chave contínua, diferente de soluções famosas como o memcached, nos permite fazer diversas operações na camada das chaves, além de contar com um punhado de estruturas de dados.
Além de salvar strings na memória, também é possível trabalhar com conjuntos, listas, ranks e números. De maneira atômica, pode-se fazer operações de união, intersecção e diferenças entre conjuntos, além de trabalhar com filas, adicionando e removendo elementos de maneira organizada.
Assim como outros bancos NoSQL, este projeto é completamente comprometido com velocidade, pouco uso de recursos, segurança e opções de configurações triviais para ganhos de escalabilidade. Para manter a velocidade dos dados com garantia de persistência, de tempos em tempos (ou a cada n mudanças) as alterações são replicadas, de maneira assíncrona, da memória RAM para o disco.
Agora, vamos aos cases. Dentro de tantas possibilidades, mostraremos algumas soluções do sistema boo-box utilizando o Redis.
Cases
Armazenamento de sessões de usuários
Este é um modelo muito simples sobre como utilizar o Redis para salvar as informações da sessão de um usuário.
Para cada sessão, gera-se uma chave que é gravada no cookie do navegador. Com essa chave, o sistema tem acesso a um hash com informações desta sessão: status do login, produtos e publicidades clicadas, preferências de idioma e outras configurações temporais, que perdem a validade após algumas horas.
O benefício de não guardar tais informações de sessão diretamente no cookie é evidente: ganhamos a segurança de integridade dos dados, não correndo o risco de algum usuário malicioso modificá-los.
Com o Redis, utilizamos operações simples de get/set para acessar estes dados diretamente da memória do servidor (ou servidores, caso exista mais de um), sem desperdício de recursos, graças ao eficiente sistema de expiração promovida por este NoSQL.
O algoritmo de expiração não monitora 100% das chaves que podem expirar. Assim como a maioria dos sistemas de cache as chaves são expiradas quando algum cliente tenta acessá-la. Se a chave estiver expirada o valor não é retornado e o registro é removido do banco.
Em bancos que gravam muitos dados que perdem a validade com o tempo, como neste exemplo, algumas chaves nunca seriam acessadas novamente; consequentemente elas nunca seriam removidas. Essas chaves precisam ser removidas de alguma maneira, então a cada segundo o Redis testa um conjunto randômico de chaves que possam estar expiradas. O algoritmo é simples, a cada execução:
- Testa 100 chaves com expiração setada.
- Deleta todas as chaves expiradas.
- Se mais de 25 chaves forem inválidas o algoritmo recomeça do 1.
Esse lógica de probabilidades continua a expirar até que o nosso conjunto de keys válidas seja próximo de 75% dos registros.
Cache de produtos de terceiros
Todo dia a boo-box exibe para a audiência milhões de produtos – de diferentes e-commerces – vinculados ao conteúdo de publishers. Os e-commerces fornecem APIs e através delas é possível buscar produtos para serem mostrados em nossas vitrines.
Num modelo ideal, cada requisição de uma vitrine boo-box faria contato com as APIs dos e-commerces parceiros, em busca de produtos compatíveis com o conteúdo em questão. Mas no mundo real da publicidade online, velocidade e escalabilidade são premissas essenciais para a qualidade de produto e, portanto, requisições síncronas a tais APIs tornariam o processamento lento demais.
Portanto, essas operações são cacheadas num banco Redis. Separamos os e-commerces em bancos distintos e obtemos os produtos segundo a keyword que foi utilizada nas buscas de todas as vitrines da rede boo-box.
Execução do cache de produtos quando há resultados para a tag solicitada.
Porém, sempre existe aquele usuário com poucos acessos e com tags que não são tão populares. Neste caso, tentamos fazer a consulta diretamente da API (com um tempo limite pequeno para não complicar o sistema).
Caso não encontremos nenhum produto para esta tag, podemos, como já foi dito acima, buscar chaves similares para mostrar nas vitrines deste publisher, enquanto um evento paralelo é acionado para adicionar esta tag no cache sem restrições de tempo. Assim, em uma próxima visualização, os produtos já estarão quentinhos no cache!
Veja como esse fluxo pode ser ilustrado:
Quando não havia resultados para a tag solicitada, buscávamos os produtos na API, entregávamos ao usuário e gravávamos os resultados no cache.
A separação de e-commerce em bancos distintos facilita as operações de busca por keywords. O Redis, por padrão, habilita 16 bancos que podem ser utilizados separadamente e, por consequência, escalados separadamente.
Com as keys de um e-commerce isoladas, podemos buscá-las através de padrões parecidos com regexp e, com sabedoria, isso pode ser um excelente recurso, mas também pode ser um problema tendo em vista que a complexidade desta função é O(n) onde n é o numero de chaves no banco utilizado.
Diagrama de sequência dessa funcionalidade:
Quando não há resultados para a tag solicitada no cache de produtos, exibimos produtos similares, depois buscamos pelos produtos exatos no e-commerce e os entregamos diretamente do cache na próxima solicitação.
Veja os logs dessa funcionalidade em ação:
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using the_velvet_underground instead underground 0.012
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using pushing_daisies instead push 0.012
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using deborah_secco instead deborah 0.017
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using oracoes_catolicas instead catolica 0.017
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using uma_linda_mulher instead linda 0.012
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using lutaram instead lutar 0.016
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using videogame_wii instead videogame 0.017
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using mini_craque_prostars instead craque 0.017
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using apple_ipod_shuffle_1_gb_silver instead apple 0.005
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using motocultivador_tratorito_branco_diesel instead motocultivador 0.017
merb : worker (port XXXX) ~ DEBUG get similar keys from redis using suporte_para_bicicleta_automovel instead automovel 0.005
Esse cache é muito custoso e não é remontado com facilidade. Portanto, assim como a primeira solução, além da velocidade, a persistência dos dados em disco é imprescindível.
A expiração, neste caso, também é muito importante pois mudanças nos catálogos ou nos preços do produto acontecem com frequência. Mesmo com uma expiração curta, não é interessante esperar que produtos populares como videogames, celulares e afins saiam do cache.
Para evitar isto, consultamos, a cada requisição, o tempo de vida restante deste produto no cache. Caso ele esteja próximo de expirar, um evento assíncrono é acionado para atualizar este produto na API do e-commerce em questão. Na apresentação Usando Redis para otimizar o sistema boo-box, feita na Campus Party Brasil 2010, mostramos detalhes deste fluxo.
Os resultados desta solução foram supreendentes como mostram as imagens abaixo. Uma queda no tempo de resposta de parte do sistema, uma economia insana de recursos que levou ao desligamentos de servidores e redução de perfil de máquinas, assim como a melhoria da manutenção do processo todo.
Tempo de resposta do sistema:
Mais velocidade no sistema após a implementação do cache de produtos.
Todo dia a boo-box exibe milhões (milhões!) de produtos de diversos e-commerces. Com o Redis cacheamos tudo com apenas 300 MB de RAM.
Busca em catálogos de produtos de terceiros
Algumas vezes temos acesso a um catálogos de produtos de um e-commerce por meio de um arquivo XML. Diariamente, este arquivo é atualizado pelo parceiro, parseado e salvo no Redis do sistema boo-box. Além de armazenados, esses produtos são também organizados para a realização de consultas. Modelar NoSQL é um pouco diferente do que modelar bancos relacionais. Não existem joins ou queries complexas, a estrutura é organizada para a pesquisa que deve ser feita.
Vamos a um exemplo prático:
Supondo que o e-commerce disponibilize o seu catálogo de produtos em um arquivo XML que tem a estrutura abaixo:
Dado que o campo “cod” é uma referência única para este produto neste catálogo, poderíamos transformá-lo em chave e salvar um hash com todas as propriedades do produto, como mostra o código abaixo:
Bonito e inútil! Ter um hash referenciado por um id a princípio não ajudaria a fazer buscas, entretanto esse será o nosso banco principal que guardará todas as informações dos produtos.
Se pensarmos em como indexar estes produtos por uma busca mais trivial (por exemplo, nome) devemos criar um novo banco e teríamos que alterar o nossa função de parser para organizar estes produtos ou melhorar suas chaves por nome:
A função slugfy foi utilizada para evitar que a mesma palavra seja tratada diferentemente por conta de caracteres de acento ou em caixa alta. Esse é um ponto crucial que pode aumentar muito a contextualização da busca. Muitos algoritmos linguísticos podem ser úteis neste caso, mas voltando ao ponto deste case, um método simples de busca para essa indexação seria:
Legal! Agora podemos buscar por nome em nosso catálogo, mas essa busca é muito engessada. Para melhorá-la, poderíamos buscar por todas as palavras do produto, esteja ela no título, nome na categoria ou até mesmo na descrição.
Aqui temos um ponto importante. Assim como o uso da função slugfy precisamos definir algumas stopwords para que, nesta função, palavras muito comuns sem valor semântico não atrapalhem a busca. Por stopwords podemos considerar artigos, pronomes etc.
A estratégia agora é criar um terceiro banco com ids que contenham uma tag específica. Mudaríamos novamente o nosso parser para preencher este banco de busca e definiríamos uma nova função:
Para realizar busca com tags, poderíamos fazer uma interseção entre as tags. Dessa forma teríamos os ids que contemplassem esta busca. Vejamos uma maneira simples de fazer isso:
Este esboço de algoritmo pode nos dar uma ideia de como modelar NoSQL. Existem muitas maneiras de melhorar esta busca: quantidade de vezes que a palavra é citada, proximidade de palavras e até mesmo a posição da palavra. Muitos algoritmos de pagerank por exemplo podem nos guiar nessas melhorias.
Um ponto importante, levantado pelo @jdrowell é a utilização da busca por regexp:
redis.keys(“*” + name + “*”)
Este é sem dúvida o comando que deve ser utilizado com maior cautela, ele pode ser um gargalo devido a sua complexidade. No nosso caso, como a quantidade de keys é fixa, esse comando não pode nos comprometer devido ao tamanho do catálogo de produtos.
Para quem quer se aprofundar neste case, sugiro a leitura do artigo indicado pelo @jdrowell que mostra um case similar de busca de textos utilizando o Redis.
Validação de visualizações e cliques de produtos
Este último é também o mais recente case de utilização do Redis e ainda está em fase de testes e validação. O adserver da boo-box hoje exibe cerca de 15 milhões de vitrines de produtos e campanhas diariamente. Todas visualizações e cliques são logadas e, a partir destes logs, exibimos estatísticas para os nossos publishers e anunciantes.
Salvamos todos os tipos de informação que podemos: as dimensões das peças, dados sobre o usuário que interagiu com a vitrine como IP, navegador e até mesmo o seu perfil de navegação. Sim, nós gostamos de dados!
Dado o volume de informações, esperar a inserção para pegar um id que possa servir de referência para esta tupla em um banco de dados relacional é muito perigoso. E fazer queries para recuperá-los seria também uma tarefa lenta. Gravar os logs em um banco rápido como o Redis e ainda poder acessá-los diretamente do adserver abre um leque de possibilidades.
Podemos, por exemplo, confirmar a renderização das vitrines pelo navegador, medir o intervalo entre clicks, controlar a veiculação de campanha no decorrer do dia, entre outras tantas coisas.
Ao colocar pela primeira vez essa feature em produção tivemos que trabalhar muito para – acredite – melhorar o tempo de inserção dos dados das vitrines no Redis. Na verdade, foi necessário tunar diversos pontos da infraestrutura (pois utilizávamos um servidor para diversos fins e tinhamos apenas uma grande instância do Redis que era utilizado por todas as features).
Além do mais, estavamos utilizando uma versão antiga do Redis, com uma política de gravação dos dados da memória para o disco que prejudicava demais a performance devido a grande quantidade de alterações. Outro fator relevante para prejudicar a velocidade foi o número de bytes por objeto salvo e o tempo de serialização e deserialização deles.
Veja o que modificamos para recolocar esse recurso no ar e melhorar o tempo de inserção chegando a 0.001 milésimos 😉
- Isolamos o servidor e habilitamos instâncias diferentes do Redis em diversas portas. Dessa forma toda a memória e processamento ficou dedicada para este serviço e o numero de núcleos do processador pode ser melhor aproveitado, o sistema passou a escalonar o Redis em diversas CPUs paralelamente.
- Atualizamos o Redis e modificamos as suas configurações para não gravar os dados em disco, tendo em vista que estes dados são voláteis e não são reutilizados.
- Otimizamos a maneira como salvamos os dados no Redis. Ao invés de objetos complexos, salvamos apenas um hash com as informações essenciais da visualização e modificamos o metodo de serialização de YAML para Marshal.
Com essas modificações o tempo de resposta caiu drasticamente, porém o uso de recurso ainda é uma questão preocupante. Por mais que a tendência da memória RAM seja ficar a cada dia mais barata, esse recurso ainda é muito custoso.
Como a quantidade de cliques é menor que de visualizações, muitos dados são gravados no Redis e nunca acessados (neste caso, mais de 99% dos objetos). É fundamental prevenir estes problemas configurando adequadamente o uso de swap pelo Redis e limpando os dados antigos para liberar memória. Em casos extremos, podemos monitorar atráves das estatísticas do Redis o processo e automatizar as ações de limpeza da memória, prevenindo o uso de swap.
No FAQ do projeto existem dicas preciosas para evitar o desperdício de memória: primeiramente utilizar diversas instâncias de 32 bits, ao invés de uma com 64 bits; modificar variáveis de ambiente e escolher o tipo de dados adequado para a sua aplicação pode ajudar a prevenir muita dor de cabeça com este gargalo.
Considerações finais
Estes cases são exemplos bem sucedidos ou promissores do experimento de novas tecnologias no sistema boo-box. Muitas vezes, a tentativa de utilizar uma nova tecnologia não é bem sucedida. Já utilizamos, por exemplo, outros bancos NoSQL, que por muito tempo foram uma boa solução mas passaram a ser um gargalo em um novo contexto.
Muitos são os desafios que enfrentamos para garantir qualidade, velocidade e estabilidade em sistemas altamente escaláveis. Conhecer as novas tecnologias, estudar e aplicar novas soluções é um esforço importante e muitas vezes ainda desconhecido. Como dissemos lá no início, compartilhar soluções é uma das maneiras de agradecer à comunidade de desenvolvedores por usarmos software livre, difundir o conhecimento criado na empresa e melhorar nossa própria ferramenta.
Contribua! [Webinsider]
Veja o post original.
…………………………
Conheça os serviços de conteúdo da Rock Content..
Acompanhe o Webinsider no Twitter.
Felipe Tio
Felipe Tio é analista de sistemas e consultor de TI. Mantém o blog Felipetio e o o Twitter @felipetio