Prácticas recomendadas

Puedes usar las prácticas recomendadas que aparecen aquí como una referencia rápida de lo que debes tener en cuenta cuando compiles una aplicación que usa Firestore en modo Datastore. Si recién empiezas a usar el modo Datastore, es posible que esta página no sea el mejor lugar para comenzar, ya que no encontrarás los conceptos básicos sobre cómo utilizarlo. Si eres un usuario nuevo, te sugerimos que comiences con Comienza a usar Firestore en modo Datastore.

General

  • Utiliza siempre caracteres UTF-8 para nombres de espacio de nombres, nombres de tipos, nombres de propiedad y nombres de claves personalizadas. Los caracteres que no son UTF-8 y que se utilizan en esos nombres pueden interferir en la funcionalidad del modo Datastore. Por ejemplo, usar un carácter que no es UTF-8 en un nombre de propiedad podría impedir la creación de un índice que use esta propiedad.
  • No uses una barra diagonal (/) en nombres de categorías de claves personalizadas. Las barras diagonales podrían interferir en funcionalidades futuras si se usan en estos nombres.
  • Evita almacenar información sensible en un ID del proyecto de Cloud. El ID del proyecto de Cloud se puede conservar más allá de la duración de tu proyecto.

Llamadas a la API

  • Utiliza operaciones por lotes en lugar de operaciones individuales para tus lecturas, escrituras y eliminaciones. Las operaciones por lotes son más eficientes debido a que realizan operaciones múltiples con la misma sobrecarga que una operación individual.
  • Si una transacción falla, intenta revertir la transacción. La reversión minimiza la latencia de reintento de una solicitud diferente que compite por los mismos recursos en una transacción. Ten en cuenta que la reversión puede fallar, por lo tanto, debe considerarse solo como un último recurso.
  • Utiliza llamadas asíncronas en vez de síncronas cuando sea posible. Las llamadas asíncronas minimizan el impacto en la latencia. Por ejemplo, considera una aplicación que necesita el resultado de un lookup() síncrono y los resultados de una consulta antes de que pueda procesar una respuesta. Si lookup() y la consulta no tienen una dependencia de datos, no es necesario esperar de forma síncrona hasta que lookup() se complete antes de iniciar la consulta.

Entidades

  • No incluyas la misma entidad (por clave) varias veces en la misma confirmación. Incluir la misma entidad varias veces en la misma confirmación podría afectar la latencia.

  • Consulta la sección sobre las actualizaciones de una entidad.

Claves

  • Los nombres de las claves se generan de forma automática si no se proporcionaron al momento de la creación de la entidad. Se asignan de modo que estén distribuidas de forma uniforme en el espacio de claves.
  • Para una clave que utiliza un nombre personalizado, utiliza siempre caracteres UTF-8, excepto una barra diagonal (/). Los caracteres que no son UTF-8 interfieren en varios procesos, como la importación de un archivo de exportación en modo Datastore a BigQuery. Una barra diagonal puede interferir en funcionalidades futuras.
  • Para una clave que usa un ID numérico:
    • No uses un número negativo para el ID. Un ID negativo podría interferir en la ordenación.
    • No uses el valor 0(cero) para el ID. Si lo haces, obtendrás un ID asignado de forma automática.
    • Si deseas asignar de forma manual tus propios ID numéricos a las entidades que creas, haz que tu aplicación obtenga un bloque de ID con el método allocateIds(). Eso evitará que el modo Datastore asigne uno de tus ID numéricos manuales a otra entidad.
  • Si asignas tu propio ID numérico manual o nombre personalizado a las entidades que creaste, no utilices valores monótonamente crecientes como:

    1, 2, 3, …,
    "Customer1", "Customer2", "Customer3", ….
    "Product 1", "Product 2", "Product 3", ….
    

    Si una aplicación genera un tráfico alto, esa numeración secuencial podría dar lugar a hotspots que afectan la latencia del modo Datastore. Para evitar el problema de los ID numéricos secuenciales, obtén ID numéricos con el método allocateIds(). El método allocateIds() genera secuencias bien distribuidas de ID numéricos.

  • Cuando especificas una clave o almacenas el nombre generado, puedes realizar más adelante un lookup() en esa entidad sin necesidad de emitir una consulta para encontrar la entidad.

