preloader
  • Inicio
  • Sharding manual y simple en PostgreSQL

blog-thumb

Concepto

En el concepto actual, vamos a combinar la herencia de tablas externas con la extensión postgres_fdw (de las siglas Foreign Data Wrapper, FDW em adelante), para lograr un mecanismo manual de sharding, el cual consiste en acceder y escribir información desde cualquier nodo al nodo correspondiente. Estas características están disponibles nativamente desde la versión 9.5.

El presente mecanismo, no tienen en cuenta el manejo de los entrypoints (FQDN de cada nodo), pero permite distribuir los datos solo utilizando características nativas.

La partición entre nodos permite una mejor localidad de datos y un modelo más escalable que mantener particiones locales. Dicho esto, los datos se dividirán en varios nodos y se organizarán usando una clave particular, que determinará en que fragmentos se asignarán los datos. Para el POC actual, vamos a especificar el “shardKey”, que es un “char(2)” tipo simple

Como se hizo esto antes

Hasta el día de hoy, la única forma de realizar hallazgos sobre este método era desde la capa de aplicación, emitiendo consultas directamente a los nodos manteniendo cierta forma determinista como {1} o usando una tabla de catálogos como {2} (NOTA: los siguientes ejemplos están usando pseudocódigo)

{1}

    query = "SELECT name,lastname FROM " + relation + partition " WHERE " id = " + person_id

{2}

    shard = query("SELECT shard FROM catalog WHERE key = " + person_id)
    query = "SELECT name,lastname FROM " + relation + shard " WHERE " id = " + person_id

¿Como vamos a implementar esto ahora?

Como las tablas externas (FT) no contienen ningún dato, es posible mantener copias de todas las bases de datos involucradas y también en instancias separadas si es necesario.

Todas las operaciones contra la tabla se realizarán a través de la tabla principal de las tablas del árbol FT y Postgres determinará el FT de destino mediante la función de exclusión de restricciones, que se detallará más adelante.

Para HA, está limitado en los nodos de datos para implementar cualquier otra solución de replicación disponible en la versión principal. Para ser justos, 9.6 admite la replicación de transmición y la decodificación lógica, que la “pglogical” harramienta utiliza para proporcionar una replicación lógica avanzada por tabla.

Tablas externas

Las tablas externas no contienen datos por si mismas y solo hacen referencia a una tabla externa en una base de datos de postgres diferente. Hay muchas extensiones diferentes que permiten tablas externas en diferentes soluciones de almacenamiento de datos, pero en este artículo en particular nos vamos a centrar en “postgres_fdw” ya que queremos explorar mas sobres los pushdowns de condición, lo que hace que las consultas en estas tablas sean más eficaces en consultas mas complejas.

Se puede encontrar un punto de referencia más extenso en mi próximo artículo.

El marco subyacente para los contenedores de datos externos admite operaciones de lectura y escritura. “postgres_fdw” no es la excepción y también admite la inserción de condiciones para evitar grandes escaneos en las tablas de origen.

En cada base de datos que contiene el FT, debe invocar la creación de la extensión:

    CREATE EXTENSION postgres_fdw:

Los FT tienen dos elementos principales, necesarios para apuntar correctamente tanto en fuente como en privilegios de usuario. Si es lo suficientemente paranoico, prefeririá usar usuarios sin privilegios con concesiones limitadas sobra las tablas que usa

  • Servidor
  • Asignación de usuarios

{1}

    CREATE SERVER shard1_main FOREIGN DATA WRAPPER postgres_fdw
      OPTIONS(host '127.0.0.1',port '5434', dbname 'shard1');
    CREATE SERVER shard2_main FOREIGN DATA WRAPPER postgres_fdw
      OPTIONS(host '127.0.0.1',port '5435', dbname 'shard2');

    -- Slaves
    CREATE SERVER shard1_main_replica FOREIGN DATA WRAPPER postgres_fdw
       OPTIONS(host '127.0.0.1',port '7777',dbname 'shard1');
    CREATE SERVER shard2_main_replica FOREIGN DATA WRAPPER postgres_fdw
       OPTIONS(host '127.0.0.1',port '8888',dbname 'shard2');

{2}

    -- User mapping
    CREATE USER MAPPING FOR postgres SERVER shard1_main OPTIONS(user 'postgres');
    CREATE USER MAPPING FOR postgres SERVER shard2_main OPTIONS(user 'postgres');

    CREATE USER MAPPING FOR postgres SERVER shard1_main_replica OPTIONS(user 'postgres');
    CREATE USER MAPPING FOR postgres SERVER shard2_main_replica OPTIONS(user 'postgres');

La definición de FT es bastante sencilla si no queremos hacer más filtrado de columnas:

    CREATE TABLE main (shardKey char(2), key bigint, avalue text);

    CREATE FOREIGN TABLE main_shard01
            (CHECK (shardKey = '01'))
            INHERITS (main)
            SERVER shard1_main;
    
    CREATE FOREIGN TABLE main_shard02
            (CHECK (shardKey = '02'))
            INHERITS (main)
            SERVER shard2_main;

