Entender las lecturas y escrituras a gran escala

Lea este documento para tomar decisiones fundamentadas sobre la arquitectura de sus aplicaciones y conseguir que tengan un alto rendimiento y fiabilidad. En este documento se tratan temas avanzados de Firestore. Si acabas de empezar a usar Firestore, consulta la guía de inicio rápido.

Firestore es una base de datos flexible y escalable para el desarrollo móvil, web y de servidores de Firebase y Google Cloud. Es muy fácil empezar a usar Firestore y escribir aplicaciones potentes y completas.

Para asegurarte de que tus aplicaciones sigan funcionando correctamente a medida que aumentan el tamaño y el tráfico de tu base de datos, es útil conocer los mecanismos de lectura y escritura en el backend de Firestore. También debes comprender la interacción de tus lecturas y escrituras con la capa de almacenamiento y las restricciones subyacentes que pueden afectar al rendimiento.

Consulta las siguientes secciones para conocer las prácticas recomendadas antes de diseñar tu aplicación.

Conocer los componentes de alto nivel

En el siguiente diagrama se muestran los componentes de alto nivel que intervienen en una solicitud a la API de Firestore.

Componentes de alto nivel

SDK y bibliotecas de cliente de Firestore

Firestore admite SDKs y bibliotecas de cliente para diferentes plataformas. Aunque una aplicación puede hacer llamadas HTTP y RPC directas a la API de Firestore, las bibliotecas de cliente proporcionan una capa de abstracción para simplificar el uso de la API e implementar las prácticas recomendadas. También pueden ofrecer funciones adicionales, como acceso sin conexión, cachés, etc.

Google Front End (GFE)

Se trata de un servicio de infraestructura común a todos los servicios de Google Cloud. GFE acepta las solicitudes entrantes y las reenvía al servicio de Google correspondiente (en este caso, el servicio de Firestore). También ofrece otras funciones importantes, como la protección frente a ataques de denegación de servicio.

Servicio de Firestore

El servicio de Firestore realiza comprobaciones en la solicitud de la API, que incluyen la autenticación, la autorización, las comprobaciones de cuota y las reglas de seguridad, y también gestiona las transacciones. Este servicio de Firestore incluye un cliente de almacenamiento que interactúa con la capa de almacenamiento para las lecturas y escrituras de datos.

Capa de almacenamiento de Firestore

La capa de almacenamiento de Firestore se encarga de almacenar tanto los datos como los metadatos, así como las funciones de base de datos asociadas que proporciona Firestore. En las siguientes secciones se describe cómo se organizan los datos en la capa de almacenamiento de Firestore y cómo se escala el sistema. Saber cómo se organizan los datos puede ayudarte a diseñar un modelo de datos escalable y a entender mejor las prácticas recomendadas de Firestore.

Intervalos y divisiones de claves

Firestore es una base de datos NoSQL orientada a documentos. Los datos se almacenan en documentos, que se organizan en jerarquías de colecciones. La jerarquía de la colección y el ID del documento se traducen a una sola clave por cada documento. Los documentos se almacenan de forma lógica y se ordenan lexicográficamente por esta única clave. Usamos el término "intervalo de claves" para referirnos a un intervalo de claves contiguo lexicográficamente.

Una base de datos de Firestore típica es demasiado grande para caber en una sola máquina física. También hay casos en los que la carga de trabajo de los datos es demasiado pesada para que la gestione una sola máquina. Para gestionar grandes cargas de trabajo, Firestore particiona los datos en partes independientes que se pueden almacenar y servir desde varias máquinas o servidores de almacenamiento. Estas particiones se realizan en las tablas de la base de datos en bloques de intervalos de claves denominados divisiones.

Replicación síncrona

Es importante tener en cuenta que la base de datos siempre se replica de forma automática y síncrona. Las divisiones de datos tienen réplicas en diferentes zonas para que estén disponibles incluso cuando una zona deje de ser accesible. La replicación coherente en las diferentes copias de la división se gestiona mediante el algoritmo Paxos para el consenso. Se elige una réplica de cada división para que actúe como líder de Paxos, que es responsable de gestionar las escrituras en esa división. La replicación síncrona te permite leer siempre la versión más reciente de los datos de Firestore.

