Аргументы в пользу вертикального масштабирования

Если вы читали какие-либо статьи или книги по проектированию систем, то, вероятно, знаете, что такое вертикальное и горизонтальное масштабирование и каковы преимущества горизонтального масштабирования. Прежде чем я объясню, как правильно настроить горизонтальное масштабирование в Postgres, позвольте мне привести пример, когда этого делать не следует.

  1. Простота: База данных на одном узле означает, что вы можете запустить свою базу данных «из коробки». Хотя я рекомендую использовать PGTune для быстрой настройки или посетить postconf для полного описания.
  2. Более простое резервное копирование и восстановление: Нет необходимости думать о состоянии реплик при создании или применении резервных копий.
  3. Отсутствие сетевой нагрузки, особенно при операциях с интенсивной записью.
  4. Временное решение: Если вам нужно решение прямо сейчас, это то, что нужно.

Предварительная подготовка

Необходимо установить следющий тулинг

Для следования этому руководству вам потребуется базовое понимание Kubernetes, CRD и Helm. Ничего сложного, достаточно краткого обзора.

Репликация

Репликация означает хранение нескольких копий данных на нескольких серверах соединенных по сети. Вот почему вам может понадобиться это сделать:

Diagram depicting a database architecture with a leader and two followers. The leader handles create, delete, and update queries, while followers handle read queries. Data synchronization is done through WAL sync. User queries are directed through a pg-pool component.

Здесь pg-pool выступает в роли балансировщика нагрузки, равномерно распределяя запросы на чтение между подписчиками и запросы на изменение данных — к лидеру. Обратите внимание, что лидер периодически синхронизирует свой WAL со своими подписчиками.

Ставим StackGres и включение балансировщика

PLAINTEXT
minikube addons enable metallb
minikube tunnel
Нажмите, чтобы развернуть и увидеть больше
PLAINTEXT
helm install stackgres-operator stackgres-charts/stackgres-operator\               --namespace stackgres-operator \
--create-namespace
Нажмите, чтобы развернуть и увидеть больше

Задаем CRD для реплицирующего кластера

PLAINTEXT
# replication.yaml

apiVersion: stackgres.io/v1
kind: SGCluster
metadata:
  name: cluster

spec:
  instances: 3 # 1 primary + 2 replicas

  postgres:
    version: "15"

  pods:
    persistentVolume:
      size: "1Gi"

  profile: development

  postgresServices:
    primary:
      type: LoadBalancer
    replicas:
      type: LoadBalancer
Нажмите, чтобы развернуть и увидеть больше

Применяем CRD

PLAINTEXT
kubectl apply -f ./replication.yaml
kubectl get pods -w
Нажмите, чтобы развернуть и увидеть больше

Получаем учетные данные

PLAINTEXT
PG_PASSWORD=$(kubectl -n default get secret cluster --template '{{ printf "%s" (index .data "superuser-password" | base64decode) }}')
echo "The superuser password is: $PG_PASSWORD"
Нажмите, чтобы развернуть и увидеть больше

Смотрим, что получилось

PLAINTEXT
kubectl exec -it cluster-0  -c patroni -- patronictl list
Нажмите, чтобы развернуть и увидеть больше

Выключаем лидера

PLAINTEXT
kubectl exec -it cluster-0  -c patroni -- patronictl list
Нажмите, чтобы развернуть и увидеть больше

Смотрим, кто теперь главный

Patroni  уже давно должен был избрать нового лидера.

PLAINTEXT
kubectl exec -it cluster-1 -c patroni -- patronictl list
Нажмите, чтобы развернуть и увидеть больше

Сообщайте что-либо только основным адресатам

PLAINTEXT
PRIMARY=$(kubectl exec -it cluster-1 -c patroni -- patronictl list | grep Leader | awk '{print $2}')
kubectl exec -it $PRIMARY -c patroni -- psql -U postgres -c "CREATE TABLE replication_test_table (id SERIAL PRIMARY KEY, data TEXT);"
kubectl exec -it $PRIMARY -c patroni -- psql -U postgres -c "INSERT INTO replication_test_table (data) VALUES ('Spread the word about our lord savior PostgreSQL!');"
Нажмите, чтобы развернуть и увидеть больше

