lunes, 29 de noviembre de 2010

MongoDB: Consistencia distribuida. Parte 5

En la parte 2 hablamos someramente sobre consistencia eventual de "escritor simple". Aquí discutiremos sobre muchos escritores, y definiremos este término con más precisión.

Por "muchos escritores" nos referimos a un sistema donde diferentes servidores de datos pueden recibir concurrentemente escrituras (y asíncronas). Algunos ejemplos de sistemas que reciben escrituras eventualmente consistentes:

* Amazon Dynamo
* Replicación CouchDB maestro-maestro

Con la consistencia eventual multi-escritora, necesitamos direccionar el fenómeno de conflicto de escrituras. Las escrituras a dos servidores al mismo tiempo pueden actualizar el mismo objeto. Hemos de solucionar el conflicto de una manera que sea aceptable para el caso en cuestión. Algunas soluciones podrían ser:

* Última escritura gana
* Fusión programática
* Operaciones conmutativas


Última escritura gana

Última escritura gana es un popular método por defecto en muchos sistemas. Si recibimos una operación que es antigua, simplemente la ignoramos. En un sistema distribuido, la definición de "último" es tan duro que los relojes no pueden ser sincronizados perfectamente. Así que muchos sistemas utilizan un vector de relojes.

Inserciones

Sorprendentemente, una operación tradicional de inserción es delicada con muchos escritores. Considera estas operaciones realizadas al mismo tiempo en diferentes servidores:

op1: insert( { _id : 'joe', age : 30 } )
op2: insert( { _id : 'joe', age : 33 } )


Si aplicamos nativamente estas dos operaciones en cualquier orden, obtendremos un resultado inconsistente. Insertar significa típicamente:

if( !already_exists(x._id) ) then set( x );
si( !ya_existe(x._id) ) entonces establece( x );


Sin embargo, con la consistencia eventual no tenemos un estado global en tiempo real. Verificar already_exists() es delicado.

La mejor solución no es soportar la inserción, si no más bien set() (establece()) - por ejemplo, "establece un nuevo valor". A veces, a ésto se le llama un upsert. Entonces, si tenemos semánticas última-escritura-gana, todo va bien.

Eliminaciones

La eliminaciones requieren de una manipulación especial en caso de objetos renacidos. Considera esta secuencia:

op1: set( { _id : 'joe', age : 40 } }
op2: delete( { _id : 'joe' } )
op3: set( { _id : 'joe', age : 33 } )


Si op2 y op3 invierten el orden de ejecución, podríamos tener un problema. Así que necesitamos recordar el borrado por un momento, y aplicar semánticas de última-operación-gana. Algunos productos llaman al recuerdo del borrado una lápida.

Actualizaciones

Las actualizaciones tienen un asunto similar a las inserciones, así que para las actualizaciones, usamos la operación set() tal y como se describió anteriormente.

Nótese que las actualizaciones parciales de objeto pueden ser delicadas para replicar de forma eficiente. Considera una operación set() donde deseamos actualizar un simple campo:

update users set age=40 where _id=’joe’

Esto no es problema con la consistencia eventual si replicamos una copia completa del objeto. Sin embargo, ¿qué ocurre si el objeto del usuario tenía 1MB de tamaño? Esto debería ir bien enviando el campo de la nueva edad y el _id, en lugar del objeto completo. Sin embargo, esto es difícil. Considera:

op1: update users set age=40 where _id='joe'
op2: update users set state='ca' where _id='joe'


No podemos replicar simplemente la actualización parcial y usar el método última-escritura-gana; la base de datos neceitará más sofisticación para manipular ésto de manera eficiente.


Fusión programática

El método última-escritura-gana es buena, pero no siempre es suficiente. Tener que resolver mediante la aplicación cliente el conflico mediante una fusión es una buena alternativa. Consideremos un ejemplo mencionado en el Papel de Amazon Dynamo: manipulaciones de carritos de la compra. Con una consistencia eventual podría no ser seguro hacer algo como:

update cart set this[our_sku].qty=1 where _id='joe'

Si hay múltiples manipulaciones del carrito, algunas podrían perderse utilizando el método última-escritura-gana. En su lugar, el papel de Dynamo habla sobre almacenar las operaciones en el objeto carrito, en lugar del estado actual de los datos. Podríamos almacenar algo como:

update cart append { time : now(), op : 'addToCart', sku : our_sku, qty : 1 }
where _id='joe'


Cuando ocurra un conflicto, los objetos carrito pueden ser fusionados. No perdemos ninguna operación. Cuando llega la hora de comprar (check out), reproducimos todas las operaciones, lo cual podría incluir ajuste de cantidades y eliminaciones desde el carrito. Después de la reproducción tenemos el estado final del carrito.

Los ejemplos anteriores utilizan un campo timestamp - en un sistema real se puede usar un vector para ordenar las operaciones en el carrito.

Es interesante notar que no sólo hemos evitado conflictos, si no que también somos capaces de realizar operaciones donde la atomicidad sería requerida.


Operaciones conmutativas

Si todas las operaciones son conmutativas (más precisamente, plegables), nunca tendremos ningún conflicto. Las operaciones pueden ser aplicadas simplemente en cualquier orden, y el resultado sería el mismo. Por ejemplo:

// x starts as { }
x.increment('a', 1);
x.increment('a', 3);
x.addToSet('b', 'foo');
x.addToSet('b', 'bar');
result: { a : 4, b : {bar,foo} }

// x starts as { }
x.addToSet('b', 'bar');
x.increment('a', 3);
x.increment('a', 1);
x.addToSet('b', 'foo');
result: { a : 4, b : {bar,foo} }


Sin embargo la composición de addToSet e increment podría no ser plegable; de este modo, tenemos que usar sólo uno o el otro para un campo particular del objeto.

Fuente: http://blog.mongodb.org/post/520888030/on-distributed-consistency-part-5-many-writer