El resultado general es un sistema escalable y de alta disponibilidad que proporciona latencias bajas tanto para las lecturas como para las escrituras, independientemente de las cargas de trabajo pesadas y a gran escala.

Diseño de datos

Firestore es una base de datos de documentos sin esquema. Sin embargo, internamente, organiza los datos principalmente en dos tablas de estilo de base de datos relacional en su capa de almacenamiento, de la siguiente manera:

  • Tabla Documentos: los documentos se almacenan en esta tabla.
  • Tabla Índices: en esta tabla se almacenan las entradas de índice que permiten obtener resultados de forma eficiente y ordenados por valor de índice.

En el siguiente diagrama se muestra cómo podrían ser las tablas de una base de datos de Firestore con las divisiones. Las divisiones se replican en tres zonas diferentes y cada división tiene un líder de Paxos asignado.

Diseño de datos

Una región frente a varias regiones

Cuando creas una base de datos, debes seleccionar una región o una multirregión.

Una ubicación regional es una ubicación geográfica específica, como us-west1. Las divisiones de datos de una base de datos de Firestore tienen réplicas en diferentes zonas de la región seleccionada, como se ha explicado anteriormente.

Una ubicación multirregional consta de un conjunto definido de regiones en las que se almacenan réplicas de la base de datos. En una implementación multirregional de Firestore, dos de las regiones tienen réplicas completas de todos los datos de la base de datos. Una tercera región tiene una réplica testigo que no mantiene un conjunto completo de datos, pero participa en la replicación. Al replicar los datos entre varias regiones, se pueden escribir y leer datos incluso si se pierde una región completa.

Para obtener más información sobre las ubicaciones de una región, consulta Ubicaciones de Firestore.

Una sola región o varias regiones

Cómo funciona una escritura en Firestore

Un cliente de Firestore puede escribir datos creando, actualizando o eliminando un solo documento. Para escribir en un solo documento, es necesario actualizar tanto el documento como sus entradas de índice asociadas de forma atómica en la capa de almacenamiento. Firestore también admite operaciones atómicas que consisten en varias lecturas o escrituras en uno o varios documentos.

En todos los tipos de escritura, Firestore proporciona las propiedades ACID (atomicidad, coherencia, aislamiento y durabilidad) de las bases de datos relacionales. Firestore también proporciona serialización, lo que significa que todas las transacciones se ejecutan en orden secuencial.

Pasos generales de una transacción de escritura

Cuando el cliente de Firestore emite una escritura o confirma una transacción mediante cualquiera de los métodos mencionados anteriormente, internamente se ejecuta como una transacción de lectura y escritura de la base de datos en la capa de almacenamiento. La transacción permite que Firestore proporcione las propiedades ACID mencionadas anteriormente.

Como primer paso de una transacción, Firestore lee el documento y determina las mutaciones que se deben realizar en los datos de la tabla Documents.

Esto también incluye hacer las actualizaciones necesarias en la tabla Indexes de la siguiente manera:

  • Los campos que se añaden a los documentos deben tener las inserciones correspondientes en la tabla Indexes.
  • Los campos que se van a quitar de los documentos deben tener las eliminaciones correspondientes en la tabla Indexes.
  • Los campos que se modifican en los documentos necesitan tanto eliminaciones (para los valores antiguos) como inserciones (para los valores nuevos) en la tabla Indexes.

Para calcular las mutaciones mencionadas anteriormente, Firestore lee la configuración de indexación del proyecto. La configuración de indexación almacena información sobre los índices de un proyecto. Firestore usa dos tipos de índices: de campo único y compuestos. Para obtener información detallada sobre los índices creados en Firestore, consulta Tipos de índices en Firestore.