Лидер оповещает свои реплики

PLAINTEXT
kubectl exec -it cluster-0 -c patroni -- psql -U postgres -c "SELECT * FROM replication_test_table;"
kubectl exec -it cluster-1 -c patroni -- psql -U postgres -c "SELECT * FROM replication_test_table;"
kubectl exec -it cluster-2 -c patroni -- psql -U postgres -c "SELECT * FROM replication_test_table;"
Нажмите, чтобы развернуть и увидеть больше

Как видите, информация распространилась очень быстро. Это стало возможным благодаря тому, что StackGres использует Patroni для координации всего процесса копирования.

Парцирование (Разделение)

Разделение данных (в нашем случае таблицы) на более мелкие, управляемые части. Это делается в рамках одного экземпляра базы данных. Postgres поддерживает это по умолчанию. Разделение определяется на уровне определения данных, и наличие нескольких реплик делает его высокодоступным. Лучше всего подходит для временных рядов, журналов или сегментации по регионам.

Типы разделения

  1. Разделение по диапазонам — данные разделяются на основе диапазонов значений (например, диапазонов дат).

  2. Разделение по спискам — разделение на основе списка значений (например, регионов или категорий).

  3. Хэш-разделение — данные распределяются с использованием хэш-функции (например, MOD(user_id, 4)).

Приведенный ниже код создает таблицу orders и наследует от нее три таблицы, используя иерархическое разделение по диапазонам, спискам и хешу. Таблица orders разделяется по годам, год далее разделяется на регионы, а регион, наконец, разделяется по хешу.

Image description

Обратите внимание, что только разделы, созданные на основе хеширования, гарантируют, что все разделы будут одинакового размера.

Настройка StackGres и включение балансировщика

PLAINTEXT
helm install stackgres-operator stackgres-charts/stackgres-operator \
    --namespace stackgres-operator \
    --create-namespace

minikube addons enable metallb
minikube tunnel
Нажмите, чтобы развернуть и увидеть больше

Получаем учетные данные

PLAINTEXT
PG_PASSWORD=$(kubectl -n default get secret cluster --template '{{ printf "%s" (index .data "superuser-password" | base64decode) }}')
echo "The superuser password is: $PG_PASSWORD"
Нажмите, чтобы развернуть и увидеть больше

Теперь ваша база данных должна быть доступна по адресу: postgresql://postgres:<password>:localhost:5432

Теперь откройте редактор SQL, например pgAdmin , и выполните следующую команду.

PLAINTEXT
-- Parent table
CREATE TABLE orders (
    order_id    INT,
    customer_id INT,
    order_date  DATE,
    region      TEXT,
    amount      INT,
    PRIMARY KEY (order_id, order_date, region, customer_id)
) PARTITION BY RANGE (order_date);


-- Range: Year 2024
CREATE TABLE orders_2024 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')
    PARTITION BY LIST (region);

-- Range: Year 2025
CREATE TABLE orders_2025 PARTITION OF orders
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01')
    PARTITION BY LIST (region);

-- 2024 - US region
CREATE TABLE orders_2024_us PARTITION OF orders_2024
    FOR VALUES IN ('US')
    PARTITION BY HASH (customer_id);

-- 2024 - EU region
CREATE TABLE orders_2024_eu PARTITION OF orders_2024
    FOR VALUES IN ('EU')
    PARTITION BY HASH (customer_id);

-- 2024 - US - Hash partitions
CREATE TABLE orders_2024_us_0 PARTITION OF orders_2024_us FOR VALUES WITH (MODULUS 2, REMAINDER 0);
CREATE TABLE orders_2024_us_1 PARTITION OF orders_2024_us FOR VALUES WITH (MODULUS 2, REMAINDER 1);

