Configurando Replica Sets no MongoDB

Filed Under (Desenvolvimento) by Samir on 14-08-2010

Tagged Under :

Atualmente de tudo o que venho utilizando no trabalho, o MongoDB sem sobra de dúvidas é uma das coisas mais empolgantes que ja vi nesses 3 últimos anos. A sensação é a mesma nos tempos antigos quando em 2003 migrei de ASP 3.0 para .NET (C#), depois em 2007 de .NET para Ruby on Rails e hoje 2010 trabalhando MongoDB e Python.

Bom, nesse post vamos falar um pouco do Replica Sets, um recurso novo que foi introduzido na versão 1.6 lançada recentemente.

O Replica Sets é um novo método de replicação com o mesmo conceito de Master/Slave Replication, porém, vai mais longe, também oferece o recurso de alta disponibilidade e recuperação automática entre os membros de um cluster.

Para trabalhar com Replica Sets você precisa ter em mente uma coisa, ter pelo menos um banco PRIMARY (Master) e N bancos SECONDARY (Slave), nesse caso vamos usar 3 bancos para replicação.

Configurando o Cluster

Crie 3 diretórios para cada banco de dados:

mkdir -p /data/db/rep0
mkdir -p /data/db/rep1
mkdir -p /data/db/rep2

Agora iremos iniciar 3 processos do MongoDB com o parâmetro –replSet.

./mongod --replSet teste --dbpath=/data/db/rep0 --port=10000
./mongod --replSet teste --dbpath=/data/db/rep1 --port=10001
./mongod --replSet teste --dbpath=/data/db/rep2 --port=10002

Se você reparar em cada processo, irá exibir a seguinte mensagem:

Sat Aug  14 16:30:19 [startReplSets] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

Isso indica que o sistema de replicação ainda não está funcionando pois ainda não foi inicializado.

Inicializando o Cluster

Para configurar o replica set, podemos conectar em qualquer um dos membros e passar um objeto de configuração pelo comando replSetInitiate.

./mongo localhost:10000
[samirmamude ~$]$ mongo localhost:10000
MongoDB shell version: 1.6.0
connecting to: localhost:10000/test
> config = {_id: 'teste', members: [
                          {_id: 0, host: 'localhost:10000'},
                          {_id: 1, host: 'localhost:10001'},
                          {_id: 2, host: 'localhost:10002'}]
           }

> rs.initiate(config);
{
   "info" : "Config now saved locally.  Should come online in about a minute.",
   "ok" : 1
}

Uma coisa muito legal é que em momento algum estamos informando explicitamente qual banco deve ser Master ou Slave, se você obvservar os logs o próprio Cluster decide por votação quem deve ser o Master e os demais como Slaves.

Para saber informações do cluster, digite o seguinte comando no shell.

rs.status()

Replication

Ainda conectado no shell, vamos inserir um registro qualquer.

db.post.save({titulo:"MongoDB e Replica Sets"});

Veja o log de cada banco Slave e observe como a informação é replicada entre os bancos.

Failover

Agora sem dúvida o recurso mais interessante do Replica Sets é a capacidade de alta disponibilidade. Imagine que por alguma falha ou acidente o banco Master para de funcionar. No caso vamos de propósito parar o processamento do banco Master.

^CSat Aug  14 16:50:16 got kill or ctrl c or hup signal 2 (Interrupt), will terminate after current cmd ends
Sat Aug  14 16:50:16 [interruptThread] now exiting
Sat Aug  14 16:50:16  dbexit:

Observando o log do primeiro banco Slave, você verá uma série de mensagens indicando um failover.

Sat Aug  14 16:50:16 [ReplSetHealthPollTask] replSet info localhost:10000 is now down (or slow to respond)
Sat Aug  14 16:50:17 [conn1] replSet info voting yea for 2
Sat Aug  14 16:50:17 [rs Manager] replSet not trying to elect self as responded yea to someone else recently
Sat Aug  14 16:50:27 [rs_sync] replSet SECONDARY

No segundo Slave:

Sat Aug  14 16:50:17 [ReplSetHealthPollTask] replSet info localhost:10000 is now down (or slow to respond)
Sat Aug  14 16:50:17 [rs Manager] replSet info electSelf 2
Sat Aug  14 16:50:17 [rs Manager] replSet PRIMARY
Sat Aug  14 16:50:27 [initandlisten] connection accepted from 127.0.0.1:61263 #5

Note que ambos notificaram que algo de errado aconteceu com o Master sendo assim, uma nova votação é realizada para decidir um novo Master, no caso nosso segundo Slave (10002), se você voltar o processamento do membro 10000, ele voltará como Slave, muito interessante não?

Isso acaba sendo muito útil caso você tenha uma aplicação com um grande volume de dados onde o principal requisito é a alta disponibilidade. Pois mesmo que um banco de dados pare de funcionar, sua aplicação dificilmente vai apresentar algum problema porque uma nova conexão é realizada com o novo Master eleito pelo cluster.

Para saber mais informações de Replica Sets, veja a prórpria documentação.

CRUD com ExtJS, Python e MongoDB – parte 1

Filed Under (Desenvolvimento) by Samir on 30-06-2010

Tagged Under : , ,

Nesse exemplo dividido em algumas partes, vou exemplificar como criar aplicações com a ExtJs utilizando algumas técnicas avançadas que irá ajudar a tornar seu código organizado e reaproveitável. Na primeira parte iremos configurar o projeto utilizando o framework Cyclone e iniciar a primeira parte do cadastro. Porém antes é melhor explicar algumas coisas.

Pré-requisitos:

Nesse exemplo vamos utilizar:

O que é esse tal de Cyclone?

Bom o Cyclone é um servidor web non-blocking escrito em Python com suporte a HTTP 1.1 e toda sua API é inspirada no Tornado no qual se trata de outro servidor web extremamente rápido, escalável e open source.

A vantagem em utilizar um servidor como o Cyclone é que podemos colocar o Nginx na frente para servir arquivos estáticos e criar múltiplas instancias do Cyclone utilizando load balancer, isso dispensa a velha combinação de Nginx+Apache com o módulo WSGI instalado.

Outra razão para utilizar o Cyclone é sua integração com o driver assíncrono TxMongo, ambos são baseados no Twisted, por tanto para facilitar a integração, não vamos reiventar a roda.

Iniciando o projeto

Bom primeiramente vamos criar a estrutura de diretórios para nosso projeto, conforme a imagem abaixo, segue a estrutura do projeto (ignore o arquivo twistd.pid).

Front-End

No arquivo index.html, incluimos os arquivos necessários para o funcionamento correto da lib ExtJs, atenção para o método static_url que iremos configurar esse atributo a seguir. Observe também que ao carrega a página no método Ext.onReady, foi incluído alguns overrides para sobreescrever as configurações no ExtJs, dessa forma evitamos a duplicação de código e centralizamos em um único lugar.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pt-br">
    <head>
        <title>Exemplo Cadastro: ExtJs e Python</title>
        <meta http-equiv="Content-type"  content="text/html; charset=utf-8" />
        <link rel="stylesheet" href="{{ static_url("ext/resources/css/ext-all.css") }}" type="text/css"/>
        <script type="text/javascript" src="{{ static_url("ext/adapter/ext/ext-base.js") }}"></script>
        <script type="text/javascript" src="{{ static_url("ext/ext-all.js") }}"></script>
        <script type="text/javascript" src="{{ static_url("ext/locale/ext-lang-pt_BR.js") }}"></script>
    </head>
    <body>
        <div id="panel" style="padding:10px;"></div>
        <script type="text/javascript" src="{{ static_url("src/cadastro.js") }}"></script>
        <script type="text/javascript">
            Ext.onReady(function(){
                Ext.BLANK_IMAGE_URL = '{{ static_url("ext/resources/images/default/s.gif") }}';
                // overrides
                Ext.form.FormPanel.prototype.labelAlign = 'top';
                Ext.form.FormPanel.prototype.bodyStyle = 'padding:5px';
                Ext.layout.FormLayout.prototype.trackLabels = true;
                // qtips
                Ext.QuickTips.init();

                var panel = new App.Form({ renderTo:'panel' });
            });
        </script>
    </body>
</html>

Agora vamos criar o formulário via javascript no arquivo cadastro.js. Aqui criamos o namespace e a classe para montar o componente que será aplicado ao div “panel”.

Ext.namespace("App");

// Stores para Combos e Grids
App.Stores = {
    _storeTipoPessoa: new Ext.data.ArrayStore({
        data:[['Pessoa Física'],['Pessoa Jurídica']],
        fields: ['nome']
    })
};

// Formulario para Cadastro de Pessoas
App.Form = Ext.extend(Ext.form.FormPanel, {
    title:'Cadastro de Pessoas',
    width:550,
    frame:true,

    initComponent:function(){

        // combos
        var _comboTipoPessoa = new Ext.form.ComboBox({
            fieldLabel: 'Tipo Pessoa',
            hiddenName: 'tipo_pessoa',
            triggerAction: 'all',
            displayField: 'nome',
            allowBlank: false,
            editable: false,
            mode: 'local',
            store: App.Stores._storeTipoPessoa,
            width:115
        });

        // colunas
        var _coluna = [{
            width:120,
            items:[_comboTipoPessoa]
        },{
            width:205,
            items:[{
                fieldLabel:'Nome', name:'nome', allowBlank:false, width:200
            }]
        },{
            items:[{
                fieldLabel:'E-mail', name:'email', allowBlank:false, width:200, vtype:'email'
            }]
        },{
            items:[{
                xtype:'textarea', fieldLabel:'Observações', name:'observacoes', width:525, height:50
            }]
        }];

        // Aplicar os elementos ao formulário e configurar os botões para ações no servidor
        Ext.apply(this,{
            items:[{
                items:[{
                    layout:'column',
                    defaults:{
                        defaultType:'textfield', /* definir o componente padrão */
                        layout:'form'
                    },
                    items:[_coluna]
                }]
            }],
            buttons:[{
                text:'Salvar'
            },{
                text:'Novo'
            },{
                text:'Excluir'
            }]
        });

        // super
        App.Form.superclass.initComponent.call(this);
    }
});

Criamos o namespace “App” e em seguida o namespace “App.Stores”, acho interessante centralizar todos os stores em um único lugar pois facilita a manutenção conforme seu projeto cresce, por tanto qualquer Store local ou remoto deve ficar dentro desse namespace. Fica uma dica se achar necessário, colocar todos os stores em outro aquivo como stores.js.

Em seguida criamos o componente “App.Form” que herda as configurações de um FormPanel, porém, poderiamos muito bem “programar” no estilo abaixo.

Ext.onReady(function(){
   var form = new Ext.form.FormPanel({title:'Cadastro...'});
   form.render(document.body);
   ...
});

Esse modelo por mais que funcione sem problemas, na maioria das vezes é a pior forma de se trabalhar com a ExtJs, pois você acaba deixando sua programação Javascript totalmente estruturada e em se tratando de um aplicativo Ajax onde na maioria dos casos o aplicativo é carregado somente em uma página, fica praticamente inviável colocar toda a lógica no arquivo index ou mesmo em arquivos separados, por tanto sempre programe Javascript Orientado a Objetos, faz bem para saúde. :-D

Back-End

O backend sempre acaba sendo um caso a parte, a ExtJs não exige que você trabalhe com uma linguagem específica de servidor, por tanto qualquer linguagem que manipule o protocolo HTTP é válida, é muito comum encontrar exemplos de integração com PHP, C#, Java, Ruby, etc…mas aqui vamos usar Python pois é o foco do assunto.

Nessa primeira etapa vamos editar o arquivo server.tac e colocar a classe WebMongo para inicializar o servidor e também a classe IndexHandler para renderizar o template index.html, veja como é simples.

# coding:utf-8

import os.path
import txmongo
import cyclone.web
from twisted.internet import defer
from twisted.application import service, internet

class IndexHandler(cyclone.web.RequestHandler):
    def get(self):
        self.render("index.html")

class WebMongo(cyclone.web.Application):
    def __init__(self):
        handlers = [
            (r"/", IndexHandler)
        ]

        mongo = txmongo.lazyMongoConnectionPool()

        settings = dict(
            static_path="./static",
            template_path="./static",
            xsrf_cookies=True,
            mongo=txmongo.lazyMongoConnectionPool()
        )

        cyclone.web.Application.__init__(self, handlers, **settings)

application = service.Application("webmongo")
wmservice = internet.TCPServer(8888, WebMongo(), interface="127.0.0.1")
wmservice.setServiceParent(application)

Na classe IndexHandler que herda de RequestHandler utilizamos o método herdado “get” para renderizar o template index.html. A classe WebMongo é quem faz o trabalho de configurar as rotas da aplicação no objeto list “handler”, criamos uma instância para o MongoDB na variável “mongo” e logo em seguida criamos um dicionário com as configurações para arquivos estáticos e outras opções.

Observe que para arquivos estáticos o mais indicado seria colocar o Nginx para essa função, assim evitamos o overhead desnecessário no Cyclone deixando o servidor processar apenas código Python.

Colocando para funcionar…

Com o MongoDB previamente instalado e configurado, vamos inicializar o servidor do banco de dados e logo em seguida inicializar o servidor Cyclone via Twisted.

MongoDB:

$ /data/mongo/bin/./mongod

O resultado deve ser algo parecido com:

/data/mongo/bin/./mongod --help for help and startup options
Wed Jun 30 22:24:40 Mongo DB : starting : pid = 6138 port = 27017 dbpath = /data/db/ master = 0 slave = 0  32-bit 

** NOTE: when using MongoDB 32 bit, you are limited to about 2 gigabytes of data
**       see http://blog.mongodb.org/post/137788967/32-bit-limitations for more

Wed Jun 30 22:24:40 db version v1.4.2, pdfile version 4.5
Wed Jun 30 22:24:40 git version: 53749fc2d547a3139fcf169d84d58442778ea4b0
Wed Jun 30 22:24:40 sys info: Darwin broadway.local 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:55:01 PDT 2009; root:xnu-1228.15.4~1/RELEASE_I386 i386 BOOST_LIB_VERSION=1_40
Wed Jun 30 22:24:40 waiting for connections on port 27017
Wed Jun 30 22:24:40 web admin interface listening on port 28017

Twisted Application:

 $ twistd -ny server.tac

Resultado:

2010-06-30 22:30:19-0300 [-] Log opened.
2010-06-30 22:30:19-0300 [-] twistd 10.0.0 (/opt/local/Library/Frameworks/Python.framework/Versions/2.6/Resources/Python.app/Contents/MacOS/Python 2.6.4) starting up.
2010-06-30 22:30:19-0300 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2010-06-30 22:30:19-0300 [-] __builtin__.WebMongo starting on 8888
2010-06-30 22:30:19-0300 [-] Starting factory <__builtin__.WebMongo instance at 0x1779ee0>

No navegador, acessar pelo endereço http://localhost:8888, o resultado final será parecido com a imagem abaixo.

Para visualizar os fontes da primeira parte, faça o download aqui. Na próxima parte vamos começar a integração de fato com MongoDB e estudar como aplicar o paradigma MVC de uma forma um pouco diferente mas totalmente funcional e muito produtiva.

Python e MongoDB

Filed Under (Desenvolvimento) by Samir on 24-05-2010

Tagged Under : ,

Parece que agora a nova tendência é mais e mais pessoas começarem a adotar bancos de dados NoSql e o MongoDB vem se destacando cada vez mais pela simplicidade, estabilidade e muito mas muito rápido!

Tem uma apresentação muito boa da PyCon 2010 onde Rick Copeland que trabalha na SourceForge, relata como migraram o portal utilizando Python, TurboGears e MongoDB, veja aqui.

Bom, como eu sempre destetei SQL a vida toda e acho que nunca vou aprender a gostar, comecei a estudar a possibilidade de migrar o MongoDB no sistema que venho trabalhando (segredo de estado!)…afinal minha vontade sempre foi ter algo mais voltado para Orientação Objetos e como eu sempre tive que recorrer a ORM’s da vida…, era sempre aquela duplicação de código interminável, mas enfim, depois de trabalhar por 2 semanas quase 15 horas por dia consegui migrar tudo e pra minha sorte, consegui reduzir drasticamamente o código do projeto. Com base nessa pequena experiência que passei, vou relatar as possibilidades que testei em Python e não foram muitas.

PyMongo:

O PyMongo é o driver oficial desenvolvido pela equipe do MongoDB, achei um pouco confuso no início, porque na verdade não se cria Models e nenhum tipo de Mapeamento de Objeto Relacional. Você simplesmente se conecta no banco, invoca a collection relacionada (ou tabela para melhor entendimento), passa um dicionário de dados em Python e manda salvar.

Exemplo:

import datetime
from pymongo import Connection

connection = Connection()
db = connection.test_database

post = {"author": "Samir", "text": "Python e MongoDB", "tags": ["mongodb", "python", "pymongo"], "date":datetime.datetime.utcnow()}
db.posts.save(post)

Esse modelo pode funcionar muito bem se você quebrar o código em partes, ou seja, um módulo ou classe que gerencia a conexão com banco de dados e se preferir pode-se criar classes para se representar uma Collection como no exemplo, criar uma classe Post, isso pode ser uma boa prática, mas não caia na besteira de mapear a classe com as colunas de uma Collection, você vai duplicar seu código atoa e só complicar as coisas, pois o MongoDB não segue a filosofia do modelo Relacional.

Depois de alguns testes e pesquisando mais opções eu decici não trabalhar com o driver diretamente, a única razão foi porque o PyMongo trabalha de forma síncrona e no meu projeto tudo é assíncrono e vou relatar isso logo abaixo.

MongoEngine

O MongoEngine foi outra opção que testei, ele é uma espécie de ORM que executa sobre o PyMongo, porém é um projeto muito recente e com pouca atividade, não aconselho a usar ainda.

A ideia do MongoEngine é trabalhar exatamente como o ORM do Django, você mapeia uma classe e usa os métodos para consultar e salvar objetos. Até ai tudo bem, mas de novo cai no velho problema, não tem o menor sentido usar ORM com o MongoDB, é claro que para fins de documentação esse pode ser o modelo ideal.

Exemplo:

from mongoengine import *
connect('test_database')

# Models
class User(Document):
    email = StringField(required=True)
    first_name = StringField(max_length=50)
    last_name = StringField(max_length=50)

class Post(Document):
    title = StringField(max_length=120, required=True)
    content = StringField(max_length=255, required=True)
    author = ReferenceField(User)
    tags = ListField(StringField(max_length=30))

# Salvando objetos
john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
john.save()

post = Post(title='Fun with MongoEngine', author=john)
post.content = 'Content here...'
post.tags = ['mongodb', 'mongoengine']
post.save()

# Consultando...
for post in Post.objects:
    print post.title

TxMongo

O TxMongo foi a melhor opção que encontrei, ele também roda sobre o PyMongo, porém toda sua estrutura é baseada no Twisted. A maior vantagem que percebi é por ele ser assíncrono e ter a possibilidade de criar vários pools de conexões ao mesmo tempo. O PyMongo por padrão cria apenas um pool de conexão o que limita bastante a performance da aplicação para múltiplos requests.

O modelo de implementação é praticamente identico ao PyMongo porém como ele é baseado no Twisted, antes precisa enteder como essa biblioteca funciona, confesso que apanhei um monte para aprender, mas vale a pena! :-D

Exemplo:

import time
import txmongo
from twisted.internet import defer, reactor

@defer.inlineCallbacks
def example():
    mongo = yield txmongo.MongoConnection()

    foo = mongo.foo # `foo` database
    test = foo.test # `test` collection

    # insert some data
    for x in xrange(10000):
        result = yield test.insert({"something":x*time.time()}, safe=True)
        print result

if __name__ == '__main__':
    example().addCallback(lambda ign: reactor.stop())
    reactor.run()

Conclusão:

Bom depois de testar essas possibilidades, o projeto vem sendo desenvolvido com ExtJS para o tudo o que imaginar no FrontEnd e no BackEnd substitui o Django pelo Cyclone que é um mini framework web assíncrono também baseado no Twisted que se integra perfeitamente com o TxMongo. No próximo post, pretendo exemplificar como integrar essas bibliotecas. Críticas ou sugestões serão bem vindas ;)