Una vez que se han calculado las mutaciones, Firestore las recoge en una transacción y, a continuación, la confirma.

Entender una transacción de escritura en la capa de almacenamiento

Como hemos comentado anteriormente, una escritura en Firestore implica una transacción de lectura y escritura en la capa de almacenamiento. En función del diseño de los datos, una escritura puede implicar una o varias divisiones, como se muestra en el diseño de los datos.

En el siguiente diagrama, la base de datos de Firestore tiene ocho divisiones (marcadas del 1 al 8) alojadas en tres servidores de almacenamiento diferentes en una sola zona, y cada división se replica en tres(o más) zonas diferentes. Cada división tiene un líder de Paxos, que puede estar en una zona diferente para cada división.

División de bases de datos de Firestore

Supongamos que tienes una base de datos de Firestore con la colección Restaurants de la siguiente manera:

Colección de restaurantes

El cliente de Firestore solicita el siguiente cambio en un documento de la colección Restaurant actualizando el valor del campo priceCategory.

Cambios en un documento de una colección

En los siguientes pasos generales se describe lo que ocurre durante la escritura:

  1. Crea una transacción de lectura y escritura.
  2. Lee el documento restaurant1 de la colección Restaurants de la tabla Documentos de la capa de almacenamiento.
  3. Lee los índices del documento en la tabla Índices.
  4. Calcula las mutaciones que se deben hacer en los datos. En este caso, hay cinco mutaciones:
    • M1: Actualiza la fila de restaurant1 en la tabla Documentos para reflejar el cambio en el valor del campo priceCategory.
    • M2 y M3: elimina las filas del valor antiguo de priceCategory en la tabla Índices de los índices descendentes y ascendentes.
    • M4 y M5: inserta las filas del nuevo valor de priceCategory en la tabla Índices de los índices descendente y ascendente.
  5. Confirma estas mutaciones.

El cliente de almacenamiento del servicio Firestore busca las divisiones que poseen las claves de las filas que se van a cambiar. Supongamos que Split 3 sirve M1 y Split 6 sirve M2-M5. Hay una transacción distribuida que implica a todas estas divisiones como participantes. Las divisiones de participantes también pueden incluir cualquier otra división de la que se hayan leído datos anteriormente como parte de la transacción de lectura y escritura.

En los siguientes pasos se describe lo que ocurre durante la confirmación:

  1. El cliente de almacenamiento emite una confirmación. La confirmación contiene las mutaciones M1-M5.
  2. Los splits 3 y 6 son los participantes de esta transacción. Se elige a uno de los participantes como coordinador, como en la división 3. El coordinador se encarga de que la transacción se confirme o se cancele de forma atómica en todos los participantes.
    • Las réplicas líderes de estas divisiones son responsables del trabajo realizado por los participantes y los coordinadores.
  3. Cada participante y coordinador ejecuta un algoritmo de Paxos con sus respectivas réplicas.
    • El líder ejecuta un algoritmo Paxos con las réplicas. Se alcanza el quórum si la mayoría de las réplicas responden al líder con una respuesta ok to commit.
    • A continuación, cada participante notifica al coordinador que está preparado (primera fase de la confirmación en dos fases). Si algún participante no puede confirmar la transacción, se cancelará toda la transacción aborts.
  4. Una vez que el coordinador sabe que todos los participantes, incluido él mismo, están preparados, comunica el resultado de la transacción accept a todos los participantes (segunda fase de la confirmación en dos fases). En esta fase, cada participante registra la decisión de confirmación en un almacenamiento estable y se confirma la transacción.
  5. El coordinador responde al cliente de almacenamiento en Firestore que la transacción se ha confirmado. Paralelamente, el coordinador y todos los participantes aplican las mutaciones a los datos.

Ciclo de vida de la confirmación

Cuando la base de datos de Firestore es pequeña, puede ocurrir que una sola división tenga todas las claves de las mutaciones M1-M5. En ese caso, solo hay un participante en la transacción y no es necesario el commit de dos fases mencionado anteriormente, por lo que las escrituras son más rápidas.