-- 2024 - EU - Hash partitions
CREATE TABLE orders_2024_eu_0 PARTITION OF orders_2024_eu FOR VALUES WITH (MODULUS 2, REMAINDER 0);
CREATE TABLE orders_2024_eu_1 PARTITION OF orders_2024_eu FOR VALUES WITH (MODULUS 2, REMAINDER 1);
Нажмите, чтобы развернуть и увидеть больше

Наполняем синтетическими данных

PLAINTEXT
-- Generate 1000 random orders
INSERT INTO orders (order_id, customer_id, order_date, region, amount)
SELECT 
    -- Generate order IDs between 1000 and 9999
    1000 + floor(random() * 9000)::int AS order_id,
    -- Generate customer IDs between 1000 and 9999
    1000 + floor(random() * 9000)::int AS customer_id,    
    -- Generate dates in 2024 (to fit the 2024 partition)
    DATE '2024-01-01' + (floor(random() * 366)::int * INTERVAL '1 day') AS order_date,    
    -- Randomly select region
    (ARRAY['US', 'EU'])[1 + floor(random() * 2)::int] AS region,    
    -- Generate random amounts between 10 and 1000
    10 + floor(random() * 990)::int AS amount
FROM 
    generate_series(1, 1000) AS i;            -- 1k rows
Нажмите, чтобы развернуть и увидеть больше
PLAINTEXT
SELECT * FROM orders
WHERE order_date = '2024-06-10'
  AND region = 'US'
Нажмите, чтобы развернуть и увидеть больше

Для запроса информации о заказах вам не нужно знать номер раздела.

Шардирование с репликацией

Шардинг разделяет большую базу данных на небольшие части, называемые шардами. Каждый шард затем распределяется между несколькими серверами чтобы наша база данных могла продолжать функционировать, даже если мы потеряем несколько машин. Маршрутизация запросов к нужному серверу осуществляется координатором, и, как и в примере с репликацией, балансировка нагрузки внутри шарда будет выполняться с помощью pg-pool.

Типы шардинга

  1. Построчный подход: Представьте, что вы делите очень толстую книгу на множество томов (фрагментов) и создаете новые тома только для отслеживания содержания (координатора). Представьте таблицу, схема которой проста, но количество строк и операций записи резко возросло. При таком методе операции чтения/записи для каждого фрагмента могут масштабироваться по мере необходимости.

  2. Подход на основе схемы: Как и в прошлый раз, мы снова делим книгу, но на этот раз берем несколько связанных глав и превращаем их в книгу по подтемам. Представьте, как очень толстый учебник по физике можно разделить на оптику, термодинамику и квантовую механику. Представьте таблицу с большим количеством столбцов, но вам не нужны все столбцы каждый раз при выполнении запроса. Поэтому вы делите таблицу на фрагменты таким образом, чтобы связанные столбцы располагались вместе.

Image description

Обратите внимание на отказоустойчивость этой архитектуры: у нас есть множество реплик не только для шардов, но и для координатора. Пока у нас есть как минимум 3 машины для работы нашего шардированного кластера, отказ одной машины не приведет к остановке нашей базы данных.

Настройка StackGres и включение балансировщика

PLAINTEXT
helm install stackgres-operator stackgres-charts/stackgres-operator \
    --namespace stackgres-operator \
    --create-namespace

minikube addons enable metallb
minikube tunnel
Нажмите, чтобы развернуть и увидеть больше

Задаем CRD для шардированного кластера

PLAINTEXT
# shard.yaml
apiVersion: stackgres.io/v1alpha1
kind: SGShardedCluster
metadata:
  name: cluster
spec:
  type: citus
  database: mydatabase
  postgres:
    version: 'latest'
  coordinator:
    instances: 2 # Number of coordinator instances
    pods:
      persistentVolume:
        size: '1Gi'
  shards:
    clusters: 3 # Number of shards
    instancesPerCluster: 3 # 1 primary and 2 replicas
    pods:
      persistentVolume:
        size: '1Gi'
  postgresServices:
    coordinator:
      primary:
        type: LoadBalancer

  profile: development