Índices

Propiedades

  • Utiliza siempre caracteres UTF-8 para propiedades de tipo string. Un carácter que no sea UTF-8 en una propiedad de tipo string podría interferir en las consultas. Si necesitas guardar datos que contengan caracteres que no son UTF-8, usa una string de bytes.
  • No utilices puntos en los nombres de las propiedades. Los puntos en los nombres de la propiedad interfieren en la indexación de propiedades de entidad incorporadas.

Consultas

  • Si necesitas acceder solo a la clave desde los resultados de la consulta, utiliza una consulta de solo claves. Una consulta de solo claves muestra los resultados a una latencia y costo más bajos que cuando se recuperan las entidades completas.
  • Si solo necesitas acceder a propiedades específicas de una entidad, utiliza una consulta de proyección. Una consulta de proyección muestra los resultados a una latencia y costo más bajos que cuando se recuperan las entidades completas.
  • Del mismo modo, si necesitas acceder solo a las propiedades que están incluidas en el filtro de consulta (por ejemplo, las enumeradas en una cláusula order by), usa una consulta de proyección.
  • No uses desplazamientos. En su lugar, utiliza cursors. Utilizar una compensación solo evita que las entidades omitidas se recuperen en tu aplicación, pero igual se recuperan de forma interna. Las entidades omitidas afectan la latencia de la consulta y tu aplicación se factura de acuerdo con las operaciones de lectura requeridas para recuperarlas.

Diseña para el escalamiento

En las siguientes prácticas recomendadas, se describe cómo evitar situaciones que generen problemas de contención.

Actualizaciones de una entidad

Mientras diseñas tu app, ten en cuenta la rapidez con la que esta actualiza entidades individuales. La mejor manera de definir el rendimiento de tu carga de trabajo es realizar pruebas de carga. La frecuencia máxima exacta con la que una app puede actualizar una sola entidad depende en gran medida de la carga de trabajo. Los factores incluyen la tasa de escritura, la contención entre las solicitudes y la cantidad de índices afectados.

Una operación de escritura de entidad actualiza la entidad y los índices asociados, y Firestore en modo Datastore aplica de forma síncrona la operación de escritura en un quórum de réplicas. Con tasas de escritura lo suficientemente altas, la base de datos comenzará a encontrar contención, mayor latencia o cualquier otro error.

Tasas altas de lectura/escritura para un rango pequeño de claves

Evita las tasas altas de lectura o escritura en documentos que se encuentren cerca lexicográficamente. De lo contrario, tu aplicación experimentará errores de contención. Este problema se conoce como generación de hotspots y tu aplicación puede experimentarlo si realiza alguna de las siguientes acciones:

  • Crea entidades nuevas a una tasa muy alta y asigna sus propios ID que aumentan monótonamente.

    El modo Datastore asigna claves con un algoritmo de dispersión. Si creas entidades nuevas con asignación automática de ID de entidad, no deberías encontrar generación de hotspots durante las operaciones de escritura.

  • Crea entidades nuevas a una tasa muy alta con la política de asignación de ID secuencial heredada.

  • Crea entidades nuevas a una tasa alta para un tipo con pocas entidades.

  • Crea entidades nuevas con un valor de propiedad indexado y que aumenta monótonamente, como una marca de tiempo, a una tasa muy alta.

  • Borra entidades de un tipo a una tasa alta.

  • Escribe en la base de datos a una tasa muy alta sin aumentar el tráfico de forma gradual.

Si experimentas un aumento repentino en la tasa de escritura de un rango pequeño de claves, puedes obtener operaciones de escritura lentas debido a un hotspot. En algún momento, el modo Datastore dividirá el espacio de claves para admitir una carga alta.