Escrituras en varias regiones

En una implementación multirregional, la distribución de réplicas en varias regiones aumenta la disponibilidad, pero tiene un coste de rendimiento. La comunicación entre réplicas de diferentes regiones requiere más tiempo de ida y vuelta. Por lo tanto, la latencia de referencia de las operaciones de Firestore es ligeramente superior a la de las implementaciones de una sola región.

Configuramos las réplicas de forma que el liderazgo de las divisiones siempre se mantenga en la región principal. La región principal es aquella desde la que se recibe el tráfico en el servidor de Firestore. Esta decisión de liderazgo reduce el retraso de ida y vuelta en la comunicación entre el cliente de almacenamiento de Firestore y el líder de la réplica (o el coordinador de las transacciones de división múltiple).

Cada escritura en Firestore también implica cierta interacción con el motor en tiempo real de Firestore. Para obtener más información sobre las consultas en tiempo real, consulta Información sobre las consultas en tiempo real a gran escala.

Información sobre el ciclo de vida de una lectura en Firestore

En esta sección se profundiza en las lecturas independientes y no en tiempo real en Firestore. Internamente, el servidor de Firestore gestiona la mayoría de estas consultas en dos fases principales:

  1. Un único análisis de intervalo en la tabla Índices
  2. Búsquedas puntuales en la tabla Documentos basadas en el resultado del análisis anterior
Puede haber consultas que requieran menos procesamiento (por ejemplo, las consultas solo con claves en el modo Datastore) o más procesamiento (por ejemplo, las consultas IN) en Firestore.

Las lecturas de datos de la capa de almacenamiento se realizan internamente mediante una transacción de base de datos para asegurar que las lecturas sean coherentes. Sin embargo, a diferencia de las transacciones utilizadas para las escrituras, estas transacciones no toman bloqueos. En su lugar, funcionan eligiendo una marca de tiempo y, a continuación, ejecutando todas las lecturas en esa marca de tiempo. Como no adquieren bloqueos, no bloquean las transacciones de lectura y escritura simultáneas. Para ejecutar esta transacción, el cliente de almacenamiento de Firestore especifica un límite de marca de tiempo, que indica a la capa de almacenamiento cómo elegir una marca de tiempo de lectura. El tipo de límite de marca de tiempo elegido por el cliente de almacenamiento en Firestore se determina mediante las opciones de lectura de la solicitud de lectura.

Entender una transacción de lectura en la capa de almacenamiento

En esta sección se describen los tipos de lecturas y cómo se procesan en la capa de almacenamiento de Firestore.

Lecturas intensas

De forma predeterminada, las lecturas de Firestore son fuertemente coherentes. Esta coherencia sólida significa que una lectura de Firestore devuelve la versión más reciente de los datos, que refleja todas las escrituras que se han confirmado hasta el inicio de la lectura.

Lectura de una sola división

El cliente de almacenamiento de Firestore busca las divisiones que tienen las claves de las filas que se van a leer. Supongamos que tiene que leer Split 3 de la sección anterior. El cliente envía la solicitud de lectura a la réplica más cercana para reducir la latencia de ida y vuelta.

En este punto, pueden darse los siguientes casos en función de la réplica elegida:

  • La solicitud de lectura se dirige a una réplica líder (zona A).
    • Como el líder siempre está actualizado, la lectura puede continuar directamente.
  • La solicitud de lectura se dirige a una réplica que no es líder (por ejemplo, la zona B).
    • Es posible que la partición 3 sepa por su estado interno que tiene suficiente información para servir la lectura y lo haga.
    • Split 3 no sabe si ha visto los datos más recientes. Envía un mensaje al líder para solicitar la marca de tiempo de la última transacción que necesita aplicar para servir la lectura. Una vez que se aplique esa transacción, se podrá realizar la lectura.

A continuación, Firestore devuelve la respuesta a su cliente.

Lectura de varios splits