Нажмите, чтобы развернуть и увидеть больше

Применяем Citus CRD

PLAINTEXT
kubectl apply -f ./shard.yaml
Нажмите, чтобы развернуть и увидеть больше

Получаем учетные данные

PLAINTEXT
PG_PASSWORD=$(kubectl -n default get secret cluster --template '{{ printf "%s" (index .data "superuser-password" | base64decode) }}')
echo "The superuser password is: $PG_PASSWORD"
Нажмите, чтобы развернуть и увидеть больше

Ваша база данных теперь доступно по адресу postgresql://postgres:<password>:localhost:5432

Теперь откройте приложение, например, редактор SQL, такое как pgAdmin, и выполните следующую команду:

Создайте некоторую распределенную таблицу

PLAINTEXT
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    name TEXT
);
SELECT create_distributed_table('users', 'id');

CREATE TABLE orders (
    id BIGINT,
    user_id BIGINT,
    product_id BIGINT,
    amount INTEGER,
    PRIMARY KEY (user_id, id)
);
SELECT create_distributed_table('orders', 'user_id');

CREATE TABLE products (
    id BIGINT PRIMARY KEY,
    name TEXT,
    price NUMERIC
);
SELECT create_reference_table('products');
Нажмите, чтобы развернуть и увидеть больше

Вставляем некоторые данные

PLAINTEXT
INSERT INTO users (id, name) VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Charlie');

INSERT INTO orders (id, user_id, product_id, amount) VALUES
(1, 1, 1, 2),
(2, 1, 2, 3),
(3, 2, 1, 1),
(4, 3, 3, 5);
INSERT INTO products (id, name, price) VALUES
(1, 'Product A', 10.00),
(2, 'Product B', 20.00),
(3, 'Product C', 30.00);
Нажмите, чтобы развернуть и увидеть больше

Смотрим как распределились шарды

PLAINTEXT
SELECT * FROM citus_shards
WHERE table_name = 'orders'::regclass;
Нажмите, чтобы развернуть и увидеть больше

Определите, какой узел является хостом какого сегмента.

PLAINTEXT
SELECT
  s.shardid,
  n.nodename,
  n.nodeport
FROM pg_dist_shard s
JOIN pg_dist_shard_placement p ON s.shardid = p.shardid
JOIN pg_dist_node n ON p.nodename = n.nodename
WHERE s.logicalrelid = 'orders'::regclass;
Нажмите, чтобы развернуть и увидеть больше

Найдите строку, содержащую определенный текст.

PLAINTEXT
SELECT get_shard_id_for_distribution_column('orders', 1);
Нажмите, чтобы развернуть и увидеть больше

Присоединяйтесь к распределенной системе (расположенной в одном месте)

PLAINTEXT
SELECT
    o.id AS order_id,
    u.name AS customer,
    o.amount
FROM orders o
JOIN users u ON o.user_id = u.id;
Нажмите, чтобы развернуть и увидеть больше

Это эффективно, потому что заказы и пользователи сегментируются с использованием одного и того же ключа (user_id и id).

Присоединиться к распределенной ссылке

PLAINTEXT
SELECT
    o.id AS order_id,
    u.name AS customer,
    p.name AS product,
    o.amount
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;
Нажмите, чтобы развернуть и увидеть больше

Это хорошо работает, потому что данные о продуктах реплицируются на всех узлах.

Авторские права

Автор: Vasiliy Fakunin

Ссылка: https://notes.melancholic.tech/posts/%D0%BC%D0%B0%D1%81%D1%88%D1%82%D0%B0%D0%B1%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-postgresql-%D0%B2-kubernetes/

Лицензия: CC BY-NC-SA 4.0

Использование материалов блога разрешается при условии: указания авторства/источника, некоммерческого использования и сохранения лицензии.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша