Como usar Mongodb para otimizar seu cache

Publicado por Jean Matheus Souto no dia dev

O Marketing BI, feature de Business Intelligence do RD Station, utiliza a plataforma Gooddata para armazenamento e processamento de dados. Os dados enviados ao GoodData são processados utilizando queries customizadas que geram um output personalizado e pronto para ser consumido pelo RD Station em forma de tabelas e/ou gráficos. Esse processamento de dados leva minutos/horas proporcionais ao número de clientes e contas do RD Station com Marketing BI. Ao final do processo, pegamos todos esses dados e transformamos em informações que ajudam na tomada de decisões e na análise de métricas do negócio de cada cliente. Executar toda essa operação nos levou ao seguinte problema: como servir uma grande quantidade de dados e ainda atingir nossas métricas de performance?

Pensando no problema, usamos a solução mais óbvia: usar o Rails.cache para guardar os dados já processados. Em um primeiro momento, essa tática nos atendeu bem, mas logo sentimos que essa abordagem não escalaria como desejávamos.

O problema

O processo de comunicação entre RD Station e Gooddata consiste em três etapas:

1 - O extrator de dados: uma app desenvolvida por nós em Sinatra que consome dos nossos databases os dados utilizados pelo Gooddata. Aplicamos este approach para filtrar e enviar somente dados que serão processados para gerar informações.

A app possui diversos extractors e cada um é responsável por montar um .csv com os dados e enviar para o servidor do Gooddata.

Um extrator nada mais é que uma classe ruby que sabe qual informações deve colocar no .csv. Segue um exemplo de extrator de contas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module Extractor
  module Extractors
    class Account < Extractor::Base

      def header
        %w(account_id account_name account_created_at)
      end

      def selected_attributes
        [:id, :name, :created_at]
      end

      def populate_row(account, _entity, row)
        row.append(:account_id, account[:id], :required, :numerical)
        row.append(:account_name, account[:name], :required)
        row.append(:account_created_at, account[:created_at], :required, :date)
      end

      def entities_for(account_id)
        @db[:accounts].where(id: account_id)
      end

    end
  end
end

2 - O Gooddata processa todos esses arquivos .csv e alimenta os reports (relatórios baseados nos dados e métricas criadas a partir da modelagem de nossos dados);

3 - Durante a madrugada, fazemos o fetch desses reports para o RD Station (aqui começa o problema!). Como foi dito, nosso primeiro approach foi colocá-los no Rails.cache. Enquanto tínhamos poucos reports e poucas contas, tudo estava indo bem. Contudo, logo começamos a perceber que o processo não estava mais sendo suficiente pois, às vezes, estourávamos nosso storage. Desta forma, quando os clientes acessavam a feature tinham que ficar esperando os dados serem processados (porque eles não estavam no cache), causando frustração.

MongoDB

Os dados do Marketing BI são atualizados diariamente. Isso nos levou a pensar que sempre podíamos ter uma versão dos dados armazenada até que essa mesma entrada sofresse uma atualização diária. Assim, assumimos que, caso o cache falhasse, nossos clientes não ficariam sem informações - apenas estariam vendo as informações do dia anterior, por exemplo. Esse é um approach aceitável para a feature visto que a maioria das informações analisadas são de um período anterior e, geralmente, com o intervalo de um mês.

Com essa ideia em mãos, escolhemos fazer o simples e eficaz: um modelo que representasse nosso report utilizando MongoDB - que serve exatamente para guardar nossos documentos (reports). Com esta modelagem, solucionamos o problema de servir os dados. Nosso modelo ficou simples e testável, além dos documentos do mongo ficarem extremamente pequenos.

Segue abaixo nosso modelo de Report:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module GoodData
  class Report
    include Mongoid::Document
    include Mongoid::Timestamps
    include GoodData::ReportStatus

    field :report_id, type: Integer
    field :account_id, type: Integer
    field :series, type: Hash
    field :x_axis, type: Hash
    field :data_fetch_url, type: String
    field :status, type: String, default: -> { NOT_FETCHED }
    field :last_update, type: Date, default: -> { Time.now }

    validates :report_id, :account_id, presence: true

  end
end

Neste modelo, temos todas as informações de um report do Gooddata e algumas outras informações como, por exemplo, o :last_update, mostrado na interface para nossos clientes visualizarem qual foi a nossa última atualização do serviço.

Essa nova forma de tratar os dados trouxe muito mais segurança e confiabilidade para servir informações de acordo com nossas métricas de performance. Sempre temos uma versão dos dados guardada no mongo - que recebe e atualiza os dados no mesmo documento. Não precisamos mais manter todo o histórico e sim somente a última versão dos dados. Isso nos garante estimar com precisão o tamanho necessário para escalar para X mais contas baseados no tamanho da nossa collection db.good_data_reports.

Para consumir os reports do Gooddata, criamos a gem gooder_data opensource (contribua por favor! :smile:). Essa gem é responsável por consumir os reports e disponibilizar os dados via a API do Goodata. Quando fazemos o request para API solicitando os dados de um report, esta nos devolve um status - podendo ser waiting, not_fetched e fetched.

Fila de processamento

Ao fazer uso do Mongo resolvemos o problema do cache dos dados. Porém, isso nos gerou uma outra preocupação - ou a deixou mais evidente. O problema da fila de processamento para atualizar os caches que, graças à nova modelagem recém implementada, conseguimos resolver de forma simples e eficaz.

Atualizar os caches consiste em schedular jobs para cada conta e para cada report do Gooddata - hoje, 1 conta representa 35 reports. Nesse processo, os jobs ficavam aguardando o término total do processamento para sair da fila, ocasionando todo o atraso da fila. Logo vimos que essa forma não era escalável e que não estávamos fazendo do jeito certo. Isto porque quando fazíamos um request para API do Gooddata ela nos devolvia - além do status - uma url de processamento que sempre é a mesma para aquele report daquela conta.

Montamos então um módulo responsável por gerenciar essa fila e saber quando tirar um report da fila que não terminou o processamento no lado do Gooddata. Esse comportamento libera slot para que outro job faça seu request e, caso receba status de fetched, atualize o documento no Mongo. Essa estratégia resolveu o problema da fila de processamento e conseguimos diminuir o tempo para atualizar os dados.

Conclusão

Seja lean. Crie soluções simples que gerem valor para seu usuário, aprenda com eles e evolua sua solução. Utilize cada tecnologia para o seu devido propósito e não a use apenas por ser o jeito mais fácil ou por preferencia pessoal. Assim como mongodb não deve ser utilizado para tudo, deve-se levantar as necessidades e analisar a melhor ferramenta à disposição para resolver determinado problema. Estes são passos que nos ajudam muito na evolução do RD Station e vem nos ajudando no processo de desenvolvimento do Marketing BI. Estamos em constante desenvolvimento e aprimoramento da feature, analisando o desempenho e estudando qual a melhor tecnologia para resolver ou escalar melhor o serviço. Por fim, sempre pense de forma simples e tenha métricas claras de qual o resultado esperado da sua nova solução.

Jean Matheus Souto

Jean Matheus Souto

Full Stack Developer

Comentários