PD
Banco de dados em memória? Não, cache de objetos!
Fechado
Você já deve ter passado por algum problema de desempenho! Quem nunca passou. Aquele monte de SQL complexo para lá e para cá, análises de queries sem fim, você tentando otimizar o máximo possível, resumindo queries, eliminando outras, criando índices, e mesmo assim o programa está lento por que se você tirar mais alguma query tudo irá parar de funcionar... Então você pensa "Como seria bom se todos dados estivessem em memória, seria muito mais rápido".
Neste artigo falaremos em como acelerar o desempenho de uma aplicação mantendo objetos de negócio em memória. Esta estratégia já foi aplicada em aplicações críticas em grandes organizações e mostrou ganhos de até 100% no desempenho de rotinas complexas. Vamos ver como aplicar.
Em primeiro lugar, para essa abordagem funcionar, é importante ler este outro artigo "Pensando em Objetos não em SQL para construir uma Aplicação" (www.adianti.com.br/forum/pt/view_876?pensando-em-objetos-nao-em-sql-), isto por que a estratégia que vamos utilizar é de cache de objetos, não de datasets. Bancos de dados como o PostgreSQL possuem o recurso de cache de datasets, ou seja, de páginas de consulta. Assim, se você repetir a mesma consulta, poderá se beneficiar do cache. Nossa estratégia é em nível de aplicação, ou seja, no PHP, e o objetivo é manter objetos inteiros na memória para que, quando eles forem necessários, não seja preciso carregar a partir do banco de dados.
Como disse, para esta estratégia funcionar, é preciso modelar, e construir a aplicação somente em torno de objetos. Com isso, esqueça aquelas queries enormes. A aplicação funcionará por meio de relacionamento entre objetos. Dessa forma, se você precisar de uma lista de todos os alunos de uma determinada turma, no lugar de fazer uma query que já traz vários dados para a memória de uma única vez, como no SQL a seguir:
Voce fará do jeito a seguir, percorrendo uma série de objetos relacionados por meio de associações. Certamente construirmos um SQL com Joins tem desempenho superior à buscar cada objeto relacionado sempre que ele for necessário e provavelmente algum DBA deve estar se retorcendo agora em alguma cadeira por aí. Mas vamos por partes.
Tornar o código legível é só o primeiro passo. Agora devemos torná-lo eficiente. Para vamos usar cache de objetos em memória. Para tal, pegamos tabelas relativamente pequenas (até 10 mil registros) e ativamos o cache delas. A não ser que você tenha MUITA memória, não é possível armazenar tabelas grandes (500 mil registros) em memória sob a pena de ficar sem RAM para o processamento das outras operações. É importante lembrar que um objeto em memória ocupa mais espaço do que somente os seus dados.
Neste caso, podemos ligar cache para as tabelas de cidade, e estados. Eventualmente para alunos e disciplinas também. Já turmas tendem a surgir todo o semestre e a tabela tende a ficar muito grande. Nesse caso o cache pode ocupar muito espaço da RAM.
O cache de objetos no Adianti funciona por que utilizamos o padrão de projeto Active Record. Assim, cada registro do banco de dados é tratado como um objeto em memória. O framework possui de maneira nativa uma classe chamada TAPCache, que se comunica com o APC do PHP. Para quem não sabe, o APC permite armazenar variáveis em memória RAM. O funcionamento se dá como exposto na figura a seguir. Sempre que um objeto é gravado pelo método store(), ele é gravado tanto no banco de dados (a), quanto no cache (d). Agora, sempre que um objeto for requisitado por meio da instanciação direta (Ex: new Estado(10)), ou indireta ($matricula->aluno->cidade->nome), primeiro o framework verifica se o objeto está na memória RAM (c). Caso encontrado, ele é disponibilizado imediatamente para a aplicação. Caso ele não esteja na memória RAM (c), então ele é carregado do banco de dados (b), e gravado na memória RAM (d), para então ser disponibilizado para a aplicação.
Este recurso vem desabilitado por padrão. Mas para ligar é muito simples. Em primeiro lugar é preciso ter o APC instalado (apt-get install php5-apcu). E em seguida, ligar o cache nas classes que você deseja manter em memória. É importante lembrar que os objetos são carregados no cache na medida em que são acessados pela primeira vez. Veja a linha com a definição da constante CACHECONTROL. Você poderá usar a classe nativa TAPCache, ou indicar ali outra classe para manipular o cache. Você poderá implementar uma classe que armazena os dados em Memcached, por exemplo. A classe indicada deverá implementar a interface AdiantiRegistryInterface.
Como falei antes no artigo, estra estratégia já foi comprovada e se demonstrou muito eficaz. Um de nossos clientes, a Univates (www.univates.br), tinha um problema crítico na hora da matrícula, eram quase 3 mil alunos tentando se matricular simultaneamente. O problema é que a rotina de matrícula é a mais complexa e faz consulta em algumas dezenas de tabelas por meio de objetos relacionados. Somente uma tabela que guardava configurações, era consultada mais de 50 vezes em diferentes pontos do processo. Decidimos ligar cache para algumas tabelas que não eram gigantes, porém eram consultadas muitas vezes em vários processos (alunos, períodos, cidades, cursos, disciplinas, dentre outras). E o resultado foi incrível. Em algumas rotinas o tempo de processamento caiu para menos da metade do tempo original, e em alguns casos caiu para 30% do tempo original. Com isso, gerou um alívio banco de dados, que em 6 meses (1 semestre), deixou de responder à pelo menos 12 milhão de consultas SQL. Nada mal!
Neste artigo falaremos em como acelerar o desempenho de uma aplicação mantendo objetos de negócio em memória. Esta estratégia já foi aplicada em aplicações críticas em grandes organizações e mostrou ganhos de até 100% no desempenho de rotinas complexas. Vamos ver como aplicar.
Em primeiro lugar, para essa abordagem funcionar, é importante ler este outro artigo "Pensando em Objetos não em SQL para construir uma Aplicação" (www.adianti.com.br/forum/pt/view_876?pensando-em-objetos-nao-em-sql-), isto por que a estratégia que vamos utilizar é de cache de objetos, não de datasets. Bancos de dados como o PostgreSQL possuem o recurso de cache de datasets, ou seja, de páginas de consulta. Assim, se você repetir a mesma consulta, poderá se beneficiar do cache. Nossa estratégia é em nível de aplicação, ou seja, no PHP, e o objetivo é manter objetos inteiros na memória para que, quando eles forem necessários, não seja preciso carregar a partir do banco de dados.
Como disse, para esta estratégia funcionar, é preciso modelar, e construir a aplicação somente em torno de objetos. Com isso, esqueça aquelas queries enormes. A aplicação funcionará por meio de relacionamento entre objetos. Dessa forma, se você precisar de uma lista de todos os alunos de uma determinada turma, no lugar de fazer uma query que já traz vários dados para a memória de uma única vez, como no SQL a seguir:
SELECT aluno.nome, cidade.nome, estado.nome
FROM turma, matricula, aluno, cidade, estado
WHERE turma.id = matricula.id_turma
AND matricula.id_aluno = aluno.id
AND aluno.id_cidade = cidade.id
AND cidade.id_estado = estado.id
AND turma.id = 10
Voce fará do jeito a seguir, percorrendo uma série de objetos relacionados por meio de associações. Certamente construirmos um SQL com Joins tem desempenho superior à buscar cada objeto relacionado sempre que ele for necessário e provavelmente algum DBA deve estar se retorcendo agora em alguma cadeira por aí. Mas vamos por partes.
- <?php
- $turma = new Turma(10);
- print $turma->disciplina->nome;
- print $turma->disciplina->curso->nome;
- foreach ($turma->getMatriculas() as $matricula)
- {
- print $matricula->aluno->nome;
- print $matricula->aluno->cidade->nome;
- print $matricula->aluno->cidade->estado->nome;
- }
- ?>
Tornar o código legível é só o primeiro passo. Agora devemos torná-lo eficiente. Para vamos usar cache de objetos em memória. Para tal, pegamos tabelas relativamente pequenas (até 10 mil registros) e ativamos o cache delas. A não ser que você tenha MUITA memória, não é possível armazenar tabelas grandes (500 mil registros) em memória sob a pena de ficar sem RAM para o processamento das outras operações. É importante lembrar que um objeto em memória ocupa mais espaço do que somente os seus dados.
Neste caso, podemos ligar cache para as tabelas de cidade, e estados. Eventualmente para alunos e disciplinas também. Já turmas tendem a surgir todo o semestre e a tabela tende a ficar muito grande. Nesse caso o cache pode ocupar muito espaço da RAM.
O cache de objetos no Adianti funciona por que utilizamos o padrão de projeto Active Record. Assim, cada registro do banco de dados é tratado como um objeto em memória. O framework possui de maneira nativa uma classe chamada TAPCache, que se comunica com o APC do PHP. Para quem não sabe, o APC permite armazenar variáveis em memória RAM. O funcionamento se dá como exposto na figura a seguir. Sempre que um objeto é gravado pelo método store(), ele é gravado tanto no banco de dados (a), quanto no cache (d). Agora, sempre que um objeto for requisitado por meio da instanciação direta (Ex: new Estado(10)), ou indireta ($matricula->aluno->cidade->nome), primeiro o framework verifica se o objeto está na memória RAM (c). Caso encontrado, ele é disponibilizado imediatamente para a aplicação. Caso ele não esteja na memória RAM (c), então ele é carregado do banco de dados (b), e gravado na memória RAM (d), para então ser disponibilizado para a aplicação.
Este recurso vem desabilitado por padrão. Mas para ligar é muito simples. Em primeiro lugar é preciso ter o APC instalado (apt-get install php5-apcu). E em seguida, ligar o cache nas classes que você deseja manter em memória. É importante lembrar que os objetos são carregados no cache na medida em que são acessados pela primeira vez. Veja a linha com a definição da constante CACHECONTROL. Você poderá usar a classe nativa TAPCache, ou indicar ali outra classe para manipular o cache. Você poderá implementar uma classe que armazena os dados em Memcached, por exemplo. A classe indicada deverá implementar a interface AdiantiRegistryInterface.
- <?php
- class Customer extends TRecord
- {
- const TABLENAME = 'customer';
- const PRIMARYKEY = 'id';
- const IDPOLICY = 'max'; // {max, serial}
- const CACHECONTROL = 'TAPCache';
- }
- ?>
Como falei antes no artigo, estra estratégia já foi comprovada e se demonstrou muito eficaz. Um de nossos clientes, a Univates (www.univates.br), tinha um problema crítico na hora da matrícula, eram quase 3 mil alunos tentando se matricular simultaneamente. O problema é que a rotina de matrícula é a mais complexa e faz consulta em algumas dezenas de tabelas por meio de objetos relacionados. Somente uma tabela que guardava configurações, era consultada mais de 50 vezes em diferentes pontos do processo. Decidimos ligar cache para algumas tabelas que não eram gigantes, porém eram consultadas muitas vezes em vários processos (alunos, períodos, cidades, cursos, disciplinas, dentre outras). E o resultado foi incrível. Em algumas rotinas o tempo de processamento caiu para menos da metade do tempo original, e em alguns casos caiu para 30% do tempo original. Com isso, gerou um alívio banco de dados, que em 6 meses (1 semestre), deixou de responder à pelo menos 12 milhão de consultas SQL. Nada mal!
Pablo o seu site adianti.com.br é muito rápido, você utiliza essa mesma técnica ou é otimização do apache ou nginx. Eu utilizo servidores da amazon AWS e tem um serviço ElastiCache ( cache de memória distribuída na nuvem utilizando o Memcached ).
Isso seria a mesma coisa da explicação?
Sei que foge do assunto mais para aplicações com muitos acessos você recomenda um provedor próprio, ou isso não interfere no desempenho da aplicação.
Eu vejo muitos artigos de como otimizar do PHP até o banco mais antes do PHP acontece muita coisa.
Muito bom o artigo.
Caro Pablo,
Primeiramente parabéns pelo seu trabalho.
Tira uma dúvida, por favor: Se algum registro já instanciado em memória, for alterado no banco por outro usuário, o sistema tem mecanismos para reconhecer esta alteração?
Abs,
Lucas
Pablo dá pra fazer UNION nas pesquisas sql????como exemplo
Lucas,
Não, o registro não deve ser alterado diretamente no banco.
Nesse caso, precisaríamos rodar um script para atualizar somente aquele ID em memória.
Att,
Pablo, como instalo o APCU no CENTOS?
Grato,
Nilson,
Desculpe, vi sua mensagem somente agora.
O sistema de notificação do fórum não é tão rápido.
Destaquei dois posts:
stackoverflow.com/questions/26818656/php5-6-and-apc-installation
https://doc.owncloud.org/server/8.1/admin_manual/configuration_server/caching_configuration.html
Att,
Pablo