Salta el contingut

Bases de dades distribuïdes

Què és una base de dades distribuïda

Una base de dades distribuïda és un sistema en el qual les dades es troben emmagatzemades en múltiples nodes (ordinadors o servidors) físicament separats, però que es presenten als usuaris i aplicacions com si fossin un únic sistema coherent.

A diferència de la replicació (on cada node té una còpia de totes les dades), la distribució implica que les dades estan dividides entre nodes (particionades, fragmentades o sharded), de manera que cada node emmagatzema i gestiona únicament un subconjunt de les dades.

Motivacions per a una base de dades distribuïda:

  • Escalabilitat horitzontal: afegir nodes per augmentar la capacitat.
  • Tolerància a fallades: si cau un node, el sistema continua funcionant amb els altres.
  • Localitat de dades: emmagatzemar les dades a prop dels usuaris que les usen.
  • Superar els límits d'un sol servidor: un volum de dades superior al que un sol servidor pot gestionar.

El teorema CAP

El teorema CAP (Consistency, Availability, Partition tolerance), formulat per Eric Brewer el 2000, afirma que un sistema distribuït no pot garantir simultàniament les tres propietats següents:

graph TD
    CAP["Teorema CAP"]
    C["Consistència\n(Consistency)\nTots els nodes veuen les\nmateix dades al mateix temps"]
    A["Disponibilitat\n(Availability)\nTotes les peticions reben\nuna resposta (no error)"]
    P["Tolerància a particions\n(Partition Tolerance)\nEl sistema continua funcionant\nmalgrat la pèrdua de missatges\nentre nodes"]

    CAP --> C
    CAP --> A
    CAP --> P

    CP["CP\nSQL traditional\nZooKeeper, etcd\nHBase, MongoDB\n(mode consistent)"]
    AP["AP\nCassandra, CouchDB\nDynamoDB, Riak\nDNS"]
    CA["CA\nNo pràcticament\npossible en sistemes\ndistribuïts reals"]

    C --- CP
    P --- CP
    C --- CA
    A --- CA
    A --- AP
    P --- AP

    style C fill:#337ab7,color:#fff
    style A fill:#5cb85c,color:#fff
    style P fill:#f0ad4e,color:#333
    style CP fill:#337ab7,color:#fff,stroke:#1a5276
    style AP fill:#5cb85c,color:#fff,stroke:#1e8449
    style CA fill:#d9534f,color:#fff
Combinació Descripció Exemples
CP (Consistent + Partition Tolerant) Garanteix consistència però pot denegar peticions si hi ha una partició de xarxa PostgreSQL amb replicació síncrona, ZooKeeper, etcd, HBase
AP (Available + Partition Tolerant) Garanteix disponibilitat però pot retornar dades desactualitzades Cassandra, DynamoDB, CouchDB, Riak
CA (Consistent + Available) Funciona correctament, però si es produeix una partició de xarxa el sistema pot fallar SGBD tradicionals en un sol node (no distribuïts realment)

PACELC: l'extensió del CAP

El teorema PACELC (proposat per Daniel Abadi el 2012) afegeix que, fins i tot quan no hi ha una partició de xarxa, un sistema distribuït ha de triar entre latència (Latency) i consistència (Consistency). Això és rellevant per a sistemes com Cassandra, on la consistència és configurable per operació.


Sharding vs particionament vs replicació

Aquests tres termes solen confondre's. Aquí la distinció clara:

Tècnica Descripció Cada node té... Objectiu principal
Replicació Còpies de les mateixes dades en múltiples nodes Una còpia completa de les dades Alta disponibilitat, escalabilitat de lectura
Particionament Divisió d'una taula en segments, gestionats pel mateix motor dins d'un servidor Un conjunt de particions Gestió, rendiment, manteniment
Sharding Divisió de les dades entre múltiples servidors independents Un fragment (shard) de les dades Escalabilitat horitzontal, volums de dades massius

Estratègies de sharding horitzontal

Range sharding (particionament per rang)

Les files s'assignen a un shard en funció del valor d'una columna clau dins d'un rang.

  • Exemple: client_id 1–1.000.000 → Shard A; 1.000.001–2.000.000 → Shard B.
  • Avantatge: Les cerques per rang son eficients (un sol shard).
  • Desavantatge: Possible desequilibri si els valors no estan distribuïts uniformement (hot spot).

Hash sharding (particionament per dispersió)

S'aplica una funció de dispersió (hash) a la clau i el resultat determina el shard.

  • Exemple: shard = hash(client_id) % num_shards.
  • Avantatge: Distribució uniforme de les dades.
  • Desavantatge: Les cerques per rang requereixen consultar tots els shards.

Directory sharding (particionament per directori)

Un servei de lookup (directori) mapeja cada clau al shard corresponent.

  • Avantatge: Molt flexible, permet redistribuir dades.
  • Desavantatge: El directori es converteix en un punt de fallada i coll d'ampolla.

Particionament natiu als SGBD

El particionament natiu divideix una taula gran en particions gestionades pel propi motor com si fossin una sola taula. El motor pot dirigir les consultes únicament a les particions rellevants (partition pruning).

-- Particionament declaratiu (PostgreSQL 10+)

-- Particionament per RANG (per data)
CREATE TABLE comandes (
    id         BIGSERIAL,
    client_id  INT,
    total      DECIMAL(12,2),
    estat      VARCHAR(20),
    creat_a    TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (creat_a);

-- Crear les particions
CREATE TABLE comandes_2023 PARTITION OF comandes
    FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');
CREATE TABLE comandes_2024 PARTITION OF comandes
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE comandes_2025 PARTITION OF comandes
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

-- Índexs a les particions
CREATE INDEX ON comandes_2024 (client_id);
CREATE INDEX ON comandes_2025 (client_id);

-- Particionament per LLISTA (per país o categoria)
CREATE TABLE productes (
    id          BIGSERIAL,
    pais        VARCHAR(10) NOT NULL,
    nom         VARCHAR(100),
    preu        DECIMAL(10,2)
) PARTITION BY LIST (pais);

CREATE TABLE productes_es PARTITION OF productes FOR VALUES IN ('ES', 'CA');
CREATE TABLE productes_fr PARTITION OF productes FOR VALUES IN ('FR', 'BE');
CREATE TABLE productes_de PARTITION OF productes FOR VALUES IN ('DE', 'AT', 'CH');
CREATE TABLE productes_altres PARTITION OF productes DEFAULT;

-- Particionament per HASH (distribució uniforme)
CREATE TABLE logs (
    id        BIGSERIAL,
    usuari_id INT NOT NULL,
    accio     TEXT,
    creat_a   TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY HASH (usuari_id);

CREATE TABLE logs_p0 PARTITION OF logs FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE logs_p1 PARTITION OF logs FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE logs_p2 PARTITION OF logs FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE logs_p3 PARTITION OF logs FOR VALUES WITH (MODULUS 4, REMAINDER 3);

-- Consultar informació de particions
SELECT
    parent.relname AS taula_pare,
    child.relname AS particio,
    pg_get_expr(child.relpartbound, child.oid) AS condicio,
    pg_size_pretty(pg_relation_size(child.oid)) AS mida
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'comandes'
ORDER BY child.relname;
-- Particionament per RANG
CREATE TABLE comandes (
    id         BIGINT AUTO_INCREMENT,
    client_id  INT,
    total      DECIMAL(12,2),
    creat_a    DATETIME NOT NULL,
    PRIMARY KEY (id, creat_a)  -- La clau de particionament ha de ser part de la PK
)
PARTITION BY RANGE (YEAR(creat_a)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION pmax  VALUES LESS THAN MAXVALUE
);

-- Particionament per LLISTA
CREATE TABLE productes_intl (
    id     BIGINT AUTO_INCREMENT,
    regio  INT NOT NULL,
    nom    VARCHAR(100),
    PRIMARY KEY (id, regio)
)
PARTITION BY LIST (regio) (
    PARTITION p_europa     VALUES IN (1, 2, 3),
    PARTITION p_america    VALUES IN (4, 5, 6),
    PARTITION p_asia       VALUES IN (7, 8, 9)
);

-- Particionament per HASH (distribució uniforme)
CREATE TABLE logs (
    id        BIGINT AUTO_INCREMENT,
    usuari_id INT NOT NULL,
    accio     TEXT,
    PRIMARY KEY (id, usuari_id)
)
PARTITION BY HASH (usuari_id)
PARTITIONS 8;

-- Particionament per KEY (similar a HASH però usa la funció interna de MySQL)
CREATE TABLE sessions (
    id    CHAR(36) NOT NULL,
    dades TEXT,
    PRIMARY KEY (id)
)
PARTITION BY KEY (id)
PARTITIONS 4;

-- Gestió de particions
-- Afegir partició (per a RANGE)
ALTER TABLE comandes ADD PARTITION (PARTITION p2026 VALUES LESS THAN (2027));
-- Eliminar partició (i totes les seves dades!)
ALTER TABLE comandes DROP PARTITION p2022;
-- Veure informació de particions
SELECT table_name, partition_name, partition_expression,
       partition_description, table_rows
FROM information_schema.PARTITIONS
WHERE table_schema = DATABASE() AND table_name = 'comandes';
-- Particionament amb funcions i esquemes de particionament

-- Pas 1: Crear la funció de particionament (defineix els límits)
CREATE PARTITION FUNCTION pf_comandes_per_any (DATETIME2)
AS RANGE RIGHT FOR VALUES (
    '2022-01-01',
    '2023-01-01',
    '2024-01-01',
    '2025-01-01',
    '2026-01-01'
);
-- RANGE RIGHT: el valor del límit pertany a la partició de la dreta

-- Pas 2: Crear l'esquema de particionament (mapa funció → filegroups)
CREATE PARTITION SCHEME ps_comandes_per_any
AS PARTITION pf_comandes_per_any
TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY]);
-- En producció, cada partició pot anar a un filegroup separat (disc separat)

