Testes com vários usuários
David Glasser de Meteoro escreveu um blog sobre um problema de consulta do MongoDB sem documentos correspondentes que ele encontrou. É fácil reproduzir o problema no mecanismo MongoDB MMAPv1 e MongoDB WiredTiger. Aqui estão as conclusões do artigo dele (a ênfase é minha)
Resumindo a história...
-
Esse problema não afeta as consultas que não usam um índice, como as consultas que apenas procuram um documento por ID.
-
Isso não afeta as consultas que fazem explicitamente uma correspondência de igualdade de valor único em todos os campos usados na chave do índice.
-
Isso não afeta as consultas que usam índices cujos campos nunca são modificados depois que o documento é inserido originalmente.
-
Mas qualquer outro tipo de consulta ao MongoDB pode não incluir todos os documentos correspondentes!
Aqui está outra maneira de ver isso. No MongoDB, se a consulta puder recuperar dois documentos usando um índice secundário (índice em algo diferente de _id) quando operações simultâneas estiverem em andamento, os resultados poderão estar errados. Esse é um cenário comum em muitos aplicativos de banco de dados.
Aqui está o teste:
-
Crie um contêiner: Bucket, tabela ou coleção.
-
Carregue os dados com um pequeno conjunto de dados, por exemplo, 300 mil documentos.
-
Crie um índice no campo que você deseja filtrar (predicados).
-
Em uma sessão, atualize o campo indexado em uma sessão e consulte na outra.
Teste do MongoDB
Etapas para reproduzir o problema no MongoDB:
-
Instale o MongoDB 3.2.
-
Crie o mongod com o MMAPv1 ou o WiredTiger.
-
Carregar dados usando tpcc.py
-
python tpcc.py -warehouses 1 -no-execute mongodb
-
Obter a contagem
> use tpcc
> db.ORDER_LINE.find().count();
299890
-
db.ORDER_LINE.ensureIndex({state:1});
Experiência 1 do MongoDB: atualizar para um valor mais alto
Configure o campo de estado com o valor aaaaaa e, em seguida, atualize simultaneamente esse valor para zzzzzz e consulte o número total de documentos com os dois valores ['aaaaaa','zzzzzz'] correspondentes ao campo. Quando o valor do campo indexado se move do valor mais baixo (aaaaaa) para o mais alto (zzzzzz), essas entradas estão se movendo de um lado para o outro da árvore B. Agora, estamos tentando ver se a varredura retorna um valor duplicado, traduzido em um valor count() mais alto.
> db.ORDER_LINE.update({OL_DIST_INFO:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 299890, "nUpserted" : 0, "nModified" : 299890 })
> db.ORDER_LINE.find({state:{$in:['aaaaaa','zzzzzz']}}).count();
299890
> db.ORDER_LINE.find({state:{$in:['aaaaaa','zzzzzz']}}).explain();
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "tpcc.ORDER_LINE",
"indexFilterSet" : false,
"parsedQuery" : {
"state" : {
"$in" : [
"aaaaaa",
"zzzzzz"
]
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"estágio" : "IXSCAN",
"keyPattern" : {
"state" : 1
},
"indexName" : "state_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"state" : [
"["aaaaaa", "aaaaaa"]",
"["zzzzzz", "zzzzzz"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "Keshavs-MacBook-Pro.local",
"port" : 27017,
"versão" : "3.0.2",
"gitVersion" : "6201872043ecbbc0a4cc169b5482dcf385fc464f"
},
"ok" : 1
}
-
Declaração de atualização 1: Atualizar todos os documentos para definir state = "zzzzzz"
db.ORDER_LINE.update({OL_DIST_INFO:{$gt:""}},
{$set: {estado: "zzzzzz"}}, {multi:true});
-
Declaração de atualização 2: Atualize todos os documentos para definir state = "aaaaaa"
db.ORDER_LINE.update({OL_DIST_INFO:{$gt:""}},
{$set: {estado: "aaaaaa"}}, {multi:true});
3. Declaração de contagem: Count documents:(state in ["aaaaaa", "zzzzzz"])
db.ORDER_LINE.find({state:{$in:['aaaaaa','zzzzzz']}}).count();
Tempo |
Sessão 1: Declaração de atualização de problemas1 (atualizar estado = "zzzzzz") |
Sessão 2: Declaração de contagem de emissões continuamente. |
T0 |
Início da declaração de atualização |
Contagem = 299890 |
T1 |
Declaração de atualização continua |
Contagem = 312736 |
T2 |
Declaração de atualização continua |
Contagem = 312730 |
T3 |
Declaração de atualização continua |
Contagem = 312778 |
T4 |
Declaração de atualização continua |
Contagem = 312656 |
T4 |
Declaração de atualização continua |
Contagem = 313514 |
T4 |
Declaração de atualização continua |
Contagem = 303116 |
T4 |
Declaração de atualização concluída |
Contagem = 299890 |
Resultado: Nesse cenário, o índice faz uma contagem dupla de muitos documentos e informa mais do que realmente tem.
Causa: Os dados no nível de folha da árvore B são classificados. À medida que a atualização da árvore B é atualizada de aaaaaa para zzzzzz, as chaves na extremidade inferior são movidas para a extremidade superior. As varreduras simultâneas não sabem dessa movimentação. O MongoDB não implementa uma varredura estável e conta as entradas à medida que elas chegam. Portanto, em um sistema de produção com muitas atualizações em andamento, ele pode contar o mesmo documento duas, três ou mais vezes. Isso depende apenas das operações simultâneas.
Experiência 2 do MongoDB: atualizar para um valor mais baixo
Vamos fazer a operação inversa para atualizar os dados de "zzzzzz" para "aaaaaa". Nesse caso, as entradas do índice estão se movendo de um valor mais alto para um valor mais baixo, fazendo com que a varredura não veja alguns dos documentos qualificados, o que mostra uma subcontagem.
Tempo |
Sessão 1: Declaração de atualização de problemas2 (update state = "aaaaaa") |
Sessão 2: Declaração de contagem de emissões continuamente. |
T0 |
Início da declaração de atualização |
Contagem = 299890 |
T1 |
Declaração de atualização continua |
Contagem = 299728 |
T2 |
Declaração de atualização continua |
Contagem = 299750 |
T3 |
Declaração de atualização continua |
Contagem = 299780 |
T4 |
Declaração de atualização continua |
Contagem = 299761 |
T4 |
Declaração de atualização continua |
Contagem = 299777 |
T4 |
Declaração de atualização continua |
Contagem = 299815 |
T4 |
Declaração de atualização concluída |
Contagem = 299890 |
Resultado: Nesse cenário, o índice perde muitos documentos e informa menos documentos do que realmente tem.
Causa: Isso expõe o efeito inverso. Quando as chaves com valores zzzzzz são modificadas para aaaaaa, os itens vão da extremidade superior para a inferior da árvore B. Mais uma vez, como não há estabilidade nas varreduras, ele perderia as chaves que foram movidas da extremidade superior para a inferior.
Experiência 3 do MongoDB: UPDATES simultâneas
Duas sessões atualizam o campo indexado de forma simultânea e contínua. Nesse caso, com base nas observações anteriores, cada uma das sessões apresenta problemas de supercontagem e subcontagem. O resultado de nModified varia porque o MongoDB relata apenas as atualizações que alteraram o valor.
Mas o número total de documentos modificados nunca é maior que 299980. Portanto, o MongoDB evita atualizar o mesmo documento duas vezes, tratando assim o problema clássico de halloween. Como eles não têm uma varredura estável, presumo que lidam com isso mantendo listas de objectIDs atualizados durante essa declaração de várias atualizações e evitando a atualização se o mesmo objeto aparecer como um documento qualificado.
SESSÃO 1
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 299890, "nUpserted" : 0, "nModified" : 299890 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 303648, "nUpserted" : 0, "nModified" : 12026 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 194732, "nUpserted" : 0, "nModified" : 138784 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 334134, "nUpserted" : 0, "nModified" : 153625 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 184379, "nUpserted" : 0, "nModified" : 146318 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 335613, "nUpserted" : 0, "nModified" : 153403 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 183559, "nUpserted" : 0, "nModified" : 146026 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 335238, "nUpserted" : 0, "nModified" : 149337 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 187815, "nUpserted" : 0, "nModified" : 150696 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 335394, "nUpserted" : 0, "nModified" : 154057 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 188774, "nUpserted" : 0, "nModified" : 153279 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 334408, "nUpserted" : 0, "nModified" : 155970 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 299890, "nUpserted" : 0, "nModified" : 0 })
>
SESSÃO 2:
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 302715, "nUpserted" : 0, "nModified" : 287864 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 195248, "nUpserted" : 0, "nModified" : 161106 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 335526, "nUpserted" : 0, "nModified" : 146265 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 190448, "nUpserted" : 0, "nModified" : 153572 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 336734, "nUpserted" : 0, "nModified" : 146487 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 189321, "nUpserted" : 0, "nModified" : 153864 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 334793, "nUpserted" : 0, "nModified" : 150553 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 186274, "nUpserted" : 0, "nModified" : 149194 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 336576, "nUpserted" : 0, "nModified" : 145833 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "aaaaaa"}}, {multi:true});
WriteResult({ "nMatched" : 183635, "nUpserted" : 0, "nModified" : 146611 })
> db.ORDER_LINE.update({state:{$gt:""}}, {$set: {state: "zzzzzz"}}, {multi:true});
WriteResult({ "nMatched" : 336904, "nUpserted" : 0, "nModified" : 143920 })
>
Teste do Couchbase
-
Instale o Couchbase 4.5.
-
Carregar dados usando tpcc.py
-
python tpcc.py -warehouses 1 -no-execute n1ql
-
Obter a contagem
> SELECT COUNT(*) FROM ORDER_LINE;
300023
-
CREATE INDEX i1 ON ORDER_LINE(state);
-
UPDATE ORDER_LINE SET state = 'aaaaaa';
Experiência 1 do Couchbase: atualizar para um valor mais alto
Faça a configuração inicial.
> UPDATE ORDER_LINE SET state = 'aaaaaa' WHERE OL_DIST_INFO > "";
Verifique se o campo (atributo) Count. state com valores "aaaaaa" é 300023.
> select count(1) a_cnt FROM ORDER_LINE where state = 'aaaaaa'
UNION ALL
select count(1) z_cnt FROM ORDER_LINE where state = 'zzzzzz';
"results": [
{
"a_cnt": 300023
},
{
"z_cnt": 0
}
],
Vamos garantir que a varredura de índice ocorra na consulta.
EXPLAIN SELECT COUNT(1) AS totalcnt
DE ORDER_LINE
WHERE state = 'aaaaaa' or state = 'zzzzzz';
"~children": [
{
"#operator": "DistinctScan",
"scan": {
"#operator": "IndexScan",
"covers": [
"cover ((ORDER_LINE
.estado
))”,
"cover ((meta(ORDER_LINE
).id
))”
],
"index" (índice): "i2",
"index_id": "665b11a6c36d4136",
"Espaço-chave": "ORDER_LINE",
"namespace": "default",
"spans": [
{
"Range": {
"Alto": [
""aaaaaa""
],
"Inclusão": 3,
"Low": [
""aaaaaa""
]
}
},
{
"Range": {
"Alto": [
""zzzzzz""
],
"Inclusão": 3,
"Low": [
""zzzzzz""
]
}
}
],
"using": "gsi"
}
},
Também podemos usar UNION ALL de dois predicados separados (state = 'aaaaaa') e (state = 'zzzzzz') para obter a contagem do índice de forma eficiente.
cbq> explain select count(1) a_cnt
DE ORDER_LINE
where state = 'aaaaaa'
UNION ALL
select count(1) z_cnt
DE ORDER_LINE
where state = 'zzzzzz';
{
"requestID": “ef99e374-48f5-435c-8d54-63d1acb9ad22”,
"signature" (assinatura): "json",
"results": [
{
"plan": {
"#operator": "UnionAll",
"children": [
{
"#operator": "Sequence",
"~children": [
{
"#operator": "IndexCountScan",
"covers": [
"cover ((ORDER_LINE
.estado
))”,
"cover ((meta(ORDER_LINE
).id
))”
],
"index" (índice): "i2",
"index_id": "665b11a6c36d4136",
"Espaço-chave": "ORDER_LINE",
"namespace": "default",
"spans": [
{
"Range": {
"Alto": [
""aaaaaa""
],
"Inclusão": 3,
"Low": [
""aaaaaa""
]
}
}
],
"using": "gsi"
},
{
"#operator": "IndexCountProject",
"result_terms": [
{
"as": "a_cnt",
"expr": "count(1)"
}
]
}
]
},
{
"#operator": "Sequence",
"~children": [
{
"#operator": "IndexCountScan",
"covers": [
"cover ((ORDER_LINE
.estado
))”,
"cover ((meta(ORDER_LINE
).id
))”
],
"index" (índice): "i2",
"index_id": "665b11a6c36d4136",
"Espaço-chave": "ORDER_LINE",
"namespace": "default",
"spans": [
{
"Range": {
"Alto": [
""zzzzzz""
],
"Inclusão": 3,
"Low": [
""zzzzzz""
]
}
}
],
"using": "gsi"
},
{
"#operator": "IndexCountProject",
"result_terms": [
{
"as": "z_cnt",
"expr": "count(1)"
}
]
}
]
}
]
},
"text": "select count(1) a_cnt FROM ORDER_LINE where state = 'aaaaaa' UNION ALL select count(1) z_cnt FROM ORDER_LINE where state = 'zzzzzz'"
}
],
"status": "success",
"metrics": {
"elapsedTime": "2.62144ms",
"executionTime": "2.597189ms",
"resultCount": 1,
"resultSize": 3902
}
}
Configure o campo state com o valor aaaaaa. Em seguida, atualize esse valor para zzzzzz e, ao mesmo tempo, consulte o número total de documentos com um dos dois valores.
Sessão 1: Atualizar o campo de estado para o valor zzzzzz
UPDATE ORDER_LINE SET state = 'zzzzzz' WHERE OL_DIST_INFO > "";
{ "mutationCount": 300023 }
Sessão 2: consultar a contagem de 'aaaaaa' e 'zzzzzz' de ORDER_LINE.
Tempo |
Sessão 1: Declaração de atualização de problemas1 (atualizar estado = "zzzzzz") |
a_cnt |
z_cnt |
Total |
T0 |
Início da declaração de atualização |
300023 |
0 |
300023 |
T1 |
Declaração de atualização continua |
288480 |
11543 |
300023 |
T2 |
Declaração de atualização continua |
259157 |
40866 |
300023 |
T3 |
Declaração de atualização continua |
197167 |
102856 |
300023 |
T4 |
Declaração de atualização continua |
165449 |
134574 |
300023 |
T5 |
Declaração de atualização continua |
135765 |
164258 |
300023 |
T6 |
Declaração de atualização continua |
86584 |
213439 |
300023 |
T7 |
Declaração de atualização concluída |
0 |
300023 |
300023 |
Resultado: As atualizações do índice ocorrem à medida que os dados são atualizados. À medida que os valores migram de "aaaaaa" para "zzzzzz", eles não são contados duas vezes.
Os índices do Couchbase fornecem varreduras estáveis por meio de instantâneos do índice em uma frequência regular. Embora esse seja um esforço considerável de engenharia, como vimos nesta edição, ele fornece estabilidade de respostas mesmo em situações extremas de simultaneidade.
Os dados que a varredura de índice recupera serão do ponto em que a varredura começa. As atualizações subsequentes que chegarem simultaneamente não serão retornadas. Isso também proporciona outro nível de estabilidade.
É importante observar que a estabilidade das varreduras é fornecida para cada varredura de índice. Os índices tiram instantâneos a cada 200 milissegundos. Quando você tem uma consulta de longa duração com várias varreduras de índice no mesmo ou em vários índices, a consulta pode usar vários snapshots. Portanto, diferentes varreduras de índice em uma consulta de longa duração retornarão resultados diferentes. Esse é um caso de uso que melhoraremos em uma versão futura para usar o mesmo snapshot em todas as varreduras de uma única consulta.
Experiência 2 do Couchbase: atualizar para um valor mais baixo
Vamos fazer a operação inversa para atualizar os dados de "zzzzzz" de volta para "aaaaaa".
Sessão 1: Atualizar o campo de estado para o valor aaaaaa
UPDATE ORDER_LINE SET state = 'aaaaaa' WHERE OL_DIST_INFO > "";
{ "mutationCount": 300023 }
Sessão 2: consultar a contagem de 'aaaaaa' e 'zzzzzz' de ORDER_LINE.
Tempo |
Sessão 1: Declaração de atualização de problemas1 (update state = "aaaaaa") |
a_cnt |
z_cnt |
Total |
T0 |
Início da declaração de atualização |
0 |
300023 |
300023 |
T1 |
Declaração de atualização continua |
28486 |
271537 |
300023 |
T2 |
Declaração de atualização continua |
87919 |
212104 |
300023 |
T3 |
Declaração de atualização continua |
150630 |
149393 |
300023 |
T4 |
Declaração de atualização continua |
272358 |
27665 |
300023 |
T5 |
Declaração de atualização continua |
299737 |
286 |
300023 |
T6 |
Declaração de atualização concluída |
0 |
300023 |
300023 |
Experiência 3 do Couchbase: ATUALIZAÇÕES simultâneas
Duas sessões atualizam o campo indexado de forma simultânea e contínua. As varreduras estáveis do índice sempre retornam a lista completa de documentos qualificados: 30023 e o Couchbase atualiza todos os documentos e informa a contagem de mutações como 30023. Uma atualização é uma atualização, independentemente de o valor antigo ser o mesmo que o novo valor ou não.
Sessão 1:
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
Sessão 2:
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'aaaaaa' where state > "";
{ "mutationCount": 300023 }
update ORDER_LINE set state = 'zzzzzz'where state > "";
{ "mutationCount": 300023 }
Conclusões
MongoDB:
-
As consultas do MongoDB perderão documentos se houver atualizações simultâneas que movam os dados de uma parte do índice para outra parte do índice (de cima para baixo).
-
As consultas do MongoDB retornarão o mesmo documento várias vezes se houver atualizações simultâneas que movam os dados de uma parte do índice para outra parte do índice (de baixo para cima).
-
As atualizações múltiplas simultâneas no MongoDB terão o mesmo problema e deixarão de atualizar todos os documentos necessários, mas evitam atualizar o mesmo documento duas vezes em uma única instrução.
-
Ao desenvolver aplicativos que usam o MongoDB, você deve projetar um modelo de dados para selecionar e atualizar apenas um documento para cada consulta. Evite consultas de vários documentos no MongoDB, pois elas retornarão resultados incorretos quando houver atualizações simultâneas.
Couchbase:
-
O Couchbase retorna o número esperado de documentos qualificados mesmo quando há atualizações simultâneas. As varreduras de índice estáveis no Couchbase oferecem a proteção ao aplicativo que o MongoDB não oferece.
-
As atualizações simultâneas se beneficiam de varreduras de índice estáveis e processam todos os documentos qualificados quando a declaração do aplicativo é emitida.
Agradecimentos
Agradecemos a Sriram Melkote e Deepkaran Salooja, da equipe de indexação do Couchbase, pela revisão e pelas valiosas contribuições para este trabalho.
Referências
-
Consistência forte do MongoDB: https://www.mongodb.com/nosql-explained
-
Concorrência do MongoDB: https://docs.mongodb.com/manual/faq/concurrency/
-
As consultas do MongoDB nem sempre retornam todos os documentos correspondentes! https://engineering.meteor.com/mongodb-queries-dont-always-return-all-matching-documents-654b6594a827#.s9y0yheuv
-
Couchbase: http://www.couchbase.com