El límite de operaciones de lectura suele ser más alto que el de las de escritura, a menos que la lectura sea desde una clave única a una tasa alta.

Los hotspots se pueden aplicar a los rangos de clave utilizados por las claves de entidad y por los índices.

En algunos casos, un hotspot puede causar un impacto mayor en una aplicación que prevenir operaciones de lectura o escritura en un rango pequeño de claves. Por ejemplo, las teclas de acceso rápido se pueden leer o escribir durante el inicio de la instancia y eso causaría que la carga de solicitudes falle.

Si tienes una clave o una propiedad indexada que aumentará monótonamente, entonces puedes agregar un hash al azar para asegurarte de que las claves se compartan a múltiples tablets.

De la misma manera, si necesitas consultar una propiedad que aumenta (o disminuye) monótonamente con un orden o un filtro, podrías indexar una nueva propiedad, para la cual debes prefijar el valor monótono con un valor que tenga cardinalidad alta a lo largo del conjunto de datos, pero que es común a todas las entidades en el alcance de la consulta que quieres realizar. Por ejemplo, si quieres consultar entradas por marcas de tiempo, pero solo necesitas que muestre resultados para un usuario a la vez, puedes prefijar la marca de tiempo con el ID del usuario y, luego, indexar esa nueva propiedad. Esto permitirá consultas y ofrecerá los resultados en orden para ese usuario, pero la presencia del ID del usuario garantizará que el índice esté bien fragmentado.

Acelera el tráfico

Acelera de forma gradual el tráfico hacia nuevos tipos o partes del espacio de claves.

Debes aumentar de forma gradual el tráfico a tipos nuevos a fin de que Firestore en modo Datastore tenga tiempo suficiente a fin de prepararse para el aumento de tráfico. Recomendamos realizar un máximo de 500 operaciones por segundo en un tipo nuevo y, luego, aumentar el tráfico en un 50% cada 5 minutos. En teoría, puedes alcanzar 740,000 operaciones por segundo después de utilizar este programa de aceleración por 90 minutos. Asegúrate de que las operaciones de escritura se encuentren distribuidas de manera uniforme en todo el rango de claves. Nuestros SRE lo llaman la regla “500/50/5”.

Este patrón de aumento gradual es importante en particular si cambias el código para que deje de usar la categoría A y, en su lugar, use la categoría B. Una manera simple de manejar esta migración es cambiar tu código para que lea la categoría B y, si no existe, luego lea la categoría A. Sin embargo, esto podría provocar un aumento repentino en el tráfico a una categoría nueva con una porción muy pequeña del espacio de claves.

El mismo problema puede surgir si migras tus entidades para utilizar un rango de claves diferente dentro del mismo tipo.

La estrategia que utilices para migrar entidades a una nueva categoría o clave dependerá de tu nuevo modelo de datos. A continuación, puedes encontrar una estrategia de ejemplo conocida como "Lecturas paralelas". Tendrás que determinar si esta estrategia es eficaz para tus datos o no. Un punto importante a tener en cuenta será el impacto del costo de las operaciones paralelas durante la migración.

Lee primero la entidad o la clave antigua. Si esa falta, entonces puedes leer desde la entidad o clave nueva. Una tasa alta de lecturas de entidades no existentes puede causar una generación de hotspots, por lo tanto, debes asegurarte de incrementar la carga de manera gradual. Una mejor estrategia es copiar la entidad antigua a la nueva y luego borrar la antigua. Acelera las lecturas paralelas de manera gradual para asegurarte de que el nuevo espacio de claves esté divido correctamente.

Una estrategia posible para acelerar lecturas o escrituras de manera gradual en una categoría nueva es utilizar un hash determinista del ID del usuario para obtener un porcentaje aleatorio de usuarios que escriban nuevas entidades. Asegúrate de que el resultado del hash del ID de usuario no se encuentre sesgado ni por tu función aleatoria ni por el comportamiento de tu usuario.