-- Pas 3: Crear la taula amb l'esquema de particionament
CREATE TABLE comandes (
    id         BIGINT IDENTITY(1,1),
    client_id  INT,
    total      DECIMAL(12,2),
    estat      NVARCHAR(20),
    creat_a    DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    CONSTRAINT PK_comandes PRIMARY KEY CLUSTERED (id, creat_a)
) ON ps_comandes_per_any (creat_a);

-- Consultar en quina partició cau un valor
SELECT $PARTITION.pf_comandes_per_any('2024-06-15') AS num_particio;

-- Veure distribució de files per partició
SELECT
    $PARTITION.pf_comandes_per_any(creat_a) AS num_particio,
    COUNT(*) AS num_files
FROM comandes
GROUP BY $PARTITION.pf_comandes_per_any(creat_a);

-- Switch de particions (moviment instantani de dades entre taules)
-- Útil per a arxivat: mou la partició antiga a una taula d'arxiu
ALTER TABLE comandes SWITCH PARTITION 1 TO comandes_arxiu PARTITION 1;
-- Particionament per RANG
CREATE TABLE comandes (
    id         NUMBER GENERATED ALWAYS AS IDENTITY,
    client_id  NUMBER,
    total      NUMBER(12,2),
    creat_a    DATE NOT NULL
)
PARTITION BY RANGE (creat_a) (
    PARTITION p2022 VALUES LESS THAN (DATE '2023-01-01'),
    PARTITION p2023 VALUES LESS THAN (DATE '2024-01-01'),
    PARTITION p2024 VALUES LESS THAN (DATE '2025-01-01'),
    PARTITION p2025 VALUES LESS THAN (DATE '2026-01-01'),
    PARTITION pmax  VALUES LESS THAN (MAXVALUE)
);

-- Particionament per LLISTA
CREATE TABLE productes (
    id    NUMBER GENERATED ALWAYS AS IDENTITY,
    pais  VARCHAR2(10) NOT NULL,
    nom   VARCHAR2(100)
)
PARTITION BY LIST (pais) (
    PARTITION p_es VALUES ('ES', 'CA'),
    PARTITION p_fr VALUES ('FR', 'BE'),
    PARTITION p_de VALUES ('DE', 'AT'),
    PARTITION p_altres VALUES (DEFAULT)
);

-- Particionament COMPOSIT (Range-Hash)
CREATE TABLE logs (
    id        NUMBER GENERATED ALWAYS AS IDENTITY,
    creat_a   DATE NOT NULL,
    usuari_id NUMBER NOT NULL,
    accio     VARCHAR2(100)
)
PARTITION BY RANGE (creat_a)
SUBPARTITION BY HASH (usuari_id)
SUBPARTITIONS 8 (
    PARTITION p2024 VALUES LESS THAN (DATE '2025-01-01'),
    PARTITION p2025 VALUES LESS THAN (DATE '2026-01-01'),
    PARTITION pmax  VALUES LESS THAN (MAXVALUE)
);

-- Veure informació de particions
SELECT table_name, partition_name, num_rows, blocks, last_analyzed
FROM user_tab_partitions
WHERE table_name = 'COMANDES'
ORDER BY partition_position;

Solucions de bases de dades distribuïdes

Citus (PostgreSQL per a sharding)

Citus és una extensió de PostgreSQL que converteix una instància en un clúster distribuït (distributed SQL). Les taules es distribueixen automàticament entre múltiples nodes worker, però es consulten com si fossin una sola taula.

-- Instal·lació de l'extensió
CREATE EXTENSION citus;

-- Crear una taula distribuïda (sharding per client_id)
SELECT create_distributed_table('comandes', 'client_id');

-- Crear una taula de referència (replicada a tots els workers)
SELECT create_reference_table('paises');

-- Les consultes s'executen igual que en PostgreSQL normal
SELECT client_id, COUNT(*), SUM(total)
FROM comandes
WHERE creat_a >= '2024-01-01'
GROUP BY client_id
ORDER BY SUM(total) DESC
LIMIT 10;