En el caso de que las lecturas se tengan que hacer desde varias divisiones, se aplica el mismo mecanismo en todas las divisiones. Una vez que se han devuelto los datos de todas las divisiones, el cliente de almacenamiento de Firestore combina los resultados. A continuación, Firestore responde a su cliente con estos datos.

Lecturas obsoletas

Las lecturas sólidas son el modo predeterminado de Firestore. Sin embargo, esto puede provocar una latencia mayor debido a la comunicación que puede ser necesaria con el líder. A menudo, tu aplicación de Firestore no necesita leer la versión más reciente de los datos y la funcionalidad funciona bien con datos que pueden tener unos segundos de antigüedad.

En ese caso, el cliente puede optar por recibir lecturas obsoletas mediante las read_time opciones de lectura. En este caso, las lecturas se realizan como si los datos estuvieran en read_time, y es muy probable que la réplica más cercana ya haya verificado que tiene datos en el read_time especificado. Para conseguir un rendimiento notablemente mejor, 15 segundos es un valor de obsolescencia razonable. Incluso en el caso de las lecturas obsoletas, las filas devueltas son coherentes entre sí.

Evitar puntos de acceso

Las divisiones de Firestore se dividen automáticamente en partes más pequeñas para distribuir el trabajo de servir tráfico a más servidores de almacenamiento cuando sea necesario o cuando se amplíe el espacio de claves. Las divisiones creadas para gestionar el exceso de tráfico se conservan durante unas 24 horas, aunque el tráfico desaparezca. Por lo tanto, si hay picos de tráfico recurrentes, las divisiones se mantienen y se introducen más divisiones cuando sea necesario. Estos mecanismos ayudan a las bases de datos de Firestore a autoescalarse cuando aumenta la carga de tráfico o el tamaño de la base de datos. Sin embargo, hay algunas limitaciones que debes tener en cuenta, tal como se explica a continuación.

Dividir el almacenamiento y la carga lleva tiempo, y aumentar el tráfico demasiado rápido puede provocar una latencia alta o errores de tiempo de espera agotado, que se conocen como puntos de acceso, mientras el servicio se ajusta. La práctica recomendada es distribuir las operaciones en el intervalo de claves, al tiempo que se aumenta el tráfico en una colección de una base de datos con 500 operaciones por segundo. Después de este aumento gradual, incrementa el tráfico hasta un 50% cada cinco minutos. Este proceso se denomina regla 500/50/5 y permite que la base de datos se escale de forma óptima para adaptarse a tu carga de trabajo.

Aunque las divisiones se crean automáticamente a medida que aumenta la carga, Firestore solo puede dividir un intervalo de claves hasta que sirva un único documento mediante un conjunto específico de servidores de almacenamiento replicados. Por lo tanto, un volumen alto y constante de operaciones simultáneas en un solo documento puede provocar un punto de acceso en ese documento. Si detecta latencias altas continuas en un solo documento, debería modificar su modelo de datos para dividir o replicar los datos en varios documentos.

Los errores de contención se producen cuando varias operaciones intentan leer o escribir el mismo documento simultáneamente.

Otro caso especial de hotspotting se produce cuando se usa una clave que aumenta o disminuye secuencialmente como ID de documento en Firestore y hay un número considerablemente alto de operaciones por segundo. Crear más divisiones no ayuda en este caso, ya que el aumento del tráfico simplemente se desplaza a la división recién creada. Como Firestore indexa automáticamente todos los campos del documento de forma predeterminada, también se pueden crear puntos de acceso móviles en el espacio de índice de un campo de documento que contenga un valor que aumente o disminuya secuencialmente, como una marca de tiempo.

Ten en cuenta que, si sigues las prácticas descritas anteriormente, Firestore puede escalarse para dar servicio a cargas de trabajo arbitrariamente grandes sin que tengas que ajustar ninguna configuración.

Solución de problemas

Firestore proporciona Key Visualizer como herramienta de diagnóstico diseñada para analizar patrones de uso y solucionar problemas de puntos de acceso.

Siguientes pasos