¿Cómo funcionan las consultas en un Sharded Cluster de MongoDB?

Categoría
Descripción

Cuando se trabaja con un sharded cluster (conjunto fragmentado) de MongoDB, el punto de entrada para las aplicaciones cliente debe ser MongoS. MongoS contiene una caché con la distribución de los shards que está almacenada en el servidor replicado de configuración (CSRS). Para cada shard, MongoS conoce el mínimo valor inclusivo y el máximo valor exclusivo que contiene. Para esta configuración, se dispone de 2 valores especiales, que evitan la necesidad de conocer específicamente el rango de valores posibles para la shard key:

  • minKey: representa el mínimo valor posible para el shard key. Se puede pensar en minkey como un "menos infinito".
  • maxKey: representa el máximo valor posible para el shard key. Se puede pensar en maxKey como un "más infinito".

Una vez que el cliente lanza la consulta, ocurre lo siguiente:

  • Se determina el listado de shards que deberían recibir la consulta. Dependiendo del predicado de la consulta (es decir, las condiciones de filtrado), MongoS decide si atacar a todos los shards en el cluster, o solo a un subconjunto de los mismos.
    • Si el predicado incluye la shard key, entonces MongoS atacará específicamente a los shards que contienen el valor de la shard key. Este tipo de consultas son las más eficaces. MongoS utiliza la tabla de configuración cacheada para saber qué shards contienen los rangos de valores solicitados. A este tipo de consultas se les denomina routed queries (consultas enrutadas).
    • Si el predicado no incluye la shard key, o el rango de valores solicitado tan amplio que cubre a varios o la totalidad de los shards, se llevará a cabo una operación llamada scatter gather, o reunión dispersa. Este tipo de operaciones pueden ser lentas, dependiendo de factores como el número de shards dentro del cluster. Cuando la shard key es de tipo hashed y la consulta se lleva a cabo para un rango de valores, casi siempre se efectúan operaciones de tipo scatter gather, puesto que puede darse el caso de que 2 valores de la clave adyacentes, al haberse aplicado sobre ellos una operación hash para distribuirlos en los shards, se encuentren en chunks completamente distintos. 
  • Independientemente de si se ataca uno o varios shards, MongoS abre un cursor contra cada shard objetivo. Cada cursor contiene el predicado de la consulta original, y devuelve la información obtenida por la consulta para ese shard.
  • Una vez se obtienen los datos de todos los cursores, MongoS junta todos los resultados y obtiene el conjunto total de documentos, devolviéndolo al cliente. Para hacer esta operación, MongoS debe esperar a que todos los shards consultados hayan devuelto algún tipo de resultado, contenga datos o no. Dependiendo del número de shards en el cluster y de la latencia de red entre los shards y el proceso MongoS, estas consultas pueden ser extremadamente lentas. En caso de que sólo se haya atacado 1 shard, MongoS podrá devolver el resultado directamente al cliente, saltándose esta operación.

Existen ciertas operaciones sobre los cursores que tienen un comportamiento específico en MongoS:

  • sort() MongoS envía la instrucción de ordenado a cada shard, y posteriormente realiza una unión ordenada de los resultados
  • limit() MongoS envía el límite a cada shard objetivo, y después lo vuelve a aplicar al conjunto de los resultados
  • skip() MongoS no envía la operación a cada shard, y ejecuta posteriormente la operación sobre el conjunto de resultados
  • en caso de que se ejecuten conjuntamente limit() y skip(), MongoS reenviará limit() y el valor de skip() a los shards para asegurarse de que se devuelven el número suficiente de documentos a MongoS para aplicar la operación limit() y skip() de forma satisfactoria

Cuando las shard keys están compuestas por varios campos, se puede utilizar un subconjunto de ellos, respetando siempre el orden, para crear consultas enrutadas. Imaginemos la shard key { "referencia": 1, "tipo": 1, "nombre": 1 }. Podemos definir estos tipos de consulta:

  • Consultas enrutadas: como respetan los campos y el orden de la shard key, MongoS será capaz de enrutar correctamente la petición a los shards que almacenan la información:
    • db.productos.find( { "referencia": (valor1) } )
    • db.productos.find( { "referencia": (valor1), "tipo": (valor2) } )
    • db.productos.find( { "referencia": (valor1), "tipo": (valor2), "nombre": (valor3) } )
  • Consultas scatter-gather: cuando se utilizan para la consulta campos que no parten de la raíz de la shard key, MongoS no será capaz de ennrutar la consulta a unos shards concretos, y deberá lanzar la consulta a todos los shards:
    • db.productos.find( { "tipo": (valor1) } )
    • db.productos.find( { "nombre": (valor1) } )

Se puede saber la forma en que MongoS ha realizado los cálculos añadiendo la función .explain() al final de la consulta:

MongoDB Enterprise mongos> db.productos.find( {"referencia": 3000} ).explain()
{
        "queryPlanner" : {
                "mongosPlannerVersion" : 1,
                "winningPlan" : {
                        "stage" : "SINGLE_SHARD",
                        "shards" : [
                                {
                                        "shardName" : "tienda-repl-2",
                                        "connectionString" : "tienda-repl-2/192.168.103.100:27004,192.168.103.100:27005,192.168.103.100:27006",
                                        "serverInfo" : {
                                                "host" : "tienda",
                                                "port" : 27004,
                                                "version" : "3.6.12",
                                                "gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
                                        },
                                        "plannerVersion" : 1,
                                        "namespace" : "tienda.productos",
                                        "indexFilterSet" : false,
                                        "parsedQuery" : {
                                                "sku" : {
                                                        "$eq" : 3000
                                                }
                                        },
                                        "winningPlan" : {
                                                "stage" : "FETCH",
                                                "inputStage" : {
                                                        "stage" : "SHARDING_FILTER",
                                                        "inputStage" : {
                                                                "stage" : "IXSCAN",
                                                                "keyPattern" : {
                                                                        "referencia" : 1
                                                                },
                                                                "indexName" : "referencia_1",
                                                                "isMultiKey" : false,
                                                                "multiKeyPaths" : {
                                                                        "referencia" : [ ]
                                                                },
                                                                "isUnique" : false,
                                                                "isSparse" : false,
                                                                "isPartial" : false,
                                                                "indexVersion" : 2,
                                                                "direction" : "forward",
                                                                "indexBounds" : {
                                                                        "sku" : [
                                                                                "[3000.0, 3000.0]"
                                                                        ]
                                                                }
                                                        }
                                                }
                                        },
                                        "rejectedPlans" : [ ]
                                }
                        ]
                }
        },
        "ok" : 1,
        "operationTime" : Timestamp(1562429411, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1562429411, 1),
                "signature" : {
                        "hash" : BinData(0,"ey1YkNpTY74Up9B5BomGM+hm+DE="),
                        "keyId" : NumberLong("6709492541378527258")
                }
        }
}

Se puede ver cómo el plan de ejecución ganador (winningPlan) incluye un escaneo de índices (IXSCAN), puesto que MongoD puede usar el propio índice para ejecutar la consulta. También se puede ver que en stage se indica SINGLE_SHARD, lo cual quiere decir que sólo se va a consulta 1 shard. Se puede ver además que en el array shards sólo se encuentra 1 elemento. En caso de que la consulta afectase a varios shards, stage tomaría el valor SHARD_MERGE, y el array shards contendría tantos elementos como shards consultados.