-- Veure la distribució de shards
SELECT * FROM citus_shards;

Vitess (MySQL per a sharding)

Vitess és la solució de sharding per a MySQL creada a YouTube (ara CNCF). Usa un proxy (vtgate) que enruta les consultes al shard correcte.

  • Usat per: YouTube, Slack, GitHub, Shopify.
  • Funcionalitats: sharding transparent, connexion pooling, online schema changes, replicació.
# keyspace.yaml — definir el schema de sharding
keyspace: commerce
shards:
  - name: "-80"
    primary: tablet-commerce-1
  - name: "80-"
    primary: tablet-commerce-2

CockroachDB

CockroachDB és un SGBD SQL distribuït natiu (no és MySQL ni PostgreSQL, però és compatible amb el protocol PostgreSQL). Ofereix:

  • Distribució automàtica de dades entre nodes.
  • Transaccions distribuïdes ACID.
  • Failover automàtic sense pèrdua de dades.
-- CockroachDB: SQL compatible amb PostgreSQL
CREATE TABLE comandes (
    id         UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    client_id  INT,
    total      DECIMAL(12,2),
    creat_a    TIMESTAMPTZ DEFAULT now()
);

-- Les dades es distribuïxen automàticament
-- Veure la distribució de ranges
SHOW RANGES FROM TABLE comandes;

YugabyteDB

YugabyteDB és un altre SGBD distribuït que ofereix compatibilitat tant amb PostgreSQL (YSQL) com amb Cassandra (YCQL). Ideal per a migrar aplicacions PostgreSQL a un entorn distribuït.


Foreign Data Wrappers (PostgreSQL)

Els FDW (Foreign Data Wrappers) permeten a PostgreSQL consultar fonts de dades externes com si fossin taules locals. Útil per a integrar múltiples bases de dades sense moure les dades.

-- Instal·lar el FDW per a PostgreSQL remot
CREATE EXTENSION postgres_fdw;

-- Definir el servidor remot
CREATE SERVER servidor_remot
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '192.168.1.200', port '5432', dbname 'bd_remota');

-- Mapear l'usuari local a l'usuari remot
CREATE USER MAPPING FOR usuari_local
SERVER servidor_remot
OPTIONS (user 'usuari_remot', password 'contrasenya');

-- Crear una taula forana que apunta a una taula remota
CREATE FOREIGN TABLE clients_remots (
    id    INT,
    nom   VARCHAR(100),
    email VARCHAR(120)
) SERVER servidor_remot
OPTIONS (schema_name 'public', table_name 'clients');

-- Consultar la taula forana com si fos local
SELECT c.nom, COUNT(co.id) AS num_comandes
FROM clients_remots c            -- taula remota
JOIN comandes co ON c.id = co.client_id  -- taula local
GROUP BY c.nom;

-- Importar totes les taules d'un esquema remot
IMPORT FOREIGN SCHEMA public
FROM SERVER servidor_remot
INTO esquema_local;

Miniactivitat — AC0608

Objectiu: Experimentar amb el particionament natiu de PostgreSQL i analitzar el partition pruning.

Passos:

  1. Crea una taula logs_particionada particionada per RANGE (per mes):

    CREATE TABLE logs_particionada (
        id        BIGSERIAL,
        creat_a   TIMESTAMPTZ NOT NULL,
        usuari_id INT,
        accio     TEXT
    ) PARTITION BY RANGE (creat_a);
    
    -- Crear particions per a cada mes de 2024
    DO $$
    DECLARE
        any_i INT := 2024;
        mes_i INT;
        data_inici DATE;
        data_fi DATE;
    BEGIN
        FOR mes_i IN 1..12 LOOP
            data_inici := make_date(any_i, mes_i, 1);
            data_fi := data_inici + INTERVAL '1 month';
            EXECUTE format(
                'CREATE TABLE logs_%s_%s PARTITION OF logs_particionada FOR VALUES FROM (%L) TO (%L)',
                any_i, lpad(mes_i::TEXT, 2, '0'), data_inici, data_fi
            );
        END LOOP;
    END;
    $$;
    

  2. Insereix 1.000.000 de registres distribuïts al llarg de 2024.

  3. Executa EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM logs_particionada WHERE creat_a BETWEEN '2024-06-01' AND '2024-06-30';

  4. Verifica que el pla d'execució accedeix únicament a la partició de juny (partition pruning).

  5. Compara el rendiment d'aquesta consulta contra la mateixa consulta en una taula NO particionada amb el mateix volum de dades.

  6. Prova d'afegir una partició per al 2025: CREATE TABLE logs_2025_01 PARTITION OF logs_particionada FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

Lliura: Comparació dels plans d'execució (amb/sense particionament) i conclusions sobre l'impacte en rendiment.