Mientras tanto, ejecuta un trabajo de Dataflow para copiar todos tus datos de las entidades o claves antiguas a las nuevas. Este trabajo en lotes debe evitar escrituras en claves secuenciales para prevenir la generación de hotspots. Cuando tu trabajo en lotes está completo, solo puedes leer desde la nueva ubicación.

Una mejor manera de realizar esta estrategia es migrar lotes pequeños de usuarios al mismo tiempo. Agrega un campo a la entidad de usuario que realiza un seguimiento del estado de la migración de ese usuario. Selecciona un lote de usuarios para migrar en función de un hash del ID de usuario. Un trabajo de Mapreduce o Dataflow migrará las claves para ese lote de usuarios. Los usuarios que tengan una migración en curso usarán lecturas paralelas.

Ten en cuenta que no es fácil revertir el proceso, a menos que realices operaciones de escritura duales de las entidades antiguas y nuevas durante la fase de migración. Esto incrementará los costos del modo Datastore en que se incurrió.

Eliminaciones

Evita borrar una gran cantidad de entidades en un rango pequeño de claves.

Firestore en modo Datastore vuelve a escribir sus tablas de forma periódica para quitar las entradas que se borraron y reorganizar los datos a fin de que las operaciones de lectura y escritura sean más eficientes. Este proceso se conoce como compactación.

Si borras una gran cantidad de entidades del modo Datastore en un pequeño rango de claves, las consultas en esta parte del índice serán más lentas hasta que se complete la compactación. En casos extremos, tus consultas pueden agotar el tiempo de espera antes de mostrar resultados.

Es un antipatrón utilizar un valor de marca de tiempo de un campo indexado para representar el tiempo de caducidad de una entidad. Para recuperar entidades vencidas, necesitarás consultar campos indexados que probablemente se encuentren en una parte superpuesta del espacio de claves con entradas de índice para las entidades borradas más recientes.

Puedes mejorar el rendimiento con "consultas de fragmentación" que agregan una string de longitud fija a la marca de tiempo de vencimiento. El índice está ordenado en la string completa para que las entidades con las mismas marcas de tiempo se puedan localizar a lo largo del rango de claves del índice. Puedes ejecutar múltiples consultas en paralelo para recuperar resultados de cada fragmentación.

Una solución más completa para el problema de la marca de tiempo de vencimiento es un "número de generación", un contador global que se actualiza de manera periódica. El número de generación está agregado en la marca de tiempo de vencimiento para que las consultas se ordenen por número de generación, luego por fragmentación y luego por marca de tiempo. La eliminación de las entidades antiguas ocurre en una generación anterior. El número de generación de una entidad que no se borró debería aumentar. Una vez que la eliminación está completa, avanzas a la próxima generación. Las consultas por una generación antigua no se realizarán correctamente hasta que la compactación esté completa. Podrías tener que esperar que varias generaciones se completen para poder consultar el índice y obtener la lista de entidades que borrar a fin de reducir el riesgo de que falten resultados debido a una coherencia eventual.

Fragmentación y replicación

Usa la fragmentación o la replicación para lidiar con los hotspots.

Puedes usar la replicación si necesitas leer una parte del rango de clave a una velocidad superior de la que se permite en Firestore en modo Datastore. Si usas esta estrategia, almacenarás N copias de la misma entidad, lo que permite una velocidad de lectura N veces más alta que la que admite una sola entidad.

Puedes usar la fragmentación si necesitas escribir en una parte del rango de clave a una velocidad superior que la que permite Firestore en modo Datastore. La fragmentación divide una entidad en unidades más pequeñas.

Algunos de los errores más comunes de fragmentación son:

  • Fragmentar con un prefijo de tiempo. Luego de que el tiempo se desplaza al siguiente prefijo, la parte nueva sin dividir se convierte en un hotspot. En cambio, debes desplazar de manera gradual una parte de tu escritura al nuevo prefijo.

  • Fragmentar solo las entidades más sobrecargadas. Si fragmentas una pequeña proporción del número total de entidades, podría no haber filas suficientes entre las entidades sobrecargadas para asegurar de que permanezcan en divisiones diferentes.

Qué sigue