Generación de secuencias en Spanner

En este documento, se describen métodos para que los administradores de bases de datos y los desarrolladores de aplicaciones generen secuencias numéricas únicas en las aplicaciones que usan Spanner.

Introducción

Suelen darse situaciones en las que una empresa requiere un ID numérico único y simple como, por ejemplo, un número de empleado o un número de factura. Las bases de datos relacionales convencionales a menudo incluyen una función para generar secuencias únicas y crecientes de forma monótona. Estas secuencias se usan a fin de generar identificadores únicos (claves de filas) para los objetos almacenados en la base de datos.

Sin embargo, el uso de valores que aumentan (o descienden) de forma monótona, como claves de filas, podría no seguir las prácticas recomendadas en Spanner, ya que crea hotspots en la base de datos, lo que lleva a un de rendimiento. En este documento, se proponen mecanismos a fin de implementar un generador de secuencias mediante una tabla de base de datos de Spanner y una lógica de capa de aplicación.

Como alternativa, Spanner admite un generador de secuencias integrado en bits invertido. Para obtener más información sobre el generador de secuencias de Spanner, consulta Crea y administra secuencias.

Requisitos para un generador de secuencias

Cada generador de secuencias debe generar un valor único para cada transacción.

Según el caso práctico, también es posible que un generador de secuencias necesite crear secuencias con las siguientes características:

  • Estar ordenado: Los valores más bajos en la secuencia no deben emitirse después de los valores más altos.
  • No tener espacios: No debe haber espacios en la secuencia.

El generador de secuencias también debe generar valores con la frecuencia que requiere la aplicación.

Puede ser difícil cumplir con todos estos requisitos, en especial en un sistema distribuido. Si es necesario para cumplir con los objetivos de rendimiento, puedes dejar de lado los requisitos de orden y ausencia de espacios de la secuencia.

Otros motores de base de datos tienen maneras de manejar estos requisitos. Por ejemplo, las secuencias en las columnas PostgreSQL y AUTO_INCREMENT en MySQL pueden generar valores únicos para transacciones diferentes, pero no pueden producir valores sin espacios si las transacciones se revierten. Para obtener más información, consulta Notas en la documentación de PostgreSQL y, también, Implicaciones de AUTO_INCREMENT en MySQL.

Generadores de secuencias mediante filas de tablas de bases de datos

La aplicación puede implementar un generador de secuencias mediante el uso de una tabla de base de datos en la que se almacenan los nombres de las secuencias y el siguiente valor en la secuencia.

Leer y aumentar la celda next_value de la secuencia dentro de una transacción de base de datos genera valores únicos, sin necesidad de ninguna otra sincronización entre los procesos de la aplicación.

Primero, define la tabla de la siguiente manera:

CREATE TABLE sequences (
    name STRING(64) NOT NULL,
    next_value INT64 NOT NULL,
) PRIMARY KEY (name)

Puedes crear secuencias si insertas una fila en la tabla con el nombre de la secuencia nueva y el valor de inicio; por ejemplo, ("invoice_id", 1). Sin embargo, debido a que la celda next_value se incrementa para cada valor de secuencia generado, el rendimiento está limitado por la frecuencia con la que se puede actualizar la fila.

Las bibliotecas cliente de Spanner usan transacciones que se pueden reintentar para resolver conflictos. Si las celdas (valores de columna) que se leen durante una transacción de lectura y escritura se modifican en otra transacción, la transacción de lectura y escritura se bloqueará hasta que se complete la otra transacción. Luego, se anulará y se volverá a intentar para que se puedan leer los valores actualizados. Esto minimiza la duración de los bloqueos de escritura, pero también significa que una transacción se puede intentar varias veces antes de que se confirme de forma correcta.

Debido a que solo puede llevarse a cabo una transacción a la vez en una fila, la frecuencia máxima de emisión de valores de secuencia es inversamente proporcional a la latencia total de la transacción.

Esta latencia total de transacción depende de varios factores, como la latencia entre la aplicación cliente y los nodos de Spanner, la latencia entre los nodos Spanner y la incertidumbre de TrueTime. Por ejemplo, la configuración multirregional tiene una latencia de transacción más alta, ya que debe esperar que se genere un quórum de confirmaciones de escritura de los nodos en diferentes regiones para completarse.

Por ejemplo, si una transacción de actualización de lectura en una sola celda (una columna en una fila) tiene una latencia de 10 milisegundos (ms), en teoría, la frecuencia máxima de emisión de valores de secuencia es 100 por segundo. Este máximo se aplica a toda la base de datos, sin importar la cantidad de instancias de la aplicación cliente o la cantidad de nodos de la base de datos. Esto se debe a que un solo nodo siempre administra una sola fila.

En la siguiente sección, se describen las formas de solucionar esta limitación.

Implementación en el lado de la aplicación

El código de la aplicación debe leer y actualizar la celda next_value en la base de datos. Existen varias formas de hacerlo, cada una de las cuales presenta diferentes desventajas y características de rendimiento.

Generador de secuencias simple dentro de la transacción

La forma más simple de manejar la generación de secuencias es incrementar el valor de la columna dentro de la transacción cada vez que la aplicación necesite un valor secuencial nuevo.

En una sola transacción, la aplicación hace lo siguiente:

  • Lee la celda next_value del nombre de la secuencia que se usará en la aplicación.
  • Aumenta y actualiza la celda next_value para el nombre de la secuencia.
  • Usa el valor recuperado para cualquier valor de columna que la aplicación necesite.
  • Completa el resto de la transacción de la aplicación.

Con este proceso, se genera una secuencia ordenada y sin espacios. Si nada actualiza la celda next_value de la base de datos a un valor menor, la secuencia también será única.

Debido a que el valor de la secuencia se recupera como parte de la transacción de aplicación más amplia, la frecuencia máxima de generación de secuencias depende de la complejidad de la transacción completa de la aplicación. Una transacción compleja tendrá una latencia más alta y, por lo tanto, una menor frecuencia máxima posible.

En un sistema distribuido, es posible que se intenten muchas transacciones al mismo tiempo, lo que genera una alta competencia en el valor de la secuencia. Debido a que la celda next_value se actualiza dentro de la transacción de la aplicación, cualquier otra transacción que intente aumentar la celda next_value al mismo tiempo se bloqueará por esta primera transacción y se volverá a intentar. Esto genera grandes aumentos en el tiempo que la aplicación necesita para completar la transacción de forma correcta, lo que puede causar problemas de rendimiento.

El siguiente código proporciona un ejemplo de un generador de secuencias simple en la transacción que muestra solo un valor de secuencia por transacción. Esta restricción existe porque las escrituras dentro de una transacción realizadas mediante la API de mutación no son visibles hasta que se confirma la transacción. Ni siquiera las lecturas de la misma transacción pueden verlas. Por lo tanto, si se llama a esta función varias veces en la misma transacción, siempre se mostrará el mismo valor de secuencia.

En el siguiente código de ejemplo, se muestra cómo implementar una función getNext() síncrona:

/**
 * Returns the next value from this sequence.
 *
 * <p>Should only be called once per transaction.
 */
long getNext(TransactionContext txn) {
  Struct result =
      txn.readRow(
          SEQUENCES_TABLE, Key.of(sequenceName), Collections.singletonList(NEXT_VALUE_COLUMN));
  if (result == null) {
    throw new NoSuchElementException(
        "Sequence " + sequenceName + " not found in table " + SEQUENCES_TABLE);
  }
  long value = result.getLong(0);
  txn.buffer(
      Mutation.newUpdateBuilder(SEQUENCES_TABLE)
          .set(SEQUENCE_NAME_COLUMN)
          .to(sequenceName)
          .set(NEXT_VALUE_COLUMN)
          .to(value + 1)
          .build());
  return value;
}

En el siguiente código de ejemplo, se muestra cómo se usa la función getNext() síncrona en una transacción:

// Simple Sequence generator created outside transaction, eg as field.
private SimpleSequenceGenerator simpleSequence = new SimpleSequenceGenerator("my Sequence");

public void usingSimpleSequenceGenerator() {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Get a sequence value
              long nextValue = simpleSequence.getNext(txn);
              // Use nextValue in the transaction
              // ...
              return null;
            }
          });
}

Generador de secuencias síncrono mejorado dentro de la transacción