FDW para escritura

Incluso si no recomiendo el siguiente enfoque, puede ser muy fácil centralizar las escrituras en los fragmentos a través del FT. Aunque, requiere codificar un disparador para administrar esto. Actualmente, el nivel mínimo de transacción para tablas externas es “REPETEABLE READ”, pero probablemente cambiará en futuras versiones.

Un enfoque muy simplista para un disparador INSERT será como se muestra a continuación:

CREATE OR REPLACE FUNCTION f_main_part() RETURNS TRIGGER AS
    $FMAINPART$
    DECLARE
            partition_name text;
    BEGIN
            partition_name := 'main_shard' || NEW.shardKey;
            EXECUTE 'INSERT INTO' || quote_ident(partition_name) || ' SELECT ($1).*' USING NEW;
            RETURN NULL;
    END;
    $FMAINPART$ LANGUAGE plpgsql;

    CREATE TRIGGER t_main BEFORE INSERT
      ON main
      FOR EACH ROW EXECUTE PROCEDURE f_main_part();

Datos sobre fragmentos

Como los fragmentos contienen datos, la declaración termina siendo una tabla común dentro del sufijo necesario para la localización:

CREATE TABLE main_shard01( shardKey char(2),
                               key bigint,
                               avalue text,
                               CHECk(shardKey='01'));
CREATE INDEX ON main_shard01(key);

Se podría hacer una prueba simple emitiendo:

    proxy=# INSERT INTO main
            SELECT '0' || round(random()*1+1),i.i,random()::text
            FROM generate_series(1,20000) i(i);
    INSERT 0 0

Probablemente esté intuyendo que la declaración anterior inserta datos en ambos nodos, y el disparador derivará la fila de acuerdo con el fragmento correspondiente

Nota: el número de fragmentos que se genera mediante "random()*1+1" el resultado que se redondea entre 1 y 2

Obtienelo mediante las columnas ocultas

La consulta de datos puede ser muy transparente, como se muestra a continuación. EL tableoid en este caso particular puede ser engañoso, ya que los oid reportados son los de los nodos, no la máquina local. Se usa solo para mostrar que efectivamente son tablas diferentes:

proxy=# select tableoid,count(*) from main group be tableoid;
 tableoid | count
 ---------+-------
    33226 | 104
    33222 |  96
(2 rows)

Por ejemplo, recuperar una sola fila es tan fácil como:

proxy=# SELECT avalue FROM main WHERE key = 1500 and shardKey = '01';
    avalue
---------------------
  0.971926014870405
(1 row)

Detrás de escena, la consulta enviada a los servidores remotos contiene el filtro correspondiente( (key = 1500) )y, localmente, la exclusión de restricciones permite evitar más escaneos en el otro FT secundario.

proxy=# explain (VERBOSE true)SELECT avalue
                FROM main WHERE key = 1500
                        and shardKey = '01';
                QUERY PLAN
----------------------------------------------------------
Append  (cost=0.00..131.95 rows=2 width=32)
    -> Seq Scan on public.main  (cost=0.00..0.00 rows=1 width=32)
         Output: main.avalue
         Filter: ((main.key = 1500) AND (main.shardKey = '01'::bpchar))
    -> Foreign Scan on public.main_shard01  (cost=100.00..131.95 rows=1 width=32)
         Output: main_shard01.avalue
         Remote SQL: SELECT avalue FROM public.main_shard01 WHERE ((key = 1500))
            AND ((shardKey = '01'::bpchar))
(7 rows)

Incluso si no queremos proporcionar la clave de fragmento, el key filtro se insertará en todos los nodos de fragmentos. SI sus claves no son únicas en los fragmentos, obtendrá un conjunto de resultados de varias filas.

proxy=# explain (VERBOSE true) SELECT avalue FROM main WHERE key = 1500;
                                    QUERY PLAN
---------------------------------------------------------------------------
Append  (cost=0.00..256.83 rows=15 width=32)
    -> Seq Scan on public.main  (cost=0.00..00.0 rows=1 width=32)
         Output: main.avalue
         Filter: (main.key = 1500)
    -> Foreign Scan on public.main_shard01  (cost=100.00..128.41 rows=7 width=32)
         Output: main_shard01.avalue
         Remote SQL: SELECT avalue FROM public.main_shard01 WHERE ((key = 1500))
    -> Foreign Scan on puclib.main_shard02  (cost=100.00..128.41 rows=7 width=32)
         Output: main_shard02.avalue
         Remote SQL: SELECT avalue FROM public.main_shard02 WHERE ((key = 1500))
(10 rows)

Consideraciones

Los contenedores de datos externos para Postgres son una gran extensión, pero tienen un precio con una sobrecarga visible en cargas de trabajo transaccionales de alta intensidad.

Espero que les haya gustado el artículo!