Puedes modificar la abstracción anterior para que se produzcan varios valores dentro de una sola transacción si haces un seguimiento de los valores de secuencia emitidos dentro de una transacción.

En una sola transacción, la aplicación hace lo siguiente:

  • Lee la celda next_value del nombre de la secuencia que se usará en la aplicación.
  • Almacena este valor como una variable de forma interna.
  • Cada vez que se solicita un valor de secuencia nuevo, se incrementa la variable next_value almacenada y se almacena en búfer una escritura que establece el valor de la celda actualizada en la base de datos.
  • Completa el resto de la transacción de la aplicación.

Si usas una abstracción, el objeto para esta abstracción debe crearse dentro de la transacción. El objeto realiza una sola lectura cuando se solicita el primer valor. El objeto realiza un seguimiento interno de la celda next_value para que se pueda generar más de un valor.

Las mismas advertencias relacionadas con la latencia y la competencia que se aplicaron a la versión anterior también se aplican a esta versión.

En el siguiente código de ejemplo, se muestra cómo implementar una función getNext() síncrona:

private final TransactionContext txn;
@Nullable private Long nextValue;

/** Creates a sequence generator for this transaction. */
public SynchronousSequenceGenerator(String sequenceName, TransactionContext txn) {
  super(sequenceName);
  this.txn = txn;
}

/**
 * Returns the next value from this sequence.
 *
 * <p>Can be called multiple times in a transaction.
 */
public long getNext() {
  if (nextValue == null) {
    // nextValue is unknown - read it.
    Struct result =
        txn.readRow(
            SEQUENCES_TABLE, Key.of(sequenceName), Collections.singletonList(NEXT_VALUE_COLUMN));
    if (result == null) {
      throw new NoSuchElementException(
          "Sequence " + sequenceName + " not found in table " + SEQUENCES_TABLE);
    }
    nextValue = result.getLong(0);
  }
  long value = nextValue;
  // increment and write nextValue to the database.
  nextValue++;
  txn.buffer(
      Mutation.newUpdateBuilder(SEQUENCES_TABLE)
          .set(SEQUENCE_NAME_COLUMN)
          .to(sequenceName)
          .set(NEXT_VALUE_COLUMN)
          .to(nextValue)
          .build());
  return value;
}

En el siguiente código de ejemplo, se muestra cómo usar la función getNext() síncrona en una solicitud de dos valores de secuencia:

public void usingSynchronousSequenceGenerator() {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Create the sequence generator object within the transaction
              SynchronousSequenceGenerator syncSequence =
                  new SynchronousSequenceGenerator("my_sequence", txn);
              // Get two sequence values
              long key1 = syncSequence.getNext();
              long key2 = syncSequence.getNext();
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

Generador de secuencias fuera de la transacción (asíncrono)

En las dos implementaciones anteriores, el rendimiento del generador depende de la latencia de la transacción de la aplicación. Puedes mejorar la frecuencia máxima, a expensas de tolerar espacios en la secuencia, si incrementas la secuencia en una transacción independiente. (Este es el enfoque que usa PostgreSQL). Debes recuperar los valores de secuencia que se usarán primero, antes de que la aplicación inicie su transacción.

La aplicación hace lo siguiente:

  • Crea una primera transacción para obtener y actualizar el valor de la secuencia:
    • Lee la celda next_value del nombre de la secuencia que se usará en la aplicación.
    • Almacena este valor como una variable.
    • Incrementa y actualiza la celda next_value del nombre de la secuencia en la base de datos.
    • Completa la transacción.
  • Usa el valor mostrado en una transacción independiente.

La latencia de esta transacción independiente será cercana a la latencia mínima, con un rendimiento que se aproximará a la frecuencia teórica máxima de 100 valores por segundo (si suponemos que la latencia de transacción es de 10 ms). Debido a que los valores de secuencia se recuperan por separado, la latencia de la transacción de la aplicación en sí no se modifica y la competencia se minimiza.

Sin embargo, si se solicita un valor de secuencia y no se usa, queda un espacio en la secuencia porque no es posible revertir los valores de secuencia solicitados. Esto puede ocurrir si la aplicación se anula o falla durante la transacción después de solicitar un valor de secuencia.

En el siguiente código de ejemplo, se muestra cómo implementar una función que recupera y aumenta la celda next_value en la base de datos:

/**
 * Gets the next sequence value from the database, and increments the database value by the amount
 * specified in a single transaction.
 */
protected Long getAndIncrementNextValueInDB(long increment) {
  return dbClient
      .readWriteTransaction()
      .run(
          txn -> {
            Struct result =
                txn.readRow(
                    SEQUENCES_TABLE,
                    Key.of(sequenceName),
                    Collections.singletonList(NEXT_VALUE_COLUMN));
            if (result == null) {
              throw new NoSuchElementException(
                  "Sequence " + sequenceName + " not found in table " + SEQUENCES_TABLE);
            }
            long value = result.getLong(0);
            txn.buffer(
                Mutation.newUpdateBuilder(SEQUENCES_TABLE)
                    .set(SEQUENCE_NAME_COLUMN)
                    .to(sequenceName)
                    .set(NEXT_VALUE_COLUMN)
                    .to(value + increment)
                    .build());
            return value;
          });
}

Puedes usar esta función con facilidad para recuperar un solo valor de secuencia nuevo, como se muestra en la siguiente implementación de una función getNext() asíncrona:

/**
 * Returns the next value from this sequence.
 *
 * Uses a separate transaction so must be used <strong>outside</strong>any other transactions.
 * See {@link #getNextInBackground()} for an alternative version that uses a background thread
 */
public long getNext() throws SpannerException {
  return getAndIncrementNextValueInDB(1);
}

En el siguiente código de ejemplo, se muestra cómo usar la función getNext() asíncrona en una solicitud de dos valores de secuencia:

// Async Sequence generator created outside transaction as a long-lived object.
private AsynchronousSequenceGenerator myAsyncSequence =
    new AsynchronousSequenceGenerator("my Sequence", dbClient);

public void usingAsynchronousSequenceGenerator() {
  // Get two sequence values
  final long key1 = myAsyncSequence.getNext();
  final long key2 = myAsyncSequence.getNext();
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

En el ejemplo de código anterior, puedes ver que los valores de secuencia se solicitan fuera de la transacción de la aplicación. Esto se debe a que Cloud Spanner no admite la ejecución de una transacción dentro de otra transacción en el mismo subproceso (lo que también se conoce como transacciones anidadas).

Para solucionar esta restricción, solicita el valor de secuencia mediante un subproceso en segundo plano y espera el resultado:

protected static final ExecutorService executor = Executors.newCachedThreadPool();

/**
 * Gets the next value using a background thread - to be used when inside a transaction to avoid
 * Nested Transaction errors.
 */
public long getNextInBackground() throws Exception {
  return executor.submit(this::getNext).get();
}

Generador de secuencias por lotes

Puedes obtener una mejora significativa del rendimiento si también descartas el requisito que establece que los valores de la secuencia deben estar en orden. Esto le permite a la aplicación reservar un lote de valores de secuencia y emitirlos de forma interna. Las instancias de aplicación individuales tienen su propio lote de valores independiente, por lo que los valores que se emiten no están en orden. Además, las instancias de aplicación que no usan todo su lote de valores (por ejemplo, si la instancia de aplicación se cierra) dejarán espacios de valores sin usar en la secuencia.

La aplicación hará lo siguiente:

  • Mantendrá un estado interno para cada secuencia que contenga el valor de inicio y el tamaño del lote, además del siguiente valor disponible.
  • Solicitará un valor de secuencia del lote.
  • Si no hay valores restantes en el lote, haz lo siguiente:
    • Crea una transacción para leer y actualizar el valor de la secuencia.
    • Lee la celda next_value de la secuencia.
    • Almacena este valor de forma interna como el valor inicial del lote nuevo.
    • Incrementa la celda next_value en la base de datos de modo que quede establecida una cantidad equivalente al tamaño del lote.
    • Completa la transacción.
  • Muestra el siguiente valor disponible y aumenta el estado interno.
  • Usa el valor que se muestra en la transacción.

Con este método, las transacciones que usan un valor de secuencia experimentarán un aumento en la latencia solo cuando se deba reservar un lote de valores de secuencia nuevo.

La ventaja es que cuando aumentas el tamaño del lote, el rendimiento se puede aumentar a cualquier nivel, ya que el factor limitante se convierte en la cantidad de lotes emitidos por segundo.

Por ejemplo, con un tamaño de lote de 100, si suponemos que la latencia es de 10 ms para obtener un lote nuevo y que, por lo tanto, hay un máximo de 100 lotes por segundo, se pueden emitir 10,000 valores de secuencia por segundo.

En el siguiente código de ejemplo, se muestra cómo implementar una función getNext() mediante el uso de lotes. Ten en cuenta que el código vuelve a usar la función getAndIncrementNextValueInDB() definida con anterioridad para recuperar nuevos lotes de valores de secuencia de la base de datos.

/**
 * Gets a new batch of sequence values from the database.
 *
 * <p>Reads next_value, increments it by batch size, then writes the updated next_value back.
 */
private synchronized void getBatch() throws SpannerException {
  if (next_value <= last_value_in_batch) {
    // already have some values left in the batch - maybe this has been refreshed by another
    // thread.
    return;
  }
  next_value = getAndIncrementNextValueInDB(batchSize);
  last_value_in_batch = next_value + batchSize - 1;
}

/**
 * Returns the next value from this sequence, getting a new batch of values if necessary.
 *
 * When getting a new batch, it creates a separate transaction, so this must be called
 * <strong>outside</strong> any other transactions. See {@link #getNextInBackground()} for an
 * alternative version that uses a background thread
 */

public synchronized long getNext() throws SpannerException {
  if (next_value > last_value_in_batch) {
    getBatch();
  }
  long value = next_value;
  next_value++;
  return value;
}

En el siguiente código de ejemplo, se muestra cómo usar la función getNext() asíncrona en una solicitud de dos valores de secuencia:

// Batch Sequence generator created outside transaction, as a long-lived object.
private BatchSequenceGenerator myBatchSequence =
    new BatchSequenceGenerator("my Sequence", /* batchSize= */ 100, dbClient);

public void usingBatchSequenceGenerator() {
  // Get two sequence values
  final long key1 = myBatchSequence.getNext();
  final long key2 = myBatchSequence.getNext();
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

Otra vez, los valores deben solicitarse fuera de la transacción (o mediante un subproceso en segundo plano), ya que Spanner no admite transacciones anidadas.

Generador asíncrono de secuencias por lotes

En el caso de las aplicaciones de alto rendimiento en las que no se acepta un aumento de latencia, puedes mejorar el rendimiento del generador por lotes anterior si cuentas con un nuevo lote de valores preparado para el momento en que se agote el lote de valores actual.

Para lograrlo, establece un umbral que indique cuándo la cantidad de valores de secuencia restantes en un lote es demasiado baja. Cuando se alcanza el umbral, el generador de secuencias comienza a solicitar un nuevo lote de valores en un subproceso en segundo plano.

Al igual que con la versión anterior, los valores no se emiten en orden y habrá espacios de valores que no se usan en la secuencia si las transacciones fallan o si las instancias de aplicación se cierran.

La aplicación hará lo siguiente:

  • Mantendrá un estado interno para cada secuencia, que contenga el valor inicial del lote y el siguiente valor disponible.
  • Solicitará un valor de secuencia del lote.
  • Si los valores restantes del lote son menores que el umbral, haz lo siguiente en un subproceso en segundo plano:
    • Crea una transacción para leer y actualizar el valor de la secuencia.
    • Lee la celda next_value del nombre de la secuencia que se usará en la aplicación.
    • Almacena este valor de forma interna como el valor inicial del siguiente lote.
    • Incrementa la celda next_value en la base de datos de modo que quede establecida una cantidad equivalente al tamaño del lote.
    • Completa la transacción.
  • Si no hay valores restantes en el lote, recupera el valor de inicio del siguiente lote desde el subproceso en segundo plano (espera que se complete si es necesario) y crea un lote nuevo en el que uses el valor de inicio que recuperaste como siguiente valor.
  • Muestra el siguiente valor y, también, incrementa el estado interno.
  • Usa el valor que se muestra en la transacción.

Para obtener un rendimiento óptimo, el subproceso en segundo plano debe iniciarse y completarse antes de que se agoten los valores de secuencia en el lote actual. De lo contrario, la aplicación deberá esperar al siguiente lote, y la latencia aumentará. Por lo tanto, deberás ajustar el tamaño del lote y el umbral bajo, según la frecuencia con la que se emiten los valores de secuencia.

Por ejemplo, supongamos que el tiempo de transacción es de 20 ms para recuperar un nuevo lote de valores, que el tamaño de lote es de 1, 000 y que la frecuencia máxima de emisión de secuencias es de 500 valores por segundo (un valor cada 2 ms). Durante los 20 ms en los que se emite un nuevo lote de valores, se emiten 10 valores de secuencia. Por lo tanto, el umbral para el número de valores de secuencia restantes debe ser superior a 10, de modo que el siguiente lote esté disponible cuando se lo necesite.

En el siguiente código de ejemplo, se muestra cómo implementar una función getNext() mediante el uso de lotes. Ten en cuenta que el código usa la función getAndIncrementNextValueInDB() definida con anterioridad para recuperar un lote de valores de secuencia mediante un subproceso en segundo plano.

/**
 * Gets a new batch of sequence values from the database.
 *
 * <p>Reads nextValue, increments it by batch size, then writes the updated nextValue back.
 * Stores the resulting value in  nextBatchStartValue, ready for when the existing pool of values
 * is exhausted.
 */
private Long readNextBatchFromDB() {
  return getAndIncrementNextValueInDB(batchSize);
}

/**
 * Returns the next value from this sequence.
 *
 * If the number of remaining values is below the low watermark, this triggers a background
 * request for new batch of values if necessary. Once the current batch is exhausted, then a the
 * new batch is used.
 */
public synchronized long getNext() throws SpannerException {
  // Check if a batch refresh is required and is not already running.
  if (nextValue >= (lastValueInBatch - lowWaterMarkForRefresh) && pendingNextBatchStart == null) {
    // Request a new batch in the background.
    pendingNextBatchStart = executor.submit(this::readNextBatchFromDB);
  }

  if (nextValue > lastValueInBatch) {
    // batch is exhausted, we should have received a new batch by now.
    try {
      // This will block if the transaction to get the next value has not completed.
      long nextBatchStart = pendingNextBatchStart.get();
      lastValueInBatch = nextBatchStart + batchSize - 1;
      nextValue = nextBatchStart;
    } catch (InterruptedException | ExecutionException e) {
      if (e.getCause() instanceof SpannerException) {
        throw (SpannerException) e.getCause();
      }
      throw new RuntimeException("Failed to retrieve new batch in background", e);
    } finally {
      pendingNextBatchStart = null;
    }
  }
  // return next value.
  long value = nextValue;
  nextValue++;
  return value;
}

En el siguiente código de ejemplo, se muestra cómo se usa la función getNext() por lotes y asíncrona en una solicitud de dos valores que se usarán en la transacción:

// Async Batch Sequence generator created outside transaction, as a long-lived object.
private AsyncBatchSequenceGenerator myAsyncBatchSequence =
    new AsyncBatchSequenceGenerator("my Sequence", /* batchSize= */ 1000, 200, dbClient);

public void usingAsyncBatchSequenceGenerator() {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Nullable
            @Override
            public Void run(TransactionContext txn) {
              // Get two sequence values
              final long key1 = myBatchSequence.getNext();
              final long key2 = myBatchSequence.getNext();
              // Use the 2 key values in the transaction
              // ...
              return null;
            }
          });
}

Ten en cuenta que, en este caso, los valores se pueden solicitar dentro de la transacción, ya que la recuperación de un nuevo lote de valores se produce en un subproceso en segundo plano.

Resumen

En la siguiente tabla, se comparan las características de los cuatro tipos de generadores de secuencias:

Síncrono Asíncrono Por lotes Por lotes y asíncrono
Valores únicos
Valores ordenados de forma global No
Sin embargo, con una carga bastante alta y un tamaño de lote bastante pequeño, los valores estarán cerca el uno del otro.
No
Sin embargo, con una carga bastante alta y un tamaño de lote bastante pequeño, los valores estarán cerca el uno del otro.
Sin espacios No No No
Rendimiento Latencia de 1 por transacción,
(~25 valores por segundo)
Entre 50 y 100 valores por segundo Entre 50 y 100 lotes de valores por segundo Entre 50 y 100 lotes de valores por segundo
Aumento de latencia > 10 ms
Mucho mayor con alta competencia (cuando una transacción lleva mucho tiempo)
10 ms en cada transacción
Mucho mayor con alta competencia
10 ms, pero solo cuando se recupera un nuevo lote de valores Cero, si el tamaño del lote y el umbral bajo se establecen en los valores adecuados

En la tabla anterior, también se ilustra el hecho de que es posible que debas dejar de lado los requisitos que establecen que los valores deben estar ordenados de manera global y que las series de valores no deben tener espacios a fin de generar valores únicos, a la vez que cumples con los requisitos de rendimiento general.

Pruebas de rendimiento

Puedes usar una herramienta de prueba o análisis de rendimiento, que se encuentra en el mismo repositorio de GitHub que las clases de generador de secuencias anteriores, para probar cada uno de estos generadores de secuencias y demostrar las características de rendimiento y latencia. La herramienta simula una latencia de transacción de la aplicación de 10 ms y ejecuta varios subprocesos de forma simultánea que solicitan valores de secuencia.

Para las pruebas de rendimiento, solo se necesita una instancia de Spanner de nodo único en la que realizar la prueba, ya que solo se modifica una fila.

Por ejemplo, en la siguiente salida, se muestra una comparación entre el rendimiento y la latencia en modo síncrono con 10 subprocesos:

$ ITERATIONS=2000
$ MODE=SYNC
$ NUMTHREADS=10
$ java -jar sequence-generator.jar \
   $INSTANCE_ID $DATABASE_ID $MODE $ITERATIONS $NUMTHREADS
2000 iterations (10 parallel threads) in 58739 milliseconds: 34.048928 values/s
Latency: 50%ile 27 ms
Latency: 75%ile 31 ms
Latency: 90%ile 1189 ms
Latency: 99%ile 2703 ms

En la siguiente tabla, se comparan los resultados de varios modos y números de subprocesos paralelos, incluida la cantidad de valores que se pueden emitir por segundo y la latencia en los percentiles 50, 90 y 99:

Modo y parámetros Cantidad de subprocesos Valores por segundo Latencia en percentil 50 (ms) Latencia en percentil 90 (ms) Latencia en
percentil 99 (ms)
SYNC 10 34 27 1,189 2,703
SYNC 50 30.6 1,191 3,513 5,982
ASYNC 10 66.5 28 611 1,460
ASYNC 50 78.1 29 1,695 3,442
BATCH
(tamaño 200)
10 494 18 20 38
BATCH (tamaño del lote 200) 50 1,195 27 55 168
ASYNC BATCH
(tamaño del lote 200, LT 50)
10 512 18 20 30
ASYNC BATCH
(tamaño del lote 200, LT 50)
50 1,622 24 28 30

Puedes ver que en el modo síncrono (SYNC), con el aumento de la cantidad de subprocesos, se produce un aumento de la competencia. Esto genera latencias de transacción mucho más altas.

En el modo asíncrono (ASYNC), debido a que la transacción para obtener la secuencia es más pequeña y, además, es independiente de la transacción de la aplicación, hay menos competencia, y la frecuencia es mayor. Sin embargo, la competencia aún puede ocurrir, lo que lleva a latencias superiores al percentil 90.

En el modo por lotes (BATCH), la latencia se reduce bastante, excepto por el percentil 99, que corresponde al momento en el que el generador necesita solicitar de forma síncrona otro lote de valores de secuencia de la base de datos. El rendimiento es muchas veces mayor en el modo BATCH que en el modo ASYNC.

El modo por lotes de 50 subprocesos tiene una latencia más alta porque las secuencias se emiten tan rápido que el factor limitante es la potencia de la instancia de máquina virtual (VM) (en este caso, una máquina de 4 CPU virtuales se ejecutó al 350% de CPU durante la prueba). Usar varias máquinas y varios procesos mostraría resultados generales similares al modo por lotes de 10 subprocesos.

En el modo ASYNC BATCH, la variación en latencia es mínima y el rendimiento es mayor, incluso con grandes cantidades de subprocesos, porque la latencia de solicitar un nuevo lote de la base de datos es completamente independiente de la transacción de la aplicación.

¿Qué sigue?